tracksiy 0.10.0 → 0.12.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 +8 -0
- package/dist/tracksiy.mjs +273 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,6 +139,14 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
|
|
|
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
140
|
| `storage` | What each project holds on the server (events, transactions, source maps, debug files), largest first, with its retention limits. | read | `storage` |
|
|
141
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` |
|
|
142
|
+
| `logs list` | What the app logged, newest first: by level, text, trace, release, attribute. --follow keeps printing new lines. | read | `logs_list` |
|
|
143
|
+
| `logs patterns` | The loudest log lines, counted across their values (message template, or the body with numbers and ids blanked). | read | `logs_patterns` |
|
|
144
|
+
| `logs stats` | Log counts per level per minute, hour or day. | read | `logs_stats` |
|
|
145
|
+
| `issues logs <id>` | What the app logged around an error: the logs in the traces of the issue's last events. | read | `issues_logs` |
|
|
146
|
+
| `user-feedback` | What users wrote through the feedback widget or API: message, page or screen, release, device, the issue it is about and its screenshots. | read | `user_feedback` |
|
|
147
|
+
| `issues attachments <id>` | Screenshots, view hierarchies and files of an issue's recent events and of the feedback about it. | read | `issues_attachments` |
|
|
148
|
+
| `attachments get <id>` | Save one attachment (a screenshot, a view hierarchy) to a file -- or print a JSON one. | read | `attachments_get` |
|
|
149
|
+
| `issues flags <id>` | Feature flag values of an issue's events against the rest of the project; `suspect` marks a value far more common in this issue. | read | `issues_flags` |
|
|
142
150
|
| `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
|
|
143
151
|
| `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
|
|
144
152
|
| `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.12.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
|
|
@@ -149,7 +149,7 @@ var Api = class {
|
|
|
149
149
|
continue;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
const data = await readBody(res);
|
|
152
|
+
const data = opts.binary && res.ok ? new Uint8Array(await res.arrayBuffer()) : await readBody(res);
|
|
153
153
|
const ok = opts.accept ? opts.accept.includes(res.status) : res.ok;
|
|
154
154
|
if (ok) return { status: res.status, data, headers: res.headers };
|
|
155
155
|
throw await this.httpError(method, path, res, data);
|
|
@@ -508,8 +508,8 @@ function table(rows, columns2) {
|
|
|
508
508
|
widths[i]--;
|
|
509
509
|
}
|
|
510
510
|
const fit = (s, n) => s.length > n ? s.slice(0, Math.max(1, n - 1)) + "\u2026" : s.padEnd(n);
|
|
511
|
-
const
|
|
512
|
-
return [
|
|
511
|
+
const line2 = (vals) => vals.map((v, i) => fit(v, widths[i])).join(" ").trimEnd();
|
|
512
|
+
return [line2(cols.map(([n]) => n.toUpperCase())), ...cells.map(line2)].join("\n");
|
|
513
513
|
}
|
|
514
514
|
function kv(obj, keys) {
|
|
515
515
|
const entries = (keys ?? Object.keys(obj)).filter((k) => k in obj && isScalar(obj[k]));
|
|
@@ -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, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}}",
|
|
1053
|
+
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}, attachments{store, maxKb, quotaMb, retentionDays}}",
|
|
1054
1054
|
async run(ctx) {
|
|
1055
1055
|
return ctx.api.get(await projectPath(ctx, "settings"));
|
|
1056
1056
|
}
|
|
@@ -1090,6 +1090,13 @@ var settingsSet = {
|
|
|
1090
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
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
1092
|
"velocity-min-events": { type: "integer", value: "<n>", description: "...with at least this many events (default 10)." },
|
|
1093
|
+
logs: { type: "boolean", description: "Store structured logs at all (default on). --no-logs drops them, counted as log:disabled." },
|
|
1094
|
+
"logs-min-level": { type: "string", enum: ["trace", "debug", "info", "warn", "error", "fatal"], description: "Drop log lines below this level at ingest (default trace)." },
|
|
1095
|
+
"logs-daily-limit": { type: "integer", value: "<n>", description: "At most this many log lines a day (default 100000); the rest are dropped and counted as log:daily_limit." },
|
|
1096
|
+
attachments: { type: "string", enum: ["off", "feedback", "all"], description: "Which attachments are kept: off, feedback (default: only what a user sent with their feedback), all (also crash screenshots, view hierarchies, files)." },
|
|
1097
|
+
"attachment-max-kb": { type: "integer", value: "<kb>", description: "Largest attachment kept (default 5120)." },
|
|
1098
|
+
"attachments-quota-mb": { type: "integer", value: "<mb>", description: "The project's attachment budget (default 500); past it new files are dropped (attachment:quota)." },
|
|
1099
|
+
"attachments-retention-days": { type: "integer", value: "<days>", description: "Keep attachments this long (default 30)." },
|
|
1093
1100
|
body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
|
|
1094
1101
|
},
|
|
1095
1102
|
examples: [
|
|
@@ -1099,7 +1106,7 @@ var settingsSet = {
|
|
|
1099
1106
|
{ 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
1107
|
{ command: "tracksiy settings set --max-events-per-issue 500 --sourcemap-keep-releases 5", description: "A noisy project keeps less on disk." }
|
|
1101
1108
|
],
|
|
1102
|
-
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}}",
|
|
1109
|
+
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}, attachments{store, maxKb, quotaMb, retentionDays}}",
|
|
1103
1110
|
async run(ctx) {
|
|
1104
1111
|
const path = await projectPath(ctx, "settings");
|
|
1105
1112
|
const i = ctx.input;
|
|
@@ -1118,6 +1125,19 @@ var settingsSet = {
|
|
|
1118
1125
|
...i.sourcemapKeepReleases !== void 0 ? { sourcemapKeepReleases: i.sourcemapKeepReleases } : {},
|
|
1119
1126
|
...i.sourcemapKeepUsedDays !== void 0 ? { sourcemapKeepUsedDays: i.sourcemapKeepUsedDays } : {}
|
|
1120
1127
|
},
|
|
1128
|
+
logs: {
|
|
1129
|
+
...current.logs ?? {},
|
|
1130
|
+
...i.logs !== void 0 ? { enabled: i.logs } : {},
|
|
1131
|
+
...i.logsMinLevel !== void 0 ? { minLevel: i.logsMinLevel } : {},
|
|
1132
|
+
...i.logsDailyLimit !== void 0 ? { dailyLimit: i.logsDailyLimit } : {}
|
|
1133
|
+
},
|
|
1134
|
+
attachments: {
|
|
1135
|
+
...current.attachments ?? {},
|
|
1136
|
+
...i.attachments !== void 0 ? { store: i.attachments } : {},
|
|
1137
|
+
...i.attachmentMaxKb !== void 0 ? { maxKb: i.attachmentMaxKb } : {},
|
|
1138
|
+
...i.attachmentsQuotaMb !== void 0 ? { quotaMb: i.attachmentsQuotaMb } : {},
|
|
1139
|
+
...i.attachmentsRetentionDays !== void 0 ? { retentionDays: i.attachmentsRetentionDays } : {}
|
|
1140
|
+
},
|
|
1121
1141
|
releaseHealth: {
|
|
1122
1142
|
...current.releaseHealth ?? {},
|
|
1123
1143
|
...i.crashFreeMin !== void 0 ? { crashFreeSessionsMin: i.crashFreeMin } : {},
|
|
@@ -1461,8 +1481,229 @@ ${checks}`;
|
|
|
1461
1481
|
};
|
|
1462
1482
|
var mobile_default = [mobile];
|
|
1463
1483
|
|
|
1464
|
-
// src/commands/
|
|
1484
|
+
// src/commands/logs.ts
|
|
1485
|
+
var LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
1486
|
+
var line = (l) => `${String(l.timestamp).slice(11, 23)} ${String(l.level).toUpperCase().padEnd(5)} ${l.body}${l.traceId ? ` [trace ${String(l.traceId).slice(0, 8)}]` : ""}`;
|
|
1487
|
+
function filters(i) {
|
|
1488
|
+
return {
|
|
1489
|
+
since: since(i.since),
|
|
1490
|
+
until: since(i.until, "until"),
|
|
1491
|
+
level: i.level,
|
|
1492
|
+
query: i.query,
|
|
1493
|
+
trace: i.trace,
|
|
1494
|
+
release: i.release,
|
|
1495
|
+
environment: i.environment,
|
|
1496
|
+
logger: i.logger,
|
|
1497
|
+
attribute: i.attribute,
|
|
1498
|
+
pattern: i.pattern,
|
|
1499
|
+
limit: i.limit,
|
|
1500
|
+
cursor: i.cursor
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
var FILTER_FLAGS = {
|
|
1504
|
+
since: { type: "string", value: TIME, description: "Window start (default 1h ago; at most 31 days)." },
|
|
1505
|
+
until: { type: "string", value: TIME, description: "Window end (default now)." },
|
|
1506
|
+
level: { type: "string", enum: LEVELS, description: "The lowest level shown." },
|
|
1507
|
+
query: { type: "string", value: "<text>", description: "Case-insensitive text in the line." },
|
|
1508
|
+
trace: { type: "string", value: "<trace id>", description: "Only this trace (32 hex)." },
|
|
1509
|
+
release: { type: "string", value: "<version>", description: "Only this release." },
|
|
1510
|
+
environment: { type: "string", value: "<name>", description: "Only this environment." },
|
|
1511
|
+
logger: { type: "string", value: "<name>", description: "Only this logger / origin (sentry.origin, logger.name)." },
|
|
1512
|
+
attribute: { type: "string", value: "<key=value>", description: "Only lines with this attribute value." },
|
|
1513
|
+
pattern: { type: "integer", value: "<n>", description: "Only this pattern (from `logs patterns`)." }
|
|
1514
|
+
};
|
|
1515
|
+
var list = {
|
|
1516
|
+
path: ["logs", "list"],
|
|
1517
|
+
aliases: [["logs"]],
|
|
1518
|
+
summary: "What the app logged, newest first: by level, text, trace, release, attribute. --follow keeps printing new lines.",
|
|
1519
|
+
description: "Structured logs from the SDKs (Sentry.logger.*, sentry-laravel's sentry_logs channel, the mobile loggers). `next` pages older lines (--cursor). --follow 10m polls every 3 s and prints new lines as they arrive (stderr), then returns them all. Per-project limits: `settings set --logs/--logs-min-level/--logs-daily-limit`.",
|
|
1520
|
+
context: "project",
|
|
1521
|
+
effect: "read",
|
|
1522
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/logs" },
|
|
1523
|
+
flags: {
|
|
1524
|
+
...FILTER_FLAGS,
|
|
1525
|
+
limit: { type: "integer", value: "<n>", description: "Lines per page (default 100, at most 500)." },
|
|
1526
|
+
cursor: { type: "string", value: "<id>", description: "The `next` of the previous page." },
|
|
1527
|
+
follow: { type: "string", value: "<duration>", description: "Keep polling for new lines this long: 5m, 1h." }
|
|
1528
|
+
},
|
|
1529
|
+
examples: [
|
|
1530
|
+
{ command: "tracksiy logs --level warn --since 30m", description: "Warnings and errors of the last half hour." },
|
|
1531
|
+
{ command: "tracksiy logs --trace 5b8efff798038103d269b633813fc60c", description: "Everything one request logged." },
|
|
1532
|
+
{ command: "tracksiy logs --query checkout --follow 10m", description: "Watch a deploy." }
|
|
1533
|
+
],
|
|
1534
|
+
output: "{since, until, logs:[{id, timestamp, level, severity, body, traceId, spanId, release, environment, logger, source, pattern, attributes}], next}",
|
|
1535
|
+
text: (d) => `${(d.logs ?? []).slice().reverse().map(line).join("\n") || "(no lines)"}${d.next ? `
|
|
1536
|
+
|
|
1537
|
+
older: --cursor ${d.next}` : ""}`,
|
|
1538
|
+
async run(ctx) {
|
|
1539
|
+
const path = await projectPath(ctx, "logs");
|
|
1540
|
+
const query = filters(ctx.input);
|
|
1541
|
+
const followSeconds = duration(ctx.input.follow, "follow") ?? 0;
|
|
1542
|
+
let data = await ctx.api.get(path, query);
|
|
1543
|
+
if (!followSeconds) return data;
|
|
1544
|
+
const seen = new Set((data.logs ?? []).map((l) => l.id));
|
|
1545
|
+
const all = [...data.logs ?? []];
|
|
1546
|
+
let lastTime = all.length ? all[0].timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
1547
|
+
const deadline = Date.now() + followSeconds * 1e3;
|
|
1548
|
+
while (Date.now() < deadline) {
|
|
1549
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
1550
|
+
const page = await ctx.api.get(path, { ...query, since: lastTime, cursor: void 0, limit: 500 });
|
|
1551
|
+
const fresh = (page.logs ?? []).filter((l) => !seen.has(l.id)).reverse();
|
|
1552
|
+
for (const l of fresh) {
|
|
1553
|
+
seen.add(l.id);
|
|
1554
|
+
all.unshift(l);
|
|
1555
|
+
ctx.log(line(l));
|
|
1556
|
+
lastTime = l.timestamp;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return { ...data, logs: all, next: null };
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
var patterns = {
|
|
1563
|
+
path: ["logs", "patterns"],
|
|
1564
|
+
summary: "The loudest log lines, counted across their values (message template, or the body with numbers and ids blanked).",
|
|
1565
|
+
context: "project",
|
|
1566
|
+
effect: "read",
|
|
1567
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/logs/patterns" },
|
|
1568
|
+
flags: {
|
|
1569
|
+
since: FILTER_FLAGS.since,
|
|
1570
|
+
until: FILTER_FLAGS.until,
|
|
1571
|
+
level: FILTER_FLAGS.level,
|
|
1572
|
+
limit: { type: "integer", value: "<n>", description: "Patterns (default 100, at most 200)." }
|
|
1573
|
+
},
|
|
1574
|
+
examples: [{ command: "tracksiy logs patterns --since 24h --level warn", description: "Which warnings repeat; then `logs --pattern <n>`." }],
|
|
1575
|
+
output: "{patterns:[{pattern, count, level, lastSeen, template, sample}]}",
|
|
1576
|
+
text: (d) => table(d.patterns ?? [], ["count", "level", ["line", (r) => (r.template ?? r.sample ?? "").slice(0, 100)], "pattern"]),
|
|
1577
|
+
async run(ctx) {
|
|
1578
|
+
const i = ctx.input;
|
|
1579
|
+
return ctx.api.get(await projectPath(ctx, "logs/patterns"), { since: since(i.since), until: since(i.until, "until"), level: i.level, limit: i.limit });
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
var stats = {
|
|
1583
|
+
path: ["logs", "stats"],
|
|
1584
|
+
summary: "Log counts per level per minute, hour or day.",
|
|
1585
|
+
context: "project",
|
|
1586
|
+
effect: "read",
|
|
1587
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/logs/stats" },
|
|
1588
|
+
flags: { since: FILTER_FLAGS.since, until: FILTER_FLAGS.until },
|
|
1589
|
+
output: "{bucketSeconds, totals{level: n}, buckets:[{start, counts}]}",
|
|
1590
|
+
text: (d) => `${kv(d.totals ?? {})}
|
|
1591
|
+
|
|
1592
|
+
${table(d.buckets ?? [], ["start", ...LEVELS.map((lvl) => [lvl, (r) => r.counts?.[lvl] ?? ""])])}`,
|
|
1593
|
+
async run(ctx) {
|
|
1594
|
+
const i = ctx.input;
|
|
1595
|
+
return ctx.api.get(await projectPath(ctx, "logs/stats"), { since: since(i.since), until: since(i.until, "until") });
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
var issueLogs = {
|
|
1599
|
+
path: ["issues", "logs"],
|
|
1600
|
+
summary: "What the app logged around an error: the logs in the traces of the issue's last events.",
|
|
1601
|
+
context: "project",
|
|
1602
|
+
effect: "read",
|
|
1603
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{id}/logs" },
|
|
1604
|
+
args: [{ name: "id", required: true, description: "The issue id." }],
|
|
1605
|
+
output: "{issueId, traces[], logs[...], notes[]}",
|
|
1606
|
+
text: (d) => `${(d.logs ?? []).slice().reverse().map(line).join("\n") || "(no lines)"}${(d.notes ?? []).length ? `
|
|
1607
|
+
|
|
1608
|
+
${d.notes.join("\n")}` : ""}`,
|
|
1609
|
+
async run(ctx) {
|
|
1610
|
+
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/logs`));
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var logs_default = [list, patterns, stats, issueLogs];
|
|
1614
|
+
|
|
1615
|
+
// src/commands/evidence.ts
|
|
1465
1616
|
import { writeFileSync } from "node:fs";
|
|
1617
|
+
var feedback = {
|
|
1618
|
+
path: ["user-feedback"],
|
|
1619
|
+
aliases: [["feedback", "list"]],
|
|
1620
|
+
summary: "What users wrote through the feedback widget or API: message, page or screen, release, device, the issue it is about and its screenshots.",
|
|
1621
|
+
context: "project",
|
|
1622
|
+
effect: "read",
|
|
1623
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/feedback" },
|
|
1624
|
+
flags: {
|
|
1625
|
+
since: { type: "string", value: TIME, description: "Default 30 days." },
|
|
1626
|
+
limit: { type: "integer", value: "<n>", description: "At most 200 (default 50)." }
|
|
1627
|
+
},
|
|
1628
|
+
examples: [{ command: "tracksiy user-feedback --since 7d", description: "This week's feedback; `attachments get <id>` saves a screenshot." }],
|
|
1629
|
+
output: "{feedback:[{id, created, message, name, email, url, issueId, release, environment, source, os, device, attachments[{id, kind, name, contentType, size, url}]}]}",
|
|
1630
|
+
text: (d) => table(d.feedback ?? [], [
|
|
1631
|
+
["when", (r) => ago(r.created)],
|
|
1632
|
+
["message", (r) => String(r.message ?? "").slice(0, 80)],
|
|
1633
|
+
["url", (r) => r.url ?? "-"],
|
|
1634
|
+
["release", (r) => r.release ?? "-"],
|
|
1635
|
+
["issue", (r) => r.issueId ?? "-"],
|
|
1636
|
+
["files", (r) => (r.attachments ?? []).map((a) => a.id).join(",") || "-"]
|
|
1637
|
+
]),
|
|
1638
|
+
async run(ctx) {
|
|
1639
|
+
return ctx.api.get(await projectPath(ctx, "feedback"), { since: since(ctx.input.since), limit: ctx.input.limit });
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
var issueAttachments = {
|
|
1643
|
+
path: ["issues", "attachments"],
|
|
1644
|
+
summary: "Screenshots, view hierarchies and files of an issue's recent events and of the feedback about it.",
|
|
1645
|
+
context: "project",
|
|
1646
|
+
effect: "read",
|
|
1647
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{id}/attachments" },
|
|
1648
|
+
args: [{ name: "id", required: true, description: "The issue id." }],
|
|
1649
|
+
output: "{issueId, attachments:[{id, eventId, kind, name, contentType, size, created, url}], notes[]}",
|
|
1650
|
+
text: (d) => `${table(d.attachments ?? [], ["id", "kind", "name", "contentType", "size", ["when", (r) => ago(r.created)]])}${(d.notes ?? []).length ? `
|
|
1651
|
+
|
|
1652
|
+
${d.notes.join("\n")}` : ""}`,
|
|
1653
|
+
async run(ctx) {
|
|
1654
|
+
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/attachments`));
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
var attachmentGet = {
|
|
1658
|
+
path: ["attachments", "get"],
|
|
1659
|
+
summary: "Save one attachment (a screenshot, a view hierarchy) to a file -- or print a JSON one.",
|
|
1660
|
+
context: "project",
|
|
1661
|
+
effect: "read",
|
|
1662
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/attachments/{id}" },
|
|
1663
|
+
args: [{ name: "id", required: true, description: "The attachment id (from `feedback` or `issues attachments`)." }],
|
|
1664
|
+
flags: { out: { type: "string", value: "<file>", description: "Where to write it. Without it, JSON and text are printed; binary needs --out." } },
|
|
1665
|
+
examples: [{ command: "tracksiy attachments get 812 --out /tmp/screenshot.png", description: "Then look at the screenshot." }],
|
|
1666
|
+
output: "{id, bytes, out} or the JSON / text content",
|
|
1667
|
+
async run(ctx) {
|
|
1668
|
+
if (!/^\d+$/.test(String(ctx.input.id ?? ""))) {
|
|
1669
|
+
throw usageError("bad_value", `attachments get expects a numeric attachment id, got ${JSON.stringify(ctx.input.id)}.`, "Ids come from `user-feedback` or `issues attachments`.");
|
|
1670
|
+
}
|
|
1671
|
+
const path = await projectPath(ctx, `attachments/${encodeURIComponent(ctx.input.id)}`);
|
|
1672
|
+
const bytes = await ctx.api.get(path, void 0, { binary: true });
|
|
1673
|
+
if (ctx.input.out) {
|
|
1674
|
+
writeFileSync(ctx.input.out, bytes);
|
|
1675
|
+
return { id: ctx.input.id, bytes: bytes.length, out: ctx.input.out };
|
|
1676
|
+
}
|
|
1677
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
1678
|
+
if (/[\u0000-\u0008\u000e-\u001f]/.test(text.slice(0, 512))) {
|
|
1679
|
+
throw usageError("binary", `Attachment ${ctx.input.id} is binary (${bytes.length} bytes).`, "Pass --out <file> to save it.");
|
|
1680
|
+
}
|
|
1681
|
+
try {
|
|
1682
|
+
return JSON.parse(text);
|
|
1683
|
+
} catch {
|
|
1684
|
+
return { id: ctx.input.id, text };
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
var issueFlags = {
|
|
1689
|
+
path: ["issues", "flags"],
|
|
1690
|
+
summary: "Feature flag values of an issue's events against the rest of the project; `suspect` marks a value far more common in this issue.",
|
|
1691
|
+
context: "project",
|
|
1692
|
+
effect: "read",
|
|
1693
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{id}/flags" },
|
|
1694
|
+
args: [{ name: "id", required: true, description: "The issue id." }],
|
|
1695
|
+
output: "{issueId, issueEvents, baselineEvents, flags:[{flag, suspect, suspectValue, values:[{result, issueShare, baselineShare}]}], notes[]}",
|
|
1696
|
+
text: (d) => `${table(d.flags ?? [], ["flag", ["suspect", (r) => r.suspect ? `YES (${r.suspectValue})` : ""], ["values", (r) => (r.values ?? []).map((v) => `${v.result}: ${Math.round(v.issueShare * 100)}% vs ${v.baselineShare == null ? "-" : Math.round(v.baselineShare * 100) + "%"}`).join(" ")]])}${(d.notes ?? []).length ? `
|
|
1697
|
+
|
|
1698
|
+
${d.notes.join("\n")}` : ""}`,
|
|
1699
|
+
async run(ctx) {
|
|
1700
|
+
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/flags`));
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
var evidence_default = [feedback, issueAttachments, attachmentGet, issueFlags];
|
|
1704
|
+
|
|
1705
|
+
// src/commands/issues.ts
|
|
1706
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
1466
1707
|
var FILTERS = {
|
|
1467
1708
|
status: { type: "string", enum: ["unresolved", "resolved", "ignored", "regressed", "all"], description: "Issue status. Server default: unresolved." },
|
|
1468
1709
|
level: { type: "string", enum: ["sample", "debug", "info", "warning", "error", "fatal"], description: "Only this level." },
|
|
@@ -1518,7 +1759,7 @@ var issueRows = (issues) => table(issues ?? [], [
|
|
|
1518
1759
|
["status", (r) => r.regressedAt && r.status === "unresolved" ? "regressed" : r.status],
|
|
1519
1760
|
["title", (r) => r.title]
|
|
1520
1761
|
]);
|
|
1521
|
-
var
|
|
1762
|
+
var list2 = {
|
|
1522
1763
|
path: ["issues", "list"],
|
|
1523
1764
|
aliases: [["issues"]],
|
|
1524
1765
|
summary: "Recent issues of a project, newest activity first, with the frames that matter inlined.",
|
|
@@ -1812,13 +2053,13 @@ var bulk = {
|
|
|
1812
2053
|
});
|
|
1813
2054
|
const result = await ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
|
|
1814
2055
|
if (ctx.input.out && ctx.input.apply) {
|
|
1815
|
-
|
|
2056
|
+
writeFileSync2(ctx.input.out, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), status: ctx.input.to, changed: result?.changed ?? [] }, null, 2));
|
|
1816
2057
|
ctx.log(`wrote ${result?.changed?.length ?? 0} change(s) to ${ctx.input.out}; undo with --revert ${ctx.input.out} --apply`);
|
|
1817
2058
|
}
|
|
1818
2059
|
return result;
|
|
1819
2060
|
}
|
|
1820
2061
|
};
|
|
1821
|
-
var issues_default = [
|
|
2062
|
+
var issues_default = [list2, view, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
|
|
1822
2063
|
|
|
1823
2064
|
// src/commands/projects.ts
|
|
1824
2065
|
var create = {
|
|
@@ -1933,8 +2174,8 @@ var DECLARE = {
|
|
|
1933
2174
|
"replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
|
|
1934
2175
|
body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
|
|
1935
2176
|
};
|
|
1936
|
-
function recipients(
|
|
1937
|
-
return (
|
|
2177
|
+
function recipients(list6, delivery) {
|
|
2178
|
+
return (list6 ?? []).map((raw) => {
|
|
1938
2179
|
const i = raw.indexOf(":");
|
|
1939
2180
|
const type = i > 0 ? raw.slice(0, i) : raw;
|
|
1940
2181
|
if (!RECIPIENT_TYPES.includes(type)) {
|
|
@@ -2115,7 +2356,7 @@ var settingsSet2 = {
|
|
|
2115
2356
|
var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
|
|
2116
2357
|
|
|
2117
2358
|
// src/commands/keys.ts
|
|
2118
|
-
var
|
|
2359
|
+
var list3 = {
|
|
2119
2360
|
path: ["keys", "list"],
|
|
2120
2361
|
aliases: [["keys"]],
|
|
2121
2362
|
summary: "Every ingest key of the project: its surface, whether ingest accepts it, its rate limit and volume.",
|
|
@@ -2172,11 +2413,11 @@ var rotate = {
|
|
|
2172
2413
|
return ctx.api.send("POST", await projectPath(ctx, `keys/${seg(ctx.input.name)}/rotate`));
|
|
2173
2414
|
}
|
|
2174
2415
|
};
|
|
2175
|
-
var keys_default = [
|
|
2416
|
+
var keys_default = [list3, set2, rotate];
|
|
2176
2417
|
|
|
2177
2418
|
// src/commands/releases.ts
|
|
2178
2419
|
import { execFileSync } from "node:child_process";
|
|
2179
|
-
var
|
|
2420
|
+
var list4 = {
|
|
2180
2421
|
path: ["releases", "list"],
|
|
2181
2422
|
aliases: [["releases"]],
|
|
2182
2423
|
summary: "What did the last deploys break? Recent releases with their events, issues and the issues they introduced.",
|
|
@@ -2322,7 +2563,7 @@ ${table(d.newIssues, ["short_id", "count", "title"])}` : ""}`,
|
|
|
2322
2563
|
return data;
|
|
2323
2564
|
}
|
|
2324
2565
|
};
|
|
2325
|
-
var releases_default = [
|
|
2566
|
+
var releases_default = [list4, health2, declare, verdict];
|
|
2326
2567
|
|
|
2327
2568
|
// src/commands/artifacts.ts
|
|
2328
2569
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -2763,7 +3004,7 @@ var artifacts_default = [sourcemapsInject, sourcemapsUpload, proguardUpload, art
|
|
|
2763
3004
|
|
|
2764
3005
|
// src/commands/tokens.ts
|
|
2765
3006
|
var SCOPE_HINT = "e.g. org:read,project:read,event:read,event:write,project:write,project:releases,project:admin";
|
|
2766
|
-
var
|
|
3007
|
+
var list5 = {
|
|
2767
3008
|
path: ["tokens", "list"],
|
|
2768
3009
|
aliases: [["tokens"]],
|
|
2769
3010
|
summary: "Your tokens, newest first, without their values.",
|
|
@@ -2876,7 +3117,7 @@ var self = {
|
|
|
2876
3117
|
return ctx.api.get("tokens/self");
|
|
2877
3118
|
}
|
|
2878
3119
|
};
|
|
2879
|
-
var tokens_default = [
|
|
3120
|
+
var tokens_default = [list5, self, create2, update, remove, rotate2];
|
|
2880
3121
|
|
|
2881
3122
|
// src/commands/support.ts
|
|
2882
3123
|
var SUPPORT = {
|
|
@@ -3540,15 +3781,15 @@ function makeContext(spec, input, globals, surface, io) {
|
|
|
3540
3781
|
const org = () => {
|
|
3541
3782
|
orgPromise ??= (async () => {
|
|
3542
3783
|
if (config.org) return config.org;
|
|
3543
|
-
const
|
|
3544
|
-
if (
|
|
3545
|
-
log(`using organization ${
|
|
3546
|
-
return
|
|
3784
|
+
const list6 = await visible();
|
|
3785
|
+
if (list6.length === 1) {
|
|
3786
|
+
log(`using organization ${list6[0].slug}`);
|
|
3787
|
+
return list6[0].slug;
|
|
3547
3788
|
}
|
|
3548
3789
|
throw usageError(
|
|
3549
3790
|
"missing_org",
|
|
3550
|
-
|
|
3551
|
-
|
|
3791
|
+
list6.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
|
|
3792
|
+
list6.length ? `Pass --org <slug>, set TRACKSIY_ORG, or add "org" to ${".tracksiy.json"}. Visible: ${list6.map((o) => o.slug).join(", ")}` : "Check the token with `tracksiy auth status`."
|
|
3552
3793
|
);
|
|
3553
3794
|
})();
|
|
3554
3795
|
return orgPromise;
|
|
@@ -3558,8 +3799,8 @@ function makeContext(spec, input, globals, surface, io) {
|
|
|
3558
3799
|
projectPromise ??= (async () => {
|
|
3559
3800
|
const o = await org();
|
|
3560
3801
|
if (config.project) return { org: o, project: config.project };
|
|
3561
|
-
const
|
|
3562
|
-
const projects2 =
|
|
3802
|
+
const list6 = await visible();
|
|
3803
|
+
const projects2 = list6.find((x) => x.slug === o)?.projects ?? [];
|
|
3563
3804
|
if (projects2.length === 1) {
|
|
3564
3805
|
log(`using project ${o}/${projects2[0].slug}`);
|
|
3565
3806
|
return { org: o, project: projects2[0].slug };
|
|
@@ -3724,11 +3965,11 @@ async function serve(io, defaults2) {
|
|
|
3724
3965
|
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
3725
3966
|
io.stderr(`tracksiy mcp ${VERSION}: ${server.toolCount} tools on stdio
|
|
3726
3967
|
`);
|
|
3727
|
-
for await (const
|
|
3728
|
-
if (!
|
|
3968
|
+
for await (const line2 of rl) {
|
|
3969
|
+
if (!line2.trim()) continue;
|
|
3729
3970
|
let msg;
|
|
3730
3971
|
try {
|
|
3731
|
-
msg = JSON.parse(
|
|
3972
|
+
msg = JSON.parse(line2);
|
|
3732
3973
|
} catch {
|
|
3733
3974
|
write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
3734
3975
|
continue;
|
|
@@ -3782,6 +4023,8 @@ var COMMANDS = [
|
|
|
3782
4023
|
...perf_default,
|
|
3783
4024
|
...storage_default,
|
|
3784
4025
|
...mobile_default,
|
|
4026
|
+
...logs_default,
|
|
4027
|
+
...evidence_default,
|
|
3785
4028
|
...issues_default,
|
|
3786
4029
|
...projects_default,
|
|
3787
4030
|
...alerts_default,
|
|
@@ -3795,7 +4038,7 @@ var COMMANDS = [
|
|
|
3795
4038
|
];
|
|
3796
4039
|
var GROUPS = [
|
|
3797
4040
|
["Start", "init auth onboarding describe"],
|
|
3798
|
-
["Look", "overview changes health storage orgs projects issues monitors performance mobile queries not-found releases alerts keys artifacts settings privacy-preview"],
|
|
4041
|
+
["Look", "overview changes health storage orgs projects issues monitors performance mobile logs user-feedback attachments queries not-found releases alerts keys artifacts settings privacy-preview"],
|
|
3799
4042
|
["Ship", "sourcemaps proguard"],
|
|
3800
4043
|
["Manage", "tokens"],
|
|
3801
4044
|
["Talk to us", "support"],
|