diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 73e5a1c6ee3c..511299fd3e3e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1037,6 +1037,7 @@ jobs:
retention-days: 7
- name: Pre-process E2E Test Dumps
+ if: always()
run: |
node ./scripts/normalize-e2e-test-dump-transaction-events.js
@@ -1193,6 +1194,7 @@ jobs:
run: pnpm ${{ matrix.assert-command || 'test:assert' }}
- name: Pre-process E2E Test Dumps
+ if: always()
run: |
node ./scripts/normalize-e2e-test-dump-transaction-events.js
diff --git a/.size-limit.js b/.size-limit.js
index bdfe8a4397e2..75545fd89194 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -224,7 +224,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
- limit: '39 KB',
+ limit: '39.1 KB',
},
// SvelteKit SDK (ESM)
{
diff --git a/dev-packages/e2e-tests/publish-packages.ts b/dev-packages/e2e-tests/publish-packages.ts
index 408d046977a2..4f2cc4056826 100644
--- a/dev-packages/e2e-tests/publish-packages.ts
+++ b/dev-packages/e2e-tests/publish-packages.ts
@@ -12,6 +12,8 @@ const packageTarballPaths = glob.sync('packages/*/sentry-*.tgz', {
// Publish built packages to the fake registry
packageTarballPaths.forEach(tarballPath => {
+ // eslint-disable-next-line no-console
+ console.log(`Publishing tarball ${tarballPath} ...`);
// `--userconfig` flag needs to be before `publish`
childProcess.exec(
`npm --userconfig ${__dirname}/test-registry.npmrc publish ${tarballPath}`,
@@ -19,14 +21,10 @@ packageTarballPaths.forEach(tarballPath => {
cwd: repositoryRoot, // Can't use __dirname here because npm would try to publish `@sentry-internal/e2e-tests`
encoding: 'utf8',
},
- (err, stdout, stderr) => {
- // eslint-disable-next-line no-console
- console.log(stdout);
- // eslint-disable-next-line no-console
- console.log(stderr);
+ err => {
if (err) {
// eslint-disable-next-line no-console
- console.error(err);
+ console.error(`Error publishing tarball ${tarballPath}`, err);
process.exit(1);
}
},
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
index 5be9ecbfc32c..3e7a0ac88266 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
@@ -17,7 +17,7 @@
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "13.2.0",
+ "next": "13.5.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/customPageExtension.page.tsx
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx
rename to dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/customPageExtension.page.tsx
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/error-getServerSideProps.tsx
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx
rename to dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/error-getServerSideProps.tsx
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts
index 8c74b2c99427..af59b41c2908 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts
@@ -46,3 +46,38 @@ test('should create a pageload transaction when the `pages` directory is used',
type: 'transaction',
});
});
+
+test('should create a pageload transaction with correct name when an error occurs in getServerSideProps', async ({
+ page,
+}) => {
+ const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/[param]/error-getServerSideProps' &&
+ transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/something/error-getServerSideProps`, { waitUntil: 'networkidle' });
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.pages_router_instrumentation',
+ },
+ },
+ transaction: '/[param]/error-getServerSideProps',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+
+ // Ensure the transaction name is not '/_error'
+ expect(transaction.transaction).not.toBe('/_error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts
index 22da2071d533..570b19b3271d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts
@@ -11,7 +11,7 @@ test('should propagate serverside `getInitialProps` trace to client', async ({ p
const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
return (
- transactionEvent.transaction === '/[param]/withInitialProps' &&
+ transactionEvent.transaction === 'GET /[param]/withInitialProps' &&
transactionEvent.contexts?.trace?.op === 'http.server'
);
});
@@ -47,7 +47,7 @@ test('should propagate serverside `getInitialProps` trace to client', async ({ p
status: 'ok',
},
},
- transaction: '/[param]/withInitialProps',
+ transaction: 'GET /[param]/withInitialProps',
transaction_info: {
source: 'route',
},
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts
index 20bbbc9437f6..765864dbf4a1 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts
@@ -11,7 +11,7 @@ test('Should record performance for getServerSideProps', async ({ page }) => {
const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
return (
- transactionEvent.transaction === '/[param]/withServerSideProps' &&
+ transactionEvent.transaction === 'GET /[param]/withServerSideProps' &&
transactionEvent.contexts?.trace?.op === 'http.server'
);
});
@@ -47,7 +47,7 @@ test('Should record performance for getServerSideProps', async ({ page }) => {
status: 'ok',
},
},
- transaction: '/[param]/withServerSideProps',
+ transaction: 'GET /[param]/withServerSideProps',
transaction_info: {
source: 'route',
},
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts
index 63082fee6e07..2d3854e2a2a4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts
@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-test('should not automatically create transactions for routes that were excluded from auto wrapping (string)', async ({
+test('should not apply build-time instrumentation for routes that were excluded from auto wrapping (string)', async ({
request,
}) => {
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
@@ -13,17 +13,13 @@ test('should not automatically create transactions for routes that were excluded
expect(await (await request.get(`/api/endpoint-excluded-with-string`)).text()).toBe('{"success":true}');
- let transactionPromiseReceived = false;
- transactionPromise.then(() => {
- transactionPromiseReceived = true;
- });
-
- await new Promise(resolve => setTimeout(resolve, 5_000));
+ const transaction = await transactionPromise;
- expect(transactionPromiseReceived).toBe(false);
+ expect(transaction.contexts?.trace?.data?.['sentry.origin']).toBeDefined();
+ expect(transaction.contexts?.trace?.data?.['sentry.origin']).not.toBe('auto.http.nextjs'); // This is the origin set by the build time instrumentation
});
-test('should not automatically create transactions for routes that were excluded from auto wrapping (regex)', async ({
+test('should not apply build-time instrumentation for routes that were excluded from auto wrapping (regex)', async ({
request,
}) => {
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
@@ -35,12 +31,8 @@ test('should not automatically create transactions for routes that were excluded
expect(await (await request.get(`/api/endpoint-excluded-with-regex`)).text()).toBe('{"success":true}');
- let transactionPromiseReceived = false;
- transactionPromise.then(() => {
- transactionPromiseReceived = true;
- });
-
- await new Promise(resolve => setTimeout(resolve, 5_000));
+ const transaction = await transactionPromise;
- expect(transactionPromiseReceived).toBe(false);
+ expect(transaction.contexts?.trace?.data?.['sentry.origin']).toBeDefined();
+ expect(transaction.contexts?.trace?.data?.['sentry.origin']).not.toBe('auto.http.nextjs'); // This is the origin set by the build time instrumentation
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts
index 0c99ba302dfa..9ae79d7bd4b0 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts
@@ -8,12 +8,12 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
return (
- transactionEvent.transaction === '/error-getServerSideProps' &&
+ transactionEvent.transaction === 'GET /[param]/error-getServerSideProps' &&
transactionEvent.contexts?.trace?.op === 'http.server'
);
});
- await page.goto('/error-getServerSideProps');
+ await page.goto('/dogsaregreat/error-getServerSideProps');
expect(await errorEventPromise).toMatchObject({
contexts: {
@@ -40,7 +40,7 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
url: expect.stringMatching(/^http.*\/error-getServerSideProps/),
},
timestamp: expect.any(Number),
- transaction: 'getServerSideProps (/error-getServerSideProps)',
+ transaction: 'getServerSideProps (/[param]/error-getServerSideProps)',
});
expect(await transactionEventPromise).toMatchObject({
@@ -60,11 +60,11 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
data: {
'http.response.status_code': 500,
'sentry.op': 'http.server',
- 'sentry.origin': 'auto.function.nextjs',
+ 'sentry.origin': 'auto',
'sentry.source': 'route',
},
op: 'http.server',
- origin: 'auto.function.nextjs',
+ origin: 'auto',
span_id: expect.any(String),
status: 'internal_error',
trace_id: expect.any(String),
@@ -80,7 +80,7 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
},
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
- transaction: '/error-getServerSideProps',
+ transaction: 'GET /[param]/error-getServerSideProps',
transaction_info: { source: 'route' },
type: 'transaction',
});
@@ -95,11 +95,12 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
return (
- transactionEvent.transaction === '/customPageExtension' && transactionEvent.contexts?.trace?.op === 'http.server'
+ transactionEvent.transaction === 'GET /[param]/customPageExtension' &&
+ transactionEvent.contexts?.trace?.op === 'http.server'
);
});
- await page.goto('/customPageExtension');
+ await page.goto('/123/customPageExtension');
expect(await errorEventPromise).toMatchObject({
contexts: {
@@ -126,7 +127,7 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
url: expect.stringMatching(/^http.*\/customPageExtension/),
},
timestamp: expect.any(Number),
- transaction: 'getServerSideProps (/customPageExtension)',
+ transaction: 'getServerSideProps (/[param]/customPageExtension)',
});
expect(await transactionEventPromise).toMatchObject({
@@ -146,11 +147,11 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
data: {
'http.response.status_code': 500,
'sentry.op': 'http.server',
- 'sentry.origin': 'auto.function.nextjs',
+ 'sentry.origin': 'auto',
'sentry.source': 'route',
},
op: 'http.server',
- origin: 'auto.function.nextjs',
+ origin: 'auto',
span_id: expect.any(String),
status: 'internal_error',
trace_id: expect.any(String),
@@ -166,7 +167,7 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
},
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
- transaction: '/customPageExtension',
+ transaction: 'GET /[param]/customPageExtension',
transaction_info: { source: 'route' },
type: 'transaction',
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx
index d2aae8c9cd8d..006a01fcfa76 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx
@@ -9,31 +9,49 @@ export default function Layout({ children }: { children: React.ReactNode }) {
Layout (/)
-
- /
+
+ /
+
-
- /client-component
+
+ /client-component
+
-
- /client-component/parameter/42
+
+ /client-component/parameter/42
+
-
- /client-component/parameter/foo/bar/baz
+
+ /client-component/parameter/foo/bar/baz
+
-
- /server-component
+
+ /server-component
+
-
- /server-component/parameter/42
+
+ /server-component/parameter/42
+
-
- /server-component/parameter/foo/bar/baz
+
+ /server-component/parameter/foo/bar/baz
+
-
- /not-found
+
+ /not-found
+
-
- /redirect
+
+ /redirect
+
{children}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts
index 6096fcfb1493..abc565f438b4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts
@@ -20,5 +20,5 @@ export async function middleware(request: NextRequest) {
// See "Matching Paths" below to learn more
export const config = {
- matcher: ['/api/endpoint-behind-middleware'],
+ matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
index 6dc023fdf1ed..d6a129f9e056 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts
@@ -7,21 +7,22 @@ export const config = {
export default async function handler() {
// Without a working async context strategy the two spans created by `Sentry.startSpan()` would be nested.
- const outerSpanPromise = Sentry.withIsolationScope(() => {
- return Sentry.startSpan({ name: 'outer-span' }, () => {
- return new Promise(resolve => setTimeout(resolve, 300));
- });
+ const outerSpanPromise = Sentry.startSpan({ name: 'outer-span' }, () => {
+ return new Promise(resolve => setTimeout(resolve, 300));
});
- setTimeout(() => {
- Sentry.withIsolationScope(() => {
- return Sentry.startSpan({ name: 'inner-span' }, () => {
+ const innerSpanPromise = new Promise(resolve => {
+ setTimeout(() => {
+ Sentry.startSpan({ name: 'inner-span' }, () => {
return new Promise(resolve => setTimeout(resolve, 100));
+ }).then(() => {
+ resolve();
});
- });
- }, 100);
+ }, 100);
+ });
await outerSpanPromise;
+ await innerSpanPromise;
return new Response('ok', { status: 200 });
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts
new file mode 100644
index 000000000000..2ca75a33ba7e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts
@@ -0,0 +1,9 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+type Data = {
+ name: string;
+};
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ res.status(200).json({ name: 'John Doe' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
index ecce719f0656..cb92cb2bab49 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts
@@ -3,7 +3,10 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
test('Should allow for async context isolation in the edge SDK', async ({ request }) => {
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint';
+ return (
+ transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
await request.get('/api/async-context-edge-endpoint');
@@ -13,8 +16,5 @@ test('Should allow for async context isolation in the edge SDK', async ({ reques
const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span');
const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span');
- // @ts-expect-error parent_span_id exists
- expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id);
- // @ts-expect-error parent_span_id exists
- expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id);
+ expect(outerSpan?.parent_span_id).toStrictEqual(innerSpan?.parent_span_id);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts
index 35984640bcf6..abfe9b323d0f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts
@@ -42,9 +42,7 @@ test('Creates a navigation transaction for app router routes', async ({ page })
// It seems to differ between Next.js versions whether the route is parameterized or not
(transactionEvent?.transaction === 'GET /server-component/parameter/foo/bar/baz' ||
transactionEvent?.transaction === 'GET /server-component/parameter/[...parameters]') &&
- transactionEvent.contexts?.trace?.data?.['http.target'].startsWith('/server-component/parameter/foo/bar/baz') &&
- (await clientNavigationTransactionPromise).contexts?.trace?.trace_id ===
- transactionEvent.contexts?.trace?.trace_id
+ transactionEvent.contexts?.trace?.data?.['http.target'].startsWith('/server-component/parameter/foo/bar/baz')
);
});
@@ -52,6 +50,10 @@ test('Creates a navigation transaction for app router routes', async ({ page })
expect(await clientNavigationTransactionPromise).toBeDefined();
expect(await serverComponentTransactionPromise).toBeDefined();
+
+ expect((await serverComponentTransactionPromise).contexts?.trace?.trace_id).toBe(
+ (await clientNavigationTransactionPromise).contexts?.trace?.trace_id,
+ );
});
test('Creates a navigation transaction for `router.push()`', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
index df7ce7afd19a..88460e3ab533 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts
@@ -4,7 +4,8 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for edge routes', async ({ request }) => {
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
- transactionEvent?.transaction === 'GET /api/edge-endpoint' && transactionEvent?.contexts?.trace?.status === 'ok'
+ transactionEvent?.transaction === 'GET /api/edge-endpoint' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
);
});
@@ -19,47 +20,42 @@ test('Should create a transaction for edge routes', async ({ request }) => {
expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok');
expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
- expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value');
});
-test('Should create a transaction with error status for faulty edge routes', async ({ request }) => {
+test('Faulty edge routes', async ({ request }) => {
const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
transactionEvent?.transaction === 'GET /api/error-edge-endpoint' &&
- transactionEvent?.contexts?.trace?.status === 'internal_error'
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
);
});
- request.get('/api/error-edge-endpoint').catch(() => {
- // Noop
- });
-
- const edgerouteTransaction = await edgerouteTransactionPromise;
-
- expect(edgerouteTransaction.contexts?.trace?.status).toBe('internal_error');
- expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
- expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge');
-
- // Assert that isolation scope works properly
- expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
- expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
-});
-
-test('Should record exceptions for faulty edge routes', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error';
+ return (
+ errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error' &&
+ errorEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
request.get('/api/error-edge-endpoint').catch(() => {
// Noop
});
- const errorEvent = await errorEventPromise;
+ const [edgerouteTransaction, errorEvent] = await Promise.all([
+ test.step('should create a transaction', () => edgerouteTransactionPromise),
+ test.step('should create an error event', () => errorEventPromise),
+ ]);
- // Assert that isolation scope works properly
- expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
- expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ test.step('should create transactions with the right fields', () => {
+ expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error');
+ expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server');
+ });
- expect(errorEvent.transaction).toBe('GET /api/error-edge-endpoint');
+ test.step('should have scope isolation', () => {
+ expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true);
+ expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
+ expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ });
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
index f5277dee6f66..934cfa2e472d 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts
@@ -1,6 +1,8 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+const packageJson = require('../package.json');
+
test('Should record exceptions for faulty edge server components', async ({ page }) => {
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Edge Server Component Error';
@@ -20,8 +22,14 @@ test('Should record exceptions for faulty edge server components', async ({ page
});
test('Should record transaction for edge server components', async ({ page }) => {
+ const nextjsVersion = packageJson.dependencies.next;
+ const nextjsMajor = Number(nextjsVersion.split('.')[0]);
+
const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'Page Server Component (/edge-server-components)';
+ return (
+ transactionEvent?.transaction === 'GET /edge-server-components' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
await page.goto('/edge-server-components');
@@ -29,9 +37,14 @@ test('Should record transaction for edge server components', async ({ page }) =>
const serverComponentTransaction = await serverComponentTransactionPromise;
expect(serverComponentTransaction).toBeDefined();
- expect(serverComponentTransaction.request?.headers).toBeDefined();
+ expect(serverComponentTransaction.contexts?.trace?.op).toBe('http.server');
- // Assert that isolation scope works properly
- expect(serverComponentTransaction.tags?.['my-isolated-tag']).toBe(true);
- expect(serverComponentTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ // For some reason headers aren't picked up on Next.js 13 - also causing scope isolation to be broken
+ if (nextjsMajor >= 14) {
+ expect(serverComponentTransaction.request?.headers).toBeDefined();
+
+ // Assert that isolation scope works properly
+ expect(serverComponentTransaction.tags?.['my-isolated-tag']).toBe(true);
+ expect(serverComponentTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ }
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
index 11a5f48799bd..a00a29672ed6 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts
@@ -3,7 +3,7 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok';
+ return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware';
});
const response = await request.get('/api/endpoint-behind-middleware');
@@ -12,53 +12,50 @@ test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransaction = await middlewareTransactionPromise;
expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
- expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ expect(middlewareTransaction.transaction_info?.source).toBe('url');
// Assert that isolation scope works properly
expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
});
-test('Should create a transaction with error status for faulty middleware', async ({ request }) => {
+test('Faulty middlewares', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return (
- transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'internal_error'
- );
+ return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-faulty-middleware';
});
- request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
- // Noop
- });
-
- const middlewareTransaction = await middlewareTransactionPromise;
-
- expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error');
- expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs');
- expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
-});
-
-test('Records exceptions happening in middleware', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
});
- request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
+ request.get('/api/endpoint-behind-faulty-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
// Noop
});
- const errorEvent = await errorEventPromise;
+ await test.step('should record transactions', async () => {
+ const middlewareTransaction = await middlewareTransactionPromise;
+ expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
+ expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ expect(middlewareTransaction.transaction_info?.source).toBe('url');
+ });
- // Assert that isolation scope works properly
- expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
- expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
- expect(errorEvent.transaction).toBe('middleware');
+ await test.step('should record exceptions', async () => {
+ const errorEvent = await errorEventPromise;
+
+ // Assert that isolation scope works properly
+ expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
+ expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ expect(errorEvent.transaction).toBe('middleware GET /api/endpoint-behind-faulty-middleware');
+ });
});
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
- transactionEvent?.transaction === 'middleware' &&
+ transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' &&
!!transactionEvent.spans?.find(span => span.op === 'http.client')
);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts
index a67e4328ba1c..10a4cd77f111 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts
@@ -8,7 +8,7 @@ test('Will capture error for SSR rendering error with a connected trace (Class C
const serverComponentTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
- transactionEvent?.transaction === '/pages-router/ssr-error-class' &&
+ transactionEvent?.transaction === 'GET /pages-router/ssr-error-class' &&
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
);
});
@@ -26,7 +26,7 @@ test('Will capture error for SSR rendering error with a connected trace (Functio
const ssrTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
return (
- transactionEvent?.transaction === '/pages-router/ssr-error-fc' &&
+ transactionEvent?.transaction === 'GET /pages-router/ssr-error-fc' &&
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
);
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts
index afa02e60884a..7e6dc5fbe300 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts
@@ -54,9 +54,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn
expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true);
expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
- expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error');
+ expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
- expect(routehandlerTransaction.contexts?.trace?.origin).toBe('auto.function.nextjs');
+ expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto');
expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error');
@@ -66,7 +66,10 @@ test('Should record exceptions and transactions for faulty route handlers', asyn
test.describe('Edge runtime', () => {
test('should create a transaction for route handlers', async ({ request }) => {
const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge';
+ return (
+ transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
const response = await request.patch('/route-handlers/bar/edge');
@@ -80,11 +83,17 @@ test.describe('Edge runtime', () => {
test('should record exceptions and transactions for faulty route handlers', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => {
- return errorEvent?.exception?.values?.[0]?.value === 'route-handler-edge-error';
+ return (
+ errorEvent?.exception?.values?.[0]?.value === 'route-handler-edge-error' &&
+ errorEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'DELETE /route-handlers/[param]/edge';
+ return (
+ transactionEvent?.transaction === 'DELETE /route-handlers/[param]/edge' &&
+ transactionEvent.contexts?.runtime?.name === 'vercel-edge'
+ );
});
await request.delete('/route-handlers/baz/edge').catch(() => {
@@ -100,12 +109,10 @@ test.describe('Edge runtime', () => {
expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true);
expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
- expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error');
+ expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
- expect(routehandlerTransaction.contexts?.runtime?.name).toBe('vercel-edge');
expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-edge-error');
- expect(routehandlerError.contexts?.runtime?.name).toBe('vercel-edge');
expect(routehandlerError.transaction).toBe('DELETE /route-handlers/[param]/edge');
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
index 49afe791328f..75f30075a47f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
@@ -16,7 +16,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => {
expect(transactionEvent.contexts?.trace).toEqual({
data: expect.objectContaining({
'sentry.op': 'http.server',
- 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.origin': 'auto',
'sentry.sample_rate': 1,
'sentry.source': 'route',
'http.method': 'GET',
@@ -27,7 +27,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => {
'otel.kind': 'SERVER',
}),
op: 'http.server',
- origin: 'auto.http.otel.http',
+ origin: 'auto',
span_id: expect.any(String),
status: 'ok',
trace_id: expect.any(String),
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
index 8d2489bab34d..278b6b1074eb 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts
@@ -76,6 +76,34 @@ test('Should send a transaction for instrumented server actions', async ({ page
expect(Object.keys(transactionEvent.request?.headers || {}).length).toBeGreaterThan(0);
});
+test('Should send a wrapped server action as a child of a nextjs transaction', async ({ page }) => {
+ const nextjsVersion = packageJson.dependencies.next;
+ const nextjsMajor = Number(nextjsVersion.split('.')[0]);
+ test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14');
+ test.skip(process.env.TEST_ENV === 'development', 'this magically only works in production');
+
+ const nextjsPostTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'POST /server-action' && transactionEvent.contexts?.trace?.origin === 'auto'
+ );
+ });
+
+ const serverActionTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
+ return transactionEvent?.transaction === 'serverAction/myServerAction';
+ });
+
+ await page.goto('/server-action');
+ await page.getByText('Run Action').click();
+
+ const nextjsTransaction = await nextjsPostTransactionPromise;
+ const serverActionTransaction = await serverActionTransactionPromise;
+
+ expect(nextjsTransaction).toBeDefined();
+ expect(serverActionTransaction).toBeDefined();
+
+ expect(nextjsTransaction.contexts?.trace?.span_id).toBe(serverActionTransaction.contexts?.trace?.parent_span_id);
+});
+
test('Should set not_found status for server actions calling notFound()', async ({ page }) => {
const nextjsVersion = packageJson.dependencies.next;
const nextjsMajor = Number(nextjsVersion.split('.')[0]);
diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs
index 1a855e5674b7..4e6483364ee4 100644
--- a/dev-packages/rollup-utils/npmHelpers.mjs
+++ b/dev-packages/rollup-utils/npmHelpers.mjs
@@ -36,6 +36,7 @@ export function makeBaseNPMConfig(options = {}) {
packageSpecificConfig = {},
addPolyfills = true,
sucrase = {},
+ bundledBuiltins = [],
} = options;
const nodeResolvePlugin = makeNodeResolvePlugin();
@@ -113,7 +114,7 @@ export function makeBaseNPMConfig(options = {}) {
// don't include imported modules from outside the package in the final output
external: [
- ...builtinModules,
+ ...builtinModules.filter(m => !bundledBuiltins.includes(m)),
...Object.keys(packageDotJSON.dependencies || {}),
...Object.keys(packageDotJSON.peerDependencies || {}),
...Object.keys(packageDotJSON.optionalDependencies || {}),
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 8dd13919442a..23047338e280 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -76,6 +76,7 @@
"access": "public"
},
"dependencies": {
+ "@opentelemetry/api": "^1.9.0",
"@opentelemetry/instrumentation-http": "0.53.0",
"@opentelemetry/semantic-conventions": "^1.27.0",
"@rollup/plugin-commonjs": "26.0.1",
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index c66f50a293f2..c50bbce37305 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -13,7 +13,7 @@ import { applyTunnelRouteOption } from './tunnelRoute';
export * from '@sentry/react';
-export { captureUnderscoreErrorException } from '../common/_error';
+export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesAssetPrefixPath__: string;
diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts
index f6906a566050..2380b743cced 100644
--- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts
+++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts
@@ -6,7 +6,7 @@ import {
} from '@sentry/core';
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
import type { Client, TransactionSource } from '@sentry/types';
-import { browserPerformanceTimeOrigin, logger, stripUrlQueryAndFragment } from '@sentry/utils';
+import { browserPerformanceTimeOrigin, logger, parseBaggageHeader, stripUrlQueryAndFragment } from '@sentry/utils';
import type { NEXT_DATA } from 'next/dist/shared/lib/utils';
import RouterImport from 'next/router';
@@ -106,7 +106,15 @@ function extractNextDataTagInformation(): NextDataTagInfo {
*/
export function pagesRouterInstrumentPageLoad(client: Client): void {
const { route, params, sentryTrace, baggage } = extractNextDataTagInformation();
- const name = route || globalObject.location.pathname;
+ const parsedBaggage = parseBaggageHeader(baggage);
+ let name = route || globalObject.location.pathname;
+
+ // /_error is the fallback page for all errors. If there is a transaction name for /_error, use that instead
+ if (parsedBaggage && parsedBaggage['sentry-transaction'] && name === '/_error') {
+ name = parsedBaggage['sentry-transaction'];
+ // Strip any HTTP method from the span name
+ name = name.replace(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\s+/i, '');
+ }
startBrowserTracingPageLoadSpan(
client,
diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts
index 354113637a30..7740c35c016c 100644
--- a/packages/nextjs/src/common/index.ts
+++ b/packages/nextjs/src/common/index.ts
@@ -1,14 +1,14 @@
-export { wrapGetStaticPropsWithSentry } from './wrapGetStaticPropsWithSentry';
-export { wrapGetInitialPropsWithSentry } from './wrapGetInitialPropsWithSentry';
-export { wrapAppGetInitialPropsWithSentry } from './wrapAppGetInitialPropsWithSentry';
-export { wrapDocumentGetInitialPropsWithSentry } from './wrapDocumentGetInitialPropsWithSentry';
-export { wrapErrorGetInitialPropsWithSentry } from './wrapErrorGetInitialPropsWithSentry';
-export { wrapGetServerSidePropsWithSentry } from './wrapGetServerSidePropsWithSentry';
+export { wrapGetStaticPropsWithSentry } from './pages-router-instrumentation/wrapGetStaticPropsWithSentry';
+export { wrapGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapGetInitialPropsWithSentry';
+export { wrapAppGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapAppGetInitialPropsWithSentry';
+export { wrapDocumentGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry';
+export { wrapErrorGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry';
+export { wrapGetServerSidePropsWithSentry } from './pages-router-instrumentation/wrapGetServerSidePropsWithSentry';
export { wrapServerComponentWithSentry } from './wrapServerComponentWithSentry';
export { wrapRouteHandlerWithSentry } from './wrapRouteHandlerWithSentry';
-export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons';
+export { wrapApiHandlerWithSentryVercelCrons } from './pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons';
export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry';
-export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry';
+export { wrapPageComponentWithSentry } from './pages-router-instrumentation/wrapPageComponentWithSentry';
export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry';
export { withServerActionInstrumentation } from './withServerActionInstrumentation';
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts
similarity index 97%
rename from packages/nextjs/src/common/_error.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/_error.ts
index 385df8244a17..3450aad8ef5e 100644
--- a/packages/nextjs/src/common/_error.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts
@@ -1,7 +1,7 @@
import { captureException, withScope } from '@sentry/core';
import { vercelWaitUntil } from '@sentry/utils';
import type { NextPageContext } from 'next';
-import { flushSafelyWithTimeout } from './utils/responseEnd';
+import { flushSafelyWithTimeout } from '../utils/responseEnd';
type ContextOrProps = {
req?: NextPageContext['req'];
diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
similarity index 93%
rename from packages/nextjs/src/common/wrapApiHandlerWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
index a6463b0a7791..30fce67e482e 100644
--- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
@@ -1,4 +1,5 @@
import {
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
captureException,
continueTrace,
@@ -6,12 +7,13 @@ import {
startSpanManual,
withIsolationScope,
} from '@sentry/core';
-import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
-import { isString, logger, objectify, vercelWaitUntil } from '@sentry/utils';
+import { isString, logger, objectify } from '@sentry/utils';
+
+import { vercelWaitUntil } from '@sentry/utils';
import type { NextApiRequest } from 'next';
-import type { AugmentedNextApiResponse, NextApiHandler } from './types';
-import { flushSafelyWithTimeout } from './utils/responseEnd';
-import { escapeNextjsTracing } from './utils/tracingUtils';
+import type { AugmentedNextApiResponse, NextApiHandler } from '../types';
+import { flushSafelyWithTimeout } from '../utils/responseEnd';
+import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';
export type AugmentedNextApiRequest = NextApiRequest & {
__withSentry_applied__?: boolean;
@@ -32,6 +34,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
thisArg,
args: [AugmentedNextApiRequest | undefined, AugmentedNextApiResponse | undefined],
) => {
+ dropNextjsRootContext();
return escapeNextjsTracing(() => {
const [req, res] = args;
diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts
similarity index 96%
rename from packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts
index 4974cd827e9a..5ca29b338cda 100644
--- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts
@@ -1,7 +1,7 @@
import { captureCheckIn } from '@sentry/core';
import type { NextApiRequest } from 'next';
-import type { VercelCronsConfig } from './types';
+import type { VercelCronsConfig } from '../types';
type EdgeRequest = {
nextUrl: URL;
@@ -9,7 +9,7 @@ type EdgeRequest = {
};
/**
- * Wraps a function with Sentry crons instrumentation by automaticaly sending check-ins for the given Vercel crons config.
+ * Wraps a function with Sentry crons instrumentation by automatically sending check-ins for the given Vercel crons config.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function wrapApiHandlerWithSentryVercelCrons any>(
diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts
similarity index 97%
rename from packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts
index 2c7b0adc7d7b..10f783b9e9e6 100644
--- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts
@@ -1,7 +1,7 @@
import type App from 'next/app';
-import { isBuild } from './utils/isBuild';
-import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
+import { isBuild } from '../utils/isBuild';
+import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils';
type AppGetInitialProps = (typeof App)['getInitialProps'];
diff --git a/packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry.ts
similarity index 96%
rename from packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry.ts
index 192e70f093b1..d7f69c621132 100644
--- a/packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry.ts
@@ -1,7 +1,7 @@
import type Document from 'next/document';
-import { isBuild } from './utils/isBuild';
-import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
+import { isBuild } from '../utils/isBuild';
+import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils';
type DocumentGetInitialProps = typeof Document.getInitialProps;
diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts
similarity index 97%
rename from packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts
index a2bd559342a4..731d3fe1e24a 100644
--- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts
@@ -1,8 +1,8 @@
import type { NextPageContext } from 'next';
import type { ErrorProps } from 'next/error';
-import { isBuild } from './utils/isBuild';
-import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
+import { isBuild } from '../utils/isBuild';
+import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils';
type ErrorGetInitialProps = (context: NextPageContext) => Promise;
diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts
similarity index 96%
rename from packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts
index 2624aefb4d24..97246ec9d122 100644
--- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts
@@ -1,7 +1,7 @@
import type { NextPage } from 'next';
-import { isBuild } from './utils/isBuild';
-import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
+import { isBuild } from '../utils/isBuild';
+import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils';
type GetInitialProps = Required['getInitialProps'];
diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts
similarity index 96%
rename from packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts
index 0037bad36300..7c4b4101d80e 100644
--- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts
@@ -1,7 +1,7 @@
import type { GetServerSideProps } from 'next';
-import { isBuild } from './utils/isBuild';
-import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
+import { isBuild } from '../utils/isBuild';
+import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils';
/**
* Create a wrapped version of the user's exported `getServerSideProps` function
diff --git a/packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts
similarity index 82%
rename from packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts
rename to packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts
index aebbf42ac684..5d083eb97ca8 100644
--- a/packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts
@@ -1,7 +1,7 @@
import type { GetStaticProps } from 'next';
-import { isBuild } from './utils/isBuild';
-import { callDataFetcherTraced, withErrorInstrumentation } from './utils/wrapperUtils';
+import { isBuild } from '../utils/isBuild';
+import { callDataFetcherTraced, withErrorInstrumentation } from '../utils/wrapperUtils';
type Props = { [key: string]: unknown };
@@ -14,7 +14,7 @@ type Props = { [key: string]: unknown };
*/
export function wrapGetStaticPropsWithSentry(
origGetStaticPropsa: GetStaticProps,
- parameterizedRoute: string,
+ _parameterizedRoute: string,
): GetStaticProps {
return new Proxy(origGetStaticPropsa, {
apply: async (wrappingTarget, thisArg, args: Parameters>) => {
@@ -23,10 +23,7 @@ export function wrapGetStaticPropsWithSentry(
}
const errorWrappedGetStaticProps = withErrorInstrumentation(wrappingTarget);
- return callDataFetcherTraced(errorWrappedGetStaticProps, args, {
- parameterizedRoute,
- dataFetchingMethodName: 'getStaticProps',
- });
+ return callDataFetcherTraced(errorWrappedGetStaticProps, args);
},
});
}
diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts
new file mode 100644
index 000000000000..8b6a45faa63b
--- /dev/null
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts
@@ -0,0 +1,91 @@
+import { captureException, getCurrentScope, withIsolationScope } from '@sentry/core';
+import { extractTraceparentData } from '@sentry/utils';
+
+interface FunctionComponent {
+ (...args: unknown[]): unknown;
+}
+
+interface ClassComponent {
+ new (...args: unknown[]): {
+ props?: unknown;
+ render(...args: unknown[]): unknown;
+ };
+}
+
+function isReactClassComponent(target: unknown): target is ClassComponent {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ return typeof target === 'function' && target?.prototype?.isReactComponent;
+}
+
+/**
+ * Wraps a page component with Sentry error instrumentation.
+ */
+export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | ClassComponent): unknown {
+ if (isReactClassComponent(pageComponent)) {
+ return class SentryWrappedPageComponent extends pageComponent {
+ public render(...args: unknown[]): unknown {
+ return withIsolationScope(() => {
+ const scope = getCurrentScope();
+ // We extract the sentry trace data that is put in the component props by datafetcher wrappers
+ const sentryTraceData =
+ typeof this.props === 'object' &&
+ this.props !== null &&
+ '_sentryTraceData' in this.props &&
+ typeof this.props._sentryTraceData === 'string'
+ ? this.props._sentryTraceData
+ : undefined;
+
+ if (sentryTraceData) {
+ const traceparentData = extractTraceparentData(sentryTraceData);
+ scope.setContext('trace', {
+ span_id: traceparentData?.parentSpanId,
+ trace_id: traceparentData?.traceId,
+ });
+ }
+
+ try {
+ return super.render(...args);
+ } catch (e) {
+ captureException(e, {
+ mechanism: {
+ handled: false,
+ },
+ });
+ throw e;
+ }
+ });
+ }
+ };
+ } else if (typeof pageComponent === 'function') {
+ return new Proxy(pageComponent, {
+ apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) {
+ return withIsolationScope(() => {
+ const scope = getCurrentScope();
+ // We extract the sentry trace data that is put in the component props by datafetcher wrappers
+ const sentryTraceData = argArray?.[0]?._sentryTraceData;
+
+ if (sentryTraceData) {
+ const traceparentData = extractTraceparentData(sentryTraceData);
+ scope.setContext('trace', {
+ span_id: traceparentData?.parentSpanId,
+ trace_id: traceparentData?.traceId,
+ });
+ }
+
+ try {
+ return target.apply(thisArg, argArray);
+ } catch (e) {
+ captureException(e, {
+ mechanism: {
+ handled: false,
+ },
+ });
+ throw e;
+ }
+ });
+ },
+ });
+ } else {
+ return pageComponent;
+ }
+}
diff --git a/packages/nextjs/src/common/span-attributes-with-logic-attached.ts b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts
new file mode 100644
index 000000000000..a272ef525dff
--- /dev/null
+++ b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts
@@ -0,0 +1,8 @@
+/**
+ * If this attribute is attached to a transaction, the Next.js SDK will drop that transaction.
+ */
+export const TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION = 'sentry.drop_transaction';
+
+export const TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL = 'sentry.sentry_trace_backfill';
+
+export const TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL = 'sentry.route_backfill';
diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
deleted file mode 100644
index 5eed59aca0a3..000000000000
--- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- SPAN_STATUS_OK,
- captureException,
- continueTrace,
- handleCallbackErrors,
- setHttpStatus,
- startSpan,
- withIsolationScope,
-} from '@sentry/core';
-import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils';
-
-import type { EdgeRouteHandler } from '../../edge/types';
-import { flushSafelyWithTimeout } from './responseEnd';
-import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils';
-
-/**
- * Wraps a function on the edge runtime with error and performance monitoring.
- */
-export function withEdgeWrapping(
- handler: H,
- options: { spanDescription: string; spanOp: string; mechanismFunctionName: string },
-): (...params: Parameters) => Promise> {
- return async function (this: unknown, ...args) {
- return escapeNextjsTracing(() => {
- const req: unknown = args[0];
- return withIsolationScope(commonObjectToIsolationScope(req), isolationScope => {
- let sentryTrace;
- let baggage;
-
- if (req instanceof Request) {
- sentryTrace = req.headers.get('sentry-trace') || '';
- baggage = req.headers.get('baggage');
-
- isolationScope.setSDKProcessingMetadata({
- request: winterCGRequestToRequestData(req),
- });
- }
-
- isolationScope.setTransactionName(options.spanDescription);
-
- return continueTrace(
- {
- sentryTrace,
- baggage,
- },
- () => {
- return startSpan(
- {
- name: options.spanDescription,
- op: options.spanOp,
- forceTransaction: true,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping',
- },
- },
- async span => {
- const handlerResult = await handleCallbackErrors(
- () => handler.apply(this, args),
- error => {
- captureException(error, {
- mechanism: {
- type: 'instrument',
- handled: false,
- data: {
- function: options.mechanismFunctionName,
- },
- },
- });
- },
- );
-
- if (handlerResult instanceof Response) {
- setHttpStatus(span, handlerResult.status);
- } else {
- span.setStatus({ code: SPAN_STATUS_OK });
- }
-
- return handlerResult;
- },
- );
- },
- ).finally(() => {
- vercelWaitUntil(flushSafelyWithTimeout());
- });
- });
- });
- };
-}
diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts
index b996b6af1877..ff57fcae3acc 100644
--- a/packages/nextjs/src/common/utils/tracingUtils.ts
+++ b/packages/nextjs/src/common/utils/tracingUtils.ts
@@ -1,7 +1,8 @@
-import { Scope, startNewTrace } from '@sentry/core';
+import { Scope, getActiveSpan, getRootSpan, spanToJSON, startNewTrace } from '@sentry/core';
import type { PropagationContext } from '@sentry/types';
import { GLOBAL_OBJ, logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
+import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';
const commonPropagationContextMap = new WeakMap