tracksiy 0.9.1 → 0.10.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
@@ -137,6 +137,8 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
137
137
  | `budgets check` | Judge every budget now -- for one release with --release. Exit code 10 if one is crossed. For a deploy to stop on. | read | `budgets_check` |
138
138
  | `performance findings` | Routes that got slower after a release and N+1 queries, which Tracksiy raised as issues on its own. | read | `performance_findings` |
139
139
  | `monitors timing <slug>` | Where an HTTP monitor's time goes (DNS, connect, server, TTFB, download, page size); how long a job runs and how close to its max runtime. | read | `monitors_timing` |
140
+ | `storage` | What each project holds on the server (events, transactions, source maps, debug files), largest first, with its retention limits. | read | `storage` |
141
+ | `mobile` | How fast the app is on phones: app start (cold/warm) against the window before, per OS and release, and per screen TTID/TTFD and slow/frozen frames, with checks for what the SDK does not report. | read | `mobile` |
140
142
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
141
143
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
142
144
  | `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
@@ -165,6 +167,7 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
165
167
  | `releases list` | What did the last deploys break? Recent releases with their events, issues and the issues they introduced. | read | `releases_list` |
166
168
  | `releases health` | Is the app crashing for its users, and did the last release make it worse? Crash-free sessions and users per release. | read | `releases_health` |
167
169
  | `releases declare <version>` | Create or update a release idempotently: its commit, URL, and when it was released. Run it from the deploy. | write | `releases_declare` |
170
+ | `releases verdict <version>` | Should the rollout of this release continue? Crash-free vs threshold and the previous build, issues spreading fast, new issues, budgets, regressions. Exit code 10 on fail. | read | `releases_verdict` |
168
171
  | `sourcemaps inject <paths...>` | Stamp built JavaScript and its source maps with a debug ID, so errors map to the exact build. Run after the build, before deploying. | write | `sourcemaps_inject` |
169
172
  | `sourcemaps upload <paths...>` | Upload built JavaScript and source maps so stack traces show original files and lines. Only missing bytes are sent. | write | `sourcemaps_upload` |
170
173
  | `proguard upload <mapping>` | Upload an Android ProGuard/R8 mapping so obfuscated stack traces are readable. | write | `proguard_upload` |
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.9.1" : "0.0.0-dev";
7
+ var VERSION = true ? "0.10.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
@@ -81,7 +81,7 @@ var seg = (value) => encodeURIComponent(String(value));
81
81
  var IDEMPOTENT = /* @__PURE__ */ new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
82
82
  var RETRY_STATUS = /* @__PURE__ */ new Set([429, 502, 503, 504]);
83
83
  var MAX_ATTEMPTS = 3;
84
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
84
+ var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
85
85
  var Api = class {
86
86
  url;
87
87
  hasToken;
@@ -1050,7 +1050,7 @@ var settingsGet = {
1050
1050
  context: "project",
1051
1051
  effect: "read",
1052
1052
  endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/settings" },
1053
- output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues}",
1053
+ output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}}",
1054
1054
  async run(ctx) {
1055
1055
  return ctx.api.get(await projectPath(ctx, "settings"));
1056
1056
  }
@@ -1082,15 +1082,24 @@ var settingsSet = {
1082
1082
  "fingerprint-rule": { type: "list", split: false, value: "type=<glob>,message=<glob>,fingerprint=<name>", description: "Group matching events into one issue. Repeatable; replaces the list." },
1083
1083
  "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." },
1084
1084
  "performance-issues": { type: "boolean", description: "Raise issues on its own for routes slower after a release and N+1 queries (default on). --no-performance-issues turns it off." },
1085
+ "max-events-per-issue": { type: "integer", value: "<n>", description: "Stored events kept per issue: the newest n-1 plus the first (default 1000; 0 keeps all, otherwise at least 10). The issue's count is unaffected. Trimmed hourly." },
1086
+ "sourcemap-keep-releases": { type: "integer", value: "<n>", description: "Source maps kept per release line (<name>@...): the newest n releases (default 10; 0 keeps every release). Pruned nightly." },
1087
+ "sourcemap-keep-used-days": { type: "integer", value: "<days>", description: "Also keep bundles a stack trace used in this many days (default 30; mobile builds also while their release has events or sessions)." },
1088
+ "crash-free-min": { type: "number", value: "<pct>", description: "Release verdict / alert: crash-free sessions under this percentage fail a release (default 99)." },
1089
+ "min-sessions": { type: "integer", value: "<n>", description: "...judged from this many sessions on; below it the verdict is unknown (default 100)." },
1090
+ "max-drop-pct": { type: "number", value: "<points>", description: "...or this many points worse than the previous release of the line (default 0.5)." },
1091
+ "velocity-pct": { type: "number", value: "<pct>", description: "Velocity alert: one issue's events in 30 minutes reaching this % of the release's sessions in the last hour (default 1)." },
1092
+ "velocity-min-events": { type: "integer", value: "<n>", description: "...with at least this many events (default 10)." },
1085
1093
  body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
1086
1094
  },
1087
1095
  examples: [
1088
1096
  { command: "tracksiy settings set --environments production,staging --environment-policy drop", description: "local and local1 stop reaching Tracksiy." },
1089
1097
  { command: "tracksiy settings set --fingerprint-rule 'type=ViewException,message=syntax error*,fingerprint=blade-syntax'", description: "Every half-written template is one issue." },
1090
1098
  { command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." },
1091
- { command: "tracksiy settings set --source-root 'strip=/var/www/shop/,prepend=backend/,surface=backend'", description: "The server checkout is only the repo's backend/ directory." }
1099
+ { command: "tracksiy settings set --source-root 'strip=/var/www/shop/,prepend=backend/,surface=backend'", description: "The server checkout is only the repo's backend/ directory." },
1100
+ { command: "tracksiy settings set --max-events-per-issue 500 --sourcemap-keep-releases 5", description: "A noisy project keeps less on disk." }
1092
1101
  ],
1093
- output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues}",
1102
+ output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}}",
1094
1103
  async run(ctx) {
1095
1104
  const path = await projectPath(ctx, "settings");
1096
1105
  const i = ctx.input;
@@ -1102,7 +1111,21 @@ var settingsSet = {
1102
1111
  environmentPolicy: i.environmentPolicy ?? current.environmentPolicy ?? "flag",
1103
1112
  fingerprintRules: pairs(i.fingerprintRule, "fingerprint-rule") ?? current.fingerprintRules ?? [],
1104
1113
  privacyRules: pairs(i.privacyRule, "privacy-rule") ?? current.privacyRules ?? [],
1105
- performanceIssues: i.performanceIssues ?? current.performanceIssues ?? true
1114
+ performanceIssues: i.performanceIssues ?? current.performanceIssues ?? true,
1115
+ storage: {
1116
+ ...current.storage ?? {},
1117
+ ...i.maxEventsPerIssue !== void 0 ? { maxEventsPerIssue: i.maxEventsPerIssue } : {},
1118
+ ...i.sourcemapKeepReleases !== void 0 ? { sourcemapKeepReleases: i.sourcemapKeepReleases } : {},
1119
+ ...i.sourcemapKeepUsedDays !== void 0 ? { sourcemapKeepUsedDays: i.sourcemapKeepUsedDays } : {}
1120
+ },
1121
+ releaseHealth: {
1122
+ ...current.releaseHealth ?? {},
1123
+ ...i.crashFreeMin !== void 0 ? { crashFreeSessionsMin: i.crashFreeMin } : {},
1124
+ ...i.minSessions !== void 0 ? { minSessions: i.minSessions } : {},
1125
+ ...i.maxDropPct !== void 0 ? { maxDropPct: i.maxDropPct } : {},
1126
+ ...i.velocityPct !== void 0 ? { velocityPct: i.velocityPct } : {},
1127
+ ...i.velocityMinEvents !== void 0 ? { velocityMinEvents: i.velocityMinEvents } : {}
1128
+ }
1106
1129
  };
1107
1130
  return ctx.api.send("PUT", path, next);
1108
1131
  }
@@ -1126,7 +1149,7 @@ var privacyPreview = {
1126
1149
  var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, monitorsDeleted, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
1127
1150
 
1128
1151
  // src/commands/perf.ts
1129
- var METRICS = "p50|p75|p95|p99|failure_rate|lcp|fcp|inp|cls|ttfb (transactions); response_p95|ttfb_p95 (HTTP monitors); duration_p95 (heartbeats)";
1152
+ var METRICS = "p50|p75|p95|p99|failure_rate|lcp|fcp|inp|cls|ttfb|app_start_cold|app_start_warm|ttid|ttfd|slow_frame_rate|frozen_frame_rate (transactions); response_p95|ttfb_p95 (HTTP monitors); duration_p95 (heartbeats)";
1130
1153
  var BUDGET_KEYS = {
1131
1154
  name: "name",
1132
1155
  transaction: "transaction",
@@ -1208,7 +1231,7 @@ var budgetsGet = {
1208
1231
  var budgetsSet = {
1209
1232
  path: ["budgets", "set"],
1210
1233
  summary: "Declare performance budgets: told once when one is crossed and once when it is met again.",
1211
- description: `The list replaces the stored one, by name (--merge keeps the others). Metrics: ${METRICS}. max is an upper bound: ms, a 0..1 ratio for failure_rate, unitless for cls. Below min-samples the verdict is unknown, never pass. Alerts go through the project's rules with performance on (the default). --body takes the API's shape, {"budgets": [{"name", "metric", "max", "transaction"?, "op"?, "monitor"? (slug or id), "windowMinutes"?, "minSamples"?}]}, or a bare list; the flag spellings window and min-samples are accepted there too, and --dry-run and --merge apply to it.`,
1234
+ description: `The list replaces the stored one, by name (--merge keeps the others). Metrics: ${METRICS}. max is an upper bound: ms, a 0..1 ratio for failure_rate and the frame rates, unitless for cls. Below min-samples the verdict is unknown, never pass. Alerts go through the project's rules with performance on (the default). --body takes the API's shape, {"budgets": [{"name", "metric", "max", "transaction"?, "op"?, "monitor"? (slug or id), "windowMinutes"?, "minSamples"?}]}, or a bare list; the flag spellings window and min-samples are accepted there too, and --dry-run and --merge apply to it.`,
1212
1235
  context: "project",
1213
1236
  effect: "write",
1214
1237
  idempotent: true,
@@ -1337,6 +1360,107 @@ ${table(
1337
1360
  };
1338
1361
  var perf_default = [budgetsGet, budgetsSet, budgetsCheck, findings, monitorsTiming];
1339
1362
 
1363
+ // src/commands/storage.ts
1364
+ function mb(bytes) {
1365
+ const n = Number(bytes ?? 0);
1366
+ if (n >= 1e9) return `${(n / 1e9).toFixed(2)} GB`;
1367
+ return `${(n / 1e6).toFixed(1)} MB`;
1368
+ }
1369
+ var storage = {
1370
+ path: ["storage"],
1371
+ summary: "What each project holds on the server (events, transactions, source maps, debug files), largest first, with its retention limits.",
1372
+ description: "Limits are per project: `tracksiy settings set --max-events-per-issue/--sourcemap-keep-releases/--sourcemap-keep-used-days`. Staff tokens also see the server's free disk and database size. Cached ten minutes; --fresh recounts.",
1373
+ context: "org",
1374
+ effect: "read",
1375
+ endpoint: { method: "GET", path: "/api/agent/v1/organizations/{org}/storage" },
1376
+ flags: { fresh: { type: "boolean", description: "Recount instead of the ten-minute cache (a few seconds)." } },
1377
+ examples: [
1378
+ { command: "tracksiy storage", description: "Which project is filling the disk?" },
1379
+ { command: "tracksiy storage --select instance,notes", description: "Free disk and warnings only (staff)." }
1380
+ ],
1381
+ output: "{generatedAt, cached, projects[{project, events{stored, approxBytes, issuesOverCap, largestIssueEvents}, transactions{stored, approxBytes}, sourceMaps{bundles, releases, releaseLines, bytes}, debugFiles{files, bytes}, approxTotalBytes, limits{...}}], instance{diskTotalBytes, diskFreeBytes, diskUsedPct, databaseBytes, uploadsBytes, eventRetentionDays, transactionRetentionDays}|null, notes[]}",
1382
+ text: (d) => `${d.instance ? kv({ disk: `${d.instance.diskUsedPct}% used, ${mb(d.instance.diskFreeBytes)} free`, database: mb(d.instance.databaseBytes), uploads: mb(d.instance.uploadsBytes) }) + "\n\n" : ""}${table(d.projects ?? [], [
1383
+ "project",
1384
+ ["total", (r) => mb(r.approxTotalBytes)],
1385
+ ["events", (r) => `${r.events.stored} (${mb(r.events.approxBytes)})`],
1386
+ ["over cap", (r) => r.events.issuesOverCap],
1387
+ ["transactions", (r) => `${r.transactions.stored} (${mb(r.transactions.approxBytes)})`],
1388
+ ["source maps", (r) => `${r.sourceMaps.bundles} in ${r.sourceMaps.releases} rel (${mb(r.sourceMaps.bytes)})`],
1389
+ ["debug files", (r) => `${r.debugFiles.files} (${mb(r.debugFiles.bytes)})`],
1390
+ ["limits", (r) => `${r.limits.maxEventsPerIssue}/issue, ${r.limits.sourcemapKeepReleases} rel, ${r.limits.sourcemapKeepUsedDays}d`]
1391
+ ])}${(d.notes ?? []).length ? `
1392
+
1393
+ ${d.notes.join("\n")}` : ""}`,
1394
+ async run(ctx) {
1395
+ return ctx.api.get(await orgPath(ctx, "storage"), { fresh: ctx.input.fresh || void 0 });
1396
+ }
1397
+ };
1398
+ var storage_default = [storage];
1399
+
1400
+ // src/commands/mobile.ts
1401
+ var pct = (v) => v === null || v === void 0 ? "-" : `${(Number(v) * 100).toFixed(1)}%`;
1402
+ var ms = (v) => v === null || v === void 0 ? "-" : `${v} ms`;
1403
+ var mobile = {
1404
+ path: ["mobile"],
1405
+ aliases: [["mobile", "vitals"]],
1406
+ summary: "How fast the app is on phones: app start (cold/warm) against the window before, per OS and release, and per screen TTID/TTFD and slow/frozen frames, with checks for what the SDK does not report.",
1407
+ description: "Read from the measurements the mobile SDKs already send with transactions (sentry-android, sentry-cocoa, sentry_flutter incl. Flutter 10 span streaming, @sentry/react-native); nothing to configure but tracesSampleRate > 0. Budgets take the same metrics: app_start_cold, app_start_warm, ttid, ttfd (p75 ms) and slow_frame_rate, frozen_frame_rate (0..1).",
1408
+ context: "project",
1409
+ effect: "read",
1410
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/mobile" },
1411
+ flags: {
1412
+ since: { type: "string", value: TIME, description: "Window start (default 7d), compared with the window before." },
1413
+ release: { type: "string", value: "<version>", description: "Only this release." },
1414
+ os: { type: "string", value: "<name>", description: "Only this OS as the SDK names it: Android, iOS." },
1415
+ sort: { type: "string", enum: ["count", "ttid", "ttfd", "slowFrames", "frozenFrames"], description: "Screen order (default count)." },
1416
+ limit: { type: "integer", value: "<n>", description: "Screens (default 30, at most 100)." }
1417
+ },
1418
+ examples: [
1419
+ { command: "tracksiy mobile --os Android --sort ttid", description: "The slowest Android screens to show." },
1420
+ { command: "tracksiy budgets set --merge --budget 'name=cold start,metric=app_start_cold,max=2000'", description: "Alert when cold start p75 passes 2 s." }
1421
+ ],
1422
+ output: "{transactions, appStart{cold{count,p50,p75,p95}, warm{...}, previousColdP75, previousWarmP75}, byOs[], byRelease[], screens[{name, op, count, ttidP50, ttidP75, ttidP95, ttfdP75, previousTtidP75, slowFrameRate, frozenFrameRate, frames, frameDelayP75, stallP75}], checks[]}",
1423
+ text: (d) => {
1424
+ const s = d.appStart ?? {};
1425
+ const head = kv({
1426
+ transactions: d.transactions,
1427
+ "cold start p75": `${ms(s.cold?.p75)} (before ${ms(s.previousColdP75)}, n=${s.cold?.count ?? 0})`,
1428
+ "warm start p75": `${ms(s.warm?.p75)} (before ${ms(s.previousWarmP75)}, n=${s.warm?.count ?? 0})`
1429
+ });
1430
+ const screens = table(d.screens ?? [], [
1431
+ "name",
1432
+ "count",
1433
+ ["ttid p75", (r) => ms(r.ttidP75)],
1434
+ ["before", (r) => ms(r.previousTtidP75)],
1435
+ ["ttfd p75", (r) => ms(r.ttfdP75)],
1436
+ ["slow", (r) => pct(r.slowFrameRate)],
1437
+ ["frozen", (r) => pct(r.frozenFrameRate)]
1438
+ ]);
1439
+ const releases = table(d.byRelease ?? [], ["release", "transactions", ["cold p75", (r) => ms(r.coldP75)], ["ttid p75", (r) => ms(r.ttidP75)], ["slow", (r) => pct(r.slowFrameRate)]]);
1440
+ const checks = (d.checks ?? []).map((c) => `${c.status.padEnd(7)} ${c.name}: ${c.detail}`).join("\n");
1441
+ return `${head}
1442
+
1443
+ Screens:
1444
+ ${screens}
1445
+
1446
+ Releases:
1447
+ ${releases}
1448
+
1449
+ ${checks}`;
1450
+ },
1451
+ async run(ctx) {
1452
+ const i = ctx.input;
1453
+ return ctx.api.get(await projectPath(ctx, "mobile"), {
1454
+ since: since(i.since),
1455
+ release: i.release,
1456
+ os: i.os,
1457
+ sort: i.sort,
1458
+ limit: i.limit
1459
+ });
1460
+ }
1461
+ };
1462
+ var mobile_default = [mobile];
1463
+
1340
1464
  // src/commands/issues.ts
1341
1465
  import { writeFileSync } from "node:fs";
1342
1466
  var FILTERS = {
@@ -1731,7 +1855,7 @@ ${(d.nextSteps ?? []).map((s, n) => `${n + 1}. ${s.action} -- ${s.why}`).join("\
1731
1855
  );
1732
1856
  }
1733
1857
  };
1734
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1858
+ var sleep2 = (ms2) => new Promise((r) => setTimeout(r, ms2));
1735
1859
  var verify = {
1736
1860
  path: ["projects", "verify"],
1737
1861
  aliases: [["verify"]],
@@ -1805,6 +1929,7 @@ var DECLARE = {
1805
1929
  threshold: { type: "string", value: "<events>/<minutes>", description: "Also alert when an issue gets this many events in this many minutes, e.g. 50/10." },
1806
1930
  uptime: { type: "boolean", description: "Alert when this project's monitors go down (default true)." },
1807
1931
  performance: { type: "boolean", description: "Alert when a performance budget is crossed or met again (default true; see `budgets set`). Rides on the new-issue or uptime rule." },
1932
+ "release-health": { type: "boolean", description: "Alert when a release's crash-free rate drops under the threshold (and recovers), or one issue spreads fast through a release (default true; see `releases verdict`). Rides on the new-issue or uptime rule." },
1808
1933
  "replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
1809
1934
  body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
1810
1935
  };
@@ -1841,6 +1966,7 @@ function declareBody(i) {
1841
1966
  threshold: threshold(i.threshold),
1842
1967
  uptime: i.uptime,
1843
1968
  performance: i.performance,
1969
+ releaseHealth: i.releaseHealth,
1844
1970
  replaceRecipients: i.replaceRecipients
1845
1971
  });
1846
1972
  }
@@ -1854,6 +1980,7 @@ ${table(d.rules ?? [], [
1854
1980
  ["threshold", (r) => r.threshold ? `${r.threshold.events}/${r.threshold.minutes}m` : "-"],
1855
1981
  ["uptime", (r) => r.uptime],
1856
1982
  ["perf", (r) => r.performance],
1983
+ ["release", (r) => r.releaseHealth],
1857
1984
  ["recipients", (r) => (r.recipients ?? []).map((x) => `${x.type}${x.delivery === "digest" ? "(digest)" : ""}`).join(", ")]
1858
1985
  ])}${(d.notes ?? []).length ? `
