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
26 changes: 26 additions & 0 deletions packages/nextjs/src/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BrowserTracing as OriginalBrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/react';
import { nextRouterInstrumentation } from '../index.client';

/**
* A custom BrowserTracing integration for Next.js.
*/
export class BrowserTracing extends OriginalBrowserTracing {
public constructor(options?: ConstructorParameters<typeof OriginalBrowserTracing>[0]) {
super({
// eslint-disable-next-line deprecation/deprecation
tracingOrigins:
process.env.NODE_ENV === 'development'
? [
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
// has cors and it doesn't like extra headers when it's accessed from a different URL.
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
/^\/(?!\/)/,
]
: // eslint-disable-next-line deprecation/deprecation
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
routingInstrumentation: nextRouterInstrumentation,
...options,
});
}
}
125 changes: 52 additions & 73 deletions packages/nextjs/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { hasTracingEnabled } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import type { BrowserOptions } from '@sentry/react';
import {
BrowserTracing,
Integrations,
defaultRequestInstrumentationOptions,
Integrations as OriginalIntegrations,
getCurrentScope,
getDefaultIntegrations as getReactDefaultIntegrations,
init as reactInit,
} from '@sentry/react';
import type { EventProcessor } from '@sentry/types';
import { addOrUpdateIntegration } from '@sentry/utils';
import type { EventProcessor, Integration } from '@sentry/types';

import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
import { buildMetadata } from '../common/metadata';
import { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
import { BrowserTracing } from './browserTracingIntegration';
import { rewriteFramesIntegration } from './rewriteFramesIntegration';
import { applyTunnelRouteOption } from './tunnelRoute';

export * from '@sentry/react';
export { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
export { captureUnderscoreErrorException } from '../common/_error';

export { Integrations };
export const Integrations = {
...OriginalIntegrations,
BrowserTracing,
};

// Previously we expected users to import `BrowserTracing` like this:
//
Expand All @@ -33,27 +34,24 @@ export { Integrations };
//
// import { BrowserTracing } from '@sentry/nextjs';
// const instance = new BrowserTracing();
export { BrowserTracing };
export { BrowserTracing, rewriteFramesIntegration };

// Treeshakable guard to remove all code related to tracing
declare const __SENTRY_TRACING__: boolean;

const globalWithInjectedValues = global as typeof global & {
__rewriteFramesAssetPrefixPath__: string;
};

/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
export function init(options: BrowserOptions): void {
const opts = {
environment: getVercelEnv(true) || process.env.NODE_ENV,
defaultIntegrations: getDefaultIntegrations(options),
...options,
};

fixBrowserTracingIntegration(opts);

applyTunnelRouteOption(opts);
buildMetadata(opts, ['nextjs', 'react']);

addClientIntegrations(opts);

reactInit(opts);

const scope = getCurrentScope();
Expand All @@ -68,72 +66,53 @@ export function init(options: BrowserOptions): void {
}
}

function addClientIntegrations(options: BrowserOptions): void {
let integrations = options.integrations || [];

// This value is injected at build time, based on the output directory specified in the build config. Though a default
// is set there, we set it here as well, just in case something has gone wrong with the injection.
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';

// eslint-disable-next-line deprecation/deprecation
const defaultRewriteFramesIntegration = new RewriteFrames({
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
iteratee: frame => {
try {
const { origin } = new URL(frame.filename as string);
frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, '');
} catch (err) {
// Filename wasn't a properly formed URL, so there's nothing we can do
}

// We need to URI-decode the filename because Next.js has wildcard routes like "/users/[id].js" which show up as "/users/%5id%5.js" in Error stacktraces.
// The corresponding sources that Next.js generates have proper brackets so we also need proper brackets in the frame so that source map resolving works.
if (frame.filename && frame.filename.startsWith('app:///_next')) {
frame.filename = decodeURI(frame.filename);
}

if (
frame.filename &&
frame.filename.match(
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
)
) {
// We don't care about these frames. It's Next.js internal code.
frame.in_app = false;
}

return frame;
},
});
integrations = addOrUpdateIntegration(defaultRewriteFramesIntegration, integrations);
// TODO v8: Remove this again
// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/sveltekit` :(
function fixBrowserTracingIntegration(options: BrowserOptions): void {
const { integrations } = options;
if (!integrations) {
return;
}

if (Array.isArray(integrations)) {
options.integrations = maybeUpdateBrowserTracingIntegration(integrations);
} else {
options.integrations = defaultIntegrations => {
const userFinalIntegrations = integrations(defaultIntegrations);

return maybeUpdateBrowserTracingIntegration(userFinalIntegrations);
};
}
}

function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
// If BrowserTracing was added, but it is not our forked version,
// replace it with our forked version with the same options
if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
const options: ConstructorParameters<typeof BrowserTracing>[0] = (browserTracing as BrowserTracing).options;
// These two options are overwritten by the custom integration
delete options.routingInstrumentation;
// eslint-disable-next-line deprecation/deprecation
delete options.tracingOrigins;
integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
}

return integrations;
}

