@cronwatch/sdk 0.14.0 → 0.14.2

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 (71) hide show
  1. package/dist/bugsnag.cjs +19 -4
  2. package/dist/bugsnag.cjs.map +1 -1
  3. package/dist/bugsnag.js +19 -4
  4. package/dist/bugsnag.js.map +1 -1
  5. package/dist/datadog.cjs +19 -4
  6. package/dist/datadog.cjs.map +1 -1
  7. package/dist/datadog.js +19 -4
  8. package/dist/datadog.js.map +1 -1
  9. package/dist/discord.cjs +27 -9
  10. package/dist/discord.cjs.map +1 -1
  11. package/dist/discord.js +27 -9
  12. package/dist/discord.js.map +1 -1
  13. package/dist/honeybadger.cjs +19 -4
  14. package/dist/honeybadger.cjs.map +1 -1
  15. package/dist/honeybadger.js +19 -4
  16. package/dist/honeybadger.js.map +1 -1
  17. package/dist/index.cjs +45 -22
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +45 -22
  22. package/dist/index.js.map +1 -1
  23. package/dist/mailgun.cjs +19 -4
  24. package/dist/mailgun.cjs.map +1 -1
  25. package/dist/mailgun.js +19 -4
  26. package/dist/mailgun.js.map +1 -1
  27. package/dist/newrelic.cjs +19 -4
  28. package/dist/newrelic.cjs.map +1 -1
  29. package/dist/newrelic.js +19 -4
  30. package/dist/newrelic.js.map +1 -1
  31. package/dist/pg-cron.cjs +7 -1
  32. package/dist/pg-cron.cjs.map +1 -1
  33. package/dist/pg-cron.js +7 -1
  34. package/dist/pg-cron.js.map +1 -1
  35. package/dist/postmark.cjs +19 -4
  36. package/dist/postmark.cjs.map +1 -1
  37. package/dist/postmark.js +19 -4
  38. package/dist/postmark.js.map +1 -1
  39. package/dist/resend.cjs +19 -4
  40. package/dist/resend.cjs.map +1 -1
  41. package/dist/resend.js +19 -4
  42. package/dist/resend.js.map +1 -1
  43. package/dist/rollbar.cjs +19 -4
  44. package/dist/rollbar.cjs.map +1 -1
  45. package/dist/rollbar.js +19 -4
  46. package/dist/rollbar.js.map +1 -1
  47. package/dist/sendgrid.cjs +19 -4
  48. package/dist/sendgrid.cjs.map +1 -1
  49. package/dist/sendgrid.js +19 -4
  50. package/dist/sendgrid.js.map +1 -1
  51. package/dist/sentry.cjs +19 -4
  52. package/dist/sentry.cjs.map +1 -1
  53. package/dist/sentry.js +19 -4
  54. package/dist/sentry.js.map +1 -1
  55. package/dist/ses.cjs +19 -4
  56. package/dist/ses.cjs.map +1 -1
  57. package/dist/ses.js +19 -4
  58. package/dist/ses.js.map +1 -1
  59. package/dist/slack.cjs +27 -9
  60. package/dist/slack.cjs.map +1 -1
  61. package/dist/slack.js +27 -9
  62. package/dist/slack.js.map +1 -1
  63. package/dist/twilio.cjs +19 -4
  64. package/dist/twilio.cjs.map +1 -1
  65. package/dist/twilio.js +19 -4
  66. package/dist/twilio.js.map +1 -1
  67. package/dist/webhook.cjs +29 -7
  68. package/dist/webhook.cjs.map +1 -1
  69. package/dist/webhook.js +29 -7
  70. package/dist/webhook.js.map +1 -1
  71. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -722,7 +722,7 @@ function onCheck(def, stored, lastRun, state, now) {
722
722
  return { state: next, alerts, nextExpectedAt, dueAt: exp.dueAt };
723
723
  }
724
724
  function isStuck(def, run, now) {
725
- return run.status === "running" && now - run.startedAt > timeoutMs(def);
725
+ return run.status === "running" && run.finishedAt === null && now - run.startedAt > timeoutMs(def);
726
726
  }