1859
1986
 
@@ -2153,7 +2280,49 @@ function commitList(values, git = runGit) {
2153
2280
  function runGit(args) {
2154
2281
  return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
2155
2282
  }
2156
- var releases_default = [list3, health2, declare];
2283
+ var verdict = {
2284
+ path: ["releases", "verdict"],
2285
+ summary: "Should the rollout of this release continue? Crash-free vs threshold and the previous build, issues spreading fast, new issues, budgets, regressions. Exit code 10 on fail.",
2286
+ description: "For a staged rollout step: `--wait 30m` re-asks every minute while the verdict is unknown (too few sessions yet). Unknown passes unless --strict. Thresholds are project settings (`settings set --crash-free-min/--min-sessions/--max-drop-pct/--velocity-pct/--velocity-min-events`). An upload token (project:releases) may run it.",
2287
+ context: "project",
2288
+ effect: "read",
2289
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/releases/{version}/verdict" },
2290
+ scopes: ["project:releases", "event:read", "project:read", "project:admin", "org:read"],
2291
+ args: [{ name: "version", required: true, description: "The release, e.g. shop-android@1.4.2+57." }],
2292
+ flags: {
2293
+ wait: { type: "string", value: "<duration>", description: "Keep asking (every 60 s) while the verdict is unknown, up to this long: 30m, 2h." },
2294
+ strict: { type: "boolean", description: "An unknown verdict fails too (exit 10)." }
2295
+ },
2296
+ examples: [
2297
+ { command: "tracksiy releases verdict shop-android@1.4.2+57 --wait 30m && fastlane supply --rollout 0.5", description: "Widen the rollout only if the 10% stage held." }
2298
+ ],
2299
+ output: "{release, previousRelease, verdict, health{sessions, crashed, crashFreeSessionsPct, ...}, previousHealth, newIssues[], velocity[], budgets[], regressions[], checks[{name, status, detail}], rules}",
2300
+ text: (d) => `${kv({ release: d.release, previous: d.previousRelease ?? "-", verdict: d.verdict, sessions: d.health?.sessions, "crash-free %": d.health?.crashFreeSessionsPct ?? "-", "previous crash-free %": d.previousHealth?.crashFreeSessionsPct ?? "-" })}
2301
+
2302
+ ${table(d.checks ?? [], ["name", "status", "detail"])}${(d.newIssues ?? []).length ? `
2303
+
2304
+ New issues:
2305
+ ${table(d.newIssues, ["short_id", "count", "title"])}` : ""}`,
2306
+ async run(ctx) {
2307
+ const path = await projectPath(ctx, `releases/${seg(ctx.input.version)}/verdict`);
2308
+ const waitSeconds = duration(ctx.input.wait, "wait") ?? 0;
2309
+ const deadline = Date.now() + waitSeconds * 1e3;
2310
+ let data = await ctx.api.get(path);
2311
+ while (data.verdict === "unknown" && Date.now() + 6e4 <= deadline) {
2312
+ await new Promise((r) => setTimeout(r, 6e4));
2313
+ data = await ctx.api.get(path);
2314
+ }
2315
+ if (data.verdict === "fail") {
2316
+ const failed = (data.checks ?? []).filter((c) => c.status === "fail").map((c) => `${c.name}: ${c.detail}`).join(" | ");
2317
+ throw new CheckFailed(`Release ${data.release} failed: ${failed}`, data, "Halt the rollout; `tracksiy releases list` and `tracksiy issues list --release` show what broke.");
2318
+ }
2319
+ if (ctx.input.strict && data.verdict === "unknown") {
2320
+ throw new CheckFailed(`Release ${data.release} could not be judged yet (${data.health?.sessions ?? 0} sessions).`, data, "Wait longer (--wait), or lower releaseHealth.minSessions.");
2321
+ }
2322
+ return data;
2323
+ }
2324
+ };
2325
+ var releases_default = [list3, health2, declare, verdict];
2157
2326
 
2158
2327
  // src/commands/artifacts.ts
2159
2328
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
@@ -2275,7 +2444,7 @@ function manifestFor(files, chunkSize) {
2275
2444
  });
