Skip to content

Commit 4f83fa7

Browse files
authored
feat(deno): Add OpenTelemetry support and vercelAI integration (#17445)
Based on the Cloudflare OTel implementation
1 parent 1c65788 commit 4f83fa7

File tree

7 files changed

+406
-2
lines changed

7 files changed

+406
-2
lines changed

packages/deno/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"/build"
2525
],
2626
"dependencies": {
27+
"@opentelemetry/api": "^1.9.0",
2728
"@sentry/core": "10.11.0"
2829
},
2930
"scripts": {
@@ -42,7 +43,7 @@
4243
"lint:es-compatibility": "es-check es2022 ./build/esm/*.js --module",
4344
"install:deno": "node ./scripts/install-deno.mjs",
4445
"test": "run-s install:deno deno-types test:unit",
45-
"test:unit": "deno test --allow-read --allow-run --no-check",
46+
"test:unit": "deno test --allow-read --allow-run --allow-env --no-check",
4647
"test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update",
4748
"yalc:publish": "yalc publish --push --sig"
4849
},

packages/deno/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,4 @@ export { normalizePathsIntegration } from './integrations/normalizepaths';
101101
export { contextLinesIntegration } from './integrations/contextlines';
102102
export { denoCronIntegration } from './integrations/deno-cron';
103103
export { breadcrumbsIntegration } from './integrations/breadcrumbs';
104+
export { vercelAIIntegration } from './integrations/tracing/vercelai';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* This is a copy of the Vercel AI integration from the cloudflare SDK.
3+
*/
4+
5+
import type { IntegrationFn } from '@sentry/core';
6+
import { addVercelAiProcessors, defineIntegration } from '@sentry/core';
7+
8+
const INTEGRATION_NAME = 'VercelAI';
9+
10+
const _vercelAIIntegration = (() => {
11+
return {
12+
name: INTEGRATION_NAME,
13+
setup(client) {
14+
addVercelAiProcessors(client);
15+
},
16+
};
17+
}) satisfies IntegrationFn;
18+
19+
/**
20+
* Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library.
21+
* This integration is not enabled by default, you need to manually add it.
22+
*
23+
* For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry).
24+
*
25+
* You need to enable collecting spans for a specific call by setting
26+
* `experimental_telemetry.isEnabled` to `true` in the first argument of the function call.
27+
*
28+
* ```javascript
29+
* const result = await generateText({
30+
* model: openai('gpt-4-turbo'),
31+
* experimental_telemetry: { isEnabled: true },
32+
* });
33+
* ```
34+
*
35+
* If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each
36+
* function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs`
37+
* to `true`.
38+
*
39+
* ```javascript
40+
* const result = await generateText({
41+
* model: openai('gpt-4-turbo'),
42+
* experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true },
43+
* });
44+
*/
45+
export const vercelAIIntegration = defineIntegration(_vercelAIIntegration);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api';
2+
import { SpanKind, trace } from '@opentelemetry/api';
3+
import {
4+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
5+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
6+
startInactiveSpan,
7+
startSpanManual,
8+
} from '@sentry/core';
9+
10+
/**
11+
* Set up a mock OTEL tracer to allow inter-op with OpenTelemetry emitted spans.
12+
* This is not perfect but handles easy/common use cases.
13+
*/
14+
export function setupOpenTelemetryTracer(): void {
15+
trace.setGlobalTracerProvider(new SentryDenoTraceProvider());
16+
}
17+
18+
class SentryDenoTraceProvider implements TracerProvider {
19+
private readonly _tracers: Map<string, Tracer> = new Map();
20+
21+
public getTracer(name: string, version?: string, options?: { schemaUrl?: string }): Tracer {
22+
const key = `${name}@${version || ''}:${options?.schemaUrl || ''}`;
23+
if (!this._tracers.has(key)) {
24+
this._tracers.set(key, new SentryDenoTracer());
25+
}
26+
27+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
28+
return this._tracers.get(key)!;
29+
}
30+
}
31+
32+
class SentryDenoTracer implements Tracer {
33+
public startSpan(name: string, options?: SpanOptions): Span {
34+
// Map OpenTelemetry SpanKind to Sentry operation
35+
const op = this._mapSpanKindToOp(options?.kind);
36+
37+
return startInactiveSpan({
38+
name,
39+
...options,
40+
attributes: {
41+
...options?.attributes,
42+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual',
43+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: op,
44+
'sentry.deno_tracer': true,
45+
},
46+
});
47+
}
48+
49+
/**
50+
* NOTE: This does not handle `context` being passed in. It will always put spans on the current scope.
51+
*/
52+
public startActiveSpan<F extends (span: Span) => unknown>(name: string, fn: F): ReturnType<F>;
53+
public startActiveSpan<F extends (span: Span) => unknown>(name: string, options: SpanOptions, fn: F): ReturnType<F>;
54+
public startActiveSpan<F extends (span: Span) => unknown>(
55+
name: string,
56+
options: SpanOptions,
57+
context: Context,
58+
fn: F,
59+
): ReturnType<F>;
60+
public startActiveSpan<F extends (span: Span) => unknown>(
61+
name: string,
62+
options: unknown,
63+
context?: unknown,
64+
fn?: F,
65+
): ReturnType<F> {
66+
const opts = (typeof options === 'object' && options !== null ? options : {}) as SpanOptions;
67+
68+
// Map OpenTelemetry SpanKind to Sentry operation
69+
const op = this._mapSpanKindToOp(opts.kind);
70+
71+
const spanOpts = {
72+
name,
73+
...opts,
74+
attributes: {
75+
...opts.attributes,
76+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual',
77+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: op,
78+
'sentry.deno_tracer': true,
79+
},
80+
};
81+
82+
const callback = (
83+
typeof options === 'function'
84+
? options
85+
: typeof context === 'function'
86+
? context
87+
: typeof fn === 'function'
88+
? fn
89+
: () => {}
90+
) as F;
91+
92+
// In OTEL the semantic matches `startSpanManual` because spans are not auto-ended
93+
return startSpanManual(spanOpts, callback) as ReturnType<F>;
94+
}
95+
96+
private _mapSpanKindToOp(kind?: SpanKind): string {
97+
switch (kind) {
98+
case SpanKind.CLIENT:
99+
return 'http.client';
100+
case SpanKind.SERVER:
101+
return 'http.server';
102+
case SpanKind.PRODUCER:
103+
return 'message.produce';
104+
case SpanKind.CONSUMER:
105+
return 'message.consume';
106+
default:
107+
return 'otel.span';
108+
}
109+
}
110+
}

