Skip to content

Commit 021f09a

Browse files
committed
feat(deno): add lruMemoizer integration
1 parent 14c669b commit 021f09a

4 files changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// <reference lib="deno.ns" />
2+
3+
import { tracingChannel } from 'node:diagnostics_channel';
4+
import type { Span, TransactionEvent } from '@sentry/core';
5+
import type { DenoClient } from '@sentry/deno';
6+
import {
7+
getActiveSpan,
8+
getCurrentScope,
9+
getGlobalScope,
10+
getIsolationScope,
11+
init,
12+
startSpan,
13+
startSpanManual,
14+
} from '@sentry/deno';
15+
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
16+
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
17+
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
18+
19+
function resetGlobals(): void {
20+
getCurrentScope().clear();
21+
getCurrentScope().setClient(undefined);
22+
getIsolationScope().clear();
23+
getGlobalScope().clear();
24+
}
25+
26+
/** See deno-redis.test.ts — same sink shape, deduped for clarity. */
27+
function transactionSink(): {
28+
beforeSendTransaction: (event: TransactionEvent) => null;
29+
waitFor: (predicate: (event: TransactionEvent) => boolean) => Promise<TransactionEvent>;
30+
} {
31+
const transactions: TransactionEvent[] = [];
32+
const waiters: { predicate: (e: TransactionEvent) => boolean; resolve: (e: TransactionEvent) => void }[] = [];
33+
return {
34+
beforeSendTransaction(event) {
35+
transactions.push(event);
36+
for (let i = waiters.length - 1; i >= 0; i--) {
37+
const w = waiters[i]!;
38+
if (w.predicate(event)) {
39+
waiters.splice(i, 1);
40+
w.resolve(event);
41+
}
42+
}
43+
return null;
44+
},
45+
waitFor(predicate) {
46+
const already = transactions.find(predicate);
47+
if (already) return Promise.resolve(already);
48+
return new Promise<TransactionEvent>(resolve => {
49+
waiters.push({ predicate, resolve });
50+
});
51+
},
52+
};
53+
}
54+
55+
function withTimeout<T>(p: Promise<T>, ms: number, what: string): Promise<T> {
56+
let timer: ReturnType<typeof setTimeout> | undefined;
57+
const timeout = new Promise<T>((_, reject) => {
58+
timer = setTimeout(() => reject(new Error(`Timed out waiting for ${what} after ${ms}ms`)), ms);
59+
});
60+
return Promise.race([p, timeout]).finally(() => {
61+
if (timer !== undefined) clearTimeout(timer);
62+
});
63+
}
64+
65+
Deno.test('lru-memoizer instrumentation: included in default integrations (Deno 2.8.0+)', () => {
66+
resetGlobals();
67+
const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
68+
const names = client.getOptions().integrations.map(i => i.name);
69+
assert(names.includes('LruMemoizer'), `LruMemoizer should be in defaults, got ${names.join(', ')}`);
70+
});
71+
72+
// lru-memoizer creates no span of its own; it restores the caller's scope onto
73+
// the memoized callback (which it fires from a detached `setImmediate`). We drive
74+
// `start` while a parent span is active (capturing the caller's context), then
75+
// drive `asyncStart` from a detached context where that span is NOT active — as
76+
// happens when the callback fires later. Without the restore, work there starts a
77+
// new trace; with it, the parent is active again and a span nests under it.
78+
Deno.test('lru-memoizer instrumentation: restores the caller scope onto the memoized callback', async () => {
79+
resetGlobals();
80+
const sink = transactionSink();
81+
init({
82+
dsn: 'https://username@domain/123',
83+
tracesSampleRate: 1,
84+
beforeSendTransaction: sink.beforeSendTransaction,
85+
});
86+
87+
const channel = tracingChannel('orchestrion:lru-memoizer:load');
88+
const ctx = { arguments: [] };
89+
90+
let parentSpan: Span | undefined;
91+
// `startSpanManual` leaves the span open after the callback but deactivates it,
92+
// so the `asyncStart` below runs with no active span — a genuine detached context.
93+
startSpanManual({ name: 'parent', op: 'test', forceTransaction: true }, span => {
94+
parentSpan = span;
95+
channel.start.runStores(ctx, () => undefined);
96+
});
97+
98+
// Detached: no active span here.
99+
assertEquals(getActiveSpan(), undefined);
100+
101+
let restoredActive: Span | undefined;
102+
channel.asyncStart.runStores(ctx, () => {
103+
restoredActive = getActiveSpan();
104+
startSpan({ name: 'memoized-work', op: 'test' }, () => undefined);
105+
});
106+
channel.asyncEnd.publish(ctx);
107+
parentSpan!.end();
108+
109+
// The callback saw the caller's span restored.
110+
assertEquals(restoredActive, parentSpan);
111+
112+
const parent = await withTimeout(
113+
sink.waitFor(t => t.transaction === 'parent'),
114+
5000,
115+
"'parent' transaction",
116+
);
117+
118+
// The span created in the restored callback nested under the caller, not a new trace.
119+
const child = parent.spans?.find(s => s.description === 'memoized-work');
120+
assertExists(
121+
child,
122+
`expected memoized-work nested under parent, got: ${parent.spans?.map(s => s.description).join(', ')}`,
123+
);
124+
});

packages/deno/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export {
121121
genericPoolChannelIntegration,
122122
knexChannelIntegration,
123123
koaChannelIntegration,
124+
lruMemoizerChannelIntegration,
124125
mongodbChannelIntegration,
125126
mongooseChannelIntegration,
126127
mysqlChannelIntegration,

packages/deno/src/sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
amqplibChannelIntegration,
1616
genericPoolChannelIntegration,
1717
koaChannelIntegration,
18+
lruMemoizerChannelIntegration,
1819
mongodbChannelIntegration,
1920
mongooseChannelIntegration,
2021
mysqlChannelIntegration,
@@ -78,6 +79,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
7879
amqplibChannelIntegration(),
7980
genericPoolChannelIntegration(),
8081
koaChannelIntegration(),
82+
lruMemoizerChannelIntegration(),
8183
mongodbChannelIntegration(),
8284
mongooseChannelIntegration(),
8385
mysqlChannelIntegration(),

packages/deno/test/__snapshots__/mod.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ snapshot[`captureException 1`] = `
118118
"Amqplib",
119119
"GenericPool",
120120
"Koa",
121+
"LruMemoizer",
121122
"Mongo",
122123
"Mongoose",
123124
"Mysql",
@@ -203,6 +204,7 @@ snapshot[`captureMessage 1`] = `
203204
"Amqplib",
204205
"GenericPool",
205206
"Koa",
207+
"LruMemoizer",
206208
"Mongo",
207209
"Mongoose",
208210
"Mysql",
@@ -295,6 +297,7 @@ snapshot[`captureMessage twice 1`] = `
295297
"Amqplib",
296298
"GenericPool",
297299
"Koa",
300+
"LruMemoizer",
298301
"Mongo",
299302
"Mongoose",
300303
"Mysql",
@@ -394,6 +397,7 @@ snapshot[`captureMessage twice 2`] = `
394397
"Amqplib",
395398
"GenericPool",
396399
"Koa",
400+
"LruMemoizer",
397401
"Mongo",
398402
"Mongoose",
399403
"Mysql",

0 commit comments

Comments
 (0)