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 +11 -0
- package/dist/tracksiy.mjs +533 -59
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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 = [
|
|
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(
|
|
2178
|
-
return (
|
|
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
|
|
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 = [
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3051
|
+
async function walk2(dir) {
|
|
2580
3052
|
const out = [];
|
|
2581
3053
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
2582
|
-
const full =
|
|
3054
|
+
const full = join3(dir, entry.name);
|
|
2583
3055
|
if (entry.isDirectory()) {
|
|
2584
|
-
if (!SKIP_DIRS.has(entry.name)) out.push(...await
|
|
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
|
|
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 (
|
|
3125
|
+
if (existsSync3(candidate)) map = candidate;
|
|
2654
3126
|
}
|
|
2655
|
-
if (!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 =
|
|
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 (!
|
|
2945
|
-
const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(
|
|
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 (!
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
|
3631
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
3160
3632
|
|
|
3161
3633
|
// src/lib/detect.ts
|
|
3162
|
-
import { existsSync as
|
|
3163
|
-
import { basename, join as
|
|
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(
|
|
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) =>
|
|
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(
|
|
3180
|
-
const pkg = readJson(
|
|
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() ??
|
|
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) =>
|
|
3239
|
-
const fw =
|
|
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") ??
|
|
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 (!
|
|
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
|
|
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 =
|
|
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 =
|
|
3331
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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
|
|
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 ??=
|
|
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 =
|
|
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
|
|
3785
|
-
if (
|
|
3786
|
-
log(`using organization ${
|
|
3787
|
-
return
|
|
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
|
-
|
|
3792
|
-
|
|
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
|
|
3803
|
-
const projects2 =
|
|
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"]
|