Skip to content

Commit 62b0c4d

Browse files
authored
feat(sveltekit): Update default integration handling & deprecate addOrUpdateIntegration (#10263)
This updates the last usage of `addOrUpdateIntegration` and deprecates it.
1 parent a682534 commit 62b0c4d

File tree

11 files changed

+253
-190
lines changed

11 files changed

+253
-190
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { BrowserTracing as OriginalBrowserTracing } from '@sentry/svelte';
2+
import { svelteKitRoutingInstrumentation } from './router';
3+
4+
/**
5+
* A custom BrowserTracing integration for Sveltekit.
6+
*/
7+
export class BrowserTracing extends OriginalBrowserTracing {
8+
public constructor(options?: ConstructorParameters<typeof OriginalBrowserTracing>[0]) {
9+
super({
10+
routingInstrumentation: svelteKitRoutingInstrumentation,
11+
...options,
12+
});
13+
}
14+
}

packages/sveltekit/src/client/sdk.ts

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { applySdkMetadata, hasTracingEnabled } from '@sentry/core';
22
import type { BrowserOptions } from '@sentry/svelte';
3-
import { BrowserTracing, WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte';
4-
import { addOrUpdateIntegration } from '@sentry/utils';
3+
import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte';
4+
import { WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte';
5+
import type { Integration } from '@sentry/types';
56

6-
import { svelteKitRoutingInstrumentation } from './router';
7+
import { BrowserTracing } from './browserTracingIntegration';
78

89
type WindowWithSentryFetchProxy = typeof WINDOW & {
910
_sentryFetchProxy?: typeof fetch;
@@ -18,15 +19,20 @@ declare const __SENTRY_TRACING__: boolean;
1819
* @param options Configuration options for the SDK.
1920
*/
2021
export function init(options: BrowserOptions): void {
21-
applySdkMetadata(options, 'sveltekit', ['sveltekit', 'svelte']);
22+
const opts = {
23+
defaultIntegrations: getDefaultIntegrations(options),
24+
...options,
25+
};
2226

23-
addClientIntegrations(options);
27+
applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'svelte']);
28+
29+
fixBrowserTracingIntegration(opts);
2430

2531
// 1. Switch window.fetch to our fetch proxy we injected earlier
2632
const actualFetch = switchToFetchProxy();
2733

2834
// 2. Initialize the SDK which will instrument our proxy
29-
initSvelteSdk(options);
35+
initSvelteSdk(opts);
3036

3137
// 3. Restore the original fetch now that our proxy is instrumented
3238
if (actualFetch) {
@@ -36,24 +42,49 @@ export function init(options: BrowserOptions): void {
3642
getCurrentScope().setTag('runtime', 'browser');
3743
}
3844

39-
function addClientIntegrations(options: BrowserOptions): void {
40-
let integrations = options.integrations || [];
45+
// TODO v8: Remove this again
46+
// We need to handle BrowserTracing passed to `integrations` that comes from `@sentry/tracing`, not `@sentry/sveltekit` :(
47+
function fixBrowserTracingIntegration(options: BrowserOptions): void {
48+
const { integrations } = options;
49+
if (!integrations) {
50+
return;
51+
}
52+
53+
if (Array.isArray(integrations)) {
54+
options.integrations = maybeUpdateBrowserTracingIntegration(integrations);
55+
} else {
56+
options.integrations = defaultIntegrations => {
57+
const userFinalIntegrations = integrations(defaultIntegrations);
58+
59+
return maybeUpdateBrowserTracingIntegration(userFinalIntegrations);
60+
};
61+
}
62+
}
63+
64+
function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] {
65+
const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing');
66+
// If BrowserTracing was added, but it is not our forked version,
67+
// replace it with our forked version with the same options
68+
if (browserTracing && !(browserTracing instanceof BrowserTracing)) {
69+
const options: ConstructorParameters<typeof BrowserTracing>[0] = (browserTracing as BrowserTracing).options;
70+
// This option is overwritten by the custom integration
71+
delete options.routingInstrumentation;
72+
integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options);
73+
}
74+
75+
return integrations;
76+
}
4177

42-
// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false",
43-
// in which case everything inside will get treeshaken away
78+
function getDefaultIntegrations(options: BrowserOptions): Integration[] | undefined {
79+
// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside
80+
// will get treeshaken away
4481
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
4582
if (hasTracingEnabled(options)) {
46-
const defaultBrowserTracingIntegration = new BrowserTracing({
47-
routingInstrumentation: svelteKitRoutingInstrumentation,
48-
});
49-
50-
integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, {
51-
'options.routingInstrumentation': svelteKitRoutingInstrumentation,
52-
});
83+
return [...getDefaultSvelteIntegrations(options), new BrowserTracing()];
5384
}
5485
}
5586

56-
options.integrations = integrations;
87+
return undefined;
5788
}
5889

5990
/**
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { defineIntegration } from '@sentry/core';
2+
import { rewriteFramesIntegration as originalRewriteFramesIntegration } from '@sentry/integrations';
3+
import type { IntegrationFn, StackFrame } from '@sentry/types';
4+
import { GLOBAL_OBJ, basename, escapeStringForRegex, join } from '@sentry/utils';
5+
import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument';
6+
import type { GlobalWithSentryValues } from '../vite/injectGlobalValues';
7+
8+
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
9+
interface RewriteFramesOptions {
10+
root?: string;
11+
prefix?: string;
12+
iteratee?: StackFrameIteratee;
13+
}
14+
15+
export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
16+
return originalRewriteFramesIntegration({
17+
iteratee: rewriteFramesIteratee,
18+
...options,
19+
});
20+
}) satisfies IntegrationFn;
21+
22+
export const rewriteFramesIntegration = defineIntegration(customRewriteFramesIntegration);
23+
24+
/**
25+
* A custom iteratee function for the `RewriteFrames` integration.
26+
*
27+
* Does the same as the default iteratee, but also removes the `module` property from the
28+
* frame to improve issue grouping.
29+
*
30+
* For some reason, our stack trace processing pipeline isn't able to resolve the bundled
31+
* module name to the original file name correctly, leading to individual error groups for
32+
* each module. Removing the `module` field makes the grouping algorithm fall back to the
33+
* `filename` field, which is correctly resolved and hence grouping works as expected.
34+
*
35+
* Exported for tests only.
36+
*/
37+
export function rewriteFramesIteratee(frame: StackFrame): StackFrame {
38+
if (!frame.filename) {
39+
return frame;
40+
}
41+
const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ;
42+
const svelteKitBuildOutDir = globalWithSentryValues.__sentry_sveltekit_output_dir;
43+
const prefix = 'app:///';
44+
45+
// Check if the frame filename begins with `/` or a Windows-style prefix such as `C:\`
46+
const isWindowsFrame = /^[a-zA-Z]:\\/.test(frame.filename);
47+
const startsWithSlash = /^\//.test(frame.filename);
48+
if (isWindowsFrame || startsWithSlash) {
49+
const filename = isWindowsFrame
50+
? frame.filename
51+
.replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix
52+
.replace(/\\/g, '/') // replace all `\\` instances with `/`
53+
: frame.filename;
54+
55+
let strippedFilename;
56+
if (svelteKitBuildOutDir) {
57+
strippedFilename = filename.replace(
58+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
59+
new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`),
60+
'',
61+
);
62+
} else {
63+
strippedFilename = basename(filename);
64+
}
65+
frame.filename = `${prefix}${strippedFilename}`;
66+
}
67+
68+
delete frame.module;
69+
70+
// In dev-mode, the WRAPPED_MODULE_SUFFIX is still present in the frame's file name.
71+
// We need to remove it to make sure that the frame's filename matches the actual file
72+
if (frame.filename.endsWith(WRAPPED_MODULE_SUFFIX)) {
73+
frame.filename = frame.filename.slice(0, -WRAPPED_MODULE_SUFFIX.length);
74+
}
75+
76+
return frame;
77+
}

packages/sveltekit/src/server/sdk.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,23 @@
11
import { applySdkMetadata, getCurrentScope } from '@sentry/core';
2-
import { RewriteFrames } from '@sentry/integrations';
32
import type { NodeOptions } from '@sentry/node';
3+
import { getDefaultIntegrations as getDefaultNodeIntegrations } from '@sentry/node';
44
import { init as initNodeSdk } from '@sentry/node';
5-
import { addOrUpdateIntegration } from '@sentry/utils';
65

7-
import { rewriteFramesIteratee } from './utils';
6+
import { rewriteFramesIntegration } from './rewriteFramesIntegration';
87

98
/**
109
*
1110
* @param options
1211
*/
1312
export function init(options: NodeOptions): void {
14-
applySdkMetadata(options, 'sveltekit', ['sveltekit', 'node']);
13+
const opts = {
14+
defaultIntegrations: [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()],
15+
...options,
16+
};
1517

16-
addServerIntegrations(options);
18+
applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'node']);
1719

18-
initNodeSdk(options);
20+
initNodeSdk(opts);
1921

2022
getCurrentScope().setTag('runtime', 'node');
2123
}
22-
23-
function addServerIntegrations(options: NodeOptions): void {
24-
options.integrations = addOrUpdateIntegration(
25-
// eslint-disable-next-line deprecation/deprecation
26-
new RewriteFrames({ iteratee: rewriteFramesIteratee }),
27-
options.integrations || [],
28-
);
29-
}

packages/sveltekit/src/server/utils.ts

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { flush } from '@sentry/node';
2-
import type { StackFrame } from '@sentry/types';
3-
import { GLOBAL_OBJ, basename, escapeStringForRegex, join, logger, tracingContextFromHeaders } from '@sentry/utils';
2+
import { logger, tracingContextFromHeaders } from '@sentry/utils';
43
import type { RequestEvent } from '@sveltejs/kit';
54

65
import { DEBUG_BUILD } from '../common/debug-build';
7-
import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument';
8-
import type { GlobalWithSentryValues } from '../vite/injectGlobalValues';
96

107
/**
118
* Takes a request event and extracts traceparent and DSC data
@@ -19,59 +16,6 @@ export function getTracePropagationData(event: RequestEvent): ReturnType<typeof
1916
return tracingContextFromHeaders(sentryTraceHeader, baggageHeader);
2017
}
2118

22-
/**
23-
* A custom iteratee function for the `RewriteFrames` integration.
24-
*
25-
* Does the same as the default iteratee, but also removes the `module` property from the
26-
* frame to improve issue grouping.
27-
*
28-
* For some reason, our stack trace processing pipeline isn't able to resolve the bundled
29-
* module name to the original file name correctly, leading to individual error groups for
30-
* each module. Removing the `module` field makes the grouping algorithm fall back to the
31-
* `filename` field, which is correctly resolved and hence grouping works as expected.
32-
*/
33-
export function rewriteFramesIteratee(frame: StackFrame): StackFrame {
34-
if (!frame.filename) {
35-
return frame;
36-
}
37-
const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ;
38-
const svelteKitBuildOutDir = globalWithSentryValues.__sentry_sveltekit_output_dir;
39-
const prefix = 'app:///';
40-
41-
// Check if the frame filename begins with `/` or a Windows-style prefix such as `C:\`
42-
const isWindowsFrame = /^[a-zA-Z]:\\/.test(frame.filename);
43-
const startsWithSlash = /^\//.test(frame.filename);
44-
if (isWindowsFrame || startsWithSlash) {
45-
const filename = isWindowsFrame
46-
? frame.filename
47-
.replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix
48-
.replace(/\\/g, '/') // replace all `\\` instances with `/`
49-
: frame.filename;
50-
51-
let strippedFilename;
52-
if (svelteKitBuildOutDir) {
53-
strippedFilename = filename.replace(
54-
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
55-
new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`),
56-
'',
57-
);
58-
} else {
59-
strippedFilename = basename(filename);
60-
}
61-
frame.filename = `${prefix}${strippedFilename}`;
62-
}
63-
64-
delete frame.module;
65-
66-
// In dev-mode, the WRAPPED_MODULE_SUFFIX is still present in the frame's file name.
67-
// We need to remove it to make sure that the frame's filename matches the actual file
68-
if (frame.filename.endsWith(WRAPPED_MODULE_SUFFIX)) {
69-
frame.filename = frame.filename.slice(0, -WRAPPED_MODULE_SUFFIX.length);
70-
}
71-
72-
return frame;
73-
}
74-
7519
/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */
7620
export async function flushIfServerless(): Promise<void> {
7721
const platformSupportsStreaming = !process.env.LAMBDA_TASK_ROOT && !process.env.VERCEL;

packages/sveltekit/test/client/sdk.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ describe('Sentry client SDK', () => {
6161
...tracingOptions,
6262
});
6363

64-
const integrationsToInit = svelteInit.mock.calls[0][0].integrations;
6564
const browserTracing = getClient<BrowserClient>()?.getIntegrationByName('BrowserTracing');
66-
67-
expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
6865
expect(browserTracing).toBeDefined();
6966
});
7067

