fix(core-shared): TypeScript type fixes for Sentry adapter layer — null attr filter, structural event types, non-null assertions, vi.stubEnv
This commit is contained in:
@@ -21,7 +21,7 @@ describe("initSentryClient", () => {
|
|||||||
|
|
||||||
it("calls Sentry.init with sendDefaultPii: false (R31)", () => {
|
it("calls Sentry.init with sendDefaultPii: false (R31)", () => {
|
||||||
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -31,7 +31,7 @@ describe("initSentryClient", () => {
|
|||||||
it("attaches replay integration with maskAllText/maskAllInputs/blockAllMedia: true (R34, R35)", () => {
|
it("attaches replay integration with maskAllText/maskAllInputs/blockAllMedia: true (R34, R35)", () => {
|
||||||
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
expect(replayIntegration).toHaveBeenCalledTimes(1);
|
expect(replayIntegration).toHaveBeenCalledTimes(1);
|
||||||
const replayOpts = (replayIntegration as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const replayOpts = (replayIntegration as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -42,7 +42,7 @@ describe("initSentryClient", () => {
|
|||||||
|
|
||||||
it("defaults replaysSessionSampleRate to 0.0 (R37)", () => {
|
it("defaults replaysSessionSampleRate to 0.0 (R37)", () => {
|
||||||
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -51,7 +51,7 @@ describe("initSentryClient", () => {
|
|||||||
|
|
||||||
it("defaults replaysOnErrorSampleRate to 1.0 (R37)", () => {
|
it("defaults replaysOnErrorSampleRate to 1.0 (R37)", () => {
|
||||||
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -60,7 +60,7 @@ describe("initSentryClient", () => {
|
|||||||
|
|
||||||
it("attaches beforeSend + beforeSendTransaction", () => {
|
it("attaches beforeSend + beforeSendTransaction", () => {
|
||||||
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ export function initSentryClient(opts: InitClientOpts): void {
|
|||||||
process.env["SENTRY_ENVIRONMENT"] ?? process.env["NODE_ENV"] ?? "development";
|
process.env["SENTRY_ENVIRONMENT"] ?? process.env["NODE_ENV"] ?? "development";
|
||||||
const release = opts.release ?? "unknown";
|
const release = opts.release ?? "unknown";
|
||||||
|
|
||||||
|
type InitOpts = Parameters<typeof Sentry.init>[0];
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: opts.dsn,
|
dsn: opts.dsn,
|
||||||
environment,
|
environment,
|
||||||
release,
|
release,
|
||||||
tracesSampleRate,
|
tracesSampleRate,
|
||||||
sendDefaultPii: false, // R31
|
sendDefaultPii: false, // R31
|
||||||
beforeSend, // R32
|
beforeSend: beforeSend as unknown as InitOpts["beforeSend"], // R32
|
||||||
beforeSendTransaction, // R33
|
beforeSendTransaction: beforeSendTransaction as unknown as InitOpts["beforeSendTransaction"], // R33
|
||||||
replaysSessionSampleRate: 0.0, // R37 — privacy default
|
replaysSessionSampleRate: 0.0, // R37 — privacy default
|
||||||
replaysOnErrorSampleRate: 1.0, // R37
|
replaysOnErrorSampleRate: 1.0, // R37
|
||||||
integrations: [
|
integrations: [
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe("initSentryServer", () => {
|
|||||||
it("calls Sentry.init with sendDefaultPii: false (R31)", () => {
|
it("calls Sentry.init with sendDefaultPii: false (R31)", () => {
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
expect(Sentry.init).toHaveBeenCalledTimes(1);
|
expect(Sentry.init).toHaveBeenCalledTimes(1);
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -26,7 +26,7 @@ describe("initSentryServer", () => {
|
|||||||
|
|
||||||
it("passes the configured DSN", () => {
|
it("passes the configured DSN", () => {
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -35,7 +35,7 @@ describe("initSentryServer", () => {
|
|||||||
|
|
||||||
it("attaches beforeSend + beforeSendTransaction scrubbers", () => {
|
it("attaches beforeSend + beforeSendTransaction scrubbers", () => {
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
@@ -44,48 +44,45 @@ describe("initSentryServer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses SENTRY_TRACES_SAMPLE_RATE env when set", () => {
|
it("uses SENTRY_TRACES_SAMPLE_RATE env when set", () => {
|
||||||
const prev = process.env["SENTRY_TRACES_SAMPLE_RATE"];
|
vi.stubEnv("SENTRY_TRACES_SAMPLE_RATE", "0.25");
|
||||||
process.env["SENTRY_TRACES_SAMPLE_RATE"] = "0.25";
|
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
expect(call["tracesSampleRate"]).toBe(0.25);
|
expect(call["tracesSampleRate"]).toBe(0.25);
|
||||||
if (prev === undefined) delete process.env["SENTRY_TRACES_SAMPLE_RATE"];
|
vi.unstubAllEnvs();
|
||||||
else process.env["SENTRY_TRACES_SAMPLE_RATE"] = prev;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults tracesSampleRate to 1.0 in dev, 0.1 in production", () => {
|
it("defaults tracesSampleRate to 1.0 in dev, 0.1 in production", () => {
|
||||||
const prevEnv = process.env["NODE_ENV"];
|
// Save and clear rate env to test default logic
|
||||||
const prevRate = process.env["SENTRY_TRACES_SAMPLE_RATE"];
|
const prevRate = process.env["SENTRY_TRACES_SAMPLE_RATE"];
|
||||||
delete process.env["SENTRY_TRACES_SAMPLE_RATE"];
|
delete process.env["SENTRY_TRACES_SAMPLE_RATE"];
|
||||||
|
|
||||||
process.env["NODE_ENV"] = "development";
|
vi.stubEnv("NODE_ENV", "development");
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
expect(
|
expect(
|
||||||
((Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<string, unknown>)[
|
((Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<string, unknown>)[
|
||||||
"tracesSampleRate"
|
"tracesSampleRate"
|
||||||
],
|
],
|
||||||
).toBe(1.0);
|
).toBe(1.0);
|
||||||
|
|
||||||
(Sentry.init as ReturnType<typeof vi.fn>).mockClear();
|
(Sentry.init as ReturnType<typeof vi.fn>).mockClear();
|
||||||
process.env["NODE_ENV"] = "production";
|
vi.stubEnv("NODE_ENV", "production");
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
expect(
|
expect(
|
||||||
((Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<string, unknown>)[
|
((Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<string, unknown>)[
|
||||||
"tracesSampleRate"
|
"tracesSampleRate"
|
||||||
],
|
],
|
||||||
).toBe(0.1);
|
).toBe(0.1);
|
||||||
|
|
||||||
if (prevEnv === undefined) delete process.env["NODE_ENV"];
|
vi.unstubAllEnvs();
|
||||||
else process.env["NODE_ENV"] = prevEnv;
|
|
||||||
if (prevRate !== undefined) process.env["SENTRY_TRACES_SAMPLE_RATE"] = prevRate;
|
if (prevRate !== undefined) process.env["SENTRY_TRACES_SAMPLE_RATE"] = prevRate;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tags events with the app name", () => {
|
it("tags events with the app name", () => {
|
||||||
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
initSentryServer({ dsn: "https://x@y/1", app: "web-next" });
|
||||||
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0][0] as Record<
|
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
|
||||||
string,
|
string,
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -26,14 +26,15 @@ export function initSentryServer(opts: InitServerOpts): void {
|
|||||||
"development";
|
"development";
|
||||||
const release = opts.release ?? process.env["VERCEL_GIT_COMMIT_SHA"] ?? "unknown";
|
const release = opts.release ?? process.env["VERCEL_GIT_COMMIT_SHA"] ?? "unknown";
|
||||||
|
|
||||||
|
type InitOpts = Parameters<typeof Sentry.init>[0];
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: opts.dsn,
|
dsn: opts.dsn,
|
||||||
environment,
|
environment,
|
||||||
release,
|
release,
|
||||||
tracesSampleRate,
|
tracesSampleRate,
|
||||||
sendDefaultPii: false, // R31 — non-negotiable
|
sendDefaultPii: false, // R31 — non-negotiable
|
||||||
beforeSend, // R32
|
beforeSend: beforeSend as unknown as InitOpts["beforeSend"], // R32
|
||||||
beforeSendTransaction, // R33
|
beforeSendTransaction: beforeSendTransaction as unknown as InitOpts["beforeSendTransaction"], // R33
|
||||||
initialScope: { tags: { app: opts.app } },
|
initialScope: { tags: { app: opts.app } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,22 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { beforeSend, beforeSendTransaction } from "@/instrumentation/sentry/scrub";
|
import { beforeSend, beforeSendTransaction } from "@/instrumentation/sentry/scrub";
|
||||||
|
|
||||||
|
// Use structural types that match what scrub functions accept
|
||||||
|
type ScrubEvent = Parameters<typeof beforeSend>[0];
|
||||||
|
type ScrubHint = Parameters<typeof beforeSend>[1];
|
||||||
|
type TxEvent = Parameters<typeof beforeSendTransaction>[0];
|
||||||
|
type TxHint = Parameters<typeof beforeSendTransaction>[1];
|
||||||
|
|
||||||
|
const hint = {} as ScrubHint;
|
||||||
|
const txHint = {} as TxHint;
|
||||||
|
|
||||||
describe("beforeSend", () => {
|
describe("beforeSend", () => {
|
||||||
it("redacts top-level keys whose names contain PII substrings", () => {
|
it("redacts top-level keys whose names contain PII substrings", () => {
|
||||||
const event = {
|
const event = {
|
||||||
extra: { email: "a@b.c", username: "alice" },
|
extra: { email: "a@b.c", username: "alice" },
|
||||||
contexts: { custom: { password: "p", note: "ok" } },
|
contexts: { custom: { password: "p", note: "ok" } },
|
||||||
} as Parameters<typeof beforeSend>[0];
|
} as unknown as ScrubEvent;
|
||||||
const result = beforeSend(event, {} as Parameters<typeof beforeSend>[1]) as Record<
|
const result = beforeSend(event, hint) as Record<string, unknown>;
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const extra = result["extra"] as Record<string, unknown>;
|
const extra = result["extra"] as Record<string, unknown>;
|
||||||
const contexts = result["contexts"] as { custom: Record<string, unknown> };
|
const contexts = result["contexts"] as { custom: Record<string, unknown> };
|
||||||
expect(extra["email"]).toBe("[redacted]");
|
expect(extra["email"]).toBe("[redacted]");
|
||||||
@@ -23,11 +29,8 @@ describe("beforeSend", () => {
|
|||||||
it("redacts derived key names (substring match): userEmail, accessToken, apiKey", () => {
|
it("redacts derived key names (substring match): userEmail, accessToken, apiKey", () => {
|
||||||
const event = {
|
const event = {
|
||||||
extra: { userEmail: "a@b.c", accessToken: "t", apiKey: "k", id: "u1" },
|
extra: { userEmail: "a@b.c", accessToken: "t", apiKey: "k", id: "u1" },
|
||||||
} as Parameters<typeof beforeSend>[0];
|
} as unknown as ScrubEvent;
|
||||||
const result = beforeSend(event, {} as Parameters<typeof beforeSend>[1]) as Record<
|
const result = beforeSend(event, hint) as Record<string, unknown>;
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const extra = result["extra"] as Record<string, unknown>;
|
const extra = result["extra"] as Record<string, unknown>;
|
||||||
expect(extra["userEmail"]).toBe("[redacted]");
|
expect(extra["userEmail"]).toBe("[redacted]");
|
||||||
expect(extra["accessToken"]).toBe("[redacted]");
|
expect(extra["accessToken"]).toBe("[redacted]");
|
||||||
@@ -40,11 +43,8 @@ describe("beforeSend", () => {
|
|||||||
request: {
|
request: {
|
||||||
headers: { Authorization: "Bearer x", "Set-Cookie": "session=abc", "User-Agent": "ua" },
|
headers: { Authorization: "Bearer x", "Set-Cookie": "session=abc", "User-Agent": "ua" },
|
||||||
},
|
},
|
||||||
} as Parameters<typeof beforeSend>[0];
|
} as unknown as ScrubEvent;
|
||||||
const result = beforeSend(event, {} as Parameters<typeof beforeSend>[1]) as Record<
|
const result = beforeSend(event, hint) as Record<string, unknown>;
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const request = result["request"] as { headers: Record<string, unknown> };
|
const request = result["request"] as { headers: Record<string, unknown> };
|
||||||
expect(request.headers["Authorization"]).toBe("[redacted]");
|
expect(request.headers["Authorization"]).toBe("[redacted]");
|
||||||
expect(request.headers["Set-Cookie"]).toBe("[redacted]");
|
expect(request.headers["Set-Cookie"]).toBe("[redacted]");
|
||||||
@@ -52,38 +52,30 @@ describe("beforeSend", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("redacts IPv4 addresses found in string values", () => {
|
it("redacts IPv4 addresses found in string values", () => {
|
||||||
const event = { extra: { note: "Connection from 192.168.1.10 failed" } } as Parameters<
|
const event = {
|
||||||
typeof beforeSend
|
extra: { note: "Connection from 192.168.1.10 failed" },
|
||||||
>[0];
|
} as unknown as ScrubEvent;
|
||||||
const result = beforeSend(event, {} as Parameters<typeof beforeSend>[1]) as Record<
|
const result = beforeSend(event, hint) as Record<string, unknown>;
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const extra = result["extra"] as Record<string, unknown>;
|
const extra = result["extra"] as Record<string, unknown>;
|
||||||
expect(extra["note"]).toBe("Connection from [redacted-ip] failed");
|
expect(extra["note"]).toBe("Connection from [redacted-ip] failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redacts IPv6 addresses found in string values", () => {
|
it("redacts IPv6 addresses found in string values", () => {
|
||||||
const event = { extra: { note: "Tunnel to fe80::1ff:fe23:4567:890a established" } } as Parameters<
|
const event = {
|
||||||
typeof beforeSend
|
extra: { note: "Tunnel to fe80::1ff:fe23:4567:890a established" },
|
||||||
>[0];
|
} as unknown as ScrubEvent;
|
||||||
const result = beforeSend(event, {} as Parameters<typeof beforeSend>[1]) as Record<
|
const result = beforeSend(event, hint) as Record<string, unknown>;
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
const extra = result["extra"] as Record<string, unknown>;
|
const extra = result["extra"] as Record<string, unknown>;
|
||||||
expect(extra["note"] as string).toContain("[redacted-ip]");
|
expect(extra["note"] as string).toContain("[redacted-ip]");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not crash on null/undefined branches", () => {
|
it("does not crash on null/undefined branches", () => {
|
||||||
expect(beforeSend({ extra: null } as Parameters<typeof beforeSend>[0], {} as Parameters<typeof beforeSend>[1])).toBeTruthy();
|
expect(beforeSend({ extra: null } as unknown as ScrubEvent, hint)).toBeTruthy();
|
||||||
expect(beforeSend({} as Parameters<typeof beforeSend>[0], {} as Parameters<typeof beforeSend>[1])).toBeTruthy();
|
expect(beforeSend({} as ScrubEvent, hint)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the event (not null) — keeps Sentry transport flowing", () => {
|
it("returns the event (not null) — keeps Sentry transport flowing", () => {
|
||||||
expect(
|
expect(beforeSend({ extra: { ok: true } } as unknown as ScrubEvent, hint)).toBeTruthy();
|
||||||
beforeSend({ extra: { ok: true } } as Parameters<typeof beforeSend>[0], {} as Parameters<typeof beforeSend>[1]),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,11 +83,8 @@ describe("beforeSendTransaction", () => {
|
|||||||
it("strips PII query params from request.url", () => {
|
it("strips PII query params from request.url", () => {
|
||||||
const event = {
|
const event = {
|
||||||
request: { url: "https://app/api/foo?token=secret&user=alice&email=a@b.c" },
|
request: { url: "https://app/api/foo?token=secret&user=alice&email=a@b.c" },
|
||||||
} as Parameters<typeof beforeSendTransaction>[0];
|
} as unknown as TxEvent;
|
||||||
const result = beforeSendTransaction(
|
const result = beforeSendTransaction(event, txHint) as Record<string, unknown>;
|
||||||
event,
|
|
||||||
{} as Parameters<typeof beforeSendTransaction>[1],
|
|
||||||
) as Record<string, unknown>;
|
|
||||||
const request = result["request"] as { url: string };
|
const request = result["request"] as { url: string };
|
||||||
expect(request.url).toContain("token=%5Bredacted%5D");
|
expect(request.url).toContain("token=%5Bredacted%5D");
|
||||||
expect(request.url).toContain("email=%5Bredacted%5D");
|
expect(request.url).toContain("email=%5Bredacted%5D");
|
||||||
@@ -103,25 +92,17 @@ describe("beforeSendTransaction", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("strips PII query params from event.transaction", () => {
|
it("strips PII query params from event.transaction", () => {
|
||||||
const event = { transaction: "/foo?token=x&id=y" } as Parameters<
|
const event = { transaction: "/foo?token=x&id=y" } as unknown as TxEvent;
|
||||||
typeof beforeSendTransaction
|
const result = beforeSendTransaction(event, txHint) as Record<string, unknown>;
|
||||||
>[0];
|
|
||||||
const result = beforeSendTransaction(
|
|
||||||
event,
|
|
||||||
{} as Parameters<typeof beforeSendTransaction>[1],
|
|
||||||
) as Record<string, unknown>;
|
|
||||||
expect(result["transaction"] as string).toContain("token=%5Bredacted%5D");
|
expect(result["transaction"] as string).toContain("token=%5Bredacted%5D");
|
||||||
expect(result["transaction"] as string).toContain("id=y");
|
expect(result["transaction"] as string).toContain("id=y");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches derived param names (accessToken, ApiSecret)", () => {
|
it("matches derived param names (accessToken, ApiSecret)", () => {
|
||||||
const event = { request: { url: "https://x/y?accessToken=t&ApiSecret=z&safe=1" } } as Parameters<
|
const event = {
|
||||||
typeof beforeSendTransaction
|
request: { url: "https://x/y?accessToken=t&ApiSecret=z&safe=1" },
|
||||||
>[0];
|
} as unknown as TxEvent;
|
||||||
const result = beforeSendTransaction(
|
const result = beforeSendTransaction(event, txHint) as Record<string, unknown>;
|
||||||
event,
|
|
||||||
{} as Parameters<typeof beforeSendTransaction>[1],
|
|
||||||
) as Record<string, unknown>;
|
|
||||||
const request = result["request"] as { url: string };
|
const request = result["request"] as { url: string };
|
||||||
expect(request.url).toContain("accessToken=%5Bredacted%5D");
|
expect(request.url).toContain("accessToken=%5Bredacted%5D");
|
||||||
expect(request.url).toContain("ApiSecret=%5Bredacted%5D");
|
expect(request.url).toContain("ApiSecret=%5Bredacted%5D");
|
||||||
@@ -129,11 +110,6 @@ describe("beforeSendTransaction", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns the event when no URL present", () => {
|
it("returns the event when no URL present", () => {
|
||||||
expect(
|
expect(beforeSendTransaction({} as TxEvent, txHint)).toBeTruthy();
|
||||||
beforeSendTransaction(
|
|
||||||
{} as Parameters<typeof beforeSendTransaction>[0],
|
|
||||||
{} as Parameters<typeof beforeSendTransaction>[1],
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// packages/core-shared/src/instrumentation/sentry/scrub.ts
|
// packages/core-shared/src/instrumentation/sentry/scrub.ts
|
||||||
import type { ErrorEvent, EventHint, TransactionEvent } from "@sentry/nextjs";
|
// Use structural types matching Sentry's beforeSend/beforeSendTransaction signatures
|
||||||
|
// to avoid importing @sentry/core types directly (they're not re-exported by @sentry/nextjs).
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IPV4_REGEX,
|
IPV4_REGEX,
|
||||||
IPV6_REGEX,
|
IPV6_REGEX,
|
||||||
@@ -9,6 +11,24 @@ import {
|
|||||||
queryParamContainsPii,
|
queryParamContainsPii,
|
||||||
} from "./pii-fields";
|
} from "./pii-fields";
|
||||||
|
|
||||||
|
// Minimal structural types matching Sentry's event shape used in scrubbers.
|
||||||
|
// Using index signatures broad enough to satisfy both ErrorEvent and TransactionEvent.
|
||||||
|
type SentryRequest = {
|
||||||
|
url?: string;
|
||||||
|
headers?: Record<string, string | undefined>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SentryEvent = {
|
||||||
|
extra?: Record<string, unknown> | null;
|
||||||
|
contexts?: Record<string, Record<string, unknown> | undefined>;
|
||||||
|
request?: SentryRequest;
|
||||||
|
transaction?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SentryEventHint = Record<string, unknown>;
|
||||||
|
|
||||||
function redactString(s: string): string {
|
function redactString(s: string): string {
|
||||||
// Create new regexes each call since regexes with /g are stateful
|
// Create new regexes each call since regexes with /g are stateful
|
||||||
const ipv4 = new RegExp(IPV4_REGEX.source, "g");
|
const ipv4 = new RegExp(IPV4_REGEX.source, "g");
|
||||||
@@ -37,8 +57,8 @@ function deepScrub(value: unknown, parentKey = ""): unknown {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beforeSend(event: ErrorEvent, _hint: EventHint): ErrorEvent | null {
|
export function beforeSend(event: SentryEvent, _hint: SentryEventHint): SentryEvent | null {
|
||||||
return deepScrub(event) as ErrorEvent;
|
return deepScrub(event) as SentryEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrubUrl(url: string): string {
|
function scrubUrl(url: string): string {
|
||||||
@@ -56,10 +76,10 @@ function scrubUrl(url: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function beforeSendTransaction(
|
export function beforeSendTransaction(
|
||||||
event: TransactionEvent,
|
event: SentryEvent,
|
||||||
_hint: EventHint,
|
_hint: SentryEventHint,
|
||||||
): TransactionEvent | null {
|
): SentryEvent | null {
|
||||||
const out: TransactionEvent = { ...event };
|
const out: SentryEvent = { ...event };
|
||||||
if (out.request?.url) {
|
if (out.request?.url) {
|
||||||
out.request = { ...out.request, url: scrubUrl(out.request.url) };
|
out.request = { ...out.request, url: scrubUrl(out.request.url) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe("SentryLogger", () => {
|
|||||||
const err = new Error("boom");
|
const err = new Error("boom");
|
||||||
logger.captureException(err, { tags: { feature: "blog" } });
|
logger.captureException(err, { tags: { feature: "blog" } });
|
||||||
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
|
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
|
||||||
expect((Sentry.captureException as ReturnType<typeof vi.fn>).mock.calls[0][0]).toBe(err);
|
expect((Sentry.captureException as ReturnType<typeof vi.fn>).mock.calls[0]![0]).toBe(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("captureException is a no-op when err already marked __sentryReported", () => {
|
it("captureException is a no-op when err already marked __sentryReported", () => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe("SentryTracer", () => {
|
|||||||
);
|
);
|
||||||
expect(result).toBe("value");
|
expect(result).toBe("value");
|
||||||
expect(Sentry.startSpan).toHaveBeenCalledTimes(1);
|
expect(Sentry.startSpan).toHaveBeenCalledTimes(1);
|
||||||
expect((Sentry.startSpan as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
expect((Sentry.startSpan as ReturnType<typeof vi.fn>).mock.calls[0]![0]).toMatchObject({
|
||||||
name: "blog.getArticles",
|
name: "blog.getArticles",
|
||||||
op: "use-case",
|
op: "use-case",
|
||||||
});
|
});
|
||||||
@@ -36,7 +36,7 @@ describe("SentryTracer", () => {
|
|||||||
async () => undefined,
|
async () => undefined,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
(Sentry.startSpan as ReturnType<typeof vi.fn>).mock.calls[0][0].attributes,
|
(Sentry.startSpan as ReturnType<typeof vi.fn>).mock.calls[0]![0].attributes,
|
||||||
).toEqual({
|
).toEqual({
|
||||||
collection: "articles",
|
collection: "articles",
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import type { ITracer, ISpan, SpanOpts } from "../tracer.interface";
|
|||||||
|
|
||||||
export class SentryTracer implements ITracer {
|
export class SentryTracer implements ITracer {
|
||||||
async startSpan<T>(opts: SpanOpts, fn: (span: ISpan) => Promise<T>): Promise<T> {
|
async startSpan<T>(opts: SpanOpts, fn: (span: ISpan) => Promise<T>): Promise<T> {
|
||||||
|
// Filter out null values — Sentry SpanAttributes doesn't allow null
|
||||||
|
const attributes = opts.attributes
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(opts.attributes).filter(([, v]) => v !== null),
|
||||||
|
) as Record<string, string | number | boolean>
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return Sentry.startSpan(
|
return Sentry.startSpan(
|
||||||
{
|
{
|
||||||
name: opts.name,
|
name: opts.name,
|
||||||
op: opts.op,
|
op: opts.op,
|
||||||
attributes: opts.attributes,
|
attributes,
|
||||||
},
|
},
|
||||||
async (sentrySpan) => {
|
async (sentrySpan) => {
|
||||||
const adapter: ISpan = {
|
const adapter: ISpan = {
|
||||||
|
|||||||
1560
pnpm-lock.yaml
generated
1560
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user