Skip to content

Commit 9ff9cdb

Browse files
committed
ref(sveltekit): Streamline how default integrations are added
1 parent 5f0b506 commit 9ff9cdb

File tree

5 files changed

+148
-90
lines changed

5 files changed

+148
-90
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 Next.js.
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+
};
26+
27+
applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'svelte']);
2228

23-
addClientIntegrations(options);
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+
}
4163

42-
// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false",
43-
// in which case everything inside will get treeshaken away
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+
}
77+
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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
function rewriteFramesIteratee(frame: StackFrame): StackFrame {
36+
if (!frame.filename) {
37+
return frame;
38+
}
39+
const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ;
40+
const svelteKitBuildOutDir = globalWithSentryValues.__sentry_sveltekit_output_dir;
41+
const prefix = 'app:///';
42+
43+
// Check if the frame filename begins with `/` or a Windows-style prefix such as `C:\`
44+
const isWindowsFrame = /^[a-zA-Z]:\\/.test(frame.filename);
45+
const startsWithSlash = /^\//.test(frame.filename);
46+
if (isWindowsFrame || startsWithSlash) {
47+
const filename = isWindowsFrame
48+
? frame.filename
49+
.replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix
50+
.replace(/\\/g, '/') // replace all `\\` instances with `/`
51+
: frame.filename;
52+
53+
let strippedFilename;
54+
if (svelteKitBuildOutDir) {
55+
strippedFilename = filename.replace(
56+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
57+
new RegExp(`^.*${escapeStringForRegex(join(svelteKitBuildOutDir, 'server'))}/`),
58+
'',
59+
);
60+
} else {
61+
strippedFilename = basename(filename);
62+
}
63+
frame.filename = `${prefix}${strippedFilename}`;
64+
}
65+
66+
delete frame.module;
67+
68+
// In dev-mode, the WRAPPED_MODULE_SUFFIX is still present in the frame's file name.
69+
// We need to remove it to make sure that the frame's filename matches the actual file
70+
if (frame.filename.endsWith(WRAPPED_MODULE_SUFFIX)) {
71+
frame.filename = frame.filename.slice(0, -WRAPPED_MODULE_SUFFIX.length);
72+
}
73+
74+
return frame;
75+
}
Lines changed: 9 additions & 15 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+
};
17+
18+
applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'node']);
1519

16-
addServerIntegrations(options);
17-
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;

0 commit comments

Comments
 (0)