diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 9c577f5425c0..21e3e75ef77c 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -101,6 +101,7 @@ const get_defaults = (prefix = '') => ({ serviceWorker: { register: true }, + tracing: false, typescript: {}, paths: { base: '', @@ -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"$/); +}); diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index a2b9bb81759d..dd8d2ba3a985 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -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) }), diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js index c27c61add43c..ebc5808200ae 100644 --- a/packages/kit/src/core/sync/write_client_manifest.js +++ b/packages/kit/src/core/sync/write_client_manifest.js @@ -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( @@ -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'}'; diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 5e93d5c1cd25..f9ac6215ae44 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -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))} }; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index ec930b7d073a..911156d57a2f 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -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. diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 05a797c92c65..ac3d8c0845b9 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -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 }; @@ -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(); } } diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 4b32f56b7350..a293a0e9c043 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -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; } diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 46150a987ae9..b93538837191 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -63,6 +63,7 @@ export async function render_data( event: new_event, state, node, + tracing: options.tracing, parent: async () => { /** @type {Record} */ const data = {}; diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index e7703359234e..8bd317c6ac79 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -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) { @@ -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); @@ -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} */ -export async function handle_action_request(event, server) { +export async function handle_action_request(event, server, tracing) { const actions = server?.actions; if (!actions) { @@ -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); @@ -216,9 +219,10 @@ function check_named_default_separate(actions) { /** * @param {import('@sveltejs/kit').RequestEvent} event * @param {NonNullable} 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'; @@ -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 */ diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index e7b462a74dda..68b77e08dbe1 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -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); } @@ -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); @@ -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); diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 74bd7444af4f..95fd65b73905 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -1,8 +1,10 @@ import { DEV } from 'esm-env'; import { disable_search, make_trackable } from '../../../utils/url.js'; -import { validate_depends } from '../../shared.js'; +import { validate_depends, validate_load_response } from '../../shared.js'; import { b64_encode } 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'; /** * Calls the user's server `load` function. @@ -11,10 +13,11 @@ import { with_event } from '../../app/server/event.js'; * state: import('types').SSRState; * node: import('types').SSRNode | undefined; * parent: () => Promise>; + * tracing: boolean; * }} opts * @returns {Promise} */ -export async function load_server_data({ event, state, node, parent }) { +export async function load_server_data({ event, state, node, parent, tracing }) { if (!node?.server) return null; let is_tracking = true; @@ -68,97 +71,112 @@ export async function load_server_data({ event, state, node, parent }) { let done = false; - const result = await with_event(event, () => - load.call(null, { - ...event, - fetch: (info, init) => { - const url = new URL(info instanceof Request ? info.url : info, event.url); + const tracer = await get_tracer({ is_enabled: tracing }); - if (DEV && done && !uses.dependencies.has(url.href)) { - console.warn( - `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` - ); - } - - // Note: server fetches are not added to uses.depends due to security concerns - return event.fetch(info, init); - }, - /** @param {string[]} deps */ - depends: (...deps) => { - for (const dep of deps) { - const { href } = new URL(dep, event.url); - - if (DEV) { - validate_depends(node.server_id || 'missing route ID', dep); - - if (done && !uses.dependencies.has(href)) { + const result = await record_span({ + name: 'sveltekit.load.server', + tracer, + attributes: { + 'sveltekit.load.node_id': node.server_id || 'unknown', + 'sveltekit.load.type': 'server', + 'sveltekit.route.id': event.route.id || 'unknown' + }, + fn: async () => { + const result = await with_event(event, () => + load.call(null, { + ...event, + fetch: (info, init) => { + const url = new URL(info instanceof Request ? info.url : info, event.url); + + if (DEV && done && !uses.dependencies.has(url.href)) { console.warn( - `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` ); } - } - - uses.dependencies.add(href); - } - }, - params: new Proxy(event.params, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { - console.warn( - `${node.server_id}: Accessing \`params.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` - ); - } - if (is_tracking) { - uses.params.add(key); - } - return target[/** @type {string} */ (key)]; - } - }), - parent: async () => { - if (DEV && done && !uses.parent) { - console.warn( - `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` - ); - } + // Note: server fetches are not added to uses.depends due to security concerns + return event.fetch(info, init); + }, + /** @param {string[]} deps */ + depends: (...deps) => { + for (const dep of deps) { + const { href } = new URL(dep, event.url); + + if (DEV) { + validate_depends(node.server_id || 'missing route ID', dep); + + if (done && !uses.dependencies.has(href)) { + console.warn( + `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + ); + } + } + + uses.dependencies.add(href); + } + }, + params: new Proxy(event.params, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { + console.warn( + `${node.server_id}: Accessing \`params.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` + ); + } + + if (is_tracking) { + uses.params.add(key); + } + return target[/** @type {string} */ (key)]; + } + }), + parent: async () => { + if (DEV && done && !uses.parent) { + console.warn( + `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` + ); + } - if (is_tracking) { - uses.parent = true; - } - return parent(); - }, - route: new Proxy(event.route, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.route) { - console.warn( - `${node.server_id}: Accessing \`route.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` - ); + if (is_tracking) { + uses.parent = true; + } + return parent(); + }, + route: new Proxy(event.route, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.route) { + console.warn( + `${node.server_id}: Accessing \`route.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` + ); + } + + if (is_tracking) { + uses.route = true; + } + return target[/** @type {'id'} */ (key)]; + } + }), + url, + untrack(fn) { + is_tracking = false; + try { + return fn(); + } finally { + is_tracking = true; + } } + }) + ); - if (is_tracking) { - uses.route = true; - } - return target[/** @type {'id'} */ (key)]; - } - }), - url, - untrack(fn) { - is_tracking = false; - try { - return fn(); - } finally { - is_tracking = true; - } - } - }) - ); + return result; + } + }); if (__SVELTEKIT_DEV__) { - validate_load_response(result, node.server_id); + validate_load_response(result, `in ${node.server_id}`); } done = true; @@ -182,6 +200,7 @@ export async function load_server_data({ event, state, node, parent }) { * server_data_promise: Promise; * state: import('types').SSRState; * csr: boolean; + * tracing: boolean; * }} opts * @returns {Promise> | null>} */ @@ -193,7 +212,8 @@ export async function load_data({ server_data_promise, state, resolve_opts, - csr + csr, + tracing }) { const server_data_node = await server_data_promise; @@ -201,20 +221,38 @@ export async function load_data({ return server_data_node?.data ?? null; } - const result = await node.universal.load.call(null, { - url: event.url, - params: event.params, - data: server_data_node?.data ?? null, - route: event.route, - fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), - setHeaders: event.setHeaders, - depends: () => {}, - parent, - untrack: (fn) => fn() + const { load } = node.universal; + + const tracer = await get_tracer({ is_enabled: tracing }); + + const result = await record_span({ + name: 'sveltekit.load.universal', + tracer, + attributes: { + 'sveltekit.load.node_id': node.universal_id || 'unknown', + 'sveltekit.load.type': 'universal', + 'sveltekit.load.environment': 'server', + 'sveltekit.route.id': event.route.id || 'unknown' + }, + fn: async () => { + const result = await load.call(null, { + url: event.url, + params: event.params, + data: server_data_node?.data ?? null, + route: event.route, + fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), + setHeaders: event.setHeaders, + depends: () => {}, + parent, + untrack: (fn) => fn() + }); + + return result; + } }); if (__SVELTEKIT_DEV__) { - validate_load_response(result, node.universal_id); + validate_load_response(result, `in ${node.universal_id}`); } return result ?? null; @@ -398,23 +436,3 @@ async function stream_to_string(stream) { } return result; } - -/** - * @param {any} data - * @param {string} [id] - */ -function validate_load_response(data, id) { - if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { - throw new Error( - `a load function in ${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 {...}\`)` - ); - } -} diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index f29e91329183..a04f2db1637e 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -51,6 +51,7 @@ export async function respond_with_error({ event, state, node: default_layout, + tracing: options.tracing, // eslint-disable-next-line @typescript-eslint/require-await parent: async () => ({}) }); @@ -66,7 +67,8 @@ export async function respond_with_error({ resolve_opts, server_data_promise, state, - csr + csr, + tracing: options.tracing }); branch.push( diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 783c7d0ee65a..5318dcdedab7 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -34,6 +34,8 @@ import { strip_resolution_suffix } from '../pathname.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'; /* global __SVELTEKIT_ADAPTER_NAME__ */ /* global __SVELTEKIT_DEV__ */ @@ -362,32 +364,67 @@ export async function respond(request, options, manifest, state) { disable_search(url); } - const response = await with_event(event, () => - options.hooks.handle({ - event, - resolve: (event, opts) => - // counter-intuitively, we need to clear the event, so that it's not - // e.g. accessible when loading modules needed to handle the request - with_event(null, () => - resolve(event, page_nodes, opts).then((response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } - - add_cookies_to_headers(response.headers, Object.values(new_cookies)); - - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); - } - - return response; - }) - ) - }) - ); + const tracer = await get_tracer({ is_enabled: options.tracing }); + + const response = await record_span({ + name: 'sveltekit.handle', + tracer, + attributes: { + 'sveltekit.route.id': event.route.id || 'unknown', + 'http.method': event.request.method, + 'http.url': event.url.href, + 'sveltekit.is_data_request': is_data_request, + 'sveltekit.is_sub_request': event.isSubRequest + }, + fn: async () => { + return await with_event(event, () => + options.hooks.handle({ + event, + resolve: (event, opts) => { + return record_span({ + name: 'sveltekit.resolve', + tracer, + attributes: { + 'sveltekit.route.id': event.route.id || 'unknown', + 'sveltekit.resolve.transform_page_chunk': !!opts?.transformPageChunk, + 'sveltekit.resolve.filter_serialized_response_headers': + !!opts?.filterSerializedResponseHeaders, + 'sveltekit.resolve.preload': !!opts?.preload + }, + fn: async (resolveSpan) => { + // counter-intuitively, we need to clear the event, so that it's not + // e.g. accessible when loading modules needed to handle the request + return with_event(null, () => + resolve(event, page_nodes, opts).then((response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } + + add_cookies_to_headers(response.headers, Object.values(new_cookies)); + + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } + + resolveSpan.setAttributes({ + 'http.response.status_code': response.status, + 'http.response.body.size': + response.headers.get('content-length') || 'unknown' + }); + + return response; + }) + ); + } + }); + } + }) + ); + } + }); // respond with 304 if etag matches if (response.status === 200 && response.headers.has('etag')) { diff --git a/packages/kit/src/runtime/shared.js b/packages/kit/src/runtime/shared.js index b5c559b4292c..fc88526fa442 100644 --- a/packages/kit/src/runtime/shared.js +++ b/packages/kit/src/runtime/shared.js @@ -14,3 +14,23 @@ export function validate_depends(route_id, dep) { export const INVALIDATED_PARAM = 'x-sveltekit-invalidated'; export const TRAILING_SLASH_PARAM = 'x-sveltekit-trailing-slash'; + +/** + * @param {any} data + * @param {string} [location_description] + */ +export function validate_load_response(data, location_description) { + if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { + throw new Error( + `a load function ${location_description} 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 {...}\`)` + ); + } +} diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 17e2425e3c17..785cc02bad05 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -111,6 +111,7 @@ export interface CSRPageNode { load?: Load; trailingSlash?: TrailingSlash; }; + universal_id?: string; } export type CSRPageNodeLoader = () => Promise; @@ -435,6 +436,7 @@ export interface SSROptions { }): string; error(values: { message: string; status: number }): string; }; + tracing: boolean; version_hash: string; } diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 74d438a6f5cc..9675daf4771a 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -676,6 +676,16 @@ declare module '@sveltejs/kit' { */ 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.