tracksiy 0.5.0 → 0.7.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 +6 -0
- package/dist/tracksiy.mjs +202 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,14 +125,20 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
|
|
|
125
125
|
| `monitors list` | Every monitor of the organization with its state, including monitors no project owns. | read | `monitors_list` |
|
|
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
|
+
| `monitors deleted` | Monitors deleted from this project: what each was, its last check, when and by whom. | read | `monitors_deleted` |
|
|
128
129
|
| `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
|
|
129
130
|
| `queries` | Which database queries and outbound HTTP calls make this project slow, with N+1 detection and the routes that run them. | read | `queries` |
|
|
131
|
+
| `not-found` | Which pages answer 404, and who links to them: broken internal links first. | read | `not_found` |
|
|
132
|
+
| `settings` | The project's reading rules: environments, grouping rules, privacy rules, source roots. | read | `settings` |
|
|
133
|
+
| `settings set` | Change the project's reading rules. Only the flags given change; the rest are kept. | write | `settings_set` |
|
|
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` |
|
|
130
135
|
| `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
|
|
131
136
|
| `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
|
|
132
137
|
| `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
|
|
133
138
|
| `issues ignore <ids...>` | Mark issues ignored: they stop alerting until unignored. Records why. | write | `issues_ignore` |
|
|
134
139
|
| `issues unresolve <ids...>` | Reopen issues. | write | `issues_unresolve` |
|
|
135
140
|
| `issues bulk-status` | Change the status of every issue matching a filter. Dry run unless --apply. | write | `issues_bulk_status` |
|
|
141
|
+
| `issues merge <ids...>` | Fold issues that are one root cause into one. Dry run unless --apply; later events of any of them land on the target. | write | `issues_merge` |
|
|
136
142
|
| `issues comment <id>` | Leave a comment on an issue for the next agent or human: what you found, what you tried. | write | `issues_comment` |
|
|
137
143
|
| `issues activity <id>` | Who did what to an issue and why: resolutions (and in which release), regressions, comments, claims. | read | `issues_activity` |
|
|
138
144
|
| `issues verify-fixed <id>` | Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed. | read | `issues_verify_fixed` |
|
package/dist/tracksiy.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
|
-
var VERSION = true ? "0.
|
|
7
|
+
var VERSION = true ? "0.7.0" : "0.0.0-dev";
|
|
8
8
|
var USER_AGENT = `tracksiy-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
|
|
9
9
|
|
|
10
10
|
// src/core/errors.ts
|
|
@@ -116,6 +116,7 @@ var Api = class {
|
|
|
116
116
|
const useAuth = opts.auth !== false;
|
|
117
117
|
if (useAuth && this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
|
|
118
118
|
if (this.opts.agent) headers["X-Tracksiy-Agent"] = this.opts.agent;
|
|
119
|
+
if (method !== "GET" && method !== "HEAD") headers["Idempotency-Key"] = opts.idempotencyKey ?? crypto.randomUUID();
|
|
119
120
|
let body;
|
|
120
121
|
if (opts.form) body = opts.form;
|
|
121
122
|
else if (opts.body !== void 0) {
|
|
@@ -845,7 +846,7 @@ var monitorsDeclare = {
|
|
|
845
846
|
idempotent: true,
|
|
846
847
|
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/monitors" },
|
|
847
848
|
flags: {
|
|
848
|
-
heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s)." },
|
|
849
|
+
heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s). `slug=seconds` works too." },
|
|
849
850
|
get: { type: "list", value: "<slug>=<url>", description: "HTTP GET check expecting 200." },
|
|
850
851
|
ping: { type: "list", value: "<slug>=<url>", description: "Reachability check." },
|
|
851
852
|
ssl: { type: "list", value: "<slug>=<url>", description: "TLS certificate check (expiry is reported on every check)." },
|
|
@@ -855,6 +856,7 @@ var monitorsDeclare = {
|
|
|
855
856
|
"expect-no-text": { type: "string", value: "<text>", description: "--get: down if the body contains this, e.g. Whoops or Exception." },
|
|
856
857
|
"user-agent": { type: "string", value: "<ua>", description: "--get/--ping: ask as this User-Agent, e.g. GPTBot/1.0, to see what crawlers get." },
|
|
857
858
|
"dry-run": { type: "boolean", description: "Show what would be created, adopted or changed; write nothing." },
|
|
859
|
+
prune: { type: "boolean", description: "The declaration is the whole list: delete this project's monitors of the declared types it does not name (listed in `pruned`)." },
|
|
858
860
|
body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the shorthand flags." }
|
|
859
861
|
},
|
|
860
862
|
examples: [
|
|
@@ -869,8 +871,9 @@ var monitorsDeclare = {
|
|
|
869
871
|
if (!body) {
|
|
870
872
|
const monitors = [];
|
|
871
873
|
for (const h of i.heartbeat ?? []) {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
+
const m = /^([^:=\s]+)(?:[:=](\d+))?$/.exec(String(h).trim());
|
|
875
|
+
if (!m) throw usageError("bad_value", `--heartbeat expects <slug>[:<interval-seconds>], got ${JSON.stringify(h)}`, "For example --heartbeat nightly-backup:93600 (or nightly-backup=93600).");
|
|
876
|
+
monitors.push(compact({ slug: m[1], type: "heartbeat", intervalSeconds: m[2] ? Number(m[2]) : void 0 }));
|
|
874
877
|
}
|
|
875
878
|
for (const type of ["get", "ping", "ssl"]) {
|
|
876
879
|
for (const item of i[type] ?? []) {
|
|
@@ -897,6 +900,7 @@ var monitorsDeclare = {
|
|
|
897
900
|
body = { monitors };
|
|
898
901
|
}
|
|
899
902
|
if (i.dryRun) body = { ...body, dryRun: true };
|
|
903
|
+
if (i.prune) body = { ...body, prune: true };
|
|
900
904
|
return ctx.api.send("POST", await projectPath(ctx, "monitors"), body);
|
|
901
905
|
}
|
|
902
906
|
};
|
|
@@ -912,6 +916,24 @@ var monitorsDelete = {
|
|
|
912
916
|
async run(ctx) {
|
|
913
917
|
requireYes(ctx, `This deletes monitor ${ctx.input.slug} and its history.`);
|
|
914
918
|
return ctx.api.send("DELETE", await projectPath(ctx, `monitors/${seg(ctx.input.slug)}${ctx.input.type ? `?type=${seg(ctx.input.type)}` : ""}`));
|
|
919
|
+
},
|
|
920
|
+
output: "{id, slug, type, url, intervalSeconds, lastCheck, lastIsUp, deleted, via, actor}: what was deleted; kept in `monitors deleted`."
|
|
921
|
+
};
|
|
922
|
+
var monitorsDeleted = {
|
|
923
|
+
path: ["monitors", "deleted"],
|
|
924
|
+
summary: "Monitors deleted from this project: what each was, its last check, when and by whom.",
|
|
925
|
+
context: "project",
|
|
926
|
+
effect: "read",
|
|
927
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/monitor-deletions" },
|
|
928
|
+
flags: {
|
|
929
|
+
since: { type: "string", value: TIME, description: "Only deletions after this." },
|
|
930
|
+
limit: { type: "integer", value: "<n>", description: "How many (default 50)." }
|
|
931
|
+
},
|
|
932
|
+
examples: [{ command: "tracksiy monitors deleted --since 7d", description: "What `--prune`, `monitors delete` or the dashboard removed this week." }],
|
|
933
|
+
output: "{deleted:[{id, slug, type, url, intervalSeconds, lastCheck, lastIsUp, deleted, via, actor}]}",
|
|
934
|
+
text: (d) => table(d.deleted ?? [], ["slug", "type", "deleted", ["by", (r) => r.actor ?? r.via ?? ""], ["last check", (r) => r.lastCheck ? `${r.lastCheck}${r.lastIsUp === false ? " (down)" : ""}` : "never"]]),
|
|
935
|
+
async run(ctx) {
|
|
936
|
+
return ctx.api.get(await projectPath(ctx, "monitor-deletions"), { since: since(ctx.input.since), limit: ctx.input.limit });
|
|
915
937
|
}
|
|
916
938
|
};
|
|
917
939
|
var performance = {
|
|
@@ -998,7 +1020,103 @@ var queries = {
|
|
|
998
1020
|
});
|
|
999
1021
|
}
|
|
1000
1022
|
};
|
|
1001
|
-
var
|
|
1023
|
+
var notFound = {
|
|
1024
|
+
path: ["not-found"],
|
|
1025
|
+
aliases: [["404"]],
|
|
1026
|
+
summary: "Which pages answer 404, and who links to them: broken internal links first.",
|
|
1027
|
+
context: "project",
|
|
1028
|
+
effect: "read",
|
|
1029
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/not-found" },
|
|
1030
|
+
flags: {
|
|
1031
|
+
since: { type: "string", value: TIME, description: "Window start (default 24h)." },
|
|
1032
|
+
internal: { type: "boolean", description: "Only 404s reached from the site's own pages." },
|
|
1033
|
+
limit: { type: "integer", value: "<n>", description: "How many paths (max 200)." }
|
|
1034
|
+
},
|
|
1035
|
+
examples: [{ command: "tracksiy not-found --internal --since 7d", description: "Broken links the site itself serves." }],
|
|
1036
|
+
output: "{since, until, scanned, pages:[{path, count, lastSeen, internal, bots, referrers[{referrer, count}]}], note}",
|
|
1037
|
+
text: (d) => table(d.pages ?? [], ["path", "count", ["internal", (r) => r.internal ? "yes" : ""], "bots", ["top referrer", (r) => r.referrers?.[0]?.referrer ?? "-"]]),
|
|
1038
|
+
async run(ctx) {
|
|
1039
|
+
return ctx.api.get(await projectPath(ctx, "not-found"), { since: since(ctx.input.since), internal: ctx.input.internal || void 0, limit: ctx.input.limit });
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
var settingsGet = {
|
|
1043
|
+
path: ["settings"],
|
|
1044
|
+
summary: "The project's reading rules: environments, grouping rules, privacy rules, source roots.",
|
|
1045
|
+
context: "project",
|
|
1046
|
+
effect: "read",
|
|
1047
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/settings" },
|
|
1048
|
+
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
|
|
1049
|
+
async run(ctx) {
|
|
1050
|
+
return ctx.api.get(await projectPath(ctx, "settings"));
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function pairs(items, flag) {
|
|
1054
|
+
if (!items) return void 0;
|
|
1055
|
+
return items.map((item) => {
|
|
1056
|
+
const out = {};
|
|
1057
|
+
for (const part of String(item).split(/,(?=\s*\w+=)/)) {
|
|
1058
|
+
const eq = part.indexOf("=");
|
|
1059
|
+
if (eq < 1) throw usageError("bad_value", `--${flag} expects key=value,key=value, got ${JSON.stringify(item)}`);
|
|
1060
|
+
out[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
1061
|
+
}
|
|
1062
|
+
return out;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
var settingsSet = {
|
|
1066
|
+
path: ["settings", "set"],
|
|
1067
|
+
summary: "Change the project's reading rules. Only the flags given change; the rest are kept.",
|
|
1068
|
+
description: "Applied to events ingested from then on. --environments + --environment-policy drop discards (and counts) events from any other environment; flag stores them tagged environment_unlisted. --fingerprint-rule groups matching events into one issue. --privacy-rule drops a header or masks a PII shape before storage -- try it first with `tracksiy privacy-preview`. --source-root makes frames carry repoPath.",
|
|
1069
|
+
context: "project",
|
|
1070
|
+
effect: "write",
|
|
1071
|
+
idempotent: true,
|
|
1072
|
+
endpoint: { method: "PUT", path: "/api/agent/v1/projects/{org}/{project}/settings" },
|
|
1073
|
+
flags: {
|
|
1074
|
+
environments: { type: "list", value: "<env,...>", description: "The environments this project reports from (empty string: any)." },
|
|
1075
|
+
"environment-policy": { type: "string", enum: ["flag", "drop"], description: "What happens to an event from another environment." },
|
|
1076
|
+
"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 (for an SDK that already sends /app/... of a repo's backend/), optionally for one surface. Repeatable; replaces the list." },
|
|
1077
|
+
"fingerprint-rule": { type: "list", split: false, value: "type=<glob>,message=<glob>,fingerprint=<name>", description: "Group matching events into one issue. Repeatable; replaces the list." },
|
|
1078
|
+
"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." },
|
|
1079
|
+
body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
|
|
1080
|
+
},
|
|
1081
|
+
examples: [
|
|
1082
|
+
{ command: "tracksiy settings set --environments production,staging --environment-policy drop", description: "local and local1 stop reaching Tracksiy." },
|
|
1083
|
+
{ command: "tracksiy settings set --fingerprint-rule 'type=ViewException,message=syntax error*,fingerprint=blade-syntax'", description: "Every half-written template is one issue." },
|
|
1084
|
+
{ command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." },
|
|
1085
|
+
{ command: "tracksiy settings set --source-root 'strip=/,prepend=backend/,surface=backend'", description: "Laravel sends /app/Http/...; the repo has it under backend/." }
|
|
1086
|
+
],
|
|
1087
|
+
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
|
|
1088
|
+
async run(ctx) {
|
|
1089
|
+
const path = await projectPath(ctx, "settings");
|
|
1090
|
+
const i = ctx.input;
|
|
1091
|
+
if (i.body) return ctx.api.send("PUT", path, i.body);
|
|
1092
|
+
const current = await ctx.api.get(path);
|
|
1093
|
+
const next = {
|
|
1094
|
+
sourceRoots: i.sourceRoot ? i.sourceRoot.map((r) => /^\s*strip=/.test(r) ? pairs([r], "source-root")[0] : r) : current.sourceRoots ?? [],
|
|
1095
|
+
environments: i.environments ? i.environments.filter((e) => e) : current.environments ?? [],
|
|
1096
|
+
environmentPolicy: i.environmentPolicy ?? current.environmentPolicy ?? "flag",
|
|
1097
|
+
fingerprintRules: pairs(i.fingerprintRule, "fingerprint-rule") ?? current.fingerprintRules ?? [],
|
|
1098
|
+
privacyRules: pairs(i.privacyRule, "privacy-rule") ?? current.privacyRules ?? []
|
|
1099
|
+
};
|
|
1100
|
+
return ctx.api.send("PUT", path, next);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
var privacyPreview = {
|
|
1104
|
+
path: ["privacy-preview"],
|
|
1105
|
+
summary: "Try privacy rules on the project's last 50 stored events: what each would change and what PII shapes would remain. Writes nothing.",
|
|
1106
|
+
context: "project",
|
|
1107
|
+
effect: "read",
|
|
1108
|
+
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/settings/privacy-preview" },
|
|
1109
|
+
flags: {
|
|
1110
|
+
"privacy-rule": { type: "list", split: false, value: "dropHeader=<name> | mask=<shape>,field=<path|*>", description: "Rules to try (default: the saved ones)." }
|
|
1111
|
+
},
|
|
1112
|
+
examples: [{ command: "tracksiy privacy-preview --privacy-rule 'mask=email,field=request.headers.User-Agent'", description: "Would this clear the PII check?" }],
|
|
1113
|
+
output: "{sampled, rules[{rule, events, values}], remaining[{pattern, field, events}]}",
|
|
1114
|
+
async run(ctx) {
|
|
1115
|
+
const rules = pairs(ctx.input.privacyRule, "privacy-rule");
|
|
1116
|
+
return ctx.api.send("POST", await projectPath(ctx, "settings/privacy-preview"), rules ? { privacyRules: rules } : {});
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, monitorsDeleted, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
|
|
1002
1120
|
|
|
1003
1121
|
// src/commands/issues.ts
|
|
1004
1122
|
import { writeFileSync } from "node:fs";
|
|
@@ -1070,15 +1188,18 @@ var list = {
|
|
|
1070
1188
|
limit: { type: "integer", value: "<n>", description: "How many issues (server default 25)." },
|
|
1071
1189
|
frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." },
|
|
1072
1190
|
unclaimed: { type: "boolean", description: "Only issues nobody holds a live claim on: what you can pick up." },
|
|
1073
|
-
|
|
1191
|
+
cursor: { type: "string", value: "<nextCursor>", description: "Continue from the previous page's nextCursor (same filters and sort)." },
|
|
1192
|
+
"claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." },
|
|
1193
|
+
tags: { type: "string", value: "<key,...>", description: "Not a filter: each issue carries these tag keys' most frequent values in topTags (at most 10 keys)." }
|
|
1074
1194
|
},
|
|
1075
1195
|
examples: [
|
|
1076
1196
|
{ command: "tracksiy issues list --since 24h", description: "Unresolved issues seen in the last day." },
|
|
1197
|
+
{ command: "tracksiy issues list --tags endpoint,client_error_kind --select issues.id,issues.title,issues.topTags", description: "What each mobile issue hit, without opening every one." },
|
|
1077
1198
|
{ command: "tracksiy issues list --unclaimed --limit 5", description: "Five issues no other agent is working on." },
|
|
1078
1199
|
{ command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
|
|
1079
1200
|
{ command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
|
|
1080
1201
|
],
|
|
1081
|
-
output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, frames?}]}",
|
|
1202
|
+
output: "{project, issueCount, total, hasMore, nextCursor, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, latestEvent{release, environment, platform, exceptionType}, frames?, topTags?}]}",
|
|
1082
1203
|
text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
|
|
1083
1204
|
${issueRows(d.issues)}`,
|
|
1084
1205
|
async run(ctx) {
|
|
@@ -1088,7 +1209,9 @@ ${issueRows(d.issues)}`,
|
|
|
1088
1209
|
limit: ctx.input.limit,
|
|
1089
1210
|
frames: ctx.input.frames,
|
|
1090
1211
|
unclaimed: ctx.input.unclaimed || void 0,
|
|
1091
|
-
claimedBy: ctx.input.claimedBy
|
|
1212
|
+
claimedBy: ctx.input.claimedBy,
|
|
1213
|
+
cursor: ctx.input.cursor,
|
|
1214
|
+
tags: ctx.input.tags
|
|
1092
1215
|
});
|
|
1093
1216
|
}
|
|
1094
1217
|
};
|
|
@@ -1099,9 +1222,15 @@ var view = {
|
|
|
1099
1222
|
effect: "read",
|
|
1100
1223
|
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}" },
|
|
1101
1224
|
args: [{ name: "id", required: true, description: "Issue id (numeric, from `issues list`)." }],
|
|
1102
|
-
flags: {
|
|
1103
|
-
|
|
1104
|
-
|
|
1225
|
+
flags: {
|
|
1226
|
+
frames: { type: "integer", value: "<n>", description: "How many frames to include." },
|
|
1227
|
+
breadcrumbs: { type: "integer", value: "<n>", description: "Breadcrumbs of the latest event (default 20, at most 50, 0 for none): what happened just before it." }
|
|
1228
|
+
},
|
|
1229
|
+
examples: [
|
|
1230
|
+
{ command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." },
|
|
1231
|
+
{ command: "tracksiy issues view 4812 --select latestEvent.breadcrumbs", description: "The requests and navigations that led up to it." }
|
|
1232
|
+
],
|
|
1233
|
+
output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, resolution?, staleEvents, claimedBy?, latestEvent{release, environment, platform, exceptionType, exceptionValue, commit, contexts, breadcrumbs[{timestamp, type, category, level, message, data}]}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, activity[], recentComments[], permalink}",
|
|
1105
1234
|
text: (d) => {
|
|
1106
1235
|
const head = kv(d, ["id", "shortId", "title", "culprit", "level", "status", "count", "eventsLast24h", "eventsLast7d", "firstSeen", "lastSeen", "regressedAt", "permalink"]);
|
|
1107
1236
|
const ev = d.latestEvent ? `
|
|
@@ -1114,14 +1243,17 @@ resolved ${d.resolution.type.replace(/_/g, " ")}: ${d.resolution.release ?? d.re
|
|
|
1114
1243
|
const claim2 = d.claimedBy ? `
|
|
1115
1244
|
claimed by ${d.claimedBy} until ${d.claimedUntil}` : "";
|
|
1116
1245
|
const act = (d.activity ?? []).slice(0, 5).map((a) => ` ${ago(a.at)} ${a.action}${a.reason ? ` (${a.reason})` : ""} by ${a.actor ?? a.user ?? a.via}${a.note ? `: ${String(a.note).split("\n")[0].slice(0, 100)}` : ""}`).join("\n");
|
|
1246
|
+
const crumbs = (d.latestEvent?.breadcrumbs ?? []).slice(-8).map((b) => ` ${b.category ?? b.type ?? "-"}${b.data?.status_code ? ` ${b.data.status_code}` : ""} ${b.message ?? [b.data?.method, b.data?.url ?? b.data?.to].filter(Boolean).join(" ")}`).join("\n");
|
|
1117
1247
|
return `${head}${res}${claim2}${ev}${frames ? `
|
|
1118
1248
|
frames (* = in app):
|
|
1119
|
-
${frames}` : ""}${
|
|
1249
|
+
${frames}` : ""}${crumbs ? `
|
|
1250
|
+
breadcrumbs (last 8, oldest first):
|
|
1251
|
+
${crumbs}` : ""}${act ? `
|
|
1120
1252
|
activity:
|
|
1121
1253
|
${act}` : ""}`;
|
|
1122
1254
|
},
|
|
1123
1255
|
async run(ctx) {
|
|
1124
|
-
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames });
|
|
1256
|
+
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames, breadcrumbs: ctx.input.breadcrumbs });
|
|
1125
1257
|
}
|
|
1126
1258
|
};
|
|
1127
1259
|
function statusCommand(name, status2, summary, examples) {
|
|
@@ -1194,6 +1326,29 @@ var activity = {
|
|
|
1194
1326
|
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/activity`), { limit: ctx.input.limit });
|
|
1195
1327
|
}
|
|
1196
1328
|
};
|
|
1329
|
+
var merge = {
|
|
1330
|
+
path: ["issues", "merge"],
|
|
1331
|
+
summary: "Fold issues that are one root cause into one. Dry run unless --apply; later events of any of them land on the target.",
|
|
1332
|
+
context: "project",
|
|
1333
|
+
effect: "write",
|
|
1334
|
+
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/merge" },
|
|
1335
|
+
args: [{ name: "ids", required: true, variadic: true, description: "The issues to fold in." }],
|
|
1336
|
+
flags: {
|
|
1337
|
+
into: { type: "string", required: true, value: "<id>", description: "The issue they become." },
|
|
1338
|
+
note: WHY.note,
|
|
1339
|
+
apply: { type: "boolean", description: "Actually merge. Default is a dry run." }
|
|
1340
|
+
},
|
|
1341
|
+
examples: [{ command: "tracksiy issues merge 5519 5526 --into 5515 --note 'same Blade template, same deploy' --apply", description: "Three issues, one cause." }],
|
|
1342
|
+
output: "{dryRun, into{...issue}, merged[], events}",
|
|
1343
|
+
async run(ctx) {
|
|
1344
|
+
return ctx.api.send("POST", await projectPath(ctx, "issues/merge"), {
|
|
1345
|
+
into: String(ctx.input.into),
|
|
1346
|
+
ids: ctx.input.ids.map(String),
|
|
1347
|
+
note: textArg(ctx.input.note, "note"),
|
|
1348
|
+
dryRun: !ctx.input.apply
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1197
1352
|
var verifyFixed = {
|
|
1198
1353
|
path: ["issues", "verify-fixed"],
|
|
1199
1354
|
summary: "Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed.",
|
|
@@ -1320,7 +1475,7 @@ var bulk = {
|
|
|
1320
1475
|
return result;
|
|
1321
1476
|
}
|
|
1322
1477
|
};
|
|
1323
|
-
var issues_default = [list, view, resolve2, ignore, unresolve, bulk, comment, activity, verifyFixed, claim, unclaim];
|
|
1478
|
+
var issues_default = [list, view, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
|
|
1324
1479
|
|
|
1325
1480
|
// src/commands/projects.ts
|
|
1326
1481
|
var create = {
|
|
@@ -1424,7 +1579,7 @@ var DECLARE = {
|
|
|
1424
1579
|
recipient: {
|
|
1425
1580
|
type: "list",
|
|
1426
1581
|
value: "email|<type>:<url>",
|
|
1427
|
-
description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
|
|
1582
|
+
description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: \`peerpacket:<channel>?to=<handle>\` has Tracksiy open the ticket with its own key, for a channel opened to your organization; or https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> with your own key -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
|
|
1428
1583
|
},
|
|
1429
1584
|
delivery: { type: "string", enum: ["immediate", "digest"], description: "For --recipient: send each alert now, or batch into the digest hours. Default immediate." },
|
|
1430
1585
|
"new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
|
|
@@ -1446,6 +1601,9 @@ function recipients(list5, delivery) {
|
|
|
1446
1601
|
return compact({ type, delivery });
|
|
1447
1602
|
}
|
|
1448
1603
|
if (!target) throw usageError("bad_value", `--recipient ${raw} has no target`, `Format: ${type}:<url>`);
|
|
1604
|
+
if (type === "peerpacket" && !target.includes("://") && !target.startsWith("peerpacket:")) {
|
|
1605
|
+
return compact({ type, url: `peerpacket:${target}`, delivery });
|
|
1606
|
+
}
|
|
1449
1607
|
return compact({ type, url: target, delivery });
|
|
1450
1608
|
});
|
|
1451
1609
|
}
|
|
@@ -1502,7 +1660,8 @@ var set = {
|
|
|
1502
1660
|
examples: [
|
|
1503
1661
|
{ command: "tracksiy alerts set --recipient email --threshold 50/10", description: "Email the project's team on new issues, on bursts, and on downtime." },
|
|
1504
1662
|
{ command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." },
|
|
1505
|
-
{ command: "tracksiy alerts set --recipient 'peerpacket:
|
|
1663
|
+
{ command: "tracksiy alerts set --recipient 'peerpacket:shop-ops?to=shop-agent'", description: "Alerts land as PeerPacket tickets for the shop's agent, opened by Tracksiy's own key (no workspace key in the config)." },
|
|
1664
|
+
{ command: "tracksiy alerts set --recipient 'peerpacket:https://peerpacket.com/channels/shop-ops?to=shop-agent&as=tracksiy-alerts&key=pp_live_\u2026'", description: "The same with your own PeerPacket key." }
|
|
1506
1665
|
],
|
|
1507
1666
|
output: "The stored alerting state, same shape as `alerts get`.",
|
|
1508
1667
|
text: rulesText,
|
|
@@ -1567,7 +1726,7 @@ var defaults = {
|
|
|
1567
1726
|
return ctx.api.send("PUT", await orgPath(ctx, "alerts/defaults"), body);
|
|
1568
1727
|
}
|
|
1569
1728
|
};
|
|
1570
|
-
var
|
|
1729
|
+
var settingsGet2 = {
|
|
1571
1730
|
path: ["alerts", "settings"],
|
|
1572
1731
|
summary: "When digest-mode recipients are told, and whether the last digests went out.",
|
|
1573
1732
|
context: "org",
|
|
@@ -1582,7 +1741,7 @@ ${table(d.recentDigests ?? [], [["sent", (r) => ago(r.sentAt)], "type", "ok", "i
|
|
|
1582
1741
|
return ctx.api.get(await orgPath(ctx, "alerts/settings"));
|
|
1583
1742
|
}
|
|
1584
1743
|
};
|
|
1585
|
-
var
|
|
1744
|
+
var settingsSet2 = {
|
|
1586
1745
|
path: ["alerts", "settings", "set"],
|
|
1587
1746
|
summary: "Declare the digest hours and timezone.",
|
|
1588
1747
|
context: "org",
|
|
@@ -1604,7 +1763,7 @@ var settingsSet = {
|
|
|
1604
1763
|
return ctx.api.send("PUT", await orgPath(ctx, "alerts/settings"), { digest: compact({ hours, timezone: ctx.input.timezone }) });
|
|
1605
1764
|
}
|
|
1606
1765
|
};
|
|
1607
|
-
var alerts_default = [get, set, test, coverage, defaults,
|
|
1766
|
+
var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
|
|
1608
1767
|
|
|
1609
1768
|
// src/commands/keys.ts
|
|
1610
1769
|
var list2 = {
|
|
@@ -1804,7 +1963,7 @@ function injectMap(json, id) {
|
|
|
1804
1963
|
return JSON.stringify(map);
|
|
1805
1964
|
}
|
|
1806
1965
|
async function findPairs(paths) {
|
|
1807
|
-
const
|
|
1966
|
+
const pairs2 = [];
|
|
1808
1967
|
const seen = /* @__PURE__ */ new Set();
|
|
1809
1968
|
for (const p of paths) {
|
|
1810
1969
|
const abs = resolve3(p);
|
|
@@ -1823,10 +1982,10 @@ async function findPairs(paths) {
|
|
|
1823
1982
|
if (existsSync2(candidate)) map = candidate;
|
|
1824
1983
|
}
|
|
1825
1984
|
if (!map && existsSync2(`${js}.map`)) map = `${js}.map`;
|
|
1826
|
-
|
|
1985
|
+
pairs2.push({ js, map, root });
|
|
1827
1986
|
}
|
|
1828
1987
|
}
|
|
1829
|
-
return
|
|
1988
|
+
return pairs2;
|
|
1830
1989
|
}
|
|
1831
1990
|
function artifactName(file, root, prefix) {
|
|
1832
1991
|
const rel = relative(root, file).split(sep).join("/");
|
|
@@ -1997,9 +2156,9 @@ async function scan(paths) {
|
|
|
1997
2156
|
throw usageError("bad_path", err.message);
|
|
1998
2157
|
}
|
|
1999
2158
|
}
|
|
2000
|
-
async function inject(
|
|
2159
|
+
async function inject(pairs2, dryRun) {
|
|
2001
2160
|
const out = [];
|
|
2002
|
-
for (const pair of
|
|
2161
|
+
for (const pair of pairs2) {
|
|
2003
2162
|
if (!pair.map) {
|
|
2004
2163
|
out.push({ file: pair.js, map: null, debugId: "", action: "skipped-no-map" });
|
|
2005
2164
|
continue;
|
|
@@ -2076,13 +2235,13 @@ ${table(d.files, ["kind", "state", "name", "detail"])}${d.skipped?.length ? `
|
|
|
2076
2235
|
skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
|
|
2077
2236
|
async run(ctx) {
|
|
2078
2237
|
const i = ctx.input;
|
|
2079
|
-
const
|
|
2080
|
-
if (!
|
|
2081
|
-
if (i.inject && !i.dryRun) await inject(
|
|
2082
|
-
const skipped =
|
|
2083
|
-
const withMaps =
|
|
2238
|
+
const pairs2 = await scan(i.paths);
|
|
2239
|
+
if (!pairs2.length) throw usageError("nothing_to_upload", "No JavaScript files found.", "Point at the build output directory (dist, build, public/build).");
|
|
2240
|
+
if (i.inject && !i.dryRun) await inject(pairs2, false);
|
|
2241
|
+
const skipped = pairs2.filter((p) => !p.map).map((p) => artifactName(p.js, p.root, i.urlPrefix));
|
|
2242
|
+
const withMaps = pairs2.filter((p) => p.map);
|
|
2084
2243
|
if (!withMaps.length) {
|
|
2085
|
-
throw usageError("nothing_to_upload", `None of the ${
|
|
2244
|
+
throw usageError("nothing_to_upload", `None of the ${pairs2.length} JavaScript file(s) has a source map.`, "Build with source maps on (e.g. Vite build.sourcemap: true, webpack devtool: 'source-map').");
|
|
2086
2245
|
}
|
|
2087
2246
|
const files = [];
|
|
2088
2247
|
let withoutId = 0;
|
|
@@ -2611,7 +2770,7 @@ function coerce(name, spec, value, previous) {
|
|
|
2611
2770
|
return n;
|
|
2612
2771
|
}
|
|
2613
2772
|
case "list": {
|
|
2614
|
-
const items = Array.isArray(value) ? value.map(String) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2773
|
+
const items = Array.isArray(value) ? value.map(String) : spec.split === false ? [String(value).trim()].filter(Boolean) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2615
2774
|
if (spec.enum) for (const item of items) checkEnum(name, spec, item);
|
|
2616
2775
|
return [...previous ?? [], ...items];
|
|
2617
2776
|
}
|
|
@@ -2903,6 +3062,17 @@ var describe = {
|
|
|
2903
3062
|
summary: "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps from one token.",
|
|
2904
3063
|
docs: `${DEFAULT_URL}/cli.md`,
|
|
2905
3064
|
contract: contract(),
|
|
3065
|
+
limits: {
|
|
3066
|
+
issuesPerPage: 100,
|
|
3067
|
+
bulkStatusCap: 5e3,
|
|
3068
|
+
mergeIds: 100,
|
|
3069
|
+
filesPerAssembly: 1e3,
|
|
3070
|
+
monitorIntervalSeconds: [60, 3456e4],
|
|
3071
|
+
claimSeconds: [60, 604800],
|
|
3072
|
+
noteCharacters: 1e4,
|
|
3073
|
+
idempotencyKeyHours: 24,
|
|
3074
|
+
note: "The CLI splits uploads past filesPerAssembly itself and sends an Idempotency-Key with every write."
|
|
3075
|
+
},
|
|
2906
3076
|
globalFlags: Object.entries(GLOBAL_FLAGS).map(([n, f]) => flagSchema(n, f)),
|
|
2907
3077
|
commands: COMMANDS.map(describeCommand)
|
|
2908
3078
|
};
|
|
@@ -3192,7 +3362,7 @@ var COMMANDS = [
|
|
|
3192
3362
|
];
|
|
3193
3363
|
var GROUPS = [
|
|
3194
3364
|
["Start", "init auth onboarding describe"],
|
|
3195
|
-
["Look", "overview changes health orgs projects issues monitors performance queries releases alerts keys artifacts"],
|
|
3365
|
+
["Look", "overview changes health orgs projects issues monitors performance queries not-found releases alerts keys artifacts settings privacy-preview"],
|
|
3196
3366
|
["Ship", "sourcemaps proguard"],
|
|
3197
3367
|
["Manage", "tokens"],
|
|
3198
3368
|
["Talk to us", "support"],
|