Skip to content

Commit af14782

Browse files
dahliaclaude
andcommitted
Make simplified overload idempotent
Prevent duplicate OpenTelemetry provider registrations and LogTape sink configurations when createFederationDebugger() is called multiple times without an explicit exporter. A module-level cache reuses the exporter from the first auto-setup call. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 95de96a commit af14782

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

packages/debugger/src/mod.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { notStrictEqual, ok, strictEqual, throws } from "node:assert/strict";
22
import { test } from "node:test";
3-
import { createFederationDebugger } from "@fedify/debugger";
3+
import { createFederationDebugger, resetAutoSetup } from "@fedify/debugger";
44
import type {
55
FederationDebuggerAuth,
66
SerializedLogRecord,
@@ -453,6 +453,7 @@ test("trace detail page shows empty message when no activities", async () => {
453453
test("simplified overload returns Federation without exporter", () => {
454454
// Save original global tracer provider to restore later
455455
const originalProvider = trace.getTracerProvider();
456+
resetAutoSetup();
456457
try {
457458
const { federation } = createMockFederation();
458459
const dbg = createFederationDebugger(federation);
@@ -463,12 +464,14 @@ test("simplified overload returns Federation without exporter", () => {
463464
} finally {
464465
// Restore original provider
465466
trace.setGlobalTracerProvider(originalProvider);
467+
resetAutoSetup();
466468
}
467469
});
468470

469471
test("simplified overload registers a global TracerProvider", () => {
470472
// Disable any existing global provider first
471473
trace.disable();
474+
resetAutoSetup();
472475
try {
473476
const { federation } = createMockFederation();
474477
// Before: the global provider should return a noop tracer
@@ -488,11 +491,13 @@ test("simplified overload registers a global TracerProvider", () => {
488491
span.end();
489492
} finally {
490493
trace.disable();
494+
resetAutoSetup();
491495
}
492496
});
493497

494498
test("simplified overload serves debug dashboard", async () => {
495499
const originalProvider = trace.getTracerProvider();
500+
resetAutoSetup();
496501
try {
497502
const { federation } = createMockFederation();
498503
const dbg = createFederationDebugger(federation);
@@ -505,11 +510,13 @@ test("simplified overload serves debug dashboard", async () => {
505510
ok(html.includes("Fedify Debug Dashboard"));
506511
} finally {
507512
trace.setGlobalTracerProvider(originalProvider);
513+
resetAutoSetup();
508514
}
509515
});
510516

511517
test("simplified overload with custom path", async () => {
512518
const originalProvider = trace.getTracerProvider();
519+
resetAutoSetup();
513520
try {
514521
const { federation } = createMockFederation();
515522
const dbg = createFederationDebugger(federation, { path: "/_dbg" });
@@ -520,11 +527,13 @@ test("simplified overload with custom path", async () => {
520527
ok(ct.includes("text/html"), `Expected text/html, got ${ct}`);
521528
} finally {
522529
trace.setGlobalTracerProvider(originalProvider);
530+
resetAutoSetup();
523531
}
524532
});
525533

526534
test("simplified overload delegates non-debug requests", async () => {
527535
const originalProvider = trace.getTracerProvider();
536+
resetAutoSetup();
528537
try {
529538
const { federation, calls } = createMockFederation();
530539
const dbg = createFederationDebugger(federation);
@@ -535,11 +544,13 @@ test("simplified overload delegates non-debug requests", async () => {
535544
strictEqual(await response.text(), "Federation response");
536545
} finally {
537546
trace.setGlobalTracerProvider(originalProvider);
547+
resetAutoSetup();
538548
}
539549
});
540550

541551
test("simplified overload JSON API returns traces", async () => {
542552
const originalProvider = trace.getTracerProvider();
553+
resetAutoSetup();
543554
try {
544555
const { federation } = createMockFederation();
545556
const dbg = createFederationDebugger(federation);
@@ -555,6 +566,7 @@ test("simplified overload JSON API returns traces", async () => {
555566
strictEqual(body.length, 0);
556567
} finally {
557568
trace.setGlobalTracerProvider(originalProvider);
569+
resetAutoSetup();
558570
}
559571
});
560572

@@ -947,6 +959,46 @@ test("auth password: forged session cookie is rejected", async () => {
947959
ok(html.includes("Login Required"));
948960
});
949961

962+
// ---------- Idempotency tests ----------
963+
964+
test("simplified overload is idempotent: repeated calls share exporter", async () => {
965+
trace.disable();
966+
resetAutoSetup();
967+
try {
968+
const { federation: fed1 } = createMockFederation();
969+
const { federation: fed2 } = createMockFederation();
970+
// First call: sets up global OTel + exporter
971+
const dbg1 = createFederationDebugger(fed1);
972+
// Second call: should reuse the same exporter, so both dashboards
973+
// see the same trace data
974+
const dbg2 = createFederationDebugger(fed2);
975+
// Both should still be functional
976+
notStrictEqual(dbg1, null);
977+
notStrictEqual(dbg2, null);
978+
strictEqual(typeof dbg1.fetch, "function");
979+
strictEqual(typeof dbg2.fetch, "function");
980+
// Both should serve the same trace data (shared exporter)
981+
const r1 = await dbg1.fetch(
982+
new Request("https://example.com/__debug__/api/traces"),
983+
{ contextData: undefined },
984+
);
985+
const r2 = await dbg2.fetch(
986+
new Request("https://example.com/__debug__/api/traces"),
987+
{ contextData: undefined },
988+
);
989+
const t1 = await r1.json();
990+
const t2 = await r2.json();
991+
strictEqual(
992+
JSON.stringify(t1),
993+
JSON.stringify(t2),
994+
"Both debugger instances should share the same exporter",
995+
);
996+
} finally {
997+
trace.disable();
998+
resetAutoSetup();
999+
}
1000+
});
1001+
9501002
// ---------- Sink property tests ----------
9511003

9521004
test("createFederationDebugger exposes a sink property", () => {
@@ -960,13 +1012,15 @@ test("createFederationDebugger exposes a sink property", () => {
9601012

9611013
test("simplified overload exposes a sink property", () => {
9621014
const originalProvider = trace.getTracerProvider();
1015+
resetAutoSetup();
9631016
try {
9641017
const { federation } = createMockFederation();
9651018
const dbg = createFederationDebugger(federation);
9661019
notStrictEqual(dbg.sink, null);
9671020
strictEqual(typeof dbg.sink, "function");
9681021
} finally {
9691022
trace.setGlobalTracerProvider(originalProvider);
1023+
resetAutoSetup();
9701024
}
9711025
});
9721026

packages/debugger/src/mod.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@ export interface SerializedLogRecord {
6363

6464
const DEFAULT_MAX_LOG_ENTRIES = 10_000;
6565

66+
/**
67+
* Cached auto-setup state so that repeated calls to
68+
* `createFederationDebugger()` without an explicit exporter reuse the same
69+
* global OpenTelemetry tracer provider and exporter instead of registering
70+
* duplicate providers and LogTape sinks.
71+
*/
72+
let _autoSetup: { exporter: FedifySpanExporter } | undefined;
73+
74+
/**
75+
* Resets the internal auto-setup state. This is intended **only for tests**
76+
* that need to exercise the auto-setup code path more than once within the
77+
* same process.
78+
*
79+
* @internal
80+
*/
81+
export function resetAutoSetup(): void {
82+
_autoSetup = undefined;
83+
}
84+
6685
/**
6786
* In-memory storage for log records grouped by trace ID.
6887
*/
@@ -316,6 +335,10 @@ export function createFederationDebugger<TContextData>(
316335
let exporter: FedifySpanExporter;
317336
if (options != null && "exporter" in options) {
318337
exporter = options.exporter;
338+
} else if (_autoSetup != null) {
339+
// Reuse the exporter from a previous auto-setup call so that repeated
340+
// calls without an explicit exporter share the same global state.
341+
exporter = _autoSetup.exporter;
319342
} else {
320343
// Auto-setup: create MemoryKvStore, FedifySpanExporter,
321344
// BasicTracerProvider, and register globally
@@ -372,6 +395,8 @@ export function createFederationDebugger<TContextData>(
372395
contextLocalStorage: new AsyncLocalStorage(),
373396
});
374397
}
398+
399+
_autoSetup = { exporter };
375400
}
376401

377402
const auth = options?.auth;

0 commit comments

Comments
 (0)