Skip to content

Commit 948e7d3

Browse files
authored
feat(sveltekit): Add options to configure fetch instrumentation script for CSP (#9969)
This PR adds options to the `sentryHandle` request handler that allows control over CSP-relevant aspects for the `<script>` that the request handler injects into the page. Previously, the injected script was blocked by browsers if CSP was enabled, due to inline `<script>`s not being allowed without a nonce or hash. Now users can specify a nonce or disable the script injection
1 parent f56219a commit 948e7d3

File tree

2 files changed

+89
-23
lines changed

2 files changed

+89
-23
lines changed

packages/sveltekit/src/server/handle.ts

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ export type SentryHandleOptions = {
2525
* @default false
2626
*/
2727
handleUnknownRoutes?: boolean;
28+
29+
/**
30+
* Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation
31+
* of `fetch` calls in `load` functions.
32+
*
33+
* @default true
34+
*/
35+
injectFetchProxyScript?: boolean;
36+
37+
/**
38+
* If this option is set, the `sentryHandle` handler will add a nonce attribute to the script
39+
* tag it injects into the page. This script is used to enable instrumentation of `fetch` calls
40+
* in `load` functions.
41+
*
42+
* Use this if your CSP policy blocks the fetch proxy script injected by `sentryHandle`.
43+
*/
44+
fetchProxyScriptNonce?: string;
2845
};
2946

3047
function sendErrorToSentry(e: unknown): unknown {
@@ -53,30 +70,51 @@ function sendErrorToSentry(e: unknown): unknown {
5370
return objectifiedErr;
5471
}
5572

56-
const FETCH_PROXY_SCRIPT = `
73+
/**
74+
* Exported only for testing
75+
*/
76+
export const FETCH_PROXY_SCRIPT = `
5777
const f = window.fetch;
5878
if(f){
5979
window._sentryFetchProxy = function(...a){return f(...a)}
6080
window.fetch = function(...a){return window._sentryFetchProxy(...a)}
6181
}
6282
`;
6383

64-
export const transformPageChunk: NonNullable<ResolveOptions['transformPageChunk']> = ({ html }) => {
65-
const transaction = getActiveTransaction();
66-
if (transaction) {
67-
const traceparentData = transaction.toTraceparent();
68-
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(transaction.getDynamicSamplingContext());
69-
const content = `<head>
70-
<meta name="sentry-trace" content="${traceparentData}"/>
71-
<meta name="baggage" content="${dynamicSamplingContext}"/>
72-
<script>${FETCH_PROXY_SCRIPT}
73-
</script>
74-
`;
75-
return html.replace('<head>', content);
76-
}
84+
/**
85+
* Adds Sentry tracing <meta> tags to the returned html page.
86+
* Adds Sentry fetch proxy script to the returned html page if enabled in options.
87+
* Also adds a nonce attribute to the script tag if users specified one for CSP.
88+
*
89+
* Exported only for testing
90+
*/
91+
export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable<ResolveOptions['transformPageChunk']> {
92+
const { fetchProxyScriptNonce, injectFetchProxyScript } = options;
93+
// if injectFetchProxyScript is not set, we default to true
94+
const shouldInjectScript = injectFetchProxyScript !== false;
95+
const nonce = fetchProxyScriptNonce ? `nonce="${fetchProxyScriptNonce}"` : '';
7796

78-
return html;
79-
};
97+
return ({ html }) => {
98+
const transaction = getActiveTransaction();
99+
if (transaction) {
100+
const traceparentData = transaction.toTraceparent();
101+
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(
102+
transaction.getDynamicSamplingContext(),
103+
);
104+
const contentMeta = `<head>
105+
<meta name="sentry-trace" content="${traceparentData}"/>
106+
<meta name="baggage" content="${dynamicSamplingContext}"/>
107+
`;
108+
const contentScript = shouldInjectScript ? `<script ${nonce}>${FETCH_PROXY_SCRIPT}</script>` : '';
109+
110+
const content = `${contentMeta}\n${contentScript}`;
111+
112+
return html.replace('<head>', content);
113+
}
114+
115+
return html;
116+
};
117+
}
80118

81119
/**
82120
* A SvelteKit handle function that wraps the request for Sentry error and
@@ -89,13 +127,14 @@ export const transformPageChunk: NonNullable<ResolveOptions['transformPageChunk'
89127
*
90128
* export const handle = sentryHandle();
91129
*
92-
* // Optionally use the sequence function to add additional handlers.
130+
* // Optionally use the `sequence` function to add additional handlers.
93131
* // export const handle = sequence(sentryHandle(), yourCustomHandler);
94132
* ```
95133
*/
96134
export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
97135
const options = {
98136
handleUnknownRoutes: false,
137+
injectFetchProxyScript: true,
99138
...handlerOptions,
100139
};
101140

@@ -139,7 +178,9 @@ async function instrumentHandle(
139178
},
140179
},
141180
async (span?: Span) => {
142-
const res = await resolve(event, { transformPageChunk });
181+
const res = await resolve(event, {
182+
transformPageChunk: addSentryCodeToPage(options),
183+
});
143184
if (span) {
144185
span.setHttpStatus(res.status);
145186
}

packages/sveltekit/test/server/handle.test.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Handle } from '@sveltejs/kit';
66
import { redirect } from '@sveltejs/kit';
77
import { vi } from 'vitest';
88

9-
import { sentryHandle, transformPageChunk } from '../../src/server/handle';
9+
import { FETCH_PROXY_SCRIPT, addSentryCodeToPage, sentryHandle } from '../../src/server/handle';
1010
import { getDefaultNodeClientOptions } from '../utils';
1111

1212
const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx');
@@ -337,7 +337,7 @@ describe('handleSentry', () => {
337337
});
338338
});
339339

