diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 90909ea2c0a2..4de371d03ed6 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,16 +1,17 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + continueTrace, getActiveSpan, - getCurrentScope, getDynamicSamplingContextFromSpan, + getRootSpan, setHttpStatus, spanToTraceHeader, withIsolationScope, } from '@sentry/core'; -import { getActiveTransaction, startSpan } from '@sentry/core'; +import { startSpan } from '@sentry/core'; import { captureException } from '@sentry/node-experimental'; -/* eslint-disable @sentry-internal/sdk/no-optional-chaining */ +import type { Span } from '@sentry/types'; import { dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; @@ -103,12 +104,12 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { - // eslint-disable-next-line deprecation/deprecation - const transaction = getActiveTransaction(); - if (transaction) { - const traceparentData = spanToTraceHeader(transaction); + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; + if (rootSpan) { + const traceparentData = spanToTraceHeader(rootSpan); const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - getDynamicSamplingContextFromSpan(transaction), + getDynamicSamplingContextFromSpan(rootSpan), ); const contentMeta = ` @@ -170,36 +171,35 @@ async function instrumentHandle( return resolve(event); } - const { dynamicSamplingContext, traceparentData, propagationContext } = getTracePropagationData(event); - getCurrentScope().setPropagationContext(propagationContext); - - 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', + const { sentryTrace, baggage } = getTracePropagationData(event); + + return continueTrace({ sentryTrace, baggage }, async () => { + 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', + }, + name: `${event.request.method} ${event.route?.id || event.url.pathname}`, }, - name: `${event.request.method} ${event.route?.id || event.url.pathname}`, - ...traceparentData, - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + async (span?: Span) => { + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage(options), + }); + if (span) { + setHttpStatus(span, res.status); + } + return res; }, - }, - async span => { - const res = await resolve(event, { - transformPageChunk: addSentryCodeToPage(options), - }); - setHttpStatus(span, res.status); - return res; - }, - ); - return resolveResult; - } catch (e: unknown) { - sendErrorToSentry(e); - throw e; - } finally { - await flushIfServerless(); - } + ); + return resolveResult; + } catch (e: unknown) { + sendErrorToSentry(e); + throw e; + } finally { + await flushIfServerless(); + } + }); } diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index 6646ac5055bc..38f9353a18d7 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -1,15 +1,13 @@ -/* eslint-disable @sentry-internal/sdk/no-optional-chaining */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - getCurrentScope, + continueTrace, startSpan, } from '@sentry/core'; import { captureException } from '@sentry/node-experimental'; import { addNonEnumerableProperty, objectify } from '@sentry/utils'; import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; -import type { TransactionContext } from '@sentry/types'; import type { SentryWrappedFlag } from '../common/utils'; import { isHttpError, isRedirect } from '../common/utils'; import { flushIfServerless, getTracePropagationData } from './utils'; @@ -70,18 +68,19 @@ export function wrapLoadWithSentry any>(origLoad: T) const routeId = event.route && event.route.id; - const traceLoadContext: TransactionContext = { - op: 'function.sveltekit.load', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', - }, - name: routeId ? routeId : event.url.pathname, - }; - try { // We need to await before returning, otherwise we won't catch any errors thrown by the load function - return await startSpan(traceLoadContext, () => wrappingTarget.apply(thisArg, args)); + return await startSpan( + { + op: 'function.sveltekit.load', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', + }, + name: routeId ? routeId : event.url.pathname, + }, + () => wrappingTarget.apply(thisArg, args), + ); } catch (e) { sendErrorToSentry(e); throw e; @@ -133,34 +132,30 @@ export function wrapServerLoadWithSentry any>(origSe // https://github.com/sveltejs/kit/blob/e133aba479fa9ba0e7f9e71512f5f937f0247e2c/packages/kit/src/runtime/server/page/load_data.js#L111C3-L124 const routeId = event.route && (Object.getOwnPropertyDescriptor(event.route, 'id')?.value as string | undefined); - const { dynamicSamplingContext, traceparentData, propagationContext } = getTracePropagationData(event); - getCurrentScope().setPropagationContext(propagationContext); - - const traceLoadContext: TransactionContext = { - op: 'function.sveltekit.server.load', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', - }, - name: routeId ? routeId : event.url.pathname, - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - }, - data: { - 'http.method': event.request.method, - }, - ...traceparentData, - }; - - try { - // We need to await before returning, otherwise we won't catch any errors thrown by the load function - return await startSpan(traceLoadContext, () => wrappingTarget.apply(thisArg, args)); - } catch (e: unknown) { - sendErrorToSentry(e); - throw e; - } finally { - await flushIfServerless(); - } + const { sentryTrace, baggage } = getTracePropagationData(event); + + return continueTrace({ sentryTrace, baggage }, async () => { + try { + // We need to await before returning, otherwise we won't catch any errors thrown by the load function + return await startSpan( + { + op: 'function.sveltekit.server.load', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', + 'http.method': event.request.method, + }, + name: routeId ? routeId : event.url.pathname, + }, + () => wrappingTarget.apply(thisArg, args), + ); + } catch (e: unknown) { + sendErrorToSentry(e); + throw e; + } finally { + await flushIfServerless(); + } + }); }, }); } diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index ad6765209548..a202742a9eea 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -1,5 +1,5 @@ import { flush } from '@sentry/node-experimental'; -import { logger, tracingContextFromHeaders } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import type { RequestEvent } from '@sveltejs/kit'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -10,12 +10,11 @@ import { DEBUG_BUILD } from '../common/debug-build'; * * Sets propagation context as a side effect. */ -// eslint-disable-next-line deprecation/deprecation -export function getTracePropagationData(event: RequestEvent): ReturnType { - const sentryTraceHeader = event.request.headers.get('sentry-trace') || ''; - const baggageHeader = event.request.headers.get('baggage'); - // eslint-disable-next-line deprecation/deprecation - return tracingContextFromHeaders(sentryTraceHeader, baggageHeader); +export function getTracePropagationData(event: RequestEvent): { sentryTrace: string; baggage: string | null } { + const sentryTrace = event.request.headers.get('sentry-trace') || ''; + const baggage = event.request.headers.get('baggage'); + + return { sentryTrace, baggage }; } /** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index 8b8a5cbd80d4..00c9d3a34f46 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -1,5 +1,13 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addTracingExtensions, +} from '@sentry/core'; +import { getClient, getCurrentScope, getIsolationScope, init } from '@sentry/node-experimental'; import * as SentryNode from '@sentry/node-experimental'; +import type { Event } from '@sentry/types'; import type { Load, ServerLoad } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; @@ -241,124 +249,180 @@ describe('wrapServerLoadWithSentry calls trace', () => { }; } + beforeEach(() => { + getCurrentScope().clear(); + getIsolationScope().clear(); + }); + it('attaches trace data if available', async () => { + const transactions: Event[] = []; + + init({ + enableTracing: true, + release: '8.0.0', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); + const client = getClient()!; + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(getServerOnlyArgs()); - expect(mockStartSpan).toHaveBeenCalledTimes(1); - expect(mockStartSpan).toHaveBeenCalledWith( - { - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - op: 'function.sveltekit.server.load', - name: '/users/[id]', - parentSampled: true, - parentSpanId: '1234567890abcdef', - traceId: '1234567890abcdef1234567890abcdef', - data: { - 'http.method': 'GET', - }, - metadata: { - dynamicSamplingContext: { - environment: 'production', - public_key: 'dogsarebadatkeepingsecrets', - release: '1.0.0', - sample_rate: '1', - trace_id: '1234567890abcdef1234567890abcdef', - transaction: 'dogpark', - }, - }, + await client.flush(); + + expect(transactions).toHaveLength(1); + const transaction = transactions[0]; + + expect(transaction.contexts?.trace).toEqual({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.sveltekit.server.load', + 'http.method': 'GET', }, - expect.any(Function), - ); + op: 'function.sveltekit.server.load', + parent_span_id: '1234567890abcdef', + span_id: expect.any(String), + trace_id: '1234567890abcdef1234567890abcdef', + origin: 'auto.function.sveltekit', + }); + expect(transaction.transaction).toEqual('/users/[id]'); + expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({ + environment: 'production', + public_key: 'dogsarebadatkeepingsecrets', + release: '1.0.0', + sample_rate: '1', + trace_id: '1234567890abcdef1234567890abcdef', + transaction: 'dogpark', + }); }); it("doesn't attach trace data if it's not available", async () => { + const transactions: Event[] = []; + + init({ + enableTracing: true, + release: '8.0.0', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); + const client = getClient()!; + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(getServerArgsWithoutTracingHeaders()); - expect(mockStartSpan).toHaveBeenCalledTimes(1); - expect(mockStartSpan).toHaveBeenCalledWith( - { - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - op: 'function.sveltekit.server.load', - name: '/users/[id]', - metadata: {}, - data: { - 'http.method': 'GET', - }, + await client.flush(); + + expect(transactions).toHaveLength(1); + const transaction = transactions[0]; + + expect(transaction.contexts?.trace).toEqual({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.sveltekit.server.load', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + 'http.method': 'GET', }, - expect.any(Function), - ); + op: 'function.sveltekit.server.load', + span_id: expect.any(String), + trace_id: expect.not.stringContaining('1234567890abcdef1234567890abcdef'), + origin: 'auto.function.sveltekit', + }); + expect(transaction.transaction).toEqual('/users/[id]'); + expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({ + environment: 'production', + public_key: 'public', + sample_rate: '1', + sampled: 'true', + release: '8.0.0', + trace_id: transaction.contexts?.trace?.trace_id, + transaction: '/users/[id]', + }); }); it("doesn't attach the DSC data if the baggage header not available", async () => { + const transactions: Event[] = []; + + init({ + enableTracing: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); + const client = getClient()!; + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(getServerArgsWithoutBaggageHeader()); - expect(mockStartSpan).toHaveBeenCalledTimes(1); - expect(mockStartSpan).toHaveBeenCalledWith( - { - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - op: 'function.sveltekit.server.load', - name: '/users/[id]', - parentSampled: true, - parentSpanId: '1234567890abcdef', - traceId: '1234567890abcdef1234567890abcdef', - data: { - 'http.method': 'GET', - }, - metadata: { - dynamicSamplingContext: {}, - }, + await client.flush(); + + expect(transactions).toHaveLength(1); + const transaction = transactions[0]; + + expect(transaction.contexts?.trace).toEqual({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.sveltekit.server.load', + 'http.method': 'GET', }, - expect.any(Function), - ); + op: 'function.sveltekit.server.load', + parent_span_id: '1234567890abcdef', + span_id: expect.any(String), + trace_id: '1234567890abcdef1234567890abcdef', + origin: 'auto.function.sveltekit', + }); + expect(transaction.transaction).toEqual('/users/[id]'); + expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({}); }); it('falls back to the raw url if `event.route.id` is not available', async () => { + const transactions: Event[] = []; + + init({ + enableTracing: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); + const client = getClient()!; + const event = getServerOnlyArgs(); // @ts-expect-error - this is fine (just tests here) delete event.route; const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(event); - expect(mockStartSpan).toHaveBeenCalledTimes(1); - expect(mockStartSpan).toHaveBeenCalledWith( - { - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - }, - op: 'function.sveltekit.server.load', - name: '/users/123', - parentSampled: true, - parentSpanId: '1234567890abcdef', - traceId: '1234567890abcdef1234567890abcdef', - data: { - 'http.method': 'GET', - }, - metadata: { - dynamicSamplingContext: { - environment: 'production', - public_key: 'dogsarebadatkeepingsecrets', - release: '1.0.0', - sample_rate: '1', - trace_id: '1234567890abcdef1234567890abcdef', - transaction: 'dogpark', - }, - }, + await client.flush(); + + expect(transactions).toHaveLength(1); + const transaction = transactions[0]; + + expect(transaction.contexts?.trace).toEqual({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.sveltekit.server.load', + 'http.method': 'GET', }, - expect.any(Function), - ); + op: 'function.sveltekit.server.load', + parent_span_id: '1234567890abcdef', + span_id: expect.any(String), + trace_id: '1234567890abcdef1234567890abcdef', + origin: 'auto.function.sveltekit', + }); + expect(transaction.transaction).toEqual('/users/123'); }); it("doesn't wrap server load more than once if the wrapper was applied multiple times", async () => { diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 3543a1128835..f3cac3eb47a0 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -23,32 +23,27 @@ const MOCK_REQUEST_EVENT: any = { }; describe('getTracePropagationData', () => { - it('returns traceParentData and DSC if both are available', () => { + it('returns sentryTrace & baggage strings if both are available', () => { const event: any = MOCK_REQUEST_EVENT; - const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event); - - expect(traceparentData).toEqual({ - parentSampled: true, - parentSpanId: '1234567890abcdef', - traceId: '1234567890abcdef1234567890abcdef', - }); - - expect(dynamicSamplingContext).toEqual({ - environment: 'production', - public_key: 'dogsarebadatkeepingsecrets', - release: '1.0.0', - sample_rate: '1', - trace_id: '1234567890abcdef1234567890abcdef', - transaction: 'dogpark', - }); + const { sentryTrace, baggage } = getTracePropagationData(event); + + expect(sentryTrace).toEqual('1234567890abcdef1234567890abcdef-1234567890abcdef-1'); + expect(baggage?.split(',').sort()).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=dogsarebadatkeepingsecrets', + 'sentry-release=1.0.0', + 'sentry-sample_rate=1', + 'sentry-trace_id=1234567890abcdef1234567890abcdef', + 'sentry-transaction=dogpark', + ]); }); - it('returns undefined if the necessary header is not avaolable', () => { + it('returns empty if the necessary header is not available', () => { const event: any = { request: { headers: { get: () => undefined } } }; - const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event); + const { sentryTrace, baggage } = getTracePropagationData(event); - expect(traceparentData).toBeUndefined(); - expect(dynamicSamplingContext).toBeUndefined(); + expect(sentryTrace).toBe(''); + expect(baggage).toBeUndefined(); }); });