tracksiy 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -125,6 +125,7 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
125
125
  | `monitors list` | Every monitor of the organization with its state, including monitors no project owns. | read | `monitors_list` |
126
126
  | `monitors declare` | Declare monitors for a project up front, idempotently: heartbeats for jobs, HTTP checks, TLS, ingest silence. | write | `monitors_declare` |
127
127
  | `monitors delete <slug>` | Remove one of a project's monitors by the slug it was declared with. | **destructive** | `monitors_delete` |
128
+ | `monitors deleted` | Monitors deleted from this project: what each was, its last check, when and by whom. | read | `monitors_deleted` |
128
129
  | `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
129
130
  | `queries` | Which database queries and outbound HTTP calls make this project slow, with N+1 detection and the routes that run them. | read | `queries` |
130
131
  | `not-found` | Which pages answer 404, and who links to them: broken internal links first. | read | `not_found` |
package/dist/tracksiy.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync2 } from "node:fs";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = true ? "0.6.0" : "0.0.0-dev";
7
+ var VERSION = true ? "0.7.0" : "0.0.0-dev";
8
8
  var USER_AGENT = `tracksiy-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
9
9
 
10
10
  // src/core/errors.ts
@@ -846,7 +846,7 @@ var monitorsDeclare = {
846
846
  idempotent: true,
847
847
  endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/monitors" },
848
848
  flags: {
849
- heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s)." },
849
+ heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s). `slug=seconds` works too." },
850
850
  get: { type: "list", value: "<slug>=<url>", description: "HTTP GET check expecting 200." },
851
851
  ping: { type: "list", value: "<slug>=<url>", description: "Reachability check." },
852
852
  ssl: { type: "list", value: "<slug>=<url>", description: "TLS certificate check (expiry is reported on every check)." },
