Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/normalizer-order.md
Original file line number Diff line number Diff line change
@@ -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: "<num>" }` 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.
48 changes: 40 additions & 8 deletions src/core/fingerprinter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<order>' }],
Expand All @@ -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 "<uuid>". Built-ins now run first, so structural
// identifiers survive.
const custom: FingerprintConfig = {
stackDepth: 3,
normalizers: [{ pattern: /ID:\d+/g, replacement: '<id>' }],
normalizers: [{ pattern: /\d+/g, replacement: '<num>' }],
}
// The user normalizer matches "ID:42" before the builtin number normalizer
// could turn "42" into "<num>"
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: '<num>' }],
}
const a = fingerprint(
'E',
'safeTxHash=0x9db6e277fdea4e17ef1597e358d7bd893d257cf62378180a9d279fdeb1d0ab58',
undefined,
custom,
)
const b = fingerprint(
'E',
'safeTxHash=0xf51b9f89c5919f9efffd9eb2450251bf668ec8410aa5c599c267027fc92b8466',
undefined,
custom,
)
expect(a).toBe(b)
})
})

Expand Down
11 changes: 7 additions & 4 deletions src/core/fingerprinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading