From 133585d536e2b1a7e8a7db4712e73b2c1a305aef Mon Sep 17 00:00:00 2001 From: Kuchizu Date: Wed, 1 Jul 2026 12:40:23 +0300 Subject: [PATCH 1/3] Trim event title to a maximum length in grouper --- workers/grouper/src/index.ts | 14 +++++++++++++- workers/grouper/tests/index.test.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/workers/grouper/src/index.ts b/workers/grouper/src/index.ts index 8203e8d3..a0837eff 100644 --- a/workers/grouper/src/index.ts +++ b/workers/grouper/src/index.ts @@ -59,10 +59,15 @@ const DB_DUPLICATE_KEY_ERROR = '11000'; const DAILY_METRICS_RETENTION_DAYS = 90; /** - * Maximum length for backtrace code line or title + * Maximum length for backtrace code line */ const MAX_CODE_LINE_LENGTH = 140; +/** + * Maximum length for event title + */ +const MAX_TITLE_LENGTH = 1000; + /** * Worker for handling Javascript events */ @@ -209,6 +214,13 @@ export default class GrouperWorker extends Worker { }; } + /** + * Trim title before hashing so hash and stored title stay consistent + */ + if (typeof task.payload.title === 'string') { + task.payload.title = rightTrim(task.payload.title, MAX_TITLE_LENGTH); + } + let uniqueEventHash = await session.measureStep('hash', () => this.getUniqueEventHash(task)); let existedEvent: GroupedEventDBScheme; let repetitionId = null; diff --git a/workers/grouper/tests/index.test.ts b/workers/grouper/tests/index.test.ts index c75351a9..18a7dd36 100644 --- a/workers/grouper/tests/index.test.ts +++ b/workers/grouper/tests/index.test.ts @@ -173,6 +173,20 @@ describe('GrouperWorker', () => { expect(await eventsCollection.find().count()).toBe(1); }); + test('Should trim event title to the maximum length before saving', async () => { + const longTitle = 'A'.repeat(5000); + + await worker.handle(generateTask({ title: longTitle })); + + const savedEvent = await eventsCollection.findOne({}); + + /** + * 1000 chars + ellipsis + */ + expect(savedEvent.payload.title.length).toBe(1001); + expect(savedEvent.payload.title.endsWith('…')).toBe(true); + }); + test('Should increment total events count on each processing', async () => { await worker.handle(generateTask()); await worker.handle(generateTask()); From b9fd948486e971a7a88bdc983f8457d1d2e51a02 Mon Sep 17 00:00:00 2001 From: Kuchizu Date: Wed, 1 Jul 2026 21:11:06 +0300 Subject: [PATCH 2/3] refactor(grouper): move title trimming into DataFilter --- workers/grouper/src/data-filter.ts | 20 ++++++++++++++++++++ workers/grouper/src/index.ts | 9 +-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/workers/grouper/src/data-filter.ts b/workers/grouper/src/data-filter.ts index 2345b8e5..d88b24e3 100644 --- a/workers/grouper/src/data-filter.ts +++ b/workers/grouper/src/data-filter.ts @@ -1,11 +1,17 @@ import type { EventAddons, EventData } from '@hawk.so/types'; import { unsafeFields } from '../../../lib/utils/unsafeFields'; +import { rightTrim } from '../../../lib/utils/string'; /** * Maximum depth for object traversal to prevent excessive memory allocations */ const MAX_TRAVERSAL_DEPTH = 20; +/** + * Maximum length for event title + */ +const MAX_TITLE_LENGTH = 1000; + /** * Recursively iterate through object and call function on each key * @@ -142,6 +148,20 @@ export default class DataFilter { }); } + /** + * Trim event title to the maximum allowed length. + * It mutates the original object. + * + * Should be called before hashing so the hash and the stored title stay consistent. + * + * @param event - event to process + */ + public trimEventTitle(event: EventData): void { + if (typeof event.title === 'string') { + event.title = rightTrim(event.title, MAX_TITLE_LENGTH); + } + } + /** * Recursively iterates object and applies filtering to its entries * diff --git a/workers/grouper/src/index.ts b/workers/grouper/src/index.ts index a0837eff..845c5590 100644 --- a/workers/grouper/src/index.ts +++ b/workers/grouper/src/index.ts @@ -63,11 +63,6 @@ const DAILY_METRICS_RETENTION_DAYS = 90; */ const MAX_CODE_LINE_LENGTH = 140; -/** - * Maximum length for event title - */ -const MAX_TITLE_LENGTH = 1000; - /** * Worker for handling Javascript events */ @@ -217,9 +212,7 @@ export default class GrouperWorker extends Worker { /** * Trim title before hashing so hash and stored title stay consistent */ - if (typeof task.payload.title === 'string') { - task.payload.title = rightTrim(task.payload.title, MAX_TITLE_LENGTH); - } + this.dataFilter.trimEventTitle(task.payload); let uniqueEventHash = await session.measureStep('hash', () => this.getUniqueEventHash(task)); let existedEvent: GroupedEventDBScheme; From 289bf6048511abd0a304346fd5d7189ddf996e7e Mon Sep 17 00:00:00 2001 From: Kuchizu Date: Wed, 8 Jul 2026 11:28:08 +0300 Subject: [PATCH 3/3] Trim grouper event titles --- workers/grouper/src/data-filter.ts | 8 ++++---- workers/grouper/src/index.ts | 13 ++++--------- workers/grouper/tests/index.test.ts | 4 ++-- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/workers/grouper/src/data-filter.ts b/workers/grouper/src/data-filter.ts index d88b24e3..06936883 100644 --- a/workers/grouper/src/data-filter.ts +++ b/workers/grouper/src/data-filter.ts @@ -8,9 +8,9 @@ import { rightTrim } from '../../../lib/utils/string'; const MAX_TRAVERSAL_DEPTH = 20; /** - * Maximum length for event title + * Maximum length for event title before appending ellipsis */ -const MAX_TITLE_LENGTH = 1000; +const MAX_TITLE_LENGTH = 400; /** * Recursively iterate through object and call function on each key @@ -141,6 +141,8 @@ export default class DataFilter { * @param event - event to process */ public processEvent(event: EventData): void { + this.trimEventTitle(event); + unsafeFields.forEach(field => { if (event[field]) { this.processField(event[field]); @@ -152,8 +154,6 @@ export default class DataFilter { * Trim event title to the maximum allowed length. * It mutates the original object. * - * Should be called before hashing so the hash and the stored title stay consistent. - * * @param event - event to process */ public trimEventTitle(event: EventData): void { diff --git a/workers/grouper/src/index.ts b/workers/grouper/src/index.ts index 845c5590..58115340 100644 --- a/workers/grouper/src/index.ts +++ b/workers/grouper/src/index.ts @@ -198,8 +198,6 @@ export default class GrouperWorker extends Worker { this.grouperMetrics.observePayloadSize(taskPayloadSize); this.memoryMonitor.logBeforeHandle(memoryBeforeHandle, handledTasksCount, taskPayloadSize, task.projectId); - this.logger.info(`[handle] project=${task.projectId} catcher=${task.catcherType} title="${task.payload.title}" payloadSize=${taskPayloadSize}b backtraceFrames=${task.payload.backtrace?.length ?? 0}`); - // FIX RELEASE TYPE // TODO: REMOVE AFTER 01.01.2026, after the most of the users update to new js catcher if (task.payload && task.payload.release !== undefined) { @@ -210,9 +208,11 @@ export default class GrouperWorker extends Worker { } /** - * Trim title before hashing so hash and stored title stay consistent + * Filter event data before logging and hashing so logs, hash and stored event stay consistent. */ - this.dataFilter.trimEventTitle(task.payload); + this.dataFilter.processEvent(task.payload); + + this.logger.info(`[handle] project=${task.projectId} catcher=${task.catcherType} title="${task.payload.title}" payloadSize=${taskPayloadSize}b backtraceFrames=${task.payload.backtrace?.length ?? 0}`); let uniqueEventHash = await session.measureStep('hash', () => this.getUniqueEventHash(task)); let existedEvent: GroupedEventDBScheme; @@ -224,11 +224,6 @@ export default class GrouperWorker extends Worker { * Trim source code lines to prevent memory leaks */ this.trimSourceCodeLines(task.payload); - - /** - * Filter sensitive information - */ - this.dataFilter.processEvent(task.payload); }); /** diff --git a/workers/grouper/tests/index.test.ts b/workers/grouper/tests/index.test.ts index 18a7dd36..e1761bc5 100644 --- a/workers/grouper/tests/index.test.ts +++ b/workers/grouper/tests/index.test.ts @@ -181,9 +181,9 @@ describe('GrouperWorker', () => { const savedEvent = await eventsCollection.findOne({}); /** - * 1000 chars + ellipsis + * 400 chars + ellipsis */ - expect(savedEvent.payload.title.length).toBe(1001); + expect(savedEvent.payload.title.length).toBe(401); expect(savedEvent.payload.title.endsWith('…')).toBe(true); });