test(core-shared): assert PII scrub runs before Sentry processors

The previous init-server-node tests only checked that an SDK handle
came back — the ADR-017 §7 invariant (PiiScrubSpanProcessor /
PiiScrubLogRecordProcessor registered BEFORE any exporter-facing
processor) was untested. Capture the NodeSDK constructor config via a
local vi.mock override and assert processor ordering for both the
empty-DSN and DSN-set paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:32:47 +02:00
parent 0234e18425
commit 16310c5d62

View File

@@ -1,5 +1,70 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { initOtelServerNode } from "./init-server-node";
import {
PiiScrubSpanProcessor,
PiiScrubLogRecordProcessor,
} from "./pii-scrub-processor";
import { SentryLogRecordForwarder } from "./sentry-bridge";
const captured = vi.hoisted(() => ({ configs: [] as unknown[] }));
// Hoisted alongside `captured`: the vi.mock factory below is hoisted above
// every top-level statement, so a plain top-level class would still be in its
// temporal dead zone when the factory runs (ReferenceError).
const FakeBatchSpanProcessor = vi.hoisted(
() =>
class FakeBatchSpanProcessor {
constructor(public readonly wrapped: unknown) {}
onStart() {}
onEnd() {}
forceFlush() {
return Promise.resolve();
}
shutdown() {
return Promise.resolve();
}
},
);
// Local mock overrides the setup-file stub (the pattern no-instrumentation.ts
// documents) so the NodeSDK constructor CONFIG is capturable — the previous
// tests asserted nothing about it, leaving the ADR-017 §7 security invariant
// (PII scrub runs before any exporter) untested.
vi.mock("@opentelemetry/sdk-node", () => ({
NodeSDK: class {
constructor(cfg: unknown) {
captured.configs.push(cfg);
}
start() {}
shutdown() {
return Promise.resolve();
}
},
tracing: { BatchSpanProcessor: FakeBatchSpanProcessor },
}));
// Instrumentations auto-enable (patch http/undici/pg globals) in their
// constructors — stub them out entirely, not just registerInstrumentations.
vi.mock("@opentelemetry/instrumentation", () => ({
registerInstrumentations: vi.fn(),
}));
vi.mock("@opentelemetry/instrumentation-http", () => ({
HttpInstrumentation: class {},
}));
vi.mock("@opentelemetry/instrumentation-undici", () => ({
UndiciInstrumentation: class {},
}));
vi.mock("@opentelemetry/instrumentation-pg", () => ({
PgInstrumentation: class {},
}));
type SdkConfig = {
spanProcessors: unknown[];
logRecordProcessors: unknown[];
};
function lastConfig(): SdkConfig {
return captured.configs[captured.configs.length - 1] as SdkConfig;
}
describe("initOtelServerNode", () => {
it("returns an SDK handle with shutdown()", () => {
@@ -8,16 +73,38 @@ describe("initOtelServerNode", () => {
serviceName: "test-service",
environment: "test",
});
expect(sdk).toBeDefined();
expect(typeof sdk.shutdown).toBe("function");
});
it("accepts a DSN and wires the Sentry bridge", () => {
const sdk = initOtelServerNode({
it("registers only PII scrub processors when DSN is empty", () => {
initOtelServerNode({
dsn: "",
serviceName: "test-service",
environment: "test",
});
const cfg = lastConfig();
expect(cfg.spanProcessors).toHaveLength(1);
expect(cfg.spanProcessors[0]).toBeInstanceOf(PiiScrubSpanProcessor);
expect(cfg.logRecordProcessors).toHaveLength(1);
expect(cfg.logRecordProcessors[0]).toBeInstanceOf(
PiiScrubLogRecordProcessor,
);
});
it("registers PII scrub processors BEFORE the Sentry processors when DSN is set (ADR-017 §7)", () => {
initOtelServerNode({
dsn: "https://test@sentry.io/1",
serviceName: "test-service",
environment: "test",
});
expect(sdk).toBeDefined();
const cfg = lastConfig();
expect(cfg.spanProcessors).toHaveLength(2);
expect(cfg.spanProcessors[0]).toBeInstanceOf(PiiScrubSpanProcessor);
expect(cfg.spanProcessors[1]).toBeInstanceOf(FakeBatchSpanProcessor);
expect(cfg.logRecordProcessors).toHaveLength(2);
expect(cfg.logRecordProcessors[0]).toBeInstanceOf(
PiiScrubLogRecordProcessor,
);
expect(cfg.logRecordProcessors[1]).toBeInstanceOf(SentryLogRecordForwarder);
});
});