tracksiy 0.12.0 → 0.14.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 CHANGED
@@ -147,6 +147,17 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
147
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
148
  | `attachments get <id>` | Save one attachment (a screenshot, a view hierarchy) to a file -- or print a JSON one. | read | `attachments_get` |
149
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` |
153
+ | `metrics list` | Metrics the app sent (Sentry.metrics.*, sentry-php traceMetrics, the server agent): name, type, unit, series and tag keys. | read | `metrics_list` |
154
+ | `metrics query <name>` | One metric as a time series: sum, count, avg, min, max, last or p50..p99 per interval, grouped by a tag. | read | `metrics_query` |
155
+ | `profiles functions` | Where the CPU time goes: functions by self or total time, with file and line, per transaction or overall. | read | `profiles_functions` |
156
+ | `profiles list` | Stored profiles newest first. | read | `profiles_list` |
157
+ | `profiles flamegraph <id>` | One profile's call tree, root to leaf, in ms (text: the heaviest paths). | read | `profiles_flamegraph` |
158
+ | `replays list` | Session replays newest first (kept only when the project turned replays on). | read | `replays_list` |
159
+ | `replays view <id>` | One replay: what the user did, in order (navigations, clicks / taps, console, network), and its segments. | read | `replays_view` |
160
+ | `issues replays <id>` | Replays that recorded this issue's events. | read | `issues_replays` |
150
161
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
151
162
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
152
163
  | `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.12.0" : "0.0.0-dev";
7
+ var VERSION = true ? "0.14.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
@@ -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}, attachments{store, maxKb, quotaMb, retentionDays}}",
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}, profiles{enabled, quotaMb, retentionDays}, replays{enabled, quotaMb, retentionDays}}",
1054
1054
  async run(ctx) {
1055
1055
  return ctx.api.get(await projectPath(ctx, "settings"));
1056
1056
  }
@@ -1097,6 +1097,12 @@ var settingsSet = {
1097
1097
  "attachment-max-kb": { type: "integer", value: "<kb>", description: "Largest attachment kept (default 5120)." },
1098
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
1099
  "attachments-retention-days": { type: "integer", value: "<days>", description: "Keep attachments this long (default 30)." },
1100
+ profiles: { type: "boolean", description: "Keep profiles (default on); --no-profiles drops them (profile:disabled)." },
1101
+ "profiles-quota-mb": { type: "integer", value: "<mb>", description: "Raw profiles' budget (default 500)." },
1102
+ "profiles-retention-days": { type: "integer", value: "<days>", description: "Keep raw profiles this long (default 7)." },
1103
+ replays: { type: "boolean", description: "Keep session replays (default OFF: a replay records the screen). --replays turns them on." },
1104
+ "replays-quota-mb": { type: "integer", value: "<mb>", description: "Replays' budget (default 1000)." },
1105
+ "replays-retention-days": { type: "integer", value: "<days>", description: "Keep replays this long (default 14)." },
1100
1106
  body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
1101
1107
  },
