From 5e85151cfe12f218ddca2416419733e98e876510 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 22 Dec 2023 09:51:04 +0100 Subject: [PATCH] feat(sveltekit): Add options to configure fetch instrumentation script for CSP --- packages/sveltekit/src/server/handle.ts | 77 ++++++++++++++----- packages/sveltekit/test/server/handle.test.ts | 35 +++++++-- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 7f9f581ca3c3..beff32affc19 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -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 { @@ -53,7 +70,10 @@ 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)} @@ -61,22 +81,40 @@ const FETCH_PROXY_SCRIPT = ` } `; -export const transformPageChunk: NonNullable = ({ html }) => { - const transaction = getActiveTransaction(); - if (transaction) { - const traceparentData = transaction.toTraceparent(); - const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(transaction.getDynamicSamplingContext()); - const content = ` - - - - `; - return html.replace('', content); - } +/** + * Adds Sentry tracing 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 { + 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 = ` + + + `; + const contentScript = shouldInjectScript ? `` : ''; + + const content = `${contentMeta}\n${contentScript}`; + + return html.replace('', content); + } + + return html; + }; +} /** * A SvelteKit handle function that wraps the request for Sentry error and @@ -89,13 +127,14 @@ export const transformPageChunk: NonNullable { - const res = await resolve(event, { transformPageChunk }); + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage(options), + }); if (span) { span.setHttpStatus(res.status); } diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index 1444b75d9ea5..cca809006d27 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -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'); @@ -337,7 +337,7 @@ describe('handleSentry', () => { }); }); -describe('transformPageChunk', () => { +describe('addSentryCodeToPage', () => { const html = ` @@ -351,16 +351,41 @@ describe('transformPageChunk', () => { `; 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('${FETCH_PROXY_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('${FETCH_PROXY_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('${FETCH_PROXY_SCRIPT}`); }); });