mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
This commit is contained in:
@@ -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),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
try {
|
||||
@@ -115,6 +128,7 @@ export async function reportError(err: unknown, ctx: ReportContext): Promise<voi
|
||||
scope.setTag("error_class", cls);
|
||||
const code = extractErrorCode(err);
|
||||
if (code) scope.setTag("error_code", code);
|
||||
if (ctx.inputFormat) scope.setTag("input_format", ctx.inputFormat);
|
||||
if (ctx.toolId) scope.setTag("tool_id", ctx.toolId);
|
||||
if (ctx.pool) scope.setTag("pool", ctx.pool);
|
||||
if (ctx.route) scope.setTag("route", ctx.route);
|
||||
|
||||
@@ -22,6 +22,7 @@ const TAG_ALLOWLIST = new Set([
|
||||
"deploy_mode",
|
||||
"subsystem",
|
||||
"status_code",
|
||||
"input_format",
|
||||
]);
|
||||
|
||||
const URL_RE = /https?:\/\/[^\s"')]+/g;
|
||||
|
||||
@@ -4,9 +4,23 @@ import {
|
||||
classifyError,
|
||||
errorSignature,
|
||||
resetThrottleForTests,
|
||||
safeFormatTag,
|
||||
shouldReport,
|
||||
} from "../../../apps/api/src/lib/error-report.js";
|
||||
|
||||
describe("safeFormatTag", () => {
|
||||
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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user