Skip to content

Commit d113796

Browse files
committed
fix(next): Fix custom RewriteFrames integration
The usage of this was not really working well to begin with, and even worse with the new functional integrations.
1 parent 76378af commit d113796

File tree

12 files changed

+317
-225
lines changed

12 files changed

+317
-225
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BrowserTracing as OriginalBrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/react';
2+
import { nextRouterInstrumentation } from '../index.client';
3+
4+
/**
5+
* A custom BrowserTracing integration for Next.js.
6+
*/
7+
export class BrowserTracing extends OriginalBrowserTracing {
8+
public constructor(options?: ConstructorParameters<typeof OriginalBrowserTracing>[0]) {
9+
super({
10+
// eslint-disable-next-line deprecation/deprecation
11+
tracingOrigins:
12+
process.env.NODE_ENV === 'development'
13+
? [
14+
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
15+
// has cors and it doesn't like extra headers when it's accessed from a different URL.
16+
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
17+
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
18+
/^\/(?!\/)/,
19+
]
20+
: // eslint-disable-next-line deprecation/deprecation
21+
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
22+
routingInstrumentation: nextRouterInstrumentation,
23+
...options,
24+
});
25+
}
26+
}

packages/nextjs/src/client/index.ts

Lines changed: 52 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import { hasTracingEnabled } from '@sentry/core';
2-
import { RewriteFrames } from '@sentry/integrations';
32
import type { BrowserOptions } from '@sentry/react';
4-
import {
5-
BrowserTracing,
6-
Integrations,
7-
defaultRequestInstrumentationOptions,
8-
getCurrentScope,
9-
init as reactInit,
10-
} from '@sentry/react';
11-
import type { EventProcessor } from '@sentry/types';
12-
import { addOrUpdateIntegration } from '@sentry/utils';
3+
import { defaultIntegrations } from '@sentry/react';
4+
import { Integrations as OriginalIntegrations, getCurrentScope, init as reactInit } from '@sentry/react';
5+
import type { EventProcessor, Integration } from '@sentry/types';
136