@@ -871,8 +871,9 @@ var monitorsDeclare = {
871
871
  if (!body) {
872
872
  const monitors = [];
873
873
  for (const h of i.heartbeat ?? []) {
874
- const [slug, interval] = String(h).split(":");
875
- monitors.push(compact({ slug, type: "heartbeat", intervalSeconds: interval ? Number(interval) : void 0 }));
874
+ const m = /^([^:=\s]+)(?:[:=](\d+))?$/.exec(String(h).trim());
875
+ if (!m) throw usageError("bad_value", `--heartbeat expects <slug>[:<interval-seconds>], got ${JSON.stringify(h)}`, "For example --heartbeat nightly-backup:93600 (or nightly-backup=93600).");
876
+ monitors.push(compact({ slug: m[1], type: "heartbeat", intervalSeconds: m[2] ? Number(m[2]) : void 0 }));
876
877
  }
877
878
  for (const type of ["get", "ping", "ssl"]) {
878
879
  for (const item of i[type] ?? []) {
@@ -915,6 +916,24 @@ var monitorsDelete = {
915
916
  async run(ctx) {
916
917
  requireYes(ctx, `This deletes monitor ${ctx.input.slug} and its history.`);
917
918
  return ctx.api.send("DELETE", await projectPath(ctx, `monitors/${seg(ctx.input.slug)}${ctx.input.type ? `?type=${seg(ctx.input.type)}` : ""}`));
919
+ },
920
+ output: "{id, slug, type, url, intervalSeconds, lastCheck, lastIsUp, deleted, via, actor}: what was deleted; kept in `monitors deleted`."
921
+ };
922
+ var monitorsDeleted = {
923
+ path: ["monitors", "deleted"],
924
+ summary: "Monitors deleted from this project: what each was, its last check, when and by whom.",
925
+ context: "project",
926
+ effect: "read",
927
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/monitor-deletions" },
928
+ flags: {
929
+ since: { type: "string", value: TIME, description: "Only deletions after this." },
930
+ limit: { type: "integer", value: "<n>", description: "How many (default 50)." }
931
+ },
932
+ examples: [{ command: "tracksiy monitors deleted --since 7d", description: "What `--prune`, `monitors delete` or the dashboard removed this week." }],
933
+ output: "{deleted:[{id, slug, type, url, intervalSeconds, lastCheck, lastIsUp, deleted, via, actor}]}",
934
+ text: (d) => table(d.deleted ?? [], ["slug", "type", "deleted", ["by", (r) => r.actor ?? r.via ?? ""], ["last check", (r) => r.lastCheck ? `${r.lastCheck}${r.lastIsUp === false ? " (down)" : ""}` : "never"]]),
935
+ async run(ctx) {
936
+ return ctx.api.get(await projectPath(ctx, "monitor-deletions"), { since: since(ctx.input.since), limit: ctx.input.limit });
918
937
  }
919
938
  };
920
939
  var performance = {
@@ -1054,15 +1073,16 @@ var settingsSet = {
1054
1073
  flags: {
1055
1074
  environments: { type: "list", value: "<env,...>", description: "The environments this project reports from (empty string: any)." },
1056
1075
  "environment-policy": { type: "string", enum: ["flag", "drop"], description: "What happens to an event from another environment." },
1057
- "source-root": { type: "list", value: "<path>", description: "A deployed-code prefix to strip from frame paths. Repeatable; replaces the list." },
1058
- "fingerprint-rule": { type: "list", value: "type=<glob>,message=<glob>,fingerprint=<name>", description: "Group matching events into one issue. Repeatable; replaces the list." },
1059
- "privacy-rule": { type: "list", value: "dropHeader=<name> | mask=<email|iban|card_number|tckn|jwt>,field=<path|*>", description: "Scrub before storage. Repeatable; replaces the list." },
1076
+ "source-root": { type: "list", split: false, value: "<path> | strip=<path>,prepend=<dir/>[,surface=<surface>]", description: "How frame paths become repo paths: a prefix to strip, or strip + prepend (for an SDK that already sends /app/... of a repo's backend/), optionally for one surface. Repeatable; replaces the list." },
1077
+ "fingerprint-rule": { type: "list", split: false, value: "type=<glob>,message=<glob>,fingerprint=<name>", description: "Group matching events into one issue. Repeatable; replaces the list." },
1078
+ "privacy-rule": { type: "list", split: false, value: "dropHeader=<name> | mask=<email|iban|card_number|tckn|jwt>,field=<path|*>", description: "Scrub before storage. Repeatable; replaces the list." },
1060
1079
  body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
1061
1080
  },
1062
1081
  examples: [
1063
1082
  { command: "tracksiy settings set --environments production,staging --environment-policy drop", description: "local and local1 stop reaching Tracksiy." },
1064
1083
  { command: "tracksiy settings set --fingerprint-rule 'type=ViewException,message=syntax error*,fingerprint=blade-syntax'", description: "Every half-written template is one issue." },
1065
- { command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." }
1084
+ { command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." },
1085
+ { command: "tracksiy settings set --source-root 'strip=/,prepend=backend/,surface=backend'", description: "Laravel sends /app/Http/...; the repo has it under backend/." }
1066
1086
  ],
1067
1087
  output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
1068
1088
  async run(ctx) {
@@ -1071,7 +1091,7 @@ var settingsSet = {
1071
1091
  if (i.body) return ctx.api.send("PUT", path, i.body);
1072
1092
  const current = await ctx.api.get(path);
1073
1093
  const next = {
1074
- sourceRoots: i.sourceRoot ?? current.sourceRoots ?? [],
1094
+ sourceRoots: i.sourceRoot ? i.sourceRoot.map((r) => /^\s*strip=/.test(r) ? pairs([r], "source-root")[0] : r) : current.sourceRoots ?? [],
1075
1095
  environments: i.environments ? i.environments.filter((e) => e) : current.environments ?? [],
1076
1096
  environmentPolicy: i.environmentPolicy ?? current.environmentPolicy ?? "flag",
1077
1097
  fingerprintRules: pairs(i.fingerprintRule, "fingerprint-rule") ?? current.fingerprintRules ?? [],
@@ -1087,7 +1107,7 @@ var privacyPreview = {
1087
1107
  effect: "read",
1088
1108
  endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/settings/privacy-preview" },
1089
1109
  flags: {
1090
- "privacy-rule": { type: "list", value: "dropHeader=<name> | mask=<shape>,field=<path|*>", description: "Rules to try (default: the saved ones)." }
1110
+ "privacy-rule": { type: "list", split: false, value: "dropHeader=<name> | mask=<shape>,field=<path|*>", description: "Rules to try (default: the saved ones)." }
1091
1111
  },
1092
1112
  examples: [{ command: "tracksiy privacy-preview --privacy-rule 'mask=email,field=request.headers.User-Agent'", description: "Would this clear the PII check?" }],
1093
1113
  output: "{sampled, rules[{rule, events, values}], remaining[{pattern, field, events}]}",
@@ -1096,7 +1116,7 @@ var privacyPreview = {
1096
1116
  return ctx.api.send("POST", await projectPath(ctx, "settings/privacy-preview"), rules ? { privacyRules: rules } : {});
1097
1117
  }
1098
1118
  };
1099
- var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
1119
+ var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, monitorsDeleted, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
1100
1120
 
1101
1121
  // src/commands/issues.ts
1102
1122
  import { writeFileSync } from "node:fs";
@@ -1169,15 +1189,17 @@ var list = {
1169
1189
  frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." },
1170
1190
  unclaimed: { type: "boolean", description: "Only issues nobody holds a live claim on: what you can pick up." },
1171
1191
  cursor: { type: "string", value: "<nextCursor>", description: "Continue from the previous page's nextCursor (same filters and sort)." },
1172
- "claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." }
1192
+ "claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." },
1193
+ tags: { type: "string", value: "<key,...>", description: "Not a filter: each issue carries these tag keys' most frequent values in topTags (at most 10 keys)." }
1173
1194
  },
1174
1195
  examples: [
1175
1196
  { command: "tracksiy issues list --since 24h", description: "Unresolved issues seen in the last day." },
1197
+ { command: "tracksiy issues list --tags endpoint,client_error_kind --select issues.id,issues.title,issues.topTags", description: "What each mobile issue hit, without opening every one." },
1176
1198
  { command: "tracksiy issues list --unclaimed --limit 5", description: "Five issues no other agent is working on." },
1177
1199
  { command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
1178
1200
  { command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
1179
1201
  ],
1180
- output: "{project, issueCount, total, hasMore, nextCursor, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, frames?}]}",
1202
+ output: "{project, issueCount, total, hasMore, nextCursor, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, latestEvent{release, environment, platform, exceptionType}, frames?, topTags?}]}",
1181
1203
  text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
1182
1204
  ${issueRows(d.issues)}`,
1183
1205
  async run(ctx) {
@@ -1188,7 +1210,8 @@ ${issueRows(d.issues)}`,
1188
1210
  frames: ctx.input.frames,
1189
1211
  unclaimed: ctx.input.unclaimed || void 0,
1190
1212
  claimedBy: ctx.input.claimedBy,
1191
- cursor: ctx.input.cursor
1213
+ cursor: ctx.input.cursor,
1214
+ tags: ctx.input.tags
1192
1215
  });
1193
1216
  }
1194
1217
  };
@@ -1199,9 +1222,15 @@ var view = {
1199
1222
  effect: "read",
1200
1223
  endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}" },
1201
1224
  args: [{ name: "id", required: true, description: "Issue id (numeric, from `issues list`)." }],
1202
- flags: { frames: { type: "integer", value: "<n>", description: "How many frames to include." } },
1203
- examples: [{ command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." }],
1204
- output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, resolution?, staleEvents, claimedBy?, latestEvent{release, environment, exceptionType, exceptionValue, contexts}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, activity[], recentComments[], permalink}",
1225
+ flags: {
1226
+ frames: { type: "integer", value: "<n>", description: "How many frames to include." },
1227
+ breadcrumbs: { type: "integer", value: "<n>", description: "Breadcrumbs of the latest event (default 20, at most 50, 0 for none): what happened just before it." }
1228
+ },
1229
+ examples: [
1230
+ { command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." },
1231
+ { command: "tracksiy issues view 4812 --select latestEvent.breadcrumbs", description: "The requests and navigations that led up to it." }
1232
+ ],
1233
+ output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, resolution?, staleEvents, claimedBy?, latestEvent{release, environment, platform, exceptionType, exceptionValue, commit, contexts, breadcrumbs[{timestamp, type, category, level, message, data}]}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, activity[], recentComments[], permalink}",
1205
1234
  text: (d) => {
1206
1235
  const head = kv(d, ["id", "shortId", "title", "culprit", "level", "status", "count", "eventsLast24h", "eventsLast7d", "firstSeen", "lastSeen", "regressedAt", "permalink"]);
1207
1236
  const ev = d.latestEvent ? `
@@ -1214,14 +1243,17 @@ resolved ${d.resolution.type.replace(/_/g, " ")}: ${d.resolution.release ?? d.re
1214
1243
  const claim2 = d.claimedBy ? `
1215
1244
  claimed by ${d.claimedBy} until ${d.claimedUntil}` : "";
1216
1245
  const act = (d.activity ?? []).slice(0, 5).map((a) => ` ${ago(a.at)} ${a.action}${a.reason ? ` (${a.reason})` : ""} by ${a.actor ?? a.user ?? a.via}${a.note ? `: ${String(a.note).split("\n")[0].slice(0, 100)}` : ""}`).join("\n");
1246
+ const crumbs = (d.latestEvent?.breadcrumbs ?? []).slice(-8).map((b) => ` ${b.category ?? b.type ?? "-"}${b.data?.status_code ? ` ${b.data.status_code}` : ""} ${b.message ?? [b.data?.method, b.data?.url ?? b.data?.to].filter(Boolean).join(" ")}`).join("\n");
1217
1247
  return `${head}${res}${claim2}${ev}${frames ? `
1218
1248
  frames (* = in app):
1219
- ${frames}` : ""}${act ? `
1249
+ ${frames}` : ""}${crumbs ? `
1250
+ breadcrumbs (last 8, oldest first):
1251
+ ${crumbs}` : ""}${act ? `
1220
1252
  activity:
1221
1253
  ${act}` : ""}`;
1222
1254
  },
1223
1255
  async run(ctx) {
1224
- return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames });
1256
+ return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames, breadcrumbs: ctx.input.breadcrumbs });
1225
1257
  }
1226
1258
  };
1227
1259
  function statusCommand(name, status2, summary, examples) {
@@ -1547,7 +1579,7 @@ var DECLARE = {
1547
1579
  recipient: {
1548
1580
  type: "list",
1549
1581
  value: "email|<type>:<url>",
1550
- description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
1582
+ description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: \`peerpacket:<channel>?to=<handle>\` has Tracksiy open the ticket with its own key, for a channel opened to your organization; or https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> with your own key -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
1551
1583
  },
1552
1584
  delivery: { type: "string", enum: ["immediate", "digest"], description: "For --recipient: send each alert now, or batch into the digest hours. Default immediate." },
1553
1585
  "new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
@@ -1569,6 +1601,9 @@ function recipients(list5, delivery) {
1569
1601
  return compact({ type, delivery });
1570
1602
  }
1571
1603
  if (!target) throw usageError("bad_value", `--recipient ${raw} has no target`, `Format: ${type}:<url>`);
1604
+ if (type === "peerpacket" && !target.includes("://") && !target.startsWith("peerpacket:")) {
1605
+ return compact({ type, url: `peerpacket:${target}`, delivery });
1606
+ }
1572
1607
  return compact({ type, url: target, delivery });
1573
1608
  });
1574
1609
  }
@@ -1625,7 +1660,8 @@ var set = {
1625
1660
  examples: [
1626
1661
  { command: "tracksiy alerts set --recipient email --threshold 50/10", description: "Email the project's team on new issues, on bursts, and on downtime." },
1627
1662
  { command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." },
1628
- { command: "tracksiy alerts set --recipient 'peerpacket:https://peerpacket.com/channels/shop-ops?to=shop-agent&as=tracksiy-alerts&key=pp_live_\u2026'", description: "Alerts land as PeerPacket tickets for the shop's agent." }
1663
+ { command: "tracksiy alerts set --recipient 'peerpacket:shop-ops?to=shop-agent'", description: "Alerts land as PeerPacket tickets for the shop's agent, opened by Tracksiy's own key (no workspace key in the config)." },
1664
+ { command: "tracksiy alerts set --recipient 'peerpacket:https://peerpacket.com/channels/shop-ops?to=shop-agent&as=tracksiy-alerts&key=pp_live_\u2026'", description: "The same with your own PeerPacket key." }
1629
1665
  ],
1630
1666
  output: "The stored alerting state, same shape as `alerts get`.",
1631
1667
  text: rulesText,
@@ -2734,7 +2770,7 @@ function coerce(name, spec, value, previous) {
2734
2770
  return n;
2735
2771
  }
2736
2772
  case "list": {
2737
- const items = Array.isArray(value) ? value.map(String) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
2773
+ const items = Array.isArray(value) ? value.map(String) : spec.split === false ? [String(value).trim()].filter(Boolean) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
2738
2774
  if (spec.enum) for (const item of items) checkEnum(name, spec, item);
2739
2775
  return [...previous ?? [], ...items];
2740
2776
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps, from one token.",
5
5
  "type": "module",
6
6
  "bin": {