diff --git a/.craft.yml b/.craft.yml
index 331d065a2ff9..e23d766b103a 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -120,6 +120,9 @@ targets:
- name: npm
id: '@sentry/tanstackstart-react'
includeNames: /^sentry-tanstackstart-react-\d.*\.tgz$/
+ - name: npm
+ id: '@sentry/vinext'
+ includeNames: /^sentry-vinext-\d.*\.tgz$/
- name: npm
id: '@sentry/gatsby'
includeNames: /^sentry-gatsby-\d.*\.tgz$/
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/.npmrc b/dev-packages/e2e-tests/test-applications/vinext-app/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/vinext-app/instrumentation-client.ts
new file mode 100644
index 000000000000..aff30b427ba6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/instrumentation-client.ts
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/vinext';
+
+Sentry.init({
+ environment: 'qa',
+ dsn: process.env.E2E_TEST_DSN,
+ tunnel: 'http://localhost:3031/',
+ tracesSampleRate: 1,
+ transportOptions: {
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/instrumentation.ts b/dev-packages/e2e-tests/test-applications/vinext-app/instrumentation.ts
new file mode 100644
index 000000000000..1d9388f6cedc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/instrumentation.ts
@@ -0,0 +1,15 @@
+import * as Sentry from '@sentry/vinext';
+
+export async function register() {
+ Sentry.init({
+ environment: 'qa',
+ dsn: process.env.E2E_TEST_DSN,
+ tunnel: 'http://localhost:3031/',
+ tracesSampleRate: 1,
+ transportOptions: {
+ bufferSize: 1000,
+ },
+ });
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/package.json b/dev-packages/e2e-tests/test-applications/vinext-app/package.json
new file mode 100644
index 000000000000..0b5cba962836
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "vinext-app-e2e-test",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vinext dev",
+ "build": "vinext build",
+ "start": "vinext start --port 3030",
+ "test": "playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/vinext": "latest || *",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@vitejs/plugin-rsc": "^0.5.20",
+ "typescript": "~5.5.0",
+ "vinext": "^0.0.19",
+ "vite": "^7.3.1"
+ },
+ "volta": {
+ "extends": "../../package.json",
+ "node": "22.18.0"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/vinext-app/playwright.config.mjs
new file mode 100644
index 000000000000..3d3ab7d8df02
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/src/app/api/test/route.ts b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/api/test/route.ts
new file mode 100644
index 000000000000..331e7c9a7bfd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/api/test/route.ts
@@ -0,0 +1,9 @@
+export async function GET() {
+ return new Response(JSON.stringify({ message: 'Hello from vinext API!' }), {
+ headers: { 'content-type': 'application/json' },
+ });
+}
+
+export async function POST() {
+ throw new Error('API Route Error');
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/src/app/error-page/ErrorButton.tsx b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/error-page/ErrorButton.tsx
new file mode 100644
index 000000000000..205017205f57
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/error-page/ErrorButton.tsx
@@ -0,0 +1,14 @@
+'use client';
+
+export default function ErrorButton() {
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/src/app/error-page/page.tsx b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/error-page/page.tsx
new file mode 100644
index 000000000000..2a836e9dde4e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/error-page/page.tsx
@@ -0,0 +1,10 @@
+import ErrorButton from './ErrorButton';
+
+export default function ErrorPage() {
+ return (
+
+
Error Test Page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/src/app/layout.tsx b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/layout.tsx
new file mode 100644
index 000000000000..f3ef34cd8b91
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/layout.tsx
@@ -0,0 +1,7 @@
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/src/app/page.tsx b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/page.tsx
new file mode 100644
index 000000000000..52bd6ada7b57
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/src/app/page.tsx
@@ -0,0 +1,8 @@
+export default function Home() {
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/vinext-app/start-event-proxy.mjs
new file mode 100644
index 000000000000..a7afc0e656c0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'vinext-app',
+});
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vinext-app/tests/errors.test.ts
new file mode 100644
index 000000000000..b8f1aec489ec
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/tests/errors.test.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('captures a client-side error', async ({ page }) => {
+ const errorEventPromise = waitForError('vinext-app', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'E2E Test Error';
+ });
+
+ await page.goto('/error-page');
+ await page.locator('#error-button').click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values?.[0]).toMatchObject({
+ type: 'Error',
+ value: 'E2E Test Error',
+ });
+});
+
+test('captures a server-side API route error', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('vinext-app', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'API Route Error';
+ });
+
+ await fetch(`${baseURL}/api/test`, { method: 'POST' });
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values?.[0]).toMatchObject({
+ type: 'Error',
+ value: 'API Route Error',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/vinext-app/tests/transactions.test.ts
new file mode 100644
index 000000000000..8d1a05c0f959
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/tests/transactions.test.ts
@@ -0,0 +1,29 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('creates a pageload transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('vinext-app', transactionEvent => {
+ return transactionEvent?.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.contexts?.trace).toMatchObject({
+ op: 'pageload',
+ });
+});
+
+test('creates a transaction for API routes', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction('vinext-app', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /api/test';
+ });
+
+ await fetch(`${baseURL}/api/test`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.transaction).toBe('GET /api/test');
+ expect(transaction.contexts?.trace?.op).toBe('http.server');
+});
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/vinext-app/tsconfig.json
new file mode 100644
index 000000000000..8420aab9d526
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*", "*.ts", "*.mts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/vinext-app/vite.config.ts b/dev-packages/e2e-tests/test-applications/vinext-app/vite.config.ts
new file mode 100644
index 000000000000..46ca32d96a43
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vinext-app/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import vinext from 'vinext';
+import { sentryVinext } from '@sentry/vinext/vite';
+
+export default defineConfig({
+ plugins: [vinext(), sentryVinext()],
+});
diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml
index 6e57ee2ea812..42354ed2b5b6 100644
--- a/dev-packages/e2e-tests/verdaccio-config/config.yaml
+++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml
@@ -224,6 +224,12 @@ packages:
unpublish: $all
# proxy: npmjs # Don't proxy for E2E tests!
+ '@sentry/vinext':
+ access: $all
+ publish: $all
+ unpublish: $all
+ # proxy: npmjs # Don't proxy for E2E tests!
+
'@sentry/wasm':
access: $all
publish: $all
diff --git a/package.json b/package.json
index 91bc549e4527..4683c3bb7e59 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
"packages/types",
"packages/typescript",
"packages/vercel-edge",
+ "packages/vinext",
"packages/vue",
"packages/wasm",
"dev-packages/browser-integration-tests",
diff --git a/packages/vinext/.eslintrc.js b/packages/vinext/.eslintrc.js
new file mode 100644
index 000000000000..d567b12530d0
--- /dev/null
+++ b/packages/vinext/.eslintrc.js
@@ -0,0 +1,22 @@
+module.exports = {
+ env: {
+ browser: true,
+ node: true,
+ },
+ overrides: [
+ {
+ files: ['vite.config.ts'],
+ parserOptions: {
+ project: ['tsconfig.test.json'],
+ },
+ },
+ {
+ files: ['src/vite/**', 'src/server/**'],
+ rules: {
+ '@sentry-internal/sdk/no-optional-chaining': 'off',
+ '@sentry-internal/sdk/no-nullish-coalescing': 'off',
+ },
+ },
+ ],
+ extends: ['../../.eslintrc.js'],
+};
diff --git a/packages/vinext/LICENSE b/packages/vinext/LICENSE
new file mode 100644
index 000000000000..b3c4b18a6317
--- /dev/null
+++ b/packages/vinext/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Functional Software, Inc. dba Sentry
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/vinext/README.md b/packages/vinext/README.md
new file mode 100644
index 000000000000..fc2e548afabc
--- /dev/null
+++ b/packages/vinext/README.md
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+# Official Sentry SDK for Vinext (Experimental)
+
+[](https://www.npmjs.com/package/@sentry/vinext)
+[](https://www.npmjs.com/package/@sentry/vinext)
+[](https://www.npmjs.com/package/@sentry/vinext)
+
+> **Warning:** This SDK is experimental and under active development. Breaking changes may occur.
+
+## General
+
+This package is a wrapper around `@sentry/node` for the server and `@sentry/react` for the client side, with added
+functionality related to Vinext.
diff --git a/packages/vinext/package.json b/packages/vinext/package.json
new file mode 100644
index 000000000000..b5ea9c739564
--- /dev/null
+++ b/packages/vinext/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "@sentry/vinext",
+ "version": "10.42.0",
+ "description": "Official Sentry SDK for vinext",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vinext",
+ "author": "Sentry",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "files": [
+ "/build"
+ ],
+ "main": "build/cjs/index.server.js",
+ "module": "build/esm/index.server.js",
+ "browser": "build/esm/index.client.js",
+ "types": "build/types/index.types.d.ts",
+ "exports": {
+ "./package.json": "./package.json",
+ ".": {
+ "types": "./build/types/index.types.d.ts",
+ "worker": {
+ "import": "./build/esm/index.worker.js",
+ "require": "./build/cjs/index.worker.js"
+ },
+ "workerd": {
+ "import": "./build/esm/index.worker.js",
+ "require": "./build/cjs/index.worker.js"
+ },
+ "browser": {
+ "import": "./build/esm/index.client.js",
+ "require": "./build/cjs/index.client.js"
+ },
+ "node": {
+ "import": "./build/esm/index.server.js",
+ "require": "./build/cjs/index.server.js"
+ }
+ },
+ "./vite": {
+ "types": "./build/types/vite/index.d.ts",
+ "import": "./build/esm/vite/index.js",
+ "require": "./build/cjs/vite/index.js"
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@sentry/cloudflare": "10.42.0",
+ "@sentry/core": "10.42.0",
+ "@sentry/node": "10.42.0",
+ "@sentry/react": "10.42.0",
+ "@sentry/vite-plugin": "^5.1.0"
+ },
+ "devDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "vite": "^5.4.11"
+ },
+ "peerDependencies": {
+ "vinext": ">=0.1.0",
+ "react": "^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vinext": {
+ "optional": true
+ }
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types",
+ "build:dev": "yarn build",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:types": "tsc -p tsconfig.types.json",
+ "build:watch": "run-p build:transpile:watch",
+ "build:dev:watch": "yarn build:watch",
+ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
+ "build:tarball": "npm pack",
+ "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.worker.ts && madge --circular src/index.types.ts",
+ "clean": "rimraf build coverage sentry-vinext-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module",
+ "test": "yarn test:unit",
+ "test:unit": "vitest run",
+ "test:watch": "vitest --watch",
+ "yalc:publish": "yalc publish --push --sig"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false
+}
diff --git a/packages/vinext/rollup.npm.config.mjs b/packages/vinext/rollup.npm.config.mjs
new file mode 100644
index 000000000000..453baf4b5567
--- /dev/null
+++ b/packages/vinext/rollup.npm.config.mjs
@@ -0,0 +1,22 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+
+export default [
+ ...makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ entrypoints: [
+ 'src/index.client.ts',
+ 'src/index.server.ts',
+ 'src/index.worker.ts',
+ 'src/client/index.ts',
+ 'src/server/index.ts',
+ 'src/vite/index.ts',
+ ],
+ packageSpecificConfig: {
+ external: ['vite', 'vinext'],
+ output: {
+ dynamicImportInCjs: true,
+ },
+ },
+ }),
+ ),
+];
diff --git a/packages/vinext/src/client/browserTracingIntegration.ts b/packages/vinext/src/client/browserTracingIntegration.ts
new file mode 100644
index 000000000000..c06f35332340
--- /dev/null
+++ b/packages/vinext/src/client/browserTracingIntegration.ts
@@ -0,0 +1,108 @@
+import type { Client, Integration } from '@sentry/core';
+import {
+ GLOBAL_OBJ,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '@sentry/core';
+import {
+ browserTracingIntegration as originalBrowserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
+} from '@sentry/react';
+
+const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
+
+/**
+ * A custom browser tracing integration for vinext.
+ *
+ * This wraps the standard browser tracing integration with vinext-specific
+ * page load and navigation instrumentation for both the Pages Router and App Router.
+ */
+export function browserTracingIntegration(
+ options: Parameters[0] = {},
+): Integration {
+ const browserTracingIntegrationInstance = originalBrowserTracingIntegration({
+ ...options,
+ instrumentNavigation: false,
+ instrumentPageLoad: false,
+ });
+
+ const { instrumentPageLoad = true, instrumentNavigation = true } = options;
+
+ return {
+ ...browserTracingIntegrationInstance,
+ afterAllSetup(client) {
+ browserTracingIntegrationInstance.afterAllSetup(client);
+
+ if (instrumentPageLoad) {
+ instrumentVinextPageLoad(client);
+ }
+
+ if (instrumentNavigation) {
+ instrumentVinextNavigation(client);
+ }
+ },
+ };
+}
+
+function instrumentVinextPageLoad(client: Client): void {
+ const route = getRouteFromWindow();
+ startBrowserTracingPageLoadSpan(client, {
+ name: route || WINDOW.location.pathname,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vinext',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url',
+ },
+ });
+}
+
+function instrumentVinextNavigation(client: Client): void {
+ const originalPushState = history.pushState.bind(history);
+ const originalReplaceState = history.replaceState.bind(history);
+
+ history.pushState = function (...args) {
+ const result = originalPushState(...args);
+ startNavigationSpan(client, 'pushState');
+ return result;
+ };
+
+ history.replaceState = function (...args) {
+ const result = originalReplaceState(...args);
+ startNavigationSpan(client, 'replaceState');
+ return result;
+ };
+
+ WINDOW.addEventListener('popstate', () => {
+ startNavigationSpan(client, 'popstate');
+ });
+}
+
+function startNavigationSpan(client: Client, trigger: string): void {
+ startBrowserTracingNavigationSpan(client, {
+ name: WINDOW.location.pathname,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vinext',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ 'vinext.navigation.trigger': trigger,
+ },
+ });
+}
+
+/**
+ * Attempts to extract a parameterized route from the vinext __NEXT_DATA__ object
+ * (which vinext injects for Pages Router compatibility).
+ */
+function getRouteFromWindow(): string | undefined {
+ try {
+ const nextData = (GLOBAL_OBJ as unknown as Record).__NEXT_DATA__ as { page?: string } | undefined;
+ if (nextData?.page && nextData.page !== '/') {
+ return nextData.page;
+ }
+ } catch {
+ // noop
+ }
+ return undefined;
+}
diff --git a/packages/vinext/src/client/index.ts b/packages/vinext/src/client/index.ts
new file mode 100644
index 000000000000..ac3cfbc488ba
--- /dev/null
+++ b/packages/vinext/src/client/index.ts
@@ -0,0 +1,62 @@
+/* eslint-disable import/export */
+import type { Client, EventProcessor, Integration } from '@sentry/core';
+import { addEventProcessor, applySdkMetadata } from '@sentry/core';
+import type { BrowserOptions } from '@sentry/react';
+import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react';
+import { browserTracingIntegration } from './browserTracingIntegration';
+
+export type { BrowserOptions };
+export { browserTracingIntegration } from './browserTracingIntegration';
+
+// Treeshakable guard to remove all code related to tracing
+declare const __SENTRY_TRACING__: boolean;
+
+/** Inits the Sentry vinext SDK on the browser. */
+export function init(options: BrowserOptions): Client | undefined {
+ const opts = {
+ environment: options.environment || process.env.NODE_ENV,
+ defaultIntegrations: getDefaultIntegrations(options),
+ ...options,
+ } satisfies BrowserOptions;
+
+ applySdkMetadata(opts, 'vinext', ['vinext', 'react']);
+
+ const client = reactInit(opts);
+
+ const filter404Transactions: EventProcessor = event =>
+ event.type === 'transaction' && event.transaction === '/404' ? null : event;
+ filter404Transactions.id = 'VinextClient404Filter';
+ addEventProcessor(filter404Transactions);
+
+ const filterRedirectErrors: EventProcessor = (_event, hint) => {
+ const originalException = hint?.originalException;
+ if (
+ typeof originalException === 'object' &&
+ originalException !== null &&
+ 'digest' in originalException &&
+ typeof (originalException as { digest: unknown }).digest === 'string' &&
+ (originalException as { digest: string }).digest.startsWith('NEXT_REDIRECT')
+ ) {
+ return null;
+ }
+ return _event;
+ };
+ filterRedirectErrors.id = 'VinextRedirectErrorFilter';
+ addEventProcessor(filterRedirectErrors);
+
+ return client;
+}
+
+function getDefaultIntegrations(options: BrowserOptions): Integration[] {
+ const customDefaultIntegrations = getReactDefaultIntegrations(options);
+
+ if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
+ customDefaultIntegrations.push(browserTracingIntegration());
+ }
+
+ return customDefaultIntegrations;
+}
+
+export * from '../common';
+
+export * from '@sentry/react';
diff --git a/packages/vinext/src/common/debug-build.ts b/packages/vinext/src/common/debug-build.ts
new file mode 100644
index 000000000000..60aa50940582
--- /dev/null
+++ b/packages/vinext/src/common/debug-build.ts
@@ -0,0 +1,8 @@
+declare const __DEBUG_BUILD__: boolean;
+
+/**
+ * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
+ *
+ * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
+ */
+export const DEBUG_BUILD = __DEBUG_BUILD__;
diff --git a/packages/vinext/src/common/index.ts b/packages/vinext/src/common/index.ts
new file mode 100644
index 000000000000..301ea7af422b
--- /dev/null
+++ b/packages/vinext/src/common/index.ts
@@ -0,0 +1,7 @@
+export type { ErrorContext, RequestInfo } from './types';
+export {
+ wrapRouteHandlerWithSentry,
+ wrapServerComponentWithSentry,
+ wrapMiddlewareWithSentry,
+ wrapApiHandlerWithSentry,
+} from './wrappers';
diff --git a/packages/vinext/src/common/types.ts b/packages/vinext/src/common/types.ts
new file mode 100644
index 000000000000..e48688a87c70
--- /dev/null
+++ b/packages/vinext/src/common/types.ts
@@ -0,0 +1,12 @@
+export type RequestInfo = {
+ path: string;
+ method: string;
+ headers: Record;
+};
+
+export type ErrorContext = {
+ routerKind: string;
+ routePath: string;
+ routeType: string;
+ revalidateReason?: 'on-demand' | 'stale' | undefined;
+};
diff --git a/packages/vinext/src/common/wrappers.ts b/packages/vinext/src/common/wrappers.ts
new file mode 100644
index 000000000000..cab75bced525
--- /dev/null
+++ b/packages/vinext/src/common/wrappers.ts
@@ -0,0 +1,189 @@
+import {
+ captureException,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+ startSpan,
+ withIsolationScope,
+} from '@sentry/core';
+
+type ServerComponentContext = {
+ componentRoute: string;
+ componentType: string;
+};
+
+/**
+ * Wraps a vinext App Router route handler with Sentry instrumentation.
+ */
+export function wrapRouteHandlerWithSentry unknown>(
+ handler: T,
+ method: string,
+ parameterizedRoute: string,
+): T {
+ return async function sentryWrappedRouteHandler(this: unknown, ...args: unknown[]) {
+ return withIsolationScope(async isolationScope => {
+ isolationScope.setTransactionName(`${method} ${parameterizedRoute}`);
+
+ return startSpan(
+ {
+ name: `${method} ${parameterizedRoute}`,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.vinext',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'http.method': method,
+ },
+ },
+ async () => {
+ try {
+ return await handler.apply(this, args);
+ } catch (error) {
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.route_handler',
+ },
+ });
+ throw error;
+ }
+ },
+ );
+ });
+ } as unknown as T;
+}
+
+/**
+ * Wraps a vinext App Router server component with Sentry instrumentation.
+ */
+export function wrapServerComponentWithSentry unknown>(
+ component: T,
+ context: ServerComponentContext,
+): T {
+ const { componentRoute, componentType } = context;
+
+ return async function sentryWrappedServerComponent(this: unknown, ...args: unknown[]) {
+ return withIsolationScope(async isolationScope => {
+ isolationScope.setTransactionName(componentRoute);
+
+ return startSpan(
+ {
+ name: `${componentType} ${componentRoute}`,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `ui.${componentType}.render`,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vinext',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'vinext.component_type': componentType,
+ },
+ },
+ async () => {
+ try {
+ return await component.apply(this, args);
+ } catch (error) {
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.server_component',
+ },
+ });
+ throw error;
+ }
+ },
+ );
+ });
+ } as unknown as T;
+}
+
+/**
+ * Wraps a vinext middleware function with Sentry instrumentation.
+ */
+export function wrapMiddlewareWithSentry unknown>(middleware: T): T {
+ return async function sentryWrappedMiddleware(this: unknown, ...args: unknown[]) {
+ return withIsolationScope(async isolationScope => {
+ // Try to extract the path from the first argument (Request object)
+ const request = args[0] as { url?: string; method?: string } | undefined;
+ let requestPath = '/';
+ let method = 'GET';
+
+ if (request?.url) {
+ try {
+ const url = new URL(request.url);
+ requestPath = url.pathname;
+ } catch {
+ // noop
+ }
+ }
+ if (request?.method) {
+ method = request.method;
+ }
+
+ isolationScope.setTransactionName(`middleware ${method} ${requestPath}`);
+
+ return startSpan(
+ {
+ name: `middleware ${method} ${requestPath}`,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.vinext.middleware',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
+ },
+ },
+ async () => {
+ try {
+ return await middleware.apply(this, args);
+ } catch (error) {
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.middleware',
+ },
+ });
+ throw error;
+ }
+ },
+ );
+ });
+ } as unknown as T;
+}
+
+/**
+ * Wraps a vinext Pages Router API handler with Sentry instrumentation.
+ */
+export function wrapApiHandlerWithSentry unknown>(
+ handler: T,
+ parameterizedRoute: string,
+): T {
+ return async function sentryWrappedApiHandler(this: unknown, ...args: unknown[]) {
+ return withIsolationScope(async isolationScope => {
+ // Try to extract the method from the first argument (IncomingMessage)
+ const req = args[0] as { method?: string } | undefined;
+ const method = req?.method || 'GET';
+
+ isolationScope.setTransactionName(`${method} ${parameterizedRoute}`);
+
+ return startSpan(
+ {
+ name: `${method} ${parameterizedRoute}`,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.vinext.api_route',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'http.method': method,
+ },
+ },
+ async () => {
+ try {
+ return await handler.apply(this, args);
+ } catch (error) {
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.api_route',
+ },
+ });
+ throw error;
+ }
+ },
+ );
+ });
+ } as unknown as T;
+}
diff --git a/packages/vinext/src/index.client.ts b/packages/vinext/src/index.client.ts
new file mode 100644
index 000000000000..4f1cce44fa36
--- /dev/null
+++ b/packages/vinext/src/index.client.ts
@@ -0,0 +1 @@
+export * from './client';
diff --git a/packages/vinext/src/index.server.ts b/packages/vinext/src/index.server.ts
new file mode 100644
index 000000000000..0ce5251aa327
--- /dev/null
+++ b/packages/vinext/src/index.server.ts
@@ -0,0 +1 @@
+export * from './server';
diff --git a/packages/vinext/src/index.types.ts b/packages/vinext/src/index.types.ts
new file mode 100644
index 000000000000..92f8bd3089fb
--- /dev/null
+++ b/packages/vinext/src/index.types.ts
@@ -0,0 +1,38 @@
+import type { Client, Integration, Options, StackParser } from '@sentry/core';
+import type * as clientSdk from './client';
+import type * as serverSdk from './server';
+
+/** Initializes Sentry vinext SDK */
+export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): Client | undefined;
+
+export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
+export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
+
+export declare const getDefaultIntegrations: (options: Options) => Integration[];
+export declare const defaultStackParser: StackParser;
+
+export declare function getSentryRelease(fallback?: string): string | undefined;
+
+export declare const ErrorBoundary: typeof clientSdk.ErrorBoundary;
+export declare const showReportDialog: typeof clientSdk.showReportDialog;
+export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary;
+
+export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;
+
+export { captureRequestError } from './server/captureRequestError';
+export { sentryVinext, type SentryVinextPluginOptions } from './vite';
+
+export {
+ wrapRouteHandlerWithSentry,
+ wrapServerComponentWithSentry,
+ wrapMiddlewareWithSentry,
+ wrapApiHandlerWithSentry,
+} from './common';
+
+export type { ErrorContext, RequestInfo } from './common';
+
+// Re-export from client
+export { browserTracingIntegration } from './client';
+
+// Re-export from server
+export { init as serverInit } from './server';
diff --git a/packages/vinext/src/index.worker.ts b/packages/vinext/src/index.worker.ts
new file mode 100644
index 000000000000..0eef2419b358
--- /dev/null
+++ b/packages/vinext/src/index.worker.ts
@@ -0,0 +1,5 @@
+/* eslint-disable import/export */
+export * from '@sentry/cloudflare';
+export { applyVinextEventProcessors, withSentry, wrapRequestHandler } from './server/worker';
+export { captureRequestError } from './server/captureRequestError';
+export * from './common';
diff --git a/packages/vinext/src/server/captureRequestError.ts b/packages/vinext/src/server/captureRequestError.ts
new file mode 100644
index 000000000000..2adbece2bda2
--- /dev/null
+++ b/packages/vinext/src/server/captureRequestError.ts
@@ -0,0 +1,49 @@
+import type { RequestEventData } from '@sentry/core';
+import { captureException, flush, headersToDict, vercelWaitUntil, withScope } from '@sentry/core';
+import type { ErrorContext, RequestInfo } from '../common/types';
+
+/**
+ * Reports errors passed to vinext's `onRequestError` instrumentation hook.
+ *
+ * Usage in `instrumentation.ts`:
+ * ```ts
+ * import * as Sentry from '@sentry/vinext';
+ * export const onRequestError = Sentry.captureRequestError;
+ * ```
+ */
+export function captureRequestError(error: unknown, request: RequestInfo, errorContext: ErrorContext): void {
+ withScope(scope => {
+ scope.setSDKProcessingMetadata({
+ normalizedRequest: {
+ headers: headersToDict(request.headers),
+ method: request.method,
+ } satisfies RequestEventData,
+ });
+
+ scope.setContext('vinext', {
+ request_path: request.path,
+ router_kind: errorContext.routerKind,
+ router_path: errorContext.routePath,
+ route_type: errorContext.routeType,
+ });
+
+ scope.setTransactionName(`${request.method} ${errorContext.routePath}`);
+
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.on_request_error',
+ },
+ });
+
+ vercelWaitUntil(flushSafelyWithTimeout());
+ });
+}
+
+async function flushSafelyWithTimeout(): Promise {
+ try {
+ await flush(2000);
+ } catch {
+ // noop
+ }
+}
diff --git a/packages/vinext/src/server/index.ts b/packages/vinext/src/server/index.ts
new file mode 100644
index 000000000000..ffdfea0086ea
--- /dev/null
+++ b/packages/vinext/src/server/index.ts
@@ -0,0 +1,100 @@
+/* eslint-disable import/export */
+import type { EventProcessor } from '@sentry/core';
+import { applySdkMetadata, consoleSandbox, getClient, getGlobalScope } from '@sentry/core';
+import type { NodeClient, NodeOptions } from '@sentry/node';
+import { getDefaultIntegrations, init as nodeInit } from '@sentry/node';
+import { DEBUG_BUILD } from '../common/debug-build';
+
+export type { NodeOptions };
+
+/** Inits the Sentry vinext SDK on the Node.js server. */
+export function init(options: NodeOptions): NodeClient | undefined {
+ if (sdkAlreadyInitialized()) {
+ DEBUG_BUILD &&
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn('[@sentry/vinext] SDK already initialized on the server.');
+ });
+ return;
+ }
+
+ const opts: NodeOptions = {
+ environment: options.environment || process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV,
+ defaultIntegrations: getDefaultIntegrations(options),
+ ...options,
+ };
+
+ applySdkMetadata(opts, 'vinext', ['vinext', 'node']);
+
+ const client = nodeInit(opts);
+
+ getGlobalScope().addEventProcessor(
+ Object.assign(
+ (event => {
+ if (event.type === 'transaction') {
+ if (event.transaction?.match(/\/__vinext\//)) {
+ return null;
+ }
+
+ if (
+ event.transaction === '/404' ||
+ event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/)
+ ) {
+ return null;
+ }
+ }
+
+ return event;
+ }) satisfies EventProcessor,
+ { id: 'VinextLowQualityTransactionsFilter' },
+ ),
+ );
+
+ getGlobalScope().addEventProcessor(
+ Object.assign(
+ ((event, hint) => {
+ if (event.type !== undefined) {
+ return event;
+ }
+
+ const originalException = hint.originalException;
+
+ const isPostponeError =
+ typeof originalException === 'object' &&
+ originalException !== null &&
+ '$$typeof' in originalException &&
+ originalException.$$typeof === Symbol.for('react.postpone');
+
+ if (isPostponeError) {
+ return null;
+ }
+
+ const exceptionMessage = event.exception?.values?.[0]?.value;
+ if (
+ exceptionMessage?.includes('Suspense Exception: This is not a real error!') ||
+ exceptionMessage?.includes('Suspense Exception: This is not a real error, and should not leak')
+ ) {
+ return null;
+ }
+
+ return event;
+ }) satisfies EventProcessor,
+ { id: 'VinextDropReactControlFlowErrors' },
+ ),
+ );
+
+ return client;
+}
+
+function sdkAlreadyInitialized(): boolean {
+ return !!getClient();
+}
+
+export { captureRequestError } from './captureRequestError';
+
+export { sentryVinext } from '../vite';
+export type { SentryVinextPluginOptions } from '../vite';
+
+export * from '../common';
+
+export * from '@sentry/node';
diff --git a/packages/vinext/src/server/worker.ts b/packages/vinext/src/server/worker.ts
new file mode 100644
index 000000000000..9b9bc460e938
--- /dev/null
+++ b/packages/vinext/src/server/worker.ts
@@ -0,0 +1,97 @@
+import type { EventProcessor } from '@sentry/core';
+import { applySdkMetadata, getClient, getGlobalScope } from '@sentry/core';
+
+/**
+ * Inits the Sentry vinext SDK on Cloudflare Workers.
+ *
+ * On Cloudflare Workers, users should use the `withSentry` wrapper from `@sentry/cloudflare`
+ * to initialize the SDK within the Worker's fetch handler. This function applies vinext-specific
+ * event processors after the Cloudflare SDK has been initialized.
+ *
+ * @example
+ * ```ts
+ * // worker/index.ts (Cloudflare Workers entry)
+ * import { withSentry } from '@sentry/cloudflare';
+ *
+ * export default withSentry(
+ * (env) => ({ dsn: env.SENTRY_DSN }),
+ * { fetch(request, env, ctx) { ... } }
+ * );
+ * ```
+ *
+ * Then call `applyVinextEventProcessors()` in the `register()` callback
+ * of your `instrumentation.ts`.
+ */
+export function applyVinextEventProcessors(): void {
+ if (sdkAlreadyInitialized()) {
+ applyProcessors();
+ }
+}
+
+function applyProcessors(): void {
+ applySdkMetadata({}, 'vinext', ['vinext', 'cloudflare']);
+
+ getGlobalScope().addEventProcessor(
+ Object.assign(
+ (event => {
+ if (event.type === 'transaction') {
+ if (event.transaction?.match(/\/__vinext\//)) {
+ return null;
+ }
+
+ if (
+ event.transaction === '/404' ||
+ event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/)
+ ) {
+ return null;
+ }
+ }
+
+ return event;
+ }) satisfies EventProcessor,
+ { id: 'VinextWorkerLowQualityTransactionsFilter' },
+ ),
+ );
+
+ getGlobalScope().addEventProcessor(
+ Object.assign(
+ ((event, hint) => {
+ if (event.type !== undefined) {
+ return event;
+ }
+
+ const originalException = hint.originalException;
+
+ const isPostponeError =
+ typeof originalException === 'object' &&
+ originalException !== null &&
+ '$$typeof' in originalException &&
+ originalException.$$typeof === Symbol.for('react.postpone');
+
+ if (isPostponeError) {
+ return null;
+ }
+
+ const exceptionMessage = event.exception?.values?.[0]?.value;
+ if (
+ exceptionMessage?.includes('Suspense Exception: This is not a real error!') ||
+ exceptionMessage?.includes('Suspense Exception: This is not a real error, and should not leak')
+ ) {
+ return null;
+ }
+
+ return event;
+ }) satisfies EventProcessor,
+ { id: 'VinextWorkerDropReactControlFlowErrors' },
+ ),
+ );
+}
+
+function sdkAlreadyInitialized(): boolean {
+ return !!getClient();
+}
+
+// We don't provide a standalone `init()` for Workers because
+// `@sentry/cloudflare` provides `withSentry()` which handles initialization.
+// Re-export cloudflare utilities for convenience.
+export { withSentry, wrapRequestHandler } from '@sentry/cloudflare';
diff --git a/packages/vinext/src/vite/autoInstrumentation.ts b/packages/vinext/src/vite/autoInstrumentation.ts
new file mode 100644
index 000000000000..05061503230b
--- /dev/null
+++ b/packages/vinext/src/vite/autoInstrumentation.ts
@@ -0,0 +1,156 @@
+import * as path from 'path';
+import type { Plugin } from 'vite';
+import type { AutoInstrumentOptions } from './types';
+
+const WRAPPED_MODULE_SUFFIX = '?sentry-auto-wrap';
+
+/**
+ * Creates a Vite plugin that automatically instruments vinext application code.
+ * Wraps App Router server components, route handlers, middleware, and Pages Router API routes.
+ */
+export function makeAutoInstrumentationPlugin(options: boolean | AutoInstrumentOptions): Plugin {
+ const resolvedOptions: Required = {
+ serverComponents: true,
+ routeHandlers: true,
+ middleware: true,
+ apiRoutes: true,
+ ...(typeof options === 'object' ? options : {}),
+ };
+
+ return {
+ name: 'sentry-vinext-auto-instrumentation',
+ enforce: 'pre',
+
+ load(id) {
+ const filename = path.basename(id);
+ const normalizedId = id.replace(/\\/g, '/');
+
+ // Skip already-wrapped modules to avoid infinite recursion
+ if (id.includes(WRAPPED_MODULE_SUFFIX)) {
+ return null;
+ }
+
+ // App Router route handlers: app/**/route.(ts|js|tsx|jsx)
+ if (resolvedOptions.routeHandlers && isRouteHandler(normalizedId, filename)) {
+ const parameterizedRoute = extractAppRouterRoute(normalizedId);
+ return getRouteHandlerWrapperCode(id, parameterizedRoute);
+ }
+
+ // App Router server components: app/**/page.(ts|js|tsx|jsx) and app/**/layout.(ts|js|tsx|jsx)
+ if (resolvedOptions.serverComponents && isServerComponent(normalizedId, filename)) {
+ const parameterizedRoute = extractAppRouterRoute(normalizedId);
+ const componentType = filename.startsWith('page') ? 'page' : 'layout';
+ return getServerComponentWrapperCode(id, parameterizedRoute, componentType);
+ }
+
+ // Middleware: middleware.(ts|js)
+ if (resolvedOptions.middleware && isMiddleware(normalizedId, filename)) {
+ return getMiddlewareWrapperCode(id);
+ }
+
+ // Pages Router API routes: pages/api/**
+ if (resolvedOptions.apiRoutes && isApiRoute(normalizedId, filename)) {
+ const parameterizedRoute = extractPagesRouterRoute(normalizedId);
+ return getApiRouteWrapperCode(id, parameterizedRoute);
+ }
+
+ return null;
+ },
+ };
+}
+
+function isRouteHandler(normalizedId: string, filename: string): boolean {
+ return /\/app\//.test(normalizedId) && /^route\.(ts|js|tsx|jsx|mts|mjs)$/.test(filename);
+}
+
+function isServerComponent(normalizedId: string, filename: string): boolean {
+ return /\/app\//.test(normalizedId) && /^(page|layout)\.(ts|js|tsx|jsx|mts|mjs)$/.test(filename);
+}
+
+function isMiddleware(normalizedId: string, filename: string): boolean {
+ return (
+ /^middleware\.(ts|js|mts|mjs)$/.test(filename) &&
+ // Ensure it's at the project root or src/ level, not nested in app/
+ !normalizedId.includes('/app/') &&
+ !normalizedId.includes('/pages/')
+ );
+}
+
+function isApiRoute(normalizedId: string, filename: string): boolean {
+ return /\/pages\/api\//.test(normalizedId) && /\.(ts|js|tsx|jsx|mts|mjs)$/.test(filename);
+}
+
+/**
+ * Extracts a parameterized route from an App Router file path.
+ * e.g. `/path/to/app/blog/[slug]/page.tsx` -> `/blog/[slug]`
+ */
+function extractAppRouterRoute(normalizedId: string): string {
+ const appMatch = normalizedId.match(/\/app(\/.*?)\/(page|layout|route)\.\w+$/);
+ if (appMatch) {
+ return appMatch[1] || '/';
+ }
+ // Root level: app/page.tsx -> /
+ if (/\/app\/(page|layout|route)\.\w+$/.test(normalizedId)) {
+ return '/';
+ }
+ return '/';
+}
+
+/**
+ * Extracts a parameterized route from a Pages Router file path.
+ * e.g. `/path/to/pages/api/users/[id].ts` -> `/api/users/[id]`
+ */
+function extractPagesRouterRoute(normalizedId: string): string {
+ const pagesMatch = normalizedId.match(/\/pages(\/.*?)\.\w+$/);
+ if (pagesMatch) {
+ // Remove /index suffix since /pages/api/index.ts -> /api
+ return pagesMatch[1]?.replace(/\/index$/, '') || '/';
+ }
+ return '/';
+}
+
+function getRouteHandlerWrapperCode(id: string, route: string): string {
+ const wrappedId = `${id}${WRAPPED_MODULE_SUFFIX}`;
+ return [
+ 'import { wrapRouteHandlerWithSentry as _sentry_wrapRouteHandler } from "@sentry/vinext";',
+ `import * as _sentry_routeModule from ${JSON.stringify(wrappedId)};`,
+ `const _sentry_route = ${JSON.stringify(route)};`,
+ 'export const GET = _sentry_routeModule.GET ? _sentry_wrapRouteHandler(_sentry_routeModule.GET, "GET", _sentry_route) : undefined;',
+ 'export const POST = _sentry_routeModule.POST ? _sentry_wrapRouteHandler(_sentry_routeModule.POST, "POST", _sentry_route) : undefined;',
+ 'export const PUT = _sentry_routeModule.PUT ? _sentry_wrapRouteHandler(_sentry_routeModule.PUT, "PUT", _sentry_route) : undefined;',
+ 'export const PATCH = _sentry_routeModule.PATCH ? _sentry_wrapRouteHandler(_sentry_routeModule.PATCH, "PATCH", _sentry_route) : undefined;',
+ 'export const DELETE = _sentry_routeModule.DELETE ? _sentry_wrapRouteHandler(_sentry_routeModule.DELETE, "DELETE", _sentry_route) : undefined;',
+ 'export const HEAD = _sentry_routeModule.HEAD ? _sentry_wrapRouteHandler(_sentry_routeModule.HEAD, "HEAD", _sentry_route) : undefined;',
+ 'export const OPTIONS = _sentry_routeModule.OPTIONS ? _sentry_wrapRouteHandler(_sentry_routeModule.OPTIONS, "OPTIONS", _sentry_route) : undefined;',
+ ].join('\n');
+}
+
+function getServerComponentWrapperCode(id: string, route: string, componentType: string): string {
+ const wrappedId = `${id}${WRAPPED_MODULE_SUFFIX}`;
+ return [
+ 'import { wrapServerComponentWithSentry as _sentry_wrapServerComponent } from "@sentry/vinext";',
+ `import * as _sentry_componentModule from ${JSON.stringify(wrappedId)};`,
+ `export default _sentry_componentModule.default ? _sentry_wrapServerComponent(_sentry_componentModule.default, { componentRoute: ${JSON.stringify(route)}, componentType: ${JSON.stringify(componentType)} }) : undefined;`,
+ `export * from ${JSON.stringify(wrappedId)};`,
+ ].join('\n');
+}
+
+function getMiddlewareWrapperCode(id: string): string {
+ const wrappedId = `${id}${WRAPPED_MODULE_SUFFIX}`;
+ return [
+ 'import { wrapMiddlewareWithSentry as _sentry_wrapMiddleware } from "@sentry/vinext";',
+ `import * as _sentry_middlewareModule from ${JSON.stringify(wrappedId)};`,
+ 'export default _sentry_middlewareModule.default ? _sentry_wrapMiddleware(_sentry_middlewareModule.default) : undefined;',
+ 'export const config = _sentry_middlewareModule.config;',
+ ].join('\n');
+}
+
+function getApiRouteWrapperCode(id: string, route: string): string {
+ const wrappedId = `${id}${WRAPPED_MODULE_SUFFIX}`;
+ return [
+ 'import { wrapApiHandlerWithSentry as _sentry_wrapApiHandler } from "@sentry/vinext";',
+ `import * as _sentry_apiModule from ${JSON.stringify(wrappedId)};`,
+ `export default _sentry_apiModule.default ? _sentry_wrapApiHandler(_sentry_apiModule.default, ${JSON.stringify(route)}) : _sentry_apiModule.default;`,
+ `export * from ${JSON.stringify(wrappedId)};`,
+ ].join('\n');
+}
diff --git a/packages/vinext/src/vite/index.ts b/packages/vinext/src/vite/index.ts
new file mode 100644
index 000000000000..606920fce6bb
--- /dev/null
+++ b/packages/vinext/src/vite/index.ts
@@ -0,0 +1,2 @@
+export { sentryVinext } from './sentryVinextPlugin';
+export type { SentryVinextPluginOptions } from './types';
diff --git a/packages/vinext/src/vite/sentryVinextPlugin.ts b/packages/vinext/src/vite/sentryVinextPlugin.ts
new file mode 100644
index 000000000000..0c3bfcd8c64a
--- /dev/null
+++ b/packages/vinext/src/vite/sentryVinextPlugin.ts
@@ -0,0 +1,116 @@
+import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
+import { sentryVitePlugin } from '@sentry/vite-plugin';
+import type { Plugin, UserConfig } from 'vite';
+import { makeAutoInstrumentationPlugin } from './autoInstrumentation';
+import type { SentryVinextPluginOptions } from './types';
+
+const DEFAULT_OPTIONS: SentryVinextPluginOptions = {
+ autoUploadSourceMaps: true,
+ autoInstrument: true,
+};
+
+/**
+ * Sentry Vite plugin for vinext applications.
+ *
+ * Handles source map upload and auto-instrumentation of server components,
+ * route handlers, and middleware.
+ *
+ * @example
+ * ```ts
+ * // vite.config.ts
+ * import { sentryVinext } from '@sentry/vinext';
+ * import vinext from 'vinext';
+ *
+ * export default defineConfig({
+ * plugins: [
+ * vinext(),
+ * sentryVinext({
+ * org: 'my-org',
+ * project: 'my-project',
+ * authToken: process.env.SENTRY_AUTH_TOKEN,
+ * }),
+ * ],
+ * });
+ * ```
+ */
+export async function sentryVinext(options: SentryVinextPluginOptions = {}): Promise {
+ const mergedOptions = {
+ ...DEFAULT_OPTIONS,
+ ...options,
+ };
+
+ const sentryPlugins: Plugin[] = [];
+
+ if (mergedOptions.autoInstrument) {
+ sentryPlugins.push(makeAutoInstrumentationPlugin(mergedOptions.autoInstrument));
+ }
+
+ sentryPlugins.push(makeSourceMapSettingsPlugin(mergedOptions));
+
+ if (mergedOptions.autoUploadSourceMaps && process.env.NODE_ENV !== 'development') {
+ const vitePluginOptions = buildSentryVitePluginOptions(mergedOptions);
+ if (vitePluginOptions) {
+ const uploadPlugins = await sentryVitePlugin(vitePluginOptions);
+ sentryPlugins.push(...uploadPlugins);
+ }
+ }
+
+ return sentryPlugins;
+}
+
+function makeSourceMapSettingsPlugin(options: SentryVinextPluginOptions): Plugin {
+ return {
+ name: 'sentry-vinext-source-map-settings',
+ apply: 'build',
+ config(config: UserConfig) {
+ const currentSourceMap = config.build?.sourcemap;
+
+ if (currentSourceMap === false) {
+ if (options.debug) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ '[Sentry] Source map generation is disabled in your Vite config. Sentry will not override this. Without source maps, code snippets on the Sentry Issues page will remain minified.',
+ );
+ }
+ return config;
+ }
+
+ if (currentSourceMap && ['hidden', 'inline', true].includes(currentSourceMap as string | boolean)) {
+ return config;
+ }
+
+ return {
+ ...config,
+ build: {
+ ...config.build,
+ sourcemap: 'hidden',
+ },
+ };
+ },
+ };
+}
+
+function buildSentryVitePluginOptions(options: SentryVinextPluginOptions): SentryVitePluginOptions | null {
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ autoInstrument: _ai,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ autoUploadSourceMaps: _au,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ bundleSizeOptimizations: _bso,
+ ...vitePluginOptions
+ } = options;
+
+ if (!vitePluginOptions.org && !process.env.SENTRY_ORG) {
+ return null;
+ }
+
+ return {
+ ...vitePluginOptions,
+ _metaOptions: {
+ telemetry: {
+ metaFramework: 'vinext',
+ },
+ },
+ };
+}
diff --git a/packages/vinext/src/vite/types.ts b/packages/vinext/src/vite/types.ts
new file mode 100644
index 000000000000..49b26625c6f1
--- /dev/null
+++ b/packages/vinext/src/vite/types.ts
@@ -0,0 +1,54 @@
+import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
+
+export interface SentryVinextPluginOptions extends Partial {
+ /**
+ * If enabled, the plugin will automatically wrap route handlers, server components,
+ * and middleware with Sentry instrumentation.
+ *
+ * @default true
+ */
+ autoInstrument?: boolean | AutoInstrumentOptions;
+
+ /**
+ * If enabled, source maps will be automatically uploaded to Sentry during production builds.
+ *
+ * @default true
+ */
+ autoUploadSourceMaps?: boolean;
+
+ /**
+ * Options for bundle size optimizations.
+ */
+ bundleSizeOptimizations?: {
+ excludeDebugStatements?: boolean;
+ excludeReplayIframe?: boolean;
+ excludeReplayShadowDom?: boolean;
+ excludeReplayWorker?: boolean;
+ };
+}
+
+export interface AutoInstrumentOptions {
+ /**
+ * Whether to auto-wrap App Router server components (page.tsx, layout.tsx).
+ * @default true
+ */
+ serverComponents?: boolean;
+
+ /**
+ * Whether to auto-wrap App Router route handlers (route.ts).
+ * @default true
+ */
+ routeHandlers?: boolean;
+
+ /**
+ * Whether to auto-wrap middleware (middleware.ts).
+ * @default true
+ */
+ middleware?: boolean;
+
+ /**
+ * Whether to auto-wrap Pages Router API routes.
+ * @default true
+ */
+ apiRoutes?: boolean;
+}
diff --git a/packages/vinext/test/common/wrappers.test.ts b/packages/vinext/test/common/wrappers.test.ts
new file mode 100644
index 000000000000..f4dd2ad0797e
--- /dev/null
+++ b/packages/vinext/test/common/wrappers.test.ts
@@ -0,0 +1,168 @@
+import * as SentryCore from '@sentry/core';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import {
+ wrapApiHandlerWithSentry,
+ wrapMiddlewareWithSentry,
+ wrapRouteHandlerWithSentry,
+ wrapServerComponentWithSentry,
+} from '../../src/common/wrappers';
+
+vi.mock('@sentry/core', async () => {
+ const actual = await vi.importActual('@sentry/core');
+ return {
+ ...actual,
+ startSpan: vi.fn((_options, callback) => callback()),
+ withIsolationScope: vi.fn(callback =>
+ callback({
+ setTransactionName: vi.fn(),
+ }),
+ ),
+ captureException: vi.fn(),
+ };
+});
+
+describe('wrapRouteHandlerWithSentry', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('calls the original handler and returns its result', async () => {
+ const handler = vi.fn().mockResolvedValue(new Response('OK'));
+ const wrapped = wrapRouteHandlerWithSentry(handler, 'GET', '/api/users');
+
+ const result = await wrapped();
+
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect(result).toBeInstanceOf(Response);
+ });
+
+ it('creates a span with the correct attributes', async () => {
+ const handler = vi.fn().mockResolvedValue(new Response('OK'));
+ const wrapped = wrapRouteHandlerWithSentry(handler, 'POST', '/api/data');
+
+ await wrapped();
+
+ expect(SentryCore.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'POST /api/data',
+ attributes: expect.objectContaining({
+ 'http.method': 'POST',
+ }),
+ }),
+ expect.any(Function),
+ );
+ });
+
+ it('captures errors and re-throws', async () => {
+ const error = new Error('handler failed');
+ const handler = vi.fn().mockRejectedValue(error);
+ const wrapped = wrapRouteHandlerWithSentry(handler, 'GET', '/api/test');
+
+ await expect(wrapped()).rejects.toThrow('handler failed');
+ expect(SentryCore.captureException).toHaveBeenCalledWith(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.route_handler',
+ },
+ });
+ });
+});
+
+describe('wrapServerComponentWithSentry', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('calls the original component', async () => {
+ const component = vi.fn().mockResolvedValue('Hello
');
+ const wrapped = wrapServerComponentWithSentry(component, {
+ componentRoute: '/blog/[slug]',
+ componentType: 'page',
+ });
+
+ await wrapped({ slug: 'test' });
+
+ expect(component).toHaveBeenCalledWith({ slug: 'test' });
+ });
+
+ it('creates a span with correct attributes', async () => {
+ const component = vi.fn().mockResolvedValue(null);
+ const wrapped = wrapServerComponentWithSentry(component, {
+ componentRoute: '/about',
+ componentType: 'layout',
+ });
+
+ await wrapped();
+
+ expect(SentryCore.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'layout /about',
+ attributes: expect.objectContaining({
+ 'vinext.component_type': 'layout',
+ }),
+ }),
+ expect.any(Function),
+ );
+ });
+});
+
+describe('wrapMiddlewareWithSentry', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('calls the original middleware', async () => {
+ const middleware = vi.fn().mockResolvedValue(new Response());
+ const wrapped = wrapMiddlewareWithSentry(middleware);
+
+ const request = { url: 'http://localhost:3000/api/test', method: 'GET' };
+ await wrapped(request);
+
+ expect(middleware).toHaveBeenCalledWith(request);
+ });
+
+ it('extracts path from request URL', async () => {
+ const middleware = vi.fn().mockResolvedValue(new Response());
+ const wrapped = wrapMiddlewareWithSentry(middleware);
+
+ await wrapped({ url: 'http://localhost:3000/protected', method: 'POST' });
+
+ expect(SentryCore.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'middleware POST /protected',
+ }),
+ expect.any(Function),
+ );
+ });
+});
+
+describe('wrapApiHandlerWithSentry', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('calls the original handler', async () => {
+ const handler = vi.fn();
+ const wrapped = wrapApiHandlerWithSentry(handler, '/api/users');
+
+ const req = { method: 'GET' };
+ const res = {};
+ await wrapped(req, res);
+
+ expect(handler).toHaveBeenCalledWith(req, res);
+ });
+
+ it('creates a span with the route', async () => {
+ const handler = vi.fn();
+ const wrapped = wrapApiHandlerWithSentry(handler, '/api/users/[id]');
+
+ await wrapped({ method: 'PUT' }, {});
+
+ expect(SentryCore.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'PUT /api/users/[id]',
+ }),
+ expect.any(Function),
+ );
+ });
+});
diff --git a/packages/vinext/test/server/captureRequestError.test.ts b/packages/vinext/test/server/captureRequestError.test.ts
new file mode 100644
index 000000000000..8f101fc1a1be
--- /dev/null
+++ b/packages/vinext/test/server/captureRequestError.test.ts
@@ -0,0 +1,90 @@
+import * as SentryCore from '@sentry/core';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { captureRequestError } from '../../src/server/captureRequestError';
+
+describe('captureRequestError', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('captures the error with the correct context', () => {
+ const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => '');
+ const withScopeSpy = vi.spyOn(SentryCore, 'withScope').mockImplementation(fn => {
+ const mockScope = {
+ setSDKProcessingMetadata: vi.fn(),
+ setContext: vi.fn(),
+ setTransactionName: vi.fn(),
+ };
+ return fn(mockScope as any);
+ });
+
+ const error = new Error('test error');
+ const request = {
+ path: '/api/users',
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ };
+ const errorContext = {
+ routerKind: 'App Router',
+ routePath: '/api/users',
+ routeType: 'route',
+ };
+
+ captureRequestError(error, request, errorContext);
+
+ expect(withScopeSpy).toHaveBeenCalledTimes(1);
+ expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.function.vinext.on_request_error',
+ },
+ });
+ });
+
+ it('sets the vinext context on the scope', () => {
+ vi.spyOn(SentryCore, 'captureException').mockImplementation(() => '');
+ const setContextFn = vi.fn();
+ vi.spyOn(SentryCore, 'withScope').mockImplementation(fn => {
+ const mockScope = {
+ setSDKProcessingMetadata: vi.fn(),
+ setContext: setContextFn,
+ setTransactionName: vi.fn(),
+ };
+ return fn(mockScope as any);
+ });
+
+ captureRequestError(
+ new Error('test'),
+ { path: '/blog/[slug]', method: 'GET', headers: {} },
+ { routerKind: 'App Router', routePath: '/blog/[slug]', routeType: 'render' },
+ );
+
+ expect(setContextFn).toHaveBeenCalledWith('vinext', {
+ request_path: '/blog/[slug]',
+ router_kind: 'App Router',
+ router_path: '/blog/[slug]',
+ route_type: 'render',
+ });
+ });
+
+ it('sets the transaction name from request method and route', () => {
+ vi.spyOn(SentryCore, 'captureException').mockImplementation(() => '');
+ const setTransactionNameFn = vi.fn();
+ vi.spyOn(SentryCore, 'withScope').mockImplementation(fn => {
+ const mockScope = {
+ setSDKProcessingMetadata: vi.fn(),
+ setContext: vi.fn(),
+ setTransactionName: setTransactionNameFn,
+ };
+ return fn(mockScope as any);
+ });
+
+ captureRequestError(
+ new Error('test'),
+ { path: '/api/data', method: 'DELETE', headers: {} },
+ { routerKind: 'Pages Router', routePath: '/api/data', routeType: 'route' },
+ );
+
+ expect(setTransactionNameFn).toHaveBeenCalledWith('DELETE /api/data');
+ });
+});
diff --git a/packages/vinext/test/server/sdk.test.ts b/packages/vinext/test/server/sdk.test.ts
new file mode 100644
index 000000000000..a886b1a1639d
--- /dev/null
+++ b/packages/vinext/test/server/sdk.test.ts
@@ -0,0 +1,141 @@
+import type { EventProcessor } from '@sentry/core';
+import * as SentryCore from '@sentry/core';
+import { SDK_VERSION } from '@sentry/node';
+import * as SentryNode from '@sentry/node';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { init as vinextInit } from '../../src/server';
+
+const nodeInit = vi.spyOn(SentryNode, 'init');
+
+describe('Vinext Server SDK init', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Ensure no client is initialized for the next test
+ vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('has the correct metadata', () => {
+ const client = vinextInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
+
+ const expectedMetadata = {
+ _metadata: {
+ sdk: {
+ name: 'sentry.javascript.vinext',
+ packages: [
+ { name: 'npm:@sentry/vinext', version: SDK_VERSION },
+ { name: 'npm:@sentry/node', version: SDK_VERSION },
+ ],
+ version: SDK_VERSION,
+ },
+ },
+ };
+
+ expect(client).not.toBeUndefined();
+ expect(nodeInit).toHaveBeenCalledTimes(1);
+ expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata));
+ });
+
+ it('registers event processors', () => {
+ const passedEventProcessors: EventProcessor[] = [];
+ vi.spyOn(SentryCore, 'getGlobalScope').mockReturnValue({
+ addEventProcessor: (ep: EventProcessor) => {
+ passedEventProcessors.push(ep);
+ return {} as any;
+ },
+ } as any);
+
+ vinextInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
+
+ expect(passedEventProcessors.length).toBe(2);
+ expect(passedEventProcessors[0]?.id).toEqual('VinextLowQualityTransactionsFilter');
+ expect(passedEventProcessors[1]?.id).toEqual('VinextDropReactControlFlowErrors');
+ });
+
+ describe('VinextLowQualityTransactionsFilter', () => {
+ function getFilter(): EventProcessor {
+ const passedEventProcessors: EventProcessor[] = [];
+ vi.spyOn(SentryCore, 'getGlobalScope').mockReturnValue({
+ addEventProcessor: (ep: EventProcessor) => {
+ passedEventProcessors.push(ep);
+ return {} as any;
+ },
+ } as any);
+
+ vinextInit({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
+
+ const filter = passedEventProcessors.find(ep => ep.id === 'VinextLowQualityTransactionsFilter');
+ expect(filter).toBeDefined();
+ return filter!;
+ }
+
+ it('filters __vinext internal transactions', () => {
+ const filter = getFilter();
+ expect(filter({ type: 'transaction', transaction: 'GET /__vinext/image' }, {})).toBeNull();
+ });
+
+ it('filters 404 transactions', () => {
+ const filter = getFilter();
+ expect(filter({ type: 'transaction', transaction: '/404' }, {})).toBeNull();
+ expect(filter({ type: 'transaction', transaction: 'GET /404' }, {})).toBeNull();
+ expect(filter({ type: 'transaction', transaction: 'GET /_not-found' }, {})).toBeNull();
+ });
+
+ it('keeps valid transactions', () => {
+ const filter = getFilter();
+ const event = { type: 'transaction' as const, transaction: 'GET /api/users' };
+ expect(filter(event, {})).toEqual(event);
+ });
+ });
+
+ describe('VinextDropReactControlFlowErrors', () => {
+ function getFilter(): EventProcessor {
+ const passedEventProcessors: EventProcessor[] = [];
+ vi.spyOn(SentryCore, 'getGlobalScope').mockReturnValue({
+ addEventProcessor: (ep: EventProcessor) => {
+ passedEventProcessors.push(ep);
+ return {} as any;
+ },
+ } as any);
+
+ vinextInit({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });
+
+ const filter = passedEventProcessors.find(ep => ep.id === 'VinextDropReactControlFlowErrors');
+ expect(filter).toBeDefined();
+ return filter!;
+ }
+
+ it('filters React Suspense errors', () => {
+ const filter = getFilter();
+ const event = {
+ exception: {
+ values: [{ value: 'Suspense Exception: This is not a real error!' }],
+ },
+ };
+ expect(filter(event, {})).toBeNull();
+ });
+
+ it('filters React postpone errors', () => {
+ const filter = getFilter();
+ const postponeError = { $$typeof: Symbol.for('react.postpone') };
+ expect(filter({}, { originalException: postponeError })).toBeNull();
+ });
+
+ it('keeps real errors', () => {
+ const filter = getFilter();
+ const event = {
+ exception: {
+ values: [{ value: 'TypeError: Cannot read property of undefined' }],
+ },
+ };
+ expect(filter(event, { originalException: new Error('real error') })).toEqual(event);
+ });
+ });
+});
diff --git a/packages/vinext/test/vite/autoInstrumentation.test.ts b/packages/vinext/test/vite/autoInstrumentation.test.ts
new file mode 100644
index 000000000000..da8678882f6b
--- /dev/null
+++ b/packages/vinext/test/vite/autoInstrumentation.test.ts
@@ -0,0 +1,112 @@
+import { describe, expect, it } from 'vitest';
+import { makeAutoInstrumentationPlugin } from '../../src/vite/autoInstrumentation';
+
+describe('makeAutoInstrumentationPlugin', () => {
+ it('creates a plugin with the correct name', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ expect(plugin.name).toBe('sentry-vinext-auto-instrumentation');
+ });
+
+ it('sets enforce to pre', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ expect(plugin.enforce).toBe('pre');
+ });
+
+ it('returns null for non-matching files', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ expect(load('/Users/project/src/utils/helper.ts')).toBeNull();
+ });
+
+ it('returns null for already-wrapped modules', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ expect(load('/Users/project/app/page.tsx?sentry-auto-wrap')).toBeNull();
+ });
+
+ it('wraps App Router route handlers', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ const result = load('/Users/project/app/api/users/route.ts');
+
+ expect(result).not.toBeNull();
+ expect(result).toContain('wrapRouteHandlerWithSentry');
+ expect(result).toContain('sentry-auto-wrap');
+ expect(result).toContain('/api/users');
+ });
+
+ it('wraps App Router page components', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ const result = load('/Users/project/app/blog/[slug]/page.tsx');
+
+ expect(result).not.toBeNull();
+ expect(result).toContain('wrapServerComponentWithSentry');
+ expect(result).toContain('/blog/[slug]');
+ expect(result).toContain('"page"');
+ });
+
+ it('wraps App Router layout components', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ const result = load('/Users/project/app/layout.tsx');
+
+ expect(result).not.toBeNull();
+ expect(result).toContain('wrapServerComponentWithSentry');
+ expect(result).toContain('"layout"');
+ });
+
+ it('wraps middleware files', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ const result = load('/Users/project/middleware.ts');
+
+ expect(result).not.toBeNull();
+ expect(result).toContain('wrapMiddlewareWithSentry');
+ });
+
+ it('wraps Pages Router API routes', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ const result = load('/Users/project/pages/api/users/[id].ts');
+
+ expect(result).not.toBeNull();
+ expect(result).toContain('wrapApiHandlerWithSentry');
+ expect(result).toContain('/api/users/[id]');
+ });
+
+ it('does not wrap middleware inside app/ directory', () => {
+ const plugin = makeAutoInstrumentationPlugin(true);
+ const load = plugin.load as (id: string) => string | null;
+ expect(load('/Users/project/app/middleware.ts')).toBeNull();
+ });
+
+ it('respects disabled options', () => {
+ const plugin = makeAutoInstrumentationPlugin({
+ serverComponents: false,
+ routeHandlers: false,
+ middleware: false,
+ apiRoutes: false,
+ });
+ const load = plugin.load as (id: string) => string | null;
+
+ expect(load('/Users/project/app/page.tsx')).toBeNull();
+ expect(load('/Users/project/app/api/route.ts')).toBeNull();
+ expect(load('/Users/project/middleware.ts')).toBeNull();
+ expect(load('/Users/project/pages/api/test.ts')).toBeNull();
+ });
+
+ it('allows selective enabling', () => {
+ const plugin = makeAutoInstrumentationPlugin({
+ serverComponents: false,
+ routeHandlers: true,
+ middleware: false,
+ apiRoutes: false,
+ });
+ const load = plugin.load as (id: string) => string | null;
+
+ expect(load('/Users/project/app/page.tsx')).toBeNull();
+ expect(load('/Users/project/app/api/route.ts')).not.toBeNull();
+ expect(load('/Users/project/middleware.ts')).toBeNull();
+ });
+});
diff --git a/packages/vinext/test/vite/sentryVinextPlugin.test.ts b/packages/vinext/test/vite/sentryVinextPlugin.test.ts
new file mode 100644
index 000000000000..27dd96a954bc
--- /dev/null
+++ b/packages/vinext/test/vite/sentryVinextPlugin.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, it, vi } from 'vitest';
+import { sentryVinext } from '../../src/vite/sentryVinextPlugin';
+
+// Mock the sentry vite plugin
+vi.mock('@sentry/vite-plugin', () => ({
+ sentryVitePlugin: vi.fn().mockResolvedValue([{ name: 'sentry-vite-plugin' }]),
+}));
+
+describe('sentryVinext', () => {
+ it('returns an array of plugins', async () => {
+ const plugins = await sentryVinext();
+ expect(Array.isArray(plugins)).toBe(true);
+ expect(plugins.length).toBeGreaterThan(0);
+ });
+
+ it('includes auto-instrumentation plugin by default', async () => {
+ const plugins = await sentryVinext();
+ const autoPlugin = plugins.find(p => p.name === 'sentry-vinext-auto-instrumentation');
+ expect(autoPlugin).toBeDefined();
+ });
+
+ it('includes source map settings plugin', async () => {
+ const plugins = await sentryVinext();
+ const sourceMapPlugin = plugins.find(p => p.name === 'sentry-vinext-source-map-settings');
+ expect(sourceMapPlugin).toBeDefined();
+ });
+
+ it('can disable auto-instrumentation', async () => {
+ const plugins = await sentryVinext({ autoInstrument: false });
+ const autoPlugin = plugins.find(p => p.name === 'sentry-vinext-auto-instrumentation');
+ expect(autoPlugin).toBeUndefined();
+ });
+
+ it('source map settings plugin enables hidden source maps when not set', async () => {
+ const plugins = await sentryVinext();
+ const sourceMapPlugin = plugins.find(p => p.name === 'sentry-vinext-source-map-settings')!;
+
+ const configHook = (sourceMapPlugin as any).config;
+ const result = configHook({ build: {} });
+
+ expect(result.build.sourcemap).toBe('hidden');
+ });
+
+ it('source map settings plugin preserves existing source map setting', async () => {
+ const plugins = await sentryVinext();
+ const sourceMapPlugin = plugins.find(p => p.name === 'sentry-vinext-source-map-settings')!;
+
+ const configHook = (sourceMapPlugin as any).config;
+ const inputConfig = { build: { sourcemap: true } };
+ const result = configHook(inputConfig);
+
+ // When source maps are already configured, the plugin returns the original config unchanged
+ expect(result).toBe(inputConfig);
+ });
+
+ it('source map settings plugin preserves disabled source maps', async () => {
+ const plugins = await sentryVinext();
+ const sourceMapPlugin = plugins.find(p => p.name === 'sentry-vinext-source-map-settings')!;
+
+ const configHook = (sourceMapPlugin as any).config;
+ const inputConfig = { build: { sourcemap: false } };
+ const result = configHook(inputConfig);
+
+ // When source maps are explicitly disabled, the plugin returns the original config unchanged
+ expect(result).toBe(inputConfig);
+ });
+});
diff --git a/packages/vinext/tsconfig.json b/packages/vinext/tsconfig.json
new file mode 100644
index 000000000000..f90136f6d07b
--- /dev/null
+++ b/packages/vinext/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+
+ "include": ["src/**/*"],
+
+ "compilerOptions": {
+ "jsx": "react-jsx"
+ }
+}
diff --git a/packages/vinext/tsconfig.test.json b/packages/vinext/tsconfig.test.json
new file mode 100644
index 000000000000..508cf3ea381b
--- /dev/null
+++ b/packages/vinext/tsconfig.test.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*", "vite.config.ts"],
+
+ "compilerOptions": {
+ "types": ["node"]
+ }
+}
diff --git a/packages/vinext/tsconfig.types.json b/packages/vinext/tsconfig.types.json
new file mode 100644
index 000000000000..65455f66bd75
--- /dev/null
+++ b/packages/vinext/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/types"
+ }
+}
diff --git a/packages/vinext/vite.config.ts b/packages/vinext/vite.config.ts
new file mode 100644
index 000000000000..1094fe0d79da
--- /dev/null
+++ b/packages/vinext/vite.config.ts
@@ -0,0 +1,9 @@
+import baseConfig from '../../vite/vite.config';
+
+export default {
+ ...baseConfig,
+ test: {
+ ...baseConfig.test,
+ environment: 'jsdom',
+ },
+};
diff --git a/yarn.lock b/yarn.lock
index ac89a4468d6a..b8f4ff6db1a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25411,6 +25411,13 @@ react-dom@^18.3.1:
loose-envify "^1.1.0"
scheduler "^0.23.2"
+react-dom@^19.0.0:
+ version "19.2.4"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591"
+ integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==
+ dependencies:
+ scheduler "^0.27.0"
+
react-error-boundary@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.1.tgz#932c5ca5cbab8ec4fe37fd7b415aa5c3a47597e7"
@@ -25516,6 +25523,11 @@ react@^18.3.1:
dependencies:
loose-envify "^1.1.0"
+react@^19.0.0:
+ version "19.2.4"
+ resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a"
+ integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==
+
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@@ -26653,6 +26665,11 @@ scheduler@^0.23.2:
dependencies:
loose-envify "^1.1.0"
+scheduler@^0.27.0:
+ version "0.27.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
+ integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
+
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"