@spfn/notification 0.1.0-beta.2 → 0.1.0-beta.4
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/dist/server.d.ts +22 -0
- package/dist/server.js +32 -6
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.d.ts
CHANGED
|
@@ -736,6 +736,12 @@ interface ErrorSlackOptions {
|
|
|
736
736
|
* @default 500
|
|
737
737
|
*/
|
|
738
738
|
minStatusCode?: number;
|
|
739
|
+
/**
|
|
740
|
+
* Throttle window in milliseconds.
|
|
741
|
+
* Duplicate errors (same name + statusCode + path) within this window are suppressed.
|
|
742
|
+
* @default 60_000
|
|
743
|
+
*/
|
|
744
|
+
throttleMs?: number;
|
|
739
745
|
/**
|
|
740
746
|
* Webhook URL override (defaults to env/config)
|
|
741
747
|
*/
|
|
@@ -752,6 +758,22 @@ interface ErrorSlackOptions {
|
|
|
752
758
|
* Create an onError callback that sends Slack notifications
|
|
753
759
|
*
|
|
754
760
|
* Returns a function matching ErrorHandler's onError signature.
|
|
761
|
+
* Duplicate errors (same name + statusCode + path) within `throttleMs` are suppressed.
|
|
762
|
+
*
|
|
763
|
+
* @deprecated Use `createMonitorErrorHandler()` from `@spfn/monitor/server` instead.
|
|
764
|
+
* It provides DB-backed error tracking with fingerprint deduplication,
|
|
765
|
+
* state-based notifications (new/reopened), and an admin dashboard.
|
|
766
|
+
*
|
|
767
|
+
* Migration:
|
|
768
|
+
* ```typescript
|
|
769
|
+
* // Before
|
|
770
|
+
* import { createErrorSlackNotifier } from '@spfn/notification/server';
|
|
771
|
+
* middleware: { onError: createErrorSlackNotifier() }
|
|
772
|
+
*
|
|
773
|
+
* // After
|
|
774
|
+
* import { createMonitorErrorHandler } from '@spfn/monitor/server';
|
|
775
|
+
* middleware: { onError: createMonitorErrorHandler() }
|
|
776
|
+
* ```
|
|
755
777
|
*/
|
|
756
778
|
declare function createErrorSlackNotifier(options?: ErrorSlackOptions): (err: Error, ctx: ErrorContext) => Promise<void>;
|
|
757
779
|
|
package/dist/server.js
CHANGED
|
@@ -3879,6 +3879,7 @@ var notificationJobRouter = defineJobRouter({
|
|
|
3879
3879
|
});
|
|
3880
3880
|
|
|
3881
3881
|
// src/integrations/error-slack.ts
|
|
3882
|
+
import { hostname } from "os";
|
|
3882
3883
|
function formatHeaders(headers) {
|
|
3883
3884
|
const entries = Object.entries(headers);
|
|
3884
3885
|
if (entries.length === 0) {
|
|
@@ -3900,9 +3901,21 @@ function shortStack(err, maxLines = 3) {
|
|
|
3900
3901
|
const lines = err.stack.split("\n").slice(1);
|
|
3901
3902
|
return lines.slice(0, maxLines).map((line) => line.trim()).join("\n");
|
|
3902
3903
|
}
|
|
3903
|
-
|
|
3904
|
+
var throttleMap = /* @__PURE__ */ new Map();
|
|
3905
|
+
function throttleKey(err, ctx) {
|
|
3906
|
+
return `${err.name}:${ctx.statusCode}:${ctx.path}`;
|
|
3907
|
+
}
|
|
3908
|
+
function getEnvLabel() {
|
|
3909
|
+
const env2 = process.env.NODE_ENV || "unknown";
|
|
3910
|
+
const host = hostname();
|
|
3911
|
+
const dbUrl = process.env.DATABASE_URL || "";
|
|
3912
|
+
const dbName = dbUrl.match(/\/([^/?]+)(\?|$)/)?.[1] || "(unknown)";
|
|
3913
|
+
return `${env2} | ${host} | db:${dbName}`;
|
|
3914
|
+
}
|
|
3915
|
+
function defaultFormat(err, ctx, suppressed = 0) {
|
|
3916
|
+
const envLabel = getEnvLabel();
|
|
3904
3917
|
const emoji = ctx.statusCode >= 500 ? ":rotating_light:" : ":warning:";
|
|
3905
|
-
const title = `${emoji} *${err.name || "Error"}* \u2014 ${ctx.statusCode}`;
|
|
3918
|
+
const title = `${emoji} *${err.name || "Error"}* \u2014 ${ctx.statusCode} [${envLabel}]`;
|
|
3906
3919
|
const fields = [
|
|
3907
3920
|
{ type: "mrkdwn", text: `*Method*
|
|
3908
3921
|
${ctx.method}` },
|
|
@@ -3929,11 +3942,12 @@ ${ctx.requestId ?? "(none)"}` }
|
|
|
3929
3942
|
type: "section",
|
|
3930
3943
|
fields
|
|
3931
3944
|
},
|
|
3932
|
-
// Timestamp
|
|
3945
|
+
// Timestamp + suppressed count
|
|
3933
3946
|
{
|
|
3934
3947
|
type: "context",
|
|
3935
3948
|
elements: [
|
|
3936
|
-
{ type: "mrkdwn", text: `*Time:* ${ctx.timestamp}` }
|
|
3949
|
+
{ type: "mrkdwn", text: `*Time:* ${ctx.timestamp}` },
|
|
3950
|
+
...suppressed > 0 ? [{ type: "mrkdwn", text: `_+${suppressed} suppressed since last notification_` }] : []
|
|
3937
3951
|
]
|
|
3938
3952
|
},
|
|
3939
3953
|
{ type: "divider" },
|
|
@@ -3960,12 +3974,24 @@ ${ctx.requestId ?? "(none)"}` }
|
|
|
3960
3974
|
return { text: title, blocks };
|
|
3961
3975
|
}
|
|
3962
3976
|
function createErrorSlackNotifier(options = {}) {
|
|
3963
|
-
|
|
3977
|
+
console.warn(
|
|
3978
|
+
"[@spfn/notification] createErrorSlackNotifier() is deprecated. Use createMonitorErrorHandler() from @spfn/monitor/server instead."
|
|
3979
|
+
);
|
|
3980
|
+
const { minStatusCode = 500, throttleMs = 6e4 } = options;
|
|
3964
3981
|
return async (err, ctx) => {
|
|
3965
3982
|
if (ctx.statusCode < minStatusCode) {
|
|
3966
3983
|
return;
|
|
3967
3984
|
}
|
|
3968
|
-
const
|
|
3985
|
+
const key = throttleKey(err, ctx);
|
|
3986
|
+
const now = Date.now();
|
|
3987
|
+
const entry = throttleMap.get(key);
|
|
3988
|
+
if (entry && now - entry.lastSent < throttleMs) {
|
|
3989
|
+
entry.suppressed++;
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
const suppressed = entry?.suppressed ?? 0;
|
|
3993
|
+
throttleMap.set(key, { lastSent: now, suppressed: 0 });
|
|
3994
|
+
const message = options.formatMessage?.(err, ctx) ?? defaultFormat(err, ctx, suppressed);
|
|
3969
3995
|
await sendSlack({
|
|
3970
3996
|
...message,
|
|
3971
3997
|
webhookUrl: options.webhookUrl
|