tracksiy 0.3.0 → 0.5.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
@@ -126,6 +126,7 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
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
128
  | `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
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` |
129
130
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
130
131
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
131
132
  | `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
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.3.0" : "0.0.0-dev";
7
+ var VERSION = true ? "0.5.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
@@ -851,10 +851,16 @@ var monitorsDeclare = {
851
851
  ssl: { type: "list", value: "<slug>=<url>", description: "TLS certificate check (expiry is reported on every check)." },
852
852
  ingest: { type: "list", value: "<slug>[:<surface>]", description: "Alert when this project (or surface) stops sending events." },
853
853
  interval: { type: "integer", value: "<seconds>", description: "Interval for --get/--ping/--ssl/--ingest monitors." },
854
+ "expect-text": { type: "string", value: "<text>", description: "--get: down unless the body contains this (a 200 with an empty shell is down)." },
855
+ "expect-no-text": { type: "string", value: "<text>", description: "--get: down if the body contains this, e.g. Whoops or Exception." },
856
+ "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
+ "dry-run": { type: "boolean", description: "Show what would be created, adopted or changed; write nothing." },
854
858
  body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the shorthand flags." }
855
859
  },
856
860
  examples: [
857
- { command: "tracksiy monitors declare --heartbeat nightly-backup:93600 --get site=https://example.com", description: "A backup heartbeat and a homepage check." }
861
+ { command: "tracksiy monitors declare --heartbeat nightly-backup:93600 --get site=https://example.com", description: "A backup heartbeat and a homepage check." },
862
+ { command: "tracksiy monitors declare --get tr-landing=https://example.com/tr --expect-text '<h1' --expect-no-text Whoops --user-agent GPTBot/1.0", description: "The page as an answer engine gets it: real content, no error template." },
863
+ { command: "tracksiy monitors declare --heartbeat tariff-sync:608400 --dry-run", description: "See the change before making it." }
858
864
  ],
859
865
  output: "Server result per monitor, including check-in URLs for heartbeats.",
860
866
  async run(ctx) {
@@ -870,7 +876,17 @@ var monitorsDeclare = {
870
876
  for (const item of i[type] ?? []) {
871
877
  const eq = String(item).indexOf("=");
872
878
  if (eq < 1) throw usageError("bad_value", `--${type} expects <slug>=<url>, got ${JSON.stringify(item)}`);
873
- monitors.push(compact({ slug: item.slice(0, eq), type, url: item.slice(eq + 1), intervalSeconds: i.interval }));
879
+ monitors.push(
880
+ compact({
881
+ slug: item.slice(0, eq),
882
+ type,
883
+ url: item.slice(eq + 1),
884
+ intervalSeconds: i.interval,
885
+ expectText: type === "get" ? i.expectText : void 0,
886
+ expectNoText: type === "get" ? i.expectNoText : void 0,
887
+ userAgent: type !== "ssl" ? i.userAgent : void 0
888
+ })
889
+ );
874
890
  }
875
891
  }
876
892
  for (const item of i.ingest ?? []) {
@@ -880,6 +896,7 @@ var monitorsDeclare = {
880
896
  if (!monitors.length) throw usageError("missing_flag", "Nothing to declare.", "Add --heartbeat, --get, --ping, --ssl, --ingest, or --body.");
881
897
  body = { monitors };
882
898
  }
899
+ if (i.dryRun) body = { ...body, dryRun: true };
883
900
  return ctx.api.send("POST", await projectPath(ctx, "monitors"), body);
884
901
  }
885
902
  };
@@ -904,16 +921,26 @@ var performance = {
904
921
  effect: "read",
905
922
  endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/performance" },
906
923
  flags: {
907
- since: { type: "string", value: TIME, description: "Window start (default: server's, 24h)." },
908
- op: { type: "string", value: "<op>", description: "Only this operation, e.g. http.server." },
909
- sort: { type: "string", value: "<key>", description: "Order (server default: slower first)." },
910
- limit: { type: "integer", value: "<n>", description: "How many transactions." }
924
+ since: { type: "string", value: TIME, description: "Window start (default: server's, 24h; 7d with --release)." },
925
+ op: { type: "string", value: "<op>", description: "Only this operation, e.g. http.server or pageload." },
926
+ sort: { type: "string", enum: ["slower", "duration", "p95", "failures"], description: "Order (default: slower first)." },
927
+ limit: { type: "integer", value: "<n>", description: "How many transactions." },
928
+ release: { type: "string", value: "<version>", description: "Compare this release's transactions with --against instead of two time windows." },
929
+ against: { type: "string", value: "<version>", description: "The release to compare with (default: the one before --release)." }
911
930
  },
912
- output: "{since, until, transactions:[{transaction, op, current{...}, previous{...}, changeRatio, slower}], uptime[...], blindSpots[...]}",
931
+ examples: [
932
+ { command: "tracksiy performance --sort p95", description: "The slowest routes by p95." },
933
+ { command: "tracksiy performance --release web@$(git rev-parse HEAD)", description: "Did this deploy make anything slower or failing?" },
934
+ { command: "tracksiy performance --op pageload --select transactions.transaction,transactions.current.webVitals", description: "Core Web Vitals (p75) per page." }
935
+ ],
936
+ output: "{since, until, release?, againstRelease?, transactions:[{transaction, op, current{count, avgDurationMs, p50Ms, p95Ms, p99Ms, failureRate, failed, status, webVitals}, previous{...}, changeRatio, slower}], uptime[...], blindSpots[...]}",
913
937
  text: (d) => `${table(d.transactions ?? [], [
914
938
  "transaction",
915
939
  "op",
916
- ["p95 ms", (r) => r.current?.p95Ms ?? r.current?.p95 ?? "-"],
940
+ ["p50", (r) => r.current?.p50Ms ?? "-"],
941
+ ["p95", (r) => r.current?.p95Ms ?? "-"],
942
+ ["p99", (r) => r.current?.p99Ms ?? "-"],
943
+ ["fail", (r) => r.current?.failureRate == null ? "-" : `${(r.current.failureRate * 100).toFixed(1)}%`],
917
944
  ["count", (r) => r.current?.count ?? "-"],
918
945
  ["change", (r) => r.changeRatio == null ? "-" : `${r.changeRatio > 0 ? "+" : ""}${Math.round(r.changeRatio * 100)}%`],
919
946
  ["slower", (r) => r.slower]
@@ -926,13 +953,55 @@ ${table(d.uptime, ["name", "url", "upNow", "avgResponseMs", "downChecks"])}` : "
926
953
  since: since(ctx.input.since),
927
954
  op: ctx.input.op,
928
955
  sort: ctx.input.sort,
956
+ limit: ctx.input.limit,
957
+ release: ctx.input.release,
958
+ against: ctx.input.against
959
+ });
960
+ }
961
+ };
962
+ var queries = {
963
+ path: ["queries"],
964
+ aliases: [["performance", "queries"]],
965
+ summary: "Which database queries and outbound HTTP calls make this project slow, with N+1 detection and the routes that run them.",
966
+ description: "Aggregated from the spans of sampled transactions (op db* and http.client), normalized so no literal is stored. Ranked by total time by default: the query that costs the most overall. nPlusOne marks a query one request ran 10+ times.",
967
+ context: "project",
968
+ effect: "read",
969
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/queries" },
970
+ flags: {
971
+ since: { type: "string", value: TIME, description: "Window start (default 24h)." },
972
+ op: { type: "string", value: "<db|http.client|op>", description: "db (every db* op), http.client, or an exact op." },
973
+ transaction: { type: "string", value: "<route>", description: "Only queries run by this transaction, e.g. /orders." },
974
+ sort: { type: "string", enum: ["total", "avg", "max", "count", "perTransaction"], description: "Order (default total)." },
975
+ limit: { type: "integer", value: "<n>", description: "How many (max 100)." }
976
+ },
977
+ examples: [
978
+ { command: "tracksiy queries --since 24h", description: "The queries that cost the most time today." },
979
+ { command: "tracksiy queries --sort perTransaction --select queries.query,queries.maxPerTransaction,queries.routes", description: "N+1 candidates." }
980
+ ],
981
+ output: "{since, until, queries:[{fingerprint, op, query, count, transactions, perTransaction, maxPerTransaction, totalMs, avgMs, maxMs, previous{count, avgMs}, changeRatio, nPlusOne, routes[{transaction, count, avgMs}]}], note}",
982
+ text: (d) => table(d.queries ?? [], [
983
+ ["total ms", (r) => r.totalMs],
984
+ ["avg", (r) => r.avgMs ?? "-"],
985
+ ["max", (r) => r.maxMs],
986
+ ["count", (r) => r.count],
987
+ ["per req", (r) => r.perTransaction],
988
+ ["n+1", (r) => r.nPlusOne ? "yes" : ""],
989
+ ["query", (r) => String(r.query).slice(0, 90)]
990
+ ]),
991
+ async run(ctx) {
992
+ return ctx.api.get(await projectPath(ctx, "queries"), {
993
+ since: since(ctx.input.since),
994
+ op: ctx.input.op,
995
+ transaction: ctx.input.transaction,
996
+ sort: ctx.input.sort,
929
997
  limit: ctx.input.limit
930
998
  });
931
999
  }
932
1000
  };
933
- var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance];
1001
+ var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance, queries];
934
1002
 
935
1003
  // src/commands/issues.ts
1004
+ import { writeFileSync } from "node:fs";
936
1005
  var FILTERS = {
937
1006
  status: { type: "string", enum: ["unresolved", "resolved", "ignored", "regressed", "all"], description: "Issue status. Server default: unresolved." },
938
1007
  level: { type: "string", enum: ["sample", "debug", "info", "warning", "error", "fatal"], description: "Only this level." },
@@ -942,7 +1011,9 @@ var FILTERS = {
942
1011
  since: { type: "string", value: TIME, description: "Last seen at or after this (lastSeenSince)." },
943
1012
  until: { type: "string", value: TIME, description: "Last seen before this (lastSeenBefore)." },
944
1013
  "first-seen-since": { type: "string", value: TIME, description: "First seen at or after this: new issues only." },
945
- "first-seen-until": { type: "string", value: TIME, description: "First seen before this." }
1014
+ "first-seen-until": { type: "string", value: TIME, description: "First seen before this." },
1015
+ "exclude-bots": { type: "boolean", description: "Leave out issues every event of which came from a crawler (ClaudeBot, Googlebot, ...)." },
1016
+ "only-bots": { type: "boolean", description: "Only issues that came entirely from crawlers." }
946
1017
  };
947
1018
  var WHY = {
948
1019
  reason: { type: "string", enum: ["fixed", "duplicate", "third_party", "expected", "wont_fix"], description: "Why, recorded on the issue's activity log." },
@@ -974,7 +1045,8 @@ function filterQuery(input) {
974
1045
  lastSeenSince: since(input.since),
975
1046
  lastSeenBefore: since(input.until, "until"),
976
1047
  firstSeenSince: since(input.firstSeenSince, "first-seen-since"),
977
- firstSeenBefore: since(input.firstSeenUntil, "first-seen-until")
1048
+ firstSeenBefore: since(input.firstSeenUntil, "first-seen-until"),
1049
+ bots: input.excludeBots ? "exclude" : input.onlyBots ? "only" : void 0
978
1050
  });
979
1051
  }
980
1052
  var issueRows = (issues) => table(issues ?? [], [
@@ -1202,11 +1274,13 @@ var bulk = {
1202
1274
  effect: "write",
1203
1275
  endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/status" },
1204
1276
  flags: {
1205
- to: { type: "string", required: true, enum: ["resolved", "ignored", "unresolved"], description: "New status." },
1277
+ to: { type: "string", enum: ["resolved", "ignored", "unresolved"], description: "New status (required unless --revert)." },
1206
1278
  ids: { type: "list", value: "<id,...>", description: "Exactly these issues." },
1207
1279
  ...FILTERS,
1208
1280
  all: { type: "boolean", description: "Every issue of the project (required when no other selector is given)." },
1209
1281
  apply: { type: "boolean", description: "Actually change them. Default is a dry run." },
1282
+ out: { type: "string", value: "<file.json>", description: "With --apply: write every changed issue and its previous status here, for --revert." },
1283
+ revert: { type: "string", value: "<file.json>", description: "Undo a saved --out: put each issue back to the status it had." },
1210
1284
  ...FIXED_IN,
1211
1285
  ...WHY
1212
1286
  },
@@ -1216,6 +1290,18 @@ var bulk = {
1216
1290
  ],
1217
1291
  output: "{dryRun, matched, changed, ids, ...} as reported by the server.",
1218
1292
  async run(ctx) {
1293
+ if (ctx.input.revert) {
1294
+ const saved = JSON.parse(textArg(`@${ctx.input.revert}`, "revert") ?? "{}");
1295
+ const changes2 = saved.changed ?? saved;
1296
+ const byStatus = /* @__PURE__ */ new Map();
1297
+ for (const c of changes2) if (c?.id && c.previousStatus) byStatus.set(c.previousStatus, [...byStatus.get(c.previousStatus) ?? [], String(c.id)]);
1298
+ const results = {};
1299
+ for (const [status2, ids] of byStatus) {
1300
+ results[status2] = await ctx.api.send("POST", await projectPath(ctx, "issues/status"), { status: status2, ids, dryRun: !ctx.input.apply, note: textArg(ctx.input.note, "note") ?? `revert of ${ctx.input.revert}` });
1301
+ }
1302
+ return { reverted: changes2.length, dryRun: !ctx.input.apply, results };
1303
+ }
1304
+ if (!ctx.input.to) throw usageError("missing_flag", "--to is required.", "Pass --to resolved|ignored|unresolved, or --revert <file>.");
1219
1305
  const filter = filterQuery(ctx.input);
1220
1306
  const hasFilter = Object.keys(filter).length > 0;
1221
1307
  const body = compact({
@@ -1226,7 +1312,12 @@ var bulk = {
1226
1312
  dryRun: !ctx.input.apply,
1227
1313
  ...whyBody(ctx.input)
1228
1314
  });
1229
- return ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
1315
+ const result = await ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
1316
+ if (ctx.input.out && ctx.input.apply) {
1317
+ writeFileSync(ctx.input.out, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), status: ctx.input.to, changed: result?.changed ?? [] }, null, 2));
1318
+ ctx.log(`wrote ${result?.changed?.length ?? 0} change(s) to ${ctx.input.out}; undo with --revert ${ctx.input.out} --apply`);
1319
+ }
1320
+ return result;
1230
1321
  }
1231
1322
  };
1232
1323
  var issues_default = [list, view, resolve2, ignore, unresolve, bulk, comment, activity, verifyFixed, claim, unclaim];
@@ -3101,7 +3192,7 @@ var COMMANDS = [
3101
3192
  ];
3102
3193
  var GROUPS = [
3103
3194
  ["Start", "init auth onboarding describe"],
3104
- ["Look", "overview changes health orgs projects issues monitors performance releases alerts keys artifacts"],
3195
+ ["Look", "overview changes health orgs projects issues monitors performance queries releases alerts keys artifacts"],
3105
3196
  ["Ship", "sourcemaps proguard"],
3106
3197
  ["Manage", "tokens"],
3107
3198
  ["Talk to us", "support"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.3.0",
3
+ "version": "0.5.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": {