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('anthropic 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('Anthropic_AI'), `Anthropic_AI should be in defaults, got ${names.join(', ')}`);
});

Deno.test('anthropic instrumentation: orchestrion @anthropic-ai/sdk: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: Deno integration tests fail to reset the global installedIntegrations array, causing subsequent tests in the same file to skip crucial setupOnce() logic and fail.
Severity: MEDIUM

Suggested Fix

Update the resetGlobals() function used in the Deno integration test suites to also clear the installedIntegrations array from @sentry/core. This can be achieved by adding installedIntegrations.splice(0, installedIntegrations.length), ensuring each test runs with a clean integration state 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-anthropic/test.ts#L69-L72

Potential issue: The global `installedIntegrations` array is not reset between Deno
integration tests that run within the same file. When the first test initializes an
integration, it adds its name to this array. A subsequent test in the same file will
then skip the integration's `setupOnce()` function because the core `setupIntegration()`
logic sees the integration as already installed. This prevents necessary setup, such as
subscribing to channels, from occurring in the second test, causing it to fail when it
expects behavior configured in `setupOnce()`.

Also affects:

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

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

dsn: 'https://username@domain/123',
tracesSampleRate: 1,
beforeSendTransaction: sink.beforeSendTransaction,
});

const channel = tracingChannel('orchestrion:@anthropic-ai/sdk:chat');

// `arguments[0]` is the request body passed to `messages.create(body, options)`.
const body = { model: 'claude-3-5-sonnet-latest', 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: 'msg_1',
model: 'claude-3-5-sonnet-20241022',
usage: { input_tokens: 10, output_tokens: 5 },
};
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 claude-3-5-sonnet-latest');
assertEquals(aiSpan!.data?.['gen_ai.system'], 'anthropic');
assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'chat');
assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'claude-3-5-sonnet-latest');
assertEquals(aiSpan!.data?.['gen_ai.response.model'], 'claude-3-5-sonnet-20241022');
assertEquals(aiSpan!.data?.['gen_ai.usage.total_tokens'], 15);
assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.orchestrion.anthropic');
});
1 change: 1 addition & 0 deletions packages/deno/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export type { DenoRedisIntegrationOptions } from './integrations/redis';
// adds to the defaults, so users who customize `defaultIntegrations` can re-add it.
export {
amqplibChannelIntegration,
anthropicChannelIntegration,
awsChannelIntegration,
dataloaderChannelIntegration,
expressChannelIntegration,
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 @@ -13,6 +13,7 @@ import {
} from '@sentry/core';
import {
amqplibChannelIntegration,
anthropicChannelIntegration,
awsChannelIntegration,
expressChannelIntegration,
firebaseChannelIntegration,
Expand Down Expand Up @@ -89,6 +90,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
...(MODULE_REGISTER_HOOKS_SUPPORTED
? [
amqplibChannelIntegration(),
anthropicChannelIntegration(),
awsChannelIntegration(),
expressChannelIntegration(),
firebaseChannelIntegration(),
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 @@ -117,6 +117,7 @@ snapshot[`captureException 1`] = `
"DenoRedis",
"Graphql",
"Amqplib",
"Anthropic_AI",
"Aws",
"Express",
"Firebase",
Expand Down Expand Up @@ -210,6 +211,7 @@ snapshot[`captureMessage 1`] = `
"DenoRedis",
"Graphql",
"Amqplib",
"Anthropic_AI",
"Aws",
"Express",
"Firebase",
Expand Down Expand Up @@ -310,6 +312,7 @@ snapshot[`captureMessage twice 1`] = `
"DenoRedis",
"Graphql",
"Amqplib",
"Anthropic_AI",
"Aws",
"Express",
"Firebase",
Expand Down Expand Up @@ -417,6 +420,7 @@ snapshot[`captureMessage twice 2`] = `
"DenoRedis",
"Graphql",
"Amqplib",
"Anthropic_AI",
"Aws",
"Express",
"Firebase",
Expand Down
Loading