Skip to content
Merged
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
13 changes: 13 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,7 @@ The following span names were adjusted:
| `router` | Framework-specific, sometimes containing the raw URL | `/users/123`, `SvelteKit Route Change` | The span's `http.route`, or `Router` if the SDK has none | `/users/:id`, `Router` |
| `handler` | Framework-specific, often carrying the request method | `GET /users/:id`, `route-handler`, `getUser` | The span's `http.route`, or `Request handler` if the SDK has none | `/users/:id`, `Request handler` |
| `function` (Angular `TraceMethod`) | The decorator's `name` option in angle brackets | `<getUser>`, `<unnamed>` | The decorator's `name` option, or `Function execution` if it has none | `Login.ngOnInit`, `getUsers`, `Function execution` |
| `function` (SvelteKit) | The route the wrapped function ran for, or the raw URL path if the SDK couldn't resolve one | `/users/[id]`, `/users/123`, `GET /api/users/[id]` | The name of the wrapped function | `load`, `GET` |
| `function.gcp` | The request method and path for HTTP functions, otherwise the trigger's event or trigger type | `POST /users`, `google.pubsub.topic.publish`, `firebase.function.http.request` | The function name, or `Serverless function execution` if the SDK cannot resolve one | `myFunction`, `Serverless function execution` |
| `function.aws` | The Lambda function name | `my-function` | Unchanged, except that the SDK now falls back to `Serverless function execution` if it cannot resolve the function name | `my-function`, `Serverless function execution` |
| `graphql` | The graphql phase and, for operations, the operation name | `query GetUser`, `graphql.parse`, `graphql.resolve user.0.name` | The operation type, or the processing type where there is none | `GraphQL query`, `GraphQL parse`, `GraphQL resolve` |
Expand Down Expand Up @@ -1041,6 +1042,18 @@ function name on `faas.name`, the request URL on `url.full`, and the invocation
rather than `custom`, matching the other FaaS spans: the name comes from the function, not from the
user. This applies in both trace lifecycles.

#### SvelteKit function spans

The spans around `wrapLoadWithSentry`, `wrapServerLoadWithSentry` and `wrapServerRouteWithSentry` are
named after the function they wrap (`load`, or the HTTP method a `+server.js` route handler is exported
as) rather than after the route it ran for. The route stays on `http.route` (`url.template` for the
client-side universal load span) and the request path on `url.path`, so `ignoreSpans` and `tracesSampler`
rules that matched these names have to match those attributes instead.

Their span description is unchanged: each span carries a `sentry.description` attribute holding the
name it had before. The same applies to the spans SvelteKit's own tracing emits (`sveltekit.load`,
`sveltekit.resolve`, `sveltekit.form_action`, ...), which the SDK marks as `function` spans.

#### Filtering and sampling