1102
1108
  examples: [
@@ -1106,7 +1112,7 @@ var settingsSet = {
1106
1112
  { 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." },
1107
1113
  { command: "tracksiy settings set --max-events-per-issue 500 --sourcemap-keep-releases 5", description: "A noisy project keeps less on disk." }
1108
1114
  ],
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}}",
1115
+ 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}, profiles{enabled, quotaMb, retentionDays}, replays{enabled, quotaMb, retentionDays}}",
1110
1116
  async run(ctx) {
1111
1117
  const path = await projectPath(ctx, "settings");
1112
1118
  const i = ctx.input;
@@ -1138,6 +1144,18 @@ var settingsSet = {
1138
1144
  ...i.attachmentsQuotaMb !== void 0 ? { quotaMb: i.attachmentsQuotaMb } : {},
1139
1145
  ...i.attachmentsRetentionDays !== void 0 ? { retentionDays: i.attachmentsRetentionDays } : {}
1140
1146
  },
1147
+ profiles: {
1148
+ ...current.profiles ?? {},
1149
+ ...i.profiles !== void 0 ? { enabled: i.profiles } : {},
1150
+ ...i.profilesQuotaMb !== void 0 ? { quotaMb: i.profilesQuotaMb } : {},
1151
+ ...i.profilesRetentionDays !== void 0 ? { retentionDays: i.profilesRetentionDays } : {}
1152
+ },
1153
+ replays: {
1154
+ ...current.replays ?? {},
1155
+ ...i.replays !== void 0 ? { enabled: i.replays } : {},
1156
+ ...i.replaysQuotaMb !== void 0 ? { quotaMb: i.replaysQuotaMb } : {},
1157
+ ...i.replaysRetentionDays !== void 0 ? { retentionDays: i.replaysRetentionDays } : {}
1158
+ },
1141
1159
  releaseHealth: {
1142
1160
  ...current.releaseHealth ?? {},
1143
1161
  ...i.crashFreeMin !== void 0 ? { crashFreeSessionsMin: i.crashFreeMin } : {},
@@ -1702,6 +1720,460 @@ ${d.notes.join("\n")}` : ""}`,
1702
1720
  };
1703
1721
  var evidence_default = [feedback, issueAttachments, attachmentGet, issueFlags];
1704
1722
 
1723
+ // src/commands/builds.ts
1724
+ import { existsSync as existsSync2 } from "node:fs";
1725
+ import { basename as basename2 } from "node:path";
1726
+
1727
+ // src/lib/appsize.ts
1728
+ import { readFileSync as readFileSync4, readdirSync, statSync } from "node:fs";
1729
+ import { basename, join as join2, relative, sep } from "node:path";
1730
+ import { deflateRawSync } from "node:zlib";
1731
+ var EOCD = 101010256;
1732
+ var ZIP64_LOCATOR = 117853008;
1733
+ var ZIP64_EOCD = 101075792;
1734
+ var CENTRAL = 33639248;
1735
+ function zipEntries(buf) {
1736
+ let eocd = -1;
1737
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) {
1738
+ if (buf.readUInt32LE(i) === EOCD) {
1739
+ eocd = i;
1740
+ break;
1741
+ }
1742
+ }
1743
+ if (eocd < 0) throw new Error("not a ZIP archive (no end of central directory)");
1744
+ let count = buf.readUInt16LE(eocd + 10);
1745
+ let offset = buf.readUInt32LE(eocd + 16);
1746
+ if (count === 65535 || offset === 4294967295) {
1747
+ const locator = eocd - 20;
1748
+ if (locator >= 0 && buf.readUInt32LE(locator) === ZIP64_LOCATOR) {
1749
+ const record = Number(buf.readBigUInt64LE(locator + 8));
1750
+ if (buf.readUInt32LE(record) === ZIP64_EOCD) {
1751
+ count = Number(buf.readBigUInt64LE(record + 32));
1752
+ offset = Number(buf.readBigUInt64LE(record + 48));
1753
+ }
1754
+ }
1755
+ }
1756
+ const out = [];
1757
+ let p = offset;
1758
+ for (let n = 0; n < count && p + 46 <= buf.length; n++) {
1759
+ if (buf.readUInt32LE(p) !== CENTRAL) break;
1760
+ let compressed = buf.readUInt32LE(p + 20);
1761
+ let uncompressed = buf.readUInt32LE(p + 24);
1762
+ const nameLen = buf.readUInt16LE(p + 28);
1763
+ const extraLen = buf.readUInt16LE(p + 30);
1764
+ const commentLen = buf.readUInt16LE(p + 32);
1765
+ const name = buf.toString("utf8", p + 46, p + 46 + nameLen);
1766
+ if (compressed === 4294967295 || uncompressed === 4294967295) {
1767
+ let e = p + 46 + nameLen;
1768
+ const end = e + extraLen;
1769
+ while (e + 4 <= end) {
1770
+ const id = buf.readUInt16LE(e);
1771
+ const size = buf.readUInt16LE(e + 2);
1772
+ if (id === 1) {
1773
+ let q = e + 4;
1774
+ if (uncompressed === 4294967295) {
1775
+ uncompressed = Number(buf.readBigUInt64LE(q));
1776
+ q += 8;
1777
+ }
1778
+ if (compressed === 4294967295) compressed = Number(buf.readBigUInt64LE(q));
1779
+ break;
1780
+ }
1781
+ e += 4 + size;
1782
+ }
1783
+ }
1784
+ if (!name.endsWith("/")) out.push({ path: name, download: compressed, install: uncompressed });
1785
+ p += 46 + nameLen + extraLen + commentLen;
1786
+ }
1787
+ return out;
1788
+ }
1789
+ function walk(dir) {
1790
+ const out = [];
1791
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1792
+ const full = join2(dir, entry.name);
1793
+ if (entry.isDirectory()) out.push(...walk(full));
1794
+ else if (entry.isFile()) out.push(full);
1795
+ }
1796
+ return out;
1797
+ }
1798
+ function dirEntries(dir) {
1799
+ return walk(dir).map((file) => {
1800
+ const size = statSync(file).size;
1801
+ let download = size;
1802
+ if (size > 0 && size <= 64 * 1024 * 1024) {
1803
+ try {
1804
+ download = Math.min(size, deflateRawSync(readFileSync4(file), { level: 6 }).length);
1805
+ } catch {
1806
+ download = size;
1807
+ }
1808
+ }
1809
+ return { path: relative(dir, file).split(sep).join("/"), download, install: size };
1810
+ });
1811
+ }
1812
+ function androidCategory(path, aab) {
1813
+ let p = path;
1814
+ if (aab) {
1815
+ if (p === "BundleConfig.pb" || p.startsWith("BUNDLE-METADATA/") || p.startsWith("META-INF/")) return "bundle metadata";
1816
+ const slash = p.indexOf("/");
1817
+ const module = slash > 0 ? p.slice(0, slash) : "";
1818
+ if (module && module !== "base") return `module:${module}`;
1819
+ p = slash > 0 ? p.slice(slash + 1) : p;
1820
+ if (p.startsWith("dex/")) return "dex";
1821
+ if (p === "resources.pb") return "resources";
1822
+ if (p.startsWith("manifest/")) return "manifest";
1823
+ if (p.startsWith("root/")) p = p.slice(5);
1824
+ }
1825
+ if (/^classes\d*\.dex$/.test(p)) return "dex";
1826
+ const lib = /^lib\/([^/]+)\/([^/]+)$/.exec(p);
1827
+ if (lib) {
1828
+ const [, abi, file] = lib;
1829
+ if (file === "libapp.so") return `dart code (${abi})`;
1830
+ if (file === "libflutter.so") return `flutter engine (${abi})`;
1831
+ if (/^lib(hermes|jsc|reactnative)/.test(file)) return `js engine (${abi})`;
1832
+ return `native (${abi})`;
1833
+ }
1834
+ if (p === "assets/index.android.bundle") return "js bundle";
1835
+ if (p.startsWith("assets/flutter_assets/")) return "flutter assets";
1836
+ if (p.startsWith("assets/")) return "assets";
1837
+ if (p.startsWith("res/") || p === "resources.arsc") return "resources";
1838
+ if (p.startsWith("META-INF/")) return "signing";
1839
+ if (p.startsWith("kotlin/")) return "kotlin metadata";
1840
+ if (p === "AndroidManifest.xml") return "manifest";
1841
+ return "other";
1842
+ }
1843
+ function iosCategory(path, appName) {
1844
+ if (path === appName) return "binary";
1845
+ if (path.startsWith("Frameworks/Flutter.framework/")) return "flutter engine";
1846
+ if (path.startsWith("Frameworks/App.framework/")) return "dart code";
1847
+ if (path.startsWith("Frameworks/")) return "frameworks";
1848
+ if (path.startsWith("PlugIns/") || path.startsWith("Extensions/")) return "extensions";
1849
+ if (path.startsWith("Watch/")) return "watch app";
1850
+ if (path.endsWith(".car")) return "asset catalog";
1851
+ if (path === "main.jsbundle") return "js bundle";
1852
+ if (/(^|\/)[^/]+\.lproj\//.test(path)) return "localizations";
1853
+ if (path.startsWith("_CodeSignature/") || path === "embedded.mobileprovision") return "signing";
1854
+ if (/\.bundle\//.test(path)) return "bundles";
1855
+ return "resources";
1856
+ }
1857
+ function appRelative(entries) {
1858
+ const first = entries.map((e) => /^(.*?\/)?([^/]+)\.app\//.exec(e.path)).find(Boolean);
1859
+ if (!first) return { entries, appName: "" };
1860
+ const prefix = (first[1] ?? "") + first[2] + ".app/";
1861
+ return {
1862
+ entries: entries.filter((e) => e.path.startsWith(prefix)).map((e) => ({ ...e, path: e.path.slice(prefix.length) })),
1863
+ appName: first[2]
1864
+ };
1865
+ }
1866
+ function analyze(path) {
1867
+ const isDir = statSync(path).isDirectory();
1868
+ const lower = path.toLowerCase().replace(/\/+$/, "");
1869
+ let kind = "other";
1870
+ if (lower.endsWith(".apk")) kind = "apk";
1871
+ else if (lower.endsWith(".aab")) kind = "aab";
1872
+ else if (lower.endsWith(".ipa")) kind = "ipa";
1873
+ else if (lower.endsWith(".xcarchive")) kind = "xcarchive";
1874
+ else if (lower.endsWith(".app")) kind = "app";
1875
+ const platform = kind === "apk" || kind === "aab" ? "android" : kind === "other" ? "other" : "ios";
1876
+ let entries;
1877
+ if (isDir) {
1878
+ const root = kind === "xcarchive" ? join2(path, "Products", "Applications") : path;
1879
+ entries = dirEntries(root);
1880
+ if (kind === "app") entries = entries.map((e) => ({ ...e, path: `${basename(path)}/${e.path}` }));
1881
+ } else {
1882
+ entries = zipEntries(readFileSync4(path));
1883
+ }
1884
+ let appName = "";
1885
+ if (platform === "ios") ({ entries, appName } = appRelative(entries));
1886
+ const categories = {};
1887
+ let download = 0;
1888
+ let install = 0;
1889
+ for (const e of entries) {
1890
+ const name = platform === "android" ? androidCategory(e.path, kind === "aab") : platform === "ios" ? iosCategory(e.path, appName) : e.path.split("/")[0] || "other";
1891
+ const c = categories[name] ??= { download: 0, install: 0, files: 0 };
1892
+ c.download += e.download;
1893
+ c.install += e.install;
1894
+ c.files += 1;
1895
+ download += e.download;
1896
+ install += e.install;
1897
+ }
1898
+ const largest = [...entries].sort((a, b) => b.download - a.download).slice(0, 50);
1899
+ return { kind, platform, downloadBytes: download, installBytes: install, fileCount: entries.length, categories, largest };
1900
+ }
1901
+
1902
+ // src/commands/builds.ts
1903
+ var mb2 = (n) => n === null || n === void 0 ? "-" : `${(Number(n) / 1e6).toFixed(2)} MB`;
1904
+ var signed = (n) => `${n > 0 ? "+" : ""}${(n / 1e6).toFixed(2)} MB`;
1905
+ function render(d) {
1906
+ const c = d.comparison;
1907
+ const head = kv({
1908
+ release: d.release,
1909
+ platform: `${d.platform} (${d.kind})`,
1910
+ download: mb2(d.downloadBytes),
1911
+ install: mb2(d.installBytes),
1912
+ files: d.fileCount,
1913
+ ...c ? { "vs previous": `${c.previousRelease}: download ${signed(c.downloadDelta)} (${c.downloadDeltaPct ?? "-"}%), install ${signed(c.installDelta)}` } : {},
1914
+ ...d.verdict ? { budgets: d.verdict } : {}
1915
+ });
1916
+ const categories = Object.entries(d.categories ?? {}).map(([category, v]) => ({ category, ...v })).sort((a, b) => b.download - a.download).slice(0, 15);
1917
+ const cats = categories.length ? `
1918
+
1919
+ ${table(categories, ["category", ["download", (r) => mb2(r.download)], ["install", (r) => mb2(r.install)], "files"])}` : "";
1920
+ const deltas = c?.categories?.length ? `
1921
+
1922
+ Changed:
1923
+ ${table(c.categories, ["category", ["download \u0394", (r) => signed(r.downloadDelta)], ["install \u0394", (r) => signed(r.installDelta)]])}` : "";
1924
+ const budgets = (d.budgets ?? []).length ? `
1925
+
1926
+ ${table(d.budgets, ["name", "metric", "max", ["value", (r) => r.value ?? "-"], "status"])}` : "";
1927
+ return head + cats + deltas + budgets;
1928
+ }
1929
+ var upload = {
1930
+ path: ["build", "upload"],
1931
+ 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.",
1932
+ 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.",
1933
+ context: "project",
1934
+ effect: "write",
1935
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/builds" },
1936
+ scopes: ["project:releases", "project:write", "project:admin"],
1937
+ args: [{ name: "path", required: true, description: "app-release.aab, app.apk, App.ipa, App.xcarchive or App.app." }],
1938
+ flags: {
1939
+ release: { type: "string", value: "<version>", description: "The release as the SDK names it: shop-android@1.4.2+57. Required." },
1940
+ commit: { type: "string", value: "<sha>", description: "The commit it was built from." },
1941
+ "build-configuration": { type: "string", value: "<name>", description: "Release, Debug, a flavor..." },
1942
+ "app-id": { type: "string", value: "<id>", description: "Package name / bundle id." },
1943
+ "max-growth": { type: "number", value: "<pct>", description: "Fail (exit 10) when the download grew more than this % against the previous build." },
1944
+ "dry-run": { type: "boolean", description: "Measure and print; record nothing." }
1945
+ },
1946
+ examples: [
1947
+ { 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." },
1948
+ { command: "tracksiy build upload build/Shop.xcarchive --release shop-ios@1.4.2+57 --dry-run", description: "Where the weight is, without recording." }
1949
+ ],
1950
+ output: "{id, release, platform, kind, downloadBytes, installBytes, fileCount, categories{...}, largest[...], comparison{previousRelease, downloadDelta, downloadDeltaPct, categories[...], newLargeFiles[...]}, budgets[...], verdict}",
1951
+ text: render,
1952
+ async run(ctx) {
1953
+ const i = ctx.input;
1954
+ if (!existsSync2(i.path)) throw usageError("not_found", `No such file or directory: ${i.path}`);
1955
+ let size;
1956
+ try {
1957
+ size = analyze(i.path);
1958
+ } catch (err) {
1959
+ throw usageError("bad_value", `Could not read ${i.path}: ${err.message}`, "Pass an .apk, .aab, .ipa, .xcarchive or .app.");
1960
+ }
1961
+ const body = {
1962
+ release: i.release,
1963
+ platform: size.platform,
1964
+ kind: size.kind,
1965
+ downloadBytes: size.downloadBytes,
1966
+ installBytes: size.installBytes,
1967
+ fileCount: size.fileCount,
1968
+ categories: size.categories,
1969
+ largest: size.largest,
1970
+ commit: i.commit ?? "",
1971
+ buildConfiguration: i.buildConfiguration ?? "",
1972
+ appId: i.appId ?? "",
1973
+ artifact: basename2(String(i.path).replace(/\/+$/, ""))
1974
+ };
1975
+ if (i.dryRun) return { dryRun: true, ...body, largest: body.largest.slice(0, 15) };
1976
+ 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.");
1977
+ const data = await ctx.api.send("POST", await projectPath(ctx, "builds"), body);
1978
+ const growth = data.comparison?.downloadDeltaPct;
1979
+ if (data.verdict === "fail") {
1980
+ const names = (data.budgets ?? []).filter((b) => b.status === "fail").map((b) => `${b.name} ${b.value} > ${b.max}`).join(", ");
1981
+ throw new CheckFailed(`Size budget crossed: ${names}.`, data, "See `build view <id>` for what grew.");
1982
+ }
1983
+ if (i.maxGrowth !== void 0 && growth !== null && growth !== void 0 && growth > i.maxGrowth) {
1984
+ throw new CheckFailed(`Download grew ${growth}% against ${data.comparison.previousRelease} (max ${i.maxGrowth}%).`, data, "See the `Changed` categories and `comparison.newLargeFiles`.");
1985
+ }
1986
+ return data;
1987
+ }
1988
+ };
1989
+ var list2 = {
1990
+ path: ["build", "list"],
1991
+ aliases: [["builds"]],
1992
+ summary: "Recorded builds newest first, with their growth against the build before.",
1993
+ context: "project",
1994
+ effect: "read",
1995
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/builds" },
1996
+ flags: {
1997
+ platform: { type: "string", enum: ["android", "ios", "other"], description: "Only this platform." },
1998
+ limit: { type: "integer", value: "<n>", description: "At most 100 (default 20)." }
1999
+ },
2000
+ output: "[{id, created, release, platform, kind, downloadBytes, installBytes, comparison{...}}]",
2001
+ 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)]]),
2002
+ async run(ctx) {
2003
+ return ctx.api.get(await projectPath(ctx, "builds"), { platform: ctx.input.platform, limit: ctx.input.limit });
2004
+ }
2005
+ };
2006
+ var view = {
2007
+ path: ["build", "view"],
2008
+ summary: "One build: size per category, the largest files, what changed against the previous build.",
2009
+ context: "project",
2010
+ effect: "read",
2011
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/builds/{id}" },
2012
+ args: [{ name: "id", required: true, description: "The build id (from `build list`)." }],
2013
+ output: "{id, release, categories{...}, largest[...], comparison{...}}",
2014
+ text: render,
2015
+ async run(ctx) {
2016
+ return ctx.api.get(await projectPath(ctx, `builds/${encodeURIComponent(ctx.input.id)}`));
2017
+ }
2018
+ };
2019
+ var builds_default = [upload, list2, view];
2020
+
2021
+ // src/commands/telemetry.ts
2022
+ var metricsList = {
2023
+ path: ["metrics", "list"],
2024
+ aliases: [["metrics"]],
2025
+ summary: "Metrics the app sent (Sentry.metrics.*, sentry-php traceMetrics, the server agent): name, type, unit, series and tag keys.",
2026
+ context: "project",
2027
+ effect: "read",
2028
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/metrics" },
2029
+ flags: { since: { type: "string", value: TIME, description: "Default 24h." } },
2030
+ output: "{since, metrics:[{name, type, unit, source, series, points, lastSeen, tagKeys[]}]}",
2031
+ text: (d) => table(d.metrics ?? [], ["name", "type", "unit", "series", "points", ["last", (r) => ago(r.lastSeen)], ["tags", (r) => (r.tagKeys ?? []).join(",")]]),
2032
+ async run(ctx) {
2033
+ return ctx.api.get(await projectPath(ctx, "metrics"), { since: since(ctx.input.since) });
2034
+ }
2035
+ };
2036
+ var metricsQuery = {
2037
+ path: ["metrics", "query"],
2038
+ summary: "One metric as a time series: sum, count, avg, min, max, last or p50..p99 per interval, grouped by a tag.",
2039
+ context: "project",
2040
+ effect: "read",
2041
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/metrics/query" },
2042
+ args: [{ name: "name", required: true, description: "The metric name." }],
2043
+ flags: {
2044
+ agg: { type: "string", enum: ["sum", "count", "avg", "min", "max", "last", "p50", "p75", "p90", "p95", "p99"], description: "Default sum." },
2045
+ since: { type: "string", value: TIME, description: "Default 24h." },
2046
+ until: { type: "string", value: TIME, description: "Default now." },
2047
+ "group-by": { type: "string", value: "<tag>", description: "One series per value of this tag (top 20)." },
2048
+ filter: { type: "string", value: "<key:value>", description: "Only points with this tag value." },
2049
+ interval: { type: "integer", value: "<seconds>", description: "Seconds per point (default by window)." }
2050
+ },
2051
+ examples: [{ command: "tracksiy metrics query checkout.duration --agg p95 --group-by route --since 6h", description: "p95 per route." }],
2052
+ output: "{name, type, unit, agg, interval, series:[{group, total, points:[{t, v, count}]}]}",
2053
+ text: (d) => `${kv({ name: d.name, type: d.type, unit: d.unit, agg: d.agg, interval: `${d.interval}s` })}
2054
+
2055
+ ${table(d.series ?? [], [["group", (r) => r.group ?? "(all)"], ["total", (r) => r.total ?? "-"], ["points", (r) => (r.points ?? []).length], ["last", (r) => (r.points ?? []).at(-1)?.v ?? "-"]])}`,
2056
+ async run(ctx) {
2057
+ const i = ctx.input;
2058
+ return ctx.api.get(await projectPath(ctx, "metrics/query"), { name: i.name, agg: i.agg, since: since(i.since), until: since(i.until, "until"), groupBy: i.groupBy, filter: i.filter, interval: i.interval });
2059
+ }
2060
+ };
2061
+ var profileFunctions = {
2062
+ path: ["profiles", "functions"],
2063
+ aliases: [["profiles"]],
2064
+ summary: "Where the CPU time goes: functions by self or total time, with file and line, per transaction or overall.",
2065
+ context: "project",
2066
+ effect: "read",
2067
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/profiles/functions" },
2068
+ flags: {
2069
+ since: { type: "string", value: TIME, description: "Default 7d." },
2070
+ transaction: { type: "string", value: "<name>", description: "Only profiles of this route / screen." },
2071
+ "application-only": { type: "boolean", description: "Only your own code (in_app frames)." },
2072
+ sort: { type: "string", enum: ["self", "total"], description: "Default self." },
2073
+ limit: { type: "integer", value: "<n>", description: "At most 200 (default 50)." }
2074
+ },
2075
+ examples: [{ command: "tracksiy profiles functions --transaction 'GET /cart' --application-only", description: "Our hottest code on one route." }],
2076
+ output: "{since, sampledMs, functions:[{function, module, file, line, isApplication, selfMs, totalMs, selfPct, totalPct, samples, profiles, transactions}], notes[]}",
2077
+ text: (d) => `${table(d.functions ?? [], [["self %", (r) => r.selfPct], ["total %", (r) => r.totalPct], "function", ["where", (r) => `${r.file ?? r.module ?? ""}${r.line ? ":" + r.line : ""}`], ["app", (r) => r.isApplication ? "yes" : ""]])}${(d.notes ?? []).length ? `
2078
+
2079
+ ${d.notes.join("\n")}` : ""}`,
2080
+ async run(ctx) {
2081
+ const i = ctx.input;
2082
+ return ctx.api.get(await projectPath(ctx, "profiles/functions"), { since: since(i.since), transaction: i.transaction, applicationOnly: i.applicationOnly || void 0, sort: i.sort, limit: i.limit });
2083
+ }
2084
+ };
2085
+ var profileList = {
2086
+ path: ["profiles", "list"],
2087
+ summary: "Stored profiles newest first.",
2088
+ context: "project",
2089
+ effect: "read",
2090
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/profiles" },
2091
+ flags: {
2092
+ since: { type: "string", value: TIME, description: "Only since then." },
2093
+ transaction: { type: "string", value: "<name>", description: "Only this transaction." },
2094
+ trace: { type: "string", value: "<trace id>", description: "Only this trace." },
2095
+ limit: { type: "integer", value: "<n>", description: "At most 200." }
2096
+ },
2097
+ output: "[{id, received, kind, platform, transaction, traceId, release, durationMs, samples, analyzed}]",
2098
+ text: (d) => table(d ?? [], ["id", ["when", (r) => ago(r.received)], "kind", "platform", "transaction", "durationMs", "samples", "analyzed"]),
2099
+ async run(ctx) {
2100
+ const i = ctx.input;
2101
+ return ctx.api.get(await projectPath(ctx, "profiles"), { since: since(i.since), transaction: i.transaction, trace: i.trace, limit: i.limit });
2102
+ }
2103
+ };
2104
+ var profileFlamegraph = {
2105
+ path: ["profiles", "flamegraph"],
2106
+ summary: "One profile's call tree, root to leaf, in ms (text: the heaviest paths).",
2107
+ context: "project",
2108
+ effect: "read",
2109
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/profiles/{id}/flamegraph" },
2110
+ args: [{ name: "id", required: true, description: "The profile id (from `profiles list`)." }],
2111
+ output: "{name, value, children[...]}",
2112
+ text: (d) => {
2113
+ const lines = [];
2114
+ const walk3 = (node, depth) => {
2115
+ if (depth > 12 || lines.length > 60) return;
2116
+ lines.push(`${" ".repeat(depth)}${Math.round(node.value)} ms ${node.name}`);
2117
+ for (const child of (node.children ?? []).slice(0, 3)) walk3(child, depth + 1);
2118
+ };
2119
+ walk3(d, 0);
2120
+ return lines.join("\n");
2121
+ },
2122
+ async run(ctx) {
2123
+ return ctx.api.get(await projectPath(ctx, `profiles/${encodeURIComponent(ctx.input.id)}/flamegraph`));
2124
+ }
2125
+ };
2126
+ var replayList = {
2127
+ path: ["replays", "list"],
2128
+ aliases: [["replays"]],
2129
+ summary: "Session replays newest first (kept only when the project turned replays on).",
2130
+ context: "project",
2131
+ effect: "read",
2132
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/replays" },
2133
+ flags: {
2134
+ since: { type: "string", value: TIME, description: "Only since then." },
2135
+ url: { type: "string", value: "<text>", description: "Only replays that visited a URL containing this." },
2136
+ limit: { type: "integer", value: "<n>", description: "At most 200." }
2137
+ },
2138
+ output: "[{replayId, started, durationMs, segments, platform, release, browser, os, device, urls[], errorIds[], hasVideo}]",
2139
+ text: (d) => table(d ?? [], ["replayId", ["when", (r) => ago(r.lastSeen)], ["length", (r) => `${Math.round(r.durationMs / 1e3)}s`], "platform", ["errors", (r) => (r.errorIds ?? []).length], ["first url", (r) => (r.urls ?? [])[0] ?? "-"]]),
2140
+ async run(ctx) {
2141
+ const i = ctx.input;
2142
+ return ctx.api.get(await projectPath(ctx, "replays"), { since: since(i.since), url: i.url, limit: i.limit });
2143
+ }
2144
+ };
2145
+ var replayView = {
2146
+ path: ["replays", "view"],
2147
+ summary: "One replay: what the user did, in order (navigations, clicks / taps, console, network), and its segments.",
2148
+ context: "project",
2149
+ effect: "read",
2150
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/replays/{replay_id}" },
2151
+ args: [{ name: "id", required: true, description: "The replay id (32 hex)." }],
2152
+ output: "{replayId, ..., segmentList[{segmentId, kind, recordingUrl, videoUrl}], timeline[{offsetMs, category, message, status?, durationMs?, level?}]}",
2153
+ text: (d) => `${kv({ replay: d.replayId, platform: d.platform, release: d.release ?? "-", length: `${Math.round((d.durationMs ?? 0) / 1e3)}s`, segments: d.segments })}
2154
+
2155
+ ${(d.timeline ?? []).map((l) => `${String(Math.round((l.offsetMs ?? 0) / 1e3)).padStart(5)}s ${String(l.category).padEnd(18)} ${l.message ?? ""}${l.status ? ` [${l.status}]` : ""}`).join("\n")}`,
2156
+ async run(ctx) {
2157
+ return ctx.api.get(await projectPath(ctx, `replays/${encodeURIComponent(ctx.input.id)}`));
2158
+ }
2159
+ };
2160
+ var issueReplays = {
2161
+ path: ["issues", "replays"],
2162
+ summary: "Replays that recorded this issue's events.",
2163
+ context: "project",
2164
+ effect: "read",
2165
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{id}/replays" },
2166
+ args: [{ name: "id", required: true, description: "The issue id." }],
2167
+ output: "{issueId, replays[...], notes[]}",
2168
+ text: (d) => `${table(d.replays ?? [], ["replayId", ["length", (r) => `${Math.round(r.durationMs / 1e3)}s`], "platform", ["first url", (r) => (r.urls ?? [])[0] ?? "-"]])}${(d.notes ?? []).length ? `
2169
+
2170
+ ${d.notes.join("\n")}` : ""}`,
2171
+ async run(ctx) {
2172
+ return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/replays`));
2173
+ }
2174
+ };
2175
+ var telemetry_default = [metricsList, metricsQuery, profileFunctions, profileList, profileFlamegraph, replayList, replayView, issueReplays];
2176
+
1705
2177
  // src/commands/issues.ts
1706
2178
  import { writeFileSync as writeFileSync2 } from "node:fs";
1707
2179
  var FILTERS = {
@@ -1759,7 +2231,7 @@ var issueRows = (issues) => table(issues ?? [], [
1759
2231
  ["status", (r) => r.regressedAt && r.status === "unresolved" ? "regressed" : r.status],
1760
2232
  ["title", (r) => r.title]
1761
2233
  ]);
1762
- var list2 = {
2234
+ var list3 = {
1763
2235
  path: ["issues", "list"],
1764
2236
  aliases: [["issues"]],
1765
2237
  summary: "Recent issues of a project, newest activity first, with the frames that matter inlined.",
@@ -1799,7 +2271,7 @@ ${issueRows(d.issues)}`,
1799
2271
  });
