Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 105 additions & 31 deletions packages/sveltekit/src/server-common/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import {
getDefaultIsolationScope,
getIsolationScope,
getTraceMetaTags,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
setHttpStatus,
spanToJSON,
startSpan,
updateSpanName,
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
Expand Down Expand Up @@ -88,11 +91,33 @@ export function isFetchProxyRequired(version: string): boolean {
return true;
}

interface BackwardsForwardsCompatibleEvent {
/**
* For now taken from: https://github.com/sveltejs/kit/pull/13899
* Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing.
* @since 2.31.0
*/
tracing?: {
/** Whether tracing is enabled. */
enabled: boolean;
current: Span;
root: Span;
};
}

async function instrumentHandle(
{ event, resolve }: Parameters<Handle>[0],
{
event,
resolve,
}: {
event: Parameters<Handle>[0]['event'] & BackwardsForwardsCompatibleEvent;
resolve: Parameters<Handle>[0]['resolve'];
},
options: SentryHandleOptions,
): Promise<Response> {
if (!event.route?.id && !options.handleUnknownRoutes) {
const routeId = event.route?.id;

if (!routeId && !options.handleUnknownRoutes) {
return resolve(event);
}

Expand All @@ -108,42 +133,79 @@ async function instrumentHandle(
}
}

const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`;
const routeName = `${event.request.method} ${routeId || event.url.pathname}`;

if (getIsolationScope() !== getDefaultIsolationScope()) {
getIsolationScope().setTransactionName(routeName);
} else {
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
}

// We only start a span if SvelteKit's native tracing is not enabled. Two reasons:
// - Used Kit version doesn't yet support tracing
// - Users didn't enable tracing
const kitTracingEnabled = event.tracing?.enabled;

try {
const resolveResult = await startSpan(
{
op: 'http.server',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
'http.method': event.request.method,
},
name: routeName,
},
async (span?: Span) => {
getCurrentScope().setSDKProcessingMetadata({
// We specifically avoid cloning the request here to avoid double read errors.
// We only read request headers so we're not consuming the body anyway.
// Note to future readers: This sounds counter-intuitive but please read
// https://github.com/getsentry/sentry-javascript/issues/14583
normalizedRequest: winterCGRequestToRequestData(event.request),
});
const res = await resolve(event, {
transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }),
});
if (span) {
setHttpStatus(span, res.status);
const resolveWithSentry: (sentrySpan?: Span) => Promise<Response> = async (sentrySpan?: Span) => {
getCurrentScope().setSDKProcessingMetadata({
// We specifically avoid cloning the request here to avoid double read errors.
// We only read request headers so we're not consuming the body anyway.
// Note to future readers: This sounds counter-intuitive but please read
// https://github.com/getsentry/sentry-javascript/issues/14583
normalizedRequest: winterCGRequestToRequestData(event.request),
});

const res = await resolve(event, {
transformPageChunk: addSentryCodeToPage({
injectFetchProxyScript: options.injectFetchProxyScript ?? true,
}),
});

const kitRootSpan = event.tracing?.root;

if (sentrySpan) {
setHttpStatus(sentrySpan, res.status);
} else if (kitRootSpan) {
// Update the root span emitted from SvelteKit to resemble a `http.server` span
// We're doing this here instead of an event processor to ensure we update the
// span name as early as possible (for dynamic sampling, et al.)
// Other spans are enhanced in the `processKitSpans` function.
const spanJson = spanToJSON(kitRootSpan);
const kitRootSpanAttributes = spanJson.data;
const originalName = spanJson.description;

const routeName = kitRootSpanAttributes['http.route'];
if (routeName && typeof routeName === 'string') {
updateSpanName(kitRootSpan, routeName);
}
return res;
},
);

kitRootSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltejs.kit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
'sveltekit.tracing.original_name': originalName,
});
}

return res;
};

const resolveResult = kitTracingEnabled
? await resolveWithSentry()
: await startSpan(
{
op: 'http.server',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
'http.method': event.request.method,
},
name: routeName,
},
resolveWithSentry,
);

return resolveResult;
} catch (e: unknown) {
sendErrorToSentry(e, 'handle');
Expand Down Expand Up @@ -176,9 +238,12 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
};

const sentryRequestHandler: Handle = input => {
const backwardsForwardsCompatibleEvent = input.event as typeof input.event & BackwardsForwardsCompatibleEvent;

// Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle)
const skipIsolation =
'_sentrySkipRequestIsolation' in input.event.locals && input.event.locals._sentrySkipRequestIsolation;
'_sentrySkipRequestIsolation' in backwardsForwardsCompatibleEvent.locals &&
backwardsForwardsCompatibleEvent.locals._sentrySkipRequestIsolation;

// In case of a same-origin `fetch` call within a server`load` function,
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
Expand All @@ -187,7 +252,9 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
// currently active span instead of a new root span to correctly reflect this
// behavior.
if (skipIsolation || input.event.isSubRequest) {
return instrumentHandle(input, options);
return instrumentHandle(input, {
...options,
});
}

return withIsolationScope(isolationScope => {
Expand All @@ -200,6 +267,13 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
// https://github.com/getsentry/sentry-javascript/issues/14583
normalizedRequest: winterCGRequestToRequestData(input.event.request),
});

if (backwardsForwardsCompatibleEvent.tracing?.enabled) {
// if sveltekit tracing is enabled (since 2.31.0), trace continuation is handled by
// kit before our hook is executed. No noeed to call `continueTrace` from our end
return instrumentHandle(input, options);
}

return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
});
};
Expand Down
74 changes: 74 additions & 0 deletions packages/sveltekit/src/server-common/processKitSpans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Integration, SpanOrigin } from '@sentry/core';
import { type SpanJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';

/**
* A small integration that preprocesses spans so that SvelteKit-generated spans
* (via Kit's tracing feature since 2.31.0) get the correct Sentry attributes
* and data.
*/
export function svelteKitSpansIntegration(): Integration {
return {
name: 'SvelteKitSpansEnhancment',
// Using preprocessEvent to ensure the processing happens before user-configured
// event processors are executed
preprocessEvent(event) {
if (event.type === 'transaction') {
event.spans?.forEach(_enhanceKitSpan);
}
},
};
}

/**
* Adds sentry-specific attributes and data to a span emitted by SvelteKit's native tracing (since 2.31.0)
* @exported for testing
*/
export function _enhanceKitSpan(span: SpanJSON): void {
let op: string | undefined = undefined;
let origin: SpanOrigin | undefined = undefined;

const spanName = span.description;

switch (spanName) {
case 'sveltekit.resolve':
op = 'http.sveltekit.resolve';
origin = 'auto.http.sveltekit';
break;
case 'sveltekit.load':
op = 'function.sveltekit.load';
origin = 'auto.function.sveltekit.load';
break;
case 'sveltekit.form_action':
op = 'function.sveltekit.form_action';
origin = 'auto.function.sveltekit.action';
break;
case 'sveltekit.remote.call':
op = 'function.sveltekit.remote';
origin = 'auto.rpc.sveltekit.remote';
break;
case 'sveltekit.handle.root':
// We don't want to overwrite the root handle span at this point since
// we already enhance the root span in our `sentryHandle` hook.
break;
default: {
if (spanName?.startsWith('sveltekit.handle.sequenced.')) {
op = 'function.sveltekit.handle';
origin = 'auto.function.sveltekit.handle';
}
break;
}
}

const previousOp = span.op || span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP];
const previousOrigin = span.origin || span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN];

if (!previousOp && op) {
span.op = op;
span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op;
}

if (!previousOrigin && origin) {
span.origin = origin;
span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin;
}
}
17 changes: 16 additions & 1 deletion packages/sveltekit/src/server-common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { captureException, objectify } from '@sentry/core';
import { captureException, GLOBAL_OBJ, objectify } from '@sentry/core';
import type { RequestEvent } from '@sveltejs/kit';
import { isHttpError, isRedirect } from '../common/utils';
import type { GlobalWithSentryValues } from '../vite/injectGlobalValues';

/**
* Takes a request event and extracts traceparent and DSC data
Expand Down Expand Up @@ -52,3 +53,17 @@ export function sendErrorToSentry(e: unknown, handlerFn: 'handle' | 'load' | 'se

return objectifiedErr;
}

/**
* During build, we inject the SvelteKit tracing config into the global object of the server.
* @returns tracing config (available since 2.31.0)
*/
export function getKitTracingConfig(): { instrumentation: boolean; tracing: boolean } {
const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ;
const kitTracingConfig = globalWithSentryValues.__sentry_sveltekit_tracing_config;

return {
instrumentation: kitTracingConfig?.instrumentation?.server ?? false,
tracing: kitTracingConfig?.tracing?.server ?? false,
};
}
27 changes: 25 additions & 2 deletions packages/sveltekit/src/server/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node';
import {
getDefaultIntegrations as getDefaultNodeIntegrations,
httpIntegration,
init as initNodeSdk,
} from '@sentry/node';
import { svelteKitSpansIntegration } from '../server-common/processKitSpans';
import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration';
import { getKitTracingConfig } from '../server-common/utils';

/**
* Initialize the Server-side Sentry SDK
* @param options
*/
export function init(options: NodeOptions): NodeClient | undefined {
const defaultIntegrations = [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()];

const config = getKitTracingConfig();
if (config.instrumentation) {
// Whenever `instrumentation` is enabled, we don't need httpIntegration to emit spans
// - if `tracing` is enabled, kit will emit the root span
// - if `tracing` is disabled, our handler will emit the root span
// In old (hooks.server.ts) based SDK setups, adding the default version of the integration does nothing
// for incoming requests. We still add it in default config for the undocumented case that anyone is
// using the SDK by `--import`ing the SDK setup directly (e.g. with adapter-node).
defaultIntegrations.push(httpIntegration({ disableIncomingRequestSpans: true }));
if (config.tracing) {
// If `tracing` is enabled, we need to instrument spans for the server
defaultIntegrations.push(svelteKitSpansIntegration());
}
}

const opts = {
defaultIntegrations: [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()],
defaultIntegrations,
...options,
};

Expand Down
21 changes: 21 additions & 0 deletions packages/sveltekit/src/vite/autoInstrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type AutoInstrumentSelection = {

type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
debug: boolean;
onlyInstrumentClient: boolean;
};

/**
Expand All @@ -41,12 +42,26 @@ type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptions): Plugin {
const { load: wrapLoadEnabled, serverLoad: wrapServerLoadEnabled, debug } = options;

let isServerBuild: boolean | undefined = undefined;

return {
name: 'sentry-auto-instrumentation',
// This plugin needs to run as early as possible, before the SvelteKit plugin virtualizes all paths and ids
enforce: 'pre',

configResolved: config => {
// The SvelteKit plugins trigger additional builds within the main (SSR) build.
// We just need a mechanism to upload source maps only once.
// `config.build.ssr` is `true` for that first build and `false` in the other ones.
// Hence we can use it as a switch to upload source maps only once in main build.
isServerBuild = !!config.build.ssr;
},

async load(id) {
if (options.onlyInstrumentClient && isServerBuild) {
return null;
}

const applyUniversalLoadWrapper =
wrapLoadEnabled &&
/^\+(page|layout)\.(js|ts|mjs|mts)$/.test(path.basename(id)) &&
Expand All @@ -58,6 +73,12 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
return getWrapperCode('wrapLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`);
}

if (options.onlyInstrumentClient) {
// Now that we've checked universal files, we can early return and avoid further
// regexp checks below for server-only files, in case `onlyInstrumentClient` is `true`.
return null;
}

const applyServerLoadWrapper =
wrapServerLoadEnabled &&
/^\+(page|layout)\.server\.(js|ts|mjs|mts)$/.test(path.basename(id)) &&
Expand Down
Loading
Loading