Skip to content

Commit 0fc4a33

Browse files
chargomeclaude
andcommitted
test(nextjs): Fire lru-memoizer load outside span to test context restore
The load callback was fired inside the startSpan callback, so the memoized callback could see the check span through normal async context propagation — the assertion passed even when orchestrion's context restore was broken. Fire the load after startSpan returns (outside the span's active context) so only the restore can make the callback observe the span, matching the node lru-memoizer integration test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3d0e11c commit 0fc4a33

1 file changed

Lines changed: 23 additions & 13 deletions

File tree

  • dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/app/api/lru-memoizer

dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/app/api/lru-memoizer/route.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ export const dynamic = 'force-dynamic';
66

77
// lru-memoizer's only job (from the SDK's perspective) is to bind the active async context onto the
88
// memoized callback, so it runs in its originating span's context whenever the load resolves. The
9-
// integration creates no spans — we assert the context restore instead. `load` captures its callback
10-
// without resolving; we fire it later from outside the span, and record on the enclosing span whether
11-
// the callback ran in that span's context. We wrap the check in our own span so it lands in the
12-
// transaction's `spans` list (the route handler's active span is nested under Next's `http.server`).
9+
// integration creates no spans — we assert the context restore instead.
10+
//
11+
// `load` captures its callback without resolving. We register the memoized call INSIDE the
12+
// `lru-memoizer-check` span (so orchestrion captures that span as the context to restore), but fire
13+
// the load AFTER `startSpan` returns — i.e. outside the span's active context. That's essential: if
14+
// we fired it inside the span, the callback would see the span through normal async propagation and
15+
// the assertion would pass even with orchestrion's context restore broken. Firing it outside means
16+
// only the restore can make the callback observe the span. Mirrors the node lru-memoizer test.
1317
export async function GET() {
1418
let memoizerLoadCallback: (() => void) | undefined;
1519
const memoizedFn = memoizer({
@@ -19,20 +23,26 @@ export async function GET() {
1923
hash: () => 'key',
2024
});
2125

22-
const contextPreserved = await Sentry.startSpan(
26+
// `startSpan` invokes its callback synchronously, so `memoizerLoadCallback` is captured by the time
27+
// it returns. We don't await here — the callback only fires once the load below runs.
28+
const spanFinished = Sentry.startSpan(
2329
{ name: 'lru-memoizer-check', op: 'run' },
2430
span =>
25-
new Promise<boolean>(resolve => {
31+
new Promise<void>(resolve => {
2632
memoizedFn({ foo: 'bar' }, () => {
27-
const preserved = Sentry.getActiveSpan()?.spanContext().spanId === span.spanContext().spanId;
28-
span.setAttribute('memoized.context_preserved', preserved);
29-
resolve(preserved);
33+
span.setAttribute(
34+
'memoized.context_preserved',
35+
Sentry.getActiveSpan()?.spanContext().spanId === span.spanContext().spanId,
36+
);
37+
resolve();
3038
});
31-
32-
// Fire the load outside the span, so the assertion above proves the context was restored.
33-
memoizerLoadCallback?.();
3439
}),
3540
);
3641

37-
return NextResponse.json({ contextPreserved });
42+
// Fire the load outside the span's context, so the assertion above proves the context was restored.
43+
memoizerLoadCallback?.();
44+
45+
await spanFinished;
46+
47+
return NextResponse.json({ status: 'ok' });
3848
}

0 commit comments

Comments
 (0)