Skip to content

Commit

Permalink
feat(sveltekit): Add options to configure fetch instrumentation scrip…
Browse files Browse the repository at this point in the history
…t 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
  • Loading branch information
Lms24 authored Dec 22, 2023
1 parent f56219a commit 948e7d3
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 23 deletions.
77 changes: 59 additions & 18 deletions packages/sveltekit/src/server/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ export type SentryHandleOptions = {
* @default false
*/
handleUnknownRoutes?: boolean;

/**
* Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation
* of `fetch` calls in `load` functions.
*
* @default true
*/
injectFetchProxyScript?: boolean;

/**
* If this option is set, the `sentryHandle` handler will add a nonce attribute to the script
* tag it injects into the page. This script is used to enable instrumentation of `fetch` calls
* in `load` functions.
*
* Use this if your CSP policy blocks the fetch proxy script injected by `sentryHandle`.
*/
fetchProxyScriptNonce?: string;
};

function sendErrorToSentry(e: unknown): unknown {
Expand Down Expand Up @@ -53,30 +70,51 @@ function sendErrorToSentry(e: unknown): unknown {
return objectifiedErr;
}

const FETCH_PROXY_SCRIPT = `
/**
* Exported only for testing
*/
export const FETCH_PROXY_SCRIPT = `
const f = window.fetch;
if(f){
window._sentryFetchProxy = function(...a){return f(...a)}
window.fetch = function(...a){return window._sentryFetchProxy(...a)}
}
`;

export const transformPageChunk: NonNullable<ResolveOptions['transformPageChunk']> = ({ html }) => {
const transaction = getActiveTransaction();
if (transaction) {
const traceparentData = transaction.toTraceparent();
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(transaction.getDynamicSamplingContext());
const content = `<head>
<meta name="sentry-trace" content="${traceparentData}"/>
<meta name="baggage" content="${dynamicSamplingContext}"/>
<script>${FETCH_PROXY_SCRIPT}
</script>
`;
return html.replace('<head>', content);
}
/**
* Adds Sentry tracing <meta> tags to the returned html page.
* Adds Sentry fetch proxy script to the returned html page if enabled in options.
* Also adds a nonce attribute to the script tag if users specified one for CSP.
*
* Exported only for testing
*/
export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable<ResolveOptions['transformPageChunk']> {
const { fetchProxyScriptNonce, injectFetchProxyScript } = options;
// if injectFetchProxyScript is not set, we default to true
const shouldInjectScript = injectFetchProxyScript !== false;
const nonce = fetchProxyScriptNonce ? `nonce="${fetchProxyScriptNonce}"` : '';

return html;
};
return ({ html }) => {
const transaction = getActiveTransaction();
if (transaction) {
const traceparentData = transaction.toTraceparent();
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(
transaction.getDynamicSamplingContext(),
);
const contentMeta = `<head>
<meta name="sentry-trace" content="${traceparentData}"/>
<meta name="baggage" content="${dynamicSamplingContext}"/>
`;
const contentScript = shouldInjectScript ? `<script ${nonce}>${FETCH_PROXY_SCRIPT}</script>` : '';

const content = `${contentMeta}\n${contentScript}`;

return html.replace('<head>', content);
}

return html;
};
}

/**
* A SvelteKit handle function that wraps the request for Sentry error and
Expand All @@ -89,13 +127,14 @@ export const transformPageChunk: NonNullable<ResolveOptions['transformPageChunk'
*
* export const handle = sentryHandle();
*
* // Optionally use the sequence function to add additional handlers.
* // Optionally use the `sequence` function to add additional handlers.
* // export const handle = sequence(sentryHandle(), yourCustomHandler);
* ```
*/
export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
const options = {
handleUnknownRoutes: false,
injectFetchProxyScript: true,
...handlerOptions,
};

Expand Down Expand Up @@ -139,7 +178,9 @@ async function instrumentHandle(
},
},
async (span?: Span) => {
const res = await resolve(event, { transformPageChunk });
const res = await resolve(event, {
transformPageChunk: addSentryCodeToPage(options),
});
if (span) {
span.setHttpStatus(res.status);
}
Expand Down
35 changes: 30 additions & 5 deletions packages/sveltekit/test/server/handle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Handle } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { vi } from 'vitest';

import { sentryHandle, transformPageChunk } from '../../src/server/handle';
import { FETCH_PROXY_SCRIPT, addSentryCodeToPage, sentryHandle } from '../../src/server/handle';
import { getDefaultNodeClientOptions } from '../utils';

const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx');
Expand Down Expand Up @@ -337,7 +337,7 @@ describe('handleSentry', () => {
});
});

describe('transformPageChunk', () => {
describe('addSentryCodeToPage', () => {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
Expand All @@ -351,16 +351,41 @@ describe('transformPageChunk', () => {
</html>`;

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

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

expect(transformed.includes('<meta name="sentry-trace"')).toEqual(true);
expect(transformed.includes('<meta name="baggage"')).toEqual(true);
expect(transformed).toContain('<meta name="sentry-trace"');
expect(transformed).toContain('<meta name="baggage"');
expect(transformed).toContain(`<script >${FETCH_PROXY_SCRIPT}</script>`);
});

it('adds a nonce attribute to the script if the `fetchProxyScriptNonce` option is specified', () => {
const transformPageChunk = addSentryCodeToPage({ fetchProxyScriptNonce: '123abc' });
const transaction = hub.startTransaction({ name: 'test' });
hub.getScope().setSpan(transaction);
const transformed = transformPageChunk({ html, done: true }) as string;

expect(transformed).toContain('<meta name="sentry-trace"');
expect(transformed).toContain('<meta name="baggage"');
expect(transformed).toContain(`<script nonce="123abc">${FETCH_PROXY_SCRIPT}</script>`);
});

it('does not add the fetch proxy script if the `injectFetchProxyScript` option is false', () => {
const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false });
const transaction = hub.startTransaction({ name: 'test' });
hub.getScope().setSpan(transaction);
const transformed = transformPageChunk({ html, done: true }) as string;

expect(transformed).toContain('<meta name="sentry-trace"');
expect(transformed).toContain('<meta name="baggage"');
expect(transformed).not.toContain(`<script >${FETCH_PROXY_SCRIPT}</script>`);
});
});

0 comments on commit 948e7d3

Please sign in to comment.