Skip to content
5 changes: 5 additions & 0 deletions .changeset/fuzzy-lies-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: ignore TypeScript errors when `@opentelemetry/api` is not installed
3 changes: 2 additions & 1 deletion packages/kit/scripts/generate-dts.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ await createBundle({
'$app/paths': 'src/runtime/app/paths/public.d.ts',
'$app/server': 'src/runtime/app/server/index.js',
'$app/state': 'src/runtime/app/state/index.js',
'$app/stores': 'src/runtime/app/stores.js'
'$app/stores': 'src/runtime/app/stores.js',
'@opentelemetry/api': 'src/exports/internal/otel.d.ts'
},
include: ['src']
});
Expand Down
12 changes: 12 additions & 0 deletions packages/kit/src/exports/internal/otel.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Users may not have this package installed, this prevents errors when using `tsc` without it. */
/* eslint-disable @typescript-eslint/no-empty-object-type */

export interface Span {}

export interface Tracer {}

export interface SpanContext {}

export interface PropagationAPI {}

export interface ContextAPI {}
1 change: 1 addition & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
LayoutParams as AppLayoutParams,
ResolvedPathname
} from '$app/types';

import { Span } from '@opentelemetry/api';

export { PrerenderOption } from '../types/private.js';
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/telemetry/otel.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/** @import { Tracer, SpanStatusCode, PropagationAPI, ContextAPI } from '@opentelemetry/api' */
/** @import { Tracer, PropagationAPI, ContextAPI } from '@opentelemetry/api' */

/** @type {Promise<{ tracer: Tracer, SpanStatusCode: typeof SpanStatusCode, propagation: PropagationAPI, context: ContextAPI }> | null} */
/** @type {Promise<{ tracer: Tracer, SpanStatusCode: { ERROR: number }, propagation: PropagationAPI, context: ContextAPI }> | null} */
export let otel = null;

if (__SVELTEKIT_SERVER_TRACING_ENABLED__) {
otel = import('@opentelemetry/api')
.then((module) => {
.then((/** @type {any} */ module) => {
return {
tracer: module.trace.getTracer('sveltekit'),
propagation: module.propagation,
Expand Down
90 changes: 47 additions & 43 deletions packages/kit/src/runtime/telemetry/record_span.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,55 +11,59 @@ export async function record_span({ name, attributes, fn }) {

const { SpanStatusCode, tracer } = await otel;

return tracer.startActiveSpan(name, { attributes }, async (span) => {
try {
return await fn(span);
} catch (error) {
if (error instanceof HttpError) {
span.setAttributes({
[`${name}.result.type`]: 'known_error',
[`${name}.result.status`]: error.status,
[`${name}.result.message`]: error.body.message
});
if (error.status >= 500) {
return tracer.startActiveSpan(
name,
{ attributes },
async (/** @type {import('@opentelemetry/api').Span} */ span) => {
try {
return await fn(span);
} catch (error) {
if (error instanceof HttpError) {
span.setAttributes({
[`${name}.result.type`]: 'known_error',
[`${name}.result.status`]: error.status,
[`${name}.result.message`]: error.body.message
});
if (error.status >= 500) {
span.recordException({
name: 'HttpError',
message: error.body.message
});
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.body.message
});
}
} else if (error instanceof Redirect) {
span.setAttributes({
[`${name}.result.type`]: 'redirect',
[`${name}.result.status`]: error.status,
[`${name}.result.location`]: error.location
});
} else if (error instanceof Error) {
span.setAttributes({
[`${name}.result.type`]: 'unknown_error'
});
span.recordException({
name: 'HttpError',
message: error.body.message
name: error.name,
message: error.message,
stack: error.stack
});
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.body.message
message: error.message
});
} else {
span.setAttributes({
[`${name}.result.type`]: 'unknown_error'
});
span.setStatus({ code: SpanStatusCode.ERROR });
}
} else if (error instanceof Redirect) {
span.setAttributes({
[`${name}.result.type`]: 'redirect',
[`${name}.result.status`]: error.status,
[`${name}.result.location`]: error.location
});
} else if (error instanceof Error) {
span.setAttributes({
[`${name}.result.type`]: 'unknown_error'
});
span.recordException({
name: error.name,
message: error.message,
stack: error.stack
});
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
} else {
span.setAttributes({
[`${name}.result.type`]: 'unknown_error'
});
span.setStatus({ code: SpanStatusCode.ERROR });
}

throw error;
} finally {
span.end();
throw error;
} finally {
span.end();
}
}
});
);
}
14 changes: 14 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3371,6 +3371,20 @@ declare module '$app/stores' {
check(): Promise<boolean>;
};

export {};
}

declare module '@opentelemetry/api' {
export interface Span {}

export interface Tracer {}

export interface SpanContext {}

export interface PropagationAPI {}

export interface ContextAPI {}

Comment on lines 3377 to 3392
Copy link
Member

@teemingc teemingc Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried this but I think it removes the autocomplete for users when they do have @opentelemetry/api installed since these types then override the actual package.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a trade off. The alternative is that more users can't build at all unless they completely disable type-checking libraries. If there is a way to get autocomplete when it is installed without breaking dependents missing OTEL, I'm all for it.

I've created floating point airthmatic within the type system, yet this somehow is a harder problem...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should conditionally generate the stub by checking if @opentelemetry/api is installed 😆 otherwise, getting sveltejs/dts-buddy#110 merged would let us use // @ts-ignore even if it's frowned upon in that thread

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that being able to use @ts-ignore would be a better and easier solution.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@teemingc do you have any plans to merge the dts-buddy PR soon? If not I'd like to get this PR merged since it would fix the bug. We could look into using ts-ignore after your PR is merged.

Copy link
Member

@teemingc teemingc Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd need another maintainer to approve the dts-buddy PR to merge it. Even then, it seems like all the tests are failing after I've merged the main branch. I'll need to take a look at those. cc: @Rich-Harris

EDIT: we can't merge this PR as is because the stubs here will break the TypeScript autocomplete for users who do have otel installed

export {};
}/**
* It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following:
Expand Down
21 changes: 0 additions & 21 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading