Skip to content

Commit 19db921

Browse files
feat: Add tracing to load, server actions, and handle/resolve
1 parent eca0d11 commit 19db921

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: '',
@@ -404,3 +405,76 @@ test('errors on loading config with incorrect default export', async () => {
404405
'The Svelte config file must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
405406
);
406407
});
408+
409+
test('accepts valid tracing values', () => {
410+
// Test boolean values
411+
assert.doesNotThrow(() => {
412+
validate_config({
413+
kit: {
414+
tracing: true
415+
}
416+
});
417+
});
418+
419+
assert.doesNotThrow(() => {
420+
validate_config({
421+
kit: {
422+
tracing: false
423+
}
424+
});
425+
});
426+
427+
// Test string values
428+
assert.doesNotThrow(() => {
429+
validate_config({
430+
kit: {
431+
tracing: 'server'
432+
}
433+
});
434+
});
435+
436+
assert.doesNotThrow(() => {
437+
validate_config({
438+
kit: {
439+
tracing: 'client'
440+
}
441+
});
442+
});
443+
444+
assert.doesNotThrow(() => {
445+
validate_config({
446+
kit: {
447+
tracing: undefined
448+
}
449+
});
450+
});
451+
});
452+
453+
test('errors on invalid tracing values', () => {
454+
assert.throws(() => {
455+
validate_config({
456+
kit: {
457+
// @ts-expect-error - given value expected to throw
458+
tracing: 'invalid'
459+
}
460+
});
461+
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
462+
463+
assert.throws(() => {
464+
validate_config({
465+
kit: {
466+
// @ts-expect-error - given value expected to throw
467+
tracing: 42
468+
}
469+
});
470+
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
471+
472+
assert.throws(() => {
473+
validate_config({
474+
kit: {
475+
// @ts-expect-error - given value expected to throw
476+
tracing: null
477+
}
478+
});
479+
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
480+
});

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
@@ -174,6 +174,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
174174
175175
export const hash = ${s(kit.router.type === 'hash')};
176176
177+
export const tracing = ${s(kit.tracing === true || kit.tracing === 'client')};
178+
177179
export const decode = (type, value) => decoders[type](value);
178180
179181
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
@@ -686,6 +686,16 @@ export interface KitConfig {
686686
*/
687687
files?(filepath: string): boolean;
688688
};
689+
/**
690+
* Whether to enable OpenTelemetry tracing for SvelteKit operations including handle hooks, load functions, and form actions.
691+
* - `true` - Enable tracing for both server and client
692+
* - `false` - Disable tracing
693+
* - `'server'` - Enable tracing only on the server side
694+
* - `'client'` - Enable tracing only on the client side
695+
* @default false
696+
* @since 2.22.0
697+
*/
698+
tracing?: boolean | 'server' | 'client';
689699
typescript?: {
690700
/**
691701
* 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
@@ -44,6 +44,8 @@ import { get_message, get_status } from '../../utils/error.js';
4444
import { writable } from 'svelte/store';
4545
import { page, update, navigating } from './state.svelte.js';
4646
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
47+
import { record_span } from '../telemetry/record_span.js';
48+
import { get_tracer } from '../telemetry/get_tracer.js';
4749

4850
export { load_css };
4951

@@ -759,13 +761,30 @@ async function load_node({ loader, parent, url, params, route, server_data_node
759761
}
760762
};
761763

764+
async function traced_load() {
765+
const tracer = await get_tracer({ is_enabled: app.tracing });
766+
767+
return record_span({
768+
name: 'sveltekit.load.universal',
769+
tracer,
770+
attributes: {
771+
'sveltekit.load.node_id': node.universal_id || 'unknown',
772+
'sveltekit.load.type': 'universal',
773+
'sveltekit.load.environment': 'client',
774+
'sveltekit.route.id': route.id || 'unknown'
775+
},
776+
fn: async () => (await node.universal?.load?.call(null, load_input)) ?? null
777+
});
778+
}
779+
762780
if (DEV) {
763781
try {
764782
lock_fetch();
765-
data = (await node.universal.load.call(null, load_input)) ?? null;
783+
data = await traced_load();
784+
766785
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
767786
throw new Error(
768-
`a load function related to route '${route.id}' returned ${
787+
`the load function located in ${node.universal_id} returned ${
769788
typeof data !== 'object'
770789
? `a ${typeof data}`
771790
: data instanceof Response
@@ -780,7 +799,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
780799
unlock_fetch();
781800
}
782801
} else {
783-
data = (await node.universal.load.call(null, load_input)) ?? null;
802+
data = await traced_load();
784803
}
785804
}
786805

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 { get_status, normalize_error } from '../../../utils/error.js';
66
import { is_form_content_type, negotiate } from '../../../utils/http.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)