From 16310c5d623eedee70765822817c1f7a7d1a5dd7 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 10 Jul 2026 16:32:47 +0200 Subject: [PATCH] test(core-shared): assert PII scrub runs before Sentry processors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../otel/init-server-node.test.ts | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/core-shared/src/instrumentation/otel/init-server-node.test.ts b/packages/core-shared/src/instrumentation/otel/init-server-node.test.ts index 054d281..1e77799 100644 --- a/packages/core-shared/src/instrumentation/otel/init-server-node.test.ts +++ b/packages/core-shared/src/instrumentation/otel/init-server-node.test.ts @@ -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); }); });