tracksiy 0.11.0 → 0.13.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 +7 -0
- package/dist/tracksiy.mjs +463 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,13 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
|
|
|
143
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
144
|
| `logs stats` | Log counts per level per minute, hour or day. | read | `logs_stats` |
|
|
145
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` |
|
|
150
|
+
| `build upload <path>` | Measure an app build (APK, AAB, IPA, .xcarchive, .app) on this machine and record its size: per category, the largest files, growth against the previous build, size budgets. The binary is not uploaded. | write | `build_upload` |
|
|
151
|
+
| `build list` | Recorded builds newest first, with their growth against the build before. | read | `build_list` |
|
|
152
|
+
| `build view <id>` | One build: size per category, the largest files, what changed against the previous build. | read | `build_view` |
|
|
146
153
|
| `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
|
|
147
154
|
| `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
|
|
148
155
|
| `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.13.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);
|
|
@@ -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}, logs{enabled, minLevel, dailyLimit}}",
|
|
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
|
}
|
|
@@ -1093,6 +1093,10 @@ var settingsSet = {
|
|
|
1093
1093
|
logs: { type: "boolean", description: "Store structured logs at all (default on). --no-logs drops them, counted as log:disabled." },
|
|
1094
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
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)." },
|
|
1096
1100
|
body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
|
|
1097
1101
|
},
|
|
1098
1102
|
examples: [
|
|
@@ -1102,7 +1106,7 @@ var settingsSet = {
|
|
|
1102
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." },
|
|
1103
1107
|
{ command: "tracksiy settings set --max-events-per-issue 500 --sourcemap-keep-releases 5", description: "A noisy project keeps less on disk." }
|
|
1104
1108
|
],
|
|
1105
|
-
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[], performanceIssues, storage{maxEventsPerIssue, sourcemapKeepReleases, sourcemapKeepUsedDays}, releaseHealth{crashFreeSessionsMin, minSessions, maxDropPct, velocityPct, velocityMinEvents}, logs{enabled, minLevel, dailyLimit}}",
|
|
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}}",
|
|
1106
1110
|
async run(ctx) {
|
|
1107
1111
|
const path = await projectPath(ctx, "settings");
|
|
1108
1112
|
const i = ctx.input;
|
|
@@ -1127,6 +1131,13 @@ var settingsSet = {
|
|
|
1127
1131
|
...i.logsMinLevel !== void 0 ? { minLevel: i.logsMinLevel } : {},
|
|
1128
1132
|
...i.logsDailyLimit !== void 0 ? { dailyLimit: i.logsDailyLimit } : {}
|
|
1129
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
|
+
},
|
|
1130
1141
|
releaseHealth: {
|
|
1131
1142
|
...current.releaseHealth ?? {},
|
|
1132
1143
|
...i.crashFreeMin !== void 0 ? { crashFreeSessionsMin: i.crashFreeMin } : {},
|
|
@@ -1601,8 +1612,396 @@ ${d.notes.join("\n")}` : ""}`,
|
|
|
1601
1612
|
};
|
|
1602
1613
|
var logs_default = [list, patterns, stats, issueLogs];
|
|
1603
1614
|
|
|
1604
|
-
// src/commands/
|
|
1615
|
+
// src/commands/evidence.ts
|
|
1605
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/builds.ts
|
|
1706
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1707
|
+
import { basename as basename2 } from "node:path";
|
|
1708
|
+
|
|
1709
|
+
// src/lib/appsize.ts
|
|
1710
|
+
import { readFileSync as readFileSync4, readdirSync, statSync } from "node:fs";
|
|
1711
|
+
import { basename, join as join2, relative, sep } from "node:path";
|
|
1712
|
+
import { deflateRawSync } from "node:zlib";
|
|
1713
|
+
var EOCD = 101010256;
|
|
1714
|
+
var ZIP64_LOCATOR = 117853008;
|
|
1715
|
+
var ZIP64_EOCD = 101075792;
|
|
1716
|
+
var CENTRAL = 33639248;
|
|
1717
|
+
function zipEntries(buf) {
|
|
1718
|
+
let eocd = -1;
|
|
1719
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
|
|
1720
|
+
if (buf.readUInt32LE(i) === EOCD) {
|
|
1721
|
+
eocd = i;
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (eocd < 0) throw new Error("not a ZIP archive (no end of central directory)");
|
|
1726
|
+
let count = buf.readUInt16LE(eocd + 10);
|
|
1727
|
+
let offset = buf.readUInt32LE(eocd + 16);
|
|
1728
|
+
if (count === 65535 || offset === 4294967295) {
|
|
1729
|
+
const locator = eocd - 20;
|
|
1730
|
+
if (locator >= 0 && buf.readUInt32LE(locator) === ZIP64_LOCATOR) {
|
|
1731
|
+
const record = Number(buf.readBigUInt64LE(locator + 8));
|
|
1732
|
+
if (buf.readUInt32LE(record) === ZIP64_EOCD) {
|
|
1733
|
+
count = Number(buf.readBigUInt64LE(record + 32));
|
|
1734
|
+
offset = Number(buf.readBigUInt64LE(record + 48));
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
const out = [];
|
|
1739
|
+
let p = offset;
|
|
1740
|
+
for (let n = 0; n < count && p + 46 <= buf.length; n++) {
|
|
1741
|
+
if (buf.readUInt32LE(p) !== CENTRAL) break;
|
|
1742
|
+
let compressed = buf.readUInt32LE(p + 20);
|
|
1743
|
+
let uncompressed = buf.readUInt32LE(p + 24);
|
|
1744
|
+
const nameLen = buf.readUInt16LE(p + 28);
|
|
1745
|
+
const extraLen = buf.readUInt16LE(p + 30);
|
|
1746
|
+
const commentLen = buf.readUInt16LE(p + 32);
|
|
1747
|
+
const name = buf.toString("utf8", p + 46, p + 46 + nameLen);
|
|
1748
|
+
if (compressed === 4294967295 || uncompressed === 4294967295) {
|
|
1749
|
+
let e = p + 46 + nameLen;
|
|
1750
|
+
const end = e + extraLen;
|
|
1751
|
+
while (e + 4 <= end) {
|
|
1752
|
+
const id = buf.readUInt16LE(e);
|
|
1753
|
+
const size = buf.readUInt16LE(e + 2);
|
|
1754
|
+
if (id === 1) {
|
|
1755
|
+
let q = e + 4;
|
|
1756
|
+
if (uncompressed === 4294967295) {
|
|
1757
|
+
uncompressed = Number(buf.readBigUInt64LE(q));
|
|
1758
|
+
q += 8;
|
|
1759
|
+
}
|
|
1760
|
+
if (compressed === 4294967295) compressed = Number(buf.readBigUInt64LE(q));
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
e += 4 + size;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (!name.endsWith("/")) out.push({ path: name, download: compressed, install: uncompressed });
|
|
1767
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
1768
|
+
}
|
|
1769
|
+
return out;
|
|
1770
|
+
}
|
|
1771
|
+
function walk(dir) {
|
|
1772
|
+
const out = [];
|
|
1773
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1774
|
+
const full = join2(dir, entry.name);
|
|
1775
|
+
if (entry.isDirectory()) out.push(...walk(full));
|
|
1776
|
+
else if (entry.isFile()) out.push(full);
|
|
1777
|
+
}
|
|
1778
|
+
return out;
|
|
1779
|
+
}
|
|
1780
|
+
function dirEntries(dir) {
|
|
1781
|
+
return walk(dir).map((file) => {
|
|
1782
|
+
const size = statSync(file).size;
|
|
1783
|
+
let download = size;
|
|
1784
|
+
if (size > 0 && size <= 64 * 1024 * 1024) {
|
|
1785
|
+
try {
|
|
1786
|
+
download = Math.min(size, deflateRawSync(readFileSync4(file), { level: 6 }).length);
|
|
1787
|
+
} catch {
|
|
1788
|
+
download = size;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return { path: relative(dir, file).split(sep).join("/"), download, install: size };
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
function androidCategory(path, aab) {
|
|
1795
|
+
let p = path;
|
|
1796
|
+
if (aab) {
|
|
1797
|
+
if (p === "BundleConfig.pb" || p.startsWith("BUNDLE-METADATA/") || p.startsWith("META-INF/")) return "bundle metadata";
|
|
1798
|
+
const slash = p.indexOf("/");
|
|
1799
|
+
const module = slash > 0 ? p.slice(0, slash) : "";
|
|
1800
|
+
if (module && module !== "base") return `module:${module}`;
|
|
1801
|
+
p = slash > 0 ? p.slice(slash + 1) : p;
|
|
1802
|
+
if (p.startsWith("dex/")) return "dex";
|
|
1803
|
+
if (p === "resources.pb") return "resources";
|
|
1804
|
+
if (p.startsWith("manifest/")) return "manifest";
|
|
1805
|
+
if (p.startsWith("root/")) p = p.slice(5);
|
|
1806
|
+
}
|
|
1807
|
+
if (/^classes\d*\.dex$/.test(p)) return "dex";
|
|
1808
|
+
const lib = /^lib\/([^/]+)\/([^/]+)$/.exec(p);
|
|
1809
|
+
if (lib) {
|
|
1810
|
+
const [, abi, file] = lib;
|
|
1811
|
+
if (file === "libapp.so") return `dart code (${abi})`;
|
|
1812
|
+
if (file === "libflutter.so") return `flutter engine (${abi})`;
|
|
1813
|
+
if (/^lib(hermes|jsc|reactnative)/.test(file)) return `js engine (${abi})`;
|
|
1814
|
+
return `native (${abi})`;
|
|
1815
|
+
}
|
|
1816
|
+
if (p === "assets/index.android.bundle") return "js bundle";
|
|
1817
|
+
if (p.startsWith("assets/flutter_assets/")) return "flutter assets";
|
|
1818
|
+
if (p.startsWith("assets/")) return "assets";
|
|
1819
|
+
if (p.startsWith("res/") || p === "resources.arsc") return "resources";
|
|
1820
|
+
if (p.startsWith("META-INF/")) return "signing";
|
|
1821
|
+
if (p.startsWith("kotlin/")) return "kotlin metadata";
|
|
1822
|
+
if (p === "AndroidManifest.xml") return "manifest";
|
|
1823
|
+
return "other";
|
|
1824
|
+
}
|
|
1825
|
+
function iosCategory(path, appName) {
|
|
1826
|
+
if (path === appName) return "binary";
|
|
1827
|
+
if (path.startsWith("Frameworks/Flutter.framework/")) return "flutter engine";
|
|
1828
|
+
if (path.startsWith("Frameworks/App.framework/")) return "dart code";
|
|
1829
|
+
if (path.startsWith("Frameworks/")) return "frameworks";
|
|
1830
|
+
if (path.startsWith("PlugIns/") || path.startsWith("Extensions/")) return "extensions";
|
|
1831
|
+
if (path.startsWith("Watch/")) return "watch app";
|
|
1832
|
+
if (path.endsWith(".car")) return "asset catalog";
|
|
1833
|
+
if (path === "main.jsbundle") return "js bundle";
|
|
1834
|
+
if (/(^|\/)[^/]+\.lproj\//.test(path)) return "localizations";
|
|
1835
|
+
if (path.startsWith("_CodeSignature/") || path === "embedded.mobileprovision") return "signing";
|
|
1836
|
+
if (/\.bundle\//.test(path)) return "bundles";
|
|
1837
|
+
return "resources";
|
|
1838
|
+
}
|
|
1839
|
+
function appRelative(entries) {
|
|
1840
|
+
const first = entries.map((e) => /^(.*?\/)?([^/]+)\.app\//.exec(e.path)).find(Boolean);
|
|
1841
|
+
if (!first) return { entries, appName: "" };
|
|
1842
|
+
const prefix = (first[1] ?? "") + first[2] + ".app/";
|
|
1843
|
+
return {
|
|
1844
|
+
entries: entries.filter((e) => e.path.startsWith(prefix)).map((e) => ({ ...e, path: e.path.slice(prefix.length) })),
|
|
1845
|
+
appName: first[2]
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
function analyze(path) {
|
|
1849
|
+
const isDir = statSync(path).isDirectory();
|
|
1850
|
+
const lower = path.toLowerCase().replace(/\/+$/, "");
|
|
1851
|
+
let kind = "other";
|
|
1852
|
+
if (lower.endsWith(".apk")) kind = "apk";
|
|
1853
|
+
else if (lower.endsWith(".aab")) kind = "aab";
|
|
1854
|
+
else if (lower.endsWith(".ipa")) kind = "ipa";
|
|
1855
|
+
else if (lower.endsWith(".xcarchive")) kind = "xcarchive";
|
|
1856
|
+
else if (lower.endsWith(".app")) kind = "app";
|
|
1857
|
+
const platform = kind === "apk" || kind === "aab" ? "android" : kind === "other" ? "other" : "ios";
|
|
1858
|
+
let entries;
|
|
1859
|
+
if (isDir) {
|
|
1860
|
+
const root = kind === "xcarchive" ? join2(path, "Products", "Applications") : path;
|
|
1861
|
+
entries = dirEntries(root);
|
|
1862
|
+
if (kind === "app") entries = entries.map((e) => ({ ...e, path: `${basename(path)}/${e.path}` }));
|
|
1863
|
+
} else {
|
|
1864
|
+
entries = zipEntries(readFileSync4(path));
|
|
1865
|
+
}
|
|
1866
|
+
let appName = "";
|
|
1867
|
+
if (platform === "ios") ({ entries, appName } = appRelative(entries));
|
|
1868
|
+
const categories = {};
|
|
1869
|
+
let download = 0;
|
|
1870
|
+
let install = 0;
|
|
1871
|
+
for (const e of entries) {
|
|
1872
|
+
const name = platform === "android" ? androidCategory(e.path, kind === "aab") : platform === "ios" ? iosCategory(e.path, appName) : e.path.split("/")[0] || "other";
|
|
1873
|
+
const c = categories[name] ??= { download: 0, install: 0, files: 0 };
|
|
1874
|
+
c.download += e.download;
|
|
1875
|
+
c.install += e.install;
|
|
1876
|
+
c.files += 1;
|
|
1877
|
+
download += e.download;
|
|
1878
|
+
install += e.install;
|
|
1879
|
+
}
|
|
1880
|
+
const largest = [...entries].sort((a, b) => b.download - a.download).slice(0, 50);
|
|
1881
|
+
return { kind, platform, downloadBytes: download, installBytes: install, fileCount: entries.length, categories, largest };
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/commands/builds.ts
|
|
1885
|
+
var mb2 = (n) => n === null || n === void 0 ? "-" : `${(Number(n) / 1e6).toFixed(2)} MB`;
|
|
1886
|
+
var signed = (n) => `${n > 0 ? "+" : ""}${(n / 1e6).toFixed(2)} MB`;
|
|
1887
|
+
function render(d) {
|
|
1888
|
+
const c = d.comparison;
|
|
1889
|
+
const head = kv({
|
|
1890
|
+
release: d.release,
|
|
1891
|
+
platform: `${d.platform} (${d.kind})`,
|
|
1892
|
+
download: mb2(d.downloadBytes),
|
|
1893
|
+
install: mb2(d.installBytes),
|
|
1894
|
+
files: d.fileCount,
|
|
1895
|
+
...c ? { "vs previous": `${c.previousRelease}: download ${signed(c.downloadDelta)} (${c.downloadDeltaPct ?? "-"}%), install ${signed(c.installDelta)}` } : {},
|
|
1896
|
+
...d.verdict ? { budgets: d.verdict } : {}
|
|
1897
|
+
});
|
|
1898
|
+
const categories = Object.entries(d.categories ?? {}).map(([category, v]) => ({ category, ...v })).sort((a, b) => b.download - a.download).slice(0, 15);
|
|
1899
|
+
const cats = categories.length ? `
|
|
1900
|
+
|
|
1901
|
+
${table(categories, ["category", ["download", (r) => mb2(r.download)], ["install", (r) => mb2(r.install)], "files"])}` : "";
|
|
1902
|
+
const deltas = c?.categories?.length ? `
|
|
1903
|
+
|
|
1904
|
+
Changed:
|
|
1905
|
+
${table(c.categories, ["category", ["download \u0394", (r) => signed(r.downloadDelta)], ["install \u0394", (r) => signed(r.installDelta)]])}` : "";
|
|
1906
|
+
const budgets = (d.budgets ?? []).length ? `
|
|
1907
|
+
|
|
1908
|
+
${table(d.budgets, ["name", "metric", "max", ["value", (r) => r.value ?? "-"], "status"])}` : "";
|
|
1909
|
+
return head + cats + deltas + budgets;
|
|
1910
|
+
}
|
|
1911
|
+
var upload = {
|
|
1912
|
+
path: ["build", "upload"],
|
|
1913
|
+
summary: "Measure an app build (APK, AAB, IPA, .xcarchive, .app) on this machine and record its size: per category, the largest files, growth against the previous build, size budgets. The binary is not uploaded.",
|
|
1914
|
+
description: "Download size = the archive's compressed sizes (for an AAB an upper bound of what Play serves a device); install size = uncompressed. Categories: dex, native per ABI, resources, assets, the JS bundle and engine, Dart code and the Flutter engine, frameworks, the main binary, asset catalogs, localizations... Exits 10 when a size budget (`budgets set --budget 'name=size,metric=app_download_mb,max=40,op=android'`) is crossed, or the download grew more than --max-growth. An upload token (project:releases) is enough.",
|
|
1915
|
+
context: "project",
|
|
1916
|
+
effect: "write",
|
|
1917
|
+
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/builds" },
|
|
1918
|
+
scopes: ["project:releases", "project:write", "project:admin"],
|
|
1919
|
+
args: [{ name: "path", required: true, description: "app-release.aab, app.apk, App.ipa, App.xcarchive or App.app." }],
|
|
1920
|
+
flags: {
|
|
1921
|
+
release: { type: "string", value: "<version>", description: "The release as the SDK names it: shop-android@1.4.2+57. Required." },
|
|
1922
|
+
commit: { type: "string", value: "<sha>", description: "The commit it was built from." },
|
|
1923
|
+
"build-configuration": { type: "string", value: "<name>", description: "Release, Debug, a flavor..." },
|
|
1924
|
+
"app-id": { type: "string", value: "<id>", description: "Package name / bundle id." },
|
|
1925
|
+
"max-growth": { type: "number", value: "<pct>", description: "Fail (exit 10) when the download grew more than this % against the previous build." },
|
|
1926
|
+
"dry-run": { type: "boolean", description: "Measure and print; record nothing." }
|
|
1927
|
+
},
|
|
1928
|
+
examples: [
|
|
1929
|
+
{ command: "tracksiy build upload app/build/outputs/bundle/release/app-release.aab --release shop-android@1.4.2+57 --max-growth 5", description: "In CI after bundleRelease: record and stop on a 5 % jump." },
|
|
1930
|
+
{ command: "tracksiy build upload build/Shop.xcarchive --release shop-ios@1.4.2+57 --dry-run", description: "Where the weight is, without recording." }
|
|
1931
|
+
],
|
|
1932
|
+
output: "{id, release, platform, kind, downloadBytes, installBytes, fileCount, categories{...}, largest[...], comparison{previousRelease, downloadDelta, downloadDeltaPct, categories[...], newLargeFiles[...]}, budgets[...], verdict}",
|
|
1933
|
+
text: render,
|
|
1934
|
+
async run(ctx) {
|
|
1935
|
+
const i = ctx.input;
|
|
1936
|
+
if (!existsSync2(i.path)) throw usageError("not_found", `No such file or directory: ${i.path}`);
|
|
1937
|
+
let size;
|
|
1938
|
+
try {
|
|
1939
|
+
size = analyze(i.path);
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
throw usageError("bad_value", `Could not read ${i.path}: ${err.message}`, "Pass an .apk, .aab, .ipa, .xcarchive or .app.");
|
|
1942
|
+
}
|
|
1943
|
+
const body = {
|
|
1944
|
+
release: i.release,
|
|
1945
|
+
platform: size.platform,
|
|
1946
|
+
kind: size.kind,
|
|
1947
|
+
downloadBytes: size.downloadBytes,
|
|
1948
|
+
installBytes: size.installBytes,
|
|
1949
|
+
fileCount: size.fileCount,
|
|
1950
|
+
categories: size.categories,
|
|
1951
|
+
largest: size.largest,
|
|
1952
|
+
commit: i.commit ?? "",
|
|
1953
|
+
buildConfiguration: i.buildConfiguration ?? "",
|
|
1954
|
+
appId: i.appId ?? "",
|
|
1955
|
+
artifact: basename2(String(i.path).replace(/\/+$/, ""))
|
|
1956
|
+
};
|
|
1957
|
+
if (i.dryRun) return { dryRun: true, ...body, largest: body.largest.slice(0, 15) };
|
|
1958
|
+
if (!i.release) throw usageError("missing_flag", "--release is required to record a build.", "Name it as the SDK does: shop-android@1.4.2+57.");
|
|
1959
|
+
const data = await ctx.api.send("POST", await projectPath(ctx, "builds"), body);
|
|
1960
|
+
const growth = data.comparison?.downloadDeltaPct;
|
|
1961
|
+
if (data.verdict === "fail") {
|
|
1962
|
+
const names = (data.budgets ?? []).filter((b) => b.status === "fail").map((b) => `${b.name} ${b.value} > ${b.max}`).join(", ");
|
|
1963
|
+
throw new CheckFailed(`Size budget crossed: ${names}.`, data, "See `build view <id>` for what grew.");
|
|
1964
|
+
}
|
|
1965
|
+
if (i.maxGrowth !== void 0 && growth !== null && growth !== void 0 && growth > i.maxGrowth) {
|
|
1966
|
+
throw new CheckFailed(`Download grew ${growth}% against ${data.comparison.previousRelease} (max ${i.maxGrowth}%).`, data, "See the `Changed` categories and `comparison.newLargeFiles`.");
|
|
1967
|
+
}
|
|
1968
|
+
return data;
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
var list2 = {
|
|
1972
|
+
path: ["build", "list"],
|
|
1973
|
+
aliases: [["builds"]],
|
|
1974
|
+
summary: "Recorded builds newest first, with their growth against the build before.",
|
|
1975
|
+
context: "project",
|
|
1976
|
+
effect: "read",
|
|
1977
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/builds" },
|
|
1978
|
+
flags: {
|
|
1979
|
+
platform: { type: "string", enum: ["android", "ios", "other"], description: "Only this platform." },
|
|
1980
|
+
limit: { type: "integer", value: "<n>", description: "At most 100 (default 20)." }
|
|
1981
|
+
},
|
|
1982
|
+
output: "[{id, created, release, platform, kind, downloadBytes, installBytes, comparison{...}}]",
|
|
1983
|
+
text: (d) => table(d ?? [], ["id", ["when", (r) => ago(r.created)], "release", "platform", ["download", (r) => mb2(r.downloadBytes)], ["\u0394", (r) => r.comparison ? signed(r.comparison.downloadDelta) : "-"], ["install", (r) => mb2(r.installBytes)]]),
|
|
1984
|
+
async run(ctx) {
|
|
1985
|
+
return ctx.api.get(await projectPath(ctx, "builds"), { platform: ctx.input.platform, limit: ctx.input.limit });
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
var view = {
|
|
1989
|
+
path: ["build", "view"],
|
|
1990
|
+
summary: "One build: size per category, the largest files, what changed against the previous build.",
|
|
1991
|
+
context: "project",
|
|
1992
|
+
effect: "read",
|
|
1993
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/builds/{id}" },
|
|
1994
|
+
args: [{ name: "id", required: true, description: "The build id (from `build list`)." }],
|
|
1995
|
+
output: "{id, release, categories{...}, largest[...], comparison{...}}",
|
|
1996
|
+
text: render,
|
|
1997
|
+
async run(ctx) {
|
|
1998
|
+
return ctx.api.get(await projectPath(ctx, `builds/${encodeURIComponent(ctx.input.id)}`));
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
var builds_default = [upload, list2, view];
|
|
2002
|
+
|
|
2003
|
+
// src/commands/issues.ts
|
|
2004
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
1606
2005
|
var FILTERS = {
|
|
1607
2006
|
status: { type: "string", enum: ["unresolved", "resolved", "ignored", "regressed", "all"], description: "Issue status. Server default: unresolved." },
|
|
1608
2007
|
level: { type: "string", enum: ["sample", "debug", "info", "warning", "error", "fatal"], description: "Only this level." },
|
|
@@ -1658,7 +2057,7 @@ var issueRows = (issues) => table(issues ?? [], [
|
|
|
1658
2057
|
["status", (r) => r.regressedAt && r.status === "unresolved" ? "regressed" : r.status],
|
|
1659
2058
|
["title", (r) => r.title]
|
|
1660
2059
|
]);
|
|
1661
|
-
var
|
|
2060
|
+
var list3 = {
|
|
1662
2061
|
path: ["issues", "list"],
|
|
1663
2062
|
aliases: [["issues"]],
|
|
1664
2063
|
summary: "Recent issues of a project, newest activity first, with the frames that matter inlined.",
|
|
@@ -1698,7 +2097,7 @@ ${issueRows(d.issues)}`,
|
|
|
1698
2097
|
});
|
|
1699
2098
|
}
|
|
1700
2099
|
};
|
|
1701
|
-
var
|
|
2100
|
+
var view2 = {
|
|
1702
2101
|
path: ["issues", "view"],
|
|
1703
2102
|
summary: "One issue in enough depth to act on: latest event, stack frames, tags, recent volume.",
|
|
1704
2103
|
context: "project",
|
|
@@ -1952,13 +2351,13 @@ var bulk = {
|
|
|
1952
2351
|
});
|
|
1953
2352
|
const result = await ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
|
|
1954
2353
|
if (ctx.input.out && ctx.input.apply) {
|
|
1955
|
-
|
|
2354
|
+
writeFileSync2(ctx.input.out, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), status: ctx.input.to, changed: result?.changed ?? [] }, null, 2));
|
|
1956
2355
|
ctx.log(`wrote ${result?.changed?.length ?? 0} change(s) to ${ctx.input.out}; undo with --revert ${ctx.input.out} --apply`);
|
|
1957
2356
|
}
|
|
1958
2357
|
return result;
|
|
1959
2358
|
}
|
|
1960
2359
|
};
|
|
1961
|
-
var issues_default = [
|
|
2360
|
+
var issues_default = [list3, view2, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
|
|
1962
2361
|
|
|
1963
2362
|
// src/commands/projects.ts
|
|
1964
2363
|
var create = {
|
|
@@ -2073,8 +2472,8 @@ var DECLARE = {
|
|
|
2073
2472
|
"replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
|
|
2074
2473
|
body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
|
|
2075
2474
|
};
|
|
2076
|
-
function recipients(
|
|
2077
|
-
return (
|
|
2475
|
+
function recipients(list7, delivery) {
|
|
2476
|
+
return (list7 ?? []).map((raw) => {
|
|
2078
2477
|
const i = raw.indexOf(":");
|
|
2079
2478
|
const type = i > 0 ? raw.slice(0, i) : raw;
|
|
2080
2479
|
if (!RECIPIENT_TYPES.includes(type)) {
|
|
@@ -2255,7 +2654,7 @@ var settingsSet2 = {
|
|
|
2255
2654
|
var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
|
|
2256
2655
|
|
|
2257
2656
|
// src/commands/keys.ts
|
|
2258
|
-
var
|
|
2657
|
+
var list4 = {
|
|
2259
2658
|
path: ["keys", "list"],
|
|
2260
2659
|
aliases: [["keys"]],
|
|
2261
2660
|
summary: "Every ingest key of the project: its surface, whether ingest accepts it, its rate limit and volume.",
|
|
@@ -2312,11 +2711,11 @@ var rotate = {
|
|
|
2312
2711
|
return ctx.api.send("POST", await projectPath(ctx, `keys/${seg(ctx.input.name)}/rotate`));
|
|
2313
2712
|
}
|
|
2314
2713
|
};
|
|
2315
|
-
var keys_default = [
|
|
2714
|
+
var keys_default = [list4, set2, rotate];
|
|
2316
2715
|
|
|
2317
2716
|
// src/commands/releases.ts
|
|
2318
2717
|
import { execFileSync } from "node:child_process";
|
|
2319
|
-
var
|
|
2718
|
+
var list5 = {
|
|
2320
2719
|
path: ["releases", "list"],
|
|
2321
2720
|
aliases: [["releases"]],
|
|
2322
2721
|
summary: "What did the last deploys break? Recent releases with their events, issues and the issues they introduced.",
|
|
@@ -2462,25 +2861,25 @@ ${table(d.newIssues, ["short_id", "count", "title"])}` : ""}`,
|
|
|
2462
2861
|
return data;
|
|
2463
2862
|
}
|
|
2464
2863
|
};
|
|
2465
|
-
var releases_default = [
|
|
2864
|
+
var releases_default = [list5, health2, declare, verdict];
|
|
2466
2865
|
|
|
2467
2866
|
// src/commands/artifacts.ts
|
|
2468
2867
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2469
|
-
import { existsSync as
|
|
2868
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
2470
2869
|
|
|
2471
2870
|
// src/lib/sourcemaps.ts
|
|
2472
2871
|
import { createHash } from "node:crypto";
|
|
2473
|
-
import { existsSync as
|
|
2872
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
2474
2873
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2475
|
-
import { dirname as dirname2, join as
|
|
2874
|
+
import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
2476
2875
|
var JS = /\.(m?js|cjs)$/i;
|
|
2477
2876
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svn", ".hg"]);
|
|
2478
|
-
async function
|
|
2877
|
+
async function walk2(dir) {
|
|
2479
2878
|
const out = [];
|
|
2480
2879
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
2481
|
-
const full =
|
|
2880
|
+
const full = join3(dir, entry.name);
|
|
2482
2881
|
if (entry.isDirectory()) {
|
|
2483
|
-
if (!SKIP_DIRS.has(entry.name)) out.push(...await
|
|
2882
|
+
if (!SKIP_DIRS.has(entry.name)) out.push(...await walk2(full));
|
|
2484
2883
|
} else if (entry.isFile()) out.push(full);
|
|
2485
2884
|
}
|
|
2486
2885
|
return out.sort();
|
|
@@ -2540,7 +2939,7 @@ async function findPairs(paths) {
|
|
|
2540
2939
|
const st = await stat(abs).catch(() => void 0);
|
|
2541
2940
|
if (!st) throw new Error(`No such file or directory: ${p}`);
|
|
2542
2941
|
const root = st.isDirectory() ? abs : dirname2(abs);
|
|
2543
|
-
const files = st.isDirectory() ? await
|
|
2942
|
+
const files = st.isDirectory() ? await walk2(abs) : [abs];
|
|
2544
2943
|
for (const js of files.filter((f) => JS.test(f))) {
|
|
2545
2944
|
if (seen.has(js)) continue;
|
|
2546
2945
|
seen.add(js);
|
|
@@ -2549,16 +2948,16 @@ async function findPairs(paths) {
|
|
|
2549
2948
|
let map;
|
|
2550
2949
|
if (url && !url.startsWith("data:")) {
|
|
2551
2950
|
const candidate = resolve3(dirname2(js), decodeURIComponent(url.split(/[?#]/)[0]));
|
|
2552
|
-
if (
|
|
2951
|
+
if (existsSync3(candidate)) map = candidate;
|
|
2553
2952
|
}
|
|
2554
|
-
if (!map &&
|
|
2953
|
+
if (!map && existsSync3(`${js}.map`)) map = `${js}.map`;
|
|
2555
2954
|
pairs2.push({ js, map, root });
|
|
2556
2955
|
}
|
|
2557
2956
|
}
|
|
2558
2957
|
return pairs2;
|
|
2559
2958
|
}
|
|
2560
2959
|
function artifactName(file, root, prefix) {
|
|
2561
|
-
const rel =
|
|
2960
|
+
const rel = relative2(root, file).split(sep2).join("/");
|
|
2562
2961
|
const p = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
2563
2962
|
return `${p}${rel}`;
|
|
2564
2963
|
}
|
|
@@ -2840,8 +3239,8 @@ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
|
|
|
2840
3239
|
}
|
|
2841
3240
|
};
|
|
2842
3241
|
function uuidFromDebugMeta(path) {
|
|
2843
|
-
if (!
|
|
2844
|
-
const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(
|
|
3242
|
+
if (!existsSync4(path)) throw usageError("bad_path", `No such file: ${path}`);
|
|
3243
|
+
const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync5(path, "utf8"));
|
|
2845
3244
|
if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
|
|
2846
3245
|
return m[1].toLowerCase();
|
|
2847
3246
|
}
|
|
@@ -2866,7 +3265,7 @@ var proguardUpload = {
|
|
|
2866
3265
|
output: "{id, state, files[{name, kind, state}], chunks{}, bytes{}, uuid}",
|
|
2867
3266
|
async run(ctx) {
|
|
2868
3267
|
const i = ctx.input;
|
|
2869
|
-
if (!
|
|
3268
|
+
if (!existsSync4(i.mapping)) throw usageError("bad_path", `No such file: ${i.mapping}`);
|
|
2870
3269
|
const data = await readFile2(i.mapping);
|
|
2871
3270
|
if (!data.length) throw usageError("bad_value", `${i.mapping} is empty.`);
|
|
2872
3271
|
const uuid = (i.uuid ?? (i.debugMeta ? uuidFromDebugMeta(i.debugMeta) : proguardUuid(data))).toLowerCase();
|
|
@@ -2903,7 +3302,7 @@ var artifacts_default = [sourcemapsInject, sourcemapsUpload, proguardUpload, art
|
|
|
2903
3302
|
|
|
2904
3303
|
// src/commands/tokens.ts
|
|
2905
3304
|
var SCOPE_HINT = "e.g. org:read,project:read,event:read,event:write,project:write,project:releases,project:admin";
|
|
2906
|
-
var
|
|
3305
|
+
var list6 = {
|
|
2907
3306
|
path: ["tokens", "list"],
|
|
2908
3307
|
aliases: [["tokens"]],
|
|
2909
3308
|
summary: "Your tokens, newest first, without their values.",
|
|
@@ -3016,7 +3415,7 @@ var self = {
|
|
|
3016
3415
|
return ctx.api.get("tokens/self");
|
|
3017
3416
|
}
|
|
3018
3417
|
};
|
|
3019
|
-
var tokens_default = [
|
|
3418
|
+
var tokens_default = [list6, self, create2, update, remove, rotate2];
|
|
3020
3419
|
|
|
3021
3420
|
// src/commands/support.ts
|
|
3022
3421
|
var SUPPORT = {
|
|
@@ -3053,33 +3452,33 @@ var support = {
|
|
|
3053
3452
|
var support_default = [support];
|
|
3054
3453
|
|
|
3055
3454
|
// src/commands/init.ts
|
|
3056
|
-
import { existsSync as
|
|
3455
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
3057
3456
|
import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
|
|
3058
|
-
import { join as
|
|
3457
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
3059
3458
|
|
|
3060
3459
|
// src/lib/detect.ts
|
|
3061
|
-
import { existsSync as
|
|
3062
|
-
import { basename, join as
|
|
3460
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "node:fs";
|
|
3461
|
+
import { basename as basename3, join as join4 } from "node:path";
|
|
3063
3462
|
function readJson(path) {
|
|
3064
3463
|
try {
|
|
3065
|
-
return JSON.parse(
|
|
3464
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
3066
3465
|
} catch {
|
|
3067
3466
|
return void 0;
|
|
3068
3467
|
}
|
|
3069
3468
|
}
|
|
3070
3469
|
function has(dir, ...names) {
|
|
3071
|
-
return names.find((n) =>
|
|
3470
|
+
return names.find((n) => existsSync5(join4(dir, n)));
|
|
3072
3471
|
}
|
|
3073
3472
|
function slugify(s) {
|
|
3074
3473
|
return s.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
3075
3474
|
}
|
|
3076
3475
|
function detect(dir, guideBase) {
|
|
3077
3476
|
const evidence = [];
|
|
3078
|
-
const composer = readJson(
|
|
3079
|
-
const pkg = readJson(
|
|
3477
|
+
const composer = readJson(join4(dir, "composer.json"));
|
|
3478
|
+
const pkg = readJson(join4(dir, "package.json"));
|
|
3080
3479
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
3081
3480
|
const hasDep = (d) => d in deps;
|
|
3082
|
-
const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ??
|
|
3481
|
+
const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ?? basename3(dir)) || "app";
|
|
3083
3482
|
const guide = (slug) => `${guideBase}/${slug}.md`;
|
|
3084
3483
|
if (composer?.require?.["laravel/framework"]) {
|
|
3085
3484
|
evidence.push("composer.json requires laravel/framework");
|
|
@@ -3134,8 +3533,8 @@ function detect(dir, guideBase) {
|
|
|
3134
3533
|
}
|
|
3135
3534
|
const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
|
|
3136
3535
|
if (py) {
|
|
3137
|
-
const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) =>
|
|
3138
|
-
const fw =
|
|
3536
|
+
const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync5(join4(dir, f)) ? readFileSync6(join4(dir, f), "utf8") : "").join("\n").toLowerCase();
|
|
3537
|
+
const fw = existsSync5(join4(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
|
|
3139
3538
|
evidence.push(`${py} (${fw})`);
|
|
3140
3539
|
return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
|
|
3141
3540
|
}
|
|
@@ -3144,7 +3543,7 @@ function detect(dir, guideBase) {
|
|
|
3144
3543
|
evidence.push(gradle);
|
|
3145
3544
|
return { platform: "android", framework: "android", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
|
|
3146
3545
|
}
|
|
3147
|
-
const apple = has(dir, "Podfile", "Package.swift") ??
|
|
3546
|
+
const apple = has(dir, "Podfile", "Package.swift") ?? readdirSync2(dir).find((f) => f.endsWith(".xcodeproj"));
|
|
3148
3547
|
if (apple) {
|
|
3149
3548
|
evidence.push(apple);
|
|
3150
3549
|
return { platform: "apple-ios", framework: "ios", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
|
|
@@ -3195,14 +3594,14 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
|
|
|
3195
3594
|
async run(ctx) {
|
|
3196
3595
|
const i = ctx.input;
|
|
3197
3596
|
const dir = resolve4(i.dir);
|
|
3198
|
-
if (!
|
|
3597
|
+
if (!existsSync6(dir)) throw usageError("bad_path", `No such directory: ${i.dir}`);
|
|
3199
3598
|
const found = detect(dir, ctx.config.url);
|
|
3200
3599
|
const platform = i.platform ?? found.platform;
|
|
3201
3600
|
const surfaces = i.surfaces ?? found.surfaces;
|
|
3202
3601
|
const name = i.name ?? found.name;
|
|
3203
3602
|
const detected = { platform, framework: found.framework, surfaces, evidence: found.evidence, name };
|
|
3204
3603
|
const steps = [];
|
|
3205
|
-
const
|
|
3604
|
+
const upload2 = found.buildDir ? `tracksiy sourcemaps upload ${found.buildDir} --inject` : void 0;
|
|
3206
3605
|
if (i.dryRun) {
|
|
3207
3606
|
steps.push({ id: "create", run: `tracksiy init${i.writeEnv ? " --write-env" : ""}`, detail: "Create the project and keys." });
|
|
3208
3607
|
return { dryRun: true, detected, guide: found.guide, steps };
|
|
@@ -3214,7 +3613,7 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
|
|
|
3214
3613
|
compact({ name, platform, surfaces, sameHost: i.sameHost })
|
|
3215
3614
|
);
|
|
3216
3615
|
const project2 = prov.project;
|
|
3217
|
-
const pfPath =
|
|
3616
|
+
const pfPath = join5(dir, PROJECT_FILE);
|
|
3218
3617
|
const pf = { org: project2.organization, project: project2.slug };
|
|
3219
3618
|
if (ctx.config.url !== DEFAULT_URL) pf.url = ctx.config.url;
|
|
3220
3619
|
await writeFile3(pfPath, JSON.stringify(pf, null, 2) + "\n");
|
|
@@ -3226,11 +3625,11 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
|
|
|
3226
3625
|
}));
|
|
3227
3626
|
let env;
|
|
3228
3627
|
if (i.writeEnv) {
|
|
3229
|
-
const envPath =
|
|
3230
|
-
if (!
|
|
3628
|
+
const envPath = join5(dir, ".env");
|
|
3629
|
+
if (!existsSync6(envPath)) {
|
|
3231
3630
|
throw usageError("no_env_file", `${envPath} does not exist; nothing was written to it.`, "Create it (and make sure it is gitignored), or set the variables yourself from `keys` in the output.");
|
|
3232
3631
|
}
|
|
3233
|
-
const current =
|
|
3632
|
+
const current = readFileSync7(envPath, "utf8");
|
|
3234
3633
|
const added = [];
|
|
3235
3634
|
const kept = [];
|
|
3236
3635
|
let block = "";
|
|
@@ -3252,7 +3651,7 @@ ${block}`);
|
|
|
3252
3651
|
if (!i.writeEnv && keys.some((k) => k.env)) {
|
|
3253
3652
|
steps.push({ id: "env", detail: `Set ${keys.filter((k) => k.env).map((k) => k.env).join(", ")} from \`keys\` (or re-run with --write-env).` });
|
|
3254
3653
|
}
|
|
3255
|
-
if (
|
|
3654
|
+
if (upload2) steps.push({ id: "sourcemaps", detail: "After each production build, upload source maps (uses project:releases).", run: upload2 });
|
|
3256
3655
|
steps.push({ id: "alerts", detail: "Decide who is told. A project without a recipient notifies nobody.", run: "tracksiy alerts set --recipient email && tracksiy alerts test" });
|
|
3257
3656
|
steps.push({ id: "verify", detail: "Deploy, trigger one error, then prove it arrived.", run: "tracksiy projects verify --wait 120" });
|
|
3258
3657
|
return {
|
|
@@ -3285,7 +3684,7 @@ function camel(name) {
|
|
|
3285
3684
|
}
|
|
3286
3685
|
|
|
3287
3686
|
// src/core/args.ts
|
|
3288
|
-
import { readFileSync as
|
|
3687
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3289
3688
|
var GLOBAL_FLAGS = {
|
|
3290
3689
|
org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
|
|
3291
3690
|
project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
|
|
@@ -3302,7 +3701,7 @@ var GLOBAL_FLAGS = {
|
|
|
3302
3701
|
};
|
|
3303
3702
|
var stdinCache;
|
|
3304
3703
|
function readStdin() {
|
|
3305
|
-
stdinCache ??=
|
|
3704
|
+
stdinCache ??= readFileSync8(0, "utf8");
|
|
3306
3705
|
return stdinCache;
|
|
3307
3706
|
}
|
|
3308
3707
|
function parseJsonValue(name, raw) {
|
|
@@ -3310,7 +3709,7 @@ function parseJsonValue(name, raw) {
|
|
|
3310
3709
|
if (raw === "-") text = readStdin();
|
|
3311
3710
|
else if (raw.startsWith("@")) {
|
|
3312
3711
|
try {
|
|
3313
|
-
text =
|
|
3712
|
+
text = readFileSync8(raw.slice(1), "utf8");
|
|
3314
3713
|
} catch (err) {
|
|
3315
3714
|
throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
|
|
3316
3715
|
}
|
|
@@ -3680,15 +4079,15 @@ function makeContext(spec, input, globals, surface, io) {
|
|
|
3680
4079
|
const org = () => {
|
|
3681
4080
|
orgPromise ??= (async () => {
|
|
3682
4081
|
if (config.org) return config.org;
|
|
3683
|
-
const
|
|
3684
|
-
if (
|
|
3685
|
-
log(`using organization ${
|
|
3686
|
-
return
|
|
4082
|
+
const list7 = await visible();
|
|
4083
|
+
if (list7.length === 1) {
|
|
4084
|
+
log(`using organization ${list7[0].slug}`);
|
|
4085
|
+
return list7[0].slug;
|
|
3687
4086
|
}
|
|
3688
4087
|
throw usageError(
|
|
3689
4088
|
"missing_org",
|
|
3690
|
-
|
|
3691
|
-
|
|
4089
|
+
list7.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
|
|
4090
|
+
list7.length ? `Pass --org <slug>, set TRACKSIY_ORG, or add "org" to ${".tracksiy.json"}. Visible: ${list7.map((o) => o.slug).join(", ")}` : "Check the token with `tracksiy auth status`."
|
|
3692
4091
|
);
|
|
3693
4092
|
})();
|
|
3694
4093
|
return orgPromise;
|
|
@@ -3698,8 +4097,8 @@ function makeContext(spec, input, globals, surface, io) {
|
|
|
3698
4097
|
projectPromise ??= (async () => {
|
|
3699
4098
|
const o = await org();
|
|
3700
4099
|
if (config.project) return { org: o, project: config.project };
|
|
3701
|
-
const
|
|
3702
|
-
const projects2 =
|
|
4100
|
+
const list7 = await visible();
|
|
4101
|
+
const projects2 = list7.find((x) => x.slug === o)?.projects ?? [];
|
|
3703
4102
|
if (projects2.length === 1) {
|
|
3704
4103
|
log(`using project ${o}/${projects2[0].slug}`);
|
|
3705
4104
|
return { org: o, project: projects2[0].slug };
|
|
@@ -3923,6 +4322,8 @@ var COMMANDS = [
|
|
|
3923
4322
|
...storage_default,
|
|
3924
4323
|
...mobile_default,
|
|
3925
4324
|
...logs_default,
|
|
4325
|
+
...evidence_default,
|
|
4326
|
+
...builds_default,
|
|
3926
4327
|
...issues_default,
|
|
3927
4328
|
...projects_default,
|
|
3928
4329
|
...alerts_default,
|
|
@@ -3936,8 +4337,8 @@ var COMMANDS = [
|
|
|
3936
4337
|
];
|
|
3937
4338
|
var GROUPS = [
|
|
3938
4339
|
["Start", "init auth onboarding describe"],
|
|
3939
|
-
["Look", "overview changes health storage orgs projects issues monitors performance mobile logs queries not-found releases alerts keys artifacts settings privacy-preview"],
|
|
3940
|
-
["Ship", "sourcemaps proguard"],
|
|
4340
|
+
["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"],
|
|
4341
|
+
["Ship", "sourcemaps proguard build builds"],
|
|
3941
4342
|
["Manage", "tokens"],
|
|
3942
4343
|
["Talk to us", "support"],
|
|
3943
4344
|
["Other", "api mcp"]
|