function getDefaultIntegrations(options: BrowserOptions): Integration[] {
const customDefaultIntegrations = [...getReactDefaultIntegrations(options), rewriteFramesIntegration()];

// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside
// will get treeshaken away
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
if (hasTracingEnabled(options)) {
const defaultBrowserTracingIntegration = new BrowserTracing({
// eslint-disable-next-line deprecation/deprecation
tracingOrigins:
process.env.NODE_ENV === 'development'
? [
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
// has cors and it doesn't like extra headers when it's accessed from a different URL.
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
/^\/(?!\/)/,
]
: // eslint-disable-next-line deprecation/deprecation
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
routingInstrumentation: nextRouterInstrumentation,
});

integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, {
'options.routingInstrumentation': nextRouterInstrumentation,
});
customDefaultIntegrations.push(new BrowserTracing());
}
}

options.integrations = integrations;
return customDefaultIntegrations;
}

/**
Expand Down
54 changes: 54 additions & 0 deletions packages/nextjs/src/client/rewriteFramesIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { defineIntegration } from '@sentry/core';
import { rewriteFramesIntegration as originalRewriteFramesIntegration } from '@sentry/integrations';
import type { IntegrationFn, StackFrame } from '@sentry/types';

const globalWithInjectedValues = global as typeof global & {
__rewriteFramesAssetPrefixPath__: string;
};

type StackFrameIteratee = (frame: StackFrame) => StackFrame;

interface RewriteFramesOptions {
root?: string;
prefix?: string;
iteratee?: StackFrameIteratee;
}

export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
// This value is injected at build time, based on the output directory specified in the build config. Though a default
// is set there, we set it here as well, just in case something has gone wrong with the injection.
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';

return originalRewriteFramesIntegration({
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
iteratee: frame => {
try {
const { origin } = new URL(frame.filename as string);
frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, '');
} catch (err) {
// Filename wasn't a properly formed URL, so there's nothing we can do
}

// We need to URI-decode the filename because Next.js has wildcard routes like "/users/[id].js" which show up as "/users/%5id%5.js" in Error stacktraces.
// The corresponding sources that Next.js generates have proper brackets so we also need proper brackets in the frame so that source map resolving works.
if (frame.filename && frame.filename.startsWith('app:///_next')) {
frame.filename = decodeURI(frame.filename);
}

if (
frame.filename &&
frame.filename.match(
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
)
) {
// We don't care about these frames. It's Next.js internal code.
frame.in_app = false;
}

return frame;
},
...options,
});
}) satisfies IntegrationFn;

export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);
39 changes: 6 additions & 33 deletions packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { SDK_VERSION, addTracingExtensions } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import type { SdkMetadata } from '@sentry/types';
import { GLOBAL_OBJ, addOrUpdateIntegration, escapeStringForRegex } from '@sentry/utils';
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
import { init as vercelEdgeInit } from '@sentry/vercel-edge';
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';

import { isBuild } from '../common/utils/isBuild';
import { rewriteFramesIntegration } from './rewriteFramesIntegration';

export type EdgeOptions = VercelEdgeOptions;

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesDistDir__?: string;
fetch: (...args: unknown[]) => unknown;
};
export { rewriteFramesIntegration };

/** Inits the Sentry NextJS SDK on the Edge Runtime. */
export function init(options: VercelEdgeOptions = {}): void {
Expand All @@ -22,8 +18,11 @@ export function init(options: VercelEdgeOptions = {}): void {
return;
}

const customDefaultIntegrations = [...getDefaultIntegrations(options), rewriteFramesIntegration()];

const opts = {
_metadata: {} as SdkMetadata,
defaultIntegrations: customDefaultIntegrations,
...options,
};

Expand All @@ -38,32 +37,6 @@ export function init(options: VercelEdgeOptions = {}): void {
version: SDK_VERSION,
};

let integrations = opts.integrations || [];

// This value is injected at build time, based on the output directory specified in the build config. Though a default
// is set there, we set it here as well, just in case something has gone wrong with the injection.
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
if (distDirName) {
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one

// Normally we would use `path.resolve` to obtain the absolute path we will strip from the stack frame to align with
// the uploaded artifacts, however we don't have access to that API in edge so we need to be a bit more lax.
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
const SOURCEMAP_FILENAME_REGEX = new RegExp(`.*${escapeStringForRegex(distDirAbsPath)}`);

// eslint-disable-next-line deprecation/deprecation
const defaultRewriteFramesIntegration = new RewriteFrames({
iteratee: frame => {
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next');
return frame;
},
});

integrations = addOrUpdateIntegration(defaultRewriteFramesIntegration, integrations);
}

opts.integrations = integrations;

vercelEdgeInit(opts);
}

