Skip to content

Commit 99d942f

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 14bf0a0 commit 99d942f

File tree

9 files changed

+247
-108
lines changed

9 files changed

+247
-108
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
2+
import {
3+
RewriteFrames as OriginalRewriteFrames,
4+
rewriteFramesIntegration as originalRewriteFramesIntegration,
5+
} from '@sentry/integrations';
6+
import type { Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types';
7+
8+
const globalWithInjectedValues = global as typeof global & {
9+
__rewriteFramesAssetPrefixPath__: string;
10+
};
11+
12+
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
13+
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 assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';
24+
25+
return originalRewriteFramesIntegration({
26+
// Turn `<origin>/<path>/_next/static/...` into `app:///_next/static/...`
27+
iteratee: frame => {
28+
try {
29+
const { origin } = new URL(frame.filename as string);
30+
frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, '');
31+
} catch (err) {
32+
// Filename wasn't a properly formed URL, so there's nothing we can do
33+
}
34+
35+
// 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.
36+
// 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.
37+
if (frame.filename && frame.filename.startsWith('app:///_next')) {
38+
frame.filename = decodeURI(frame.filename);
39+
}
40+
41+
if (
42+
frame.filename &&
43+
frame.filename.match(
44+
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
45+
)
46+
) {
47+
// We don't care about these frames. It's Next.js internal code.
48+
frame.in_app = false;
49+
}
50+
51+
return frame;
52+
},
53+
...options,
54+
});
55+
}) satisfies IntegrationFn;
56+
57+
export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);
58+
59+
/**
60+
* Rewrite event frames paths.
61+
* @deprecated Use `rewriteFramesIntegration()` instead.
62+
*/
63+
// eslint-disable-next-line deprecation/deprecation
64+
export const RewriteFrames = convertIntegrationFnToClass(
65+
// eslint-disable-next-line deprecation/deprecation
66+
OriginalRewriteFrames.id,
67+
rewriteFramesIntegration,
68+
) as IntegrationClass<Integration & { processEvent: (event: Event) => Event }> & {
69+
new (options?: {
70+
root?: string;
71+
prefix?: string;
72+
iteratee?: StackFrameIteratee;
73+
}): Integration;
74+
};

