@pinelab/vendure-plugin-alerting 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # 1.0.0 (2026-05-27)
2
+
3
+ - Initial release.
4
+ - Event-based and log-based alerting.
5
+ - `WebhookNotifier` out of the box.
6
+ - Deduplication and JobQueue-backed delivery.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Vendure Alerting Plugin
2
+
3
+ [Official documentation here](https://plugins.pinelab.studio/plugin/vendure-plugin-alerting)
4
+
5
+ Send notifications based on Vendure events and log messages via configurable notifiers like webhooks, slack or N8N.
6
+
7
+ ## Getting started
8
+
9
+ ```ts
10
+ import {
11
+ AlertingPlugin,
12
+ EventAlert,
13
+ LogAlert,
14
+ WebhookNotifier,
15
+ } from '@pinelab/vendure-plugin-alerting';
16
+ import { PaymentStateTransitionEvent } from '@vendure/core';
17
+
18
+ // Slack example. Get the webhook URL from your slack account: https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
19
+ const slack = new WebhookNotifier({
20
+ name: 'slack',
21
+ url: process.env.SLACK_WEBHOOK!,
22
+ });
23
+
24
+ const n8n = new WebhookNotifier({
25
+ name: 'n8n',
26
+ url: 'https://n8n.example.com/webhook/alert',
27
+ });
28
+
29
+ const plugins: VendurePlugin[] = [
30
+ AlertingPlugin.init({
31
+ alerts: [
32
+ // Alert on order payment failure
33
+ new EventAlert([slack, n8n])
34
+ .on(PaymentStateTransitionEvent)
35
+ .filter((e) => e.toState === 'Error') // Use this to decide when to alert
36
+ .notify((e) => `Payment error for order "${e.order.code}"`), // Notifies via Slack and n8n
37
+
38
+ // Alert on any error log from PaymentService
39
+ new LogAlert([slack])
40
+ .onLog('error', 'warn')
41
+ .filter((log) => log.loggerCtx === 'PaymentService')
42
+ .notify((log) => ({
43
+ subject: `[${log.level}] ${log.loggerCtx}`,
44
+ text: log.message,
45
+ })),
46
+ ],
47
+ // Deduplicate identical notifications to prevent alert storms: Only notify every once per X ms
48
+ deduplicationWindowMs: 60_000, // default
49
+ }),
50
+ ];
51
+ ```
52
+
53
+ ## Email notifier
54
+
55
+ Send alerts as plain-text emails via Vendure's built-in `NodemailerEmailSender`. No templates are required — the alert subject and text are sent directly.
56
+
57
+ ```ts
58
+ import { EmailNotifier } from '@pinelab/vendure-plugin-alerting';
59
+
60
+ const email = new EmailNotifier({
61
+ name: 'email',
62
+ from: 'alerts@example.com',
63
+ to: 'admin@example.com',
64
+ transport: {
65
+ type: 'smtp',
66
+ host: 'smtp.example.com',
67
+ port: 587,
68
+ auth: {
69
+ user: 'username',
70
+ pass: 'password',
71
+ },
72
+ },
73
+ });
74
+
75
+ new LogAlert([email]).onLog('error').notify((log) => ({
76
+ subject: `[${log.level}] ${log.loggerCtx}`,
77
+ text: log.message,
78
+ }));
79
+ ```
80
+
81
+ You can use any transport supported by the EmailPlugin, such as `ses`, `sendmail`, or `file`. You can also pass a custom `EmailSender` via the `emailSender` option.
82
+
83
+ ## Custom Notifiers
84
+
85
+ Implement the `Notifier` interface to create custom channels (e.g. SMS) and use custom metadata.
86
+
87
+ ```ts
88
+ import { AlertMessage, Notifier } from '@pinelab/vendure-plugin-alerting';
89
+
90
+ // Example notifier that uses custom metadata
91
+ export class CustomNotifier implements Notifier {
92
+ name = 'my-custom-notifier';
93
+
94
+ notify({subject, text, metadata}: AlertMessage): Promise<void> {
95
+ // This is where you would implement the logic to send the alert message to your desired destination.
96
+ // With the given metadata you could send custom body or headers for example.
97
+ const authorization = metadata.myHeaders.bearerToken
98
+ }
99
+ }
100
+
101
+ // You would use this Notifier like this:
102
+ const customNotifier = new CustomNotifier();
103
+ new EventAlert([customNotifier])
104
+ .on(PaymentStateTransitionEvent)
105
+ .filter((e) => e.toState === 'Error')
106
+ .notify((log) => ({
107
+ subject: `My subject`,
108
+ text: `my custom message`,
109
+ metadata: {
110
+ key: "you can pass anything you want to your custom notifier here"
111
+ key2: "It is up to your notifier to use the metadata object"
112
+ }
113
+ })),
114
+ ```
115
+
116
+ ## Deduplication
117
+
118
+ Identical alerts (same notifier + subject + text) fired within `deduplicationWindowMs` are dropped. This prevents alert storms when many events fire in rapid succession.
119
+
120
+ ## Retry
121
+
122
+ Alert delivery is backed by the Vendure JobQueue. If a notifier throws, the job is retried with the configured retry strategy.
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,6 @@
1
+ import { Type } from '@vendure/core';
2
+ import { AlertingPluginOptions } from './types';
3
+ export declare class AlertingPlugin {
4
+ static options: AlertingPluginOptions;
5
+ static init(options: AlertingPluginOptions): Type<AlertingPlugin>;
6
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var AlertingPlugin_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.AlertingPlugin = void 0;
11
+ const core_1 = require("@vendure/core");
12
+ const constants_1 = require("./constants");
13
+ const alerting_service_1 = require("./services/alerting.service");
14
+ let AlertingPlugin = AlertingPlugin_1 = class AlertingPlugin {
15
+ static init(options) {
16
+ this.options = {
17
+ ...this.options,
18
+ ...options,
19
+ };
20
+ return AlertingPlugin_1;
21
+ }
22
+ };
23
+ exports.AlertingPlugin = AlertingPlugin;
24
+ AlertingPlugin.options = {
25
+ alerts: [],
26
+ };
27
+ exports.AlertingPlugin = AlertingPlugin = AlertingPlugin_1 = __decorate([
28
+ (0, core_1.VendurePlugin)({
29
+ imports: [core_1.PluginCommonModule],
30
+ providers: [
31
+ alerting_service_1.AlertingService,
32
+ {
33
+ provide: constants_1.PLUGIN_INIT_OPTIONS,
34
+ useFactory: () => AlertingPlugin.options,
35
+ },
36
+ ],
37
+ configuration: (config) => {
38
+ config.logger = alerting_service_1.AlertingService.wrapLogger(config.logger);
39
+ return config;
40
+ },
41
+ compatibility: '>=3.2.0',
42
+ })
43
+ ], AlertingPlugin);
@@ -0,0 +1,82 @@
1
+ import { Type, VendureEvent } from '@vendure/core';
2
+ import { Notifier } from './notifier';
3
+ import { AlertMessage, AlertLogLevel, LogAlertContext } from '../types';
4
+ /**
5
+ * Abstract base for alert definitions consumed by {@link AlertingService}.
6
+ * Holds the shared notifier list, optional filter, and message builder.
7
+ *
8
+ * @typeParam T - The type of trigger this alert receives (a Vendure event or
9
+ * a {@link LogAlertContext}).
10
+ */
11
+ export declare abstract class BaseAlert<T> {
12
+ readonly notifiers: Notifier[];
13
+ /** Optional predicate that decides whether the alert should fire. */
14
+ filterFn?: (trigger: T) => boolean | Promise<boolean>;
15
+ /** Function that converts the trigger into an {@link AlertMessage}
16
+ * (or a plain string, which is auto-wrapped as `{ subject: 'Alert', text: <string> }`). */
17
+ notifyFn: (trigger: T) => string | AlertMessage | Promise<string | AlertMessage>;
18
+ /**
19
+ * @param notifiers - The channels that will receive the alert when it fires.
20
+ */
21
+ constructor(notifiers: Notifier[]);
22
+ /**
23
+ * Only fire the alert when the predicate returns true for the trigger.
24
+ */
25
+ filter(fn: (trigger: T) => boolean | Promise<boolean>): this;
26
+ /**
27
+ * Define how the {@link AlertMessage} is built from the trigger.
28
+ * Returning a string is shorthand for `{ subject: 'Alert', text: <string> }`.
29
+ */
30
+ notify(fn: (trigger: T) => string | AlertMessage | Promise<string | AlertMessage>): this;
31
+ }
32
+ /**
33
+ * Builder for an alert rule that reacts to Vendure {@linkcode EventBus} events.
34
+ *
35
+ * The generic parameter {@linkcode E} is automatically inferred from the events
36
+ * passed to {@linkcode on()}. It starts as `never` so the alert only fires when
37
+ * explicitly subscribed to event types.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * new EventAlert([webhookNotifier])
42
+ * .on(ProductEvent, OrderPlacedEvent)
43
+ * .filter(e => e.type === 'created')
44
+ * .notify(e => `Product ${e.type}`);
45
+ * ```
46
+ */
47
+ export declare class EventAlert<E extends VendureEvent = never> extends BaseAlert<E> {
48
+ /** Vendure event types this alert subscribes to. */
49
+ events: Type<VendureEvent>[];
50
+ /**
51
+ * Trigger this alert when any of the given Vendure events are published.
52
+ *
53
+ * @returns A new {@linkcode EventAlert} whose event union now includes all
54
+ * the events passed in.
55
+ */
56
+ on<T extends VendureEvent[]>(...events: {
57
+ [I in keyof T]: Type<T[I]>;
58
+ }): EventAlert<E | T[number]>;
59
+ }
60
+ /**
61
+ * Builder for an alert rule that reacts to log messages emitted through
62
+ * Vendure's {@linkcode VendureLogger}.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * new LogAlert([webhookNotifier])
67
+ * .onLog('error', 'warn')
68
+ * .filter(log => log.loggerCtx === 'PaymentGateway')
69
+ * .notify(log => ({
70
+ * subject: `[${log.level}] Payment issue`,
71
+ * text: log.message,
72
+ * }));
73
+ * ```
74
+ */
75
+ export declare class LogAlert extends BaseAlert<LogAlertContext> {
76
+ /** Log levels this alert subscribes to. */
77
+ logLevels: AlertLogLevel[];
78
+ /**
79
+ * Trigger this alert when a log message is emitted at any of the given levels.
80
+ */
81
+ onLog(...levels: AlertLogLevel[]): this;
82
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogAlert = exports.EventAlert = exports.BaseAlert = void 0;
4
+ /**
5
+ * Abstract base for alert definitions consumed by {@link AlertingService}.
6
+ * Holds the shared notifier list, optional filter, and message builder.
7
+ *
8
+ * @typeParam T - The type of trigger this alert receives (a Vendure event or
9
+ * a {@link LogAlertContext}).
10
+ */
11
+ class BaseAlert {
12
+ /**
13
+ * @param notifiers - The channels that will receive the alert when it fires.
14
+ */
15
+ constructor(notifiers) {
16
+ this.notifiers = notifiers;
17
+ }
18
+ /**
19
+ * Only fire the alert when the predicate returns true for the trigger.
20
+ */
21
+ filter(fn) {
22
+ this.filterFn = fn;
23
+ return this;
24
+ }
25
+ /**
26
+ * Define how the {@link AlertMessage} is built from the trigger.
27
+ * Returning a string is shorthand for `{ subject: 'Alert', text: <string> }`.
28
+ */
29
+ notify(fn) {
30
+ this.notifyFn = fn;
31
+ return this;
32
+ }
33
+ }
34
+ exports.BaseAlert = BaseAlert;
35
+ /**
36
+ * Builder for an alert rule that reacts to Vendure {@linkcode EventBus} events.
37
+ *
38
+ * The generic parameter {@linkcode E} is automatically inferred from the events
39
+ * passed to {@linkcode on()}. It starts as `never` so the alert only fires when
40
+ * explicitly subscribed to event types.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * new EventAlert([webhookNotifier])
45
+ * .on(ProductEvent, OrderPlacedEvent)
46
+ * .filter(e => e.type === 'created')
47
+ * .notify(e => `Product ${e.type}`);
48
+ * ```
49
+ */
50
+ class EventAlert extends BaseAlert {
51
+ constructor() {
52
+ super(...arguments);
53
+ /** Vendure event types this alert subscribes to. */
54
+ this.events = [];
55
+ }
56
+ /**
57
+ * Trigger this alert when any of the given Vendure events are published.
58
+ *
59
+ * @returns A new {@linkcode EventAlert} whose event union now includes all
60
+ * the events passed in.
61
+ */
62
+ on(...events) {
63
+ this.events.push(...events);
64
+ return this;
65
+ }
66
+ }
67
+ exports.EventAlert = EventAlert;
68
+ /**
69
+ * Builder for an alert rule that reacts to log messages emitted through
70
+ * Vendure's {@linkcode VendureLogger}.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * new LogAlert([webhookNotifier])
75
+ * .onLog('error', 'warn')
76
+ * .filter(log => log.loggerCtx === 'PaymentGateway')
77
+ * .notify(log => ({
78
+ * subject: `[${log.level}] Payment issue`,
79
+ * text: log.message,
80
+ * }));
81
+ * ```
82
+ */
83
+ class LogAlert extends BaseAlert {
84
+ constructor() {
85
+ super(...arguments);
86
+ /** Log levels this alert subscribes to. */
87
+ this.logLevels = [];
88
+ }
89
+ /**
90
+ * Trigger this alert when a log message is emitted at any of the given levels.
91
+ */
92
+ onLog(...levels) {
93
+ this.logLevels.push(...levels);
94
+ return this;
95
+ }
96
+ }
97
+ exports.LogAlert = LogAlert;
@@ -0,0 +1,14 @@
1
+ import { AlertMessage } from '../types';
2
+ /**
3
+ * Defines an outgoing notification channel (e.g. webhook, email, SMS).
4
+ * Each notifier has a unique name and knows how to deliver an {@link AlertMessage}.
5
+ */
6
+ export interface Notifier {
7
+ /** Unique name; used as job-data discriminator and in logs */
8
+ readonly name: string;
9
+ /**
10
+ * Deliver the alert message.
11
+ * Throwing causes the JobQueue to retry the delivery.
12
+ */
13
+ notify(message: AlertMessage): Promise<void>;
14
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,26 @@
1
+ import { AlertMessage } from '../../types';
2
+ import { Notifier } from '../notifier';
3
+ import { EmailSender, EmailTransportOptions } from '@vendure/email-plugin';
4
+ export interface EmailNotifierConfig {
5
+ /** Unique name; used as job-data discriminator and in logs */
6
+ name: string;
7
+ /** Sender address */
8
+ from: string;
9
+ /** Recipient address */
10
+ to: string;
11
+ /** Vendure email transport options (e.g. SMTP, SES, file) */
12
+ transport: EmailTransportOptions;
13
+ /** Optional custom EmailSender; defaults to NodemailerEmailSender */
14
+ emailSender?: EmailSender;
15
+ }
16
+ /**
17
+ * Notifier that sends alert messages as plain-text emails via Vendure's
18
+ * built-in {@link NodemailerEmailSender}. No templates are required —
19
+ * the alert subject and text are sent directly as the email subject and body.
20
+ */
21
+ export declare class EmailNotifier implements Notifier {
22
+ private config;
23
+ readonly name: string;
24
+ constructor(config: EmailNotifierConfig);
25
+ notify(message: AlertMessage): Promise<void>;
26
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EmailNotifier = void 0;
4
+ const email_plugin_1 = require("@vendure/email-plugin");
5
+ /**
6
+ * Notifier that sends alert messages as plain-text emails via Vendure's
7
+ * built-in {@link NodemailerEmailSender}. No templates are required —
8
+ * the alert subject and text are sent directly as the email subject and body.
9
+ */
10
+ class EmailNotifier {
11
+ constructor(config) {
12
+ this.config = config;
13
+ this.name = config.name;
14
+ }
15
+ async notify(message) {
16
+ const emailDetails = {
17
+ from: this.config.from,
18
+ recipient: this.config.to,
19
+ subject: message.subject,
20
+ body: message.text,
21
+ attachments: [],
22
+ };
23
+ const sender = this.config.emailSender ?? new email_plugin_1.NodemailerEmailSender();
24
+ await sender.send(emailDetails, this.config.transport);
25
+ }
26
+ }
27
+ exports.EmailNotifier = EmailNotifier;
@@ -0,0 +1,15 @@
1
+ import { AlertMessage } from '../../types';
2
+ import { Notifier } from '../notifier';
3
+ interface WebhookNotifierConfig {
4
+ name: string;
5
+ url: string;
6
+ method?: 'POST' | 'GET';
7
+ headers?: Record<string, string>;
8
+ }
9
+ export declare class WebhookNotifier implements Notifier {
10
+ private config;
11
+ readonly name: string;
12
+ constructor(config: WebhookNotifierConfig);
13
+ notify(message: AlertMessage): Promise<void>;
14
+ }
15
+ export {};
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebhookNotifier = void 0;
7
+ const node_fetch_1 = __importDefault(require("node-fetch"));
8
+ class WebhookNotifier {
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.name = config.name;
12
+ }
13
+ async notify(message) {
14
+ const headers = {
15
+ 'content-type': 'application/json',
16
+ ...(this.config.headers || {}),
17
+ ...(message.metadata?.headers || {}),
18
+ };
19
+ const body = this.config.method === 'GET'
20
+ ? undefined
21
+ : JSON.stringify(message.metadata?.body ?? message);
22
+ const res = await (0, node_fetch_1.default)(this.config.url, {
23
+ method: this.config.method || 'POST',
24
+ headers,
25
+ body,
26
+ });
27
+ if (!res.ok) {
28
+ throw new Error(`Webhook ${this.name} returned ${res.status}: ${await res.text()}`);
29
+ }
30
+ }
31
+ }
32
+ exports.WebhookNotifier = WebhookNotifier;
@@ -0,0 +1,2 @@
1
+ export declare const PLUGIN_INIT_OPTIONS: unique symbol;
2
+ export declare const loggerCtx = "AlertingPlugin";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loggerCtx = exports.PLUGIN_INIT_OPTIONS = void 0;
4
+ exports.PLUGIN_INIT_OPTIONS = Symbol('PLUGIN_INIT_OPTIONS');
5
+ exports.loggerCtx = 'AlertingPlugin';
@@ -0,0 +1,7 @@
1
+ export * from './alerting.plugin';
2
+ export * from './types';
3
+ export * from './config/alert';
4
+ export * from './config/notifier';
5
+ export * from './config/notifiers/webhook-notifier';
6
+ export * from './config/notifiers/email-notifier';
7
+ export * from './services/alerting.service';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./alerting.plugin"), exports);
18
+ __exportStar(require("./types"), exports);
19
+ __exportStar(require("./config/alert"), exports);
20
+ __exportStar(require("./config/notifier"), exports);
21
+ __exportStar(require("./config/notifiers/webhook-notifier"), exports);
22
+ __exportStar(require("./config/notifiers/email-notifier"), exports);
23
+ __exportStar(require("./services/alerting.service"), exports);
@@ -0,0 +1,20 @@
1
+ import { VendureLogger } from '@vendure/core';
2
+ import { AlertLogLevel, LogAlertContext } from '../types';
3
+ /**
4
+ * VendureLogger wrapper that forwards log calls to the original logger
5
+ * and buffers entries so the AlertingService can process them.
6
+ */
7
+ export declare class AlertingLogger implements VendureLogger {
8
+ private readonly wrapped;
9
+ private static logBuffer;
10
+ private static isReady;
11
+ private static onLogHandler?;
12
+ static setHandler(handler: (log: LogAlertContext) => void): void;
13
+ static pushLog(level: AlertLogLevel, message: string, ctx?: string): void;
14
+ constructor(wrapped: VendureLogger);
15
+ info(message: string, ctx?: string): void;
16
+ warn(message: string, ctx?: string): void;
17
+ error(message: string, ctx?: string, trace?: string): void;
18
+ debug(message: string, ctx?: string): void;
19
+ verbose(message: string, ctx?: string): void;
20
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AlertingLogger = void 0;
4
+ /**
5
+ * VendureLogger wrapper that forwards log calls to the original logger
6
+ * and buffers entries so the AlertingService can process them.
7
+ */
8
+ class AlertingLogger {
9
+ static setHandler(handler) {
10
+ this.onLogHandler = handler;
11
+ this.isReady = true;
12
+ // Flush buffered logs
13
+ while (this.logBuffer.length > 0) {
14
+ const log = this.logBuffer.shift();
15
+ handler(log);
16
+ }
17
+ }
18
+ static pushLog(level, message, ctx) {
19
+ const log = { level, message, loggerCtx: ctx };
20
+ if (this.isReady && this.onLogHandler) {
21
+ this.onLogHandler(log);
22
+ }
23
+ else {
24
+ this.logBuffer.push(log);
25
+ }
26
+ }
27
+ constructor(wrapped) {
28
+ this.wrapped = wrapped;
29
+ }
30
+ info(message, ctx) {
31
+ this.wrapped.info(message, ctx);
32
+ AlertingLogger.pushLog('info', message, ctx);
33
+ }
34
+ warn(message, ctx) {
35
+ this.wrapped.warn(message, ctx);
36
+ AlertingLogger.pushLog('warn', message, ctx);
37
+ }
38
+ error(message, ctx, trace) {
39
+ this.wrapped.error(message, ctx, trace);
40
+ AlertingLogger.pushLog('error', message, ctx);
41
+ }
42
+ debug(message, ctx) {
43
+ this.wrapped.debug(message, ctx);
44
+ }
45
+ verbose(message, ctx) {
46
+ this.wrapped.verbose(message, ctx);
47
+ }
48
+ }
49
+ exports.AlertingLogger = AlertingLogger;
50
+ AlertingLogger.logBuffer = [];
51
+ AlertingLogger.isReady = false;
@@ -0,0 +1,20 @@
1
+ import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
2
+ import { EventBus, JobQueueService, VendureLogger } from '@vendure/core';
3
+ import { AlertingPluginOptions } from '../types';
4
+ export declare class AlertingService implements OnModuleInit, OnApplicationBootstrap {
5
+ private eventBus;
6
+ private jobQueueService;
7
+ private options;
8
+ private jobQueue;
9
+ private dedupMap;
10
+ private notifiersByName;
11
+ constructor(eventBus: EventBus, jobQueueService: JobQueueService, options: AlertingPluginOptions);
12
+ onModuleInit(): Promise<void>;
13
+ onApplicationBootstrap(): void;
14
+ static wrapLogger(logger: VendureLogger): VendureLogger;
15
+ private handleTrigger;
16
+ private processJob;
17
+ private dedupKey;
18
+ private hash;
19
+ private cleanupDedupMap;
20
+ }
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.AlertingService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const core_1 = require("@vendure/core");
18
+ const constants_1 = require("../constants");
19
+ const alert_1 = require("../config/alert");
20
+ const alert_logger_1 = require("./alert-logger");
21
+ let AlertingService = class AlertingService {
22
+ constructor(eventBus, jobQueueService, options) {
23
+ this.eventBus = eventBus;
24
+ this.jobQueueService = jobQueueService;
25
+ this.options = options;
26
+ this.dedupMap = new Map();
27
+ this.notifiersByName = new Map();
28
+ }
29
+ async onModuleInit() {
30
+ this.jobQueue = await this.jobQueueService.createQueue({
31
+ name: 'alerting-notify',
32
+ process: async (job) => {
33
+ try {
34
+ await this.processJob(job.data.notifierName, job.data.alertMessage);
35
+ }
36
+ catch (e) {
37
+ const err = e instanceof Error ? e : new Error(String(e));
38
+ core_1.Logger.error(`Failed to process alert job for notifier "${job.data.notifierName}": ${err.message}`, constants_1.loggerCtx, err.stack);
39
+ throw e;
40
+ }
41
+ },
42
+ });
43
+ }
44
+ onApplicationBootstrap() {
45
+ // Build notifier lookup from all alerts
46
+ for (const alert of this.options.alerts) {
47
+ for (const notifier of alert.notifiers) {
48
+ this.notifiersByName.set(notifier.name, notifier);
49
+ }
50
+ }
51
+ // Subscribe to configured events (EventAlert only)
52
+ for (const alert of this.options.alerts) {
53
+ if (alert instanceof alert_1.EventAlert) {
54
+ for (const EventType of alert.events) {
55
+ this.eventBus.ofType(EventType).subscribe(async (event) => {
56
+ try {
57
+ await this.handleTrigger(alert, event);
58
+ }
59
+ catch (e) {
60
+ core_1.Logger.error(`Failed to handle event ${event.constructor.name} for alert: ${e}`, constants_1.loggerCtx);
61
+ }
62
+ });
63
+ }
64
+ }
65
+ }
66
+ // Subscribe to log buffer (LogAlert only)
67
+ alert_logger_1.AlertingLogger.setHandler((log) => {
68
+ for (const alert of this.options.alerts) {
69
+ if (alert instanceof alert_1.LogAlert && alert.logLevels.includes(log.level)) {
70
+ this.handleTrigger(alert, log).catch((e) => {
71
+ core_1.Logger.error(`Failed to handle log alert: ${e}`, constants_1.loggerCtx);
72
+ });
73
+ }
74
+ }
75
+ });
76
+ }
77
+ static wrapLogger(logger) {
78
+ return new alert_logger_1.AlertingLogger(logger);
79
+ }
80
+ async handleTrigger(alert, trigger) {
81
+ // Prevent infinite loops: skip logs emitted by this plugin
82
+ const logCtx = trigger.loggerCtx;
83
+ if (logCtx === constants_1.loggerCtx) {
84
+ return;
85
+ }
86
+ // Apply filter if configured
87
+ if (alert.filterFn) {
88
+ const passes = await alert.filterFn(trigger);
89
+ if (!passes) {
90
+ return;
91
+ }
92
+ }
93
+ // Build message
94
+ const raw = await alert.notifyFn(trigger);
95
+ const message = typeof raw === 'string' ? { subject: 'Alert', text: raw } : raw;
96
+ // Enqueue one job per notifier
97
+ for (const notifier of alert.notifiers) {
98
+ await this.jobQueue.add({
99
+ notifierName: notifier.name,
100
+ alertMessage: message,
101
+ });
102
+ }
103
+ }
104
+ async processJob(notifierName, message) {
105
+ const windowMs = this.options.deduplicationWindowMs ?? 60_000;
106
+ const key = this.dedupKey(notifierName, message);
107
+ const lastSent = this.dedupMap.get(key);
108
+ const now = Date.now();
109
+ if (lastSent && now - lastSent < windowMs) {
110
+ core_1.Logger.info(`Dropping duplicate alert for notifier "${notifierName}" with subject "${message.subject}"`, constants_1.loggerCtx);
111
+ return;
112
+ }
113
+ this.dedupMap.set(key, now);
114
+ this.cleanupDedupMap(windowMs);
115
+ const notifier = this.notifiersByName.get(notifierName);
116
+ if (!notifier) {
117
+ throw new Error(`Notifier "${notifierName}" not found`);
118
+ }
119
+ await notifier.notify(message);
120
+ core_1.Logger.info(`Sent alert to notifier "${notifierName}" with subject "${message.subject}"`, constants_1.loggerCtx);
121
+ }
122
+ dedupKey(notifierName, message) {
123
+ const str = `${notifierName}:${message.subject}:${message.text}`;
124
+ return this.hash(str);
125
+ }
126
+ hash(input) {
127
+ let hash = 0;
128
+ for (let i = 0; i < input.length; i++) {
129
+ const char = input.charCodeAt(i);
130
+ hash = (hash << 5) - hash + char;
131
+ hash |= 0;
132
+ }
133
+ return hash.toString(16);
134
+ }
135
+ cleanupDedupMap(windowMs) {
136
+ const cutoff = Date.now() - windowMs;
137
+ for (const [key, ts] of this.dedupMap.entries()) {
138
+ if (ts < cutoff) {
139
+ this.dedupMap.delete(key);
140
+ }
141
+ }
142
+ }
143
+ };
144
+ exports.AlertingService = AlertingService;
145
+ exports.AlertingService = AlertingService = __decorate([
146
+ (0, common_1.Injectable)(),
147
+ __param(2, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
148
+ __metadata("design:paramtypes", [core_1.EventBus,
149
+ core_1.JobQueueService, Object])
150
+ ], AlertingService);
@@ -0,0 +1,22 @@
1
+ import { EventAlert, LogAlert } from './config/alert';
2
+ export interface AlertMessage {
3
+ subject: string;
4
+ text: string;
5
+ metadata?: Record<string, any>;
6
+ }
7
+ export type AlertLogLevel = 'info' | 'warn' | 'error';
8
+ export interface LogAlertContext {
9
+ level: AlertLogLevel;
10
+ message: string;
11
+ loggerCtx?: string;
12
+ }
13
+ export type AlertTrigger<E = unknown> = E | LogAlertContext;
14
+ export interface AlertingPluginOptions {
15
+ alerts: Array<EventAlert<any> | LogAlert>;
16
+ /**
17
+ * Time window for deduplication in ms. Identical AlertMessages
18
+ * (same notifier + subject + text hash) within this window are dropped.
19
+ * Default: 60_000
20
+ */
21
+ deduplicationWindowMs?: number;
22
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@pinelab/vendure-plugin-alerting",
3
+ "version": "1.0.0",
4
+ "description": "Vendure plugin for sending alerts based on events and log to various notifiers like Slack, email or N8N",
5
+ "author": "Martijn van de Brug <martijn@pinelab.studio>",
6
+ "homepage": "https://plugins.pinelab.studio/",
7
+ "repository": "https://github.com/Pinelab-studio/pinelab-vendure-plugins",
8
+ "license": "MIT",
9
+ "private": false,
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "rimraf dist && tsc",
22
+ "start": "yarn ts-node test/dev-server.ts",
23
+ "test": "vitest run",
24
+ "lint": "echo 'No linting configured'"
25
+ },
26
+ "dependencies": {
27
+ "node-fetch": "^2.6.7",
28
+ "catch-unknown": "^2.0.0"
29
+ }
30
+ }