Expand Down
52 changes: 52 additions & 0 deletions packages/nextjs/src/edge/rewriteFramesIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { defineIntegration } from '@sentry/core';
import {
RewriteFrames as OriginalRewriteFrames,
rewriteFramesIntegration as originalRewriteFramesIntegration,
} from '@sentry/integrations';
import type { IntegrationFn, StackFrame } from '@sentry/types';
import { GLOBAL_OBJ, escapeStringForRegex } from '@sentry/utils';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesDistDir__?: string;
};

type StackFrameIteratee = (frame: StackFrame) => StackFrame;
interface RewriteFramesOptions {
root?: string;
prefix?: string;
iteratee?: StackFrameIteratee;
}

export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
// This value is injected at build time, based on the output directory specified in the build config. Though a default
// is set there, we set it here as well, just in case something has gone wrong with the injection.
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;

if (distDirName) {
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one

// Normally we would use `path.resolve` to obtain the absolute path we will strip from the stack frame to align with
// the uploaded artifacts, however we don't have access to that API in edge so we need to be a bit more lax.
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
const SOURCEMAP_FILENAME_REGEX = new RegExp(`.*${escapeStringForRegex(distDirAbsPath)}`);

return originalRewriteFramesIntegration({
iteratee: frame => {
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next');
return frame;
},
...options,
});
}

// Do nothing if we can't find a distDirName
return {
// eslint-disable-next-line deprecation/deprecation
name: OriginalRewriteFrames.id,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setupOnce: () => {},
processEvent: event => event,
};
}) satisfies IntegrationFn;

export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);
3 changes: 3 additions & 0 deletions packages/nextjs/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export declare const defaultIntegrations: Integration[];
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;

// eslint-disable-next-line deprecation/deprecation
export declare const rewriteFramesIntegration: typeof clientSdk.rewriteFramesIntegration;

export declare function getSentryRelease(fallback?: string): string | undefined;

export declare const ErrorBoundary: typeof clientSdk.ErrorBoundary;
Expand Down
Loading