From c042b4def81ee05a5c578db85d1f7a01c65b694b Mon Sep 17 00:00:00 2001 From: Srujan Gurram Date: Wed, 22 Apr 2026 19:20:11 +0530 Subject: [PATCH] fix(fingerprinter): run built-in normalizers before user rules User-defined normalizers ran first, so a broad rule like `/\d+/g` would strip digits out of UUIDs and hex addresses before UUID_RE / HEX_RE could match. Every trade ID and tx hash then produced a distinct fingerprint, which turned each occurrence into a fresh onset and suppression never kicked in. Built-ins now collapse structural identifiers first, and user rules compose on top of the normalized output. Adds regression tests covering UUID and hex survival when a `\d+` user rule is configured. --- .changeset/normalizer-order.md | 15 +++++++++++ src/core/fingerprinter.test.ts | 48 ++++++++++++++++++++++++++++------ src/core/fingerprinter.ts | 11 +++++--- 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 .changeset/normalizer-order.md diff --git a/.changeset/normalizer-order.md b/.changeset/normalizer-order.md new file mode 100644 index 0000000..afa2062 --- /dev/null +++ b/.changeset/normalizer-order.md @@ -0,0 +1,15 @@ +--- +"@iqai/alert-logger": patch +--- + +fix(fingerprinter): run built-in normalizers before user-defined ones + +User-defined normalizers previously ran before the built-in ones, so a +broad rule like `{ pattern: /\d+/g, replacement: "" }` would strip +digits out of UUIDs and hex addresses before `UUID_RE` and `HEX_RE` had a +chance to match. Every trade ID or transaction hash then produced a +distinct fingerprint, which made the aggregator treat each occurrence as +a fresh onset and suppression never kicked in. + +Built-ins now collapse structural identifiers first, and user rules +compose on top of the normalized output. diff --git a/src/core/fingerprinter.test.ts b/src/core/fingerprinter.test.ts index 8e1ef10..c11453a 100644 --- a/src/core/fingerprinter.test.ts +++ b/src/core/fingerprinter.test.ts @@ -118,7 +118,7 @@ describe('fingerprint', () => { // ── Custom normalizers ────────────────────────────────────────────── describe('custom normalizers', () => { - it('applies user-defined normalizers before builtins', () => { + it('applies user-defined normalizers on top of builtins', () => { const custom: FingerprintConfig = { stackDepth: 3, normalizers: [{ pattern: /order-\w+/g, replacement: '' }], @@ -128,16 +128,48 @@ describe('fingerprint', () => { expect(a).toBe(b) }) - it('user normalizers run before builtins so they can match raw text', () => { + it('does not let broad user rules break built-in UUID normalization', () => { + // A user-supplied `/\d+/g` rule used to run before built-ins and strip + // digits out of UUIDs, leaving distinct strings where a UUID should + // have collapsed to "". Built-ins now run first, so structural + // identifiers survive. const custom: FingerprintConfig = { stackDepth: 3, - normalizers: [{ pattern: /ID:\d+/g, replacement: '' }], + normalizers: [{ pattern: /\d+/g, replacement: '' }], } - // The user normalizer matches "ID:42" before the builtin number normalizer - // could turn "42" into "" - const hash = fingerprint('E', 'lookup ID:42', undefined, custom) - const hashWithDifferentId = fingerprint('E', 'lookup ID:99', undefined, custom) - expect(hash).toBe(hashWithDifferentId) + const a = fingerprint( + 'E', + 'Trade a51c80e4-3307-4d5f-a035-03c8fd1f767d permanently failed', + undefined, + custom, + ) + const b = fingerprint( + 'E', + 'Trade 1bbc368f-a1fa-44af-9d04-ffaa804a30b1 permanently failed', + undefined, + custom, + ) + expect(a).toBe(b) + }) + + it('does not let broad user rules break built-in hex normalization', () => { + const custom: FingerprintConfig = { + stackDepth: 3, + normalizers: [{ pattern: /\d+/g, replacement: '' }], + } + const a = fingerprint( + 'E', + 'safeTxHash=0x9db6e277fdea4e17ef1597e358d7bd893d257cf62378180a9d279fdeb1d0ab58', + undefined, + custom, + ) + const b = fingerprint( + 'E', + 'safeTxHash=0xf51b9f89c5919f9efffd9eb2450251bf668ec8410aa5c599c267027fc92b8466', + undefined, + custom, + ) + expect(a).toBe(b) }) }) diff --git a/src/core/fingerprinter.ts b/src/core/fingerprinter.ts index dc9eeea..e8c0a07 100644 --- a/src/core/fingerprinter.ts +++ b/src/core/fingerprinter.ts @@ -16,13 +16,16 @@ const BUILTIN_NORMALIZERS: NormalizerRule[] = [ function normalizeMessage(message: string, userNormalizers: NormalizerRule[]): string { let result = message - // Apply user-defined normalizers first - for (const rule of userNormalizers) { + // Apply built-in normalizers first so structural identifiers (UUIDs, hex + // addresses, timestamps, numbers) are collapsed before user rules run. + // Running user rules first lets a broad pattern like `/\d+/g` strip digits + // out of UUIDs/hex, which prevents the structural regexes from matching + // and turns every ID into its own fingerprint. + for (const rule of BUILTIN_NORMALIZERS) { result = result.replace(rule.pattern, rule.replacement) } - // Then apply built-in normalizers - for (const rule of BUILTIN_NORMALIZERS) { + for (const rule of userNormalizers) { result = result.replace(rule.pattern, rule.replacement) }