tracksiy 0.5.0 → 0.6.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
@@ -127,12 +127,17 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
127
127
  | `monitors delete <slug>` | Remove one of a project's monitors by the slug it was declared with. | **destructive** | `monitors_delete` |
128
128
  | `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
129
129
  | `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
+ | `not-found` | Which pages answer 404, and who links to them: broken internal links first. | read | `not_found` |
131
+ | `settings` | The project's reading rules: environments, grouping rules, privacy rules, source roots. | read | `settings` |
132
+ | `settings set` | Change the project's reading rules. Only the flags given change; the rest are kept. | write | `settings_set` |
133
+ | `privacy-preview` | Try privacy rules on the project's last 50 stored events: what each would change and what PII shapes would remain. Writes nothing. | read | `privacy_preview` |
130
134
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
131
135
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
132
136
  | `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
133
137
  | `issues ignore <ids...>` | Mark issues ignored: they stop alerting until unignored. Records why. | write | `issues_ignore` |
134
138
  | `issues unresolve <ids...>` | Reopen issues. | write | `issues_unresolve` |
135
139
  | `issues bulk-status` | Change the status of every issue matching a filter. Dry run unless --apply. | write | `issues_bulk_status` |
140
+ | `issues merge <ids...>` | Fold issues that are one root cause into one. Dry run unless --apply; later events of any of them land on the target. | write | `issues_merge` |
136
141
  | `issues comment <id>` | Leave a comment on an issue for the next agent or human: what you found, what you tried. | write | `issues_comment` |
137
142
  | `issues activity <id>` | Who did what to an issue and why: resolutions (and in which release), regressions, comments, claims. | read | `issues_activity` |
138
143
  | `issues verify-fixed <id>` | Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed. | read | `issues_verify_fixed` |
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.5.0" : "0.0.0-dev";
7
+ var VERSION = true ? "0.6.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
@@ -116,6 +116,7 @@ var Api = class {
116
116
  const useAuth = opts.auth !== false;
117
117
  if (useAuth && this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
118
118
  if (this.opts.agent) headers["X-Tracksiy-Agent"] = this.opts.agent;
119
+ if (method !== "GET" && method !== "HEAD") headers["Idempotency-Key"] = opts.idempotencyKey ?? crypto.randomUUID();
119
120
  let body;
120
121
  if (opts.form) body = opts.form;
121
122
  else if (opts.body !== void 0) {
@@ -855,6 +856,7 @@ var monitorsDeclare = {
855
856
  "expect-no-text": { type: "string", value: "<text>", description: "--get: down if the body contains this, e.g. Whoops or Exception." },
856
857
  "user-agent": { type: "string", value: "<ua>", description: "--get/--ping: ask as this User-Agent, e.g. GPTBot/1.0, to see what crawlers get." },
857
858
  "dry-run": { type: "boolean", description: "Show what would be created, adopted or changed; write nothing." },
859
+ prune: { type: "boolean", description: "The declaration is the whole list: delete this project's monitors of the declared types it does not name (listed in `pruned`)." },
858
860
  body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the shorthand flags." }
859
861
  },
860
862
  examples: [
@@ -897,6 +899,7 @@ var monitorsDeclare = {
897
899
  body = { monitors };
898
900
  }
899
901
  if (i.dryRun) body = { ...body, dryRun: true };
902
+ if (i.prune) body = { ...body, prune: true };
900
903
  return ctx.api.send("POST", await projectPath(ctx, "monitors"), body);
901
904
  }
902
905
  };
@@ -998,7 +1001,102 @@ var queries = {
998
1001
  });
999
1002
  }
1000
1003
  };
1001
- var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance, queries];
1004
+ var notFound = {
1005
+ path: ["not-found"],
1006
+ aliases: [["404"]],
1007
+ summary: "Which pages answer 404, and who links to them: broken internal links first.",
1008
+ context: "project",
1009
+ effect: "read",
1010
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/not-found" },
1011
+ flags: {
1012
+ since: { type: "string", value: TIME, description: "Window start (default 24h)." },
1013
+ internal: { type: "boolean", description: "Only 404s reached from the site's own pages." },
1014
+ limit: { type: "integer", value: "<n>", description: "How many paths (max 200)." }
1015
+ },
1016
+ examples: [{ command: "tracksiy not-found --internal --since 7d", description: "Broken links the site itself serves." }],
1017
+ output: "{since, until, scanned, pages:[{path, count, lastSeen, internal, bots, referrers[{referrer, count}]}], note}",
1018
+ text: (d) => table(d.pages ?? [], ["path", "count", ["internal", (r) => r.internal ? "yes" : ""], "bots", ["top referrer", (r) => r.referrers?.[0]?.referrer ?? "-"]]),
1019
+ async run(ctx) {
1020
+ return ctx.api.get(await projectPath(ctx, "not-found"), { since: since(ctx.input.since), internal: ctx.input.internal || void 0, limit: ctx.input.limit });
1021
+ }
1022
+ };
1023
+ var settingsGet = {
1024
+ path: ["settings"],
1025
+ summary: "The project's reading rules: environments, grouping rules, privacy rules, source roots.",
1026
+ context: "project",
1027
+ effect: "read",
1028
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/settings" },
1029
+ output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
1030
+ async run(ctx) {
1031
+ return ctx.api.get(await projectPath(ctx, "settings"));
1032
+ }
1033
+ };
1034
+ function pairs(items, flag) {
1035
+ if (!items) return void 0;
1036
+ return items.map((item) => {
1037
+ const out = {};
1038
+ for (const part of String(item).split(/,(?=\s*\w+=)/)) {
1039
+ const eq = part.indexOf("=");
1040
+ if (eq < 1) throw usageError("bad_value", `--${flag} expects key=value,key=value, got ${JSON.stringify(item)}`);
1041
+ out[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
1042
+ }
1043
+ return out;
1044
+ });
1045
+ }
1046
+ var settingsSet = {
1047
+ path: ["settings", "set"],
1048
+ summary: "Change the project's reading rules. Only the flags given change; the rest are kept.",
1049
+ description: "Applied to events ingested from then on. --environments + --environment-policy drop discards (and counts) events from any other environment; flag stores them tagged environment_unlisted. --fingerprint-rule groups matching events into one issue. --privacy-rule drops a header or masks a PII shape before storage -- try it first with `tracksiy privacy-preview`. --source-root makes frames carry repoPath.",
1050
+ context: "project",
1051
+ effect: "write",
1052
+ idempotent: true,
1053
+ endpoint: { method: "PUT", path: "/api/agent/v1/projects/{org}/{project}/settings" },
1054
+ flags: {
1055
+ environments: { type: "list", value: "<env,...>", description: "The environments this project reports from (empty string: any)." },
1056
+ "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." },
1060
+ body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
1061
+ },
1062
+ examples: [
1063
+ { command: "tracksiy settings set --environments production,staging --environment-policy drop", description: "local and local1 stop reaching Tracksiy." },
1064
+ { 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." }
1066
+ ],
1067
+ output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
1068
+ async run(ctx) {
1069
+ const path = await projectPath(ctx, "settings");
1070
+ const i = ctx.input;
1071
+ if (i.body) return ctx.api.send("PUT", path, i.body);
1072
+ const current = await ctx.api.get(path);
1073
+ const next = {
1074
+ sourceRoots: i.sourceRoot ?? current.sourceRoots ?? [],
1075
+ environments: i.environments ? i.environments.filter((e) => e) : current.environments ?? [],
1076
+ environmentPolicy: i.environmentPolicy ?? current.environmentPolicy ?? "flag",
1077
+ fingerprintRules: pairs(i.fingerprintRule, "fingerprint-rule") ?? current.fingerprintRules ?? [],
1078
+ privacyRules: pairs(i.privacyRule, "privacy-rule") ?? current.privacyRules ?? []
1079
+ };
1080
+ return ctx.api.send("PUT", path, next);
1081
+ }
1082
+ };
1083
+ var privacyPreview = {
1084
+ path: ["privacy-preview"],
1085
+ summary: "Try privacy rules on the project's last 50 stored events: what each would change and what PII shapes would remain. Writes nothing.",
1086
+ context: "project",
1087
+ effect: "read",
1088
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/settings/privacy-preview" },
1089
+ flags: {
1090
+ "privacy-rule": { type: "list", value: "dropHeader=<name> | mask=<shape>,field=<path|*>", description: "Rules to try (default: the saved ones)." }
1091
+ },
1092
+ examples: [{ command: "tracksiy privacy-preview --privacy-rule 'mask=email,field=request.headers.User-Agent'", description: "Would this clear the PII check?" }],
1093
+ output: "{sampled, rules[{rule, events, values}], remaining[{pattern, field, events}]}",
1094
+ async run(ctx) {
1095
+ const rules = pairs(ctx.input.privacyRule, "privacy-rule");
1096
+ return ctx.api.send("POST", await projectPath(ctx, "settings/privacy-preview"), rules ? { privacyRules: rules } : {});
1097
+ }
1098
+ };
1099
+ var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
1002
1100
 
1003
1101
  // src/commands/issues.ts
1004
1102
  import { writeFileSync } from "node:fs";
@@ -1070,6 +1168,7 @@ var list = {
1070
1168
  limit: { type: "integer", value: "<n>", description: "How many issues (server default 25)." },
1071
1169
  frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." },
1072
1170
  unclaimed: { type: "boolean", description: "Only issues nobody holds a live claim on: what you can pick up." },
1171
+ cursor: { type: "string", value: "<nextCursor>", description: "Continue from the previous page's nextCursor (same filters and sort)." },
1073
1172
  "claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." }
1074
1173
  },
1075
1174
  examples: [
@@ -1078,7 +1177,7 @@ var list = {
1078
1177
  { command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
1079
1178
  { command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
1080
1179
  ],
1081
- output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, frames?}]}",
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?}]}",
1082
1181
  text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
1083
1182
  ${issueRows(d.issues)}`,
