From bc782cab3df75d9d51e80c9c331c2769509efd84 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 09:06:01 +0000 Subject: [PATCH 01/13] feat(nextjs): Add client routing instrumentation for app router --- .../{performance.ts => pagesRouterRoutingInstrumentation.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/nextjs/src/client/{performance.ts => pagesRouterRoutingInstrumentation.ts} (100%) diff --git a/packages/nextjs/src/client/performance.ts b/packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts similarity index 100% rename from packages/nextjs/src/client/performance.ts rename to packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts From 491d6a78703107f7b9d9136c55efc602e0286c5d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 11:23:32 +0000 Subject: [PATCH 02/13] mvp --- .../client/appRouterRoutingInstrumentation.ts | 113 ++++++++++++++++++ packages/nextjs/src/client/index.ts | 31 ++++- .../pagesRouterRoutingInstrumentation.ts | 13 +- 3 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 packages/nextjs/src/client/appRouterRoutingInstrumentation.ts diff --git a/packages/nextjs/src/client/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/appRouterRoutingInstrumentation.ts new file mode 100644 index 000000000000..dc0cb476d5a1 --- /dev/null +++ b/packages/nextjs/src/client/appRouterRoutingInstrumentation.ts @@ -0,0 +1,113 @@ +import { WINDOW } from '@sentry/react'; +import type { HandlerDataFetch, Primitive, Transaction, TransactionContext } from '@sentry/types'; +import { addInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils'; + +type StartTransactionCb = (context: TransactionContext) => Transaction | undefined; + +const DEFAULT_TAGS = { + 'routing.instrumentation': 'next-app-router', +} as const; + +// We keep track of the active transaction so we can finish it when we start a navigation transaction. +let activeTransaction: Transaction | undefined = undefined; + +// We keep track of the previous location name so we can set the `from` field on navigation transactions. +// This is either a route or a pathname. +let prevLocationName: string | undefined = undefined; + +/** + * Creates routing instrumention for Next Router. Only supported for + * client side routing. Works for Next >= 10. + * + * Leverages the SingletonRouter from the `next/router` to + * generate pageload/navigation transactions and parameterize + * transaction names. + */ +export function appRouterInstrumentation( + startTransactionCb: StartTransactionCb, + startTransactionOnPageLoad: boolean = true, + startTransactionOnLocationChange: boolean = true, +): void { + prevLocationName = WINDOW.location.pathname; + + if (startTransactionOnPageLoad) { + activeTransaction = startTransactionCb({ + name: prevLocationName, + op: 'pageload', + tags: DEFAULT_TAGS, + // pageload should always start at timeOrigin (and needs to be in s, not ms) + startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, + metadata: { source: 'url' }, + }); + } + + if (startTransactionOnLocationChange) { + addInstrumentationHandler('fetch', (handlerData: HandlerDataFetch) => { + // The instrumentation handler is invoked twice - once for starting a request and once when the req finishes + // We can use the existence of the end-timestamp to filter out "finishing"-events. + if (handlerData.endTimestamp !== undefined) { + return; + } + + const parsedNavigatingRscRequest = parseNavigatingRscRequest(handlerData.args); + + if (parsedNavigatingRscRequest === null) { + return; + } + + const transactionName = parsedNavigatingRscRequest.targetPathname; + const tags: Record = { + ...DEFAULT_TAGS, + from: prevLocationName, + }; + + prevLocationName = transactionName; + + if (activeTransaction) { + activeTransaction.finish(); + } + + startTransactionCb({ + name: transactionName, + op: 'navigation', + tags, + metadata: { source: 'url' }, + }); + }); + } +} + +function parseNavigatingRscRequest(fetchArgs: unknown[]): null | { + targetPathname: string; +} { + // Make sure the first arg is a URL object + if (!fetchArgs[0] || typeof fetchArgs[0] !== 'object' || (fetchArgs[0] as URL).searchParams === undefined) { + return null; + } + + // Make sure the second argument is some kind of fetch config obj that contains headers + if (!fetchArgs[1] || typeof fetchArgs[1] !== 'object' || !('headers' in fetchArgs[1])) { + return null; + } + + try { + const url = fetchArgs[0] as URL; + const headers = fetchArgs[1].headers as Record; + + // Not an RSC request + if (headers['RSC'] !== '1') { + return null; + } + + // Prefetch requests are not navigating RSC requests + if (headers['Next-Router-Prefetch'] === '1') { + return null; + } + + return { + targetPathname: url.pathname, + }; + } catch { + return null; + } +} diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 9de007bd2e95..1f44d80906e0 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -7,19 +7,33 @@ import { defaultRequestInstrumentationOptions, init as reactInit, Integrations, + WINDOW, } from '@sentry/react'; import type { EventProcessor } from '@sentry/types'; -import { addOrUpdateIntegration } from '@sentry/utils'; +import { addOrUpdateIntegration, GLOBAL_OBJ } from '@sentry/utils'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; import { buildMetadata } from '../common/metadata'; -import { nextRouterInstrumentation } from './performance'; +import { appRouterInstrumentation } from './appRouterRoutingInstrumentation'; +import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation'; import { applyTunnelRouteOption } from './tunnelRoute'; export * from '@sentry/react'; -export { nextRouterInstrumentation } from './performance'; export { captureUnderscoreErrorException } from '../common/_error'; +export { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation'; + +/** + * Creates routing instrumention for Next Router. Only supported for + * client side routing. Works for Next >= 10. + * + * Leverages the SingletonRouter from the `next/router` to + * generate pageload/navigation transactions and parameterize + * transaction names. + * + * @deprecated Use pagesRouterInstrumentation instead. + */ +export const nextRouterInstrumentation = pagesRouterInstrumentation; export { Integrations }; @@ -38,8 +52,11 @@ export { BrowserTracing }; // Treeshakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; -const globalWithInjectedValues = global as typeof global & { +const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { __rewriteFramesAssetPrefixPath__: string; + next?: { + appDir?: boolean; + }; }; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ @@ -111,14 +128,16 @@ function addClientIntegrations(options: BrowserOptions): void { // will get treeshaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { if (hasTracingEnabled(options)) { + const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__'); + const defaultBrowserTracingIntegration = new BrowserTracing({ // eslint-disable-next-line deprecation/deprecation tracingOrigins: [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], - routingInstrumentation: nextRouterInstrumentation, + routingInstrumentation: isAppRouter ? appRouterInstrumentation : pagesRouterInstrumentation, }); integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, { - 'options.routingInstrumentation': nextRouterInstrumentation, + 'options.routingInstrumentation': isAppRouter ? appRouterInstrumentation : pagesRouterInstrumentation, }); } } diff --git a/packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts index bdf1509d9ec1..52c8569098c0 100644 --- a/packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts @@ -1,7 +1,12 @@ import { getCurrentHub } from '@sentry/core'; import { WINDOW } from '@sentry/react'; import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; -import { logger, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils'; +import { + browserPerformanceTimeOrigin, + logger, + stripUrlQueryAndFragment, + tracingContextFromHeaders, +} from '@sentry/utils'; import type { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils'; import { default as Router } from 'next/router'; import type { ParsedUrlQuery } from 'querystring'; @@ -86,7 +91,7 @@ function extractNextDataTagInformation(): NextDataTagInfo { } const DEFAULT_TAGS = { - 'routing.instrumentation': 'next-router', + 'routing.instrumentation': 'next-pages-router', } as const; // We keep track of the active transaction so we can finish it when we start a navigation transaction. @@ -106,7 +111,7 @@ const client = getCurrentHub().getClient(); * generate pageload/navigation transactions and parameterize * transaction names. */ -export function nextRouterInstrumentation( +export function pagesRouterInstrumentation( startTransactionCb: StartTransactionCb, startTransactionOnPageLoad: boolean = true, startTransactionOnLocationChange: boolean = true, @@ -126,6 +131,8 @@ export function nextRouterInstrumentation( name: prevLocationName, op: 'pageload', tags: DEFAULT_TAGS, + // pageload should always start at timeOrigin (and needs to be in s, not ms) + startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, ...(params && client && client.getOptions().sendDefaultPii && { data: params }), ...traceparentData, metadata: { From c4cae8ee05f544031063b6302fed5ff87d170cce Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 11:28:55 +0000 Subject: [PATCH 03/13] Restructuring --- packages/nextjs/src/client/index.ts | 24 ++++--------------- .../appRouterRoutingInstrumentation.ts | 14 +++++------ .../routing/nextRoutingInstrumentation.ts | 23 ++++++++++++++++++ .../pagesRouterRoutingInstrumentation.ts | 0 4 files changed, 33 insertions(+), 28 deletions(-) rename packages/nextjs/src/client/{ => routing}/appRouterRoutingInstrumentation.ts (88%) create mode 100644 packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts rename packages/nextjs/src/client/{ => routing}/pagesRouterRoutingInstrumentation.ts (100%) diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 1f44d80906e0..741a1a3b63ac 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -7,7 +7,6 @@ import { defaultRequestInstrumentationOptions, init as reactInit, Integrations, - WINDOW, } from '@sentry/react'; import type { EventProcessor } from '@sentry/types'; import { addOrUpdateIntegration, GLOBAL_OBJ } from '@sentry/utils'; @@ -15,25 +14,12 @@ import { addOrUpdateIntegration, GLOBAL_OBJ } from '@sentry/utils'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; import { buildMetadata } from '../common/metadata'; -import { appRouterInstrumentation } from './appRouterRoutingInstrumentation'; -import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation'; +import { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation'; import { applyTunnelRouteOption } from './tunnelRoute'; export * from '@sentry/react'; +export { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation'; export { captureUnderscoreErrorException } from '../common/_error'; -export { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation'; - -/** - * Creates routing instrumention for Next Router. Only supported for - * client side routing. Works for Next >= 10. - * - * Leverages the SingletonRouter from the `next/router` to - * generate pageload/navigation transactions and parameterize - * transaction names. - * - * @deprecated Use pagesRouterInstrumentation instead. - */ -export const nextRouterInstrumentation = pagesRouterInstrumentation; export { Integrations }; @@ -128,16 +114,14 @@ function addClientIntegrations(options: BrowserOptions): void { // will get treeshaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { if (hasTracingEnabled(options)) { - const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__'); - const defaultBrowserTracingIntegration = new BrowserTracing({ // eslint-disable-next-line deprecation/deprecation tracingOrigins: [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], - routingInstrumentation: isAppRouter ? appRouterInstrumentation : pagesRouterInstrumentation, + routingInstrumentation: nextRouterInstrumentation, }); integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, { - 'options.routingInstrumentation': isAppRouter ? appRouterInstrumentation : pagesRouterInstrumentation, + 'options.routingInstrumentation': nextRouterInstrumentation, }); } } diff --git a/packages/nextjs/src/client/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts similarity index 88% rename from packages/nextjs/src/client/appRouterRoutingInstrumentation.ts rename to packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index dc0cb476d5a1..7375e1d547a3 100644 --- a/packages/nextjs/src/client/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -8,13 +8,6 @@ const DEFAULT_TAGS = { 'routing.instrumentation': 'next-app-router', } as const; -// We keep track of the active transaction so we can finish it when we start a navigation transaction. -let activeTransaction: Transaction | undefined = undefined; - -// We keep track of the previous location name so we can set the `from` field on navigation transactions. -// This is either a route or a pathname. -let prevLocationName: string | undefined = undefined; - /** * Creates routing instrumention for Next Router. Only supported for * client side routing. Works for Next >= 10. @@ -28,7 +21,12 @@ export function appRouterInstrumentation( startTransactionOnPageLoad: boolean = true, startTransactionOnLocationChange: boolean = true, ): void { - prevLocationName = WINDOW.location.pathname; + // We keep track of the active transaction so we can finish it when we start a navigation transaction. + let activeTransaction: Transaction | undefined = undefined; + + // We keep track of the previous location name so we can set the `from` field on navigation transactions. + // This is either a route or a pathname. + let prevLocationName = WINDOW.location.pathname; if (startTransactionOnPageLoad) { activeTransaction = startTransactionCb({ diff --git a/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts new file mode 100644 index 000000000000..5c36ca61a3db --- /dev/null +++ b/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts @@ -0,0 +1,23 @@ +import { WINDOW } from '@sentry/react'; +import type { Transaction, TransactionContext } from '@sentry/types'; + +import { appRouterInstrumentation } from './appRouterRoutingInstrumentation'; +import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation'; + +type StartTransactionCb = (context: TransactionContext) => Transaction | undefined; + +/** + * TODO + */ +export function nextRouterInstrumentation( + startTransactionCb: StartTransactionCb, + startTransactionOnPageLoad: boolean = true, + startTransactionOnLocationChange: boolean = true, +): void { + const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__'); + if (isAppRouter) { + appRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange); + } else { + pagesRouterInstrumentation(startTransactionCb, startTransactionOnPageLoad, startTransactionOnLocationChange); + } +} diff --git a/packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts similarity index 100% rename from packages/nextjs/src/client/pagesRouterRoutingInstrumentation.ts rename to packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts From 45bdbb2f2c6c5d88a9676b55e8f7ffe2b83baa62 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 12:18:13 +0000 Subject: [PATCH 04/13] cleanup --- packages/nextjs/src/client/index.ts | 7 ++----- .../client/routing/appRouterRoutingInstrumentation.ts | 7 +------ .../src/client/routing/nextRoutingInstrumentation.ts | 2 +- .../routing/pagesRouterRoutingInstrumentation.ts | 2 +- packages/nextjs/test/performance/client.test.ts | 10 +++++----- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 741a1a3b63ac..b46da56cf98f 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -9,7 +9,7 @@ import { Integrations, } from '@sentry/react'; import type { EventProcessor } from '@sentry/types'; -import { addOrUpdateIntegration, GLOBAL_OBJ } from '@sentry/utils'; +import { addOrUpdateIntegration } from '@sentry/utils'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; @@ -38,11 +38,8 @@ export { BrowserTracing }; // Treeshakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; -const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { +const globalWithInjectedValues = global as typeof global & { __rewriteFramesAssetPrefixPath__: string; - next?: { - appDir?: boolean; - }; }; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 7375e1d547a3..194c9b88d702 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -9,12 +9,7 @@ const DEFAULT_TAGS = { } as const; /** - * Creates routing instrumention for Next Router. Only supported for - * client side routing. Works for Next >= 10. - * - * Leverages the SingletonRouter from the `next/router` to - * generate pageload/navigation transactions and parameterize - * transaction names. + * Instruments the Next.js Clientside App Router. */ export function appRouterInstrumentation( startTransactionCb: StartTransactionCb, diff --git a/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts index 5c36ca61a3db..3010faad4183 100644 --- a/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts @@ -7,7 +7,7 @@ import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation' type StartTransactionCb = (context: TransactionContext) => Transaction | undefined; /** - * TODO + * Instruments the Next.js Clientside Router. */ export function nextRouterInstrumentation( startTransactionCb: StartTransactionCb, diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 52c8569098c0..e9a4710618e5 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -104,7 +104,7 @@ let prevLocationName: string | undefined = undefined; const client = getCurrentHub().getClient(); /** - * Creates routing instrumention for Next Router. Only supported for + * Instruments the Next.js pages router. Only supported for * client side routing. Works for Next >= 10. * * Leverages the SingletonRouter from the `next/router` to diff --git a/packages/nextjs/test/performance/client.test.ts b/packages/nextjs/test/performance/client.test.ts index cc0f212cbf18..c57661fc276c 100644 --- a/packages/nextjs/test/performance/client.test.ts +++ b/packages/nextjs/test/performance/client.test.ts @@ -4,7 +4,7 @@ import { JSDOM } from 'jsdom'; import type { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils'; import { default as Router } from 'next/router'; -import { nextRouterInstrumentation } from '../../src/client/performance'; +import { pagesRouterInstrumentation } from '../../src/client/routing/pagesRouterRoutingInstrumentation'; const globalObject = WINDOW as typeof WINDOW & { __BUILD_MANIFEST?: { @@ -212,7 +212,7 @@ describe('nextRouterInstrumentation', () => { (url, route, query, props, hasNextData, expectedStartTransactionArgument) => { const mockStartTransaction = createMockStartTransaction(); setUpNextPage({ url, route, query, props, hasNextData }); - nextRouterInstrumentation(mockStartTransaction); + pagesRouterInstrumentation(mockStartTransaction); expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockStartTransaction).toHaveBeenLastCalledWith(expectedStartTransactionArgument); }, @@ -221,7 +221,7 @@ describe('nextRouterInstrumentation', () => { it('does not create a pageload transaction if option not given', () => { const mockStartTransaction = createMockStartTransaction(); setUpNextPage({ url: 'https://example.com/', route: '/', hasNextData: false }); - nextRouterInstrumentation(mockStartTransaction, false); + pagesRouterInstrumentation(mockStartTransaction, false); expect(mockStartTransaction).toHaveBeenCalledTimes(0); }); }); @@ -268,7 +268,7 @@ describe('nextRouterInstrumentation', () => { ], }); - nextRouterInstrumentation(mockStartTransaction, false, true); + pagesRouterInstrumentation(mockStartTransaction, false, true); Router.events.emit('routeChangeStart', targetLocation); @@ -304,7 +304,7 @@ describe('nextRouterInstrumentation', () => { navigatableRoutes: ['/home', '/posts/[id]'], }); - nextRouterInstrumentation(mockStartTransaction, false, false); + pagesRouterInstrumentation(mockStartTransaction, false, false); Router.events.emit('routeChangeStart', '/posts/42'); From 2d81468a8b3686b25b03495f7aa381083d76ec49 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 13:11:11 +0000 Subject: [PATCH 05/13] Fix tests --- packages/nextjs/test/performance/client.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/test/performance/client.test.ts b/packages/nextjs/test/performance/client.test.ts index c57661fc276c..95f994f86c28 100644 --- a/packages/nextjs/test/performance/client.test.ts +++ b/packages/nextjs/test/performance/client.test.ts @@ -57,7 +57,7 @@ function createMockStartTransaction() { ); } -describe('nextRouterInstrumentation', () => { +describe('pagesRouterInstrumentation', () => { const originalGlobalDocument = WINDOW.document; const originalGlobalLocation = WINDOW.location; @@ -136,7 +136,7 @@ describe('nextRouterInstrumentation', () => { name: '/[user]/posts/[id]', op: 'pageload', tags: { - 'routing.instrumentation': 'next-router', + 'routing.instrumentation': 'next-pages-router', }, metadata: { source: 'route', @@ -162,7 +162,7 @@ describe('nextRouterInstrumentation', () => { name: '/some-page', op: 'pageload', tags: { - 'routing.instrumentation': 'next-router', + 'routing.instrumentation': 'next-pages-router', }, metadata: { source: 'route', @@ -183,7 +183,7 @@ describe('nextRouterInstrumentation', () => { name: '/', op: 'pageload', tags: { - 'routing.instrumentation': 'next-router', + 'routing.instrumentation': 'next-pages-router', }, metadata: { source: 'route', @@ -200,7 +200,7 @@ describe('nextRouterInstrumentation', () => { name: '/lforst/posts/1337', op: 'pageload', tags: { - 'routing.instrumentation': 'next-router', + 'routing.instrumentation': 'next-pages-router', }, metadata: { source: 'url', @@ -278,7 +278,7 @@ describe('nextRouterInstrumentation', () => { name: expectedTransactionName, op: 'navigation', tags: expect.objectContaining({ - 'routing.instrumentation': 'next-router', + 'routing.instrumentation': 'next-pages-router', }), metadata: expect.objectContaining({ source: expectedTransactionSource, From 461e81f2723759b89681f58380f8925326d69caa Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 13:12:56 +0000 Subject: [PATCH 06/13] Add e2e test --- ...client-app-routing-instrumentation.test.ts | 52 +++++++++++++++++++ .../nextjs-app-dir/tests/trace.test.ts | 30 ----------- 2 files changed, 52 insertions(+), 30 deletions(-) create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts delete mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/tests/trace.test.ts diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts new file mode 100644 index 000000000000..3fa3691e0637 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +test('Creates a pageload transaction for app router routes', async ({ page }) => { + const randomRoute = String(Math.random()); + + const clientPageloadTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => { + return ( + transactionEvent?.transaction === `/server-component/parameter/${randomRoute}` && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/server-component/parameter/${randomRoute}`); + + expect(await clientPageloadTransactionPromise).toBeDefined(); +}); + +test('Creates a navigation transaction for app router routes', async ({ page }) => { + const randomRoute = String(Math.random()); + + const clientPageloadTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => { + return ( + transactionEvent?.transaction === `/server-component/parameter/${randomRoute}` && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/server-component/parameter/${randomRoute}`); + await clientPageloadTransactionPromise; + await page.getByText('Page (/server-component/parameter/[parameter])').isVisible(); + + const clientNavigationTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => { + return ( + transactionEvent?.transaction === '/server-component/parameter/foo/bar/baz' && + transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + const servercomponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])' && + (await clientNavigationTransactionPromise).contexts?.trace?.trace_id === + transactionEvent.contexts?.trace?.trace_id + ); + }); + + await page.getByText('/server-component/parameter/foo/bar/baz').click(); + + expect(await clientNavigationTransactionPromise).toBeDefined(); + expect(await servercomponentTransactionPromise).toBeDefined(); +}); diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/trace.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/trace.test.ts deleted file mode 100644 index ed9a68513a19..000000000000 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/trace.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test } from '@playwright/test'; -import { waitForTransaction } from '../event-proxy-server'; - -if (process.env.TEST_ENV === 'production') { - // TODO: Fix that this is flakey on dev server - might be an SDK bug - test('Sends connected traces for server components', async ({ page }, testInfo) => { - await page.goto('/client-component'); - - const clientTransactionName = `e2e-next-js-app-dir: ${testInfo.title}`; - - const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'Page Server Component (/server-component)' && - (await clientTransactionPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id - ); - }); - - const clientTransactionPromise = waitForTransaction('nextjs-13-app-dir', transactionEvent => { - return transactionEvent?.transaction === clientTransactionName; - }); - - await page.getByPlaceholder('Transaction name').fill(clientTransactionName); - await page.getByText('Start transaction').click(); - await page.getByRole('link', { name: /^\/server-component$/ }).click(); - await page.getByText('Page (/server-component)').isVisible(); - await page.getByText('Stop transaction').click(); - - await serverComponentTransaction; - }); -} From e6da6a4f380a73318c64126a988b0619a3c51daf Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 14:08:58 +0000 Subject: [PATCH 07/13] Rename test file --- .../{client.test.ts => pagesRouterInstrumentation.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/nextjs/test/performance/{client.test.ts => pagesRouterInstrumentation.test.ts} (100%) diff --git a/packages/nextjs/test/performance/client.test.ts b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts similarity index 100% rename from packages/nextjs/test/performance/client.test.ts rename to packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts From ce3e2bf12cb8560a6d7084749998dc778d53b5f6 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 14:15:34 +0000 Subject: [PATCH 08/13] Add unit tests --- .../appRouterRoutingInstrumentation.ts | 13 +- .../appRouterInstrumentation.test.ts | 158 ++++++++++++++++++ 2 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 packages/nextjs/test/performance/appRouterInstrumentation.test.ts diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 194c9b88d702..91c9737b0bce 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -42,13 +42,18 @@ export function appRouterInstrumentation( return; } - const parsedNavigatingRscRequest = parseNavigatingRscRequest(handlerData.args); + // Only GET requests can be navigating RSC requests + if (handlerData.fetchData.method !== 'GET') { + return; + } + + const parsedNavigatingRscFetchArgs = parseNavigatingRscFetchArgs(handlerData.args); - if (parsedNavigatingRscRequest === null) { + if (parsedNavigatingRscFetchArgs === null) { return; } - const transactionName = parsedNavigatingRscRequest.targetPathname; + const transactionName = parsedNavigatingRscFetchArgs.targetPathname; const tags: Record = { ...DEFAULT_TAGS, from: prevLocationName, @@ -70,7 +75,7 @@ export function appRouterInstrumentation( } } -function parseNavigatingRscRequest(fetchArgs: unknown[]): null | { +function parseNavigatingRscFetchArgs(fetchArgs: unknown[]): null | { targetPathname: string; } { // Make sure the first arg is a URL object diff --git a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts new file mode 100644 index 000000000000..e4347c751308 --- /dev/null +++ b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts @@ -0,0 +1,158 @@ +import { WINDOW } from '@sentry/react'; +import { HandlerDataFetch } from '@sentry/types'; +import * as sentryUtils from '@sentry/utils'; +import { JSDOM } from 'jsdom'; + +import { appRouterInstrumentation } from '../../src/client/routing/appRouterRoutingInstrumentation'; + +const addInstrumentationHandlerSpy = jest.spyOn(sentryUtils, 'addInstrumentationHandler'); + +function setUpPage(url: string) { + const dom = new JSDOM('

nothingness

', { url }); + + // The Next.js routing instrumentations requires a few things to be present on pageload: + // 1. Access to window.document API for `window.document.getElementById` + // 2. Access to window.location API for `window.location.pathname` + Object.defineProperty(WINDOW, 'document', { value: dom.window.document, writable: true }); + Object.defineProperty(WINDOW, 'location', { value: dom.window.document.location, writable: true }); +} + +describe('appRouterInstrumentation', () => { + const originalGlobalDocument = WINDOW.document; + const originalGlobalLocation = WINDOW.location; + + afterEach(() => { + // Clean up JSDom + Object.defineProperty(WINDOW, 'document', { value: originalGlobalDocument }); + Object.defineProperty(WINDOW, 'location', { value: originalGlobalLocation }); + }); + + it('should create a pageload transactions with the current location name', () => { + setUpPage('https://example.com/some/page?someParam=foobar'); + const startTransactionCallbackFn = jest.fn(); + appRouterInstrumentation(startTransactionCallbackFn, true, false); + expect(startTransactionCallbackFn).toHaveBeenCalledWith({ + name: '/some/page', + op: 'pageload', + tags: { + 'routing.instrumentation': 'next-app-router', + }, + startTimestamp: expect.any(Number), + metadata: { source: 'url' }, + }); + }); + + it('should not create a pageload transaction when `startTransactionOnPageLoad` is false', () => { + setUpPage('https://example.com/some/page?someParam=foobar'); + const startTransactionCallbackFn = jest.fn(); + appRouterInstrumentation(startTransactionCallbackFn, false, false); + expect(startTransactionCallbackFn).not.toHaveBeenCalled(); + }); + + it('should create a navigation transactions when a navigation RSC request is sent', () => { + setUpPage('https://example.com/some/page?someParam=foobar'); + let fetchInstrumentationHandlerCallback: (arg: HandlerDataFetch) => void; + + addInstrumentationHandlerSpy.mockImplementationOnce((_type, callback) => { + fetchInstrumentationHandlerCallback = callback; + }); + + const startTransactionCallbackFn = jest.fn(); + appRouterInstrumentation(startTransactionCallbackFn, false, true); + + fetchInstrumentationHandlerCallback!({ + args: [ + new URL('https://example.com/some/server/component/page?_rsc=2rs8t'), + { + headers: { + RSC: '1', + }, + }, + ], + fetchData: { method: 'GET', url: 'https://example.com/some/server/component/page?_rsc=2rs8t' }, + startTimestamp: 1337, + }); + + expect(startTransactionCallbackFn).toHaveBeenCalledWith({ + name: '/some/server/component/page', + op: 'navigation', + metadata: { source: 'url' }, + tags: { + from: '/some/page', + 'routing.instrumentation': 'next-app-router', + }, + }); + }); + + it.each([ + [ + 'no RSC header', + { + args: [ + new URL('https://example.com/some/server/component/page?_rsc=2rs8t'), + { + headers: {}, + }, + ], + fetchData: { method: 'GET', url: 'https://example.com/some/server/component/page?_rsc=2rs8t' }, + startTimestamp: 1337, + }, + ], + [ + 'no GET request', + { + args: [ + new URL('https://example.com/some/server/component/page?_rsc=2rs8t'), + { + headers: { + RSC: '1', + }, + }, + ], + fetchData: { method: 'POST', url: 'https://example.com/some/server/component/page?_rsc=2rs8t' }, + startTimestamp: 1337, + }, + ], + [ + 'prefetch request', + { + args: [ + new URL('https://example.com/some/server/component/page?_rsc=2rs8t'), + { + headers: { + RSC: '1', + 'Next-Router-Prefetch': '1', + }, + }, + ], + fetchData: { method: 'GET', url: 'https://example.com/some/server/component/page?_rsc=2rs8t' }, + startTimestamp: 1337, + }, + ], + ])( + 'should not create naviagtion transactions for fetch requests that are not navigating RSC requests (%s)', + (_, fetchCallbackData) => { + setUpPage('https://example.com/some/page?someParam=foobar'); + let fetchInstrumentationHandlerCallback: (arg: HandlerDataFetch) => void; + + addInstrumentationHandlerSpy.mockImplementationOnce((_type, callback) => { + fetchInstrumentationHandlerCallback = callback; + }); + + const startTransactionCallbackFn = jest.fn(); + appRouterInstrumentation(startTransactionCallbackFn, false, true); + fetchInstrumentationHandlerCallback!(fetchCallbackData); + expect(startTransactionCallbackFn).not.toHaveBeenCalled(); + }, + ); + + it('should not create navigation transactions when `startTransactionOnLocationChange` is false', () => { + setUpPage('https://example.com/some/page?someParam=foobar'); + const addInstrumentationHandlerImpl = jest.fn(); + const startTransactionCallbackFn = jest.fn(); + + addInstrumentationHandlerSpy.mockImplementationOnce(addInstrumentationHandlerImpl); + appRouterInstrumentation(startTransactionCallbackFn, false, false); + expect(addInstrumentationHandlerImpl).not.toHaveBeenCalled(); + }); +}); From 5c30fd1ef861a7e330dd0e01f1ffab0975d76825 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 14:26:43 +0000 Subject: [PATCH 09/13] Fix tests --- .../appRouterInstrumentation.test.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts index e4347c751308..4b63c91b4c28 100644 --- a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts @@ -1,5 +1,5 @@ import { WINDOW } from '@sentry/react'; -import { HandlerDataFetch } from '@sentry/types'; +import type { HandlerDataFetch } from '@sentry/types'; import * as sentryUtils from '@sentry/utils'; import { JSDOM } from 'jsdom'; @@ -31,15 +31,16 @@ describe('appRouterInstrumentation', () => { setUpPage('https://example.com/some/page?someParam=foobar'); const startTransactionCallbackFn = jest.fn(); appRouterInstrumentation(startTransactionCallbackFn, true, false); - expect(startTransactionCallbackFn).toHaveBeenCalledWith({ - name: '/some/page', - op: 'pageload', - tags: { - 'routing.instrumentation': 'next-app-router', - }, - startTimestamp: expect.any(Number), - metadata: { source: 'url' }, - }); + expect(startTransactionCallbackFn).toHaveBeenCalledWith( + expect.objectContaining({ + name: '/some/page', + op: 'pageload', + tags: { + 'routing.instrumentation': 'next-app-router', + }, + metadata: { source: 'url' }, + }), + ); }); it('should not create a pageload transaction when `startTransactionOnPageLoad` is false', () => { From 5b44ede501e953d49a07447d263757041f506915 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 14:51:57 +0000 Subject: [PATCH 10/13] test --- .../test/performance/pagesRouterInstrumentation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts index 95f994f86c28..bd08866e4e75 100644 --- a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts @@ -135,6 +135,7 @@ describe('pagesRouterInstrumentation', () => { { name: '/[user]/posts/[id]', op: 'pageload', + startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -161,6 +162,7 @@ describe('pagesRouterInstrumentation', () => { { name: '/some-page', op: 'pageload', + startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -182,6 +184,7 @@ describe('pagesRouterInstrumentation', () => { { name: '/', op: 'pageload', + startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -199,6 +202,7 @@ describe('pagesRouterInstrumentation', () => { { name: '/lforst/posts/1337', op: 'pageload', + startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, From 678ac675d94e6df501d137a93d97223d7010775e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 3 Nov 2023 15:03:36 +0000 Subject: [PATCH 11/13] test --- .../test/performance/pagesRouterInstrumentation.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts index bd08866e4e75..6db342fb426c 100644 --- a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts @@ -135,7 +135,6 @@ describe('pagesRouterInstrumentation', () => { { name: '/[user]/posts/[id]', op: 'pageload', - startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -162,7 +161,6 @@ describe('pagesRouterInstrumentation', () => { { name: '/some-page', op: 'pageload', - startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -184,7 +182,6 @@ describe('pagesRouterInstrumentation', () => { { name: '/', op: 'pageload', - startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -202,7 +199,6 @@ describe('pagesRouterInstrumentation', () => { { name: '/lforst/posts/1337', op: 'pageload', - startTimestamp: expect.any(Number), tags: { 'routing.instrumentation': 'next-pages-router', }, @@ -218,7 +214,9 @@ describe('pagesRouterInstrumentation', () => { setUpNextPage({ url, route, query, props, hasNextData }); pagesRouterInstrumentation(mockStartTransaction); expect(mockStartTransaction).toHaveBeenCalledTimes(1); - expect(mockStartTransaction).toHaveBeenLastCalledWith(expectedStartTransactionArgument); + expect(mockStartTransaction).toHaveBeenLastCalledWith( + expect.objectContaining(expectedStartTransactionArgument), + ); }, ); From 3870b173232d6eca143105bda07c829e3fd50033 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 6 Nov 2023 12:29:54 +0000 Subject: [PATCH 12/13] Add origin --- .../src/client/routing/appRouterRoutingInstrumentation.ts | 2 ++ .../src/client/routing/pagesRouterRoutingInstrumentation.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 91c9737b0bce..5d7ab3596c8e 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -27,6 +27,7 @@ export function appRouterInstrumentation( activeTransaction = startTransactionCb({ name: prevLocationName, op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', tags: DEFAULT_TAGS, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, @@ -68,6 +69,7 @@ export function appRouterInstrumentation( startTransactionCb({ name: transactionName, op: 'navigation', + origin: 'auto.navigation.nextjs.app_router_instrumentation', tags, metadata: { source: 'url' }, }); diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index e9a4710618e5..a633e1cec6dc 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -130,6 +130,7 @@ export function pagesRouterInstrumentation( activeTransaction = startTransactionCb({ name: prevLocationName, op: 'pageload', + origin: 'auto.pageload.nextjs.pages_router_instrumentation', tags: DEFAULT_TAGS, // pageload should always start at timeOrigin (and needs to be in s, not ms) startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, @@ -172,6 +173,7 @@ export function pagesRouterInstrumentation( const navigationTransaction = startTransactionCb({ name: transactionName, op: 'navigation', + origin: 'auto.navigation.nextjs.pages_router_instrumentation', tags, metadata: { source: transactionSource }, }); @@ -184,8 +186,8 @@ export function pagesRouterInstrumentation( // hooks). Instead, we'll simply let the navigation transaction finish itself (it's an `IdleTransaction`). const nextRouteChangeSpan = navigationTransaction.startChild({ op: 'ui.nextjs.route-change', + origin: 'auto.ui.nextjs.pages_router_instrumentation', description: 'Next.js Route Change', - origin: 'auto.navigation.nextjs', }); const finishRouteChangeSpan = (): void => { From 724ec2330037b036f1add0bcd181f2c9dc69c0e7 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 6 Nov 2023 14:50:18 +0000 Subject: [PATCH 13/13] ci --- .../nextjs/test/performance/appRouterInstrumentation.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts index 4b63c91b4c28..bebb0cbf8ab6 100644 --- a/packages/nextjs/test/performance/appRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/appRouterInstrumentation.test.ts @@ -35,6 +35,7 @@ describe('appRouterInstrumentation', () => { expect.objectContaining({ name: '/some/page', op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', tags: { 'routing.instrumentation': 'next-app-router', }, @@ -77,6 +78,7 @@ describe('appRouterInstrumentation', () => { expect(startTransactionCallbackFn).toHaveBeenCalledWith({ name: '/some/server/component/page', op: 'navigation', + origin: 'auto.navigation.nextjs.app_router_instrumentation', metadata: { source: 'url' }, tags: { from: '/some/page',