Skip to content

Commit 474c747

Browse files
committed
Merge branch 'master' into fix-patterns-grouping
2 parents ea557b9 + 620264f commit 474c747

File tree

16 files changed

+678
-1
lines changed

16 files changed

+678
-1
lines changed

lib/utils/decl.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Decl of number
3+
*
4+
* @param value - value to decl
5+
* @param titles - titles to decl: ['новое событие', 'новых события', 'новых событий']
6+
* @example declOfNum(1, ['новое событие', 'новых события', 'новых событий']) -> 'новое событие'
7+
* @example declOfNum(2, ['новое событие', 'новых события', 'новых событий']) -> 'новых события'
8+
* @example declOfNum(10, ['новое событие', 'новых события', 'новых событий']) -> 'новых событий'
9+
* @example declOfNum(21, ['новое событие', 'новых события', 'новых событий']) -> 'новое событие'
10+
* @returns decl of number
11+
*/
12+
export function declOfNum(value: number, titles: string[]): string {
13+
const decimalBase = 10;
14+
const hundredBase = 100;
15+
const minExclusiveTeens = 4;
16+
const maxExclusiveTeens = 20;
17+
const manyFormIndex = 2;
18+
const maxCaseIndex = 5;
19+
const declCases = [manyFormIndex, 0, 1, 1, 1, manyFormIndex];
20+
21+
const valueModHundred = value % hundredBase;
22+
const valueModTen = value % decimalBase;
23+
const isTeens = valueModHundred > minExclusiveTeens && valueModHundred < maxExclusiveTeens;
24+
const caseIndex = isTeens
25+
? manyFormIndex
26+
: declCases[valueModTen < maxCaseIndex ? valueModTen : maxCaseIndex];
27+
28+
return titles[caseIndex];
29+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"test:javascript": "jest workers/javascript",
2727
"test:release": "jest workers/release",
2828
"test:slack": "jest workers/slack",
29+
"test:loop": "jest workers/loop",
2930
"test:limiter": "jest workers/limiter --runInBand",
3031
"test:grouper": "jest workers/grouper",
3132
"test:diff": "jest ./workers/grouper/tests/diff.test.ts",
@@ -37,6 +38,7 @@
3738
"run-sentry": "yarn worker hawk-worker-sentry",
3839
"run-js": "yarn worker hawk-worker-javascript",
3940
"run-slack": "yarn worker hawk-worker-slack",
41+
"run-loop": "yarn worker hawk-worker-loop",
4042
"run-grouper": "yarn worker hawk-worker-grouper",
4143
"run-archiver": "yarn worker hawk-worker-archiver",
4244
"run-accountant": "yarn worker hawk-worker-accountant",

workers/loop/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "hawk-worker-loop",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "src/index.ts",
6+
"license": "MIT",
7+
"workerType": "sender/loop",
8+
"scripts": {
9+
"test": "echo \"Error: no test specified\" && exit 1"
10+
},
11+
"dependencies": {
12+
"@slack/webhook": "^5.0.3",
13+
"json-templater": "^1.2.0"
14+
}
15+
}

workers/loop/src/deliverer.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { IncomingWebhook } from '@slack/webhook';
2+
import { createLogger, format, Logger, transports } from 'winston';
3+
4+
/**
5+
* Deliverer is the man who will send messages to external service
6+
* Separated from the provider to allow testing 'send' method
7+
* Loop is Slack-like platform, so we use Slack API to send messages.
8+
*
9+
*/
10+
export default class LoopDeliverer {
11+
/**
12+
* Logger module
13+
* (default level='info')
14+
*/
15+
private logger: Logger = createLogger({
16+
level: process.env.LOG_LEVEL || 'info',
17+
transports: [
18+
new transports.Console({
19+
format: format.combine(
20+
format.timestamp(),
21+
format.colorize(),
22+
format.simple(),
23+
format.printf((msg) => `${msg.timestamp} - ${msg.level}: ${msg.message}`)
24+
),
25+
}),
26+
],
27+
});
28+
29+
/**
30+
* Sends message to the Loop through the Incoming Webhook app
31+
* https://developers.loop.ru/integrate/webhooks/incoming/
32+
*
33+
* @param endpoint - where to send
34+
* @param message - what to send
35+
*/
36+
public async deliver(endpoint: string, message: string): Promise<void> {
37+
try {
38+
const webhook = new IncomingWebhook(endpoint, {
39+
username: 'Hawk',
40+
});
41+
42+
await webhook.send(message);
43+
} catch (e) {
44+
this.logger.log('error', 'Can\'t deliver Incoming Webhook. Loop returns an error: ', e);
45+
}
46+
}
47+
}

workers/loop/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as pkg from './../package.json';
2+
import LoopProvider from './provider';
3+
import SenderWorker from 'hawk-worker-sender/src';
4+
import { ChannelType } from 'hawk-worker-notifier/types/channel';
5+
6+
/**
7+
* Worker to send email notifications
8+
*/
9+
export default class LoopSenderWorker extends SenderWorker {
10+
/**
11+
* Worker type
12+
*/
13+
public readonly type: string = pkg.workerType;
14+
15+
/**
16+
* Email channel type
17+
*/
18+
protected channelType = ChannelType.Loop;
19+
20+
/**
21+
* Email provider
22+
*/
23+
protected provider = new LoopProvider();
24+
}

