Skip to content

Commit ac803e9

Browse files
authored
feat(core): Add default behaviour for rewriteFramesIntegration in browser (#11535)
1 parent bb9efed commit ac803e9

File tree

2 files changed

+128
-31
lines changed

2 files changed

+128
-31
lines changed

packages/core/src/integrations/rewriteframes.ts

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,61 @@
1-
import type { Event, IntegrationFn, StackFrame, Stacktrace } from '@sentry/types';
2-
import { basename, relative } from '@sentry/utils';
1+
import type { Event, StackFrame, Stacktrace } from '@sentry/types';
2+
import { GLOBAL_OBJ, basename, relative } from '@sentry/utils';
33
import { defineIntegration } from '../integration';
44

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

77
const INTEGRATION_NAME = 'RewriteFrames';
88

99
interface RewriteFramesOptions {
10+
/**
11+
* Root path (the beginning of the path) that will be stripped from the frames' filename.
12+
*
13+
* This option has slightly different behaviour in the browser and on servers:
14+
* - In the browser, the value you provide in `root` will be stripped from the beginning stack frames' paths (if the path started with the value).
15+
* - On the server, the root value will only replace the beginning of stack frame filepaths, when the path is absolute. If no `root` value is provided and the path is absolute, the frame will be reduced to only the filename and the provided `prefix` option.
16+
*
17+
* Browser example:
18+
* - Original frame: `'http://example.com/my/path/static/asset.js'`
19+
* - `root: 'http://example.com/my/path'`
20+
* - `assetPrefix: 'app://'`
21+
* - Resulting frame: `'app:///static/asset.js'`
22+
*
23+
* Server example:
24+
* - Original frame: `'/User/local/my/path/static/asset.js'`
25+
* - `root: '/User/local/my/path'`
26+
* - `assetPrefix: 'app://'`
27+
* - Resulting frame: `'app:///static/asset.js'`
28+
*/
1029
root?: string;
30+
31+
/**
32+
* A custom prefix that stack frames will be prepended with.
33+
*
34+
* Default: `'app://'`
35+
*
36+
* This option has slightly different behaviour in the browser and on servers:
37+
* - In the browser, the value you provide in `prefix` will prefix the resulting filename when the value you provided in `root` was applied. Effectively replacing whatever `root` matched in the beginning of the frame with `prefix`.
38+
* - On the server, the prefix is applied to all stackframes with absolute paths. On Windows, the drive identifier (e.g. "C://") is replaced with the prefix.
39+
*/
1140
prefix?: string;
41+
42+
/**
43+
* Defines an iterator that is used to iterate through all of the stack frames for modification before being sent to Sentry.
44+
* Setting this option will effectively disable both the `root` and the `prefix` options.
45+
*/
1246
iteratee?: StackFrameIteratee;
1347
}
1448

15-
const _rewriteFramesIntegration = ((options: RewriteFramesOptions = {}) => {
49+
/**
50+
* Rewrite event frames paths.
51+
*/
52+
export const rewriteFramesIntegration = defineIntegration((options: RewriteFramesOptions = {}) => {
1653
const root = options.root;
1754
const prefix = options.prefix || 'app:///';
1855

19-
const iteratee: StackFrameIteratee =
20-
options.iteratee ||
21-
((frame: StackFrame) => {
22-
if (!frame.filename) {
23-
return frame;
24-
}
25-
// Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\`
26-
const isWindowsFrame =
27-
/^[a-zA-Z]:\\/.test(frame.filename) ||
28-
// or the presence of a backslash without a forward slash (which are not allowed on Windows)
29-
(frame.filename.includes('\\') && !frame.filename.includes('/'));
30-
// Check if the frame filename begins with `/`
31-
const startsWithSlash = /^\//.test(frame.filename);
32-
if (isWindowsFrame || startsWithSlash) {
33-
const filename = isWindowsFrame
34-
? frame.filename
35-
.replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix
36-
.replace(/\\/g, '/') // replace all `\\` instances with `/`
37-
: frame.filename;
38-
const base = root ? relative(root, filename) : basename(filename);
39-
frame.filename = `${prefix}${base}`;
40-
}
41-
return frame;
42-
});
56+
const isBrowser = 'window' in GLOBAL_OBJ && GLOBAL_OBJ.window !== undefined;
57+
58+
const iteratee: StackFrameIteratee = options.iteratee || generateIteratee({ isBrowser, root, prefix });
4359

4460
/** Process an exception event. */
4561
function _processExceptionsEvent(event: Event): Event {
@@ -81,9 +97,53 @@ const _rewriteFramesIntegration = ((options: RewriteFramesOptions = {}) => {
8197
return processedEvent;
8298
},
8399
};
84-
}) satisfies IntegrationFn;
100+
});
85101

86102
/**
87-
* Rewrite event frames paths.
103+
* Exported only for tests.
88104
*/
89-
export const rewriteFramesIntegration = defineIntegration(_rewriteFramesIntegration);
105+
export function generateIteratee({
106+
isBrowser,
107+
root,
108+
prefix,
109+
}: {
110+
isBrowser: boolean;
111+
root?: string;
112+
prefix: string;
113+
}): StackFrameIteratee {
114+
return (frame: StackFrame) => {
115+
if (!frame.filename) {
116+
return frame;
117+
}
118+
119+
// Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\`
120+
const isWindowsFrame =
121+
/^[a-zA-Z]:\\/.test(frame.filename) ||
122+
// or the presence of a backslash without a forward slash (which are not allowed on Windows)
123+
(frame.filename.includes('\\') && !frame.filename.includes('/'));
124+
125+
// Check if the frame filename begins with `/`
126+
const startsWithSlash = /^\//.test(frame.filename);
127+
128+
if (isBrowser) {
129+
if (root) {
130+
const oldFilename = frame.filename;
131+
if (oldFilename.indexOf(root) === 0) {
132+
frame.filename = oldFilename.replace(root, prefix);
133+
}
134+
}
135+
} else {
136+
if (isWindowsFrame || startsWithSlash) {
137+
const filename = isWindowsFrame
138+
? frame.filename
139+
.replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix
140+
.replace(/\\/g, '/') // replace all `\\` instances with `/`
141+
: frame.filename;
142+
const base = root ? relative(root, filename) : basename(filename);
143+
frame.filename = `${prefix}${base}`;
144+
}
145+
}
146+
147+
return frame;
148+
};
149+
}

packages/core/test/lib/integrations/rewriteframes.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Event, StackFrame } from '@sentry/types';
22

3-
import { rewriteFramesIntegration } from '../../../src/integrations/rewriteframes';
3+
import { generateIteratee, rewriteFramesIntegration } from '../../../src/integrations/rewriteframes';
44

55
let rewriteFrames: ReturnType<typeof rewriteFramesIntegration>;
66
let exceptionEvent: Event;
@@ -11,6 +11,17 @@ let windowsExceptionEventWithoutPrefix: Event;
1111
let windowsExceptionEventWithBackslashPrefix: Event;
1212
let multipleStacktracesEvent: Event;
1313

14+
const originalWindow = global.window;
15+
16+
beforeAll(() => {
17+
// @ts-expect-error We need to do this because the integration has different behaviour on the browser and on the client
18+
global.window = undefined;
19+
});
20+
21+
afterAll(() => {
22+
global.window = originalWindow;
23+
});
24+
1425
describe('RewriteFrames', () => {
1526
beforeEach(() => {
1627
exceptionEvent = {
@@ -298,4 +309,30 @@ describe('RewriteFrames', () => {
298309
expect(rewriteFrames.processEvent?.(brokenEvent, {}, {} as any)).toEqual(brokenEvent);
299310
});
300311
});
312+
313+
describe('generateIteratee()', () => {
314+
describe('on the browser', () => {
315+
it('should replace the `root` value in the filename with the `assetPrefix` value', () => {
316+
const iteratee = generateIteratee({
317+
isBrowser: true,
318+
prefix: 'my-prefix://',
319+
root: 'http://example.com/my/path',
320+
});
321+
322+
const result = iteratee({ filename: 'http://example.com/my/path/static/asset.js' });
323+
expect(result.filename).toBe('my-prefix:///static/asset.js');
324+
});
325+
326+
it('should replace not the `root` value in the filename with the `assetPrefix` value, if the root value is not at the beginning of the frame', () => {
327+
const iteratee = generateIteratee({
328+
isBrowser: true,
329+
prefix: 'my-prefix://',
330+
root: '/my/path',
331+
});
332+
333+
const result = iteratee({ filename: 'http://example.com/my/path/static/asset.js' });
334+
expect(result.filename).toBe('http://example.com/my/path/static/asset.js'); // unchanged
335+
});
336+
});
337+
});
301338
});

0 commit comments

Comments
 (0)