Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// <reference lib="deno.ns" />

import { tracingChannel } from 'node:diagnostics_channel';
import type { TransactionEvent } from '@sentry/core';
import type { DenoClient } from '@sentry/deno';
import { getCurrentScope, getGlobalScope, getIsolationScope, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';

function resetGlobals(): void {
getCurrentScope().clear();
getCurrentScope().setClient(undefined);
getIsolationScope().clear();
getGlobalScope().clear();
}

/** See deno-redis.test.ts — same sink shape, deduped for clarity. */
function transactionSink(): {
beforeSendTransaction: (event: TransactionEvent) => null;
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
} {
const transactions: TransactionEvent[] = [];
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
return {
beforeSendTransaction(event) {
transactions.push(event);
for (let i = waiters.length - 1; i >= 0; i--) {
const w = waiters[i]!;
if (w.predicate(event)) {
waiters.splice(i, 1);
w.resolve(event);
}
}
return null;
},
waitFor(predicate) {
const already = transactions.find(predicate);
if (already) return Promise.resolve(already);
return new Promise<TransactionEvent>(resolve => {
waiters.push({ predicate, resolve });
});
},
};
}

function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
});
return Promise.race([p, timeout]).finally(() => {
if (timer !== undefined) clearTimeout(timer);
});
}

Deno.test('openai instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('OpenAI'), `OpenAI should be in defaults, got ${names.join(', ')}`);
});

Deno.test('openai instrumentation: orchestrion:openai:chat channel produces a nested gen_ai span', async () => {
resetGlobals();
const sink = transactionSink();
init({
Comment on lines +64 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The global installedIntegrations array is not reset between tests, causing setupOnce() to be skipped in the second test, which prevents channel subscribers from being registered and leads to test failure.
Severity: MEDIUM

Suggested Fix

The global state needs to be reset between test runs. The installedIntegrations array should be cleared as part of the test teardown or setup process. For example, the resetGlobals() function could be modified to also clear this array, ensuring each test runs in an isolated environment and setupOnce() is called correctly.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts#L68-L71

Potential issue: The integration test suite contains two separate tests that both call
`init()`. The first test populates the global `installedIntegrations` array. Because
this array is not reset between tests, the second test's call to `init()` finds the
integration is already present in the list. This prevents the `setupOnce()` function
from running during the second test. As `setupOnce()` is responsible for registering
subscribers to the `orchestrion:openai:chat` channel, no subscribers are registered for
the second test. Consequently, when the test publishes events to this channel, there are
no listeners, causing the test to fail its assertions for created spans.

Also affects:

  • dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts:57~62

Did we get this right? 👍 / 👎 to inform future reviews.

dsn: 'https://username@domain/123',
tracesSampleRate: 1,
beforeSendTransaction: sink.beforeSendTransaction,
});
Comment on lines +68 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The global installedIntegrations array is not reset between tests, causing setupOnce() to be skipped on subsequent init() calls and breaking integration functionality.
Severity: MEDIUM

Suggested Fix

Reset the installedIntegrations array between tests. This can be achieved by adding installedIntegrations.length = 0 to the resetGlobals() function or another appropriate test teardown hook to ensure each test starts with a clean state.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts#L68-L71

Potential issue: The global `installedIntegrations` array is not reset between tests.
When `init()` is called a second time in the test suite, `setupIntegration()` finds the
integration name already present in the array and skips the `setupOnce()` call. This
prevents necessary channel listeners from being established for the second test run.
Consequently, expected spans are not created, leading to test failures, such as an
assertion for an `aiSpan` failing because the `gen_ai.chat` span was never generated.


const channel = tracingChannel('orchestrion:openai:chat');

// `arguments[0]` is the request body passed to `create(body, options)`.
const body = { model: 'gpt-4o', messages: [{ role: 'user', content: 'hi' }] };
const ctx: Record<string, unknown> = { arguments: [body] };

startSpan({ name: 'parent', op: 'test' }, () => {
channel.start.runStores(ctx, () => undefined);
channel.end.publish(ctx);
ctx.result = {
id: 'chatcmpl-1',
model: 'gpt-4o-2024-08-06',
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
};
channel.asyncEnd.publish(ctx);
});

const parent = await withTimeout(
sink.waitFor(t => t.transaction === 'parent'),
5000,
"'parent' transaction",
);

const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.chat');
assertExists(aiSpan, `expected a gen_ai.chat child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
assertEquals(aiSpan!.description, 'chat gpt-4o');
assertEquals(aiSpan!.data?.['gen_ai.system'], 'openai');
assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'chat');
assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'gpt-4o');
assertEquals(aiSpan!.data?.['gen_ai.response.model'], 'gpt-4o-2024-08-06');
assertEquals(aiSpan!.data?.['gen_ai.usage.total_tokens'], 15);
assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.orchestrion.openai');
});
1 change: 1 addition & 0 deletions packages/deno/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export {
mongooseChannelIntegration,
mysqlChannelIntegration,
mysql2ChannelIntegration,
openaiChannelIntegration,
postgresChannelIntegration,
postgresJsChannelIntegration,
tediousChannelIntegration,
Expand Down
2 changes: 2 additions & 0 deletions packages/deno/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
mongooseChannelIntegration,
mysqlChannelIntegration,
mysql2ChannelIntegration,
openaiChannelIntegration,
postgresChannelIntegration,
postgresJsChannelIntegration,
tediousChannelIntegration,
Expand Down Expand Up @@ -100,6 +101,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
mongooseChannelIntegration(),
mysqlChannelIntegration(),
mysql2ChannelIntegration(),
openaiChannelIntegration(),
postgresChannelIntegration(),
postgresJsChannelIntegration(),
tediousChannelIntegration(),
Expand Down
4 changes: 4 additions & 0 deletions packages/deno/test/__snapshots__/mod.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ snapshot[`captureException 1`] = `
"Mongoose",
"Mysql",
"Mysql2",
"OpenAI",
"Postgres",
"PostgresJs",
"Tedious",
Expand Down Expand Up @@ -221,6 +222,7 @@ snapshot[`captureMessage 1`] = `
"Mongoose",
"Mysql",
"Mysql2",
"OpenAI",
"Postgres",
"PostgresJs",
"Tedious",
Expand Down Expand Up @@ -320,6 +322,7 @@ snapshot[`captureMessage twice 1`] = `
"Mongoose",
"Mysql",
"Mysql2",
"OpenAI",
"Postgres",
"PostgresJs",
"Tedious",
Expand Down Expand Up @@ -426,6 +429,7 @@ snapshot[`captureMessage twice 2`] = `
"Mongoose",
"Mysql",
"Mysql2",
"OpenAI",
"Postgres",
"PostgresJs",
"Tedious",
Expand Down
Loading