@cronwatch/sdk 0.11.1 → 0.12.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.
Files changed (104) hide show
  1. package/README.md +4 -1
  2. package/dist/anthropic.d.cts +1 -1
  3. package/dist/anthropic.d.ts +1 -1
  4. package/dist/bugsnag.cjs +1 -1
  5. package/dist/bugsnag.cjs.map +1 -1
  6. package/dist/bugsnag.d.cts +1 -1
  7. package/dist/bugsnag.d.ts +1 -1
  8. package/dist/bugsnag.js +1 -1
  9. package/dist/bugsnag.js.map +1 -1
  10. package/dist/{client-CRMfhylq.d.cts → client-BkPMRkDq.d.cts} +1 -1
  11. package/dist/{client-D2Xk2600.d.ts → client-DySsV5T0.d.ts} +1 -1
  12. package/dist/d1.d.cts +1 -1
  13. package/dist/d1.d.ts +1 -1
  14. package/dist/datadog.cjs +1 -0
  15. package/dist/datadog.cjs.map +1 -1
  16. package/dist/datadog.d.cts +1 -1
  17. package/dist/datadog.d.ts +1 -1
  18. package/dist/datadog.js +1 -0
  19. package/dist/datadog.js.map +1 -1
  20. package/dist/discord.cjs +1 -0
  21. package/dist/discord.cjs.map +1 -1
  22. package/dist/discord.d.cts +1 -1
  23. package/dist/discord.d.ts +1 -1
  24. package/dist/discord.js +1 -0
  25. package/dist/discord.js.map +1 -1
  26. package/dist/{email-BWuUiCkw.d.ts → email-Bx3ehK0O.d.ts} +1 -1
  27. package/dist/{email-Dcqe3k8n.d.cts → email-CD6NO8CV.d.cts} +1 -1
  28. package/dist/honeybadger.cjs +1 -0
  29. package/dist/honeybadger.cjs.map +1 -1
  30. package/dist/honeybadger.d.cts +1 -1
  31. package/dist/honeybadger.d.ts +1 -1
  32. package/dist/honeybadger.js +1 -0
  33. package/dist/honeybadger.js.map +1 -1
  34. package/dist/index.cjs +66 -6
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +66 -6
  39. package/dist/index.js.map +1 -1
  40. package/dist/mailgun.cjs.map +1 -1
  41. package/dist/mailgun.d.cts +2 -2
  42. package/dist/mailgun.d.ts +2 -2
  43. package/dist/mailgun.js.map +1 -1
  44. package/dist/newrelic.cjs +1 -1
  45. package/dist/newrelic.cjs.map +1 -1
  46. package/dist/newrelic.d.cts +1 -1
  47. package/dist/newrelic.d.ts +1 -1
  48. package/dist/newrelic.js +1 -1
  49. package/dist/newrelic.js.map +1 -1
  50. package/dist/pg-cron.cjs +2 -1
  51. package/dist/pg-cron.cjs.map +1 -1
  52. package/dist/pg-cron.d.cts +2 -2
  53. package/dist/pg-cron.d.ts +2 -2
  54. package/dist/pg-cron.js +2 -1
  55. package/dist/pg-cron.js.map +1 -1
  56. package/dist/postgres.d.cts +1 -1
  57. package/dist/postgres.d.ts +1 -1
  58. package/dist/postmark.cjs.map +1 -1
  59. package/dist/postmark.d.cts +2 -2
  60. package/dist/postmark.d.ts +2 -2
  61. package/dist/postmark.js.map +1 -1
  62. package/dist/resend.cjs.map +1 -1
  63. package/dist/resend.d.cts +2 -2
  64. package/dist/resend.d.ts +2 -2
  65. package/dist/resend.js.map +1 -1
  66. package/dist/rollbar.cjs +1 -1
  67. package/dist/rollbar.cjs.map +1 -1
  68. package/dist/rollbar.d.cts +1 -1
  69. package/dist/rollbar.d.ts +1 -1
  70. package/dist/rollbar.js +1 -1
  71. package/dist/rollbar.js.map +1 -1
  72. package/dist/sendgrid.cjs.map +1 -1
  73. package/dist/sendgrid.d.cts +2 -2
  74. package/dist/sendgrid.d.ts +2 -2
  75. package/dist/sendgrid.js.map +1 -1
  76. package/dist/sentry.cjs +1 -1
  77. package/dist/sentry.cjs.map +1 -1
  78. package/dist/sentry.d.cts +1 -1
  79. package/dist/sentry.d.ts +1 -1
  80. package/dist/sentry.js +1 -1
  81. package/dist/sentry.js.map +1 -1
  82. package/dist/ses.cjs.map +1 -1
  83. package/dist/ses.d.cts +2 -2
  84. package/dist/ses.d.ts +2 -2
  85. package/dist/ses.js.map +1 -1
  86. package/dist/slack.cjs +1 -0
  87. package/dist/slack.cjs.map +1 -1
  88. package/dist/slack.d.cts +1 -1
  89. package/dist/slack.d.ts +1 -1
  90. package/dist/slack.js +1 -0
  91. package/dist/slack.js.map +1 -1
  92. package/dist/sqlite.d.cts +1 -1
  93. package/dist/sqlite.d.ts +1 -1
  94. package/dist/twilio.cjs.map +1 -1
  95. package/dist/twilio.d.cts +1 -1
  96. package/dist/twilio.d.ts +1 -1
  97. package/dist/twilio.js.map +1 -1
  98. package/dist/{types-DLnlUie8.d.cts → types-Bm972Uqj.d.cts} +19 -1
  99. package/dist/{types-DLnlUie8.d.ts → types-Bm972Uqj.d.ts} +19 -1
  100. package/dist/webhook.cjs.map +1 -1
  101. package/dist/webhook.d.cts +1 -1
  102. package/dist/webhook.d.ts +1 -1
  103. package/dist/webhook.js.map +1 -1
  104. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-CRMfhylq.cjs';
