diff --git a/.changeset/fingerprint-aggregation.md b/.changeset/fingerprint-aggregation.md new file mode 100644 index 0000000..69e9c17 --- /dev/null +++ b/.changeset/fingerprint-aggregation.md @@ -0,0 +1,8 @@ +--- +"@iqai/alert-logger": patch +--- + +fix: improve default fingerprint aggregation to reduce alert noise + +- Normalize titles with the same rules used for messages (UUIDs, hex addresses, timestamps, numbers) so dynamic values in titles don't split fingerprints. +- 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 }`. diff --git a/src/core/fingerprinter.test.ts b/src/core/fingerprinter.test.ts index a8266ec..688bb85 100644 --- a/src/core/fingerprinter.test.ts +++ b/src/core/fingerprinter.test.ts @@ -187,6 +187,25 @@ describe('fingerprint', () => { expect(a).not.toBe(b) }) + it('groups same error from different callers with default stackDepth', () => { + const stackA = [ + 'Error: test', + ' at throwSite (/app/src/shared.ts:10:5)', + ' at callerA (/app/src/a.ts:20:3)', + ' at handlerA (/app/src/routes-a.ts:30:1)', + ].join('\n') + const stackB = [ + 'Error: test', + ' at throwSite (/app/src/shared.ts:10:5)', + ' at callerB (/app/src/b.ts:40:3)', + ' at handlerB (/app/src/routes-b.ts:50:1)', + ].join('\n') + + const a = fingerprint('E', 'test', makeErrorWithStack(stackA), cfg) + const b = fingerprint('E', 'test', makeErrorWithStack(stackB), cfg) + expect(a).toBe(b) + }) + it('includes file, line, and column in stack key', () => { const stackA = ['Error: test', ' at fn (/app/src/index.ts:10:5)'].join('\n') const stackB = ['Error: test', ' at fn (/app/src/index.ts:10:99)'].join('\n') @@ -205,10 +224,16 @@ describe('fingerprint', () => { expect(hash).toMatch(/^[0-9a-f]{32}$/) }) - it('uses title as errorName when error is undefined', () => { + it('uses normalized title as errorName when error is undefined', () => { const a = fingerprint('TitleA', 'same msg', undefined, cfg) const b = fingerprint('TitleB', 'same msg', undefined, cfg) expect(a).not.toBe(b) }) + + it('normalizes dynamic parts in title when error is undefined', () => { + const a = fingerprint('GET /users/0xABC123DEF/positions', 'table missing', undefined, cfg) + const b = fingerprint('GET /users/0xDEF456ABC/positions', 'table missing', undefined, cfg) + expect(a).toBe(b) + }) }) }) diff --git a/src/core/fingerprinter.ts b/src/core/fingerprinter.ts index aec4bf1..b90a100 100644 --- a/src/core/fingerprinter.ts +++ b/src/core/fingerprinter.ts @@ -69,9 +69,10 @@ export function fingerprint( return md5(dedupKey) } + const normalizedTitle = normalizeMessage(title, config.normalizers) const normalizedMessage = normalizeMessage(message, config.normalizers) const stackKey = extractStackKey(error, config.stackDepth) - const errorName = error?.name ?? title + const errorName = error?.name ?? normalizedTitle return md5(errorName + normalizedMessage + stackKey) } diff --git a/src/core/types.ts b/src/core/types.ts index a0df041..b3fceeb 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -139,7 +139,7 @@ export const DEFAULT_QUEUE: QueueConfig = { } export const DEFAULT_FINGERPRINT: FingerprintConfig = { - stackDepth: 3, + stackDepth: 1, normalizers: [], }