Skip to content
Open
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
87 changes: 53 additions & 34 deletions packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,6 +24,9 @@ export function svelteKitSpansIntegration(): Integration {
event.spans?.forEach(_enhanceKitSpan);
}
},
processSpan(span) {
_enhanceKitSpanStreamed(span);
},
};
}

Expand All @@ -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 {};
}
}
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -169,4 +174,106 @@ describe('svelteKitSpansIntegration', () => {
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op');
});
});

describe('_enhanceKitSpanStreamed', () => {
function makeStreamedSpan(overrides: Partial<StreamedSpanJSON> = {}): 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');
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing integration or E2E test for feat PR

Low Severity

This is a feat PR that only includes unit tests. Per the project review rules, feat PRs are expected to include at least one integration or E2E test. There is an existing E2E test application at dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/ that could potentially be extended to verify span streaming behavior end-to-end. Flagging this because it was mentioned in the project rules file.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 488e1e1. Configure here.

});
Loading