Skip to content
Open
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
12 changes: 12 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ services:
- ./:/usr/src/app
- workers-deps:/usr/src/app/node_modules

hawk-worker-webhook:
build:
dockerfile: "dev.Dockerfile"
context: .
env_file:
- .env
restart: unless-stopped
entrypoint: yarn run-webhook
volumes:
- ./:/usr/src/app
- workers-deps:/usr/src/app/node_modules

volumes:
workers-deps:

Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hawk.workers",
"private": true,
"version": "0.1.2",
"version": "0.1.3",
"description": "Hawk workers",
"repository": "git@github.com:codex-team/hawk.workers.git",
"license": "BUSL-1.1",
Expand Down Expand Up @@ -34,6 +34,7 @@
"test:notifier": "jest workers/notifier",
"test:js": "jest workers/javascript",
"test:task-manager": "jest workers/task-manager",
"test:webhook": "jest workers/webhook",
"test:clear": "jest --clearCache",
"run-default": "yarn worker hawk-worker-default",
"run-sentry": "yarn worker hawk-worker-sentry",
Expand All @@ -49,13 +50,14 @@
"run-email": "yarn worker hawk-worker-email",
"run-telegram": "yarn worker hawk-worker-telegram",
"run-limiter": "yarn worker hawk-worker-limiter",
"run-task-manager": "yarn worker hawk-worker-task-manager"
"run-task-manager": "yarn worker hawk-worker-task-manager",
"run-webhook": "yarn worker hawk-worker-webhook"
},
"dependencies": {
"@babel/parser": "^7.26.9",
"@babel/traverse": "7.26.9",
"@hawk.so/nodejs": "^3.1.1",
"@hawk.so/types": "^0.5.7",
"@hawk.so/types": "^0.5.9",
"@types/amqplib": "^0.8.2",
"@types/jest": "^29.5.14",
"@types/mongodb": "^3.5.15",
Expand Down
1 change: 1 addition & 0 deletions workers/notifier/types/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum ChannelType {
Telegram = 'telegram',
Slack = 'slack',
Loop = 'loop',
Webhook = 'webhook',
}

/**
Expand Down
11 changes: 11 additions & 0 deletions workers/webhook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "hawk-worker-webhook",
"version": "1.0.0",
"description": "Webhook sender worker — delivers event notifications as JSON POST requests",
"main": "src/index.ts",
"license": "MIT",
"workerType": "sender/webhook",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
86 changes: 86 additions & 0 deletions workers/webhook/src/deliverer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import https from 'https';
import http from 'http';
import { createLogger, format, Logger, transports } from 'winston';

/**
* Timeout for webhook delivery in milliseconds
*/
const DELIVERY_TIMEOUT_MS = 10000;

/**
* HTTP status code threshold for error responses
*/
const HTTP_ERROR_STATUS = 400;

/**
* Deliverer sends JSON POST requests to external webhook endpoints
*/
export default class WebhookDeliverer {
/**
* Logger module
* (default level='info')
*/
private logger: Logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
transports: [
new transports.Console({
format: format.combine(
format.timestamp(),
format.colorize(),
format.simple(),
format.printf((msg) => `${msg.timestamp} - ${msg.level}: ${msg.message}`)
),
}),
],
});

/**
* Sends JSON payload to the webhook endpoint via HTTP POST
*
* @param endpoint - URL to POST to
* @param payload - JSON body to send
*/
public async deliver(endpoint: string, payload: Record<string, unknown>): Promise<void> {
const body = JSON.stringify(payload);
const url = new URL(endpoint);
const transport = url.protocol === 'https:' ? https : http;

return new Promise<void>((resolve) => {
const req = transport.request(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Hawk-Webhook/1.0',
'Content-Length': Buffer.byteLength(body),
},
timeout: DELIVERY_TIMEOUT_MS,
},
(res) => {
res.resume();

if (res.statusCode && res.statusCode >= HTTP_ERROR_STATUS) {
this.logger.log('error', `Webhook delivery failed: ${res.statusCode} ${res.statusMessage} for ${endpoint}`);
}

resolve();
}
);

req.on('error', (e) => {
this.logger.log('error', `Can't deliver webhook to ${endpoint}: ${e.message}`);
resolve();
});

req.on('timeout', () => {
this.logger.log('error', `Webhook delivery timed out for ${endpoint}`);
req.destroy();
resolve();
});

req.write(body);
req.end();
});
}
}
24 changes: 24 additions & 0 deletions workers/webhook/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as pkg from './../package.json';
import WebhookProvider from './provider';
import SenderWorker from 'hawk-worker-sender/src';
import { ChannelType } from 'hawk-worker-notifier/types/channel';