@@ -77,10 +74,7 @@ describe('Sentry client SDK', () => {
7774
...tracingOptions,
7875
});
7976

80-
const integrationsToInit = svelteInit.mock.calls[0][0].integrations;
8177
const browserTracing = getClient<BrowserClient>()?.getIntegrationByName('BrowserTracing');
82-
83-
expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
8478
expect(browserTracing).toBeUndefined();
8579
});
8680

@@ -96,10 +90,7 @@ describe('Sentry client SDK', () => {
9690
enableTracing: true,
9791
});
9892

99-
const integrationsToInit = svelteInit.mock.calls[0][0].integrations;
10093
const browserTracing = getClient<BrowserClient>()?.getIntegrationByName('BrowserTracing');
101-
102-
expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
10394
expect(browserTracing).toBeUndefined();
10495

10596
// @ts-expect-error this is fine in the test
@@ -113,12 +104,9 @@ describe('Sentry client SDK', () => {
113104
enableTracing: true,
114105
});
115106

116-
const integrationsToInit = svelteInit.mock.calls[0][0].integrations;
117-
118107
const browserTracing = getClient<BrowserClient>()?.getIntegrationByName('BrowserTracing') as BrowserTracing;
119108
const options = browserTracing.options;
120109

121-
expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
122110
expect(browserTracing).toBeDefined();
123111

124112
// This shows that the user-configured options are still here

0 commit comments

Comments
 (0)