1084
1183
  async run(ctx) {
@@ -1088,7 +1187,8 @@ ${issueRows(d.issues)}`,
1088
1187
  limit: ctx.input.limit,
1089
1188
  frames: ctx.input.frames,
1090
1189
  unclaimed: ctx.input.unclaimed || void 0,
1091
- claimedBy: ctx.input.claimedBy
1190
+ claimedBy: ctx.input.claimedBy,
1191
+ cursor: ctx.input.cursor
1092
1192
  });
1093
1193
  }
1094
1194
  };
@@ -1194,6 +1294,29 @@ var activity = {
1194
1294
  return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/activity`), { limit: ctx.input.limit });
1195
1295
  }
1196
1296
  };
1297
+ var merge = {
1298
+ path: ["issues", "merge"],
1299
+ summary: "Fold issues that are one root cause into one. Dry run unless --apply; later events of any of them land on the target.",
1300
+ context: "project",
1301
+ effect: "write",
1302
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/merge" },
1303
+ args: [{ name: "ids", required: true, variadic: true, description: "The issues to fold in." }],
1304
+ flags: {
1305
+ into: { type: "string", required: true, value: "<id>", description: "The issue they become." },
1306
+ note: WHY.note,
1307
+ apply: { type: "boolean", description: "Actually merge. Default is a dry run." }
1308
+ },
1309
+ examples: [{ command: "tracksiy issues merge 5519 5526 --into 5515 --note 'same Blade template, same deploy' --apply", description: "Three issues, one cause." }],
1310
+ output: "{dryRun, into{...issue}, merged[], events}",
1311
+ async run(ctx) {
1312
+ return ctx.api.send("POST", await projectPath(ctx, "issues/merge"), {
1313
+ into: String(ctx.input.into),
1314
+ ids: ctx.input.ids.map(String),
1315
+ note: textArg(ctx.input.note, "note"),
1316
+ dryRun: !ctx.input.apply
1317
+ });
1318
+ }
1319
+ };
1197
1320
  var verifyFixed = {
1198
1321
  path: ["issues", "verify-fixed"],
1199
1322
  summary: "Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed.",
@@ -1320,7 +1443,7 @@ var bulk = {
1320
1443
  return result;
1321
1444
  }
1322
1445
  };
