@hookflo/tern 4.1.1 → 4.2.3-beta
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/README.md +33 -0
- package/dist/alerts.d.ts +7 -0
- package/dist/alerts.js +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/notifications/channels/discord/build-payload.d.ts +17 -0
- package/dist/notifications/channels/discord/build-payload.js +51 -0
- package/dist/notifications/channels/discord/index.d.ts +1 -0
- package/dist/notifications/channels/discord/index.js +5 -0
- package/dist/notifications/channels/slack/build-payload.d.ts +5 -0
- package/dist/notifications/channels/slack/build-payload.js +80 -0
- package/dist/notifications/channels/slack/index.d.ts +1 -0
- package/dist/notifications/channels/slack/index.js +5 -0
- package/dist/notifications/constants.d.ts +8 -0
- package/dist/notifications/constants.js +15 -0
- package/dist/notifications/index.d.ts +10 -0
- package/dist/notifications/index.js +27 -0
- package/dist/notifications/send-alert.d.ts +2 -0
- package/dist/notifications/send-alert.js +69 -0
- package/dist/notifications/types.d.ts +48 -0
- package/dist/notifications/types.js +2 -0
- package/dist/notifications/utils.d.ts +4 -0
- package/dist/notifications/utils.js +38 -0
- package/dist/upstash/controls.d.ts +2 -6
- package/dist/upstash/controls.js +41 -0
- package/dist/upstash/index.d.ts +1 -1
- package/dist/upstash/types.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -481,6 +481,39 @@ interface WebhookConfig {
|
|
|
481
481
|
}
|
|
482
482
|
```
|
|
483
483
|
|
|
484
|
+
|
|
485
|
+
## Alerting (Slack + Discord)
|
|
486
|
+
|
|
487
|
+
For the simplest DX, configure webhooks once in `createTernControls` and call `controls.alert(...)`.
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
import { createTernControls } from '@hookflo/tern/upstash';
|
|
491
|
+
|
|
492
|
+
const controls = createTernControls({
|
|
493
|
+
token: process.env.QSTASH_TOKEN!,
|
|
494
|
+
notifications: {
|
|
495
|
+
slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,
|
|
496
|
+
discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Non-DLQ event alert with defaults
|
|
501
|
+
await controls.alert();
|
|
502
|
+
|
|
503
|
+
// DLQ alert + replay flow
|
|
504
|
+
await controls.alert({
|
|
505
|
+
dlq: true,
|
|
506
|
+
dlqId: 'dlq_xxx',
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Behavior
|
|
511
|
+
|
|
512
|
+
- `controls.alert()` with no params sends a normal (non-DLQ) alert with internal defaults.
|
|
513
|
+
- `controls.alert({ dlq: true, dlqId })` sends a DLQ alert and attempts replay internally via `controls.replay(dlqId)`.
|
|
514
|
+
- `eventId` is auto-filled from `dlqId` for DLQ alerts.
|
|
515
|
+
- Optional overrides like `message`, `metadata`, `source`, or `branding` can still be passed.
|
|
516
|
+
|
|
484
517
|
## Testing
|
|
485
518
|
|
|
486
519
|
### Run All Tests
|
package/dist/alerts.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './notifications';
|
|
2
|
+
export declare const __alertInternals: {
|
|
3
|
+
resolveDestinations: typeof import("./notifications/utils").resolveDestinations;
|
|
4
|
+
normalizeAlertOptions: typeof import("./notifications/utils").normalizeAlertOptions;
|
|
5
|
+
buildSlackPayload: typeof import("./notifications/channels/slack").buildSlackPayload;
|
|
6
|
+
buildDiscordPayload: typeof import("./notifications/channels/discord").buildDiscordPayload;
|
|
7
|
+
};
|
package/dist/alerts.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
exports.__alertInternals = void 0;
|
|
18
|
+
__exportStar(require("./notifications"), exports);
|
|
19
|
+
const notifications_1 = require("./notifications");
|
|
20
|
+
// Backward-compatible alias used by previous version internals.
|
|
21
|
+
exports.__alertInternals = notifications_1.__notificationInternals;
|
package/dist/index.d.ts
CHANGED
|
@@ -31,4 +31,5 @@ export { createAlgorithmVerifier } from './verifiers/algorithms';
|
|
|
31
31
|
export { createCustomVerifier } from './verifiers/custom-algorithms';
|
|
32
32
|
export { normalizePayload, getPlatformNormalizationCategory, getPlatformsByCategory, } from './normalization/simple';
|
|
33
33
|
export * from './adapters';
|
|
34
|
+
export * from './alerts';
|
|
34
35
|
export default WebhookVerificationService;
|
package/dist/index.js
CHANGED
|
@@ -352,4 +352,5 @@ Object.defineProperty(exports, "normalizePayload", { enumerable: true, get: func
|
|
|
352
352
|
Object.defineProperty(exports, "getPlatformNormalizationCategory", { enumerable: true, get: function () { return simple_2.getPlatformNormalizationCategory; } });
|
|
353
353
|
Object.defineProperty(exports, "getPlatformsByCategory", { enumerable: true, get: function () { return simple_2.getPlatformsByCategory; } });
|
|
354
354
|
__exportStar(require("./adapters"), exports);
|
|
355
|
+
__exportStar(require("./alerts"), exports);
|
|
355
356
|
exports.default = WebhookVerificationService;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AlertPayloadBuilderInput } from "../../types";
|
|
2
|
+
export declare function buildDiscordPayload(input: AlertPayloadBuilderInput): {
|
|
3
|
+
embeds: {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
color: number;
|
|
7
|
+
fields: {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
inline?: boolean;
|
|
11
|
+
}[];
|
|
12
|
+
footer: {
|
|
13
|
+
text: string;
|
|
14
|
+
} | undefined;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildDiscordPayload = buildDiscordPayload;
|
|
4
|
+
const utils_1 = require("../../utils");
|
|
5
|
+
const constants_1 = require("../../constants");
|
|
6
|
+
function buildDiscordPayload(input) {
|
|
7
|
+
const isDLQ = input.dlq;
|
|
8
|
+
const title = isDLQ ? "Dead Letter Queue — Event Failed" : "Webhook Received";
|
|
9
|
+
const description = isDLQ
|
|
10
|
+
? "Event exhausted all retries. Manual replay required."
|
|
11
|
+
: "Event verified and queued for processing.";
|
|
12
|
+
const fields = [];
|
|
13
|
+
if (input.source) {
|
|
14
|
+
fields.push({ name: "Platform", value: input.source, inline: true });
|
|
15
|
+
}
|
|
16
|
+
fields.push({
|
|
17
|
+
name: "Severity",
|
|
18
|
+
value: input.severity.toLowerCase(),
|
|
19
|
+
inline: true,
|
|
20
|
+
});
|
|
21
|
+
if (isDLQ) {
|
|
22
|
+
fields.push({ name: "Queue", value: "dlq", inline: true });
|
|
23
|
+
}
|
|
24
|
+
if (input.eventId) {
|
|
25
|
+
fields.push({
|
|
26
|
+
name: isDLQ ? "DLQ ID" : "Event ID",
|
|
27
|
+
value: `\`${input.eventId}\``,
|
|
28
|
+
inline: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const metadataString = (0, utils_1.compactMetadata)(input.metadata);
|
|
32
|
+
if (metadataString) {
|
|
33
|
+
fields.push({ name: "Details", value: `\`\`\`${metadataString}\`\`\`` });
|
|
34
|
+
}
|
|
35
|
+
const replayLine = input.replayUrl
|
|
36
|
+
? `\n\n[${input.replayLabel ?? "Replay Event"}](${input.replayUrl})`
|
|
37
|
+
: "";
|
|
38
|
+
const footerText = input.branding === false ? undefined : "Alert from Tern · tern.hookflo.com";
|
|
39
|
+
return {
|
|
40
|
+
embeds: [
|
|
41
|
+
{
|
|
42
|
+
title,
|
|
43
|
+
description: `${description}${replayLine}`,
|
|
44
|
+
color: parseInt(constants_1.severityColorMap[input.severity].replace("#", ""), 16),
|
|
45
|
+
fields,
|
|
46
|
+
footer: footerText ? { text: footerText } : undefined,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildDiscordPayload } from './build-payload';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildDiscordPayload = void 0;
|
|
4
|
+
var build_payload_1 = require("./build-payload");
|
|
5
|
+
Object.defineProperty(exports, "buildDiscordPayload", { enumerable: true, get: function () { return build_payload_1.buildDiscordPayload; } });
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSlackPayload = buildSlackPayload;
|
|
4
|
+
const utils_1 = require("../../utils");
|
|
5
|
+
const constants_1 = require("../../constants");
|
|
6
|
+
function buildSlackPayload(input) {
|
|
7
|
+
const isDLQ = input.dlq;
|
|
8
|
+
const title = isDLQ ? "Dead Letter Queue — Event Failed" : "Webhook Received";
|
|
9
|
+
const message = isDLQ
|
|
10
|
+
? "Event exhausted all retries. Manual replay required."
|
|
11
|
+
: "Event verified and queued for processing.";
|
|
12
|
+
const fields = [
|
|
13
|
+
input.source
|
|
14
|
+
? { type: "mrkdwn", text: `*Platform*\n${input.source}` }
|
|
15
|
+
: null,
|
|
16
|
+
{
|
|
17
|
+
type: "mrkdwn",
|
|
18
|
+
text: `*Severity*\n${input.severity.toLowerCase()}`,
|
|
19
|
+
},
|
|
20
|
+
isDLQ ? { type: "mrkdwn", text: "*Queue*\ndlq" } : null,
|
|
21
|
+
input.eventId
|
|
22
|
+
? {
|
|
23
|
+
type: "mrkdwn",
|
|
24
|
+
text: `*${isDLQ ? "DLQ ID" : "Event ID"}*\n\`${input.eventId}\``,
|
|
25
|
+
}
|
|
26
|
+
: null,
|
|
27
|
+
].filter(Boolean);
|
|
28
|
+
const blocks = [
|
|
29
|
+
{
|
|
30
|
+
type: "section",
|
|
31
|
+
text: {
|
|
32
|
+
type: "mrkdwn",
|
|
33
|
+
text: `*${title}*\n${message}`,
|
|
34
|
+
},
|
|
35
|
+
fields,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const metadataString = (0, utils_1.compactMetadata)(input.metadata);
|
|
39
|
+
if (metadataString) {
|
|
40
|
+
blocks.push({
|
|
41
|
+
type: "section",
|
|
42
|
+
text: {
|
|
43
|
+
type: "mrkdwn",
|
|
44
|
+
text: `\`\`\`${metadataString}\`\`\``,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (input.replayUrl) {
|
|
49
|
+
blocks.push({
|
|
50
|
+
type: "actions",
|
|
51
|
+
elements: [
|
|
52
|
+
{
|
|
53
|
+
type: "button",
|
|
54
|
+
text: {
|
|
55
|
+
type: "plain_text",
|
|
56
|
+
text: input.replayLabel ?? "Replay Event",
|
|
57
|
+
emoji: false,
|
|
58
|
+
},
|
|
59
|
+
url: input.replayUrl,
|
|
60
|
+
style: "danger",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (input.branding !== false) {
|
|
66
|
+
blocks.push({
|
|
67
|
+
type: "context",
|
|
68
|
+
elements: [
|
|
69
|
+
{
|
|
70
|
+
type: "mrkdwn",
|
|
71
|
+
text: `Alert from <${constants_1.TERN_BRAND_URL}|Tern>`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
text: title,
|
|
78
|
+
blocks,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildSlackPayload } from './build-payload';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSlackPayload = void 0;
|
|
4
|
+
var build_payload_1 = require("./build-payload");
|
|
5
|
+
Object.defineProperty(exports, "buildSlackPayload", { enumerable: true, get: function () { return build_payload_1.buildSlackPayload; } });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AlertSeverity } from './types';
|
|
2
|
+
export declare const TERN_BRAND_URL = "https://tern.hookflo.com";
|
|
3
|
+
export declare const DEFAULT_DLQ_TITLE = "DLQ Event Alert";
|
|
4
|
+
export declare const DEFAULT_ALERT_TITLE = "Webhook Event Alert";
|
|
5
|
+
export declare const DEFAULT_REPLAY_LABEL = "Replay DLQ Event";
|
|
6
|
+
export declare const DEFAULT_DLQ_MESSAGE = "Webhook event moved to DLQ after retry attempts.";
|
|
7
|
+
export declare const DEFAULT_ALERT_MESSAGE = "Webhook event received.";
|
|
8
|
+
export declare const severityColorMap: Record<AlertSeverity, string>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.severityColorMap = exports.DEFAULT_ALERT_MESSAGE = exports.DEFAULT_DLQ_MESSAGE = exports.DEFAULT_REPLAY_LABEL = exports.DEFAULT_ALERT_TITLE = exports.DEFAULT_DLQ_TITLE = exports.TERN_BRAND_URL = void 0;
|
|
4
|
+
exports.TERN_BRAND_URL = 'https://tern.hookflo.com';
|
|
5
|
+
exports.DEFAULT_DLQ_TITLE = 'DLQ Event Alert';
|
|
6
|
+
exports.DEFAULT_ALERT_TITLE = 'Webhook Event Alert';
|
|
7
|
+
exports.DEFAULT_REPLAY_LABEL = 'Replay DLQ Event';
|
|
8
|
+
exports.DEFAULT_DLQ_MESSAGE = 'Webhook event moved to DLQ after retry attempts.';
|
|
9
|
+
exports.DEFAULT_ALERT_MESSAGE = 'Webhook event received.';
|
|
10
|
+
exports.severityColorMap = {
|
|
11
|
+
info: '#3B82F6',
|
|
12
|
+
warning: '#F59E0B',
|
|
13
|
+
error: '#EF4444',
|
|
14
|
+
critical: '#7C3AED',
|
|
15
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
import { normalizeAlertOptions, resolveDestinations } from './utils';
|
|
3
|
+
import { buildSlackPayload } from './channels/slack';
|
|
4
|
+
import { buildDiscordPayload } from './channels/discord';
|
|
5
|
+
export declare const __notificationInternals: {
|
|
6
|
+
resolveDestinations: typeof resolveDestinations;
|
|
7
|
+
normalizeAlertOptions: typeof normalizeAlertOptions;
|
|
8
|
+
buildSlackPayload: typeof buildSlackPayload;
|
|
9
|
+
buildDiscordPayload: typeof buildDiscordPayload;
|
|
10
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
exports.__notificationInternals = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
const utils_1 = require("./utils");
|
|
20
|
+
const slack_1 = require("./channels/slack");
|
|
21
|
+
const discord_1 = require("./channels/discord");
|
|
22
|
+
exports.__notificationInternals = {
|
|
23
|
+
resolveDestinations: utils_1.resolveDestinations,
|
|
24
|
+
normalizeAlertOptions: utils_1.normalizeAlertOptions,
|
|
25
|
+
buildSlackPayload: slack_1.buildSlackPayload,
|
|
26
|
+
buildDiscordPayload: discord_1.buildDiscordPayload,
|
|
27
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendAlert = sendAlert;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const slack_1 = require("./channels/slack");
|
|
6
|
+
const discord_1 = require("./channels/discord");
|
|
7
|
+
async function postWebhook(webhookUrl, body) {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch(webhookUrl, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'content-type': 'application/json',
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify(body),
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
status: response.status,
|
|
20
|
+
error: `Webhook call failed with ${response.status}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { ok: true, status: response.status };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
error: error.message,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function buildPayload(channel, options) {
|
|
33
|
+
const normalized = (0, utils_1.normalizeAlertOptions)(options);
|
|
34
|
+
return channel === 'slack' ? (0, slack_1.buildSlackPayload)(normalized) : (0, discord_1.buildDiscordPayload)(normalized);
|
|
35
|
+
}
|
|
36
|
+
async function sendAlert(config, options) {
|
|
37
|
+
const destinations = (0, utils_1.resolveDestinations)(config);
|
|
38
|
+
if (destinations.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
total: 0,
|
|
42
|
+
delivered: 0,
|
|
43
|
+
results: [{
|
|
44
|
+
channel: 'slack',
|
|
45
|
+
webhookUrl: '',
|
|
46
|
+
ok: false,
|
|
47
|
+
error: 'No valid alert webhook destinations configured',
|
|
48
|
+
}],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const results = await Promise.all(destinations.map(async (destination) => {
|
|
52
|
+
const payload = buildPayload(destination.channel, options);
|
|
53
|
+
const response = await postWebhook(destination.webhookUrl, payload);
|
|
54
|
+
return {
|
|
55
|
+
channel: destination.channel,
|
|
56
|
+
webhookUrl: destination.webhookUrl,
|
|
57
|
+
ok: response.ok,
|
|
58
|
+
status: response.status,
|
|
59
|
+
error: response.error,
|
|
60
|
+
};
|
|
61
|
+
}));
|
|
62
|
+
const delivered = results.filter((result) => result.ok).length;
|
|
63
|
+
return {
|
|
64
|
+
success: delivered === results.length,
|
|
65
|
+
total: results.length,
|
|
66
|
+
delivered,
|
|
67
|
+
results,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type AlertSeverity = 'info' | 'warning' | 'error' | 'critical';
|
|
2
|
+
export type AlertChannel = 'slack' | 'discord';
|
|
3
|
+
export interface AlertChannelConfig {
|
|
4
|
+
webhookUrl: string;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface AlertConfig {
|
|
8
|
+
slack?: AlertChannelConfig;
|
|
9
|
+
discord?: AlertChannelConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface SendAlertOptions {
|
|
12
|
+
dlq?: boolean;
|
|
13
|
+
dlqId?: string;
|
|
14
|
+
eventId?: string;
|
|
15
|
+
source?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
severity?: AlertSeverity;
|
|
19
|
+
replayUrl?: string;
|
|
20
|
+
replayLabel?: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
branding?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SendAlertResult {
|
|
25
|
+
channel: AlertChannel;
|
|
26
|
+
webhookUrl: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
status?: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SendAlertSummary {
|
|
32
|
+
success: boolean;
|
|
33
|
+
total: number;
|
|
34
|
+
delivered: number;
|
|
35
|
+
results: SendAlertResult[];
|
|
36
|
+
}
|
|
37
|
+
export interface AlertDestination {
|
|
38
|
+
channel: AlertChannel;
|
|
39
|
+
webhookUrl: string;
|
|
40
|
+
}
|
|
41
|
+
export interface AlertPayloadBuilderInput extends SendAlertOptions {
|
|
42
|
+
title: string;
|
|
43
|
+
message: string;
|
|
44
|
+
severity: AlertSeverity;
|
|
45
|
+
replayLabel: string;
|
|
46
|
+
eventId?: string;
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AlertConfig, AlertDestination, AlertPayloadBuilderInput, SendAlertOptions } from './types';
|
|
2
|
+
export declare function compactMetadata(metadata?: Record<string, unknown>): string | undefined;
|
|
3
|
+
export declare function resolveDestinations(config: AlertConfig): AlertDestination[];
|
|
4
|
+
export declare function normalizeAlertOptions(options: SendAlertOptions): AlertPayloadBuilderInput;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compactMetadata = compactMetadata;
|
|
4
|
+
exports.resolveDestinations = resolveDestinations;
|
|
5
|
+
exports.normalizeAlertOptions = normalizeAlertOptions;
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
function compactMetadata(metadata) {
|
|
8
|
+
if (!metadata || Object.keys(metadata).length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
const entries = Object.entries(metadata).slice(0, 8);
|
|
11
|
+
return entries.map(([key, value]) => `${key}: ${String(value)}`).join('\n');
|
|
12
|
+
}
|
|
13
|
+
function resolveDestinations(config) {
|
|
14
|
+
const destinations = [];
|
|
15
|
+
if (config.slack?.enabled !== false && config.slack?.webhookUrl) {
|
|
16
|
+
destinations.push({ channel: 'slack', webhookUrl: config.slack.webhookUrl });
|
|
17
|
+
}
|
|
18
|
+
if (config.discord?.enabled !== false && config.discord?.webhookUrl) {
|
|
19
|
+
destinations.push({ channel: 'discord', webhookUrl: config.discord.webhookUrl });
|
|
20
|
+
}
|
|
21
|
+
return destinations;
|
|
22
|
+
}
|
|
23
|
+
function normalizeAlertOptions(options) {
|
|
24
|
+
const isDlq = options.dlq === true;
|
|
25
|
+
const severity = options.severity || (isDlq ? 'error' : 'info');
|
|
26
|
+
const title = options.title || (isDlq ? constants_1.DEFAULT_DLQ_TITLE : constants_1.DEFAULT_ALERT_TITLE);
|
|
27
|
+
const message = options.message || (isDlq ? constants_1.DEFAULT_DLQ_MESSAGE : constants_1.DEFAULT_ALERT_MESSAGE);
|
|
28
|
+
const replayLabel = options.replayLabel || constants_1.DEFAULT_REPLAY_LABEL;
|
|
29
|
+
return {
|
|
30
|
+
...options,
|
|
31
|
+
dlq: isDlq,
|
|
32
|
+
title,
|
|
33
|
+
message,
|
|
34
|
+
severity,
|
|
35
|
+
replayLabel,
|
|
36
|
+
eventId: options.eventId || options.dlqId,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function createTernControls(config: TernControlsConfig):
|
|
3
|
-
dlq(): Promise<DLQMessage[]>;
|
|
4
|
-
replay(dlqId: string): Promise<ReplayResult>;
|
|
5
|
-
events(filter?: EventFilter): Promise<TernEvent[]>;
|
|
6
|
-
};
|
|
1
|
+
import { TernControls, TernControlsConfig } from './types';
|
|
2
|
+
export declare function createTernControls(config: TernControlsConfig): TernControls;
|
package/dist/upstash/controls.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createTernControls = createTernControls;
|
|
4
|
+
const send_alert_1 = require("../notifications/send-alert");
|
|
4
5
|
const QSTASH_API_BASE = 'https://qstash.upstash.io/v2';
|
|
5
6
|
function parseBody(body) {
|
|
6
7
|
if (!body)
|
|
@@ -114,5 +115,45 @@ function createTernControls(config) {
|
|
|
114
115
|
: mapped;
|
|
115
116
|
return statusFiltered.slice(0, filter.limit ?? 20);
|
|
116
117
|
},
|
|
118
|
+
async alert(options = {}) {
|
|
119
|
+
let replayMeta = {};
|
|
120
|
+
if (options.dlq) {
|
|
121
|
+
if (!options.dlqId || options.dlqId.trim() === '') {
|
|
122
|
+
throw new Error('[tern] controls.alert() with dlq: true requires dlqId.');
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const replay = await this.replay(options.dlqId);
|
|
126
|
+
replayMeta = {
|
|
127
|
+
replayAttempted: true,
|
|
128
|
+
replaySuccess: replay.success,
|
|
129
|
+
replayedAt: replay.replayedAt,
|
|
130
|
+
replayDlqId: options.dlqId,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
replayMeta = {
|
|
135
|
+
replayAttempted: true,
|
|
136
|
+
replaySuccess: false,
|
|
137
|
+
replayDlqId: options.dlqId,
|
|
138
|
+
replayError: error.message,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return (0, send_alert_1.sendAlert)({
|
|
143
|
+
slack: config.notifications?.slackWebhookUrl
|
|
144
|
+
? { webhookUrl: config.notifications.slackWebhookUrl }
|
|
145
|
+
: undefined,
|
|
146
|
+
discord: config.notifications?.discordWebhookUrl
|
|
147
|
+
? { webhookUrl: config.notifications.discordWebhookUrl }
|
|
148
|
+
: undefined,
|
|
149
|
+
}, {
|
|
150
|
+
...options,
|
|
151
|
+
eventId: options.eventId || options.dlqId,
|
|
152
|
+
metadata: {
|
|
153
|
+
...(options.metadata || {}),
|
|
154
|
+
...replayMeta,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
},
|
|
117
158
|
};
|
|
118
159
|
}
|
package/dist/upstash/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createTernControls } from './controls';
|
|
2
2
|
export { handleQueuedRequest, handleProcess, handleReceive, resolveQueueConfig, } from './queue';
|
|
3
|
-
export type { DLQMessage, EventFilter, QueueOption, QueuedMessage, ReplayResult, ResolvedQueueConfig, TernControlsConfig, TernEvent, } from './types';
|
|
3
|
+
export type { ControlAlertOptions, DLQMessage, EventFilter, QueueOption, QueuedMessage, ReplayResult, ResolvedQueueConfig, TernControls, TernControlsConfig, TernEvent, } from './types';
|
package/dist/upstash/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WebhookPlatform } from '../types';
|
|
2
|
+
import type { SendAlertOptions, SendAlertSummary } from '../notifications/types';
|
|
2
3
|
export type QueueOption = true | {
|
|
3
4
|
token: string;
|
|
4
5
|
signingKey: string;
|
|
@@ -13,6 +14,10 @@ export interface ResolvedQueueConfig {
|
|
|
13
14
|
}
|
|
14
15
|
export interface TernControlsConfig {
|
|
15
16
|
token: string;
|
|
17
|
+
notifications?: {
|
|
18
|
+
slackWebhookUrl?: string;
|
|
19
|
+
discordWebhookUrl?: string;
|
|
20
|
+
};
|
|
16
21
|
}
|
|
17
22
|
export interface DLQMessage {
|
|
18
23
|
id: string;
|
|
@@ -45,3 +50,16 @@ export interface QueuedMessage {
|
|
|
45
50
|
payload: unknown;
|
|
46
51
|
metadata: Record<string, unknown>;
|
|
47
52
|
}
|
|
53
|
+
export type ControlAlertOptions = (SendAlertOptions & {
|
|
54
|
+
dlq: true;
|
|
55
|
+
dlqId: string;
|
|
56
|
+
}) | (SendAlertOptions & {
|
|
57
|
+
dlq?: false;
|
|
58
|
+
dlqId?: never;
|
|
59
|
+
});
|
|
60
|
+
export interface TernControls {
|
|
61
|
+
dlq: () => Promise<DLQMessage[]>;
|
|
62
|
+
replay: (dlqId: string) => Promise<ReplayResult>;
|
|
63
|
+
events: (filter?: EventFilter) => Promise<TernEvent[]>;
|
|
64
|
+
alert: (options?: ControlAlertOptions) => Promise<SendAlertSummary>;
|
|
65
|
+
}
|
package/package.json
CHANGED