Skip to content

Commit 3239f66

Browse files
fix(fingerprinter): run built-in normalizers before user rules (#20)
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.
1 parent 942e36f commit 3239f66

3 files changed

Lines changed: 62 additions & 12 deletions

File tree

.changeset/normalizer-order.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@iqai/alert-logger": patch
3+
---
4+
5+
fix(fingerprinter): run built-in normalizers before user-defined ones
6+
7+
User-defined normalizers previously ran before the built-in ones, so a
8+
broad rule like `{ pattern: /\d+/g, replacement: "<num>" }` would strip
9+
digits out of UUIDs and hex addresses before `UUID_RE` and `HEX_RE` had a
10+
chance to match. Every trade ID or transaction hash then produced a
11+
distinct fingerprint, which made the aggregator treat each occurrence as
12+
a fresh onset and suppression never kicked in.
13+
14+
Built-ins now collapse structural identifiers first, and user rules
15+
compose on top of the normalized output.

src/core/fingerprinter.test.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ describe('fingerprint', () => {
118118
// ── Custom normalizers ──────────────────────────────────────────────
119119

120120
describe('custom normalizers', () => {
121-
it('applies user-defined normalizers before builtins', () => {
121+
it('applies user-defined normalizers on top of builtins', () => {
122122
const custom: FingerprintConfig = {
123123
stackDepth: 3,
124124
normalizers: [{ pattern: /order-\w+/g, replacement: '<order>' }],
@@ -128,16 +128,48 @@ describe('fingerprint', () => {
128128
expect(a).toBe(b)
129129
})
130130

131-
it('user normalizers run before builtins so they can match raw text', () => {
131+
it('does not let broad user rules break built-in UUID normalization', () => {
132+
// A user-supplied `/\d+/g` rule used to run before built-ins and strip
133+
// digits out of UUIDs, leaving distinct strings where a UUID should
134+
// have collapsed to "<uuid>". Built-ins now run first, so structural
135+
// identifiers survive.
132136
const custom: FingerprintConfig = {
133137
stackDepth: 3,
134-
normalizers: [{ pattern: /ID:\d+/g, replacement: '<id>' }],
138+
normalizers: [{ pattern: /\d+/g, replacement: '<num>' }],
135139
}
136-
// The user normalizer matches "ID:42" before the builtin number normalizer
137-
// could turn "42" into "<num>"
138-
const hash = fingerprint('E', 'lookup ID:42', undefined, custom)
139-
const hashWithDifferentId = fingerprint('E', 'lookup ID:99', undefined, custom)
140-
expect(hash).toBe(hashWithDifferentId)
140+
const a = fingerprint(
141+
'E',
142+
'Trade a51c80e4-3307-4d5f-a035-03c8fd1f767d permanently failed',
143+
undefined,
144+
custom,
145+
)
146+
const b = fingerprint(
147+
'E',
148+
'Trade 1bbc368f-a1fa-44af-9d04-ffaa804a30b1 permanently failed',
149+
undefined,
150+
custom,
151+
)
152+
expect(a).toBe(b)
153+
})
154+
155+
it('does not let broad user rules break built-in hex normalization', () => {
156+
const custom: FingerprintConfig = {
157+
stackDepth: 3,
158+
normalizers: [{ pattern: /\d+/g, replacement: '<num>' }],
159+
}
160+
const a = fingerprint(
161+
'E',
162+
'safeTxHash=0x9db6e277fdea4e17ef1597e358d7bd893d257cf62378180a9d279fdeb1d0ab58',
163+
undefined,
164+
custom,
165+
)
166+
const b = fingerprint(
167+
'E',
168+
'safeTxHash=0xf51b9f89c5919f9efffd9eb2450251bf668ec8410aa5c599c267027fc92b8466',
169+
undefined,
170+
custom,
171+
)
172+
expect(a).toBe(b)
141173
})
142174
})
143175

src/core/fingerprinter.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ const BUILTIN_NORMALIZERS: NormalizerRule[] = [
1616
function normalizeMessage(message: string, userNormalizers: NormalizerRule[]): string {
1717
let result = message
1818

19-
// Apply user-defined normalizers first
20-
for (const rule of userNormalizers) {
19+
// Apply built-in normalizers first so structural identifiers (UUIDs, hex
20+
// addresses, timestamps, numbers) are collapsed before user rules run.
21+
// Running user rules first lets a broad pattern like `/\d+/g` strip digits
22+
// out of UUIDs/hex, which prevents the structural regexes from matching
23+
// and turns every ID into its own fingerprint.
24+
for (const rule of BUILTIN_NORMALIZERS) {
2125
result = result.replace(rule.pattern, rule.replacement)
2226
}
2327

24-
// Then apply built-in normalizers
25-
for (const rule of BUILTIN_NORMALIZERS) {
28+
for (const rule of userNormalizers) {
2629
result = result.replace(rule.pattern, rule.replacement)
2730
}
2831

0 commit comments

Comments
 (0)