@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 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
- function defaultFormat(err, ctx) {
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
- const { minStatusCode = 500 } = options;
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 message = options.formatMessage?.(err, ctx) ?? defaultFormat(err, ctx);
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