When span streaming is enabled (i.e. by default) `ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ test('server pageload request span has nested request span for sub request', asy
expect.arrayContaining([
// load span where the server load function initiates the sub request:
expect.objectContaining({
name: '/server-load-fetch',
name: 'load',
is_segment: false,
attributes: expect.objectContaining({ 'sentry.op': { value: 'function', type: 'string' } }),
attributes: expect.objectContaining({
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'load', type: 'string' },
'http.route': { value: '/server-load-fetch', type: 'string' },
'sentry.description': { value: '/server-load-fetch', type: 'string' },
}),
}),
// sub request span:
expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ test('server pageload request span has nested request span for sub request', asy
expect.arrayContaining([
// load span where the server load function initiates the sub request:
expect.objectContaining({
name: '/server-load-fetch',
name: 'load',
is_segment: false,
attributes: expect.objectContaining({ 'sentry.op': { value: 'function', type: 'string' } }),
attributes: expect.objectContaining({
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'load', type: 'string' },
'http.route': { value: '/server-load-fetch', type: 'string' },
'sentry.description': { value: '/server-load-fetch', type: 'string' },
}),
}),
// sub request span:
expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,13 @@ test('server trace for a `QUERY` server route includes the wrapped route handler
expect(getSegmentChildSpans(serverTraceSpans, serverSpan)).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'QUERY /query-server-route',
name: 'QUERY',
attributes: expect.objectContaining({
'sentry.origin': { value: 'auto.function.sveltekit', type: 'string' },
'code.function.name': { value: 'QUERY', type: 'string' },
'http.request.method': { value: 'QUERY', type: 'string' },
'http.route': { value: '/query-server-route', type: 'string' },
'sentry.description': { value: 'QUERY /query-server-route', type: 'string' },
}),
}),
]),
Expand Down
22 changes: 20 additions & 2 deletions packages/sveltekit/src/client/load.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import {
addNonEnumerableProperty,
getClient,
handleCallbackErrors,
hasSpanStreamingEnabled,
objectify,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
} from '@sentry/core';
import { startSpan } from '@sentry/core/browser';
import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes';
import {
SENTRY_SEGMENT_NAME_SOURCE,
CODE_FUNCTION_NAME,
SENTRY_DESCRIPTION,
SENTRY_OP,
URL_PATH,
URL_TEMPLATE,
} from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';
import { captureException } from '@sentry/svelte';
import type { LoadEvent } from '@sveltejs/kit';
Expand Down Expand Up @@ -74,16 +83,25 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)
addNonEnumerableProperty(patchedEvent, '__sentry_wrapped__', true);

const routeId = getRouteId(event);
const routeOrPathname = routeId ? routeId : event.url.pathname;

const client = getClient();
const hasSpanStreaming = !!client && hasSpanStreamingEnabled(client);

return startSpan(
{
// With span streaming, span names have to be low cardinality, so we use the function name.
name: hasSpanStreaming ? 'load' : routeOrPathname,
attributes: {
[SENTRY_OP]: FUNCTION,
[CODE_FUNCTION_NAME]: 'load',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url',
[URL_PATH]: event.url.pathname,
...(routeId && { [URL_TEMPLATE]: routeId }),
// Relay infers the description from `code.function.name`, which would drop the route.
...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routeOrPathname }),
},
name: routeId ? routeId : event.url.pathname,
},
() => handleCallbackErrors(() => wrappingTarget.apply(thisArg, [patchedEvent]), sendErrorToSentry),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Integration, SpanJSON, SpanOrigin, StreamedSpanJSON } from '@sentry/core';
import { safeSetSpanJSONAttributes, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import { SENTRY_DESCRIPTION, SENTRY_OP } from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';

/**
Expand Down Expand Up @@ -64,7 +64,9 @@ export function _enhanceKitSpanStreamed(span: StreamedSpanJSON): void {

const previousOrigin = span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined;

safeSetSpanJSONAttributes(span, { [SENTRY_OP]: FUNCTION });
// Kit's span names carry no `code.function.name`, so without an explicit description, these spans
// would be described by the `function` op's static fallback instead of the operation they ran.
safeSetSpanJSONAttributes(span, { [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: span.name });

if (previousOrigin === 'manual') {
// `safeSetSpanJSONAttributes` skips existing keys, so overwrite the 'manual' sentinel directly.
Expand Down
36 changes: 31 additions & 5 deletions packages/sveltekit/src/server-common/load.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { addNonEnumerableProperty, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import {
addNonEnumerableProperty,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startSpan,
} from '@sentry/core';
import { flushIfServerless } from '@sentry/core/server';
import {
SENTRY_SEGMENT_NAME_SOURCE,
CODE_FUNCTION_NAME,
HTTP_REQUEST_METHOD,
HTTP_ROUTE,
SENTRY_DESCRIPTION,
SENTRY_OP,
URL_PATH,
} from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';
import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit';
Expand Down Expand Up @@ -36,19 +45,27 @@ export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T)

addNonEnumerableProperty(event, '__sentry_wrapped__', true);

const routeId = getRouteId(event);
const routeId = getRouteId(event) ?? undefined;
const routeOrPathname = routeId ? routeId : event.url.pathname;

const client = getClient();
const hasSpanStreaming = !!client && hasSpanStreamingEnabled(client);

try {
// We need to await before returning, otherwise we won't catch any errors thrown by the load function
return await startSpan(
{
name: hasSpanStreaming ? 'load' : routeOrPathname,
attributes: {
[SENTRY_OP]: FUNCTION,
[CODE_FUNCTION_NAME]: 'load',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url',
[URL_PATH]: event.url.pathname,
[HTTP_ROUTE]: routeId,
// Relay infers the description from `code.function.name`, which would drop the route.
...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routeOrPathname }),
},
name: routeId ? routeId : event.url.pathname,
},
() => wrappingTarget.apply(thisArg, args),
);
Expand Down Expand Up @@ -100,7 +117,11 @@ export function wrapServerLoadWithSentry<T extends (...args: any) => any>(origSe
// Accessing any member of `event.route` causes SvelteKit to invalidate the
// server `load` function's data on every route change. We use `getRouteId` which uses
// SvelteKit 2's `untrack` when available, otherwise getOwnPropertyDescriptor for 1.x.
const routeId = getRouteId(event);
const routeId = getRouteId(event) ?? undefined;
const routeOrPathname = routeId ? routeId : event.url.pathname;

const client = getClient();
const hasSpanStreaming = !!client && hasSpanStreamingEnabled(client);

try {
// We need to await before returning, otherwise we won't catch any errors thrown by the load function
Expand All @@ -112,8 +133,13 @@ export function wrapServerLoadWithSentry<T extends (...args: any) => any>(origSe
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server',
[SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url',
[HTTP_REQUEST_METHOD]: event.request.method,
[URL_PATH]: event.url.pathname,
[HTTP_ROUTE]: routeId,
// Relay infers the description from `code.function.name`, which would drop the route.
...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routeOrPathname }),
},
name: routeId ? routeId : event.url.pathname,
// With span streaming, span names have to be low cardinality, so we use the function name.
name: hasSpanStreaming ? 'load' : routeOrPathname,
},
() => wrappingTarget.apply(thisArg, args),
);
Expand Down
30 changes: 26 additions & 4 deletions packages/sveltekit/src/server-common/serverRoute.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { addNonEnumerableProperty, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import {
addNonEnumerableProperty,
FUNCTION_SPAN_NAME_FALLBACK,
getClient,
hasSpanStreamingEnabled,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startSpan,
} from '@sentry/core';
import { flushIfServerless } from '@sentry/core/server';
import { CODE_FUNCTION_NAME, HTTP_REQUEST_METHOD, SENTRY_OP } from '@sentry/conventions/attributes';
import {
CODE_FUNCTION_NAME,
HTTP_REQUEST_METHOD,
HTTP_ROUTE,
SENTRY_DESCRIPTION,
SENTRY_OP,
} from '@sentry/conventions/attributes';
import { FUNCTION } from '@sentry/conventions/op';
import type { RequestEvent } from '@sveltejs/kit';
import { sendErrorToSentry } from './utils';
Expand Down Expand Up @@ -40,20 +53,29 @@ export function wrapServerRouteWithSentry<T extends RequestEvent>(
return wrappingTarget.apply(thisArg, args);
}

const routeId = event.route?.id;
const routeId = event.route?.id ?? undefined;
const httpMethod = event.request.method;
const methodAndRoute = `${httpMethod} ${routeId || 'Server Route'}`;

addNonEnumerableProperty(event, '__sentry_wrapped__', true);

const client = getClient();
const hasSpanStreaming = !!client && hasSpanStreamingEnabled(client);

try {
return await startSpan(
{
name: `${httpMethod} ${routeId || 'Server Route'}`,
// With span streaming, span names have to be low cardinality, so we use the handler's
// function name, which for `+server.js` routes is the HTTP method it is exported as.
name: hasSpanStreaming ? httpMethod || FUNCTION_SPAN_NAME_FALLBACK : methodAndRoute,
attributes: {
[SENTRY_OP]: FUNCTION,
[CODE_FUNCTION_NAME]: httpMethod,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[HTTP_REQUEST_METHOD]: httpMethod,
[HTTP_ROUTE]: routeId,
// Relay infers the description from `code.function.name`, which would drop the route.
...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: methodAndRoute }),
Comment thread
Lms24 marked this conversation as resolved.
},
onlyIfParent: true,
},
Expand Down
67 changes: 66 additions & 1 deletion packages/sveltekit/test/client/load.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes';
import type { Client } from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import * as SentryCore from '@sentry/core';
import * as SentrySvelte from '@sentry/svelte';
import type { Load } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { wrapLoadWithSentry } from '../../src/client/load';

const mockCaptureException = vi.spyOn(SentrySvelte, 'captureException').mockImplementation(() => 'xx');
Expand Down Expand Up @@ -108,6 +110,8 @@ describe('wrapLoadWithSentry', () => {
'code.function.name': 'load',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: 'route',
'url.path': '/users/123',
'url.template': '/users/[id]',
},
name: '/users/[id]',
},
Expand Down Expand Up @@ -136,6 +140,7 @@ describe('wrapLoadWithSentry', () => {
'code.function.name': 'load',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
'url.path': '/users/123',
},
name: '/users/123',
},
Expand Down Expand Up @@ -171,6 +176,66 @@ describe('wrapLoadWithSentry', () => {
expect.any(Function),
);
});

describe('with span streaming enabled', () => {
beforeEach(() => {
vi.spyOn(SentryCore, 'getClient').mockImplementation(
() => ({ getOptions: () => ({ traceLifecycle: 'stream' }) }) as unknown as Client,
);
});

afterEach(() => {
vi.mocked(SentryCore.getClient).mockRestore();
});

// `MOCK_LOAD_ARGS.route` is mutated by the tests above, so build a fresh event here.
const getLoadArgs = (): any => ({
params: { id: '123' },
route: { id: '/users/[id]' },
url: new URL('http://localhost:3000/users/123'),
});

it('names the span after the load function and keeps the route in the description', async () => {
const wrappedLoad = wrapLoadWithSentry(async () => ({}));
await wrappedLoad(getLoadArgs());

expect(mockStartSpan).toHaveBeenCalledWith(
{
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function',
'code.function.name': 'load',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: 'route',
'url.path': '/users/123',
'url.template': '/users/[id]',
'sentry.description': '/users/[id]',
},
name: 'load',
},
expect.any(Function),
);
});

it("keeps the raw URL as description if `event.route.id` isn't available", async () => {
const wrappedLoad = wrapLoadWithSentry(async () => ({}));
await wrappedLoad({ ...getLoadArgs(), route: {} });

expect(mockStartSpan).toHaveBeenCalledWith(
{
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function',
'code.function.name': 'load',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
'url.path': '/users/123',
'sentry.description': '/users/123',
},
name: 'load',
},
expect.any(Function),
);
});
});
});

it('adds an exception mechanism', async () => {
Expand Down
Loading
Loading