Skip to content

Commit 74ff497

Browse files
feat: Add tracing to load, server actions, and handle/resolve
1 parent 7f08f16 commit 74ff497

File tree

15 files changed

+365
-130
lines changed

15 files changed

+365
-130
lines changed

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const get_defaults = (prefix = '') => ({
101101
serviceWorker: {
102102
register: true
103103
},
104+
tracing: false,
104105
typescript: {},
105106
paths: {
106107
base: '',
@@ -380,3 +381,76 @@ test('errors on loading config with incorrect default export', async () => {
380381
'svelte.config.js must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
381382
);
382383
});
384+
385+
test('accepts valid tracing values', () => {
386+
// Test boolean values
387+
assert.doesNotThrow(() => {
388+
validate_config({
389+
kit: {
390+
tracing: true
391+
}
392+
});
393+
});
394+
395+
assert.doesNotThrow(() => {
396+
validate_config({
397+
kit: {
398+
tracing: false
399+
}
400+
});
401+
});
402+
403+
// Test string values
404+
assert.doesNotThrow(() => {
405+
validate_config({
406+
kit: {
407+
tracing: 'server'
408+
}
409+
});
410+
});
411+
412+
assert.doesNotThrow(() => {
413+
validate_config({
414+
kit: {
415+
tracing: 'client'
416+
}
417+
});
418+
});
419+
420+
assert.doesNotThrow(() => {
421+
validate_config({
422+
kit: {
423+
tracing: undefined
424+
}
425+
});
426+
});
427+
});
428+
429+
test('errors on invalid tracing values', () => {
430+
assert.throws(() => {
431+
validate_config({
432+
kit: {
433+
// @ts-expect-error - given value expected to throw
434+
tracing: 'invalid'
435+
}
436+
});
437+
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
438+
439+
assert.throws(() => {
440+
validate_config({
441+
kit: {
442+
// @ts-expect-error - given value expected to throw
443+
tracing: 42
444+
}
445+
});
446+
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
447+
448+
assert.throws(() => {
449+
validate_config({
450+
kit: {
451+
// @ts-expect-error - given value expected to throw
452+
tracing: null
453+
}
454+
});
455+
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
456+
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ const options = object(
270270
files: fun((filename) => !/\.DS_Store/.test(filename))
271271
}),
272272

273+
tracing: validate(false, (input, keypath) => {
274+
if (typeof input === 'boolean') return input;
275+
if (input === 'server' || input === 'client') return input;
276+
throw new Error(`${keypath} should be true, false, "server", or "client"`);
277+
}),
278+
273279
typescript: object({
274280
config: fun((config) => config)
275281
}),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
175175
176176
export const hash = ${s(kit.router.type === 'hash')};
177177
178+
export const tracing = ${s(kit.tracing === true || kit.tracing === 'client')};
179+
178180
export const decode = (type, value) => decoders[type](value);
179181
180182
export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const options = {
6060
.replace(/%sveltekit\.status%/g, '" + status + "')
6161
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
6262
},
63+
tracing: ${config.kit.tracing === true || config.kit.tracing === 'server'},
6364
version_hash: ${s(hash(config.kit.version.name))}
6465
};
6566

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,16 @@ export interface KitConfig {
694694
*/
695695
files?(filepath: string): boolean;
696696
};
697+
/**
698+
* Whether to enable OpenTelemetry tracing for SvelteKit operations including handle hooks, load functions, and form actions.
699+
* - `true` - Enable tracing for both server and client
700+
* - `false` - Disable tracing
701+
* - `'server'` - Enable tracing only on the server side
702+
* - `'client'` - Enable tracing only on the client side
703+
* @default false
704+
* @since 2.22.0
705+
*/
706+
tracing?: boolean | 'server' | 'client';
697707
typescript?: {
698708
/**
699709
* A function that allows you to edit the generated `tsconfig.json`. You can mutate the config (recommended) or return a new one.

packages/kit/src/runtime/client/client.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { get_message, get_status } from '../../utils/error.js';
4141
import { writable } from 'svelte/store';
4242
import { page, update, navigating } from './state.svelte.js';
4343
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
44+
import { record_span } from '../telemetry/record_span.js';
45+
import { get_tracer } from '../telemetry/get_tracer.js';
4446

4547
export { load_css };
4648

@@ -753,13 +755,30 @@ async function load_node({ loader, parent, url, params, route, server_data_node
753755
}
754756
};
755757

758+
async function traced_load() {
759+
const tracer = await get_tracer({ is_enabled: app.tracing });
760+
761+
return record_span({
762+
name: 'sveltekit.load.universal',
763+
tracer,
764+
attributes: {
765+
'sveltekit.load.node_id': node.universal_id || 'unknown',
766+
'sveltekit.load.type': 'universal',
767+
'sveltekit.load.environment': 'client',
768+
'sveltekit.route.id': route.id || 'unknown'
769+
},
770+
fn: async () => (await node.universal?.load?.call(null, load_input)) ?? null
771+
});
772+
}
773+
756774
if (DEV) {
757775
try {
758776
lock_fetch();
759-
data = (await node.universal.load.call(null, load_input)) ?? null;
777+
data = await traced_load();
778+
760779
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
761780
throw new Error(
762-
`a load function related to route '${route.id}' returned ${
781+
`the load function located in ${node.universal_id} returned ${
763782
typeof data !== 'object'
764783
? `a ${typeof data}`
765784
: data instanceof Response
@@ -774,7 +793,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
774793
unlock_fetch();
775794
}
776795
} else {
777-
data = (await node.universal.load.call(null, load_input)) ?? null;
796+
data = await traced_load();
778797
}
779798
}
780799

packages/kit/src/runtime/client/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export interface SvelteKitApp {
5656
*/
5757
hash: boolean;
5858

59+
/**
60+
* Whether OpenTelemetry tracing is enabled (config.tracing === true || config.tracing === 'client')
61+
*/
62+
tracing: boolean;
63+
5964
root: typeof SvelteComponent;
6065
}
6166

packages/kit/src/runtime/server/data/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export async function render_data(
6363
event: new_event,
6464
state,
6565
node,
66+
tracing: options.tracing,
6667
parent: async () => {
6768
/** @type {Record<string, any>} */
6869
const data = {};

packages/kit/src/runtime/server/page/actions.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { is_form_content_type, negotiate } from '../../../utils/http.js';
66
import { HttpError, Redirect, ActionFailure, SvelteKitError } from '../../control.js';
77
import { handle_error_and_jsonify } from '../utils.js';
88
import { with_event } from '../../app/server/event.js';
9+
import { record_span } from '../../telemetry/record_span.js';
10+
import { get_tracer } from '../../telemetry/get_tracer.js';
911

1012
/** @param {import('@sveltejs/kit').RequestEvent} event */
1113
export function is_action_json_request(event) {
@@ -51,7 +53,7 @@ export async function handle_action_json_request(event, options, server) {
5153
check_named_default_separate(actions);
5254

5355
try {
54-
const data = await call_action(event, actions);
56+
const data = await call_action(event, actions, options.tracing);
5557

5658
if (__SVELTEKIT_DEV__) {
5759
validate_action_return(data);
@@ -139,9 +141,10 @@ export function is_action_request(event) {
139141
/**
140142
* @param {import('@sveltejs/kit').RequestEvent} event
141143
* @param {import('types').SSRNode['server'] | undefined} server
144+
* @param {boolean} tracing
142145
* @returns {Promise<import('@sveltejs/kit').ActionResult>}
143146
*/
144-
export async function handle_action_request(event, server) {
147+
export async function handle_action_request(event, server, tracing) {
145148
const actions = server?.actions;
146149

147150
if (!actions) {
@@ -164,7 +167,7 @@ export async function handle_action_request(event, server) {
164167
check_named_default_separate(actions);
165168

166169
try {
167-
const data = await call_action(event, actions);
170+
const data = await call_action(event, actions, tracing);
168171

169172
if (__SVELTEKIT_DEV__) {
170173
validate_action_return(data);
@@ -216,9 +219,10 @@ function check_named_default_separate(actions) {
216219
/**
217220
* @param {import('@sveltejs/kit').RequestEvent} event
218221
* @param {NonNullable<import('types').ServerNode['actions']>} actions
222+
* @param {boolean} tracing
219223
* @throws {Redirect | HttpError | SvelteKitError | Error}
220224
*/
221-
async function call_action(event, actions) {
225+
async function call_action(event, actions, tracing) {
222226
const url = new URL(event.request.url);
223227

224228
let name = 'default';
@@ -247,7 +251,30 @@ async function call_action(event, actions) {
247251
);
248252
}
249253

250-
return with_event(event, () => action(event));
254+
const tracer = await get_tracer({ is_enabled: tracing });
255+
256+
return record_span({
257+
name: 'sveltekit.action',
258+
tracer,
259+
attributes: {
260+
'sveltekit.action.name': name,
261+
'sveltekit.route.id': event.route.id || 'unknown'
262+
},
263+
fn: async (action_span) => {
264+
const result = await with_event(event, () => action(event));
265+
if (result instanceof ActionFailure) {
266+
action_span.setAttributes({
267+
'sveltekit.action.result.type': 'failure',
268+
'sveltekit.action.result.status': result.status
269+
});
270+
} else {
271+
action_span.setAttributes({
272+
'sveltekit.action.result.type': 'success'
273+
});
274+
}
275+
return result;
276+
}
277+
});
251278
}
252279

253280
/** @param {any} data */

packages/kit/src/runtime/server/page/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function render_page(event, page, options, manifest, state, nodes,
5656
if (is_action_request(event)) {
5757
// for action requests, first call handler in +page.server.js
5858
// (this also determines status code)
59-
action_result = await handle_action_request(event, leaf_node.server);
59+
action_result = await handle_action_request(event, leaf_node.server, options.tracing);
6060
if (action_result?.type === 'redirect') {
6161
return redirect_response(action_result.status, action_result.location);
6262
}
@@ -166,7 +166,8 @@ export async function render_page(event, page, options, manifest, state, nodes,
166166
if (parent) Object.assign(data, parent.data);
167167
}
168168
return data;
169-
}
169+
},
170+
tracing: options.tracing
170171
});
171172
} catch (e) {
172173
load_error = /** @type {Error} */ (e);
@@ -194,7 +195,8 @@ export async function render_page(event, page, options, manifest, state, nodes,
194195
resolve_opts,
195196
server_data_promise: server_promises[i],
196197
state,
197-
csr
198+
csr,
199+
tracing: options.tracing
198200
});
199201
} catch (e) {
200202
load_error = /** @type {Error} */ (e);

0 commit comments

Comments
 (0)