diff --git a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts index c38108c75542..fe5d1ed31e23 100644 --- a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts +++ b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts @@ -1,5 +1,9 @@ -import type { Integration, SpanJSON, SpanOrigin } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import type { Integration, SpanJSON, SpanOrigin, StreamedSpanJSON } from '@sentry/core'; +import { + safeSetSpanJSONAttributes, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, +} from '@sentry/core'; /** * A small integration that preprocesses spans so that SvelteKit-generated spans @@ -20,6 +24,9 @@ export function svelteKitSpansIntegration(): Integration { event.spans?.forEach(_enhanceKitSpan); } }, + processSpan(span) { + _enhanceKitSpanStreamed(span); + }, }; } @@ -28,51 +35,63 @@ export function svelteKitSpansIntegration(): Integration { * @exported for testing */ export function _enhanceKitSpan(span: SpanJSON): void { - let op: string | undefined = undefined; - let origin: SpanOrigin | undefined = undefined; - - const spanName = span.description; + const { op, origin } = _getKitSpanEnhancement(span.description); 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 || previousOrigin === 'manual') && origin) { + span.origin = origin; + span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; + } +} + +/** + * Streaming-mode counterpart of {@link _enhanceKitSpan} operating on {@link StreamedSpanJSON}. + * @exported for testing + */ +export function _enhanceKitSpanStreamed(span: StreamedSpanJSON): void { + const { op, origin } = _getKitSpanEnhancement(span.name); + const previousOrigin = span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined; + + if (op) { + safeSetSpanJSONAttributes(span, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op }); + } + + if (previousOrigin === 'manual' && origin) { + // `safeSetSpanJSONAttributes` skips existing keys, so overwrite the 'manual' sentinel directly. + span.attributes![SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; + } else { + safeSetSpanJSONAttributes(span, { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin }); + } +} + +function _getKitSpanEnhancement(spanName: string | undefined): { + op?: string; + origin?: SpanOrigin; +} { switch (spanName) { case 'sveltekit.resolve': - op = 'function.sveltekit.resolve'; - origin = 'auto.http.sveltekit'; - break; + return { op: 'function.sveltekit.resolve', origin: 'auto.http.sveltekit' }; case 'sveltekit.load': - op = 'function.sveltekit.load'; - origin = 'auto.function.sveltekit.load'; - break; + return { op: 'function.sveltekit.load', origin: 'auto.function.sveltekit.load' }; case 'sveltekit.form_action': - op = 'function.sveltekit.form_action'; - origin = 'auto.function.sveltekit.action'; - break; + return { op: 'function.sveltekit.form_action', origin: 'auto.function.sveltekit.action' }; case 'sveltekit.remote.call': - op = 'function.sveltekit.remote'; - origin = 'auto.rpc.sveltekit.remote'; - break; + return { op: 'function.sveltekit.remote', origin: 'auto.rpc.sveltekit.remote' }; 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: { + return {}; + default: if (spanName?.startsWith('sveltekit.handle.sequenced.')) { - op = 'function.sveltekit.handle'; - origin = 'auto.function.sveltekit.handle'; + return { op: 'function.sveltekit.handle', origin: 'auto.function.sveltekit.handle' }; } - break; - } - } - - if (!previousOp && op) { - span.op = op; - span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op; - } - - if ((!previousOrigin || previousOrigin === 'manual') && origin) { - span.origin = origin; - span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; + return {}; } } diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index 0d95cb3d6fb6..b051d613aad1 100644 --- a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -1,14 +1,19 @@ -import type { SpanJSON, TransactionEvent } from '@sentry/core'; +import type { SpanJSON, StreamedSpanJSON, TransactionEvent } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { describe, expect, it } from 'vitest'; -import { _enhanceKitSpan, svelteKitSpansIntegration } from '../../../src/server-common/integrations/svelteKitSpans'; +import { + _enhanceKitSpan, + _enhanceKitSpanStreamed, + svelteKitSpansIntegration, +} from '../../../src/server-common/integrations/svelteKitSpans'; describe('svelteKitSpansIntegration', () => { - it('has a name and a preprocessEventHook', () => { + it('has a name and a preprocessEvent and processSpan hook', () => { const integration = svelteKitSpansIntegration(); expect(integration.name).toBe('SvelteKitSpansEnhancement'); expect(typeof integration.preprocessEvent).toBe('function'); + expect(typeof integration.processSpan).toBe('function'); }); it('enhances spans from SvelteKit', () => { @@ -169,4 +174,106 @@ describe('svelteKitSpansIntegration', () => { expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); }); }); + + describe('_enhanceKitSpanStreamed', () => { + function makeStreamedSpan(overrides: Partial = {}): StreamedSpanJSON { + return { + name: 'unspecified', + span_id: '123', + trace_id: 'abc', + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + is_segment: false, + attributes: {}, + ...overrides, + }; + } + + it.each([ + ['sveltekit.resolve', 'function.sveltekit.resolve', 'auto.http.sveltekit'], + ['sveltekit.load', 'function.sveltekit.load', 'auto.function.sveltekit.load'], + ['sveltekit.form_action', 'function.sveltekit.form_action', 'auto.function.sveltekit.action'], + ['sveltekit.remote.call', 'function.sveltekit.remote', 'auto.rpc.sveltekit.remote'], + ['sveltekit.handle.sequenced.0', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'], + ['sveltekit.handle.sequenced.myHandler', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'], + ])('enhances %s span with the correct op and origin', (spanName, op, origin) => { + const span = makeStreamedSpan({ name: spanName, attributes: { someAttribute: 'someValue' } }); + + _enhanceKitSpanStreamed(span); + + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op); + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); + }); + + it("doesn't change spans from other origins", () => { + const span = makeStreamedSpan({ name: 'someOtherSpan' }); + + _enhanceKitSpanStreamed(span); + + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + }); + + it("doesn't overwrite the sveltekit.handle.root span", () => { + const rootHandleSpan = makeStreamedSpan({ + name: 'sveltekit.handle.root', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + }, + }); + + _enhanceKitSpanStreamed(rootHandleSpan); + + expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + }); + + it("doesn't enhance unrelated spans", () => { + const span = makeStreamedSpan({ + name: 'someOtherSpan', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', + }, + }); + + _enhanceKitSpanStreamed(span); + + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db'); + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); + }); + + it("doesn't overwrite already set ops or origins on sveltekit spans", () => { + // for example, if users manually set this (for whatever reason) + const span = makeStreamedSpan({ + name: 'sveltekit.resolve', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.custom.origin', + }, + }); + + _enhanceKitSpanStreamed(span); + + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.custom.origin'); + }); + + it('overwrites previously set "manual" origins on sveltekit spans', () => { + const span = makeStreamedSpan({ + name: 'sveltekit.resolve', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + }, + }); + + _enhanceKitSpanStreamed(span); + + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + }); + }); });