147
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
158
import { getVercelEnv } from '../common/getVercelEnv';
169
import { buildMetadata } from '../common/metadata';
17-
import { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
10+
import { BrowserTracing } from './browserTracingIntegration';
11+
import { rewriteFramesIntegration } from './rewriteFramesIntegration';
1812
import { applyTunnelRouteOption } from './tunnelRoute';
1913

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

24-
export { Integrations };
18+
export const Integrations = {
19+
...OriginalIntegrations,
20+
BrowserTracing,
21+
};
2522

2623
// Previously we expected users to import `BrowserTracing` like this:
2724
//
@@ -33,27 +30,24 @@ export { Integrations };
3330
//
3431
// import { BrowserTracing } from '@sentry/nextjs';
3532
// const instance = new BrowserTracing();
36-
export { BrowserTracing };
33+
export { BrowserTracing, rewriteFramesIntegration };
3734

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

41-
const globalWithInjectedValues = global as typeof global & {
42-
__rewriteFramesAssetPrefixPath__: string;
43-
};
44-
4538
/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
4639
export function init(options: BrowserOptions): void {
4740
const opts = {
4841
environment: getVercelEnv(true) || process.env.NODE_ENV,
42+
defaultIntegrations: getDefaultIntegrations(options),
4943
...options,
5044
};
5145

46+
fixBrowserTracingIntegration(opts);
47+
5248
applyTunnelRouteOption(opts);
5349
buildMetadata(opts, ['nextjs', 'react']);
5450

55-
addClientIntegrations(opts);
56-
5751
reactInit(opts);
5852

5953
const scope = getCurrentScope();
@@ -68,72 +62,53 @@ export function init(options: BrowserOptions): void {
6862
}
6963
}
7064

71-
function addClientIntegrations(options: BrowserOptions): void {
72-
let integrations = options.integrations || [];
73-
74-
// This value is injected at build time, based on the output directory specified in the build config. Though a default
75-
// is set there, we set it here as well, just in case something has gone wrong with the injection.
76-
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';
77-
78-
// eslint-disable-next-line deprecation/deprecation
79-
const defaultRewriteFramesIntegration = new RewriteFrames({
80-
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
81-
iteratee: frame => {
82-
try {
83-
const { origin } = new URL(frame.filename as string);
84-
frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, '');
85-
} catch (err) {
86-
// Filename wasn't a properly formed URL, so there's nothing we can do
87-
}
88-
89-
// 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.
90-
// 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.
91-
if (frame.filename && frame.filename.startsWith('app:///_next')) {
92-
frame.filename = decodeURI(frame.filename);
93-
}
94-
95-
if (
96-
frame.filename &&
97-
frame.filename.match(
98-
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
99-
)
100-
) {
101-
// We don't care about these frames. It's Next.js internal code.
102-
frame.in_app = false;
103-
}
104-
105-
return frame;
106-
},
107-
});
108-
integrations = addOrUpdateIntegration(defaultRewriteFramesIntegration, integrations);
65+
// TODO v8: Remove this again
66+
// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/sveltekit` :(
67+
function fixBrowserTracingIntegration(options: BrowserOptions): void {
68+
const { integrations } = options;
69+
if (!integrations) {
70+
return;
71+
}
72+
73+
if (Array.isArray(integrations)) {
74+
options.integrations = maybeUpdateBrowserTracingIntegration(integrations);
75+
} else {
76+
options.integrations = defaultIntegrations => {
77+
const userFinalIntegrations = integrations(defaultIntegrations);
78+
79+
return maybeUpdateBrowserTracingIntegration(userFinalIntegrations);
80+
};
81+
}
82+
}
83+
84+
function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
85+
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
86+
// If BrowserTracing was added, but it is not our forked version,
87+
// replace it with our forked version with the same options
88+
if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
89+
const options: ConstructorParameters<typeof BrowserTracing>[0] = (browserTracing as BrowserTracing).options;
90+
// These two options are overwritten by the custom integration
91+
delete options.routingInstrumentation;
92+
// eslint-disable-next-line deprecation/deprecation
93+
delete options.tracingOrigins;
94+
integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
95+
}
96+
97+
return integrations;
98+
}
99+
100+
function getDefaultIntegrations(options: BrowserOptions): Integration[] {
101+
const customDefaultIntegrations = defaultIntegrations.slice().concat([rewriteFramesIntegration()]);
109102

110103
// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside
111104
// will get treeshaken away
112105
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
113106
if (hasTracingEnabled(options)) {
114-
const defaultBrowserTracingIntegration = new BrowserTracing({
115-
// eslint-disable-next-line deprecation/deprecation
116-
tracingOrigins:
117-
process.env.NODE_ENV === 'development'
118-
? [
119-
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
120-
// has cors and it doesn't like extra headers when it's accessed from a different URL.
121-
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
122-
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
123-
/^\/(?!\/)/,
124-
]
125-
: // eslint-disable-next-line deprecation/deprecation
126-
[...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/],
127-
routingInstrumentation: nextRouterInstrumentation,
128-
});
129-
130-
integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, {
131-
'options.routingInstrumentation': nextRouterInstrumentation,
132-
});
107+
customDefaultIntegrations.push(new BrowserTracing());
133108
}
134109
}
135110

136-
options.integrations = integrations;
111+
return customDefaultIntegrations;
137112
}
138113

