Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/assets/Hawk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion workers/email/src/templates/emails/event/html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
{% endblock %}

{% block unsubscribeLink %}
{{ host ~ '/unsubscribe/' ~ project._id }}
{{ host ~ '/unsubscribe/' ~ project._id ~ '/' ~ notificationRuleId }}
{% endblock %}

{% block unsubscribeText %}
Expand Down
2 changes: 2 additions & 0 deletions workers/email/tests/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
21 changes: 12 additions & 9 deletions workers/grouper/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 } }
);
});
Expand All @@ -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}`);
}
}
}
Expand All @@ -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<EventAddons>): Promise<string | null> {
private async findMatchingPattern(
patterns: ProjectEventGroupingPatternsDBScheme[],
event: EventDataAccepted<EventAddons>
): Promise<ProjectEventGroupingPatternsDBScheme | null> {
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;
Expand All @@ -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<string[]> {
private async getProjectPatterns(projectId: string): Promise<ProjectEventGroupingPatternsDBScheme[]> {
return this.cache.get(`project:${projectId}:patterns`, async () => {
const project = await this.accountsDb.getConnection()
.collection('projects')
Expand Down
24 changes: 13 additions & 11 deletions workers/grouper/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 .*' }],
};

/**
Expand Down Expand Up @@ -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' }));
Expand All @@ -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' }));
Expand All @@ -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' }));
Expand All @@ -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' }));
Expand All @@ -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' }));
Expand Down
20 changes: 8 additions & 12 deletions workers/notifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions workers/sender/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export default abstract class SenderWorker extends Worker {
project,
events: eventsData,
period: channel.minPeriod,
notificationRuleId: rule._id,
},
} as EventNotification | SeveralEventsNotification);
}
Expand Down
6 changes: 6 additions & 0 deletions workers/sender/types/template-variables/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
Loading