diff --git a/.github/assets/Hawk.png b/.github/assets/Hawk.png new file mode 100644 index 000000000..11f183dcd Binary files /dev/null and b/.github/assets/Hawk.png differ diff --git a/workers/email/src/templates/emails/event/html.twig b/workers/email/src/templates/emails/event/html.twig index 64b35bba8..35c7c5032 100644 --- a/workers/email/src/templates/emails/event/html.twig +++ b/workers/email/src/templates/emails/event/html.twig @@ -58,7 +58,7 @@ {% endblock %} {% block unsubscribeLink %} - {{ host ~ '/unsubscribe/' ~ project._id }} + {{ host ~ '/unsubscribe/' ~ project._id ~ '/' ~ notificationRuleId }} {% endblock %} {% block unsubscribeText %} diff --git a/workers/email/tests/provider.test.ts b/workers/email/tests/provider.test.ts index 1a19314fa..89f39f407 100644 --- a/workers/email/tests/provider.test.ts +++ b/workers/email/tests/provider.test.ts @@ -81,6 +81,7 @@ describe('EmailProvider', () => { period: 60, host: process.env.GARAGE_URL!, hostOfStatic: process.env.API_STATIC_URL!, + notificationRuleId: '5d206f7f9aaf7c0071d64596', project: { _id: new ObjectId('5d206f7f9aaf7c0071d64596'), token: 'project-token', @@ -131,6 +132,7 @@ describe('EmailProvider', () => { }], host: process.env.GARAGE_URL!, hostOfStatic: process.env.API_STATIC_URL!, + notificationRuleId: '5d206f7f9aaf7c0071d64596', project: { _id: new ObjectId('5d206f7f9aaf7c0071d64596'), token: 'project-token', diff --git a/workers/grouper/src/index.ts b/workers/grouper/src/index.ts index 321fa1a6c..73bc589dc 100644 --- a/workers/grouper/src/index.ts +++ b/workers/grouper/src/index.ts @@ -6,7 +6,7 @@ import { Worker } from '../../../lib/worker'; import * as WorkerNames from '../../../lib/workerNames'; import * as pkg from '../package.json'; import type { GroupWorkerTask, RepetitionDelta } from '../types/group-worker-task'; -import type { EventAddons, EventDataAccepted, GroupedEventDBScheme, BacktraceFrame, SourceCodeLine } from '@hawk.so/types'; +import type { EventAddons, EventDataAccepted, GroupedEventDBScheme, BacktraceFrame, SourceCodeLine, ProjectEventGroupingPatternsDBScheme } from '@hawk.so/types'; import type { RepetitionDBScheme } from '../types/repetition'; import { DatabaseReadWriteError, DiffCalculationError, ValidationError } from '../../../lib/workerErrors'; import { decodeUnsafeFields, encodeUnsafeFields } from '../../../lib/utils/unsafeFields'; @@ -317,11 +317,11 @@ export default class GrouperWorker extends Worker { if (matchingPattern !== null && matchingPattern !== undefined) { try { - const originalEvent = await this.cache.get(`${projectId}:${matchingPattern}:originalEvent`, async () => { + const originalEvent = await this.cache.get(`${projectId}:${matchingPattern._id}:originalEvent`, async () => { return await this.eventsDb.getConnection() .collection(`events:${projectId}`) .findOne( - { 'payload.title': { $regex: matchingPattern } }, + { 'payload.title': { $regex: matchingPattern.pattern } }, { sort: { _id: 1 } } ); }); @@ -332,7 +332,7 @@ export default class GrouperWorker extends Worker { return originalEvent; } } catch (e) { - this.logger.error(`Error while getting original event for pattern ${matchingPattern}`); + this.logger.error(`Error while getting original event for pattern ${matchingPattern}: ${e.message}`); } } } @@ -345,15 +345,18 @@ export default class GrouperWorker extends Worker { * * @param patterns - list of the patterns of the related project * @param event - event which title would be cheched - * @returns {string | null} matched pattern or null if no match + * @returns {ProjectEventGroupingPatternsDBScheme | null} matched pattern object or null if no match */ - private async findMatchingPattern(patterns: string[], event: EventDataAccepted): Promise { + private async findMatchingPattern( + patterns: ProjectEventGroupingPatternsDBScheme[], + event: EventDataAccepted + ): Promise { if (!patterns || patterns.length === 0) { return null; } return patterns.filter(pattern => { - const patternRegExp = new RegExp(pattern); + const patternRegExp = new RegExp(pattern.pattern); return event.title.match(patternRegExp); }).pop() || null; @@ -363,9 +366,9 @@ export default class GrouperWorker extends Worker { * Method that gets event patterns for a project * * @param projectId - id of the project to find related event patterns - * @returns {string[]} EventPatterns object with projectId and list of patterns + * @returns {ProjectEventGroupingPatternsDBScheme[]} EventPatterns object with projectId and list of patterns */ - private async getProjectPatterns(projectId: string): Promise { + private async getProjectPatterns(projectId: string): Promise { return this.cache.get(`project:${projectId}:patterns`, async () => { const project = await this.accountsDb.getConnection() .collection('projects') diff --git a/workers/grouper/tests/index.test.ts b/workers/grouper/tests/index.test.ts index fcbb95546..aa955d737 100644 --- a/workers/grouper/tests/index.test.ts +++ b/workers/grouper/tests/index.test.ts @@ -67,7 +67,7 @@ const projectMock = { }, unreadCount: 0, description: 'Test project for grouper worker tests', - eventGroupingPatterns: [ 'New error .*' ], + eventGroupingPatterns: [ { _id: mongodb.ObjectId(), pattern: 'New error .*' }], }; /** @@ -490,7 +490,9 @@ describe('GrouperWorker', () => { }); test('should group events with titles matching one pattern', async () => { - jest.spyOn(GrouperWorker.prototype as any, 'getProjectPatterns').mockResolvedValue([ 'New error .*' ]); + jest.spyOn(GrouperWorker.prototype as any, 'getProjectPatterns').mockResolvedValue([ + { _id: new mongodb.ObjectId, pattern: 'New error .*' } + ]); const findMatchingPatternSpy = jest.spyOn(GrouperWorker.prototype as any, 'findMatchingPattern'); await worker.handle(generateTask({ title: 'New error 0000000000000000' })); @@ -507,9 +509,9 @@ describe('GrouperWorker', () => { test('should handle multiple patterns and match the first one that applies', async () => { jest.spyOn(GrouperWorker.prototype as any, 'getProjectPatterns').mockResolvedValue([ - 'Database error: .*', - 'Network error: .*', - 'New error: .*', + { _id: mongodb.ObjectId(), pattern: 'Database error: .*' }, + { _id: mongodb.ObjectId(), pattern: 'Network error: .*' }, + { _id: mongodb.ObjectId(), pattern: 'New error: .*' }, ]); await worker.handle(generateTask({ title: 'Database error: connection failed' })); @@ -526,8 +528,8 @@ describe('GrouperWorker', () => { test('should handle complex regex patterns', async () => { jest.spyOn(GrouperWorker.prototype as any, 'getProjectPatterns').mockResolvedValue([ - 'Error \\d{3}: [A-Za-z\\s]+ in file .*\\.js$', - 'Warning \\d{3}: .*', + { _id: mongodb.ObjectId(), pattern: 'Error \\d{3}: [A-Za-z\\s]+ in file .*\\.js$' }, + { _id: mongodb.ObjectId(), pattern: 'Warning \\d{3}: .*' }, ]); await worker.handle(generateTask({ title: 'Error 404: Not Found in file index.js' })); @@ -544,8 +546,8 @@ describe('GrouperWorker', () => { test('should maintain separate groups for different patterns', async () => { jest.spyOn(GrouperWorker.prototype as any, 'getProjectPatterns').mockResolvedValue([ - 'TypeError: .*', - 'ReferenceError: .*', + { _id: mongodb.ObjectId(), pattern: 'TypeError: .*' }, + { _id: mongodb.ObjectId(), pattern: 'ReferenceError: .*' }, ]); await worker.handle(generateTask({ title: 'TypeError: null is not an object' })); @@ -566,8 +568,8 @@ describe('GrouperWorker', () => { test('should handle patterns with special regex characters', async () => { jest.spyOn(GrouperWorker.prototype as any, 'getProjectPatterns').mockResolvedValue([ - 'Error \\[\\d+\\]: .*', - 'Warning \\(code=\\d+\\): .*', + { _id: new mongodb.ObjectID(), pattern: 'Error \\[\\d+\\]: .*'} , + { _id: new mongodb.ObjectID(), pattern: 'Warning \\(code=\\d+\\): .*'} , ]); await worker.handle(generateTask({ title: 'Error [123]: Database connection failed' })); diff --git a/workers/notifier/README.md b/workers/notifier/README.md index abb6486dc..707d5289e 100644 --- a/workers/notifier/README.md +++ b/workers/notifier/README.md @@ -2,6 +2,11 @@ Handles new events from Grouper Worker, holds it and sends to sender worlers +This repository is a part of the Hawk ecosystemm. You can register [here](https://garage.hawk.so/login) + +![alt text](../../.github/assets/Hawk.png) + + ## How to run 1. Make sure you are in Workers root directory @@ -12,21 +17,12 @@ Handles new events from Grouper Worker, holds it and sends to sender worlers ## Events handling scheme ``` -1) On task received -> receive task -> get project notification rules -> filter rules - -> check channel timer - a) if timer doesn't exist - -> send tasks to sender workers - -> set timeout for minPeriod - b) if timer exists - -> push event to channel's buffer - -2) On timeout - -> get events from channel's buffer - -> flush channel's buffer - -> send tasks to sender workers + -> update eventsCount in redis + -> get updated eventCount + -> send notification if eventCount == treshold ``` ### Event example diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index fef4be5d2..a92c9d190 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -196,6 +196,7 @@ export default abstract class SenderWorker extends Worker { project, events: eventsData, period: channel.minPeriod, + notificationRuleId: rule._id, }, } as EventNotification | SeveralEventsNotification); } diff --git a/workers/sender/types/template-variables/event.ts b/workers/sender/types/template-variables/event.ts index 51735fd29..701444ece 100644 --- a/workers/sender/types/template-variables/event.ts +++ b/workers/sender/types/template-variables/event.ts @@ -45,6 +45,12 @@ export interface EventsTemplateVariables extends CommonTemplateVariables { * Minimal pause between second notification, in seconds */ period: number; + + /** + * Id of notification rule to unsubscribe. + * Required for email notifications – to form unsubscribe link. + */ + notificationRuleId?: string; } /**