workers/loop/src/provider.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import NotificationsProvider from 'hawk-worker-sender/src/provider';
2+
import { Notification, EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';
3+
import templates from './templates';
4+
import { LoopTemplate } from '../types/template';
5+
import LoopDeliverer from './deliverer';
6+
7+
/**
8+
* This class provides a 'send' method that will renders and sends a notification
9+
*/
10+
export default class LoopProvider extends NotificationsProvider {
11+
/**
12+
* Class with the 'deliver' method for sending messages to the Loop
13+
*/
14+
private readonly deliverer: LoopDeliverer;
15+
16+
/**
17+
* Constructor allows to separate dependencies that can't be tested,
18+
* so in tests they will be mocked.
19+
*/
20+
constructor() {
21+
super();
22+
23+
this.deliverer = new LoopDeliverer();
24+
}
25+
26+
/**
27+
* Send loop message to recipient
28+
*
29+
* @param to - recipient endpoint
30+
* @param notification - notification with payload and type
31+
*/
32+
public async send(to: string, notification: Notification): Promise<void> {
33+
let template: LoopTemplate;
34+
35+
switch (notification.type) {
36+
case 'event': template = templates.EventTpl; break;
37+
case 'several-events':template = templates.SeveralEventsTpl; break;
38+
}
39+
40+
const message = template(notification.payload as EventsTemplateVariables);
41+
42+
await this.deliverer.deliver(to, message);
43+
}
44+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { GroupedEventDBScheme } from '@hawk.so/types';
2+
import type { EventsTemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables';
3+
import { toMaxLen } from '../../../slack/src/templates/utils';
4+
5+
/**
6+
* Renders backtrace overview
7+
*
8+
* @param event - event to render
9+
*/
10+
function renderBacktrace(event: GroupedEventDBScheme): string {
11+
let code = '';
12+
13+
const firstNotEmptyFrame = event.payload.backtrace.find(frame => !!frame.sourceCode);
14+
15+
if (!firstNotEmptyFrame) {
16+
return code;
17+
}
18+
19+
code = firstNotEmptyFrame.sourceCode.map(({ line, content }) => {
20+
let colDelimiter = ': ';
21+
22+
if (line === firstNotEmptyFrame.line) {
23+
colDelimiter = ' ->';
24+
}
25+
26+
const MAX_SOURCE_CODE_LINE_LENGTH = 65;
27+
28+
return `${line}${colDelimiter} ${toMaxLen(content, MAX_SOURCE_CODE_LINE_LENGTH)}`;
29+
}).join('\n');
30+
31+
return code;
32+
}
33+
34+
/**
35+
* Return tpl with data substitutions
36+
*
37+
* @param tplData - event template data
38+
*/
39+
export default function render(tplData: EventsTemplateVariables): string {
40+
const eventInfo = tplData.events[0] as TemplateEventData;
41+
const event = eventInfo.event;
42+
const eventURL = tplData.host + '/project/' + tplData.project._id + '/event/' + event._id + '/';
43+
let location = 'Неизвестное место';
44+
45+
if (event.payload.backtrace && event.payload.backtrace.length > 0) {
46+
location = event.payload.backtrace[0].file;
47+
}
48+
49+
return ''.concat(
50+
`**${event.payload.title}**`,
51+
'\n',
52+
`*${location}*\n`,
53+
'```\n' + renderBacktrace(event) + '\n```',
54+
'\n',
55+
`[Посмотреть подробности](${eventURL}) `, `| *${tplData.project.name}*`, ` | ${eventInfo.newCount} новых (${eventInfo.event.totalCount} всего)`
56+
);
57+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import EventTpl from './event';
2+
import SeveralEventsTpl from './several-events';
3+
4+
export default {
5+
EventTpl,
6+
SeveralEventsTpl,
7+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';
2+
import { declOfNum } from '../../../../lib/utils/decl';
3+
4+
/**
5+
* Return tpl with data substitutions
6+
*
7+
* @param tplData - event template data
8+
*/
9+
export default function render(tplData: EventsTemplateVariables): string {
10+
const projectUrl = tplData.host + '/project/' + tplData.project._id;
11+
let message = tplData.events.length + ' ' + declOfNum(
12+
tplData.events.length,
13+
['новое событие', 'новых события', 'новых событий']
14+
) + '\n\n';
15+
16+
tplData.events.forEach(({ event, newCount }) => {
17+
message += `(${newCount}) ${event.payload.title} \n`;
18+
});
19+
20+
message += `\n[Посмотреть все события](${projectUrl}) | *${tplData.project.name}*`;
21+
22+
return message;
23+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { EventNotification } from 'hawk-worker-sender/types/template-variables';
2+
import { ObjectId } from 'mongodb';
3+
4+
/**
5+
* Example of new-events notify template variables
6+
*/
7+
export default {
8+
type: 'event',
9+
payload: {
10+
events: [
11+
{
12+
event: {
13+
totalCount: 10,
14+
timestamp: Date.now(),
15+
payload: {
16+
title: 'New event',
17+
backtrace: [ {
18+
file: 'file',
19+
line: 1,
20+
sourceCode: [ {
21+
line: 1,
22+
content: 'code',
23+
} ],
24+
} ],
25+
},
26+
},
27+
daysRepeated: 1,
28+
newCount: 1,
29+
},
30+
],
31+
period: 60,
32+
host: process.env.GARAGE_URL,
33+
hostOfStatic: process.env.API_STATIC_URL,
34+
project: {
35+
_id: new ObjectId('5d206f7f9aaf7c0071d64596'),
36+
token: 'project-token',
37+
name: 'Project',
38+
workspaceId: new ObjectId('5d206f7f9aaf7c0071d64596'),
39+
uidAdded: new ObjectId('5d206f7f9aaf7c0071d64596'),
40+
notifications: [],
41+
},
42+
},
43+
} as EventNotification;

0 commit comments

Comments
 (0)