1800
2272
  }
1801
2273
  };
1802
- var view = {
2274
+ var view2 = {
1803
2275
  path: ["issues", "view"],
1804
2276
  summary: "One issue in enough depth to act on: latest event, stack frames, tags, recent volume.",
1805
2277
  context: "project",
@@ -2059,7 +2531,7 @@ var bulk = {
2059
2531
  return result;
2060
2532
  }
2061
2533
  };
2062
- var issues_default = [list2, view, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
2534
+ var issues_default = [list3, view2, resolve2, ignore, unresolve, bulk, merge, comment, activity, verifyFixed, claim, unclaim];
2063
2535
 
2064
2536
  // src/commands/projects.ts
2065
2537
  var create = {
@@ -2174,8 +2646,8 @@ var DECLARE = {
2174
2646
  "replace-recipients": { type: "boolean", description: "Make the recipient list exactly this one (default: add to it)." },
2175
2647
  body: { type: "json", value: "<json|@file|->", description: "Raw request body; overrides the other flags." }
2176
2648
  };
2177
- function recipients(list6, delivery) {
2178
- return (list6 ?? []).map((raw) => {
2649
+ function recipients(list7, delivery) {
2650
+ return (list7 ?? []).map((raw) => {
2179
2651
  const i = raw.indexOf(":");
2180
2652
  const type = i > 0 ? raw.slice(0, i) : raw;
2181
2653
  if (!RECIPIENT_TYPES.includes(type)) {
@@ -2356,7 +2828,7 @@ var settingsSet2 = {
2356
2828
  var alerts_default = [get, set, test, coverage, defaults, settingsGet2, settingsSet2];
2357
2829
 
2358
2830
  // src/commands/keys.ts
2359
- var list3 = {
2831
+ var list4 = {
2360
2832
  path: ["keys", "list"],
2361
2833
  aliases: [["keys"]],
2362
2834
  summary: "Every ingest key of the project: its surface, whether ingest accepts it, its rate limit and volume.",
@@ -2413,11 +2885,11 @@ var rotate = {
2413
2885
  return ctx.api.send("POST", await projectPath(ctx, `keys/${seg(ctx.input.name)}/rotate`));
2414
2886
  }
2415
2887
  };
2416
- var keys_default = [list3, set2, rotate];
2888
+ var keys_default = [list4, set2, rotate];
2417
2889
 
2418
2890
  // src/commands/releases.ts
2419
2891
  import { execFileSync } from "node:child_process";
2420
- var list4 = {
2892
+ var list5 = {
2421
2893
  path: ["releases", "list"],
2422
2894
  aliases: [["releases"]],
2423
2895
  summary: "What did the last deploys break? Recent releases with their events, issues and the issues they introduced.",
@@ -2563,25 +3035,25 @@ ${table(d.newIssues, ["short_id", "count", "title"])}` : ""}`,
2563
3035
  return data;
2564
3036
  }
2565
3037
  };
2566
- var releases_default = [list4, health2, declare, verdict];
3038
+ var releases_default = [list5, health2, declare, verdict];
2567
3039
 
2568
3040
  // src/commands/artifacts.ts
2569
3041
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2570
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
3042
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
2571
3043
 
2572
3044
  // src/lib/sourcemaps.ts
2573
3045
  import { createHash } from "node:crypto";
2574
- import { existsSync as existsSync2 } from "node:fs";
3046
+ import { existsSync as existsSync3 } from "node:fs";
2575
3047
  import { readdir, readFile, stat } from "node:fs/promises";
2576
- import { dirname as dirname2, join as join2, relative, resolve as resolve3, sep } from "node:path";
3048
+ import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
2577
3049
  var JS = /\.(m?js|cjs)$/i;
2578
3050
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".svn", ".hg"]);
2579
- async function walk(dir) {
3051
+ async function walk2(dir) {
2580
3052
  const out = [];
2581
3053
  for (const entry of await readdir(dir, { withFileTypes: true })) {
2582
- const full = join2(dir, entry.name);
3054
+ const full = join3(dir, entry.name);
2583
3055
  if (entry.isDirectory()) {
2584
- if (!SKIP_DIRS.has(entry.name)) out.push(...await walk(full));
3056
+ if (!SKIP_DIRS.has(entry.name)) out.push(...await walk2(full));
2585
3057
  } else if (entry.isFile()) out.push(full);
2586
3058
  }
2587
3059
  return out.sort();
@@ -2641,7 +3113,7 @@ async function findPairs(paths) {
2641
3113
  const st = await stat(abs).catch(() => void 0);
2642
3114
  if (!st) throw new Error(`No such file or directory: ${p}`);
2643
3115
  const root = st.isDirectory() ? abs : dirname2(abs);
2644
- const files = st.isDirectory() ? await walk(abs) : [abs];
3116
+ const files = st.isDirectory() ? await walk2(abs) : [abs];
2645
3117
  for (const js of files.filter((f) => JS.test(f))) {
2646
3118
  if (seen.has(js)) continue;
2647
3119
  seen.add(js);
@@ -2650,16 +3122,16 @@ async function findPairs(paths) {
2650
3122
  let map;
2651
3123
  if (url && !url.startsWith("data:")) {
2652
3124
  const candidate = resolve3(dirname2(js), decodeURIComponent(url.split(/[?#]/)[0]));
2653
- if (existsSync2(candidate)) map = candidate;
3125
+ if (existsSync3(candidate)) map = candidate;
2654
3126
  }
2655
- if (!map && existsSync2(`${js}.map`)) map = `${js}.map`;
3127
+ if (!map && existsSync3(`${js}.map`)) map = `${js}.map`;
2656
3128
  pairs2.push({ js, map, root });
2657
3129
  }
2658
3130
  }
2659
3131
  return pairs2;
2660
3132
  }
2661
3133
  function artifactName(file, root, prefix) {
2662
- const rel = relative(root, file).split(sep).join("/");
3134
+ const rel = relative2(root, file).split(sep2).join("/");
2663
3135
  const p = prefix.endsWith("/") ? prefix : `${prefix}/`;
2664
3136
  return `${p}${rel}`;
2665
3137
  }
@@ -2941,8 +3413,8 @@ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
2941
3413
  }
2942
3414
  };
2943
3415
  function uuidFromDebugMeta(path) {
2944
- if (!existsSync3(path)) throw usageError("bad_path", `No such file: ${path}`);
2945
- const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync4(path, "utf8"));
3416
+ if (!existsSync4(path)) throw usageError("bad_path", `No such file: ${path}`);
3417
+ const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync5(path, "utf8"));
2946
3418
  if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
2947
3419
  return m[1].toLowerCase();
2948
3420
  }
@@ -2967,7 +3439,7 @@ var proguardUpload = {
2967
3439
  output: "{id, state, files[{name, kind, state}], chunks{}, bytes{}, uuid}",
2968
3440
  async run(ctx) {
2969
3441
  const i = ctx.input;
2970
- if (!existsSync3(i.mapping)) throw usageError("bad_path", `No such file: ${i.mapping}`);
3442
+ if (!existsSync4(i.mapping)) throw usageError("bad_path", `No such file: ${i.mapping}`);
2971
3443
  const data = await readFile2(i.mapping);
2972
3444
  if (!data.length) throw usageError("bad_value", `${i.mapping} is empty.`);
2973
3445
  const uuid = (i.uuid ?? (i.debugMeta ? uuidFromDebugMeta(i.debugMeta) : proguardUuid(data))).toLowerCase();
@@ -3004,7 +3476,7 @@ var artifacts_default = [sourcemapsInject, sourcemapsUpload, proguardUpload, art
3004
3476
 
3005
3477
  // src/commands/tokens.ts
3006
3478
  var SCOPE_HINT = "e.g. org:read,project:read,event:read,event:write,project:write,project:releases,project:admin";
3007
- var list5 = {
3479
+ var list6 = {
3008
3480
  path: ["tokens", "list"],
3009
3481
  aliases: [["tokens"]],
3010
3482
  summary: "Your tokens, newest first, without their values.",
@@ -3117,7 +3589,7 @@ var self = {
3117
3589
  return ctx.api.get("tokens/self");
3118
3590
  }
3119
3591
  };
3120
- var tokens_default = [list5, self, create2, update, remove, rotate2];
3592
+ var tokens_default = [list6, self, create2, update, remove, rotate2];
3121
3593
 
3122
3594
  // src/commands/support.ts
3123
3595
  var SUPPORT = {
@@ -3154,33 +3626,33 @@ var support = {
3154
3626
  var support_default = [support];
3155
3627
 
3156
3628
  // src/commands/init.ts
3157
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
3629
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
3158
3630
  import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
3159
- import { join as join4, resolve as resolve4 } from "node:path";
3631
+ import { join as join5, resolve as resolve4 } from "node:path";
3160
3632
 
3161
3633
  // src/lib/detect.ts
3162
- import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
3163
- import { basename, join as join3 } from "node:path";
3634
+ import { existsSync as existsSync5, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "node:fs";
3635
+ import { basename as basename3, join as join4 } from "node:path";
3164
3636
  function readJson(path) {
3165
3637
  try {
3166
- return JSON.parse(readFileSync5(path, "utf8"));
3638
+ return JSON.parse(readFileSync6(path, "utf8"));
3167
3639
  } catch {
3168
3640
  return void 0;
3169
3641
  }
3170
3642
  }
3171
3643
  function has(dir, ...names) {
3172
- return names.find((n) => existsSync4(join3(dir, n)));
3644
+ return names.find((n) => existsSync5(join4(dir, n)));
3173
3645
  }
3174
3646
  function slugify(s) {
3175
3647
  return s.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
3176
3648
  }
3177
3649
  function detect(dir, guideBase) {
3178
3650
  const evidence = [];
3179
- const composer = readJson(join3(dir, "composer.json"));
3180
- const pkg = readJson(join3(dir, "package.json"));
3651
+ const composer = readJson(join4(dir, "composer.json"));
3652
+ const pkg = readJson(join4(dir, "package.json"));
3181
3653
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
3182
3654
  const hasDep = (d) => d in deps;
3183
- const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ?? basename(dir)) || "app";
3655
+ const name = slugify(pkg?.name ?? composer?.name?.split("/").pop() ?? basename3(dir)) || "app";
3184
3656
  const guide = (slug) => `${guideBase}/${slug}.md`;
3185
3657
  if (composer?.require?.["laravel/framework"]) {
3186
3658
  evidence.push("composer.json requires laravel/framework");
@@ -3235,8 +3707,8 @@ function detect(dir, guideBase) {
3235
3707
  }
3236
3708
  const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
3237
3709
  if (py) {
3238
- const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync5(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
3239
- const fw = existsSync4(join3(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
3710
+ const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync5(join4(dir, f)) ? readFileSync6(join4(dir, f), "utf8") : "").join("\n").toLowerCase();
3711
+ const fw = existsSync5(join4(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
3240
3712
  evidence.push(`${py} (${fw})`);
3241
3713
  return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
3242
3714
  }
@@ -3245,7 +3717,7 @@ function detect(dir, guideBase) {
3245
3717
  evidence.push(gradle);
3246
3718
  return { platform: "android", framework: "android", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
3247
3719
  }
3248
- const apple = has(dir, "Podfile", "Package.swift") ?? readdirSync(dir).find((f) => f.endsWith(".xcodeproj"));
3720
+ const apple = has(dir, "Podfile", "Package.swift") ?? readdirSync2(dir).find((f) => f.endsWith(".xcodeproj"));
3249
3721
  if (apple) {
3250
3722
  evidence.push(apple);
3251
3723
  return { platform: "apple-ios", framework: "ios", surfaces: ["mobile"], env: {}, guide: guide("mobile"), name, evidence };
@@ -3296,14 +3768,14 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3296
3768
  async run(ctx) {
3297
3769
  const i = ctx.input;
3298
3770
  const dir = resolve4(i.dir);
3299
- if (!existsSync5(dir)) throw usageError("bad_path", `No such directory: ${i.dir}`);
3771
+ if (!existsSync6(dir)) throw usageError("bad_path", `No such directory: ${i.dir}`);
3300
3772
  const found = detect(dir, ctx.config.url);
3301
3773
  const platform = i.platform ?? found.platform;
3302
3774
  const surfaces = i.surfaces ?? found.surfaces;
3303
3775
  const name = i.name ?? found.name;
3304
3776
  const detected = { platform, framework: found.framework, surfaces, evidence: found.evidence, name };
3305
3777
  const steps = [];
3306
- const upload = found.buildDir ? `tracksiy sourcemaps upload ${found.buildDir} --inject` : void 0;
3778
+ const upload2 = found.buildDir ? `tracksiy sourcemaps upload ${found.buildDir} --inject` : void 0;
3307
3779
  if (i.dryRun) {
3308
3780
  steps.push({ id: "create", run: `tracksiy init${i.writeEnv ? " --write-env" : ""}`, detail: "Create the project and keys." });
3309
3781
  return { dryRun: true, detected, guide: found.guide, steps };
@@ -3315,7 +3787,7 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3315
3787
  compact({ name, platform, surfaces, sameHost: i.sameHost })
3316
3788
  );
3317
3789
  const project2 = prov.project;
3318
- const pfPath = join4(dir, PROJECT_FILE);
3790
+ const pfPath = join5(dir, PROJECT_FILE);
3319
3791
  const pf = { org: project2.organization, project: project2.slug };
3320
3792
  if (ctx.config.url !== DEFAULT_URL) pf.url = ctx.config.url;
3321
3793
  await writeFile3(pfPath, JSON.stringify(pf, null, 2) + "\n");
@@ -3327,11 +3799,11 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
3327
3799
  }));
3328
3800
  let env;
3329
3801
  if (i.writeEnv) {
3330
- const envPath = join4(dir, ".env");
3331
- if (!existsSync5(envPath)) {
3802
+ const envPath = join5(dir, ".env");
3803
+ if (!existsSync6(envPath)) {
3332
3804
  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.");
3333
3805
  }
3334
- const current = readFileSync6(envPath, "utf8");
3806
+ const current = readFileSync7(envPath, "utf8");
3335
3807
  const added = [];
3336
3808
  const kept = [];
3337
3809
  let block = "";
@@ -3353,7 +3825,7 @@ ${block}`);
3353
3825
  if (!i.writeEnv && keys.some((k) => k.env)) {
3354
3826
  steps.push({ id: "env", detail: `Set ${keys.filter((k) => k.env).map((k) => k.env).join(", ")} from \`keys\` (or re-run with --write-env).` });
3355
3827
  }
3356
- if (upload) steps.push({ id: "sourcemaps", detail: "After each production build, upload source maps (uses project:releases).", run: upload });
3828
+ if (upload2) steps.push({ id: "sourcemaps", detail: "After each production build, upload source maps (uses project:releases).", run: upload2 });
3357
3829
  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" });
3358
3830
  steps.push({ id: "verify", detail: "Deploy, trigger one error, then prove it arrived.", run: "tracksiy projects verify --wait 120" });
3359
3831
  return {
@@ -3386,7 +3858,7 @@ function camel(name) {
3386
3858
  }
3387
3859
 
3388
3860
  // src/core/args.ts
3389
- import { readFileSync as readFileSync7 } from "node:fs";
3861
+ import { readFileSync as readFileSync8 } from "node:fs";
3390
3862
  var GLOBAL_FLAGS = {
3391
3863
  org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
3392
3864
  project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
@@ -3403,7 +3875,7 @@ var GLOBAL_FLAGS = {
3403
3875
  };
3404
3876
  var stdinCache;
3405
3877
  function readStdin() {
3406
- stdinCache ??= readFileSync7(0, "utf8");
3878
+ stdinCache ??= readFileSync8(0, "utf8");
3407
3879
  return stdinCache;
3408
3880
  }
3409
3881
  function parseJsonValue(name, raw) {
@@ -3411,7 +3883,7 @@ function parseJsonValue(name, raw) {
3411
3883
  if (raw === "-") text = readStdin();
3412
3884
  else if (raw.startsWith("@")) {
3413
3885
  try {
3414
- text = readFileSync7(raw.slice(1), "utf8");
3886
+ text = readFileSync8(raw.slice(1), "utf8");
3415
3887
  } catch (err) {
3416
3888
  throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
3417
3889
  }
@@ -3781,15 +4253,15 @@ function makeContext(spec, input, globals, surface, io) {
3781
4253
  const org = () => {
3782
4254
  orgPromise ??= (async () => {
3783
4255
  if (config.org) return config.org;
3784
- const list6 = await visible();
3785
- if (list6.length === 1) {
3786
- log(`using organization ${list6[0].slug}`);
3787
- return list6[0].slug;
4256
+ const list7 = await visible();
4257
+ if (list7.length === 1) {
4258
+ log(`using organization ${list7[0].slug}`);
4259
+ return list7[0].slug;
3788
4260
  }
3789
4261
  throw usageError(
3790
4262
  "missing_org",
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`."
4263
+ list7.length ? "Which organization? This token can see several." : "This token cannot see any organization.",
4264
+ 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`."
3793
4265
  );
3794
4266
  })();
3795
4267
  return orgPromise;
@@ -3799,8 +4271,8 @@ function makeContext(spec, input, globals, surface, io) {
3799
4271
  projectPromise ??= (async () => {
3800
4272
  const o = await org();
3801
4273
  if (config.project) return { org: o, project: config.project };
3802
- const list6 = await visible();
3803
- const projects2 = list6.find((x) => x.slug === o)?.projects ?? [];
4274
+ const list7 = await visible();
4275
+ const projects2 = list7.find((x) => x.slug === o)?.projects ?? [];
3804
4276
  if (projects2.length === 1) {
3805
4277
  log(`using project ${o}/${projects2[0].slug}`);
3806
4278
  return { org: o, project: projects2[0].slug };
@@ -4025,6 +4497,8 @@ var COMMANDS = [
4025
4497
  ...mobile_default,
4026
4498
  ...logs_default,
4027
4499
  ...evidence_default,
4500
+ ...builds_default,
4501
+ ...telemetry_default,
4028
4502
  ...issues_default,
4029
4503
  ...projects_default,
4030
4504
  ...alerts_default,
@@ -4038,8 +4512,8 @@ var COMMANDS = [
4038
4512
  ];
4039
4513
  var GROUPS = [
4040
4514
  ["Start", "init auth onboarding describe"],
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"],
4042
- ["Ship", "sourcemaps proguard"],
4515
+ ["Look", "overview changes health storage orgs projects issues monitors performance mobile logs metrics profiles replays user-feedback attachments queries not-found releases alerts keys artifacts settings privacy-preview"],
4516
+ ["Ship", "sourcemaps proguard build builds"],
4043
4517
  ["Manage", "tokens"],
4044
4518
  ["Talk to us", "support"],
4045
4519
  ["Other", "api mcp"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps, from one token.",
5
5
  "type": "module",
6
6
  "bin": {