2276
2445
  return { manifest, chunks };
2277
2446
  }
2278
- var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
2447
+ var sleep3 = (ms2) => new Promise((r) => setTimeout(r, ms2));
2279
2448
  function packManifest(manifest, maxFiles, maxBytes) {
2280
2449
  const units = [];
2281
2450
  for (const entry of manifest) {
@@ -3611,6 +3780,8 @@ var COMMANDS = [
3611
3780
  ...auth_default,
3612
3781
  ...fleet_default,
3613
3782
  ...perf_default,
3783
+ ...storage_default,
3784
+ ...mobile_default,
3614
3785
  ...issues_default,
3615
3786
  ...projects_default,
3616
3787
  ...alerts_default,
@@ -3624,7 +3795,7 @@ var COMMANDS = [
3624
3795
  ];
3625
3796
  var GROUPS = [
3626
3797
  ["Start", "init auth onboarding describe"],
3627
- ["Look", "overview changes health orgs projects issues monitors performance queries not-found releases alerts keys artifacts settings privacy-preview"],
3798
+ ["Look", "overview changes health storage orgs projects issues monitors performance mobile queries not-found releases alerts keys artifacts settings privacy-preview"],
3628
3799
  ["Ship", "sourcemaps proguard"],
3629
3800
  ["Manage", "tokens"],
3630
3801
  ["Talk to us", "support"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.9.1",
3
+ "version": "0.10.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": {