Skip to content

Commit 78a65ea

Browse files
feat: Add tracing to load, server actions, and handle/resolve (#13900)
* feat: Add tracing to `load`, server actions, and `handle`/`resolve` * feat: add universal IDs to client nodes * approve otp dep build * add links for docs * since tags * clarify * explain util * import the rest of the types through jsdoc import statements * generate types --------- Co-authored-by: Chew Tee Ming <[email protected]>
1 parent 62777fe commit 78a65ea

36 files changed

+1701
-232
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@parcel/watcher",
4141
"esbuild",
4242
"netlify-cli",
43+
"protobufjs",
4344
"rolldown",
4445
"sharp",
4546
"svelte-preprocess",

packages/kit/src/core/config/index.spec.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ const get_defaults = (prefix = '') => ({
7676
publicPrefix: 'PUBLIC_',
7777
privatePrefix: ''
7878
},
79+
experimental: {
80+
tracing: undefined
81+
},
7982
files: {
8083
assets: join(prefix, 'static'),
8184
hooks: {
@@ -404,3 +407,60 @@ test('errors on loading config with incorrect default export', async () => {
404407
'The Svelte config file must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
405408
);
406409
});
410+
411+
test('accepts valid tracing values', () => {
412+
assert.doesNotThrow(() => {
413+
validate_config({
414+
kit: {
415+
experimental: {
416+
tracing: 'server'
417+
}
418+
}
419+
});
420+
});
421+
422+
assert.doesNotThrow(() => {
423+
validate_config({
424+
kit: {
425+
experimental: {
426+
tracing: undefined
427+
}
428+
}
429+
});
430+
});
431+
});
432+
433+
test('errors on invalid tracing values', () => {
434+
assert.throws(() => {
435+
validate_config({
436+
kit: {
437+
experimental: {
438+
// @ts-expect-error - given value expected to throw
439+
tracing: true
440+
}
441+
}
442+
});
443+
}, /^config\.kit\.experimental\.tracing should be undefined or "server"$/);
444+
445+
assert.throws(() => {
446+
validate_config({
447+
kit: {
448+
experimental: {
449+
// @ts-expect-error - given value expected to throw
450+
tracing: false
451+
}
452+
}
453+
});
454+
}, /^config\.kit\.experimental\.tracing should be undefined or "server"$/);
455+
456+
assert.throws(() => {
457+
validate_config({
458+
kit: {
459+
experimental: {
460+
// @ts-expect-error - given value expected to throw
461+
tracing: 'client'
462+
}
463+
}
464+
});
465+
}, /^config\.kit\.experimental\.tracing should be undefined or "server"$/);
466+
});

packages/kit/src/core/config/options.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ const options = object(
120120
privatePrefix: string('')
121121
}),
122122

123+
experimental: object({
124+
tracing: validate(undefined, (input, keypath) => {
125+
if (input !== 'server') {
126+
throw new Error(`${keypath} should be undefined or "server"`);
127+
}
128+
return input;
129+
})
130+
}),
131+
123132
files: object({
124133
assets: string('static'),
125134
hooks: object({

packages/kit/src/core/sync/write_server.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ import { set_building, set_prerendering } from '__sveltekit/environment';
3333
import { set_assets } from '__sveltekit/paths';
3434
import { set_manifest, set_read_implementation } from '__sveltekit/server';
3535
import { set_private_env, set_public_env, set_safe_public_env } from '${runtime_directory}/shared-server.js';
36+
import { get_tracer, enable_tracing } from '${runtime_directory}/telemetry/get_tracer.js';
37+
38+
if (${s(config.kit.experimental.tracing === 'server')}) {
39+
enable_tracing();
40+
}
3641
3742
export const options = {
3843
app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
@@ -60,6 +65,7 @@ export const options = {
6065
.replace(/%sveltekit\.status%/g, '" + status + "')
6166
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
6267
},
68+
tracer: get_tracer(),
6369
version_hash: ${s(hash(config.kit.version.name))}
6470
};
6571

packages/kit/src/exports/hooks/sequence.js

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/** @import { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit' */
2+
/** @import { MaybePromise } from 'types' */
3+
import { with_event } from '../../runtime/app/server/event.js';
4+
import { get_tracer } from '../../runtime/telemetry/get_tracer.js';
5+
import { record_span } from '../../runtime/telemetry/record_span.js';
6+
17
/**
28
* A helper function for sequencing multiple `handle` calls in a middleware-like manner.
39
* The behavior for the `handle` options is as follows:
@@ -66,56 +72,77 @@
6672
* first post-processing
6773
* ```
6874
*
69-
* @param {...import('@sveltejs/kit').Handle} handlers The chain of `handle` functions
70-
* @returns {import('@sveltejs/kit').Handle}
75+
* @param {...Handle} handlers The chain of `handle` functions
76+
* @returns {Handle}
7177
*/
7278
export function sequence(...handlers) {
7379
const length = handlers.length;
7480
if (!length) return ({ event, resolve }) => resolve(event);
7581

76-
return ({ event, resolve }) => {
82+
return async ({ event, resolve }) => {
83+
// there's an assumption here that people aren't doing something insane like sequence(() => {}, sequence(() => {}))
84+
// worst case there is that future spans get a lower-down span as their root span -- the tracing would still work,
85+
// it'd just look a little weird
86+
const { rootSpan } = event.tracing;
87+
const tracer = await get_tracer();
7788
return apply_handle(0, event, {});
7889

7990
/**
8091
* @param {number} i
81-
* @param {import('@sveltejs/kit').RequestEvent} event
82-
* @param {import('@sveltejs/kit').ResolveOptions | undefined} parent_options
83-
* @returns {import('types').MaybePromise<Response>}
92+
* @param {RequestEvent} event
93+
* @param {ResolveOptions | undefined} parent_options
94+
* @returns {MaybePromise<Response>}
8495
*/
8596
function apply_handle(i, event, parent_options) {
8697
const handle = handlers[i];
8798

88-
return handle({
89-
event,
90-
resolve: (event, options) => {
91-
/** @type {import('@sveltejs/kit').ResolveOptions['transformPageChunk']} */
92-
const transformPageChunk = async ({ html, done }) => {
93-
if (options?.transformPageChunk) {
94-
html = (await options.transformPageChunk({ html, done })) ?? '';
95-
}
99+
return record_span({
100+
tracer,
101+
name: 'sveltekit.handle.child',
102+
attributes: {
103+
'sveltekit.handle.child.index': i
104+
},
105+
fn: async (span) => {
106+
const traced_event = { ...event, tracing: { rootSpan, currentSpan: span } };
107+
return await with_event(traced_event, () =>
108+
handle({
109+
event: traced_event,
110+
resolve: (event, options) => {
111+
/** @type {ResolveOptions['transformPageChunk']} */
112+
const transformPageChunk = async ({ html, done }) => {
113+
if (options?.transformPageChunk) {
114+
html = (await options.transformPageChunk({ html, done })) ?? '';
115+
}
96116

97-
if (parent_options?.transformPageChunk) {
98-
html = (await parent_options.transformPageChunk({ html, done })) ?? '';
99-
}
117+
if (parent_options?.transformPageChunk) {
118+
html = (await parent_options.transformPageChunk({ html, done })) ?? '';
119+
}
100120

101-
return html;
102-
};
121+
return html;
122+
};
103123

104-
/** @type {import('@sveltejs/kit').ResolveOptions['filterSerializedResponseHeaders']} */
105-
const filterSerializedResponseHeaders =
106-
parent_options?.filterSerializedResponseHeaders ??
107-
options?.filterSerializedResponseHeaders;
124+
/** @type {ResolveOptions['filterSerializedResponseHeaders']} */
125+
const filterSerializedResponseHeaders =
126+
parent_options?.filterSerializedResponseHeaders ??
127+
options?.filterSerializedResponseHeaders;
108128

109-
/** @type {import('@sveltejs/kit').ResolveOptions['preload']} */
110-
const preload = parent_options?.preload ?? options?.preload;
129+
/** @type {ResolveOptions['preload']} */
130+
const preload = parent_options?.preload ?? options?.preload;
111131

112-
return i < length - 1
113-
? apply_handle(i + 1, event, {
114-
transformPageChunk,
115-
filterSerializedResponseHeaders,
116-
preload
117-
})
118-
: resolve(event, { transformPageChunk, filterSerializedResponseHeaders, preload });
132+
return i < length - 1
133+
? apply_handle(i + 1, event, {
134+
transformPageChunk,
135+
filterSerializedResponseHeaders,
136+
preload
137+
})
138+
: resolve(event, {
139+
transformPageChunk,
140+
filterSerializedResponseHeaders,
141+
preload
142+
});
143+
}
144+
})
145+
);
119146
}
120147
});
121148
}

packages/kit/src/exports/hooks/sequence.spec.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { installPolyfills } from '../node/polyfills.js';
44

55
installPolyfills();
66

7+
const dummy_event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({
8+
tracing: { rootSpan: {} }
9+
});
10+
711
test('applies handlers in sequence', async () => {
812
/** @type {string[]} */
913
const order = [];
@@ -29,10 +33,9 @@ test('applies handlers in sequence', async () => {
2933
}
3034
);
3135

32-
const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({});
3336
const response = new Response();
3437

35-
assert.equal(await handler({ event, resolve: () => response }), response);
38+
assert.equal(await handler({ event: dummy_event, resolve: () => response }), response);
3639
expect(order).toEqual(['1a', '2a', '3a', '3b', '2b', '1b']);
3740
});
3841

@@ -47,9 +50,8 @@ test('uses transformPageChunk option passed to non-terminal handle function', as
4750
async ({ event, resolve }) => resolve(event)
4851
);
4952

50-
const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({});
5153
const response = await handler({
52-
event,
54+
event: dummy_event,
5355
resolve: async (_event, opts = {}) => {
5456
let html = '';
5557

@@ -84,9 +86,8 @@ test('merges transformPageChunk option', async () => {
8486
}
8587
);
8688

87-
const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({});
8889
const response = await handler({
89-
event,
90+
event: dummy_event,
9091
resolve: async (_event, opts = {}) => {
9192
let html = '';
9293

@@ -117,9 +118,8 @@ test('uses first defined preload option', async () => {
117118
}
118119
);
119120

120-
const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({});
121121
const response = await handler({
122-
event,
122+
event: dummy_event,
123123
resolve: (_event, opts = {}) => {
124124
let html = '';
125125

@@ -150,9 +150,8 @@ test('uses first defined filterSerializedResponseHeaders option', async () => {
150150
}
151151
);
152152

153-
const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({});
154153
const response = await handler({
155-
event,
154+
event: dummy_event,
156155
resolve: (_event, opts = {}) => {
157156
let html = '';
158157

packages/kit/src/exports/public.d.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '../types/private.js';
1919
import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from 'types';
2020
import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte';
21+
import { Span } from '@opentelemetry/api';
2122

2223
export { PrerenderOption } from '../types/private.js';
2324

@@ -401,6 +402,15 @@ export interface KitConfig {
401402
*/
402403
privatePrefix?: string;
403404
};
405+
/** Experimental features. Here be dragons. Breaking changes may occur in minor releases. */
406+
experimental?: {
407+
/**
408+
* Whether to enable server-side [OpenTelemetry](https://opentelemetry.io/) tracing for SvelteKit operations including the [`handle` hook](https://svelte.dev/docs/kit/hooks#Server-hooks-handle), [`load` functions](https://svelte.dev/docs/kit/load), and [form actions](https://svelte.dev/docs/kit/form-actions).
409+
* @default undefined
410+
* @since 2.26.0 // TODO: update this before publishing
411+
*/
412+
tracing?: 'server';
413+
};
404414
/**
405415
* Where to find various files within your project.
406416
*/
@@ -967,6 +977,17 @@ export interface LoadEvent<
967977
* ```
968978
*/
969979
untrack: <T>(fn: () => T) => T;
980+
981+
/**
982+
* Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing.\
983+
* @since 2.26.0 // TODO: update this before publishing
984+
*/
985+
tracing: {
986+
/** The root span for the request. This span is named `sveltekit.handle.root`. */
987+
rootSpan: Span;
988+
/** The span associated with the current `load` function. */
989+
currentSpan: Span;
990+
};
970991
}
971992

972993
export interface NavigationEvent<
@@ -1242,6 +1263,17 @@ export interface RequestEvent<
12421263
* `true` for `+server.js` calls coming from SvelteKit without the overhead of actually making an HTTP request. This happens when you make same-origin `fetch` requests on the server.
12431264
*/
12441265
isSubRequest: boolean;
1266+
1267+
/**
1268+
* Access to spans for tracing. If tracing is not enabled, these spans will do nothing.
1269+
* @since 2.26.0 // TODO: update this before publishing
1270+
*/
1271+
tracing: {
1272+
/** The root span for the request. This span is named `sveltekit.handle.root`. */
1273+
rootSpan: Span;
1274+
/** The span associated with the current `handle` hook, `load` function, or form action. */
1275+
currentSpan: Span;
1276+
};
12451277
}
12461278

12471279
/**
@@ -1398,6 +1430,17 @@ export interface ServerLoadEvent<
13981430
* ```
13991431
*/
14001432
untrack: <T>(fn: () => T) => T;
1433+
1434+
/**
1435+
* Access to spans for tracing. If tracing is not enabled, these spans will do nothing.
1436+
* @since 2.26.0 // TODO: update this before publishing
1437+
*/
1438+
tracing: {
1439+
/** The root span for the request. This span is named `sveltekit.handle.root`. */
1440+
rootSpan: Span;
1441+
/** The span associated with the current server `load` function. */
1442+
currentSpan: Span;
1443+
};
14011444
}
14021445

14031446
/**

0 commit comments

Comments
 (0)