packages/deno/src/sdk.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { denoContextIntegration } from './integrations/context';
1616
import { contextLinesIntegration } from './integrations/contextlines';
1717
import { globalHandlersIntegration } from './integrations/globalhandlers';
1818
import { normalizePathsIntegration } from './integrations/normalizepaths';
19+
import { setupOpenTelemetryTracer } from './opentelemetry/tracer';
1920
import { makeFetchTransport } from './transports';
2021
import type { DenoOptions } from './types';
2122

@@ -97,5 +98,13 @@ export function init(options: DenoOptions = {}): Client {
9798
transport: options.transport || makeFetchTransport,
9899
};
99100

100-
return initAndBind(DenoClient, clientOptions);
101+
const client = initAndBind(DenoClient, clientOptions);
102+
103+
// Set up OpenTelemetry compatibility to capture spans from libraries using @opentelemetry/api
104+
// Note: This is separate from Deno's native OTEL support and doesn't capture auto-instrumented spans
105+
if (!options.skipOpenTelemetrySetup) {
106+
setupOpenTelemetryTracer();
107+
}
108+
109+
return client;
101110
}

packages/deno/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ export interface BaseDenoOptions {
2323
/** Sets an optional server name (device name) */
2424
serverName?: string;
2525

26+
/**
27+
* The Deno SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
28+
* via a custom trace provider.
29+
* This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
30+
* HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
31+
* This should be good enough for many, but not all integrations.
32+
*
33+
* If you want to opt-out of setting up the OpenTelemetry compatibility tracer, set this to `true`.
34+
*
35+
* @default false
36+
*/
37+
skipOpenTelemetrySetup?: boolean;
38+
2639
/** Callback that is executed when a fatal global error occurs. */
2740
onFatalError?(this: void, error: Error): void;
2841
}

0 commit comments

Comments
 (0)