139114
/**
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { defineIntegration } from '@sentry/core';
2+
import { rewriteFramesIntegration as originalRewriteFramesIntegration } from '@sentry/integrations';
3+
import type { IntegrationFn, StackFrame } from '@sentry/types';
4+
5+
const globalWithInjectedValues = global as typeof global & {
6+
__rewriteFramesAssetPrefixPath__: string;
7+
};
8+
9+
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
10+
11+
interface RewriteFramesOptions {
12+
root?: string;
13+
prefix?: string;
14+
iteratee?: StackFrameIteratee;
15+
}
16+
17+
export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
18+
// This value is injected at build time, based on the output directory specified in the build config. Though a default
19+
// is set there, we set it here as well, just in case something has gone wrong with the injection.
20+
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';
21+
22+
return originalRewriteFramesIntegration({
23+
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
24+
iteratee: frame => {
25+
try {
26+
const { origin } = new URL(frame.filename as string);
27+
frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, '');
28+
} catch (err) {
29+
// Filename wasn't a properly formed URL, so there's nothing we can do
30+
}
31+
32+
// 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.
33+
// 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.
34+
if (frame.filename && frame.filename.startsWith('app:///_next')) {
35+
frame.filename = decodeURI(frame.filename);
36+
}
37+
38+
if (
39+
frame.filename &&
40+
frame.filename.match(
41+
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
42+
)
43+
) {
44+
// We don't care about these frames. It's Next.js internal code.
45+
frame.in_app = false;
46+
}
47+
48+
return frame;
49+
},
50+
...options,
51+
});
52+
}) satisfies IntegrationFn;
53+
54+
export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);

packages/nextjs/src/edge/index.ts

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
import { SDK_VERSION, addTracingExtensions } from '@sentry/core';
2-
import { RewriteFrames } from '@sentry/integrations';
3-
import type { SdkMetadata } from '@sentry/types';
4-
import { GLOBAL_OBJ, addOrUpdateIntegration, escapeStringForRegex } from '@sentry/utils';
2+
import type { Integration, SdkMetadata } from '@sentry/types';
53
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
6-
import { init as vercelEdgeInit } from '@sentry/vercel-edge';
4+
import { defaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
75

86
import { isBuild } from '../common/utils/isBuild';
7+
import { rewriteFramesIntegration } from './rewriteFramesIntegration';
98

109
export type EdgeOptions = VercelEdgeOptions;
1110

12-
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
13-
__rewriteFramesDistDir__?: string;
14-
fetch: (...args: unknown[]) => unknown;
15-
};
11+
export { rewriteFramesIntegration };
1612

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

21+
const customDefaultIntegrations = (defaultIntegrations.slice() as Integration[]).concat([rewriteFramesIntegration()]);
22+
2523
const opts = {
2624
_metadata: {} as SdkMetadata,
25+
defaultIntegrations: customDefaultIntegrations,
2726
...options,
2827
};
2928

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

41-
let integrations = opts.integrations || [];
42-
43-
// This value is injected at build time, based on the output directory specified in the build config. Though a default
44-
// is set there, we set it here as well, just in case something has gone wrong with the injection.
45-
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
46-
if (distDirName) {
47-
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one
48-
49-
// Normally we would use `path.resolve` to obtain the absolute path we will strip from the stack frame to align with
50-
// the uploaded artifacts, however we don't have access to that API in edge so we need to be a bit more lax.
51-
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
52-
const SOURCEMAP_FILENAME_REGEX = new RegExp(`.*${escapeStringForRegex(distDirAbsPath)}`);
53-
54-
// eslint-disable-next-line deprecation/deprecation
55-
const defaultRewriteFramesIntegration = new RewriteFrames({
56-
iteratee: frame => {
57-
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next');
58-
return frame;
59-
},
60-
});
61-
62-
integrations = addOrUpdateIntegration(defaultRewriteFramesIntegration, integrations);
63-
}
64-
65-
opts.integrations = integrations;
66-
6740
vercelEdgeInit(opts);
6841
}
6942

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { defineIntegration } from '@sentry/core';
2+
import {
3+
RewriteFrames as OriginalRewriteFrames,
4+
rewriteFramesIntegration as originalRewriteFramesIntegration,
5+
} from '@sentry/integrations';
6+
import type { IntegrationFn, StackFrame } from '@sentry/types';
7+
import { GLOBAL_OBJ, escapeStringForRegex } from '@sentry/utils';
8+
9+
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
10+
__rewriteFramesDistDir__?: string;
11+
};
12+
13+
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
14+
interface RewriteFramesOptions {
15+
root?: string;
16+
prefix?: string;
17+
iteratee?: StackFrameIteratee;
18+
}
19+
20+
export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
21+
// This value is injected at build time, based on the output directory specified in the build config. Though a default
22+
// is set there, we set it here as well, just in case something has gone wrong with the injection.
23+
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
24+
25+
if (distDirName) {
26+
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one
27+
28+
// Normally we would use `path.resolve` to obtain the absolute path we will strip from the stack frame to align with
29+
// the uploaded artifacts, however we don't have access to that API in edge so we need to be a bit more lax.
30+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
31+
const SOURCEMAP_FILENAME_REGEX = new RegExp(`.*${escapeStringForRegex(distDirAbsPath)}`);
32+
33+
return originalRewriteFramesIntegration({
34+
iteratee: frame => {
35+
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next');
36+
return frame;
37+
},
38+
...options,
39+
});
40+
}
41+
42+
// Do nothing if we can't find a distDirName
43+
return {
44+
// eslint-disable-next-line deprecation/deprecation
45+
name: OriginalRewriteFrames.id,
46+
// eslint-disable-next-line @typescript-eslint/no-empty-function
47+
setupOnce: () => {},
48+
processEvent: event => event,
49+
};
50+
}) satisfies IntegrationFn;
51+
52+
export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);

packages/nextjs/src/index.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export declare const Integrations: typeof clientSdk.Integrations &
2727
export declare const defaultIntegrations: Integration[];
2828
export declare const defaultStackParser: StackParser;
2929

30+
// eslint-disable-next-line deprecation/deprecation
31+
export declare const rewriteFramesIntegration: typeof clientSdk.rewriteFramesIntegration;
32+
3033
export declare function getSentryRelease(fallback?: string): string | undefined;
3134

3235
export declare const ErrorBoundary: typeof clientSdk.ErrorBoundary;

0 commit comments

Comments
 (0)