diff --git a/src/lib/support/__tests__/support-report-redaction.test.ts b/src/lib/support/__tests__/support-report-redaction.test.ts index f42c18c92..bf9613186 100644 --- a/src/lib/support/__tests__/support-report-redaction.test.ts +++ b/src/lib/support/__tests__/support-report-redaction.test.ts @@ -21,11 +21,13 @@ import { type SupportReportFacts, } from "../report"; import { + HOST_TLDS, redactSupportText, SUPPORT_REDACTION_STEPS, type SupportRedactionContext, } from "../redact"; import type { WideEvent } from "@/lib/logging/types"; +import { locales, type Locale } from "@/lib/i18n/config"; const ACCOUNT_EMAIL = "anna.schmidt@example.org"; const ACCOUNT_ID = "cmq7x0abcdefghijklmnopqrs"; @@ -366,3 +368,64 @@ describe("error classes never carry the message", () => { expect(JSON.stringify(top)).not.toContain(SEEDS.ids); }); }); + +describe("host names under every app language's country codes", () => { + // Where each shipped language is at home. A new locale fails here until its + // countries are listed, and a listed country fails until HOST_TLDS has it. + const HOST_TLD_LOCALE_REGIONS: Record = { + de: ["de", "at", "ch", "li", "lu", "be"], + en: ["uk", "ie", "us", "ca", "au", "nz"], + fr: ["fr", "be", "ch", "lu", "ca"], + es: ["es", "mx", "ar"], + it: ["it", "ch"], + pl: ["pl"], + ko: ["kr"], + nl: ["nl", "be"], + "pt-BR": ["br", "pt", "ao", "mz"], + }; + + it("covers every shipped locale", () => { + expect(Object.keys(HOST_TLD_LOCALE_REGIONS).sort()).toEqual( + [...locales].sort(), + ); + }); + + it.each(Object.entries(HOST_TLD_LOCALE_REGIONS))( + "redacts a host under each of %s's country codes", + (_locale, tlds) => { + for (const tld of tlds) { + expect(HOST_TLDS, tld).toContain(tld); + const text = redactSupportText(`reached nas.familie.${tld} late`, { + knownValues: [], + }); + expect(text, tld).toBe("reached [host] late"); + } + }, + ); + + it("redacts Brazilian and Portuguese hosts, with a port or a second-level registry", () => { + const cases = [ + "saude.minhacasa.br:3000", + "vida.example.com.br", + "healthlog.casa.pt", + "nas.lan.com.pt:8443", + ]; + for (const host of cases) { + const text = redactSupportText(`unexpected_silence ${host}`, { + knownValues: [], + }); + expect(text, host).not.toMatch(/minhacasa|example|casa|lan\.com/); + expect(text).toContain("[host]"); + } + }); + + it("still leaves report prose and action names alone", () => { + const text = redactSupportText( + "- Actions: mental-health.create, coach.budget.exceeded, measurement.batch.ingest\n- Node: v22.11.0", + { knownValues: [] }, + ); + expect(text).toContain("mental-health.create"); + expect(text).toContain("coach.budget.exceeded"); + expect(text).toContain("measurement.batch.ingest"); + }); +}); diff --git a/src/lib/support/redact.ts b/src/lib/support/redact.ts index 7ab5359ed..0ba0ccf3b 100644 --- a/src/lib/support/redact.ts +++ b/src/lib/support/redact.ts @@ -39,8 +39,16 @@ function escapeRegExp(value: string): string { */ const KNOWN_VALUE_MIN_LENGTH = 5; -/** Top-level domains a host name in a self-hosted setup ends in. */ -const HOST_TLDS = [ +/** + * Top-level domains a host name in a self-hosted setup ends in: the generic + * ones, the home-network suffixes, and the country codes of every place the + * app's languages are at home (`HOST_TLD_LOCALE_REGIONS` in the test pins + * that list against `locales`), plus the neighbours a self-hoster there is + * as likely to register under. A second-level registry (`com.br`, `co.uk`) + * needs nothing extra: the label before it is matched as a subdomain. + */ +export const HOST_TLDS = [ + // Generic. "com", "net", "org", @@ -50,18 +58,44 @@ const HOST_TLDS = [ "cloud", "info", "me", + "co", + "xyz", + "online", + "site", + "eu", + // Country codes. "de", "at", "ch", - "eu", - "uk", + "li", + "lu", + "be", + "nl", "fr", "es", + "pt", + "br", + "ao", + "mz", "it", "pl", - "nl", + "cz", + "sk", "kr", + "jp", + "uk", + "ie", "us", + "ca", + "au", + "nz", + "mx", + "ar", + "se", + "dk", + "no", + "fi", + // Home-network and private suffixes. "local", "lan", "home", @@ -69,7 +103,9 @@ const HOST_TLDS = [ "arpa", "localdomain", "ts", -].join("|"); +]; + +const HOST_TLD_PATTERN = HOST_TLDS.join("|"); export const SUPPORT_REDACTION_STEPS: readonly SupportRedactionStep[] = [ { @@ -132,7 +168,7 @@ export const SUPPORT_REDACTION_STEPS: readonly SupportRedactionStep[] = [ apply: (text) => text.replace( new RegExp( - `\\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+(?:${HOST_TLDS})\\b`, + `\\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+(?:${HOST_TLD_PATTERN})\\b`, "gi", ), "[host]",