2
- export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-CRMfhylq.cjs';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.cjs';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.cjs';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-BkPMRkDq.cjs';
2
+ export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-BkPMRkDq.cjs';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Bm972Uqj.cjs';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-Bm972Uqj.cjs';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.11.1";
14
+ declare const VERSION = "0.12.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-D2Xk2600.js';
2
- export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-D2Xk2600.js';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.js';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.js';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-DySsV5T0.js';
2
+ export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-DySsV5T0.js';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Bm972Uqj.js';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-Bm972Uqj.js';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.11.1";
14
+ declare const VERSION = "0.12.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.js CHANGED
@@ -326,7 +326,7 @@ function isObject(value) {
326
326
  }
327
327
  function normalizeState(state, job) {
328
328
  if (!isObject(state)) return emptyState(job);
329
- const { sending, ...rest } = state;
329
+ const { sending, underFloor, ...rest } = state;
330
330
  const open = isObject(state.open) ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === "number")) : {};
331
331
  const number = (value) => typeof value === "number" ? value : null;
332
332
  const list = (value) => Array.isArray(value) ? value : [];
@@ -339,7 +339,8 @@ function normalizeState(state, job) {
339
339
  lastAlertAt: number(state.lastAlertAt),
340
340
  pendingRecovery: list(state.pendingRecovery).filter((c) => typeof c === "string"),
341
341
  undelivered: list(state.undelivered).filter((alert) => isObject(alert)),
342
- ...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}
342
+ ...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {},
343
+ ...Array.isArray(underFloor) && underFloor.some((m) => typeof m === "string") ? { underFloor: underFloor.filter((m) => typeof m === "string") } : {}
343
344
  };
344
345
  }
345
346
  var MAX_UNDELIVERED = 20;
@@ -439,6 +440,26 @@ function budgetBreaches(def, run, history) {
439
440
  }
440
441
  return breaches;
441
442
  }
443
+ function floorBreaches(def, run, history, previous = []) {
444
+ const breaches = [];
445
+ for (const [metric, value] of Object.entries(run.metrics)) {
446
+ const floor = def.floor?.[metric];
447
+ if (floor !== void 0) {
448
+ if (value < floor) breaches.push({ metric, value, limit: floor, basis: "floor" });
449
+ continue;
450
+ }
451
+ if (value > 0) continue;
452
+ if (previous.includes(metric)) {
453
+ breaches.push({ metric, value, limit: 0, basis: "0 or less on the run before too" });
454
+ continue;
455
+ }
456
+ const past = history.filter((r) => r.status === "ok" && typeof r.metrics[metric] === "number").slice(0, BASELINE_WINDOW).map((r) => r.metrics[metric]);
457
+ if (past.length < BASELINE_MIN_RUNS || !past.every((v) => v > 0)) continue;
458
+ const lowest = Math.min(...past);
459
+ breaches.push({ metric, value, limit: lowest, basis: `the last ${past.length} runs all reported more than 0, the lowest ${formatNumber(lowest)}` });
460
+ }
461
+ return breaches;
462
+ }
442
463
  function hasFullBaseline(history) {
443
464
  return history.filter((r) => r.status === "ok").length >= BASELINE_WINDOW;
444
465
  }
