From 40022b753a657a2cb7b0faffdc515244222d51c2 Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Sun, 30 Aug 2026 14:43:44 +0530 Subject: [PATCH 1/3] fix(runtime-node): report flush failures correctly --- packages/runtime-node/src/lifecycle.ts | 2 +- packages/runtime-node/test/lifecycle.test.mjs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 packages/runtime-node/test/lifecycle.test.mjs diff --git a/packages/runtime-node/src/lifecycle.ts b/packages/runtime-node/src/lifecycle.ts index 880d44b..cc0386e 100644 --- a/packages/runtime-node/src/lifecycle.ts +++ b/packages/runtime-node/src/lifecycle.ts @@ -204,7 +204,7 @@ export function installAutterAutoFlush( }); const drained = Promise.allSettled( targets.map((target) => target.forceFlush()), - ).then(() => true); + ).then((results) => results.every((result) => result.status === "fulfilled")); const ok = await Promise.race([drained, timedOut]); clearTimeout(timer); if (ok) { diff --git a/packages/runtime-node/test/lifecycle.test.mjs b/packages/runtime-node/test/lifecycle.test.mjs new file mode 100644 index 0000000..3d5fa68 --- /dev/null +++ b/packages/runtime-node/test/lifecycle.test.mjs @@ -0,0 +1,22 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { installAutterAutoFlush } from "../dist/index.js"; + +test("flush reports false when a target rejects", async () => { + const failingTarget = { + forceFlush() { + return Promise.reject(new Error("flush failed")); + }, + }; + + const handle = installAutterAutoFlush({ + targets: [failingTarget], + log: false, + warnOnUnflushedExit: false, + }); + + const result = await handle.flush("reproduction"); + handle.dispose(); + + assert.equal(result, false); +}); From 5fd9311b76d73371b6ff5bef04eb62a2bee4ffed Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Sun, 30 Aug 2026 23:45:57 +0530 Subject: [PATCH 2/3] fix(runtime): redact deeply nested attributes --- packages/runtime-node/src/redact.ts | 65 +++++++++++++++------- packages/runtime-node/test/redact.test.mjs | 35 ++++++++++++ 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/packages/runtime-node/src/redact.ts b/packages/runtime-node/src/redact.ts index 93431a7..3a5ede5 100644 --- a/packages/runtime-node/src/redact.ts +++ b/packages/runtime-node/src/redact.ts @@ -131,22 +131,46 @@ function redactString(value: string, r: CompiledRedactor): string { return out; } -function redactValue(value: unknown, r: CompiledRedactor, depth: number): unknown { - if (typeof value === "string") return redactString(value, r); - if (Array.isArray(value)) { - if (depth <= 0) return value; - return value.map((item) => redactValue(item, r, depth - 1)); - } - // Defensive: OTLP attributes are flat, but callers hand us arbitrary - // objects — walk one more level so nothing sensitive hides inside. - if (typeof value === "object" && value !== null && depth > 0) { - const out: Record = {}; - for (const [k, v] of Object.entries(value as Record)) { - out[k] = isSensitiveKey(k, r) ? r.mask : redactValue(v, r, depth - 1); - } - return out; - } - return value; +function redactValue( + value: unknown, + r: CompiledRedactor, + ancestors: WeakMap, +): unknown { + if (typeof value === "string") return redactString(value, r); + + if (Array.isArray(value)) { + const existing = ancestors.get(value); + if (existing) return existing; + + const out: unknown[] = []; + ancestors.set(value, out); + + for (const item of value) { + out.push(redactValue(item, r, ancestors)); + } + + ancestors.delete(value); + return out; + } + + if (typeof value === "object" && value !== null) { + const existing = ancestors.get(value); + if (existing) return existing; + + const out: Record = {}; + ancestors.set(value, out); + + for (const [k, v] of Object.entries(value as Record)) { + out[k] = isSensitiveKey(k, r) + ? r.mask + : redactValue(v, r, ancestors); + } + + ancestors.delete(value); + return out; + } + + return value; } function isSensitiveKey(key: string, r: CompiledRedactor): boolean { @@ -164,13 +188,12 @@ export function redactAttributes( options?: RedactOptions, ): Attributes { const r = compile(options); - return redactWith(attributes, r, 4); + return redactWith(attributes, r); } function redactWith( attributes: Attributes | null | undefined, - r: CompiledRedactor, - maxDepth: number, + r: CompiledRedactor, ): Attributes { const out: Attributes = {}; if (!attributes) return out; @@ -178,7 +201,7 @@ function redactWith( if (value === undefined) continue; out[key] = isSensitiveKey(key, r) ? r.mask - : (redactValue(value, r, maxDepth) as Attributes[string]); + : (redactValue(value, r, new WeakMap()) as Attributes[string]); } return out; } @@ -194,5 +217,5 @@ export function makeRedactor( return (attributes) => ({ ...(attributes ?? {}) }); } const r = compile(options === true ? undefined : options); - return (attributes) => redactWith(attributes, r, 4); + return (attributes) => redactWith(attributes, r); } diff --git a/packages/runtime-node/test/redact.test.mjs b/packages/runtime-node/test/redact.test.mjs index 3cf0a1d..ece81b6 100644 --- a/packages/runtime-node/test/redact.test.mjs +++ b/packages/runtime-node/test/redact.test.mjs @@ -124,3 +124,38 @@ test("empty/nullish input yields an empty object", () => { assert.deepEqual(redactAttributes(), {}); assert.deepEqual(redactAttributes(null), {}); }); + +test("redacts sensitive keys beyond the nested traversal depth", () => { + const out = redactAttributes({ + context: { + level1: { + level2: { + level3: { + level4: { + password: "SECRET", + }, + }, + }, + }, + }, + }); + + assert.equal( + out.context.level1.level2.level3.level4.password, + MASK, + ); +}); + +test("handles circular references without leaking sensitive values", () => { + const context = {}; + const nested = { password: "SECRET", safe: "ok" }; + + context.self = context; + context.nested = nested; + + const out = redactAttributes({ context }); + + assert.equal(out.context.nested.password, MASK); + assert.equal(out.context.nested.safe, "ok"); + assert.equal(out.context.self, out.context); +}); From 20fce3949365f2e035a6a9cfa3b6a740b04e0fdf Mon Sep 17 00:00:00 2001 From: rajeshaipython-stack Date: Mon, 31 Aug 2026 17:39:36 +0530 Subject: [PATCH 3/3] fix(runtime-node): handle flush target failures --- packages/runtime-node/src/lifecycle.ts | 17 +++- packages/runtime-node/src/server.ts | 20 +++-- packages/runtime-node/test/lifecycle.test.mjs | 78 +++++++++++++++++++ 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/packages/runtime-node/src/lifecycle.ts b/packages/runtime-node/src/lifecycle.ts index cc0386e..1cd0a7a 100644 --- a/packages/runtime-node/src/lifecycle.ts +++ b/packages/runtime-node/src/lifecycle.ts @@ -203,10 +203,19 @@ export function installAutterAutoFlush( timer = setTimeout(() => resolve(false), timeoutMs); }); const drained = Promise.allSettled( - targets.map((target) => target.forceFlush()), - ).then((results) => results.every((result) => result.status === "fulfilled")); - const ok = await Promise.race([drained, timedOut]); - clearTimeout(timer); + targets.map((target) => + Promise.resolve().then(() => target.forceFlush()), + ), + ).then((results) => + results.every((result) => result.status === "fulfilled"), + ); + + let ok: boolean; + try { + ok = await Promise.race([drained, timedOut]); + } finally { + clearTimeout(timer); + } if (ok) { telemetryStats.markAllFlushed(); if (log) { diff --git a/packages/runtime-node/src/server.ts b/packages/runtime-node/src/server.ts index 7594e55..df209bc 100644 --- a/packages/runtime-node/src/server.ts +++ b/packages/runtime-node/src/server.ts @@ -907,12 +907,22 @@ export function initAutterServer(options: AutterServerOptions): AutterServer { // (NodeSDK exposes no forceFlush(), but the processors we handed it do). const flushTarget: FlushTarget = { forceFlush: async () => { - await Promise.allSettled([ - alwaysOnProvider.forceFlush(), - mainSpanProcessor.forceFlush(), - ...(errorTraceBuffer ? [errorTraceBuffer.forceFlush()] : []), - metricReader.forceFlush(), + const results = await Promise.allSettled([ + Promise.resolve().then(() => alwaysOnProvider.forceFlush()), + Promise.resolve().then(() => mainSpanProcessor.forceFlush()), + ...(errorTraceBuffer + ? [Promise.resolve().then(() => errorTraceBuffer.forceFlush())] + : []), + Promise.resolve().then(() => metricReader.forceFlush()), ]); + + const failed = results.find( + (result) => result.status === "rejected", + ); + + if (failed) { + throw failed.reason; + } }, }; registerFlushTarget("active-server", flushTarget); diff --git a/packages/runtime-node/test/lifecycle.test.mjs b/packages/runtime-node/test/lifecycle.test.mjs index 3d5fa68..8a11e0b 100644 --- a/packages/runtime-node/test/lifecycle.test.mjs +++ b/packages/runtime-node/test/lifecycle.test.mjs @@ -20,3 +20,81 @@ test("flush reports false when a target rejects", async () => { assert.equal(result, false); }); + +test("flush reports false when a target throws synchronously", async () => { + const failingTarget = { + forceFlush() { + throw new Error("sync flush failed"); + }, + }; + + const handle = installAutterAutoFlush({ + targets: [failingTarget], + log: false, + warnOnUnflushedExit: false, + }); + + const result = await handle.flush("sync-throw"); + handle.dispose(); + + assert.equal(result, false); +}); + +test("flush reports true when all targets fulfill", async () => { + const firstTarget = { + forceFlush() { + return Promise.resolve(); + }, + }; + + const secondTarget = { + forceFlush() { + return Promise.resolve(); + }, + }; + + const handle = installAutterAutoFlush({ + targets: [firstTarget, secondTarget], + log: false, + warnOnUnflushedExit: false, + }); + + const result = await handle.flush("success"); + handle.dispose(); + + assert.equal(result, true); +}); + +test("flush reports false when a built-in-style target propagates an exporter rejection", async () => { + const exporter = { + forceFlush() { + return Promise.reject(new Error("exporter failed")); + }, + }; + + const builtInStyleTarget = { + async forceFlush() { + const results = await Promise.allSettled([ + exporter.forceFlush(), + Promise.resolve(), + ]); + + const failed = results.find((result) => result.status === "rejected"); + + if (failed) { + throw failed.reason; + } + }, + }; + + const handle = installAutterAutoFlush({ + targets: [builtInStyleTarget], + log: false, + warnOnUnflushedExit: false, + }); + + const result = await handle.flush("built-in-style-rejection"); + handle.dispose(); + + assert.equal(result, false); +});