From d28c65223d32f4073741b857eeaa198e5b41ae9b Mon Sep 17 00:00:00 2001 From: Reversean Date: Wed, 24 Jun 2026 19:43:28 +0300 Subject: [PATCH] feat(grouper): propagate occurrence count to totalCount and repetitions - event-worker / javascript-worker: forward CatcherMessage.count to the grouper task instead of dropping it - grouper: use count instead of a hardcoded 1 when - setting totalCount on first occurrence - incrementing totalCount on repetitions - incrementing the daily events aggregate - grouper: record count on the repetition document itself, omitted when absent or 1 (single occurrence), set when a message represents more than one real occurrence --- lib/event-worker.ts | 1 + lib/types/hawk-types-augmentation.d.ts | 13 +++++++ workers/grouper/src/index.ts | 29 +++++++++++---- workers/grouper/tests/index.test.ts | 51 +++++++++++++++++++++++++- workers/javascript/src/index.ts | 1 + 5 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 lib/types/hawk-types-augmentation.d.ts diff --git a/lib/event-worker.ts b/lib/event-worker.ts index a031ed9a..fb5d456d 100644 --- a/lib/event-worker.ts +++ b/lib/event-worker.ts @@ -27,6 +27,7 @@ export abstract class EventWorker extends Worker { catcherType: this.type as CatcherMessageType, payload: task.payload as CatcherMessagePayload, timestamp: task.timestamp, + count: task.count, } as GroupWorkerTask); } diff --git a/lib/types/hawk-types-augmentation.d.ts b/lib/types/hawk-types-augmentation.d.ts new file mode 100644 index 00000000..90a09918 --- /dev/null +++ b/lib/types/hawk-types-augmentation.d.ts @@ -0,0 +1,13 @@ +import type { CatcherMessageType } from '@hawk.so/types'; + +// TODO: delete this file once the @hawk.so/types dependency is bumped past that release +declare module '@hawk.so/types' { + // eslint-disable-next-line @typescript-eslint/no-shadow, @typescript-eslint/naming-convention, no-unused-vars, @typescript-eslint/no-unused-vars + interface CatcherMessage<_Type extends CatcherMessageType> { + count?: number; + } + + interface RepetitionDBScheme { + count?: number; + } +} diff --git a/workers/grouper/src/index.ts b/workers/grouper/src/index.ts index 44460c8d..1f745622 100644 --- a/workers/grouper/src/index.ts +++ b/workers/grouper/src/index.ts @@ -253,7 +253,7 @@ export default class GrouperWorker extends Worker { */ await this.saveEvent(task.projectId, { groupHash: uniqueEventHash, - totalCount: 1, + totalCount: task.count ?? 1, catcherType: task.catcherType, payload: task.payload, timestamp: task.timestamp, @@ -297,7 +297,7 @@ export default class GrouperWorker extends Worker { */ await this.incrementEventCounterAndAffectedUsers(task.projectId, { groupHash: uniqueEventHash, - }, incrementAffectedUsers); + }, incrementAffectedUsers, task.count ?? 1); /** * Decode existed event to calculate diffs correctly @@ -333,6 +333,15 @@ export default class GrouperWorker extends Worker { timestamp: task.timestamp, } as RepetitionDBScheme; + /** + * Store count only when it carries information (>1) + * Absent or 1 both mean "single occurrence", + * so omitting it keeps repetition documents lean. + */ + if (task.count !== undefined && task.count > 1) { + newRepetition.count = task.count; + } + repetitionId = await this.saveRepetition(task.projectId, newRepetition); /** @@ -355,7 +364,8 @@ export default class GrouperWorker extends Worker { uniqueEventHash, task.timestamp, repetitionId, - incrementDailyAffectedUsers + incrementDailyAffectedUsers, + task.count ?? 1 ); this.memoryMonitor.logHandleCompletion( @@ -768,8 +778,9 @@ export default class GrouperWorker extends Worker { * @param projectId - project id to increment * @param query - query to get event * @param incrementAffected - if true, usersAffected counter will be incremented + * @param incrementBy - how many occurrences this repetition represents */ - private async incrementEventCounterAndAffectedUsers(projectId, query, incrementAffected: boolean): Promise { + private async incrementEventCounterAndAffectedUsers(projectId, query, incrementAffected: boolean, incrementBy = 1): Promise { if (!projectId || !mongodb.ObjectID.isValid(projectId)) { throw new ValidationError('Controller.saveEvent: Project ID is invalid or missed'); } @@ -779,13 +790,13 @@ export default class GrouperWorker extends Worker { const updateQuery = incrementAffected ? { $inc: { - totalCount: 1, + totalCount: incrementBy, usersAffected: 1, }, } : { $inc: { - totalCount: 1, + totalCount: incrementBy, }, }; @@ -806,6 +817,7 @@ export default class GrouperWorker extends Worker { * @param {string} eventTimestamp - timestamp of the last event * @param {string|null} repetitionId - event's last repetition id * @param {boolean} shouldIncrementAffectedUsers - whether to increment affected users + * @param {number} incrementBy - how many occurrences this task represents * @returns {Promise} */ private async saveDailyEvents( @@ -813,7 +825,8 @@ export default class GrouperWorker extends Worker { eventHash: string, eventTimestamp: number, repetitionId: string | null, - shouldIncrementAffectedUsers: boolean + shouldIncrementAffectedUsers: boolean, + incrementBy = 1 ): Promise { if (!projectId || !mongodb.ObjectID.isValid(projectId)) { throw new ValidationError('GrouperWorker.saveDailyEvents: Project ID is invalid or missed'); @@ -838,7 +851,7 @@ export default class GrouperWorker extends Worker { lastRepetitionId: repetitionId, }, $inc: { - count: 1, + count: incrementBy, affectedUsers: shouldIncrementAffectedUsers ? 1 : 0, }, }, diff --git a/workers/grouper/tests/index.test.ts b/workers/grouper/tests/index.test.ts index c75351a9..17656098 100644 --- a/workers/grouper/tests/index.test.ts +++ b/workers/grouper/tests/index.test.ts @@ -79,12 +79,14 @@ const projectMock = { * * @param event - allows to override some event properties in generated task * @param timestamp - timestamp of the event, defaults to current time + * @param count - number of occurrences this task represents */ -function generateTask(event: Partial> = undefined, timestamp: number = new Date().getTime()): GroupWorkerTask { +function generateTask(event: Partial> = undefined, timestamp: number = new Date().getTime(), count: number = undefined): GroupWorkerTask { return { projectId: projectIdMock, catcherType: 'errors/javascript', timestamp, + count, payload: Object.assign({ title: 'Hawk client catcher test', backtrace: [], @@ -182,6 +184,19 @@ describe('GrouperWorker', () => { expect((await eventsCollection.findOne({})).totalCount).toBe(4); }); + test('Should set total events count to repetition count on first occurrence when count is provided', async () => { + await worker.handle(generateTask(undefined, undefined, 5)); + + expect((await eventsCollection.findOne({})).totalCount).toBe(5); + }); + + test('Should increment total events count by repetition count on processing when count is provided', async () => { + await worker.handle(generateTask(undefined, undefined, 3)); + await worker.handle(generateTask(undefined, undefined, 2)); + + expect((await eventsCollection.findOne({})).totalCount).toBe(5); + }); + test('Should not increment total usersAffected count if it is event from first user', async () => { await worker.handle(generateTask({ user: { id: '123' } })); await worker.handle(generateTask({ user: { id: '123' } })); @@ -351,6 +366,13 @@ describe('GrouperWorker', () => { expect((await dailyEventsCollection.findOne({})).count).toBe(4); }); + test('Should update events count per day by repetition count', async () => { + await worker.handle(generateTask(undefined, undefined, 3)); + await worker.handle(generateTask(undefined, undefined, 2)); + + expect((await dailyEventsCollection.findOne({})).count).toBe(5); + }); + test('Should update last repetition id', async () => { await worker.handle(generateTask()); await worker.handle(generateTask()); @@ -374,6 +396,33 @@ describe('GrouperWorker', () => { }).toArray()).length).toBe(2); }); + test('Should save repetition count when count is greater than 1', async () => { + await worker.handle(generateTask()); + await worker.handle(generateTask(undefined, undefined, 7)); + + const savedRepetition = await repetitionsCollection.findOne({}); + + expect(savedRepetition.count).toBe(7); + }); + + test('Should omit repetition count when count is absent', async () => { + await worker.handle(generateTask()); + await worker.handle(generateTask()); + + const savedRepetition = await repetitionsCollection.findOne({}); + + expect(savedRepetition.count).toBeUndefined(); + }); + + test('Should omit repetition count when count is exactly 1', async () => { + await worker.handle(generateTask()); + await worker.handle(generateTask(undefined, undefined, 1)); + + const savedRepetition = await repetitionsCollection.findOne({}); + + expect(savedRepetition.count).toBeUndefined(); + }); + test('Should stringify delta', async () => { const generatedTask = generateTask(); diff --git a/workers/javascript/src/index.ts b/workers/javascript/src/index.ts index 84fae857..2ee1155c 100644 --- a/workers/javascript/src/index.ts +++ b/workers/javascript/src/index.ts @@ -94,6 +94,7 @@ export default class JavascriptEventWorker extends EventWorker { catcherType: this.type as CatcherMessageType, payload: event.payload as CatcherMessagePayload, timestamp: event.timestamp, + count: event.count, } as GroupWorkerTask); }