tracksiy 0.8.0 → 0.9.1
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 +5 -0
- package/dist/tracksiy.mjs +233 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,6 +132,11 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
|
|
|
132
132
|
| `settings` | The project's reading rules: environments, grouping rules, privacy rules, source roots. | read | `settings` |
|
|
133
133
|
| `settings set` | Change the project's reading rules. Only the flags given change; the rest are kept. | write | `settings_set` |
|
|
134
134
|
| `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` |
|
|
135
|
+
| `budgets get` | The project's performance budgets and each one's last verdict (judged every 5 minutes). | read | `budgets_get` |
|
|
136
|
+
| `budgets set` | Declare performance budgets: told once when one is crossed and once when it is met again. | write | `budgets_set` |
|
|
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
|
+
| `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
|
+
| `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` |
|
|
135
140
|
| `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
|
|
136
141
|
| `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
|
|
137
142
|
| `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.
|
|
7
|
+
var VERSION = true ? "0.9.1" : "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
|
|
@@ -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 ?? [], [
|
|
@@ -946,7 +946,7 @@ var performance = {
|
|
|
946
946
|
flags: {
|
|
947
947
|
since: { type: "string", value: TIME, description: "Window start (default: server's, 24h; 7d with --release)." },
|
|
948
948
|
op: { type: "string", value: "<op>", description: "Only this operation, e.g. http.server or pageload." },
|
|
949
|
-
sort: { type: "string", enum: ["slower", "duration", "p95", "failures"], description: "Order (default: slower first)." },
|
|
949
|
+
sort: { type: "string", enum: ["slower", "duration", "p95", "failures", "impact"], description: "Order (default: slower first). impact: total time taken (count x mean), the route that costs the most overall." },
|
|
950
950
|
limit: { type: "integer", value: "<n>", description: "How many transactions." },
|
|
951
951
|
release: { type: "string", value: "<version>", description: "Compare this release's transactions with --against instead of two time windows." },
|
|
952
952
|
against: { type: "string", value: "<version>", description: "The release to compare with (default: the one before --release)." }
|
|
@@ -954,9 +954,11 @@ var performance = {
|
|
|
954
954
|
examples: [
|
|
955
955
|
{ command: "tracksiy performance --sort p95", description: "The slowest routes by p95." },
|
|
956
956
|
{ command: "tracksiy performance --release web@$(git rev-parse HEAD)", description: "Did this deploy make anything slower or failing?" },
|
|
957
|
-
{ command: "tracksiy performance --op pageload --select transactions.transaction,transactions.current.webVitals", description: "Core Web Vitals (p75) per page." }
|
|
957
|
+
{ command: "tracksiy performance --op pageload --select transactions.transaction,transactions.current.webVitals", description: "Core Web Vitals (p75) per page." },
|
|
958
|
+
{ command: "tracksiy performance --sort impact", description: "Where the server's time goes: count x mean, with each route's share." },
|
|
959
|
+
{ command: "tracksiy performance --op ui.interaction --sort p95", description: "The slowest interactions (INP) by route." }
|
|
958
960
|
],
|
|
959
|
-
output: "{since, until, release?, againstRelease?, transactions:[{transaction, op, current{count, avgDurationMs, p50Ms, p95Ms, p99Ms, failureRate, failed, status, webVitals}, previous{...}, changeRatio, slower}], uptime[...], blindSpots[...]}",
|
|
961
|
+
output: "{since, until, release?, againstRelease?, transactions:[{transaction, op, current{count, avgDurationMs, p50Ms, p95Ms, p99Ms, failureRate, failed, status, webVitals}, previous{...}, changeRatio, slower, totalTimeMs, timeShare}], uptime[...], blindSpots[...]}",
|
|
960
962
|
text: (d) => `${table(d.transactions ?? [], [
|
|
961
963
|
"transaction",
|
|
962
964
|
"op",
|
|
@@ -966,7 +968,8 @@ var performance = {
|
|
|
966
968
|
["fail", (r) => r.current?.failureRate == null ? "-" : `${(r.current.failureRate * 100).toFixed(1)}%`],
|
|
967
969
|
["count", (r) => r.current?.count ?? "-"],
|
|
968
970
|
["change", (r) => r.changeRatio == null ? "-" : `${r.changeRatio > 0 ? "+" : ""}${Math.round(r.changeRatio * 100)}%`],
|
|
969
|
-
["slower", (r) => r.slower]
|
|
971
|
+
["slower", (r) => r.slower],
|
|
972
|
+
["share", (r) => r.timeShare == null ? "-" : `${(r.timeShare * 100).toFixed(1)}%`]
|
|
970
973
|
])}${(d.uptime ?? []).length ? `
|
|
971
974
|
|
|
972
975
|
uptime:
|
|
@@ -1001,7 +1004,7 @@ var queries = {
|
|
|
1001
1004
|
{ command: "tracksiy queries --since 24h", description: "The queries that cost the most time today." },
|
|
1002
1005
|
{ command: "tracksiy queries --sort perTransaction --select queries.query,queries.maxPerTransaction,queries.routes", description: "N+1 candidates." }
|
|
1003
1006
|
],
|
|
1004
|
-
output: "{since, until, queries:[{fingerprint, op, query, count, transactions, perTransaction, maxPerTransaction, totalMs, avgMs, maxMs, previous{count, avgMs}, changeRatio, nPlusOne, routes[{transaction, count, avgMs}]}], note}",
|
|
1007
|
+
output: "{since, until, queries:[{fingerprint, op, query, count, transactions, perTransaction, maxPerTransaction, totalMs, avgMs, maxMs, previous{count, avgMs}, changeRatio, nPlusOne, routes[{transaction, count, avgMs}], origin{file, line, function, repoPath}}], note}",
|
|
1005
1008
|
text: (d) => table(d.queries ?? [], [
|
|
1006
1009
|
["total ms", (r) => r.totalMs],
|
|
1007
1010
|
["avg", (r) => r.avgMs ?? "-"],
|
|
@@ -1009,7 +1012,8 @@ var queries = {
|
|
|
1009
1012
|
["count", (r) => r.count],
|
|
1010
1013
|
["per req", (r) => r.perTransaction],
|
|
1011
1014
|
["n+1", (r) => r.nPlusOne ? "yes" : ""],
|
|
1012
|
-
["query", (r) => String(r.query).slice(0, 90)]
|
|
1015
|
+
["query", (r) => String(r.query).slice(0, 90)],
|
|
1016
|
+
["where", (r) => r.origin ? `${r.origin.repoPath ?? r.origin.file}${r.origin.line ? `:${r.origin.line}` : ""}` : ""]
|
|
1013
1017
|
]),
|
|
1014
1018
|
async run(ctx) {
|
|
1015
1019
|
return ctx.api.get(await projectPath(ctx, "queries"), {
|
|
@@ -1046,7 +1050,7 @@ var settingsGet = {
|
|
|
1046
1050
|
context: "project",
|
|
1047
1051
|
effect: "read",
|
|
1048
1052
|
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/settings" },
|
|
1049
|
-
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
|
|
1053
|
+
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues}",
|
|
1050
1054
|
async run(ctx) {
|
|
1051
1055
|
return ctx.api.get(await projectPath(ctx, "settings"));
|
|
1052
1056
|
}
|
|
@@ -1077,6 +1081,7 @@ var settingsSet = {
|
|
|
1077
1081
|
"source-root": { type: "list", split: false, value: "<path> | strip=<path>,prepend=<dir/>[,surface=<surface>]", description: "How frame paths become repo paths: a prefix to strip, or strip + prepend, optionally for one surface. Each rule is tried on the frame's abs_path first (sentry-laravel: the full server path, /home/admin/web/shop/backend/app/...), then on filename (/app/...). Repeatable; replaces the list." },
|
|
1078
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." },
|
|
1079
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
|
+
"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." },
|
|
1080
1085
|
body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
|
|
1081
1086
|
},
|
|
1082
1087
|
examples: [
|
|
@@ -1085,7 +1090,7 @@ var settingsSet = {
|
|
|
1085
1090
|
{ command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." },
|
|
1086
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." }
|
|
1087
1092
|
],
|
|
1088
|
-
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
|
|
1093
|
+
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues}",
|
|
1089
1094
|
async run(ctx) {
|
|
1090
1095
|
const path = await projectPath(ctx, "settings");
|
|
1091
1096
|
const i = ctx.input;
|
|
@@ -1096,7 +1101,8 @@ var settingsSet = {
|
|
|
1096
1101
|
environments: i.environments ? i.environments.filter((e) => e) : current.environments ?? [],
|
|
1097
1102
|
environmentPolicy: i.environmentPolicy ?? current.environmentPolicy ?? "flag",
|
|
1098
1103
|
fingerprintRules: pairs(i.fingerprintRule, "fingerprint-rule") ?? current.fingerprintRules ?? [],
|
|
1099
|
-
privacyRules: pairs(i.privacyRule, "privacy-rule") ?? current.privacyRules ?? []
|
|
1104
|
+
privacyRules: pairs(i.privacyRule, "privacy-rule") ?? current.privacyRules ?? [],
|
|
1105
|
+
performanceIssues: i.performanceIssues ?? current.performanceIssues ?? true
|
|
1100
1106
|
};
|
|
1101
1107
|
return ctx.api.send("PUT", path, next);
|
|
1102
1108
|
}
|
|
@@ -1119,6 +1125,218 @@ var privacyPreview = {
|
|
|
1119
1125
|
};
|
|
1120
1126
|
var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, monitorsDeleted, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
|
|
1121
1127
|
|
|
1128
|
+
// 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)";
|
|
1130
|
+
var BUDGET_KEYS = {
|
|
1131
|
+
name: "name",
|
|
1132
|
+
transaction: "transaction",
|
|
1133
|
+
op: "op",
|
|
1134
|
+
monitor: "monitor",
|
|
1135
|
+
metric: "metric",
|
|
1136
|
+
max: "max",
|
|
1137
|
+
window: "windowMinutes",
|
|
1138
|
+
"window-minutes": "windowMinutes",
|
|
1139
|
+
windowMinutes: "windowMinutes",
|
|
1140
|
+
"min-samples": "minSamples",
|
|
1141
|
+
minSamples: "minSamples"
|
|
1142
|
+
};
|
|
1143
|
+
var NUMERIC = /* @__PURE__ */ new Set(["max", "windowMinutes", "minSamples"]);
|
|
1144
|
+
BUDGET_KEYS["window_minutes"] = "windowMinutes";
|
|
1145
|
+
BUDGET_KEYS["min_samples"] = "minSamples";
|
|
1146
|
+
function normalizeBudget(raw) {
|
|
1147
|
+
const out = {};
|
|
1148
|
+
for (const [key, value] of Object.entries(raw ?? {})) {
|
|
1149
|
+
out[BUDGET_KEYS[key] ?? key] = value;
|
|
1150
|
+
}
|
|
1151
|
+
return out;
|
|
1152
|
+
}
|
|
1153
|
+
function budgetsBody(body) {
|
|
1154
|
+
const wrapped = Array.isArray(body) ? { budgets: body } : body;
|
|
1155
|
+
if (!wrapped || !Array.isArray(wrapped.budgets)) {
|
|
1156
|
+
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}]}'`);
|
|
1157
|
+
}
|
|
1158
|
+
return { ...wrapped, budgets: wrapped.budgets.map(normalizeBudget) };
|
|
1159
|
+
}
|
|
1160
|
+
function parseBudget(raw) {
|
|
1161
|
+
const out = {};
|
|
1162
|
+
for (const part of String(raw).split(/,(?=\s*[\w-]+=)/)) {
|
|
1163
|
+
const eq = part.indexOf("=");
|
|
1164
|
+
if (eq < 1) throw usageError("bad_value", `--budget expects key=value,..., got ${JSON.stringify(raw)}`, "e.g. --budget 'name=init,transaction=GET /api/v1/init,metric=p95,max=800'");
|
|
1165
|
+
const key = part.slice(0, eq).trim();
|
|
1166
|
+
const field = BUDGET_KEYS[key];
|
|
1167
|
+
if (!field) throw usageError("bad_value", `--budget: unknown key ${JSON.stringify(key)}`, `Keys: name, transaction, op, monitor, metric, max, window, min-samples.`);
|
|
1168
|
+
const value = part.slice(eq + 1).trim();
|
|
1169
|
+
if (NUMERIC.has(field)) {
|
|
1170
|
+
const n = Number(value);
|
|
1171
|
+
if (!Number.isFinite(n)) throw usageError("bad_value", `--budget ${key}=${value} is not a number`);
|
|
1172
|
+
out[field] = n;
|
|
1173
|
+
} else {
|
|
1174
|
+
out[field] = value;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
for (const required of ["name", "metric", "max"]) {
|
|
1178
|
+
if (out[required] === void 0) throw usageError("bad_value", `--budget ${JSON.stringify(raw)} has no ${required}=`);
|
|
1179
|
+
}
|
|
1180
|
+
return out;
|
|
1181
|
+
}
|
|
1182
|
+
var budgetsText = (d) => `${table(d.budgets ?? [], [
|
|
1183
|
+
"name",
|
|
1184
|
+
["target", (r) => r.monitor ? `monitor ${r.monitor}` : [r.op, r.transaction].filter(Boolean).join(" ") || "every transaction"],
|
|
1185
|
+
"metric",
|
|
1186
|
+
["max", (r) => `${r.max}${r.unit === "ms" ? " ms" : ""}`],
|
|
1187
|
+
["value", (r) => r.value == null ? "-" : `${Math.round(r.value * 1e3) / 1e3}`],
|
|
1188
|
+
["status", (r) => r.status ?? "not yet"],
|
|
1189
|
+
["samples", (r) => r.samples],
|
|
1190
|
+
["since", (r) => r.breachedSince ? ago(r.breachedSince) : ""]
|
|
1191
|
+
])}${(d.changes ?? []).length ? `
|
|
1192
|
+
|
|
1193
|
+
changes:
|
|
1194
|
+
${table(d.changes, ["name", "action"])}` : ""}`;
|
|
1195
|
+
var budgetsGet = {
|
|
1196
|
+
path: ["budgets", "get"],
|
|
1197
|
+
aliases: [["budgets"]],
|
|
1198
|
+
summary: "The project's performance budgets and each one's last verdict (judged every 5 minutes).",
|
|
1199
|
+
context: "project",
|
|
1200
|
+
effect: "read",
|
|
1201
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/budgets" },
|
|
1202
|
+
output: "{budgets:[{name, transaction, op, monitor, metric, max, unit, windowMinutes, minSamples, status, value, samples, evaluatedAt, breachedSince}], notes[]}",
|
|
1203
|
+
text: budgetsText,
|
|
1204
|
+
async run(ctx) {
|
|
1205
|
+
return ctx.api.get(await projectPath(ctx, "budgets"));
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
var budgetsSet = {
|
|
1209
|
+
path: ["budgets", "set"],
|
|
1210
|
+
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.`,
|
|
1212
|
+
context: "project",
|
|
1213
|
+
effect: "write",
|
|
1214
|
+
idempotent: true,
|
|
1215
|
+
endpoint: { method: "PUT", path: "/api/agent/v1/projects/{org}/{project}/budgets" },
|
|
1216
|
+
flags: {
|
|
1217
|
+
budget: {
|
|
1218
|
+
type: "list",
|
|
1219
|
+
split: false,
|
|
1220
|
+
value: "name=<n>,metric=<m>,max=<x>[,transaction=<route|glob*>][,op=<op>][,monitor=<slug>][,window=<min>][,min-samples=<n>]",
|
|
1221
|
+
description: "One budget. Repeatable."
|
|
1222
|
+
},
|
|
1223
|
+
merge: { type: "boolean", description: "Keep the stored budgets not named here (default: the list is the whole declaration)." },
|
|
1224
|
+
"dry-run": { type: "boolean", description: "Report the changes without writing." },
|
|
1225
|
+
body: { type: "json", value: "<json|@file|->", description: "Raw body {budgets:[...]}; overrides the flags." }
|
|
1226
|
+
},
|
|
1227
|
+
examples: [
|
|
1228
|
+
{ 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." },
|
|
1229
|
+
{ 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." },
|
|
1230
|
+
{ 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." },
|
|
1231
|
+
{ command: "tracksiy budgets set --body @tools/tracksiy/budgets.json --dry-run", description: "The whole declaration from a file; shows the changes, writes nothing." },
|
|
1232
|
+
{ 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." }
|
|
1233
|
+
],
|
|
1234
|
+
output: "The stored budgets and `changes` [{name, action}], same shape as `budgets get`.",
|
|
1235
|
+
text: budgetsText,
|
|
1236
|
+
async run(ctx) {
|
|
1237
|
+
const path = await projectPath(ctx, "budgets");
|
|
1238
|
+
const i = ctx.input;
|
|
1239
|
+
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": []}'.`);
|
|
1240
|
+
const fromBody = i.body ? budgetsBody(i.body) : void 0;
|
|
1241
|
+
let budgets = fromBody ? fromBody.budgets : (i.budget ?? []).map(parseBudget);
|
|
1242
|
+
if (fromBody && i.budget?.length) budgets = [...budgets, ...i.budget.map(parseBudget)];
|
|
1243
|
+
if (i.merge) {
|
|
1244
|
+
const current = await ctx.api.get(path);
|
|
1245
|
+
const named = new Set(budgets.map((b) => b.name));
|
|
1246
|
+
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 }));
|
|
1247
|
+
budgets = [...kept, ...budgets];
|
|
1248
|
+
}
|
|
1249
|
+
const dryRun = i.dryRun || fromBody?.dryRun || void 0;
|
|
1250
|
+
return ctx.api.send("PUT", path, compact({ budgets, dryRun }));
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
var budgetsCheck = {
|
|
1254
|
+
path: ["budgets", "check"],
|
|
1255
|
+
summary: "Judge every budget now -- for one release with --release. Exit code 10 if one is crossed. For a deploy to stop on.",
|
|
1256
|
+
description: "With --release, transaction budgets look only at that release's transactions since it was first known (run it after the canary or the first minutes of traffic). Monitor budgets use their window. Writes nothing and alerts nobody. An unknown verdict (too few samples) passes unless --strict. An upload token (project:releases) may run it.",
|
|
1257
|
+
context: "project",
|
|
1258
|
+
effect: "read",
|
|
1259
|
+
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/budgets/check" },
|
|
1260
|
+
scopes: ["project:releases", "event:read", "project:read", "project:write", "project:admin"],
|
|
1261
|
+
flags: {
|
|
1262
|
+
release: { type: "string", value: "<version>", description: "Judge transaction budgets on this release only." },
|
|
1263
|
+
since: { type: "string", value: TIME, description: "Instead of each budget's window: from this time." },
|
|
1264
|
+
strict: { type: "boolean", description: "An unknown verdict (too few samples) fails too." }
|
|
1265
|
+
},
|
|
1266
|
+
examples: [
|
|
1267
|
+
{ command: "tracksiy budgets check --release web@$(git rev-parse HEAD)", description: "In the deploy, after a few minutes of traffic: stop if the release broke a budget." }
|
|
1268
|
+
],
|
|
1269
|
+
output: "{ok, failed, unknown, release, results:[{name, metric, max, status, value, samples, minSamples, scope}]}",
|
|
1270
|
+
text: (d) => `${kv({ ok: d.ok, failed: d.failed, unknown: d.unknown, release: d.release ?? "-" })}
|
|
1271
|
+
|
|
1272
|
+
${table(d.results ?? [], ["name", "metric", "max", ["value", (r) => r.value ?? "-"], "status", "samples", "scope"])}`,
|
|
1273
|
+
async run(ctx) {
|
|
1274
|
+
const data = await ctx.api.send("POST", await projectPath(ctx, "budgets/check"), compact({ release: ctx.input.release, since: since(ctx.input.since) }));
|
|
1275
|
+
const unknown = (data.results ?? []).filter((r) => r.status === "unknown").length;
|
|
1276
|
+
if (data.failed) {
|
|
1277
|
+
const names = data.results.filter((r) => r.status === "fail").map((r) => r.name).join(", ");
|
|
1278
|
+
throw new CheckFailed(`${data.failed} budget(s) crossed: ${names}.`, data, "See `tracksiy performance --release <r>` and `tracksiy queries` for what got slow.");
|
|
1279
|
+
}
|
|
1280
|
+
if (ctx.input.strict && unknown) {
|
|
1281
|
+
throw new CheckFailed(`${unknown} budget(s) could not be judged (too few samples).`, data, "Wait for more traffic, or lower minSamples.");
|
|
1282
|
+
}
|
|
1283
|
+
return data;
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
var findings = {
|
|
1287
|
+
path: ["performance", "findings"],
|
|
1288
|
+
summary: "Routes that got slower after a release and N+1 queries, which Tracksiy raised as issues on its own.",
|
|
1289
|
+
context: "project",
|
|
1290
|
+
effect: "read",
|
|
1291
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/performance/findings" },
|
|
1292
|
+
flags: {
|
|
1293
|
+
since: { type: "string", value: TIME, description: "Default 30 days." },
|
|
1294
|
+
kind: { type: "string", enum: ["regression", "n_plus_one"], description: "Only one kind." },
|
|
1295
|
+
limit: { type: "integer", value: "<n>", description: "How many (max 200)." }
|
|
1296
|
+
},
|
|
1297
|
+
examples: [{ command: "tracksiy performance findings --kind regression --since 7d", description: "What the last deploys made slower." }],
|
|
1298
|
+
output: "{enabled, findings:[{kind, release, created, issue{id, title, status}, data}], note}",
|
|
1299
|
+
text: (d) => `${d.enabled ? "" : "performanceIssues is OFF for this project (settings set --performance-issues).\n\n"}${table(d.findings ?? [], [
|
|
1300
|
+
"kind",
|
|
1301
|
+
["release", (r) => r.release ?? "-"],
|
|
1302
|
+
["issue", (r) => r.issue?.id ?? "-"],
|
|
1303
|
+
["status", (r) => r.issue?.status ?? "-"],
|
|
1304
|
+
["what", (r) => String(r.issue?.title ?? r.data?.transaction ?? "").slice(0, 100)],
|
|
1305
|
+
["when", (r) => ago(r.created)]
|
|
1306
|
+
])}`,
|
|
1307
|
+
async run(ctx) {
|
|
1308
|
+
return ctx.api.get(await projectPath(ctx, "performance/findings"), { since: since(ctx.input.since), kind: ctx.input.kind, limit: ctx.input.limit });
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
var monitorsTiming = {
|
|
1312
|
+
path: ["monitors", "timing"],
|
|
1313
|
+
summary: "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.",
|
|
1314
|
+
context: "project",
|
|
1315
|
+
effect: "read",
|
|
1316
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/monitors/{slug}/timing" },
|
|
1317
|
+
args: [{ name: "slug", required: true, description: "The monitor's slug (its name; quote it when it has spaces), or its id from `monitors list`." }],
|
|
1318
|
+
flags: { since: { type: "string", value: TIME, description: "Default 24h for HTTP, 7d for jobs; compared with the window before." } },
|
|
1319
|
+
examples: [
|
|
1320
|
+
{ command: "tracksiy monitors timing home", description: "Is the home page slow at the server, or on the wire?" },
|
|
1321
|
+
{ command: "tracksiy monitors timing db-backup-s3 --since 30d", description: "Is the backup getting slower?" }
|
|
1322
|
+
],
|
|
1323
|
+
output: "{monitor, type, since, until, checks, http{phases{dns, connect, wait, ttfb, download, total: {p50, p95}}, bytes{p50, p95}, previousTtfbP95, ttfbChangeRatio, latest}, job{runs, p50Ms, p95Ms, maxMs, previousP95Ms, changeRatio, maxRuntimeMs, headroom, slowing}, note}",
|
|
1324
|
+
text: (d) => {
|
|
1325
|
+
if (d.job) return kv({ monitor: d.monitor, checks: d.checks, ...d.job });
|
|
1326
|
+
const phases = d.http?.phases ?? {};
|
|
1327
|
+
return `${kv({ monitor: d.monitor, type: d.type, checks: d.checks, "ttfb p95 before": d.http?.previousTtfbP95 ?? "-", "ttfb change": d.http?.ttfbChangeRatio ?? "-", bytes: d.http?.bytes?.p50 ?? "-" })}
|
|
1328
|
+
|
|
1329
|
+
${table(
|
|
1330
|
+
Object.entries(phases).map(([phase, v]) => ({ phase, p50: v.p50, p95: v.p95 })),
|
|
1331
|
+
["phase", "p50", "p95"]
|
|
1332
|
+
)}`;
|
|
1333
|
+
},
|
|
1334
|
+
async run(ctx) {
|
|
1335
|
+
return ctx.api.get(await projectPath(ctx, `monitors/${seg(ctx.input.slug)}/timing`), { since: since(ctx.input.since) });
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
var perf_default = [budgetsGet, budgetsSet, budgetsCheck, findings, monitorsTiming];
|
|
1339
|
+
|
|
1122
1340
|
// src/commands/issues.ts
|
|
1123
1341
|
import { writeFileSync } from "node:fs";
|
|
1124
1342
|
var FILTERS = {
|
|
@@ -1586,6 +1804,7 @@ var DECLARE = {
|
|
|
1586
1804
|
"new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
|
|
1587
1805
|
threshold: { type: "string", value: "<events>/<minutes>", description: "Also alert when an issue gets this many events in this many minutes, e.g. 50/10." },
|
|
1588
1806
|
uptime: { type: "boolean", description: "Alert when this project's monitors go down (default true)." },
|
|
1807
|
+
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." },
|
|
1589
1808
|
"replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
|
|
1590
1809
|
body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
|
|
1591
1810
|
};
|
|
@@ -1621,6 +1840,7 @@ function declareBody(i) {
|
|
|
1621
1840
|
newIssue: i.newIssue,
|
|
1622
1841
|
threshold: threshold(i.threshold),
|
|
1623
1842
|
uptime: i.uptime,
|
|
1843
|
+
performance: i.performance,
|
|
1624
1844
|
replaceRecipients: i.replaceRecipients
|
|
1625
1845
|
});
|
|
1626
1846
|
}
|
|
@@ -1633,6 +1853,7 @@ ${table(d.rules ?? [], [
|
|
|
1633
1853
|
["new issue", (r) => r.newIssue],
|
|
1634
1854
|
["threshold", (r) => r.threshold ? `${r.threshold.events}/${r.threshold.minutes}m` : "-"],
|
|
1635
1855
|
["uptime", (r) => r.uptime],
|
|
1856
|
+
["perf", (r) => r.performance],
|
|
1636
1857
|
["recipients", (r) => (r.recipients ?? []).map((x) => `${x.type}${x.delivery === "digest" ? "(digest)" : ""}`).join(", ")]
|
|
1637
1858
|
])}${(d.notes ?? []).length ? `
|
|
1638
1859
|
|
|
@@ -3389,6 +3610,7 @@ var COMMANDS = [
|
|
|
3389
3610
|
...init_default,
|
|
3390
3611
|
...auth_default,
|
|
3391
3612
|
...fleet_default,
|
|
3613
|
+
...perf_default,
|
|
3392
3614
|
...issues_default,
|
|
3393
3615
|
...projects_default,
|
|
3394
3616
|
...alerts_default,
|