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:
@@ -1,5 +1,70 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { initOtelServerNode } from "./init-server-node";
|
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", () => {
|
describe("initOtelServerNode", () => {
|
||||||
it("returns an SDK handle with shutdown()", () => {
|
it("returns an SDK handle with shutdown()", () => {
|
||||||
@@ -8,16 +73,38 @@ describe("initOtelServerNode", () => {
|
|||||||
serviceName: "test-service",
|
serviceName: "test-service",
|
||||||
environment: "test",
|
environment: "test",
|
||||||
});
|
});
|
||||||
expect(sdk).toBeDefined();
|
|
||||||
expect(typeof sdk.shutdown).toBe("function");
|
expect(typeof sdk.shutdown).toBe("function");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts a DSN and wires the Sentry bridge", () => {
|
it("registers only PII scrub processors when DSN is empty", () => {
|
||||||
const sdk = initOtelServerNode({
|
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",
|
dsn: "https://test@sentry.io/1",
|
||||||
serviceName: "test-service",
|
serviceName: "test-service",
|
||||||
environment: "test",
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user