Skip to content

Commit 417688e

Browse files
fix: improve default fingerprint aggregation to reduce alert noise (#13)
Normalize titles with the same rules used for messages so dynamic values (UUIDs, hex addresses, numbers) don't create separate fingerprints. Reduce default stackDepth from 3 to 1 so the same error originating from one throw site but reached via different callers groups into a single aggregation stream. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 44e476a commit 417688e

4 files changed

Lines changed: 37 additions & 3 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@iqai/alert-logger": patch
3+
---
4+
5+
fix: improve default fingerprint aggregation to reduce alert noise
6+
7+
- Normalize titles with the same rules used for messages (UUIDs, hex addresses, timestamps, numbers) so dynamic values in titles don't split fingerprints.
8+
- Reduce default `stackDepth` from 3 to 1 so the same error from different callers groups into a single aggregation stream. Users can restore the previous behavior with `fingerprint: { stackDepth: 3 }`.

src/core/fingerprinter.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,25 @@ describe('fingerprint', () => {
187187
expect(a).not.toBe(b)
188188
})
189189

190+
it('groups same error from different callers with default stackDepth', () => {
191+
const stackA = [
192+
'Error: test',
193+
' at throwSite (/app/src/shared.ts:10:5)',
194+
' at callerA (/app/src/a.ts:20:3)',
195+
' at handlerA (/app/src/routes-a.ts:30:1)',
196+
].join('\n')
197+
const stackB = [
198+
'Error: test',
199+
' at throwSite (/app/src/shared.ts:10:5)',
200+
' at callerB (/app/src/b.ts:40:3)',
201+
' at handlerB (/app/src/routes-b.ts:50:1)',
202+
].join('\n')
203+
204+
const a = fingerprint('E', 'test', makeErrorWithStack(stackA), cfg)
205+
const b = fingerprint('E', 'test', makeErrorWithStack(stackB), cfg)
206+
expect(a).toBe(b)
207+
})
208+
190209
it('includes file, line, and column in stack key', () => {
191210
const stackA = ['Error: test', ' at fn (/app/src/index.ts:10:5)'].join('\n')
192211
const stackB = ['Error: test', ' at fn (/app/src/index.ts:10:99)'].join('\n')
@@ -205,10 +224,16 @@ describe('fingerprint', () => {
205224
expect(hash).toMatch(/^[0-9a-f]{32}$/)
206225
})
207226

208-
it('uses title as errorName when error is undefined', () => {
227+
it('uses normalized title as errorName when error is undefined', () => {
209228
const a = fingerprint('TitleA', 'same msg', undefined, cfg)
210229
const b = fingerprint('TitleB', 'same msg', undefined, cfg)
211230
expect(a).not.toBe(b)
212231
})
232+
233+
it('normalizes dynamic parts in title when error is undefined', () => {
234+
const a = fingerprint('GET /users/0xABC123DEF/positions', 'table missing', undefined, cfg)
235+
const b = fingerprint('GET /users/0xDEF456ABC/positions', 'table missing', undefined, cfg)
236+
expect(a).toBe(b)
237+
})
213238
})
214239
})

src/core/fingerprinter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ export function fingerprint(
6969
return md5(dedupKey)
7070
}
7171

72+
const normalizedTitle = normalizeMessage(title, config.normalizers)
7273
const normalizedMessage = normalizeMessage(message, config.normalizers)
7374
const stackKey = extractStackKey(error, config.stackDepth)
74-
const errorName = error?.name ?? title
75+
const errorName = error?.name ?? normalizedTitle
7576

7677
return md5(errorName + normalizedMessage + stackKey)
7778
}

src/core/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const DEFAULT_QUEUE: QueueConfig = {
139139
}
140140

141141
export const DEFAULT_FINGERPRINT: FingerprintConfig = {
142-
stackDepth: 3,
142+
stackDepth: 1,
143143
normalizers: [],
144144
}
145145

0 commit comments

Comments
 (0)