Skip to content

ref(sveltekit): Handle SvelteKit-generated spans in sentryHandle #17423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
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,
};
}
24 changes: 22 additions & 2 deletions packages/sveltekit/src/server/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
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
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
2 changes: 2 additions & 0 deletions packages/sveltekit/src/vite/injectGlobalValues.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { InternalGlobal } from '@sentry/core';
import type { SvelteKitTracingConfig } from './svelteConfig';

export type GlobalSentryValues = {
__sentry_sveltekit_output_dir?: string;
__sentry_sveltekit_tracing_config?: SvelteKitTracingConfig;
};

/**
Expand Down
9 changes: 7 additions & 2 deletions packages/sveltekit/src/vite/sourceMaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { Plugin, UserConfig } from 'vite';
import { WRAPPED_MODULE_SUFFIX } from '../common/utils';
import type { GlobalSentryValues } from './injectGlobalValues';
import { getGlobalValueInjectionCode, VIRTUAL_GLOBAL_VALUES_FILE } from './injectGlobalValues';
import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig';
import { getAdapterOutputDir, getHooksFileName, getTracingConfig, loadSvelteConfig } from './svelteConfig';
import type { CustomSentryVitePluginOptions } from './types';

// sorcery has no types, so these are some basic type definitions:
Expand Down Expand Up @@ -153,8 +153,11 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug

const globalSentryValues: GlobalSentryValues = {
__sentry_sveltekit_output_dir: adapterOutputDir,
__sentry_sveltekit_tracing_config: getTracingConfig(svelteConfig),
};

console.log('xx globalSentryValues', globalSentryValues);

const sourceMapSettingsPlugin: Plugin = {
name: 'sentry-sveltekit-update-source-map-setting-plugin',
apply: 'build', // only apply this plugin at build time
Expand Down Expand Up @@ -235,8 +238,10 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
transform: async (code, id) => {
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id);
const isInstrumentationServerFile = /instrumentation\.server\./.test(id);

if (isServerHooksFile) {
if (isServerHooksFile || isInstrumentationServerFile) {
console.log('xx inject global values', id);
const ms = new MagicString(code);
ms.append(`\n; import "${VIRTUAL_GLOBAL_VALUES_FILE}";\n`);
return {
Expand Down
Loading
Loading