1323
- var issues_default = [list, view, resolve2, ignore, unresolve, bulk, comment, activity, verifyFixed, claim, unclaim];
1446
+ var issues_default = [list, view, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
1324
1447
 
1325
1448
  // src/commands/projects.ts
1326
1449
  var create = {
@@ -1567,7 +1690,7 @@ var defaults = {
1567
1690
  return ctx.api.send("PUT", await orgPath(ctx, "alerts/defaults"), body);
1568
1691
  }
1569
1692
  };
1570
- var settingsGet = {
1693
+ var settingsGet2 = {
1571
1694
  path: ["alerts", "settings"],
1572
1695
  summary: "When digest-mode recipients are told, and whether the last digests went out.",
1573
1696
  context: "org",
@@ -1582,7 +1705,7 @@ ${table(d.recentDigests ?? [], [["sent", (r) => ago(r.sentAt)], "type", "ok", "i
1582
1705
  return ctx.api.get(await orgPath(ctx, "alerts/settings"));
1583
1706
  }
1584
1707
  };
1585
- var settingsSet = {
1708
+ var settingsSet2 = {
1586
1709
  path: ["alerts", "settings", "set"],
1587
1710
  summary: "Declare the digest hours and timezone.",
1588
1711
  context: "org",
@@ -1604,7 +1727,7 @@ var settingsSet = {
1604
1727
  return ctx.api.send("PUT", await orgPath(ctx, "alerts/settings"), { digest: compact({ hours, timezone: ctx.input.timezone }) });
1605
1728
  }
1606
1729
  };
1607
- var alerts_default = [get, set, test, coverage, defaults, settingsGet, settingsSet];
1730
+ var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
1608
1731
 
1609
1732
  // src/commands/keys.ts
1610
1733
  var list2 = {
@@ -1804,7 +1927,7 @@ function injectMap(json, id) {
1804
1927
  return JSON.stringify(map);
1805
1928
  }
1806
1929
  async function findPairs(paths) {
1807
- const pairs = [];
1930
+ const pairs2 = [];
1808
1931
  const seen = /* @__PURE__ */ new Set();
1809
1932
  for (const p of paths) {
1810
1933
  const abs = resolve3(p);
@@ -1823,10 +1946,10 @@ async function findPairs(paths) {
1823
1946
  if (existsSync2(candidate)) map = candidate;
1824
1947
  }
1825
1948
  if (!map && existsSync2(`${js}.map`)) map = `${js}.map`;
1826
- pairs.push({ js, map, root });
1949
+ pairs2.push({ js, map, root });
1827
1950
  }
1828
1951
  }
1829
- return pairs;
1952
+ return pairs2;
1830
1953
  }
1831
1954
  function artifactName(file, root, prefix) {
1832
1955
  const rel = relative(root, file).split(sep).join("/");
@@ -1997,9 +2120,9 @@ async function scan(paths) {
1997
2120
  throw usageError("bad_path", err.message);
1998
2121
  }
1999
2122
  }
2000
- async function inject(pairs, dryRun) {
2123
+ async function inject(pairs2, dryRun) {
2001
2124
  const out = [];
2002
- for (const pair of pairs) {
2125
+ for (const pair of pairs2) {
2003
2126
  if (!pair.map) {
2004
2127
  out.push({ file: pair.js, map: null, debugId: "", action: "skipped-no-map" });
2005
2128
  continue;
@@ -2076,13 +2199,13 @@ ${table(d.files, ["kind", "state", "name", "detail"])}${d.skipped?.length ? `
2076
2199
  skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
2077
2200
  async run(ctx) {
2078
2201
  const i = ctx.input;
2079
- const pairs = await scan(i.paths);
2080
- if (!pairs.length) throw usageError("nothing_to_upload", "No JavaScript files found.", "Point at the build output directory (dist, build, public/build).");
2081
- if (i.inject && !i.dryRun) await inject(pairs, false);
2082
- const skipped = pairs.filter((p) => !p.map).map((p) => artifactName(p.js, p.root, i.urlPrefix));
2083
- const withMaps = pairs.filter((p) => p.map);
2202
+ const pairs2 = await scan(i.paths);
2203
+ if (!pairs2.length) throw usageError("nothing_to_upload", "No JavaScript files found.", "Point at the build output directory (dist, build, public/build).");
2204
+ if (i.inject && !i.dryRun) await inject(pairs2, false);
2205
+ const skipped = pairs2.filter((p) => !p.map).map((p) => artifactName(p.js, p.root, i.urlPrefix));
2206
+ const withMaps = pairs2.filter((p) => p.map);
2084
2207
  if (!withMaps.length) {
2085
- throw usageError("nothing_to_upload", `None of the ${pairs.length} JavaScript file(s) has a source map.`, "Build with source maps on (e.g. Vite build.sourcemap: true, webpack devtool: 'source-map').");
2208
+ throw usageError("nothing_to_upload", `None of the ${pairs2.length} JavaScript file(s) has a source map.`, "Build with source maps on (e.g. Vite build.sourcemap: true, webpack devtool: 'source-map').");
2086
2209
  }
2087
2210
  const files = [];
2088
2211
  let withoutId = 0;
@@ -2903,6 +3026,17 @@ var describe = {
2903
3026
  summary: "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps from one token.",
2904
3027
  docs: `${DEFAULT_URL}/cli.md`,
2905
3028
  contract: contract(),
3029
+ limits: {
3030
+ issuesPerPage: 100,
3031
+ bulkStatusCap: 5e3,
3032
+ mergeIds: 100,
3033
+ filesPerAssembly: 1e3,
3034
+ monitorIntervalSeconds: [60, 3456e4],
3035
+ claimSeconds: [60, 604800],
3036
+ noteCharacters: 1e4,
3037
+ idempotencyKeyHours: 24,
3038
+ note: "The CLI splits uploads past filesPerAssembly itself and sends an Idempotency-Key with every write."
3039
+ },
2906
3040
  globalFlags: Object.entries(GLOBAL_FLAGS).map(([n, f]) => flagSchema(n, f)),
2907
3041
  commands: COMMANDS.map(describeCommand)
2908
3042
  };
@@ -3192,7 +3326,7 @@ var COMMANDS = [
3192
3326
  ];
3193
3327
  var GROUPS = [
3194
3328
  ["Start", "init auth onboarding describe"],
3195
- ["Look", "overview changes health orgs projects issues monitors performance queries releases alerts keys artifacts"],
3329
+ ["Look", "overview changes health orgs projects issues monitors performance queries not-found releases alerts keys artifacts settings privacy-preview"],
3196
3330
  ["Ship", "sourcemaps proguard"],
3197
3331
  ["Manage", "tokens"],
3198
3332
  ["Talk to us", "support"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": {