From 281b4a06e39756a3a2b981e93d033454eb494c8c Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Thu, 16 Jul 2026 19:27:16 +0800 Subject: [PATCH] feat(telemetry): add a safe input_format tag to worker error reports (#541) Every worker tool error now carries an input_format tag (file extension only, never the filename) for triage, derived once at the worker error path and added to the scrubber allowlist. --- apps/api/src/jobs/worker.ts | 3 ++- apps/api/src/lib/error-report.ts | 14 ++++++++++++++ apps/api/src/lib/sentry-scrub.ts | 1 + tests/unit/api/error-report.test.ts | 14 ++++++++++++++ tests/unit/api/sentry-scrub.test.ts | 3 ++- 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apps/api/src/jobs/worker.ts b/apps/api/src/jobs/worker.ts index 841a9da4..bc1c531d 100644 --- a/apps/api/src/jobs/worker.ts +++ b/apps/api/src/jobs/worker.ts @@ -39,7 +39,7 @@ import { db, schema } from "../db/index.js"; import { trackEvent } from "../lib/analytics.js"; import { analyticsEnabled } from "../lib/analytics-gate.js"; import { resolveConcurrency } from "../lib/env.js"; -import { reportError } from "../lib/error-report.js"; +import { reportError, safeFormatTag } from "../lib/error-report.js"; import { friendlyError } from "../lib/errors.js"; import { logger } from "../lib/logger.js"; import { jobDuration, jobsTotal } from "../lib/metrics.js"; @@ -1047,6 +1047,7 @@ export function startWorkers(): void { source: "worker", pool, toolId: (job.data as ToolJobData | undefined)?.toolId, + inputFormat: safeFormatTag((job.data as ToolJobData | undefined)?.filename), }); }); diff --git a/apps/api/src/lib/error-report.ts b/apps/api/src/lib/error-report.ts index f5a25e18..05d031d2 100644 --- a/apps/api/src/lib/error-report.ts +++ b/apps/api/src/lib/error-report.ts @@ -35,6 +35,8 @@ export interface ReportContext { method?: string; statusCode?: number; subsystem?: string; + /** Safe input format (file extension) for triage; never the filename. */ + inputFormat?: string; } export function classifyError(err: unknown, source?: ReportContext["source"]): ErrorClass { @@ -99,6 +101,17 @@ export function errorSignature(err: unknown): string { return `${name}:${code}:${frame}`; } +/** + * The lowercase file extension as a safe, non-PII tag value, or undefined. The + * filename itself can carry user data, but a short alphanumeric extension (jpg, + * png, pdf, mp4) is a safe triage signal for which input format failed. + */ +export function safeFormatTag(filename?: string): string | undefined { + if (!filename) return undefined; + const m = filename.match(/\.([a-z0-9]{1,8})$/i); + return m ? m[1].toLowerCase() : undefined; +} + /** Fire-and-forget; never throws, never blocks. */ export async function reportError(err: unknown, ctx: ReportContext): Promise { try { @@ -115,6 +128,7 @@ export async function reportError(err: unknown, ctx: ReportContext): Promise { + it("returns the lowercase extension as a safe, non-PII tag", () => { + expect(safeFormatTag("photo.JPEG")).toBe("jpeg"); + expect(safeFormatTag("doc.pdf")).toBe("pdf"); + expect(safeFormatTag("clip.final.mp4")).toBe("mp4"); + }); + it("returns undefined when there is no plausible extension", () => { + expect(safeFormatTag(undefined)).toBeUndefined(); + expect(safeFormatTag("noext")).toBeUndefined(); + expect(safeFormatTag("weird.name-with-dashes")).toBeUndefined(); + }); +}); + describe("classifyError", () => { it("expected: tool input, aborts, worker cancel/timeout strings, zod, upload validation", () => { expect(classifyError(new ToolInputError("bad csv"))).toBe("expected"); diff --git a/tests/unit/api/sentry-scrub.test.ts b/tests/unit/api/sentry-scrub.test.ts index 0e4ebd07..5602fe8c 100644 --- a/tests/unit/api/sentry-scrub.test.ts +++ b/tests/unit/api/sentry-scrub.test.ts @@ -14,7 +14,7 @@ const evt = (over: AnyEvent = {}): AnyEvent => ({ runtime: { name: "node", version: "22.1.0" }, device: { hostname: "leak" }, }, - tags: { tool_id: "resize", secret_tag: "leak" }, + tags: { tool_id: "resize", input_format: "webp", secret_tag: "leak" }, exception: { values: [ { @@ -99,6 +99,7 @@ describe("buildBeforeSend (api)", () => { runtime: { name: "node", version: "22.1.0" }, }); expect(out.tags.tool_id).toBe("resize"); + expect(out.tags.input_format).toBe("webp"); expect(out.tags.secret_tag).toBeUndefined(); }); it("drops contexts entirely when nothing allowlisted survives", () => {