Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,13 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
): void;

/** @inheritdoc */
public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void;
public on(
hook: 'startPageLoadSpan',
callback: (
options: StartSpanOptions,
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
) => void,
): void;

/** @inheritdoc */
public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
Expand Down Expand Up @@ -500,7 +506,11 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void;

/** @inheritdoc */
public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void;
public emit(
hook: 'startPageLoadSpan',
options: StartSpanOptions,
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
): void;

/** @inheritdoc */
public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void;
Expand Down
38 changes: 11 additions & 27 deletions packages/nextjs/src/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {
browserTracingIntegration as originalBrowserTracingIntegration,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from '@sentry/react';
import type { Integration, StartSpanOptions } from '@sentry/types';
import { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react';
import type { Integration } from '@sentry/types';
import { nextRouterInstrumentNavigation, nextRouterInstrumentPageLoad } from './routing/nextRoutingInstrumentation';

/**
* A custom browser tracing integration for Next.js.
Expand All @@ -18,36 +14,24 @@ export function browserTracingIntegration(
instrumentPageLoad: false,
});

const { instrumentPageLoad = true, instrumentNavigation = true } = options;

return {
...browserTracingIntegrationInstance,
afterAllSetup(client) {
const startPageloadCallback = (startSpanOptions: StartSpanOptions): void => {
startBrowserTracingPageLoadSpan(client, startSpanOptions);
};

const startNavigationCallback = (startSpanOptions: StartSpanOptions): void => {
startBrowserTracingNavigationSpan(client, startSpanOptions);
};

// We need to run the navigation span instrumentation before the `afterAllSetup` hook on the normal browser
// tracing integration because we need to ensure the order of execution is as follows:
// Instrumentation to start span on RSC fetch request runs -> Instrumentation to put tracing headers from active span on fetch runs
// If it were the other way around, the RSC fetch request would not receive the tracing headers from the navigation transaction.
nextRouterInstrumentation(
false,
options.instrumentNavigation === undefined ? true : options.instrumentNavigation,
startPageloadCallback,
startNavigationCallback,
);
if (instrumentNavigation) {
nextRouterInstrumentNavigation(client);
}

browserTracingIntegrationInstance.afterAllSetup(client);

nextRouterInstrumentation(
options.instrumentPageLoad === undefined ? true : options.instrumentPageLoad,
false,
startPageloadCallback,
startNavigationCallback,
);
if (instrumentPageLoad) {
nextRouterInstrumentPageLoad(client);
}
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,55 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
import { WINDOW } from '@sentry/react';
import type { StartSpanOptions } from '@sentry/types';
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
import type { Client } from '@sentry/types';
import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils';

type StartSpanCb = (context: StartSpanOptions) => void;

/**
* Instruments the Next.js Client App Router.
*/
export function appRouterInstrumentation(
shouldInstrumentPageload: boolean,
shouldInstrumentNavigation: boolean,
startPageloadSpanCallback: StartSpanCb,
startNavigationSpanCallback: StartSpanCb,
): void {
// 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 currPathname = WINDOW.location.pathname;

if (shouldInstrumentPageload) {
startPageloadSpanCallback({
name: currPathname,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
}
/** Instruments the Next.js app router for pageloads. */
export function appRouterInstrumentPageLoad(client: Client): void {
startBrowserTracingPageLoadSpan(client, {
name: WINDOW.location.pathname,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
}

if (shouldInstrumentNavigation) {
addFetchInstrumentationHandler(handlerData => {
// 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;
}
/** Instruments the Next.js app router for navigation. */
export function appRouterInstrumentNavigation(client: Client): void {
addFetchInstrumentationHandler(handlerData => {
// 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;
}

// Only GET requests can be navigating RSC requests
if (handlerData.fetchData.method !== 'GET') {
return;
}
// Only GET requests can be navigating RSC requests
if (handlerData.fetchData.method !== 'GET') {
return;
}

const parsedNavigatingRscFetchArgs = parseNavigatingRscFetchArgs(handlerData.args);
const parsedNavigatingRscFetchArgs = parseNavigatingRscFetchArgs(handlerData.args);

if (parsedNavigatingRscFetchArgs === null) {
return;
}
if (parsedNavigatingRscFetchArgs === null) {
return;
}

const newPathname = parsedNavigatingRscFetchArgs.targetPathname;
currPathname = newPathname;
const newPathname = parsedNavigatingRscFetchArgs.targetPathname;

startNavigationSpanCallback({
name: newPathname,
attributes: {
from: currPathname,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
startBrowserTracingNavigationSpan(client, {
name: newPathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
}
});
}

function parseNavigatingRscFetchArgs(fetchArgs: unknown[]): null | {
Expand Down
41 changes: 18 additions & 23 deletions packages/nextjs/src/client/routing/nextRoutingInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
import { WINDOW } from '@sentry/react';
import type { StartSpanOptions } from '@sentry/types';
import type { Client } from '@sentry/types';

import { appRouterInstrumentation } from './appRouterRoutingInstrumentation';
import { pagesRouterInstrumentation } from './pagesRouterRoutingInstrumentation';
import { appRouterInstrumentNavigation, appRouterInstrumentPageLoad } from './appRouterRoutingInstrumentation';
import { pagesRouterInstrumentNavigation, pagesRouterInstrumentPageLoad } from './pagesRouterRoutingInstrumentation';

type StartSpanCb = (context: StartSpanOptions) => void;
/**
* Instruments the Next.js Client Router for page loads.
*/
export function nextRouterInstrumentPageLoad(client: Client): void {
const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__');
if (isAppRouter) {
appRouterInstrumentPageLoad(client);
} else {
pagesRouterInstrumentPageLoad(client);
}
}

/**
* Instruments the Next.js Client Router.
* Instruments the Next.js Client Router for navigation.
*/
export function nextRouterInstrumentation(
shouldInstrumentPageload: boolean,
shouldInstrumentNavigation: boolean,
startPageloadSpanCallback: StartSpanCb,
startNavigationSpanCallback: StartSpanCb,
): void {
export function nextRouterInstrumentNavigation(client: Client): void {
const isAppRouter = !WINDOW.document.getElementById('__NEXT_DATA__');
if (isAppRouter) {
appRouterInstrumentation(
shouldInstrumentPageload,
shouldInstrumentNavigation,
startPageloadSpanCallback,
startNavigationSpanCallback,
);
appRouterInstrumentNavigation(client);
} else {
pagesRouterInstrumentation(
shouldInstrumentPageload,
shouldInstrumentNavigation,
startPageloadSpanCallback,
startNavigationSpanCallback,
);
pagesRouterInstrumentNavigation(client);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getClient,
} from '@sentry/core';
import { WINDOW } from '@sentry/react';
import type { StartSpanOptions, TransactionSource } from '@sentry/types';
import {
browserPerformanceTimeOrigin,
logger,
propagationContextFromHeaders,
stripUrlQueryAndFragment,
} from '@sentry/utils';
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
import type { Client, TransactionSource } from '@sentry/types';
import { browserPerformanceTimeOrigin, logger, stripUrlQueryAndFragment } from '@sentry/utils';
import type { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils';
import RouterImport from 'next/router';

Expand All @@ -30,8 +24,6 @@ const globalObject = WINDOW as typeof WINDOW & {
};
};

type StartSpanCb = (context: StartSpanOptions) => void;

/**
* Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app.
*/
Expand Down Expand Up @@ -104,73 +96,67 @@ function extractNextDataTagInformation(): NextDataTagInfo {
}

/**
* Instruments the Next.js pages router. Only supported for
* client side routing. Works for Next >= 10.
* Instruments the Next.js pages router for pageloads.
* 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 pagesRouterInstrumentation(
shouldInstrumentPageload: boolean,
shouldInstrumentNavigation: boolean,
startPageloadSpanCallback: StartSpanCb,
startNavigationSpanCallback: StartSpanCb,
): void {
export function pagesRouterInstrumentPageLoad(client: Client): void {
const { route, params, sentryTrace, baggage } = extractNextDataTagInformation();
const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
let prevLocationName = route || globalObject.location.pathname;
const name = route || globalObject.location.pathname;

if (shouldInstrumentPageload) {
const client = getClient();
startPageloadSpanCallback({
name: prevLocationName,
startBrowserTracingPageLoadSpan(
client,
{
name,
// pageload should always start at timeOrigin (and needs to be in s, not ms)
startTime: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
traceId,
parentSpanId,
parentSampled: sampled,
...(params && client && client.getOptions().sendDefaultPii && { data: params }),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url',
...(params && client.getOptions().sendDefaultPii && { ...params }),
},
metadata: {
dynamicSamplingContext: dsc,
},
});
}
},
{ sentryTrace, baggage },
);
}

if (shouldInstrumentNavigation) {
Router.events.on('routeChangeStart', (navigationTarget: string) => {
const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget);
const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget);

let newLocation: string;
let spanSource: TransactionSource;

if (matchedRoute) {
newLocation = matchedRoute;
spanSource = 'route';
} else {
newLocation = strippedNavigationTarget;
spanSource = 'url';
}

startNavigationSpanCallback({
name: newLocation,
attributes: {
from: prevLocationName,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
},
});

prevLocationName = newLocation;
/**
* Instruments the Next.js pages router for navigation.
* 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 pagesRouterInstrumentNavigation(client: Client): void {
Router.events.on('routeChangeStart', (navigationTarget: string) => {
const strippedNavigationTarget = stripUrlQueryAndFragment(navigationTarget);
const matchedRoute = getNextRouteFromPathname(strippedNavigationTarget);

let newLocation: string;
let spanSource: TransactionSource;

if (matchedRoute) {
newLocation = matchedRoute;
spanSource = 'route';
} else {
newLocation = strippedNavigationTarget;
spanSource = 'url';
}

startBrowserTracingNavigationSpan(client, {
name: newLocation,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
},
});
}
});
}

function getNextRouteFromPathname(pathname: string): string | undefined {
Expand Down
Loading