/**
* Worker to send webhook notifications
*/
export default class WebhookSenderWorker extends SenderWorker {
/**
* Worker type
*/
public readonly type: string = pkg.workerType;

/**
* Webhook channel type
*/
protected channelType = ChannelType.Webhook;

/**
* Webhook provider
*/
protected provider = new WebhookProvider();
}
41 changes: 41 additions & 0 deletions workers/webhook/src/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import NotificationsProvider from 'hawk-worker-sender/src/provider';
import { Notification, EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';
import templates from './templates';
import { WebhookTemplate } from '../types/template';
import WebhookDeliverer from './deliverer';

/**
* This class provides a 'send' method that renders and sends a webhook notification
*/
export default class WebhookProvider extends NotificationsProvider {
/**
* Class with the 'deliver' method for sending HTTP POST requests
*/
private readonly deliverer: WebhookDeliverer;

constructor() {
super();

this.deliverer = new WebhookDeliverer();
}

/**
* Send webhook notification to recipient
*
* @param to - recipient endpoint URL
* @param notification - notification with payload and type
*/
public async send(to: string, notification: Notification): Promise<void> {
let template: WebhookTemplate;

switch (notification.type) {
case 'event': template = templates.EventTpl; break;
case 'several-events': template = templates.SeveralEventsTpl; break;
default: return;
}

const payload = template(notification.payload as EventsTemplateVariables);

await this.deliverer.deliver(to, payload);
}
}
30 changes: 30 additions & 0 deletions workers/webhook/src/templates/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { EventsTemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables';

/**
* Builds webhook JSON payload for a single event notification.
* Mirrors the same data structure other workers receive, serialized as JSON.
*
* @param tplData - event template data
*/
export default function render(tplData: EventsTemplateVariables): Record<string, unknown> {
const eventInfo = tplData.events[0] as TemplateEventData;
const event = eventInfo.event;
const eventURL = tplData.host + '/project/' + tplData.project._id + '/event/' + event._id + '/';

return {
project: {
id: tplData.project._id.toString(),
name: tplData.project.name,
},
event: {
id: event._id?.toString() ?? null,
groupHash: event.groupHash,
totalCount: event.totalCount,
newCount: eventInfo.newCount,
daysRepeated: eventInfo.daysRepeated,
url: eventURL,
payload: event.payload,
},
period: tplData.period,
};
}
7 changes: 7 additions & 0 deletions workers/webhook/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import EventTpl from './event';
import SeveralEventsTpl from './several-events';

export default {
EventTpl,
SeveralEventsTpl,
};
33 changes: 33 additions & 0 deletions workers/webhook/src/templates/several-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables';

/**
* Builds webhook JSON payload for a several-events notification.
* Mirrors the same data structure other workers receive, serialized as JSON.
*
* @param tplData - event template data
*/
export default function render(tplData: EventsTemplateVariables): Record<string, unknown> {
const projectUrl = tplData.host + '/project/' + tplData.project._id;

return {
project: {
id: tplData.project._id.toString(),
name: tplData.project.name,
url: projectUrl,
},
events: tplData.events.map(({ event, newCount, daysRepeated }) => {
const eventURL = tplData.host + '/project/' + tplData.project._id + '/event/' + event._id + '/';

return {
id: event._id?.toString() ?? null,
groupHash: event.groupHash,
totalCount: event.totalCount,
newCount,
daysRepeated,
url: eventURL,
payload: event.payload,
};
}),
period: tplData.period,
};
}
43 changes: 43 additions & 0 deletions workers/webhook/tests/__mocks__/event-notify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { EventNotification } from 'hawk-worker-sender/types/template-variables';
import { ObjectId } from 'mongodb';

/**
* Example of new-events notify template variables
*/
export default {
type: 'event',
payload: {
events: [
{
event: {
totalCount: 10,
timestamp: Date.now(),
payload: {
title: 'New event',
backtrace: [ {
file: 'file',
line: 1,
sourceCode: [ {
line: 1,
content: 'code',
} ],
} ],
},
},
daysRepeated: 1,
newCount: 1,
},
],
period: 60,
host: process.env.GARAGE_URL,
hostOfStatic: process.env.API_STATIC_URL,
project: {
_id: new ObjectId('5d206f7f9aaf7c0071d64596'),
token: 'project-token',
name: 'Project',
workspaceId: new ObjectId('5d206f7f9aaf7c0071d64596'),
uidAdded: new ObjectId('5d206f7f9aaf7c0071d64596'),
notifications: [],
},
},
} as EventNotification;
Loading
Loading