@@ -476,6 +497,16 @@ function onRunFinish(def, run, state, history, now) {
476
497
  } else {
477
498
  closeCondition(next, "over_budget");
478
499
  }
500
+ const short = floorBreaches(def, run, history, next.underFloor);
501
+ if (short.length > 0) {
502
+ next.underFloor = short.map((b) => b.metric);
503
+ if (openCondition(next, "under_floor", now)) {
504
+ alerts.push({ type: "under_floor", run, details: { breaches: short } });
505
+ }
506
+ } else {
507
+ delete next.underFloor;
508
+ closeCondition(next, "under_floor");
509
+ }
479
510
  const pending = next.pendingRecovery ?? [];
480
511
  if (pending.length > 0 && openConditions(next).length === 0) {
481
512
  alerts.push({ type: "recovered", run, details: { after: [...pending] } });
@@ -660,6 +691,14 @@ ${out}`);
660
691
  if (run) lines.push(`Started ${when(run.startedAt, now)}.`);
661
692
  break;
662
693
  }
694
+ case "under_floor": {
695
+ title = `${name} fell short`;
696
+ for (const b of draft.details.breaches) {
697
+ lines.push(b.basis === "floor" ? `${b.metric}: ${formatNumber(b.value)}, below the floor of ${formatNumber(b.limit)}.` : `${b.metric}: ${formatNumber(b.value)} (${b.basis}).`);
698
+ }
699
+ if (run) lines.push(`Started ${when(run.startedAt, now)}.`);
700
+ break;
701
+ }
663
702
  case "recovered": {
664
703
  if (draft.details.reason === "unscheduled") {
665
704
  title = `${name} is no longer scheduled`;
@@ -1078,7 +1117,7 @@ function toneOf(run, job, now) {
1078
1117
  if (run.status === "failed") return "bad";
1079
1118
  if (run.status === "timeout") return "timeout";
1080
1119
  const latest = job.lastRun?.id === run.id;
1081
- return latest && (job.open.includes("over_budget") || job.open.includes("slow")) ? "warn" : "ok";
1120
+ return latest && (job.open.includes("over_budget") || job.open.includes("under_floor") || job.open.includes("slow")) ? "warn" : "ok";
1082
1121
  }
1083
1122
  function timeoutText(job) {
1084
1123
  return safely(() => formatDuration(timeoutMs(job.definition)), "configured");
@@ -1088,13 +1127,17 @@ function describeRun(run, tone, job, now) {
1088
1127
  if (tone === "running") return `running since ${at}, ${formatDuration(now - run.startedAt)} so far`;
1089
1128
  if (tone === "stuck") return `running since ${at}, past its ${timeoutText(job)} timeout`;
1090
1129
  const took = run.durationMs !== null ? `, took ${formatDuration(run.durationMs)}` : "";
1091
- const extra = tone === "warn" ? job.open.includes("over_budget") ? ", over budget" : ", slow" : "";
1130
+ const extra = tone === "warn" ? job.open.includes("over_budget") ? ", over budget" : job.open.includes("under_floor") ? ", under floor" : ", slow" : "";
1092
1131
  return `${run.status} at ${at}${took}${extra}`;
1093
1132
  }
1094
1133
  function overCeilings(job) {
1095
1134
  const metrics = job.lastRun?.metrics ?? {};
1096
1135
  return Object.entries(job.definition.budget ?? {}).filter(([k, limit]) => (metrics[k] ?? -Infinity) > limit).map(([k]) => k);
1097
1136
  }
1137
+ function underFloors(job) {
1138
+ const floors = job.definition.floor ?? {};
1139
+ return Object.entries(job.lastRun?.metrics ?? {}).filter(([k, v]) => floors[k] !== void 0 ? v < floors[k] : v <= 0).map(([k]) => k);
1140
+ }
1098
1141
  function laneNote(job, missed, now) {
1099
1142
  const last = job.lastRun;
1100
1143
  if (job.silencedUntil !== null && job.silencedUntil > now) return `silenced until ${when2(job.silencedUntil, now)}`;
@@ -1110,6 +1153,10 @@ function laneNote(job, missed, now) {
1110
1153
  const over = overCeilings(job);
1111
1154
  return `went over budget${over.length ? ` on ${over.join(" and ")}` : ""} at ${when2(last.startedAt, now)}`;
1112
1155
  }
1156
+ if (job.open.includes("under_floor") && last) {
1157
+ const under = underFloors(job);
1158
+ return `fell short${under.length ? ` on ${under.join(" and ")}` : ""} at ${when2(last.startedAt, now)}`;
1159
+ }
1113
1160
  if (job.open.includes("slow") && last?.durationMs != null) return `slow: took ${formatDuration(last.durationMs)}`;
1114
1161
  if (job.open.includes("failed")) return "failing";
1115
1162
  if (!last && job.nextExpectedAt !== null) return `no runs yet, first due ${when2(job.nextExpectedAt, now)}`;
@@ -1226,7 +1273,7 @@ function legend() {
1226
1273
  [box("run ok"), "ran"],
1227
1274
  [box("run bad"), "failed"],
1228
1275
  [box("run timeout"), "timed out"],
1229
- [box("run warn"), "over budget or slow"],
1276
+ [box("run warn"), "over budget, under floor or slow"],
1230
1277
  [box("run running"), "running"],
1231
1278
  [box("missed"), "missed"]
1232
1279
  ];
@@ -1644,6 +1691,7 @@ function jobPage(job, runs, now, base, complete = true) {
1644
1691
  <dt>Timeout</dt><dd>${h(d.timeout ?? "1h")}</dd>
1645
1692
  ${d.maxDuration ? `<dt>Max duration</dt><dd>${h(d.maxDuration)}</dd>` : ""}
1646
1693
  ${d.budget ? `<dt>Budget</dt><dd>${h(Object.entries(d.budget).map(([k, v]) => `${k} \u2264 ${v}`).join(", "))}</dd>` : ""}
1694
+ ${d.floor ? `<dt>Floor</dt><dd>${h(Object.entries(d.floor).map(([k, v]) => `${k} \u2265 ${v}`).join(", "))}</dd>` : ""}
1647
1695
  ${d.expect ? `<dt>Expect</dt><dd>${h(d.expect)}</dd>` : ""}
1648
1696
  ${d.failuresBeforeAlert && d.failuresBeforeAlert > 1 ? `<dt>Alert after</dt><dd>${h(d.failuresBeforeAlert)} consecutive failures</dd>` : ""}
1649
1697
  ${d.tags?.length ? `<dt>Tags</dt><dd>${d.tags.map((t) => h(t)).join(", ")}</dd>` : ""}
@@ -1661,7 +1709,7 @@ function messagePage(title, message, base, signIn = false) {
1661
1709
  }
1662
1710
 
1663
1711
  // src/version.ts
1664
- var VERSION = "0.11.1";
1712
+ var VERSION = "0.12.0";
1665
1713
  var API_VERSION = 1;
1666
1714
 
1667
1715
  // src/routes/index.ts
@@ -2155,6 +2203,18 @@ function validateDefinition(def) {
2155
2203
  }
2156
2204
  }
2157
2205
  }
2206
+ if (def.floor !== void 0) {
2207
+ if (typeof def.floor !== "object" || def.floor === null) throw new Error(`job "${name}": floor must be an object of { metric: floor }`);
2208
+ for (const [metric, floor] of Object.entries(def.floor)) {
2209
+ if (typeof floor !== "number" || !Number.isFinite(floor)) {
2210
+ throw new Error(`job "${name}": floor.${metric} must be a finite number (got ${String(floor)})`);
2211
+ }
2212
+ const ceiling = def.budget?.[metric];
2213
+ if (ceiling !== void 0 && floor > ceiling) {
2214
+ throw new Error(`job "${name}": floor.${metric} (${floor}) is above budget.${metric} (${ceiling}), so every run would alert`);
2215
+ }
2216
+ }
2217
+ }
2158
2218
  if (def.expect !== void 0 && typeof def.expect !== "string" && !(def.expect instanceof RegExp) && typeof def.expect !== "function") {
2159
2219
  throw new Error(`job "${name}": expect must be a string, a RegExp or a function`);
2160
2220
  }