tracksiy 0.9.0 → 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 +3 -0
- package/dist/tracksiy.mjs +210 -19
- package/package.json +1 -1
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.
|
|
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 = (
|
|
84
|
+
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
85
85
|
var Api = class {
|
|
86
86
|
url;
|
|
87
87
|
hasToken;
|
|
@@ -822,7 +822,7 @@ var monitorsList = {
|
|
|
822
822
|
status: { type: "string", enum: ["all", "up", "down", "never"], description: "Filter by state. Default all." },
|
|
823
823
|
type: { type: "string", enum: ["heartbeat", "get", "ping", "ssl", "ingest"], description: "Only this monitor type." }
|
|
824
824
|
},
|
|
825
|
-
output: "{summary{total, up, down, never, unalertable}, monitors:[{id, name, project, monitorType, url, upNow, lastCheck, overdue, alertable, checkInUrl, downReason, lastCheckDetail{status, expectedStatus, bytes, truncated, expectedTextFound, forbiddenTextFound, error, ms}}], notes[]}",
|
|
825
|
+
output: "{summary{total, up, down, never, unalertable}, monitors:[{id, name, slug, project, monitorType, url, upNow, lastCheck, overdue, alertable, checkInUrl, downReason, lastCheckDetail{status, expectedStatus, bytes, truncated, expectedTextFound, forbiddenTextFound, error, ms}}], notes[]}",
|
|
826
826
|
text: (d) => `${kv(d.summary ?? {})}
|
|
827
827
|
|
|
828
828
|
${table(d.monitors ?? [], [
|
|
@@ -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",
|
|
@@ -1141,6 +1164,22 @@ var BUDGET_KEYS = {
|
|
|
1141
1164
|
minSamples: "minSamples"
|
|
1142
1165
|
};
|
|
1143
1166
|
var NUMERIC = /* @__PURE__ */ new Set(["max", "windowMinutes", "minSamples"]);
|
|
1167
|
+
BUDGET_KEYS["window_minutes"] = "windowMinutes";
|
|
1168
|
+
BUDGET_KEYS["min_samples"] = "minSamples";
|
|
1169
|
+
function normalizeBudget(raw) {
|
|
1170
|
+
const out = {};
|
|
1171
|
+
for (const [key, value] of Object.entries(raw ?? {})) {
|
|
1172
|
+
out[BUDGET_KEYS[key] ?? key] = value;
|
|
1173
|
+
}
|
|
1174
|
+
return out;
|
|
1175
|
+
}
|
|
1176
|
+
function budgetsBody(body) {
|
|
1177
|
+
const wrapped = Array.isArray(body) ? { budgets: body } : body;
|
|
1178
|
+
if (!wrapped || !Array.isArray(wrapped.budgets)) {
|
|
1179
|
+
throw usageError("bad_value", '--body must be {"budgets": [...]} or a list of budgets.', `e.g. --body '{"budgets": [{"name": "init", "transaction": "GET /api/v1/init", "metric": "p95", "max": 800, "windowMinutes": 60}]}'`);
|
|
1180
|
+
}
|
|
1181
|
+
return { ...wrapped, budgets: wrapped.budgets.map(normalizeBudget) };
|
|
1182
|
+
}
|
|
1144
1183
|
function parseBudget(raw) {
|
|
1145
1184
|
const out = {};
|
|
1146
1185
|
for (const part of String(raw).split(/,(?=\s*[\w-]+=)/)) {
|
|
@@ -1192,7 +1231,7 @@ var budgetsGet = {
|
|
|
1192
1231
|
var budgetsSet = {
|
|
1193
1232
|
path: ["budgets", "set"],
|
|
1194
1233
|
summary: "Declare performance budgets: told once when one is crossed and once when it is met again.",
|
|
1195
|
-
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).`,
|
|
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.`,
|
|
1196
1235
|
context: "project",
|
|
1197
1236
|
effect: "write",
|
|
1198
1237
|
idempotent: true,
|
|
@@ -1211,23 +1250,27 @@ var budgetsSet = {
|
|
|
1211
1250
|
examples: [
|
|
1212
1251
|
{ command: "tracksiy budgets set --budget 'name=init,transaction=GET /api/v1/init,metric=p95,max=800'", description: "Told when the init call's p95 passes 800 ms over an hour." },
|
|
1213
1252
|
{ command: "tracksiy budgets set --merge --budget 'name=inp,op=ui.interaction,metric=inp,max=200,window=1440'", description: "INP p75 over a day, across every route; keeps the other budgets." },
|
|
1214
|
-
{ command: "tracksiy budgets set --merge --budget 'name=backup,monitor=db-backup-s3,metric=duration_p95,max=1200000,window=10080,min-samples=3'", description: "Told before the nightly backup gets near its max runtime." }
|
|
1253
|
+
{ command: "tracksiy budgets set --merge --budget 'name=backup,monitor=db-backup-s3,metric=duration_p95,max=1200000,window=10080,min-samples=3'", description: "Told before the nightly backup gets near its max runtime." },
|
|
1254
|
+
{ command: "tracksiy budgets set --body @tools/tracksiy/budgets.json --dry-run", description: "The whole declaration from a file; shows the changes, writes nothing." },
|
|
1255
|
+
{ command: "tracksiy budgets set --merge --budget 'name=site-ttfb,monitor=41,metric=ttfb_p95,max=800'", description: "A monitor by its id (from `monitors list`), for one whose name has spaces." }
|
|
1215
1256
|
],
|
|
1216
1257
|
output: "The stored budgets and `changes` [{name, action}], same shape as `budgets get`.",
|
|
1217
1258
|
text: budgetsText,
|
|
1218
1259
|
async run(ctx) {
|
|
1219
1260
|
const path = await projectPath(ctx, "budgets");
|
|
1220
1261
|
const i = ctx.input;
|
|
1221
|
-
if (i.body
|
|
1222
|
-
|
|
1223
|
-
let budgets = (i.budget ?? []).map(parseBudget);
|
|
1262
|
+
if (!i.body && !i.budget?.length && !i.merge) throw usageError("missing_flag", "No --budget given.", `Pass --budget ... (repeatable), or --body. To delete every budget: --body '{"budgets": []}'.`);
|
|
1263
|
+
const fromBody = i.body ? budgetsBody(i.body) : void 0;
|
|
1264
|
+
let budgets = fromBody ? fromBody.budgets : (i.budget ?? []).map(parseBudget);
|
|
1265
|
+
if (fromBody && i.budget?.length) budgets = [...budgets, ...i.budget.map(parseBudget)];
|
|
1224
1266
|
if (i.merge) {
|
|
1225
1267
|
const current = await ctx.api.get(path);
|
|
1226
1268
|
const named = new Set(budgets.map((b) => b.name));
|
|
1227
1269
|
const kept = (current.budgets ?? []).filter((b) => !named.has(b.name)).map((b) => compact({ name: b.name, transaction: b.transaction ?? void 0, op: b.op ?? void 0, monitor: b.monitor ?? void 0, metric: b.metric, max: b.max, windowMinutes: b.windowMinutes, minSamples: b.minSamples }));
|
|
1228
1270
|
budgets = [...kept, ...budgets];
|
|
1229
1271
|
}
|
|
1230
|
-
|
|
1272
|
+
const dryRun = i.dryRun || fromBody?.dryRun || void 0;
|
|
1273
|
+
return ctx.api.send("PUT", path, compact({ budgets, dryRun }));
|
|
1231
1274
|
}
|
|
1232
1275
|
};
|
|
1233
1276
|
var budgetsCheck = {
|
|
@@ -1294,7 +1337,7 @@ var monitorsTiming = {
|
|
|
1294
1337
|
context: "project",
|
|
1295
1338
|
effect: "read",
|
|
1296
1339
|
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/monitors/{slug}/timing" },
|
|
1297
|
-
args: [{ name: "slug", required: true, description: "The monitor's slug (its name)
|
|
1340
|
+
args: [{ name: "slug", required: true, description: "The monitor's slug (its name; quote it when it has spaces), or its id from `monitors list`." }],
|
|
1298
1341
|
flags: { since: { type: "string", value: TIME, description: "Default 24h for HTTP, 7d for jobs; compared with the window before." } },
|
|
1299
1342
|
examples: [
|
|
1300
1343
|
{ command: "tracksiy monitors timing home", description: "Is the home page slow at the server, or on the wire?" },
|
|
@@ -1317,6 +1360,107 @@ ${table(
|
|
|
1317
1360
|
};
|
|
1318
1361
|
var perf_default = [budgetsGet, budgetsSet, budgetsCheck, findings, monitorsTiming];
|
|
1319
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
|
+
|
|
1320
1464
|
// src/commands/issues.ts
|
|
1321
1465
|
import { writeFileSync } from "node:fs";
|
|
1322
1466
|
var FILTERS = {
|
|
@@ -1711,7 +1855,7 @@ ${(d.nextSteps ?? []).map((s, n) => `${n + 1}. ${s.action} -- ${s.why}`).join("\
|
|
|
1711
1855
|
);
|
|
1712
1856
|
}
|
|
1713
1857
|
};
|
|
1714
|
-
var sleep2 = (
|
|
1858
|
+
var sleep2 = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
1715
1859
|
var verify = {
|
|
1716
1860
|
path: ["projects", "verify"],
|
|
1717
1861
|
aliases: [["verify"]],
|
|
@@ -1785,6 +1929,7 @@ var DECLARE = {
|
|
|
1785
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." },
|
|
1786
1930
|
uptime: { type: "boolean", description: "Alert when this project's monitors go down (default true)." },
|
|
1787
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." },
|
|
1788
1933
|
"replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
|
|
1789
1934
|
body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
|
|
1790
1935
|
};
|
|
@@ -1821,6 +1966,7 @@ function declareBody(i) {
|
|
|
1821
1966
|
threshold: threshold(i.threshold),
|
|
1822
1967
|
uptime: i.uptime,
|
|
1823
1968
|
performance: i.performance,
|
|
1969
|
+
releaseHealth: i.releaseHealth,
|
|
1824
1970
|
replaceRecipients: i.replaceRecipients
|
|
1825
1971
|
});
|
|
1826
1972
|
}
|
|
@@ -1834,6 +1980,7 @@ ${table(d.rules ?? [], [
|
|
|
1834
1980
|
["threshold", (r) => r.threshold ? `${r.threshold.events}/${r.threshold.minutes}m` : "-"],
|
|
1835
1981
|
["uptime", (r) => r.uptime],
|
|
1836
1982
|
["perf", (r) => r.performance],
|
|
1983
|
+
["release", (r) => r.releaseHealth],
|
|
1837
1984
|
["recipients", (r) => (r.recipients ?? []).map((x) => `${x.type}${x.delivery === "digest" ? "(digest)" : ""}`).join(", ")]
|
|
1838
1985
|
])}${(d.notes ?? []).length ? `
|
|
1839
1986
|
|
|
@@ -2133,7 +2280,49 @@ function commitList(values, git = runGit) {
|
|
|
2133
2280
|
function runGit(args) {
|
|
2134
2281
|
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
2135
2282
|
}
|
|
2136
|
-
var
|
|
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];
|
|
2137
2326
|
|
|
2138
2327
|
// src/commands/artifacts.ts
|
|
2139
2328
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -2255,7 +2444,7 @@ function manifestFor(files, chunkSize) {
|
|
|
2255
2444
|
});
|
|
2256
2445
|
return { manifest, chunks };
|
|
2257
2446
|
}
|
|
2258
|
-
var sleep3 = (
|
|
2447
|
+
var sleep3 = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
2259
2448
|
function packManifest(manifest, maxFiles, maxBytes) {
|
|
2260
2449
|
const units = [];
|
|
2261
2450
|
for (const entry of manifest) {
|
|
@@ -3591,6 +3780,8 @@ var COMMANDS = [
|
|
|
3591
3780
|
...auth_default,
|
|
3592
3781
|
...fleet_default,
|
|
3593
3782
|
...perf_default,
|
|
3783
|
+
...storage_default,
|
|
3784
|
+
...mobile_default,
|
|
3594
3785
|
...issues_default,
|
|
3595
3786
|
...projects_default,
|
|
3596
3787
|
...alerts_default,
|
|
@@ -3604,7 +3795,7 @@ var COMMANDS = [
|
|
|
3604
3795
|
];
|
|
3605
3796
|
var GROUPS = [
|
|
3606
3797
|
["Start", "init auth onboarding describe"],
|
|
3607
|
-
["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"],
|
|
3608
3799
|
["Ship", "sourcemaps proguard"],
|
|
3609
3800
|
["Manage", "tokens"],
|
|
3610
3801
|
["Talk to us", "support"],
|