727
727
  function muteOpens(previous, next) {
728
728
  const muted = cloneState(next);
@@ -993,11 +993,15 @@ function stringify(part) {
993
993
  }
994
994
  }
995
995
  function createRecorder(run) {
996
+ const window = OUTPUT_CAP + REDACT_EDGE + 2;
997
+ const copy = (text) => JSON.parse(JSON.stringify(text));
996
998
  const lines = [];
997
999
  let size = 0;
998
1000
  const head = [];
999
1001
  let headSize = 0;
1000
- let dropped = false;
1002
+ let whole = "";
1003
+ let tail2 = "";
1004
+ let logged = false;
1001
1005
  const metrics = {};
1002
1006
  const controller = new AbortController();
1003
1007
  const context = {
@@ -1006,16 +1010,23 @@ function createRecorder(run) {
1006
1010
  startedAt: run.startedAt,
1007
1011
  signal: controller.signal,
1008
1012
  log(...parts) {
1009
- const line = parts.map(stringify).join(" ");
1013
+ let line = parts.map(stringify).join(" ");
1014
+ const separator = logged ? "\n" : "";
1015
+ logged = true;
1016
+ if (whole !== null) whole = whole.length + separator.length + line.length <= 2 * OUTPUT_CAP ? whole + separator + line : null;
1017
+ tail2 = copy((tail2 + separator + copy(line.slice(-OUTPUT_CAP))).slice(-OUTPUT_CAP));
1010
1018
  if (headSize < OUTPUT_CAP) {
1011
- head.push(line);
1019
+ head.push(copy(line.slice(0, OUTPUT_CAP - headSize)));
1012
1020
  headSize += line.length + 1;
1013
1021
  }
1022
+ line = stripNul(line);
1023
+ if (line.length >= window) {
1024
+ line = copy(line.slice(-68609));
1025
+ }
1014
1026
  lines.push(line);
1015
1027
  size += line.length + 1;
1016
- while (size > 64 * 1024 && lines.length > 1) {
1028
+ while (size > window && lines.length > 1) {
1017
1029
  size -= lines.shift().length + 1;
1018
- dropped = true;
1019
1030
  }
1020
1031
  },
1021
1032
  metric(name, value) {
@@ -1032,10 +1043,8 @@ function createRecorder(run) {
1032
1043
  context,
1033
1044
  output: () => lines.length === 0 ? null : lines.join("\n"),
1034
1045
  expectText: () => {
1035
- if (lines.length === 0) return null;
1036
- const all = lines.join("\n");
1037
- if (!dropped && all.length <= 2 * OUTPUT_CAP) return all;
1038
- return head.join("\n").slice(0, OUTPUT_CAP) + "\n" + all.slice(-OUTPUT_CAP);
1046
+ if (!logged) return null;
1047
+ return whole ?? head.join("\n").slice(0, OUTPUT_CAP) + "\n" + tail2;
1039
1048
  },
1040
1049
  metrics: () => ({ ...metrics }),
1041
1050
  abort: () => controller.abort()
@@ -1209,7 +1218,7 @@ function staticAsset(path, base) {
1209
1218
  }
1210
1219
 
1211
1220
  // src/version.ts
1212
- var VERSION = "0.14.0";
1221
+ var VERSION = "0.14.2";
1213
1222
  var API_VERSION = 1;
1214
1223
 
1215
1224
  // src/routes/timeline.ts
@@ -1999,6 +2008,7 @@ function stripBase(pathname, base) {
1999
2008
  }
2000
2009
  var MAX_REQUEST_BODY = 1 << 20;
2001
2010
  var TOO_LARGE = /* @__PURE__ */ Symbol("too large");
2011
+ var BAD_BODY = /* @__PURE__ */ Symbol("malformed body");
2002
2012
  async function readLimited(request) {
2003
2013
  const length = Number(request.headers.get("content-length"));
2004
2014
  if (Number.isFinite(length) && length > MAX_REQUEST_BODY) return TOO_LARGE;
@@ -2045,14 +2055,14 @@ async function readBody(request) {
2045
2055
  try {
2046
2056
  if (json2) {
2047
2057
  const data = await copy.json();
2048
- return typeof data === "object" && data !== null ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) : {};
2058
+ return typeof data === "object" && data !== null && !Array.isArray(data) ? Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)])) : BAD_BODY;
2049
2059
  }
2050
2060
  const fields = await copy.formData();
2051
2061
  const out = {};
2052
2062
  for (const [k, v] of fields.entries()) out[k] = String(v);
2053
2063
  return out;
2054
2064
  } catch {
2055
- return {};
2065
+ return BAD_BODY;
2056
2066
  }
2057
2067
  }
2058
2068
  function crossSite(request, publicOrigin) {
@@ -2118,6 +2128,7 @@ function buildRoutes(cw, options = {}) {
2118
2128
  const signInPage = () => generated ? html(messagePage("Sign in", "CRONWATCH_TOKEN is not set, so this development server made a token. The sign-in link is in the server log: open it once, or enter the token from it below, and this browser stays signed in.", base, true), 401) : html(messagePage("Sign in", "Enter your CRONWATCH_TOKEN and this browser stays signed in.", base, true), 401);
2119
2129
  if (token && method === "POST" && path === "/signin") {
2120
2130
  const body = await readBody(request);
2131
+ if (body === BAD_BODY) return html(messagePage("Bad request", "Malformed request body", base), 400);
2121
2132
  const sent = body === TOO_LARGE ? void 0 : body.token;
2122
2133
  if (sent === void 0 || !constantTimeEqual(sent, token)) return signInPage();
2123
2134
  return redirect(signInReturn(request.headers.get("referer"), publicOrigin, base), { "set-cookie": await signInCookie() });
@@ -2175,6 +2186,7 @@ function buildRoutes(cw, options = {}) {
2175
2186
  if (parts[2] === "silence") {
2176
2187
  const body = await readBody(request);
2177
2188
  if (body === TOO_LARGE) return html(messagePage("Not silenced", "The request was too large.", base), 413);
2189
+ if (body === BAD_BODY) return html(messagePage("Not silenced", "Malformed request body", base), 400);
2178
2190
  let duration;
2179
2191
  try {
2180
2192
  duration = silenceDuration(body.for);
@@ -2212,6 +2224,7 @@ function buildRoutes(cw, options = {}) {
2212
2224
  if (rest[2] === "silence") {
2213
2225
  const body = await readBody(request);
2214
2226
  if (body === TOO_LARGE) return api({ ok: false, error: "Request body too large" }, 413);
2227
+ if (body === BAD_BODY) return api({ ok: false, error: "Malformed request body" }, 400);
2215
2228
  let duration;
2216
2229
  try {
2217
2230
  duration = silenceDuration(body.for ?? url.searchParams.get("for"));
@@ -2847,9 +2860,10 @@ var Cronwatch = class {
2847
2860
  if (stored.job !== run.job) return `belongs to job "${stored.job}"`;
2848
2861
  }
2849
2862
  }
2863
+ const tracked = (await this.readState(run.job)).pendingRuns?.includes(run.id) ?? false;
2850
2864
  const claim = await this.claimFinish(run);
2851
2865
  if ("ignored" in claim) return claim.ignored;
2852
- if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt);
2866
+ if (!claim.lateAfterTimeout || run.status === "ok") await this.finishRun(toStored(definition), run, finishedAt, tracked && !claim.lateAfterTimeout);
2853
2867
  return null;
2854
2868
  }
2855
2869
  /** A conditional write (Store.updateRunIf), or for a store without one, a read then a plain write. */
@@ -3134,17 +3148,16 @@ ${id}`;
3134
3148
  const definition = toStored(declared);
3135
3149
  const stored = await this.store.getRun(run.id);
3136
3150
  if (stored) return this.recordOver(definition, stored, run, evaluate);
3151
+ const inserted = evaluate ? { ...run, status: "running" } : run;
3137
3152
  try {
3138
- await this.store.insertRun(run);
3153
+ await this.store.insertRun(inserted);
3139
3154
  } catch (e) {
3140
3155
  const again = await this.store.getRun(run.id).catch(() => null);
3141
3156
  if (again) return this.recordOver(definition, again, run, evaluate);
3142
3157
  throw e;
3143
3158
  }
3144
3159
  if (!evaluate) return [];
3145
- await this.updateState(run.job, (before) => ({ state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 }));
3146
- if (run.status === "running") return [];
3147
- return this.finishRun(definition, run, this.now());
3160
+ return this.recordOver(definition, inserted, run, true);
3148
3161
  }
3149
3162
  /** recordRun() for a run already stored. */
3150
3163
  async recordOver(definition, stored, run, evaluate) {
@@ -3152,7 +3165,16 @@ ${id}`;
3152
3165
  this.report(new Error(`run ${run.id} of ${run.job} belongs to job "${stored.job}"; ignored`), `recording ${run.job}`);
3153
3166
  return [];
3154
3167
  }
3155
- if (stored.status !== "running" && stored.status !== "timeout" || run.status === "running") return [];
3168
+ if (stored.status !== "running" && stored.status !== "timeout") return [];
3169
+ if (evaluate && stored.status === "running") {
3170
+ await this.updateState(run.job, async (before) => {
3171
+ const current = await this.store.getRun(run.id);
3172
+ if (!current || current.job !== run.job || current.status !== "running" || before.pendingRuns?.includes(run.id)) return { state: before, result: void 0 };
3173
+ return { state: holdRun(onRunStart(definition, run, before, this.now()), run.id), result: void 0 };
3174
+ });
3175
+ }
3176
+ if (run.status === "running") return [];
3177
+ const tracked = (await this.readState(run.job)).pendingRuns?.includes(run.id) ?? false;
3156
3178
  const claim = await this.claimFinish(run);
3157
3179
  if ("ignored" in claim) {
3158
3180
  this.report(new Error(`run ${run.id} of ${run.job} ${claim.ignored}; ignored`), `recording ${run.job}`);
@@ -3163,18 +3185,19 @@ ${id}`;
3163
3185
  return [];
3164
3186
  }
3165
3187
  if (claim.lateAfterTimeout && run.status !== "ok") return [];
3166
- return this.finishRun(definition, run, this.now());
3188
+ return this.finishRun(definition, run, this.now(), tracked && !claim.lateAfterTimeout);
3167
3189
  }
3168
3190
  /**
3169
3191
  * Evaluate a finished run (ok, failed, or timed out by a check), already
3170
3192
  * written, against the job's state and send what that produces. The
3171
3193
  * alerts are written with that state (see outbox()). Never throws.
3172
3194
  */
3173
- async finishRun(definition, run, now) {
3195
+ async finishRun(definition, run, now, pendingOnly = false) {
3174
3196
  let held;
3175
3197
  try {
3176
3198
  let history = null;
3177
3199
  ({ result: held } = await this.updateState(run.job, async (previous) => {
3200
+ if (pendingOnly && !(previous.pendingRuns ?? []).includes(run.id)) return { state: previous, result: { alerts: [], dropped: 0 } };
3178
3201
  history ??= await this.history(run);
3179
3202
  const before = releaseRun(previous, run.id);
3180
3203
  return this.outbox(applySilence(before, onRunFinish(definition, run, before, history, now), now), definition, now);
@@ -3319,7 +3342,7 @@ ${id}`;
3319
3342
  continue;
3320
3343
  }
3321
3344
  if (run.status === "running" || typeof run.finishedAt !== "number" || run.finishedAt > now - SEND_LEASE_MS) continue;
3322
- alerts.push(...await this.finishRun(stored.definition, run, now));
3345
+ alerts.push(...await this.finishRun(stored.definition, run, now, true));
3323
3346
  } catch (e) {
3324
3347
  this.onError(e, `evaluating run ${id} of ${stored.name}`);
3325
3348
  }