Skip to content

Commit 7492f1b

Browse files
authored
Merge branch 'develop' into abhi-sveltekit-perf-monitor-load-client
2 parents 95679d7 + 046c0c2 commit 7492f1b

File tree

13 files changed

+455
-89
lines changed

13 files changed

+455
-89
lines changed

packages/nextjs/src/server/wrapServerComponentWithSentry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
1+
import { addTracingExtensions, captureException, getCurrentHub, startTransaction } from '@sentry/core';
22
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
33
import * as domain from 'domain';
44

@@ -11,6 +11,8 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
1111
appDirComponent: F,
1212
context: ServerComponentContext,
1313
): F {
14+
addTracingExtensions();
15+
1416
const { componentRoute, componentType } = context;
1517

1618
// Even though users may define server components as async functions, for the client bundles

packages/sveltekit/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"devDependencies": {
3232
"@sveltejs/kit": "^1.11.0",
33+
"svelte": "^3.44.0",
3334
"typescript": "^4.9.3",
3435
"vite": "4.0.0"
3536
},
Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js';
22

3-
export default
4-
makeNPMConfigVariants(
5-
makeBaseNPMConfig({
6-
entrypoints: [
7-
'src/index.server.ts',
8-
'src/index.client.ts',
9-
'src/client/index.ts',
10-
'src/server/index.ts',
11-
],
12-
}),
13-
)
14-
;
3+
export default makeNPMConfigVariants(
4+
makeBaseNPMConfig({
5+
entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'],
6+
packageSpecificConfig: {
7+
external: ['$app/stores'],
8+
},
9+
}),
10+
);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { getActiveTransaction } from '@sentry/core';
2+
import { WINDOW } from '@sentry/svelte';
3+
import type { Span, Transaction, TransactionContext } from '@sentry/types';
4+
5+
import { navigating, page } from '$app/stores';
6+
7+
const DEFAULT_TAGS = {
8+
'routing.instrumentation': '@sentry/sveltekit',
9+
};
10+
11+
/**
12+
* Automatically creates pageload and navigation transactions for the client-side SvelteKit router.
13+
*
14+
* This instrumentation makes use of SvelteKit's `page` and `navigating` stores which can be accessed
15+
* anywhere on the client side.
16+
*
17+
* @param startTransactionFn the function used to start (idle) transactions
18+
* @param startTransactionOnPageLoad controls if pageload transactions should be created (defaults to `true`)
19+
* @param startTransactionOnLocationChange controls if navigation transactions should be created (defauls to `true`)
20+
*/
21+
export function svelteKitRoutingInstrumentation<T extends Transaction>(
22+
startTransactionFn: (context: TransactionContext) => T | undefined,
23+
startTransactionOnPageLoad: boolean = true,
24+
startTransactionOnLocationChange: boolean = true,
25+
): void {
26+
if (startTransactionOnPageLoad) {
27+
instrumentPageload(startTransactionFn);
28+
}
29+
30+
if (startTransactionOnLocationChange) {
31+
instrumentNavigations(startTransactionFn);
32+
}
33+
}
34+
35+
function instrumentPageload(startTransactionFn: (context: TransactionContext) => Transaction | undefined): void {
36+
const initialPath = WINDOW && WINDOW.location && WINDOW.location.pathname;
37+
38+
const pageloadTransaction = startTransactionFn({
39+
name: initialPath,
40+
op: 'pageload',
41+
description: initialPath,
42+
tags: {
43+
...DEFAULT_TAGS,
44+
},
45+
});
46+
47+
page.subscribe(page => {
48+
if (!page) {
49+
return;
50+
}
51+
52+
const routeId = page.route && page.route.id;
53+
54+
if (pageloadTransaction && routeId) {
55+
pageloadTransaction.setName(routeId, 'route');
56+
}
57+
});
58+
}
59+
60+
/**
61+
* Use the `navigating` store to start a transaction on navigations.
62+
*/
63+
function instrumentNavigations(startTransactionFn: (context: TransactionContext) => Transaction | undefined): void {
64+
let routingSpan: Span | undefined = undefined;
65+
let activeTransaction: Transaction | undefined;
66+
67+
navigating.subscribe(navigation => {
68+
if (!navigation) {
69+
// `navigating` emits a 'null' value when the navigation is completed.
70+
// So in this case, we can finish the routing span. If the transaction was an IdleTransaction,
71+
// it will finish automatically and if it was user-created users also need to finish it.
72+
if (routingSpan) {
73+
routingSpan.finish();
74+
routingSpan = undefined;
75+
}
76+
return;
77+
}
78+
79+
const routeDestination = navigation.to && navigation.to.route.id;
80+
const routeOrigin = navigation.from && navigation.from.route.id;
81+
82+
if (routeOrigin === routeDestination) {
83+
return;
84+
}
85+
86+
activeTransaction = getActiveTransaction();
87+
88+
if (!activeTransaction) {
89+
activeTransaction = startTransactionFn({
90+
name: routeDestination || (WINDOW && WINDOW.location && WINDOW.location.pathname),
91+
op: 'navigation',
92+
metadata: { source: 'route' },
93+
tags: {
94+
...DEFAULT_TAGS,
95+
},
96+
});
97+
}
98+
99+
if (activeTransaction) {
100+
if (routingSpan) {
101+
// If a routing span is still open from a previous navigation, we finish it.
102+
routingSpan.finish();
103+
}
104+
routingSpan = activeTransaction.startChild({
105+
op: 'ui.sveltekit.routing',
106+
description: 'SvelteKit Route Change',
107+
});
108+
activeTransaction.setTag('from', routeOrigin);
109+
}
110+
});
111+
}

packages/sveltekit/src/client/sdk.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { defaultRequestInstrumentationOptions } from '@sentry-internal/tracing';
21
import { hasTracingEnabled } from '@sentry/core';
32
import type { BrowserOptions } from '@sentry/svelte';
43
import { BrowserTracing, configureScope, init as initSvelteSdk } from '@sentry/svelte';
54
import { addOrUpdateIntegration } from '@sentry/utils';
65

76
import { applySdkMetadata } from '../common/metadata';
7+
import { svelteKitRoutingInstrumentation } from './router';
88

99
// Treeshakable guard to remove all code related to tracing
1010
declare const __SENTRY_TRACING__: boolean;
1111

1212
/**
13+
* Initialize the client side of the Sentry SvelteKit SDK.
1314
*
14-
* @param options
15+
* @param options Configuration options for the SDK.
1516
*/
1617
export function init(options: BrowserOptions): void {
1718
applySdkMetadata(options, ['sveltekit', 'svelte']);
@@ -33,14 +34,11 @@ function addClientIntegrations(options: BrowserOptions): void {
3334
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
3435
if (hasTracingEnabled(options)) {
3536
const defaultBrowserTracingIntegration = new BrowserTracing({
36-
tracePropagationTargets: [...defaultRequestInstrumentationOptions.tracePropagationTargets],
37-
// TODO: Add SvelteKit router instrumentations
38-
// routingInstrumentation: sveltekitRoutingInstrumentation,
37+
routingInstrumentation: svelteKitRoutingInstrumentation,
3938
});
4039

4140
integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, {
42-
// TODO: Add SvelteKit router instrumentations
43-
// options.routingInstrumentation: sveltekitRoutingInstrumentation,
41+
'options.routingInstrumentation': svelteKitRoutingInstrumentation,
4442
});
4543
}
4644
}
Lines changed: 22 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2-
import { captureException, getCurrentHub, startTransaction } from '@sentry/node';
3-
import type { Transaction } from '@sentry/types';
2+
import type { Span } from '@sentry/core';
3+
import { trace } from '@sentry/core';
4+
import { captureException } from '@sentry/node';
45
import {
56
addExceptionMechanism,
67
baggageHeaderToDynamicSamplingContext,
78
extractTraceparentData,
8-
isThenable,
99
objectify,
1010
} from '@sentry/utils';
1111
import type { Handle } from '@sveltejs/kit';
@@ -51,53 +51,30 @@ function sendErrorToSentry(e: unknown): unknown {
5151
*/
5252
export const sentryHandle: Handle = ({ event, resolve }) => {
5353
return domain.create().bind(() => {
54-
let maybePromiseResult;
55-
5654
const sentryTraceHeader = event.request.headers.get('sentry-trace');
5755
const baggageHeader = event.request.headers.get('baggage');
5856
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
5957
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
6058

61-
// transaction could be undefined if hub extensions were not added.
62-
const transaction: Transaction | undefined = startTransaction({
63-
op: 'http.server',
64-
name: `${event.request.method} ${event.route.id}`,
65-
status: 'ok',
66-
...traceparentData,
67-
metadata: {
68-
source: 'route',
69-
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
70-
},
71-
});
72-
73-
getCurrentHub().getScope()?.setSpan(transaction);
74-
75-
try {
76-
maybePromiseResult = resolve(event);
77-
} catch (e) {
78-
transaction?.setStatus('internal_error');
79-
const sentryError = sendErrorToSentry(e);
80-
transaction?.finish();
81-
throw sentryError;
82-
}
83-
84-
if (isThenable(maybePromiseResult)) {
85-
Promise.resolve(maybePromiseResult).then(
86-
response => {
87-
transaction?.setHttpStatus(response.status);
88-
transaction?.finish();
89-
},
90-
e => {
91-
transaction?.setStatus('internal_error');
92-
sendErrorToSentry(e);
93-
transaction?.finish();
59+
return trace(
60+
{
61+
op: 'http.server',
62+
name: `${event.request.method} ${event.route.id}`,
63+
status: 'ok',
64+
...traceparentData,
65+
metadata: {
66+
source: 'route',
67+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
9468
},
95-
);
96-
} else {
97-
transaction?.setHttpStatus(maybePromiseResult.status);
98-
transaction?.finish();
99-
}
100-
101-
return maybePromiseResult;
69+
},
70+
async (span?: Span) => {
71+
const res = await resolve(event);
72+
if (span) {
73+
span.setHttpStatus(res.status);
74+
}
75+
return res;
76+
},
77+
sendErrorToSentry,
78+
);
10279
})();
10380
};

packages/sveltekit/src/server/load.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2+
import { trace } from '@sentry/core';
13
import { captureException } from '@sentry/node';
2-
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
4+
import {
5+
addExceptionMechanism,
6+
baggageHeaderToDynamicSamplingContext,
7+
extractTraceparentData,
8+
objectify,
9+
} from '@sentry/utils';
310
import type { HttpError, ServerLoad } from '@sveltejs/kit';
11+
import * as domain from 'domain';
412

513
function isHttpError(err: unknown): err is HttpError {
614
return typeof err === 'object' && err !== null && 'status' in err && 'body' in err;
@@ -44,21 +52,30 @@ function sendErrorToSentry(e: unknown): unknown {
4452
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
4553
return new Proxy(origLoad, {
4654
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad>) => {
47-
let maybePromiseResult;
55+
return domain.create().bind(() => {
56+
const [event] = args;
4857

49-
try {
50-
maybePromiseResult = wrappingTarget.apply(thisArg, args);
51-
} catch (e) {
52-
throw sendErrorToSentry(e);
53-
}
58+
const sentryTraceHeader = event.request.headers.get('sentry-trace');
59+
const baggageHeader = event.request.headers.get('baggage');
60+
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
61+
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
5462

55-
if (isThenable(maybePromiseResult)) {
56-
Promise.resolve(maybePromiseResult).then(null, e => {
57-
sendErrorToSentry(e);
58-
});
59-
}
60-
61-
return maybePromiseResult;
63+
const routeId = event.route.id;
64+
return trace(
65+
{
66+
op: 'function.sveltekit.load',
67+
name: routeId ? routeId : event.url.pathname,
68+
status: 'ok',
69+
...traceparentData,
70+
metadata: {
71+
source: routeId ? 'route' : 'url',
72+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
73+
},
74+
},
75+
() => wrappingTarget.apply(thisArg, args),
76+
sendErrorToSentry,
77+
);
78+
})();
6279
},
6380
});
6481
}

0 commit comments

Comments
 (0)