Skip to content

feat: Add tracing to load, server actions, and handle/resolve #13900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: elliott/init-tracing
Choose a base branch
from
Draft
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
74 changes: 74 additions & 0 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const get_defaults = (prefix = '') => ({
serviceWorker: {
register: true
},
tracing: false,
typescript: {},
paths: {
base: '',
Expand Down Expand Up @@ -380,3 +381,76 @@ test('errors on loading config with incorrect default export', async () => {
'svelte.config.js must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration'
);
});

test('accepts valid tracing values', () => {
// Test boolean values
assert.doesNotThrow(() => {
validate_config({
kit: {
tracing: true
}
});
});

assert.doesNotThrow(() => {
validate_config({
kit: {
tracing: false
}
});
});

// Test string values
assert.doesNotThrow(() => {
validate_config({
kit: {
tracing: 'server'
}
});
});

assert.doesNotThrow(() => {
validate_config({
kit: {
tracing: 'client'
}
});
});

assert.doesNotThrow(() => {
validate_config({
kit: {
tracing: undefined
}
});
});
});

test('errors on invalid tracing values', () => {
assert.throws(() => {
validate_config({
kit: {
// @ts-expect-error - given value expected to throw
tracing: 'invalid'
}
});
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);

assert.throws(() => {
validate_config({
kit: {
// @ts-expect-error - given value expected to throw
tracing: 42
}
});
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);

assert.throws(() => {
validate_config({
kit: {
// @ts-expect-error - given value expected to throw
tracing: null
}
});
}, /^config\.kit\.tracing should be true, false, "server", or "client"$/);
});
6 changes: 6 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ const options = object(
files: fun((filename) => !/\.DS_Store/.test(filename))
}),

tracing: validate(false, (input, keypath) => {
if (typeof input === 'boolean') return input;
if (input === 'server' || input === 'client') return input;
throw new Error(`${keypath} should be true, false, "server", or "client"`);
}),

typescript: object({
config: fun((config) => config)
}),
Expand Down
7 changes: 7 additions & 0 deletions packages/kit/src/core/sync/write_client_manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
);
}

if (kit.tracing === 'client' || kit.tracing === true) {
// we don't need to include these if tracing is disabled -- they make the manifest bigger
declarations.push(`export const universal_id = ${s(node.universal)};`);
}

if (node.component) {
declarations.push(
`export { default as component } from ${s(
Expand Down Expand Up @@ -174,6 +179,8 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {

export const hash = ${s(kit.router.type === 'hash')};

export const tracing = ${s(kit.tracing === true || kit.tracing === 'client')};

export const decode = (type, value) => decoders[type](value);

export { default as root } from '../root.${isSvelte5Plus() ? 'js' : 'svelte'}';
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const options = {
.replace(/%sveltekit\.status%/g, '" + status + "')
.replace(/%sveltekit\.error\.message%/g, '" + message + "')}
},
tracing: ${config.kit.tracing === true || config.kit.tracing === 'server'},
version_hash: ${s(hash(config.kit.version.name))}
};
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,16 @@ export interface KitConfig {
*/
files?(filepath: string): boolean;
};
/**
* Whether to enable OpenTelemetry tracing for SvelteKit operations including handle hooks, load functions, and form actions.
* - `true` - Enable tracing for both server and client
* - `false` - Disable tracing
* - `'server'` - Enable tracing only on the server side
* - `'client'` - Enable tracing only on the client side
* @default false
* @since 2.22.0
*/
tracing?: boolean | 'server' | 'client';
typescript?: {
/**
* A function that allows you to edit the generated `tsconfig.json`. You can mutate the config (recommended) or return a new one.
Expand Down
48 changes: 32 additions & 16 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ import {
import { validate_page_exports } from '../../utils/exports.js';
import { compact } from '../../utils/array.js';
import { HttpError, Redirect, SvelteKitError } from '../control.js';
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js';
import {
INVALIDATED_PARAM,
TRAILING_SLASH_PARAM,
validate_depends,
validate_load_response
} from '../shared.js';
import { get_message, get_status } from '../../utils/error.js';
import { writable } from 'svelte/store';
import { page, update, navigating } from './state.svelte.js';
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
import { record_span } from '../telemetry/record_span.js';
import { get_tracer } from '../telemetry/get_tracer.js';

export { load_css };

Expand Down Expand Up @@ -753,28 +760,37 @@ async function load_node({ loader, parent, url, params, route, server_data_node
}
};

async function traced_load() {
const tracer = await get_tracer({ is_enabled: app.tracing });

return record_span({
name: 'sveltekit.load.universal',
tracer,
attributes: {
'sveltekit.load.node_id': node.universal_id || 'unknown',
'sveltekit.load.type': 'universal',
'sveltekit.load.environment': 'client',
'sveltekit.route.id': route.id || 'unknown'
},
fn: async () => (await node.universal?.load?.call(null, load_input)) ?? null
});
}

if (DEV) {
try {
lock_fetch();
data = (await node.universal.load.call(null, load_input)) ?? null;
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
throw new Error(
`a load function related to route '${route.id}' returned ${
typeof data !== 'object'
? `a ${typeof data}`
: data instanceof Response
? 'a Response object'
: Array.isArray(data)
? 'an array'
: 'a non-plain object'
}, but must return a plain object at the top level (i.e. \`return {...}\`)`
);
}
data = await traced_load();

validate_load_response(
data,
// universal_id isn't populated if tracing is disabled because it adds otherwise unnecessary bloat to the manifest
node.universal_id ? `in ${node.universal_id}` : `related to route '${route.id}'`
);
} finally {
unlock_fetch();
}
} else {
data = (await node.universal.load.call(null, load_input)) ?? null;
data = await traced_load();
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/kit/src/runtime/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export interface SvelteKitApp {
*/
hash: boolean;

/**
* Whether OpenTelemetry tracing is enabled (config.tracing === true || config.tracing === 'client')
*/
tracing: boolean;

root: typeof SvelteComponent;
}

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/server/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function render_data(
event: new_event,
state,
node,
tracing: options.tracing,
parent: async () => {
/** @type {Record<string, any>} */
const data = {};
Expand Down
37 changes: 32 additions & 5 deletions packages/kit/src/runtime/server/page/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { is_form_content_type, negotiate } from '../../../utils/http.js';
import { HttpError, Redirect, ActionFailure, SvelteKitError } from '../../control.js';
import { handle_error_and_jsonify } from '../utils.js';
import { with_event } from '../../app/server/event.js';
import { record_span } from '../../telemetry/record_span.js';
import { get_tracer } from '../../telemetry/get_tracer.js';

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

try {
const data = await call_action(event, actions);
const data = await call_action(event, actions, options.tracing);

if (__SVELTEKIT_DEV__) {
validate_action_return(data);
Expand Down Expand Up @@ -139,9 +141,10 @@ export function is_action_request(event) {
/**
* @param {import('@sveltejs/kit').RequestEvent} event
* @param {import('types').SSRNode['server'] | undefined} server
* @param {boolean} tracing
* @returns {Promise<import('@sveltejs/kit').ActionResult>}
*/
export async function handle_action_request(event, server) {
export async function handle_action_request(event, server, tracing) {
const actions = server?.actions;

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

try {
const data = await call_action(event, actions);
const data = await call_action(event, actions, tracing);

if (__SVELTEKIT_DEV__) {
validate_action_return(data);
Expand Down Expand Up @@ -216,9 +219,10 @@ function check_named_default_separate(actions) {
/**
* @param {import('@sveltejs/kit').RequestEvent} event
* @param {NonNullable<import('types').ServerNode['actions']>} actions
* @param {boolean} tracing
* @throws {Redirect | HttpError | SvelteKitError | Error}
*/
async function call_action(event, actions) {
async function call_action(event, actions, tracing) {
const url = new URL(event.request.url);

let name = 'default';
Expand Down Expand Up @@ -247,7 +251,30 @@ async function call_action(event, actions) {
);
}

return with_event(event, () => action(event));
const tracer = await get_tracer({ is_enabled: tracing });

return record_span({
name: 'sveltekit.action',
tracer,
attributes: {
'sveltekit.action.name': name,
'sveltekit.route.id': event.route.id || 'unknown'
},
fn: async (action_span) => {
const result = await with_event(event, () => action(event));
if (result instanceof ActionFailure) {
action_span.setAttributes({
'sveltekit.action.result.type': 'failure',
'sveltekit.action.result.status': result.status
});
} else {
action_span.setAttributes({
'sveltekit.action.result.type': 'success'
});
}
return result;
}
});
}

/** @param {any} data */
Expand Down
8 changes: 5 additions & 3 deletions packages/kit/src/runtime/server/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function render_page(event, page, options, manifest, state, nodes,
if (is_action_request(event)) {
// for action requests, first call handler in +page.server.js
// (this also determines status code)
action_result = await handle_action_request(event, leaf_node.server);
action_result = await handle_action_request(event, leaf_node.server, options.tracing);
if (action_result?.type === 'redirect') {
return redirect_response(action_result.status, action_result.location);
}
Expand Down Expand Up @@ -166,7 +166,8 @@ export async function render_page(event, page, options, manifest, state, nodes,
if (parent) Object.assign(data, parent.data);
}
return data;
}
},
tracing: options.tracing
});
} catch (e) {
load_error = /** @type {Error} */ (e);
Expand Down Expand Up @@ -194,7 +195,8 @@ export async function render_page(event, page, options, manifest, state, nodes,
resolve_opts,
server_data_promise: server_promises[i],
state,
csr
csr,
tracing: options.tracing
});
} catch (e) {
load_error = /** @type {Error} */ (e);
Expand Down
Loading
Loading