test(sentry): extract pure scrub module + add unit tests — privacy regression guard

Extracts scrubUrl/scrubString/scrubObject into src/lib/scrub.ts (no RN deps)
so they can be tested with node --test without native module issues.

Adds src/lib/scrub.test.ts with 13 test cases covering:
- basic-auth credential stripping
- query-param secret redaction (token, api_key, password, access_token)
- clean URL passthrough
- mixed-param URL (only secrets redacted)
- embedded URL in error message strings
- nested object recursive scrubbing
- non-string value preservation

Closes #40

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dennis V
2026-06-23 16:31:15 +00:00
parent ca57bab72d
commit 6fee057355
3 changed files with 134 additions and 24 deletions

View File

@@ -13,6 +13,7 @@ import * as Sentry from "@sentry/react-native"
import appJson from "../../app.json"
import { log } from "./logbuffer"
import type { DiagnosticReport } from "./diagnostics"
import { scrubUrl, scrubString, scrubObject } from "./scrub"
const DSN = process.env.EXPO_PUBLIC_SENTRY_DSN
const APP_VERSION = (appJson as { expo?: { version?: string } }).expo?.version ?? "unknown"
@@ -124,31 +125,9 @@ function toError(value: unknown): Error {
}
}
// --- Scrubbing -----------------------------------------------------------
// --- Scrubbing (pure functions live in ./scrub for testability) ----------
// Strip basic-auth credentials and any `?token=` style query secrets so URLs
// can be safely sent or logged.
export function scrubUrl(url: string): string {
return url
.replace(/\/\/[^@/]+@/, "//<redacted>@")
.replace(/([?&](?:token|access_token|api_key|key|password|pwd|auth)=)[^&#]*/gi, "$1<redacted>")
}
function scrubString(s: string): string {
// Catch any embedded URL inside a free-text string (error messages often
// contain them, e.g. "fetch failed: https://user:pw@host/...").
return s.replace(/https?:\/\/\S+/g, (m) => scrubUrl(m))
}
function scrubObject(obj: Record<string, unknown>): Record<string, unknown> {
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(obj)) {
if (typeof v === "string") out[k] = scrubString(v)
else if (v && typeof v === "object" && !Array.isArray(v)) out[k] = scrubObject(v as Record<string, unknown>)
else out[k] = v
}
return out
}
export { scrubUrl } from "./scrub"
function scrubEvent<T extends Sentry.Event>(event: T): T {
if (event.request?.url) event.request.url = scrubUrl(event.request.url)