packages/nextjs/src/client/index.ts

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { hasTracingEnabled } from '@sentry/core';
2-
import { RewriteFrames } from '@sentry/integrations';
3-
import type { BrowserOptions } from '@sentry/react';
2+
import type { BrowserOptions} from '@sentry/react';
3+
import { defaultIntegrations } from '@sentry/react';
44
import {
55
BrowserTracing,
66
Integrations,
@@ -14,12 +14,18 @@ import { addOrUpdateIntegration } from '@sentry/utils';
1414
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
1515
import { getVercelEnv } from '../common/getVercelEnv';
1616
import { buildMetadata } from '../common/metadata';
17+
import { RewriteFrames } from './RewriteFrames';
1718
import { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
1819
import { applyTunnelRouteOption } from './tunnelRoute';
1920

2021
export * from '@sentry/react';
2122
export { nextRouterInstrumentation } from './routing/nextRoutingInstrumentation';
2223
export { captureUnderscoreErrorException } from '../common/_error';
24+
export {
25+
rewriteFramesIntegration,
26+
// eslint-disable-next-line deprecation/deprecation
27+
RewriteFrames,
28+
} from './RewriteFrames';
2329

2430
export { Integrations };
2531

@@ -38,14 +44,16 @@ export { BrowserTracing };
3844
// Treeshakable guard to remove all code related to tracing
3945
declare const __SENTRY_TRACING__: boolean;
4046

41-
const globalWithInjectedValues = global as typeof global & {
42-
__rewriteFramesAssetPrefixPath__: string;
43-
};
44-
4547
/** Inits the Sentry NextJS SDK on the browser with the React SDK. */
4648
export function init(options: BrowserOptions): void {
49+
const customDefaultIntegrations = defaultIntegrations.slice().concat([
50+
// eslint-disable-next-line deprecation/deprecation
51+
new RewriteFrames(),
52+
]);
53+
4754
const opts = {
4855
environment: getVercelEnv(true) || process.env.NODE_ENV,
56+
defaultIntegrations: customDefaultIntegrations,
4957
...options,
5058
};
5159

@@ -71,42 +79,6 @@ export function init(options: BrowserOptions): void {
7179
function addClientIntegrations(options: BrowserOptions): void {
7280
let integrations = options.integrations || [];
7381

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);
109-
11082
// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside
11183
// will get treeshaken away
11284
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
2+
import {
3+
RewriteFrames as OriginalRewriteFrames,
4+
rewriteFramesIntegration as originalRewriteFramesIntegration,
5+
} from '@sentry/integrations';
6+
import type { Event, Integration, IntegrationClass, 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+
let iteratee = (frame: StackFrame): StackFrame => frame;
26+
27+
if (distDirName) {
28+
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one
29+
30+
// Normally we would use `path.resolve` to obtain the absolute path we will strip from the stack frame to align with
31+
// the uploaded artifacts, however we don't have access to that API in edge so we need to be a bit more lax.
32+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
33+
const SOURCEMAP_FILENAME_REGEX = new RegExp(`.*${escapeStringForRegex(distDirAbsPath)}`);
34+
35+
iteratee = frame => {
36+
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next');
37+
return frame;
38+
};
39+
}
40+
41+
return originalRewriteFramesIntegration({
42+
iteratee,
43+
...options,
44+
});
45+
}) satisfies IntegrationFn;
46+
47+
export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);
48+
49+
/**
50+
* Rewrite event frames paths.
51+
* @deprecated Use `rewriteFramesIntegration()` instead.
52+
*/
53+
// eslint-disable-next-line deprecation/deprecation
54+
export const RewriteFrames = convertIntegrationFnToClass(
55+
// eslint-disable-next-line deprecation/deprecation
56+
OriginalRewriteFrames.id,
57+
rewriteFramesIntegration,
58+
) as IntegrationClass<Integration & { processEvent: (event: Event) => Event }> & {
59+
new (options?: {
60+
root?: string;
61+
prefix?: string;
62+
iteratee?: StackFrameIteratee;
63+
}): Integration;
64+
};

packages/nextjs/src/edge/index.ts

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
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';
4+
import { defaultIntegrations } from '@sentry/vercel-edge';
65
import { init as vercelEdgeInit } from '@sentry/vercel-edge';
76

87
import { isBuild } from '../common/utils/isBuild';
8+
import { RewriteFrames } from './RewriteFrames';
9+
export {
10+
rewriteFramesIntegration, // eslint-disable-next-line deprecation/deprecation
11+
RewriteFrames,
12+
} from './RewriteFrames';
913

1014
export type EdgeOptions = VercelEdgeOptions;
1115

12-
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
13-
__rewriteFramesDistDir__?: string;
14-
fetch: (...args: unknown[]) => unknown;
15-
};
16-
1716
/** Inits the Sentry NextJS SDK on the Edge Runtime. */
1817
export function init(options: VercelEdgeOptions = {}): void {
1918
addTracingExtensions();
@@ -22,8 +21,14 @@ export function init(options: VercelEdgeOptions = {}): void {
2221
return;
2322
}
2423

24+
const customDefaultIntegrations = (defaultIntegrations.slice() as Integration[]).concat([
25+
// eslint-disable-next-line deprecation/deprecation
26+
new RewriteFrames(),
27+
]);
28+
2529
const opts = {
2630
_metadata: {} as SdkMetadata,
31+
defaultIntegrations: customDefaultIntegrations,
2732
...options,
2833
};
2934

@@ -38,32 +43,6 @@ export function init(options: VercelEdgeOptions = {}): void {
3843
version: SDK_VERSION,
3944
};
4045

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-
6746
vercelEdgeInit(opts);
6847
}
6948

packages/nextjs/src/index.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ 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 RewriteFrames: typeof clientSdk.RewriteFrames;
32+
export declare const rewriteFramesIntegration: typeof clientSdk.rewriteFramesIntegration;
33+
3034
export declare function getSentryRelease(fallback?: string): string | undefined;
3135

3236
export declare const ErrorBoundary: typeof clientSdk.ErrorBoundary;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as path from 'path';
2+
import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core';
3+
import {
4+
RewriteFrames as OriginalRewriteFrames,
5+
rewriteFramesIntegration as originalRewriteFramesIntegration,
6+
} from '@sentry/integrations';
7+
import type { Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types';
8+
import { escapeStringForRegex } from '@sentry/utils';
9+
10+
const globalWithInjectedValues = global as typeof global & {
11+
__rewriteFramesDistDir__?: string;
12+
};
13+
14+
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
15+
interface RewriteFramesOptions {
16+
root?: string;
17+
prefix?: string;
18+
iteratee?: StackFrameIteratee;
19+
}
20+
21+
export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
22+
// This value is injected at build time, based on the output directory specified in the build config. Though a default
23+
// is set there, we set it here as well, just in case something has gone wrong with the injection.
24+
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
25+
26+
let iteratee = (frame: StackFrame): StackFrame => frame;
27+
28+
if (distDirName) {
29+
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so
30+
// we can read in the project directory from the currently running process
31+
const distDirAbsPath = path.resolve(distDirName).replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one
32+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- user input is escaped
33+
const SOURCEMAP_FILENAME_REGEX = new RegExp(escapeStringForRegex(distDirAbsPath));
34+
35+
iteratee = frame => {
36+
frame.filename = frame.filename?.replace(SOURCEMAP_FILENAME_REGEX, 'app:///_next');
37+
return frame;
38+
};
39+
}
40+
41+
return originalRewriteFramesIntegration({
42+
iteratee,
43+
...options,
44+
});
45+
}) satisfies IntegrationFn;
46+
47+
export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);
48+
49+
/**
50+
* Rewrite event frames paths.
51+
* @deprecated Use `rewriteFramesIntegration()` instead.
52+
*/
53+
// eslint-disable-next-line deprecation/deprecation
54+
export const RewriteFrames = convertIntegrationFnToClass(
55+
// eslint-disable-next-line deprecation/deprecation
56+
OriginalRewriteFrames.id,
57+
rewriteFramesIntegration,
58+
) as IntegrationClass<Integration & { processEvent: (event: Event) => Event }> & {
59+
new (options?: {
60+
root?: string;
61+
prefix?: string;
62+
iteratee?: StackFrameIteratee;
63+
}): Integration;
64+
};

0 commit comments

Comments
 (0)