340-
describe('transformPageChunk', () => {
340+
describe('addSentryCodeToPage', () => {
341341
const html = `<!DOCTYPE html>
342342
<html lang="en">
343343
<head>
@@ -351,16 +351,41 @@ describe('transformPageChunk', () => {
351351
</html>`;
352352

353353
it('does not add meta tags if no active transaction', () => {
354+
const transformPageChunk = addSentryCodeToPage({});
354355
const transformed = transformPageChunk({ html, done: true });
355356
expect(transformed).toEqual(html);
356357
});
357358

358-
it('adds meta tags if there is an active transaction', () => {
359+
it('adds meta tags and the fetch proxy script if there is an active transaction', () => {
360+
const transformPageChunk = addSentryCodeToPage({});
359361
const transaction = hub.startTransaction({ name: 'test' });
360362
hub.getScope().setSpan(transaction);
361363
const transformed = transformPageChunk({ html, done: true }) as string;
362364

363-
expect(transformed.includes('<meta name="sentry-trace"')).toEqual(true);
364-
expect(transformed.includes('<meta name="baggage"')).toEqual(true);
365+
expect(transformed).toContain('<meta name="sentry-trace"');
366+
expect(transformed).toContain('<meta name="baggage"');
367+
expect(transformed).toContain(`<script >${FETCH_PROXY_SCRIPT}</script>`);
368+
});
369+
370+
it('adds a nonce attribute to the script if the `fetchProxyScriptNonce` option is specified', () => {
371+
const transformPageChunk = addSentryCodeToPage({ fetchProxyScriptNonce: '123abc' });
372+
const transaction = hub.startTransaction({ name: 'test' });
373+
hub.getScope().setSpan(transaction);
374+
const transformed = transformPageChunk({ html, done: true }) as string;
375+
376+
expect(transformed).toContain('<meta name="sentry-trace"');
377+
expect(transformed).toContain('<meta name="baggage"');
378+
expect(transformed).toContain(`<script nonce="123abc">${FETCH_PROXY_SCRIPT}</script>`);
379+
});
380+
381+
it('does not add the fetch proxy script if the `injectFetchProxyScript` option is false', () => {
382+
const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false });
383+
const transaction = hub.startTransaction({ name: 'test' });
384+
hub.getScope().setSpan(transaction);
385+
const transformed = transformPageChunk({ html, done: true }) as string;
386+
387+
expect(transformed).toContain('<meta name="sentry-trace"');
388+
expect(transformed).toContain('<meta name="baggage"');
389+
expect(transformed).not.toContain(`<script >${FETCH_PROXY_SCRIPT}</script>`);
365390
});
366391
});

0 commit comments

Comments
 (0)