@keepsnappy/agent 0.1.0-alpha.15 → 0.1.0-alpha.17
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 +36 -6
- package/dist/appium-driver.js +124 -17
- package/dist/keepsnappy.js +1924 -373
- package/examples/farm/workflows/keepsnappy-measure.yml +3 -1
- package/examples/github-actions.yml +3 -1
- package/package.json +1 -1
package/dist/keepsnappy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// apps/agent/src/cli.ts
|
|
4
|
-
import { resolve as
|
|
4
|
+
import { resolve as resolve8 } from "node:path";
|
|
5
5
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
6
6
|
|
|
7
7
|
// apps/agent/src/config.ts
|
|
@@ -212,6 +212,15 @@ class ConfigError extends Error {
|
|
|
212
212
|
this.name = "ConfigError";
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
+
|
|
216
|
+
class UsageError extends ConfigError {
|
|
217
|
+
constructor(lines) {
|
|
218
|
+
super(lines);
|
|
219
|
+
this.message = lines.join(`
|
|
220
|
+
`);
|
|
221
|
+
this.name = "UsageError";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
215
224
|
var TITLE_MAX = 200;
|
|
216
225
|
var DESCRIPTION_MAX = 2000;
|
|
217
226
|
var TOP = ["project", "app", "platform", "device", "repetitions", "flows", "steps", "output", "timeout_seconds"];
|
|
@@ -1664,6 +1673,36 @@ var MEASURES = {
|
|
|
1664
1673
|
decimals: 2,
|
|
1665
1674
|
what: "The share of samples at heat level serious or above"
|
|
1666
1675
|
},
|
|
1676
|
+
"sampler.thermal.raw_start": {
|
|
1677
|
+
unit: "state",
|
|
1678
|
+
worse: "none",
|
|
1679
|
+
at: ["journey"],
|
|
1680
|
+
producer: "derive",
|
|
1681
|
+
label: "Raw Android thermal status at the start (0–6)",
|
|
1682
|
+
short: "raw Android thermal status at the start (0–6)",
|
|
1683
|
+
decimals: 0,
|
|
1684
|
+
what: "The first raw Android thermal status reported during the journey"
|
|
1685
|
+
},
|
|
1686
|
+
"sampler.thermal.raw_max": {
|
|
1687
|
+
unit: "state",
|
|
1688
|
+
worse: "none",
|
|
1689
|
+
at: ["journey"],
|
|
1690
|
+
producer: "derive",
|
|
1691
|
+
label: "Raw Android thermal status (peak, 0–6)",
|
|
1692
|
+
short: "raw Android thermal status (peak, 0–6)",
|
|
1693
|
+
decimals: 0,
|
|
1694
|
+
what: "The highest raw Android thermal status reported during the journey"
|
|
1695
|
+
},
|
|
1696
|
+
"sampler.thermal.raw_light_share": {
|
|
1697
|
+
unit: "ratio",
|
|
1698
|
+
worse: "none",
|
|
1699
|
+
at: ["journey"],
|
|
1700
|
+
producer: "derive",
|
|
1701
|
+
label: "Share of Android samples at LIGHT or above",
|
|
1702
|
+
short: "share of Android samples at LIGHT or above",
|
|
1703
|
+
decimals: 2,
|
|
1704
|
+
what: "The share of samples with a raw Android thermal status of 1 (LIGHT) or above, among samples reporting that status"
|
|
1705
|
+
},
|
|
1667
1706
|
"sampler.thermal.heats_up": {
|
|
1668
1707
|
unit: "share",
|
|
1669
1708
|
worse: "higher",
|
|
@@ -2103,6 +2142,7 @@ var DIMENSIONS = {
|
|
|
2103
2142
|
};
|
|
2104
2143
|
var BREAKDOWN_MEASURES = {
|
|
2105
2144
|
self_ms: { unit: "ms", worse: "higher", label: "render time", decimals: 1 },
|
|
2145
|
+
rolled_up_ms: { unit: "ms", worse: "none", label: "render time from libraries", decimals: 1 },
|
|
2106
2146
|
total_ms: { unit: "ms", worse: "higher", label: "total render time", decimals: 1 },
|
|
2107
2147
|
renders: { unit: "count", worse: "higher", label: "renders", decimals: 0 },
|
|
2108
2148
|
mounts: { unit: "count", worse: "higher", label: "mounts", decimals: 0 },
|
|
@@ -2112,7 +2152,53 @@ var BREAKDOWN_MEASURES = {
|
|
|
2112
2152
|
var measureDef = (id) => isMeasureId(id) ? MEASURES[id] : undefined;
|
|
2113
2153
|
var HEAT_METRIC = "sampler.thermal.heats_up";
|
|
2114
2154
|
var HEAT_LABEL = MEASURES[HEAT_METRIC].label;
|
|
2155
|
+
var FULL_ROW_MS = 2.5;
|
|
2156
|
+
// packages/facts/dist/src/components.js
|
|
2157
|
+
var FRAGMENT_VERSION = 2;
|
|
2158
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2159
|
+
var portable = (value) => typeof value === "string" && value.length > 0 && !/^(?:\/|[A-Za-z]:)/.test(value) && !value.includes("\\");
|
|
2160
|
+
var componentFile = (file) => portable(file) && !file.split("/").some((part) => part === ".." || part === "." || part === "");
|
|
2161
|
+
var isComponentName = (name) => typeof name === "string" && /^[\p{Lu}\p{Lt}][\p{ID_Continue}$\u200C\u200D]*$/u.test(name);
|
|
2162
|
+
var encodePart = (part) => encodeURIComponent(part).replace(/%2F/g, "/").replace(/%40/g, "@").replace(/~/g, "%7E");
|
|
2163
|
+
function decodePart(part) {
|
|
2164
|
+
try {
|
|
2165
|
+
const decoded = decodeURIComponent(part);
|
|
2166
|
+
return encodePart(decoded) === part ? decoded : null;
|
|
2167
|
+
} catch {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function componentKey(name, file) {
|
|
2172
|
+
if (name !== "default" && !isComponentName(name) || !componentFile(file))
|
|
2173
|
+
throw new Error("invalid client component name or file");
|
|
2174
|
+
return `${name}@${encodePart(file)}`;
|
|
2175
|
+
}
|
|
2176
|
+
function parseComponentKey(key) {
|
|
2177
|
+
if (typeof key !== "string")
|
|
2178
|
+
return null;
|
|
2179
|
+
const separator = key.indexOf("@");
|
|
2180
|
+
const name = key.slice(0, separator);
|
|
2181
|
+
const file = decodePart(key.slice(separator + 1));
|
|
2182
|
+
if (separator < 1 || name !== "default" && !isComponentName(name) || !componentFile(file))
|
|
2183
|
+
return null;
|
|
2184
|
+
return componentKey(name, file) === key ? { name, file } : null;
|
|
2185
|
+
}
|
|
2186
|
+
function isComponentFragment(value) {
|
|
2187
|
+
if (!object(value) || value.version !== FRAGMENT_VERSION || !portable(value.source) || typeof value.hash !== "string" || !/^[a-f0-9]{64}$/.test(value.hash) || !Array.isArray(value.components))
|
|
2188
|
+
return false;
|
|
2189
|
+
return value.components.every((component) => {
|
|
2190
|
+
if (!object(component) || typeof component.key !== "string")
|
|
2191
|
+
return false;
|
|
2192
|
+
const parsed = parseComponentKey(component.key);
|
|
2193
|
+
return parsed !== null && parsed.name === component.name && parsed.file === component.file && Number.isInteger(component.line) && component.line > 0;
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
function isComponentConfiguration(value) {
|
|
2197
|
+
return object(value) && value.version === FRAGMENT_VERSION && typeof value.directory === "string" && /^[a-zA-Z0-9-]+$/.test(value.directory);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2115
2200
|
// packages/facts/dist/src/places.js
|
|
2201
|
+
var placeWithoutVisit = (id) => placeRef(id).stem.replace(/#(\d+|repeat)$/, "");
|
|
2116
2202
|
var AFTER_3S = "+3s";
|
|
2117
2203
|
var kindOf = (stem) => {
|
|
2118
2204
|
if (stem === "*")
|
|
@@ -2136,16 +2222,97 @@ function placeRef(id) {
|
|
|
2136
2222
|
const stem = after3s ? id.slice(0, -AFTER_3S.length) : id;
|
|
2137
2223
|
return { id, kind: kindOf(stem), stem, segment: after3s ? "after3s" : "whole" };
|
|
2138
2224
|
}
|
|
2225
|
+
var placeSegment = (name, file) => `${name}@${encodePart(file)}`;
|
|
2226
|
+
var buildPlaceId = (parts) => {
|
|
2227
|
+
if (parts.kind === "screen")
|
|
2228
|
+
return `screen:${parts.screen}#${parts.visit}`;
|
|
2229
|
+
if (parts.kind === "launch")
|
|
2230
|
+
return `launch:${parts.screen}`;
|
|
2231
|
+
return `${parts.kind}:${parts.screen}›${parts.parent ? parts.parent + "›" : ""}${parts.target}${parts.k ? "~" + parts.k : ""}`;
|
|
2232
|
+
};
|
|
2233
|
+
function parseClientKey(key) {
|
|
2234
|
+
const anonymous = key.startsWith("default@");
|
|
2235
|
+
const parsed = parseComponentKey(anonymous ? "Default" + key.slice(7) : key);
|
|
2236
|
+
if (!parsed)
|
|
2237
|
+
return null;
|
|
2238
|
+
const name = anonymous ? "default" : parsed.name;
|
|
2239
|
+
return placeSegment(name, parsed.file) === key ? { name, file: parsed.file } : null;
|
|
2240
|
+
}
|
|
2241
|
+
var decodePlaceSegment = (key) => {
|
|
2242
|
+
if (key === "(no screen)")
|
|
2243
|
+
return { key, name: key, file: null };
|
|
2244
|
+
const at = key.indexOf("@");
|
|
2245
|
+
if (at < 1 || at === key.length - 1)
|
|
2246
|
+
return null;
|
|
2247
|
+
try {
|
|
2248
|
+
const name = key.slice(0, at);
|
|
2249
|
+
const file = decodePart(key.slice(at + 1));
|
|
2250
|
+
if (file === null)
|
|
2251
|
+
return null;
|
|
2252
|
+
if (name !== "(outside app components)" && !parseClientKey(key))
|
|
2253
|
+
return null;
|
|
2254
|
+
return placeSegment(name, file) === key ? { key, name, file } : null;
|
|
2255
|
+
} catch {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
var placeParts = (id) => {
|
|
2260
|
+
const stem = id.replace(/\+3s$/, "");
|
|
2261
|
+
const suffix = /#(\d+|repeat)$/.exec(stem);
|
|
2262
|
+
const base = suffix ? stem.slice(0, suffix.index) : stem;
|
|
2263
|
+
const occurrence = /~(\d+)$/.exec(base);
|
|
2264
|
+
const body = occurrence ? base.slice(0, occurrence.index) : base;
|
|
2265
|
+
const colon = body.indexOf(":");
|
|
2266
|
+
const kind = body.slice(0, colon);
|
|
2267
|
+
if (kind !== "screen" && kind !== "launch" && kind !== "press" && kind !== "drag")
|
|
2268
|
+
return null;
|
|
2269
|
+
const segments = body.slice(colon + 1).split("›");
|
|
2270
|
+
if (kind === "screen" || kind === "launch" ? segments.length !== 1 : segments.length < 2 || segments.length > 3)
|
|
2271
|
+
return null;
|
|
2272
|
+
const parts = segments.map(decodePlaceSegment);
|
|
2273
|
+
if (parts.some((p) => p === null))
|
|
2274
|
+
return null;
|
|
2275
|
+
if (parts.slice(1).some((p) => p.name === "(no screen)"))
|
|
2276
|
+
return null;
|
|
2277
|
+
if (parts.length === 3 && parts[1].name === "(outside app components)")
|
|
2278
|
+
return null;
|
|
2279
|
+
const screen = parts[0];
|
|
2280
|
+
const target = kind === "press" || kind === "drag" ? parts.at(-1) : null;
|
|
2281
|
+
return {
|
|
2282
|
+
kind,
|
|
2283
|
+
screen,
|
|
2284
|
+
target,
|
|
2285
|
+
parent: parts.length === 3 ? parts[1] : null,
|
|
2286
|
+
k: occurrence ? Number(occurrence[1]) : null,
|
|
2287
|
+
visit: suffix ? suffix[1] === "repeat" ? "repeat" : Number(suffix[1]) : null,
|
|
2288
|
+
outsideHost: target?.name === "(outside app components)" ? target.file : null
|
|
2289
|
+
};
|
|
2290
|
+
};
|
|
2139
2291
|
// packages/facts/dist/src/frame.js
|
|
2140
2292
|
var UNKNOWN_REFRESH_HZ = 60;
|
|
2141
2293
|
var LIMIT_FRAME_MS = 1000 / 60;
|
|
2142
2294
|
function frameBudgetMs(refreshHz) {
|
|
2143
2295
|
return 1000 / (refreshHz !== null && refreshHz !== undefined && Number.isFinite(refreshHz) && refreshHz > 0 ? refreshHz : UNKNOWN_REFRESH_HZ);
|
|
2144
2296
|
}
|
|
2297
|
+
// packages/facts/dist/src/profile-items.js
|
|
2298
|
+
function encodeProfileItems(rows, fullRowMs = FULL_ROW_MS) {
|
|
2299
|
+
const components = [], small = [];
|
|
2300
|
+
for (const row of rows) {
|
|
2301
|
+
if (row.selfMs === null || row.selfMs >= fullRowMs)
|
|
2302
|
+
components.push(row);
|
|
2303
|
+
else
|
|
2304
|
+
small.push([row.key ?? row.name, row.selfMs]);
|
|
2305
|
+
}
|
|
2306
|
+
small.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
2307
|
+
return { components, ...small.length ? { small } : {} };
|
|
2308
|
+
}
|
|
2145
2309
|
// packages/derive/dist/src/launch.js
|
|
2146
2310
|
var obj = (v) => typeof v === "object" && v !== null ? v : {};
|
|
2147
2311
|
var isScreenWindow = (w) => w.raw.kind === "screen" || w.name.startsWith("screen:");
|
|
2148
|
-
var launchPlaceOf = (name) =>
|
|
2312
|
+
var launchPlaceOf = (name) => {
|
|
2313
|
+
const p = placeParts(name);
|
|
2314
|
+
return p ? buildPlaceId({ kind: "launch", screen: p.screen.key }) : `launch:${placeWithoutVisit(name).replace(/^screen:/, "")}`;
|
|
2315
|
+
};
|
|
2149
2316
|
var isScreenName = (name) => name.startsWith("screen:") || name.startsWith("launch:");
|
|
2150
2317
|
function withLaunchNames(windows) {
|
|
2151
2318
|
const screens = windows.filter(isScreenWindow);
|
|
@@ -2154,6 +2321,9 @@ function withLaunchNames(windows) {
|
|
|
2154
2321
|
const isLaunch = (w) => marked ? obj(w.raw.screen).launch === true : w === first;
|
|
2155
2322
|
return windows.map((w) => isScreenWindow(w) && isLaunch(w) ? { ...w, name: launchPlaceOf(w.name) } : w);
|
|
2156
2323
|
}
|
|
2324
|
+
|
|
2325
|
+
// packages/derive/dist/src/model.js
|
|
2326
|
+
var DERIVATION_VERSION = 27;
|
|
2157
2327
|
// packages/stats/dist/src/core/rng.js
|
|
2158
2328
|
function splitmix32(seed) {
|
|
2159
2329
|
let s = seed >>> 0;
|
|
@@ -3130,7 +3300,7 @@ var obj3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v :
|
|
|
3130
3300
|
var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
3131
3301
|
var round = (x, digits = 3) => Math.round(x * 10 ** digits) / 10 ** digits;
|
|
3132
3302
|
var HOST_COMPONENTS = new Set(["View", "Text", "Image", "TextInput", "ScrollView"]);
|
|
3133
|
-
var isHostComponent = (name) => HOST_COMPONENTS.has(name) || /^RCT[A-Z]/.test(name);
|
|
3303
|
+
var isHostComponent = (name) => !parseComponentKey(name) && (HOST_COMPONENTS.has(name) || /^RCT[A-Z]/.test(name));
|
|
3134
3304
|
function causeCounts(raw, mounts) {
|
|
3135
3305
|
const c = obj3(raw);
|
|
3136
3306
|
const out = {};
|
|
@@ -3150,7 +3320,7 @@ function causeCounts(raw, mounts) {
|
|
|
3150
3320
|
}
|
|
3151
3321
|
return out;
|
|
3152
3322
|
}
|
|
3153
|
-
function componentRow(row, causes) {
|
|
3323
|
+
function componentRow(row, causes, rollup, detail) {
|
|
3154
3324
|
if (!Array.isArray(row) || typeof row[0] !== "string")
|
|
3155
3325
|
return;
|
|
3156
3326
|
const six = row.length >= 6;
|
|
@@ -3158,15 +3328,57 @@ function componentRow(row, causes) {
|
|
|
3158
3328
|
const mounts = num2(row[2]) ?? 0;
|
|
3159
3329
|
return {
|
|
3160
3330
|
name: row[0],
|
|
3331
|
+
...detail !== false && Object.hasOwn(rollup, row[0]) ? rollupOf(rollup[row[0]]) : {},
|
|
3161
3332
|
renders,
|
|
3162
3333
|
mounts,
|
|
3163
3334
|
instances: six ? num2(row[3]) ?? null : null,
|
|
3164
3335
|
selfMs: num2(row[six ? 4 : 3]) ?? null,
|
|
3165
3336
|
totalMs: num2(row[six ? 5 : 4]) ?? null,
|
|
3166
|
-
causes: causeCounts(causes[row[0]], mounts)
|
|
3337
|
+
causes: detail === false ? {} : causeCounts(causes[row[0]], mounts),
|
|
3338
|
+
...detail === undefined ? {} : { detailRenders: detail ? renders : 0 }
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
function rollupOf(raw) {
|
|
3342
|
+
const x = obj3(raw), rest = obj3(x.rolledUpRest);
|
|
3343
|
+
const rolledUp = (Array.isArray(x.rolledUp) ? x.rolledUp : []).flatMap((row) => Array.isArray(row) && typeof row[0] === "string" && num2(row[1]) !== undefined && num2(row[2]) !== undefined ? [{ name: row[0], ms: row[1], renders: row[2] }] : []);
|
|
3344
|
+
return {
|
|
3345
|
+
rolledUp,
|
|
3346
|
+
rolledUpRest: { ms: num2(rest.ms) ?? 0, n: num2(rest.n) ?? 0, ...rest.saturated === true ? { saturated: true } : {} },
|
|
3347
|
+
...typeof x.parent === "string" ? { parent: x.parent, parentOccurrences: { [x.parent]: 1 } } : {},
|
|
3348
|
+
...num2(x.rolledUpMs) === undefined ? {} : { rolledUpMs: num2(x.rolledUpMs) }
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
function mergeRollups(a, b) {
|
|
3352
|
+
const names = new Map;
|
|
3353
|
+
for (const row of [...a.rolledUp ?? [], ...b.rolledUp ?? []]) {
|
|
3354
|
+
const was = names.get(row.name);
|
|
3355
|
+
names.set(row.name, { name: row.name, ms: (was?.ms ?? 0) + row.ms, renders: (was?.renders ?? 0) + row.renders });
|
|
3356
|
+
}
|
|
3357
|
+
const x = a.rolledUpRest, y = b.rolledUpRest;
|
|
3358
|
+
return { rolledUp: [...names.values()], rolledUpRest: {
|
|
3359
|
+
ms: (x?.ms ?? 0) + (y?.ms ?? 0),
|
|
3360
|
+
n: Math.max(x?.n ?? 0, y?.n ?? 0),
|
|
3361
|
+
...x?.saturated || y?.saturated ? { saturated: true } : {},
|
|
3362
|
+
...x?.nAtLeast || y?.nAtLeast || x?.n && y?.n ? { nAtLeast: true } : {}
|
|
3363
|
+
} };
|
|
3364
|
+
}
|
|
3365
|
+
function shownRollup(r, n) {
|
|
3366
|
+
const ranked = [...r.rolledUp ?? []].sort((a, b) => b.ms - a.ms || a.name.localeCompare(b.name));
|
|
3367
|
+
const cut = ranked.slice(5), rest = r.rolledUpRest;
|
|
3368
|
+
return {
|
|
3369
|
+
rolledUp: ranked.slice(0, 5).map((x) => ({ name: x.name, ms: round(x.ms / n), renders: round(x.renders / n) })),
|
|
3370
|
+
rolledUpRest: { ms: round(((rest?.ms ?? 0) + cut.reduce((s, x) => s + x.ms, 0)) / n), n: Math.max(rest?.n ?? 0, cut.length), ...rest?.saturated ? { saturated: true } : {}, ...rest?.nAtLeast || rest?.n && cut.length ? { nAtLeast: true } : {} }
|
|
3167
3371
|
};
|
|
3168
3372
|
}
|
|
3169
|
-
function
|
|
3373
|
+
function outsideOf(profiles, n) {
|
|
3374
|
+
const parts = profiles.flatMap((p) => p.outside ? [p.outside] : []);
|
|
3375
|
+
if (!parts.length)
|
|
3376
|
+
return {};
|
|
3377
|
+
const merged = parts.reduce((a, b) => mergeRollups(a, b), {});
|
|
3378
|
+
return { outside: { ...n === undefined ? merged : shownRollup(merged, n), selfMs: round(parts.reduce((s, p) => s + p.selfMs, 0) / (n ?? 1)), mountSelfMs: round(parts.reduce((s, p) => s + p.mountSelfMs, 0) / (n ?? 1)) } };
|
|
3379
|
+
}
|
|
3380
|
+
var stampsOf = (ps) => ps.every((p) => p.stamped !== undefined) ? { stamped: ps.reduce((s, p) => s + p.stamped, 0) } : {};
|
|
3381
|
+
function windowProfile(raw, detailCoverage = true) {
|
|
3170
3382
|
const p = obj3(obj3(raw.react).profile);
|
|
3171
3383
|
if (p.renderer !== "profiling" && p.renderer !== "production")
|
|
3172
3384
|
return;
|
|
@@ -3187,11 +3399,14 @@ function windowProfile(raw) {
|
|
|
3187
3399
|
}
|
|
3188
3400
|
return {
|
|
3189
3401
|
renderer: p.renderer,
|
|
3402
|
+
...Number.isSafeInteger(p.slowestCommitSeq) && p.slowestCommitSeq >= 0 && num2(r.max) !== undefined && r.max >= 0 ? { slowestCommit: { seq: p.slowestCommitSeq, endAtMs: num2(p.slowestCommitEndAtMs) ?? null, ms: r.max } } : {},
|
|
3190
3403
|
commits: num2(p.commits) ?? 0,
|
|
3404
|
+
...num2(p.stamped) === undefined ? {} : { stamped: num2(p.stamped) },
|
|
3405
|
+
...p.outside && p.renderer === "profiling" ? { outside: { ...rollupOf(p.outside), selfMs: num2(obj3(p.outside).selfMs) ?? 0, mountSelfMs: num2(obj3(p.outside).mountSelfMs) ?? 0 } } : {},
|
|
3191
3406
|
renderMs: p.renderer === "profiling" && sum !== undefined ? { sum, max: num2(r.max) ?? sum, n: num2(r.n) ?? 0 } : null,
|
|
3192
3407
|
overBudget: Object.keys(overBudget).length ? overBudget : null,
|
|
3193
3408
|
updaters,
|
|
3194
|
-
components: (Array.isArray(p.components) ? p.components : []).map((row) => componentRow(row, causes)).filter((c) => !!c),
|
|
3409
|
+
components: (Array.isArray(p.components) ? p.components : []).map((row, index) => componentRow(row, causes, obj3(p.rollup), detailCoverage ? index < (num2(p.detailRows) ?? Infinity) : undefined)).filter((c) => !!c),
|
|
3195
3410
|
omitted: num2(p.omitted) ?? 0,
|
|
3196
3411
|
selfMsTotal: num2(p.selfMsTotal) ?? null,
|
|
3197
3412
|
mountSelfMs: num2(p.mountSelfMs) ?? null
|
|
@@ -3242,13 +3457,23 @@ function addComponent(into, c) {
|
|
|
3242
3457
|
into.set(c.name, {
|
|
3243
3458
|
name: c.name,
|
|
3244
3459
|
renders: was.renders + c.renders,
|
|
3460
|
+
...was.detailRenders === undefined && c.detailRenders === undefined ? {} : { detailRenders: (was.detailRenders ?? was.renders) + (c.detailRenders ?? c.renders) },
|
|
3245
3461
|
mounts: was.mounts + c.mounts,
|
|
3246
3462
|
instances: was.instances === null && c.instances === null ? null : Math.max(was.instances ?? 0, c.instances ?? 0),
|
|
3247
3463
|
selfMs: plus(was.selfMs, c.selfMs),
|
|
3248
3464
|
totalMs: plus(was.totalMs, c.totalMs),
|
|
3249
|
-
causes
|
|
3465
|
+
causes,
|
|
3466
|
+
...was.rolledUpMs === undefined && c.rolledUpMs === undefined ? {} : { rolledUpMs: (was.rolledUpMs ?? 0) + (c.rolledUpMs ?? 0), ...mergeRollups(was, c) },
|
|
3467
|
+
...mergedParent(was, c)
|
|
3250
3468
|
});
|
|
3251
3469
|
}
|
|
3470
|
+
function mergedParent(a, b) {
|
|
3471
|
+
const counts = { ...a.parentOccurrences };
|
|
3472
|
+
for (const [k, v] of Object.entries(b.parentOccurrences ?? {}))
|
|
3473
|
+
counts[k] = (counts[k] ?? 0) + v;
|
|
3474
|
+
const parent = Object.keys(counts).sort((a, b) => counts[b] - counts[a] || a.localeCompare(b))[0];
|
|
3475
|
+
return parent ? { parent, parentOccurrences: counts } : {};
|
|
3476
|
+
}
|
|
3252
3477
|
function exactMountOf(profiles) {
|
|
3253
3478
|
let selfMsTotal = 0;
|
|
3254
3479
|
let mountSelfMs = 0;
|
|
@@ -3262,17 +3487,20 @@ function exactMountOf(profiles) {
|
|
|
3262
3487
|
}
|
|
3263
3488
|
return { selfMsTotal, mountSelfMs };
|
|
3264
3489
|
}
|
|
3265
|
-
function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = false) {
|
|
3490
|
+
function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = false, allComponents = false) {
|
|
3266
3491
|
const picked = [];
|
|
3267
3492
|
for (const cl of clusters(windows)) {
|
|
3268
3493
|
const withProfile = cl.flatMap((w) => {
|
|
3269
|
-
const p = windowProfile(w.raw);
|
|
3494
|
+
const p = windowProfile(w.raw, allComponents);
|
|
3270
3495
|
return p ? [{ w, p }] : [];
|
|
3271
3496
|
});
|
|
3272
3497
|
if (!withProfile.length)
|
|
3273
3498
|
continue;
|
|
3274
3499
|
const weight = (x) => x.p.renderMs?.sum ?? x.p.components.reduce((s, c) => s + c.renders, 0);
|
|
3275
|
-
picked.push(withProfile.reduce((best, x) =>
|
|
3500
|
+
picked.push(withProfile.reduce((best, x) => {
|
|
3501
|
+
const span = (y) => y.w.endWall - y.w.startWall;
|
|
3502
|
+
return weight(x) > weight(best) || weight(x) === weight(best) && (span(x) > span(best) || allComponents && span(x) === span(best) && windowOrder(x.w, best.w) < 0) ? x : best;
|
|
3503
|
+
}));
|
|
3276
3504
|
}
|
|
3277
3505
|
if (!picked.length)
|
|
3278
3506
|
return;
|
|
@@ -3295,10 +3523,13 @@ function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = fal
|
|
|
3295
3523
|
overFrame = f === undefined || overFrame === undefined ? undefined : overFrame + f;
|
|
3296
3524
|
}
|
|
3297
3525
|
}
|
|
3526
|
+
const chosen = [...picked].sort((a, b) => (b.p.renderMs?.max ?? -1) - (a.p.renderMs?.max ?? -1) || windowOrder(a.w, b.w))[0];
|
|
3298
3527
|
const renderMs = timed ? picked.reduce((s, x) => s + (x.p.renderMs?.sum ?? 0), 0) : allIdle ? 0 : undefined;
|
|
3299
3528
|
const parts = picked.map((x) => timerCarriedRenderMs(x.w.raw));
|
|
3300
3529
|
const carried = parts.every((v) => v !== undefined) ? parts.reduce((s, v) => s + v, 0) : undefined;
|
|
3301
3530
|
return {
|
|
3531
|
+
window: chosen.w,
|
|
3532
|
+
...chosen.p.slowestCommit ? { slowestCommit: chosen.p.slowestCommit } : {},
|
|
3302
3533
|
renderer: timed ? "profiling" : "production",
|
|
3303
3534
|
commits: picked.reduce((s, x) => s + x.p.commits, 0),
|
|
3304
3535
|
renderMs,
|
|
@@ -3308,20 +3539,24 @@ function occurrence(windows, budgetMs, idleNeutral = false, frameMs, upper = fal
|
|
|
3308
3539
|
updaters,
|
|
3309
3540
|
components,
|
|
3310
3541
|
omitted: picked.reduce((s, x) => s + x.p.omitted, 0),
|
|
3311
|
-
distinctInWindow: Math.max(...picked.map((x) => x.p.components.length + x.p.omitted)),
|
|
3542
|
+
distinctInWindow: Math.max(...picked.map((x) => (allComponents ? x.p.components.filter((c) => !isHostComponent(c.name)).length : x.p.components.length) + x.p.omitted)),
|
|
3312
3543
|
exactMount: exactMountOf(picked.map((x) => x.p)),
|
|
3544
|
+
...outsideOf(picked.map((x) => x.p)),
|
|
3545
|
+
...stampsOf(picked.map((x) => x.p)),
|
|
3313
3546
|
...allIdle ? { idle: true } : {},
|
|
3314
3547
|
...upper ? { timerCarriedMs: carried, renderMsUpper: renderMs === undefined || carried === undefined ? undefined : renderMs + carried } : {}
|
|
3315
3548
|
};
|
|
3316
3549
|
}
|
|
3317
3550
|
var UPDATERS_KEPT = 5;
|
|
3551
|
+
var CPU_FUNCTIONS_KEPT = 10;
|
|
3552
|
+
var UNTIMED_COMPONENTS_KEPT = 10;
|
|
3318
3553
|
var topCauses = (causes, renders) => {
|
|
3319
3554
|
if (renders <= 0)
|
|
3320
3555
|
return {};
|
|
3321
3556
|
const entries = Object.entries(causes).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3);
|
|
3322
3557
|
return Object.fromEntries(entries.map(([k, v]) => [k, round(v / renders)]));
|
|
3323
3558
|
};
|
|
3324
|
-
function stepProfile(step, occ, budgetMs, topN, cpu, exactMount) {
|
|
3559
|
+
function stepProfile(step, occ, budgetMs, topN, cpu, exactMount, fullRowMs) {
|
|
3325
3560
|
const n = occ.length;
|
|
3326
3561
|
const mean = (x) => x / n;
|
|
3327
3562
|
const timed = occ.every((o) => o.renderer === "profiling");
|
|
@@ -3334,8 +3569,9 @@ function stepProfile(step, occ, budgetMs, topN, cpu, exactMount) {
|
|
|
3334
3569
|
const host = [...all.values()].filter((c) => isHostComponent(c.name));
|
|
3335
3570
|
const rank = (a, b) => (timed ? (b.selfMs ?? 0) - (a.selfMs ?? 0) : 0) || b.renders - a.renders || a.name.localeCompare(b.name);
|
|
3336
3571
|
own.sort(rank);
|
|
3337
|
-
const
|
|
3338
|
-
const
|
|
3572
|
+
const limit = fullRowMs === undefined ? topN : timed ? own.length : UNTIMED_COMPONENTS_KEPT;
|
|
3573
|
+
const kept = own.slice(0, limit);
|
|
3574
|
+
const rest = own.slice(limit);
|
|
3339
3575
|
const ms = (x) => timed && x !== null ? round(mean(x)) : null;
|
|
3340
3576
|
const updaters = {};
|
|
3341
3577
|
for (const o of occ)
|
|
@@ -3361,31 +3597,36 @@ function stepProfile(step, occ, budgetMs, topN, cpu, exactMount) {
|
|
|
3361
3597
|
cpuOut = {
|
|
3362
3598
|
busyMs: round(median(cpu.map((c) => c.busyMs))),
|
|
3363
3599
|
symbolicated: sym.length ? sym.every(Boolean) : null,
|
|
3364
|
-
functions: [...fns.values()].sort((a, b) => b.self - a.self || a.key.localeCompare(b.key)).slice(0, topN).map((f) => ({ key: f.key, name: f.name, selfMs: round(f.self / k), totalMs: f.total === null ? null : round(f.total / k) }))
|
|
3600
|
+
functions: [...fns.values()].sort((a, b) => b.self - a.self || a.key.localeCompare(b.key)).slice(0, fullRowMs === undefined ? topN : CPU_FUNCTIONS_KEPT).map((f) => ({ key: f.key, name: f.name, selfMs: round(f.self / k), totalMs: f.total === null ? null : round(f.total / k) }))
|
|
3365
3601
|
};
|
|
3366
3602
|
}
|
|
3603
|
+
const rows = kept.map((c) => ({
|
|
3604
|
+
key: c.name,
|
|
3605
|
+
...c.parent ? { parent: c.parent } : {},
|
|
3606
|
+
...c.rolledUpMs === undefined ? {} : { rolledUpMs: round(mean(c.rolledUpMs)), ...shownRollup(c, n) },
|
|
3607
|
+
name: c.name,
|
|
3608
|
+
renders: round(mean(c.renders)),
|
|
3609
|
+
mounts: round(mean(c.mounts)),
|
|
3610
|
+
instances: c.instances,
|
|
3611
|
+
selfMs: ms(c.selfMs),
|
|
3612
|
+
totalMs: ms(c.totalMs),
|
|
3613
|
+
causes: topCauses(c.causes, c.detailRenders ?? c.renders)
|
|
3614
|
+
}));
|
|
3367
3615
|
return {
|
|
3368
3616
|
step,
|
|
3369
3617
|
occurrences: n,
|
|
3618
|
+
...outsideOf(occ, n),
|
|
3619
|
+
...stampsOf(occ),
|
|
3370
3620
|
renderer,
|
|
3371
3621
|
frameBudgetMs: round(budgetMs, 1),
|
|
3372
3622
|
commits: round(mean(commits)),
|
|
3373
3623
|
renderMs: timed ? round(mean(occ.reduce((s, o) => s + (o.renderMs ?? 0), 0))) : null,
|
|
3374
3624
|
commitMaxMs: timed ? round(Math.max(...occ.map((o) => o.commitMaxMs ?? 0))) : null,
|
|
3375
|
-
|
|
3376
|
-
key: c.name,
|
|
3377
|
-
name: c.name,
|
|
3378
|
-
renders: round(mean(c.renders)),
|
|
3379
|
-
mounts: round(mean(c.mounts)),
|
|
3380
|
-
instances: c.instances,
|
|
3381
|
-
selfMs: ms(c.selfMs),
|
|
3382
|
-
totalMs: ms(c.totalMs),
|
|
3383
|
-
causes: topCauses(c.causes, c.renders)
|
|
3384
|
-
})),
|
|
3625
|
+
...fullRowMs !== undefined && timed ? encodeProfileItems(rows, fullRowMs) : { components: rows },
|
|
3385
3626
|
host: { renders: round(mean(host.reduce((s, c) => s + c.renders, 0))), selfMs: ms(host.reduce((s, c) => s + (c.selfMs ?? 0), 0)) },
|
|
3386
3627
|
omitted: {
|
|
3387
3628
|
count: rest.length,
|
|
3388
|
-
selfMs: ms(rest.reduce((s, c) => s + (c.selfMs ?? 0), 0)),
|
|
3629
|
+
selfMs: fullRowMs !== undefined && timed && occ.some((o) => o.omitted > 0) ? exact.every((e) => e !== undefined) ? Math.max(0, ms(exact.reduce((s, e) => s + e.selfMsTotal, 0) - [...all.values()].reduce((s, c) => s + (c.selfMs ?? 0), 0) - occ.reduce((s, o) => s + (o.outside?.selfMs ?? 0), 0))) : null : ms(rest.reduce((s, c) => s + (c.selfMs ?? 0), 0)),
|
|
3389
3630
|
sdkOmitted: occ.reduce((s, o) => s + o.omitted, 0)
|
|
3390
3631
|
},
|
|
3391
3632
|
updaters: commits > 0 ? Object.entries(updaters).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, UPDATERS_KEPT).map(([k, v]) => [k, round(v / commits)]) : [],
|
|
@@ -3423,7 +3664,7 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3423
3664
|
for (const [name, all] of [...byName].sort((a, b) => a[0].localeCompare(b[0])))
|
|
3424
3665
|
groups.push([name, all.map((w) => [w])]);
|
|
3425
3666
|
for (const [key, occs] of groups) {
|
|
3426
|
-
const found = occs.map((ws) => occurrence(ws.filter((w) => w.raw.endReason !== "timeout"), budget, options.idleWindowsNeutral, options.countFrameMs, options.upperBound === true)).filter((o) => !!o);
|
|
3667
|
+
const found = occs.map((ws) => occurrence(ws.filter((w) => w.raw.endReason !== "timeout"), budget, options.idleWindowsNeutral, options.countFrameMs, options.upperBound === true, options.fullRowMs !== undefined)).filter((o) => !!o);
|
|
3427
3668
|
const decided = found.some((o) => !o.idle) && found.every((o) => o.idle || o.renderer === "profiling");
|
|
3428
3669
|
const occ = decided ? found.map((o) => o.idle ? { ...o, renderer: "profiling" } : o) : found;
|
|
3429
3670
|
const cpu = options.cpu?.get(key);
|
|
@@ -3438,10 +3679,21 @@ function profileMetrics(windows, steps, options = {}) {
|
|
|
3438
3679
|
if (vals.length && vals.every((v) => v !== undefined))
|
|
3439
3680
|
metrics.set(`${key}.${f}`, median(vals));
|
|
3440
3681
|
}
|
|
3441
|
-
|
|
3682
|
+
const profile = stepProfile(key, occ, budget, topN, cpu, options.exactMount === true, options.fullRowMs);
|
|
3683
|
+
const kind = placeRef(key).kind;
|
|
3684
|
+
const active = occ.filter((o) => !o.idle);
|
|
3685
|
+
const selected = middleBy((active.length ? active : occ).filter((o) => o.window), (o) => o.commitMaxMs, (o) => num2(o.window.raw.id) ?? 0, (o) => num2(o.window.raw.t0) ?? o.window.startWall);
|
|
3686
|
+
const identity = options.commitIdentity && metrics.has(`${key}.commit_max_ms`) && ["screen", "press", "drag", "other"].includes(kind) && selected?.slowestCommit;
|
|
3687
|
+
const openedBy = selected?.window ? options.openedBy?.get(selected.window) : undefined;
|
|
3688
|
+
profiles.push({ ...profile, ...identity ? { slowestCommit: identity } : {}, ...openedBy ? { openedBy } : {} });
|
|
3442
3689
|
}
|
|
3443
3690
|
return { metrics, profiles };
|
|
3444
3691
|
}
|
|
3692
|
+
var windowOrder = (a, b) => (num2(a.raw.id) ?? 0) - (num2(b.raw.id) ?? 0) || (num2(a.raw.t0) ?? a.startWall) - (num2(b.raw.t0) ?? b.startWall);
|
|
3693
|
+
function middleBy(items, time, id, t0) {
|
|
3694
|
+
const ordered = [...items].sort((a, b) => (time(a) ?? -1) - (time(b) ?? -1) || id(a) - id(b) || t0(a) - t0(b));
|
|
3695
|
+
return ordered[Math.floor((ordered.length - 1) / 2)];
|
|
3696
|
+
}
|
|
3445
3697
|
|
|
3446
3698
|
// packages/derive/dist/src/facts.js
|
|
3447
3699
|
var ROUTE_CHANGE_ROW = "route_change";
|
|
@@ -4287,6 +4539,14 @@ function v6Metrics(windows, samples, t0, t1, version) {
|
|
|
4287
4539
|
out.set("sampler.thermal.start", thermal[0]);
|
|
4288
4540
|
out.set("sampler.thermal.hot_share", thermal.filter((v) => v >= 2).length / thermal.length);
|
|
4289
4541
|
}
|
|
4542
|
+
if (version >= 27) {
|
|
4543
|
+
const raw = inRun.map((s) => num7(s.raw.thermalRaw)).filter((v) => v !== undefined);
|
|
4544
|
+
if (raw.length) {
|
|
4545
|
+
out.set("sampler.thermal.raw_start", raw[0]);
|
|
4546
|
+
out.set("sampler.thermal.raw_max", Math.max(...raw));
|
|
4547
|
+
out.set("sampler.thermal.raw_light_share", raw.filter((v) => v >= 1).length / raw.length);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4290
4550
|
const hz = inRun.map((s) => num7(obj7(s.raw.frames).maxHz)).filter((v) => v !== undefined);
|
|
4291
4551
|
if (hz.length)
|
|
4292
4552
|
out.set("sampler.max_hz", Math.max(...hz));
|
|
@@ -4601,12 +4861,14 @@ function endsHome(windows, endCommandWall) {
|
|
|
4601
4861
|
}
|
|
4602
4862
|
function endMetrics(markers, events, samples, windows, launchMb, opts = {}) {
|
|
4603
4863
|
const out = new Map;
|
|
4604
|
-
const
|
|
4864
|
+
const declared = markers.some((m) => m.phase === "start");
|
|
4865
|
+
const readsEnd = declared || opts.markerless === true && events.gc.length > 0;
|
|
4866
|
+
const n = readsEnd ? runEndCommand(markers, events, windows) : null;
|
|
4605
4867
|
const command = n === null ? undefined : events.gc.find((g) => g.n === n && g.status === "ok");
|
|
4606
|
-
const home = endsHome(windows, command?.wall);
|
|
4868
|
+
const home = endsHome(windows, declared ? command?.wall : undefined);
|
|
4607
4869
|
if (home !== undefined)
|
|
4608
4870
|
out.set("sdk.journey_ends_home", home);
|
|
4609
|
-
if (!
|
|
4871
|
+
if (!readsEnd)
|
|
4610
4872
|
return out;
|
|
4611
4873
|
out.set("sdk.end_gc", command ? 1 : 0);
|
|
4612
4874
|
const g = n === null ? undefined : cleanOf(events, windows).get(n);
|
|
@@ -4614,6 +4876,8 @@ function endMetrics(markers, events, samples, windows, launchMb, opts = {}) {
|
|
|
4614
4876
|
return out;
|
|
4615
4877
|
const post = g.post === undefined ? undefined : footprintsBySeq(samples).get(g.post);
|
|
4616
4878
|
const manual = opts.manualRead ? manualReadFootprint(samples, g.n) : undefined;
|
|
4879
|
+
if (!declared && manual === undefined)
|
|
4880
|
+
return out;
|
|
4617
4881
|
const footprint = manual ?? post;
|
|
4618
4882
|
if (footprint !== undefined) {
|
|
4619
4883
|
if (opts.manualRead)
|
|
@@ -4678,8 +4942,7 @@ function keyFirstOpens(windows) {
|
|
|
4678
4942
|
}
|
|
4679
4943
|
|
|
4680
4944
|
// packages/derive/dist/src/derive.js
|
|
4681
|
-
var
|
|
4682
|
-
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
|
|
4945
|
+
var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27];
|
|
4683
4946
|
var windowsForVersion = (windows, version = DERIVATION_VERSION) => version >= 22 ? keyFirstOpens(windows) : windows;
|
|
4684
4947
|
function stepWindowsFor(windows, version = DERIVATION_VERSION) {
|
|
4685
4948
|
return windowsForVersion(version >= 16 ? withLaunchNames(windows).filter((w) => !isBackgroundWindow(w)) : windows, version);
|
|
@@ -4698,6 +4961,22 @@ function samplerFrameBudgetMs(samples) {
|
|
|
4698
4961
|
}
|
|
4699
4962
|
return hz > 1 ? frameBudgetMs(Math.round(hz)) : undefined;
|
|
4700
4963
|
}
|
|
4964
|
+
function profileOptions(windows, version, processWindows = windows) {
|
|
4965
|
+
if (version < 26)
|
|
4966
|
+
return {};
|
|
4967
|
+
const processes = new Set(processWindows.map((w) => w.raw.react?.causes?.process).filter((p) => typeof p === "string"));
|
|
4968
|
+
const presses = windows.filter((w) => w.raw.kind === "press" && placeRef(w.name).kind === "press" && placeRef(w.name).segment !== "after3s").sort((a, b) => appClock(b)[0] - appClock(a)[0] || a.name.localeCompare(b.name));
|
|
4969
|
+
const openedBy = new Map;
|
|
4970
|
+
for (const screen of windows.filter((w) => placeRef(w.name).kind === "screen")) {
|
|
4971
|
+
const start = appClock(screen)[0], opener = presses.find((w) => {
|
|
4972
|
+
const [from, to] = appClock(w);
|
|
4973
|
+
return from <= start && start < to;
|
|
4974
|
+
});
|
|
4975
|
+
if (opener)
|
|
4976
|
+
openedBy.set(screen, opener.name);
|
|
4977
|
+
}
|
|
4978
|
+
return { fullRowMs: FULL_ROW_MS, commitIdentity: processes.size <= 1, openedBy };
|
|
4979
|
+
}
|
|
4701
4980
|
function deriveRun(input) {
|
|
4702
4981
|
const { run, markers = [], ri, clockOffsetMs, samples = [], memWindow } = input;
|
|
4703
4982
|
const version = input.derivationVersion ?? DERIVATION_VERSION;
|
|
@@ -4719,7 +4998,9 @@ function deriveRun(input) {
|
|
|
4719
4998
|
return { rows, steps, run: final, profiles: [], derivationVersion: version };
|
|
4720
4999
|
const budget = input.frameBudgetMs ?? (version >= 6 ? samplerFrameBudgetMs(samplesInRun(samples, final.t0, final.t1)) : undefined);
|
|
4721
5000
|
const compareMs = version >= 20 ? LIMIT_FRAME_MS : budget ?? frameBudgetMs();
|
|
5001
|
+
const allComponents = profileOptions(windows, version, input.windows);
|
|
4722
5002
|
const prof = profileMetrics(windows, steps, {
|
|
5003
|
+
...allComponents,
|
|
4723
5004
|
frameBudgetMs: version >= 11 ? compareMs : Number(compareMs.toFixed(1)),
|
|
4724
5005
|
...version >= 12 ? { countFrameMs: LIMIT_FRAME_MS } : {},
|
|
4725
5006
|
...cpu === undefined ? {} : { cpu },
|
|
@@ -4729,6 +5010,7 @@ function deriveRun(input) {
|
|
|
4729
5010
|
...version >= 16 ? { upperBound: true } : {}
|
|
4730
5011
|
});
|
|
4731
5012
|
const bg = backgroundMetrics(background, {
|
|
5013
|
+
...allComponents,
|
|
4732
5014
|
frameBudgetMs: compareMs,
|
|
4733
5015
|
countFrameMs: LIMIT_FRAME_MS,
|
|
4734
5016
|
...input.profileTopN === undefined ? {} : { topN: input.profileTopN },
|
|
@@ -4759,7 +5041,7 @@ function deriveRun(input) {
|
|
|
4759
5041
|
if (version >= 19) {
|
|
4760
5042
|
const events = input.events ?? { gc: [], appState: [] };
|
|
4761
5043
|
const launch = extra.get("mem.launch_mb");
|
|
4762
|
-
for (const [k, v] of [...endMetrics(markers, events, samplesInRun(samples, final.t0, final.t1), sdkWindows, launch, { manualRead: version >= 23 }), ...jsHeapMetrics(events, sdkWindows)])
|
|
5044
|
+
for (const [k, v] of [...endMetrics(markers, events, samplesInRun(samples, final.t0, final.t1), sdkWindows, launch, { manualRead: version >= 23, markerless: version >= 24 }), ...jsHeapMetrics(events, sdkWindows)])
|
|
4763
5045
|
extra.set(k, v);
|
|
4764
5046
|
}
|
|
4765
5047
|
const base = { run_id: final.run_id, arm: final.arm, block: final.block, valid: final.valid, invalid_reason: final.invalid_reason };
|
|
@@ -4898,7 +5180,7 @@ async function call(f, base, path, headers, body, method = "POST") {
|
|
|
4898
5180
|
try {
|
|
4899
5181
|
return text ? JSON.parse(text) : {};
|
|
4900
5182
|
} catch {
|
|
4901
|
-
|
|
5183
|
+
throw new UploadError(`${method} ${path} -> ${res.status}, the answer is not JSON (is the URL the KeepSnappy API?): ${text.slice(0, 200)}`);
|
|
4902
5184
|
}
|
|
4903
5185
|
}
|
|
4904
5186
|
async function uploadRuns(deps, outDir, runs, opts) {
|
|
@@ -4962,44 +5244,24 @@ async function uploadRuns(deps, outDir, runs, opts) {
|
|
|
4962
5244
|
return { uploaded, skipped };
|
|
4963
5245
|
}
|
|
4964
5246
|
|
|
4965
|
-
// packages/present/dist/src/
|
|
4966
|
-
var
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
tap: "Tap",
|
|
4970
|
-
scroll: "Scroll",
|
|
4971
|
-
step: "Test step",
|
|
4972
|
-
journey: "Whole journey",
|
|
4973
|
-
background: "Background",
|
|
4974
|
-
after3s: "3 s after"
|
|
4975
|
-
};
|
|
4976
|
-
var KIND_WORD = {
|
|
4977
|
-
journey: PLACE_KIND_WORDS.journey,
|
|
4978
|
-
screen: PLACE_KIND_WORDS.screen,
|
|
4979
|
-
launch: PLACE_KIND_WORDS.launch,
|
|
4980
|
-
press: PLACE_KIND_WORDS.tap,
|
|
4981
|
-
drag: PLACE_KIND_WORDS.scroll,
|
|
4982
|
-
step: PLACE_KIND_WORDS.step,
|
|
4983
|
-
background: PLACE_KIND_WORDS.background
|
|
4984
|
-
};
|
|
4985
|
-
var BACKGROUND_SOURCES = {
|
|
4986
|
-
websocket: "data pushed over a WebSocket",
|
|
4987
|
-
launch: "work started at app launch",
|
|
4988
|
-
link: "work of a deep link outside its screen",
|
|
4989
|
-
touch: "work of a tap outside its window",
|
|
4990
|
-
input: "work of typing outside its window"
|
|
4991
|
-
};
|
|
4992
|
-
var OPEN_MARK = /#(1|repeat)$/;
|
|
5247
|
+
// packages/present/dist/src/capital.js
|
|
5248
|
+
var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5249
|
+
|
|
5250
|
+
// packages/present/dist/src/placeWords.js
|
|
4993
5251
|
var GENERIC_GROUPS = new Set(["app", "tabs", "root"]);
|
|
4994
5252
|
var ORDINAL_SUFFIXES = ["th", "st", "nd", "rd"];
|
|
4995
5253
|
var LAST_TEEN = 13;
|
|
4996
5254
|
var FIRST_TEEN = 11;
|
|
4997
5255
|
var words = (s) => s.replace(/[._\-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
4998
5256
|
var cap = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
5257
|
+
var nameWords = (s) => cap(words(s));
|
|
4999
5258
|
var ordinal = (n) => {
|
|
5000
5259
|
const inTeens = n % 100 >= FIRST_TEEN && n % 100 <= LAST_TEEN;
|
|
5001
5260
|
return `${n}${inTeens ? "th" : ORDINAL_SUFFIXES[n % 10] ?? "th"}`;
|
|
5002
5261
|
};
|
|
5262
|
+
function routeFileName(path) {
|
|
5263
|
+
return path.split("/").filter((p) => p && !/^\(.*\)$/.test(p) && p !== "index").map(nameWords).join(" ") || "App start";
|
|
5264
|
+
}
|
|
5003
5265
|
function screenName(path) {
|
|
5004
5266
|
const parts = path.split("/").filter((p) => p && p !== "__root");
|
|
5005
5267
|
const plain = parts.filter((p) => !/^\(.*\)$/.test(p) && !/^\[.*\]$/.test(p) && p !== "index");
|
|
@@ -5011,50 +5273,7 @@ function screenName(path) {
|
|
|
5011
5273
|
const name = cap(words(leaf));
|
|
5012
5274
|
return group && !GENERIC_GROUPS.has(group) && !plain.slice(0, -1).length ? `${cap(words(group))} › ${name}` : name;
|
|
5013
5275
|
}
|
|
5014
|
-
|
|
5015
|
-
if (kind === "journey")
|
|
5016
|
-
return PLACE_KIND_WORDS.journey;
|
|
5017
|
-
if (kind === "other")
|
|
5018
|
-
return words(stem);
|
|
5019
|
-
const rest = stem.slice(stem.indexOf(":") + 1).replace(kind === "press" || kind === "drag" ? OPEN_MARK : /$^/, "");
|
|
5020
|
-
if (kind === "background")
|
|
5021
|
-
return `${KIND_WORD.background}: ${BACKGROUND_SOURCES[rest] ?? words(rest)}`;
|
|
5022
|
-
if (kind === "launch")
|
|
5023
|
-
return `${KIND_WORD.launch}: ${screenName(rest)}`;
|
|
5024
|
-
if (kind !== "screen")
|
|
5025
|
-
return `${KIND_WORD[kind]}: ${words(rest)}`;
|
|
5026
|
-
const visitMark = /^(.*)#(\d+)$/.exec(rest);
|
|
5027
|
-
const visit = visitMark ? Number(visitMark[2]) : 1;
|
|
5028
|
-
return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
|
|
5029
|
-
}
|
|
5030
|
-
function windowName(id) {
|
|
5031
|
-
if (id === "flow")
|
|
5032
|
-
return PLACE_KIND_WORDS.journey;
|
|
5033
|
-
const ref = placeRef(id);
|
|
5034
|
-
const name = stemName(ref.stem, ref.kind);
|
|
5035
|
-
return ref.segment === "after3s" ? `${name} · ${PLACE_KIND_WORDS.after3s}` : name;
|
|
5036
|
-
}
|
|
5037
|
-
function placeKindWord(id) {
|
|
5038
|
-
if (id === "flow")
|
|
5039
|
-
return PLACE_KIND_WORDS.journey;
|
|
5040
|
-
const { kind } = placeRef(id);
|
|
5041
|
-
return kind === "other" ? "Window" : KIND_WORD[kind];
|
|
5042
|
-
}
|
|
5043
|
-
var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
|
|
5044
|
-
var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
|
|
5045
|
-
function placeLabel(id, screen) {
|
|
5046
|
-
if (id === "flow")
|
|
5047
|
-
return PLACE_KIND_WORDS.journey;
|
|
5048
|
-
const ref = placeRef(id);
|
|
5049
|
-
const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5050
|
-
if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
|
|
5051
|
-
const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
|
|
5052
|
-
return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`;
|
|
5053
|
-
}
|
|
5054
|
-
const name = windowName(ref.stem);
|
|
5055
|
-
const colon = name.indexOf(": ");
|
|
5056
|
-
return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
|
|
5057
|
-
}
|
|
5276
|
+
|
|
5058
5277
|
// packages/present/dist/src/states.js
|
|
5059
5278
|
var STATE_WORDS = {
|
|
5060
5279
|
worse: "Worse",
|
|
@@ -5064,7 +5283,10 @@ var STATE_WORDS = {
|
|
|
5064
5283
|
"cant-tell": "Can’t tell"
|
|
5065
5284
|
};
|
|
5066
5285
|
var INFO_WORDS = { heat: "Phone heated up" };
|
|
5286
|
+
var isWorseState = (state) => state === "worse";
|
|
5067
5287
|
var NOT_BLOCKING = "not blocking";
|
|
5288
|
+
var badgedWord = (word, blocking) => word === STATE_WORDS.worse && blocking === false ? `${word} (${NOT_BLOCKING})` : word;
|
|
5289
|
+
|
|
5068
5290
|
// packages/present/dist/src/values.js
|
|
5069
5291
|
var NO_VALUE = "–";
|
|
5070
5292
|
var FALLBACK_DECIMALS = 1;
|
|
@@ -5120,14 +5342,425 @@ function fieldValueText(field, v) {
|
|
|
5120
5342
|
const def = fieldDef(field.split(KEY_SEPARATOR)[0]);
|
|
5121
5343
|
return format(def?.unit, def?.decimals ?? FALLBACK_DECIMALS, v);
|
|
5122
5344
|
}
|
|
5345
|
+
|
|
5346
|
+
// packages/present/dist/src/rowWords.js
|
|
5347
|
+
var GONE_TAILS = {
|
|
5348
|
+
branch: { notInRun: "on main, not in this run", removed: "on main, not rendered in this run" },
|
|
5349
|
+
journey: { notInRun: "in the sessions before, not in the newest session", removed: "in the sessions before, not rendered in the newest session" }
|
|
5350
|
+
};
|
|
5351
|
+
var ROW_STATE_WORDS = {
|
|
5352
|
+
new: "New in this PR: not compared with main",
|
|
5353
|
+
notInRun: upperFirst(GONE_TAILS.branch.notInRun),
|
|
5354
|
+
removed: upperFirst(GONE_TAILS.branch.removed)
|
|
5355
|
+
};
|
|
5356
|
+
var JOURNEY_ROW_WORDS = {
|
|
5357
|
+
new: "New in the newest session: not compared with the sessions before",
|
|
5358
|
+
notInRun: upperFirst(GONE_TAILS.journey.notInRun),
|
|
5359
|
+
removed: upperFirst(GONE_TAILS.journey.removed)
|
|
5360
|
+
};
|
|
5361
|
+
var NEW_IN_PR_CHIP = "New in this PR";
|
|
5362
|
+
var placeNamesChangedTitle = (sdk) => `Place names changed with SDK ${sdk}`;
|
|
5363
|
+
var restartRowText = (sdk, mainSessions, needed) => `${placeNamesChangedTitle(sdk)}: ${restartContextText(mainSessions, needed)}`;
|
|
5364
|
+
var restartContextText = (mainSessions, needed) => `main has ${mainSessions} of ${needed} sessions on the new names`;
|
|
5365
|
+
function removedRowsText(n, page) {
|
|
5366
|
+
return { lead: `${plural(n, "component", "components")} ${GONE_TAILS[page].removed}:`, rest: " not counted." };
|
|
5367
|
+
}
|
|
5368
|
+
function notInRunRowsText(c, page) {
|
|
5369
|
+
const what = [c.screens ? plural(c.screens, "screen", "screens") : "", c.places ? `${c.places} ${c.places === 1 ? "tap or scroll" : "taps and scrolls"}` : ""].filter(Boolean).join(" and ");
|
|
5370
|
+
const them = c.screens + c.places === 1 ? "it" : "them";
|
|
5371
|
+
return { lead: `${what} ${GONE_TAILS[page].notInRun}:`, rest: ` the ${page === "journey" ? "newest session" : "journey"} did not reach ${them}; not counted.` };
|
|
5372
|
+
}
|
|
5373
|
+
var OUTSIDE_APP_NAME = "Outside app components";
|
|
5374
|
+
var OTHER_RENDER_WORK_NAME = "Other render work";
|
|
5375
|
+
var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
5376
|
+
var rolledUpText = (rolledUpMs, totalMs) => `From libraries: ${valueText("self_ms", rolledUpMs)} of ${valueText("self_ms", totalMs)}`;
|
|
5377
|
+
var libraryLineText = (l) => `${l.name} · ${l.renders} rendered · ${valueText("self_ms", l.ms)}`;
|
|
5378
|
+
var rolledUpRestText = (rest) => `${plural(rest.n, "other library", "other libraries")} · ${valueText("self_ms", rest.ms)}`;
|
|
5379
|
+
var measureWords = (measure) => isMeasureId(measure) ? MEASURES[measure].label : `component ${BREAKDOWN_MEASURES[measure].label}`;
|
|
5380
|
+
var TIER_STATE = { worse: "worse", slight: "warning", better: "better", same: "no-change" };
|
|
5381
|
+
function leadBody(subject, measure, main, pr) {
|
|
5382
|
+
const unit = shownUnit(isMeasureId(measure) ? MEASURES[measure].unit : BREAKDOWN_MEASURES[measure].unit).unit;
|
|
5383
|
+
const to = valueText(measure, pr);
|
|
5384
|
+
const from = valueText(measure, main);
|
|
5385
|
+
const bare = unit && from.endsWith(` ${unit}`) && to.endsWith(` ${unit}`) ? from.slice(0, from.length - unit.length - 1) : from;
|
|
5386
|
+
return `${subject}, ${measureWords(measure)} ${bare} → ${to}`;
|
|
5387
|
+
}
|
|
5388
|
+
var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_STATE[tier]]}: ${leadBody(subject, measure, main, pr)}`;
|
|
5389
|
+
var FOR_INFORMATION = "For information";
|
|
5390
|
+
var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
|
|
5391
|
+
function foldedText(n, rows, page) {
|
|
5392
|
+
return `${n} ${GONE_TAILS[page][rows === "components" ? "removed" : "notInRun"]}`;
|
|
5393
|
+
}
|
|
5394
|
+
function placeShownName(p, o = {}) {
|
|
5395
|
+
const target = p.target ? componentShownName(p.target.name, p.target.file) : libraryViewName(p.kind, p.host ?? "");
|
|
5396
|
+
const named = p.target && p.parent ? `${target} in ${componentShownName(p.parent.name, p.parent.file)}${p.k ? `, ${ordinal(p.k)}` : ""}` : target;
|
|
5397
|
+
return o.onScreen ? `${named} on ${o.onScreen}` : named;
|
|
5398
|
+
}
|
|
5399
|
+
var libraryViewName = (kind, hostType) => `${kind === "drag" ? "Scroll" : "Tap"} outside app components (${hostType})`;
|
|
5400
|
+
function componentShownName(name, file) {
|
|
5401
|
+
if (name !== "default" || !file)
|
|
5402
|
+
return name;
|
|
5403
|
+
const path = file.replace(/\.[^.\/]+$/, "");
|
|
5404
|
+
const route = /^(?:src\/)?app\/(.*)$/.exec(path)?.[1];
|
|
5405
|
+
const parts = path.split("/");
|
|
5406
|
+
const stem = parts.at(-1) ?? "";
|
|
5407
|
+
if (route !== undefined && stem !== "_layout")
|
|
5408
|
+
return routeFileName(route);
|
|
5409
|
+
const label = stem === "index" || stem === "_layout" ? parts.at(-2) ?? stem : stem;
|
|
5410
|
+
return nameWords(label);
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
// packages/present/dist/src/places.js
|
|
5414
|
+
var PLACE_KIND_WORDS = {
|
|
5415
|
+
screen: "Screen",
|
|
5416
|
+
launch: "Launch screen",
|
|
5417
|
+
tap: "Tap",
|
|
5418
|
+
scroll: "Scroll",
|
|
5419
|
+
step: "Test step",
|
|
5420
|
+
journey: "Whole journey",
|
|
5421
|
+
background: "Background",
|
|
5422
|
+
after3s: "3 s after"
|
|
5423
|
+
};
|
|
5424
|
+
var KIND_WORD = {
|
|
5425
|
+
journey: PLACE_KIND_WORDS.journey,
|
|
5426
|
+
screen: PLACE_KIND_WORDS.screen,
|
|
5427
|
+
launch: PLACE_KIND_WORDS.launch,
|
|
5428
|
+
press: PLACE_KIND_WORDS.tap,
|
|
5429
|
+
drag: PLACE_KIND_WORDS.scroll,
|
|
5430
|
+
step: PLACE_KIND_WORDS.step,
|
|
5431
|
+
background: PLACE_KIND_WORDS.background
|
|
5432
|
+
};
|
|
5433
|
+
var BACKGROUND_SOURCES = {
|
|
5434
|
+
websocket: "data pushed over a WebSocket",
|
|
5435
|
+
launch: "work started at app launch",
|
|
5436
|
+
link: "work of a deep link outside its screen",
|
|
5437
|
+
touch: "work of a tap outside its window",
|
|
5438
|
+
input: "work of typing outside its window"
|
|
5439
|
+
};
|
|
5440
|
+
var OPEN_WORDS = { first: "first time", repeat: "again" };
|
|
5441
|
+
var OPEN_MARK = /#(1|repeat)$/;
|
|
5442
|
+
var isInputKind = (kind) => kind === "press" || kind === "drag";
|
|
5443
|
+
function placeOpen(id) {
|
|
5444
|
+
const ref = placeRef(id);
|
|
5445
|
+
if (!isInputKind(ref.kind))
|
|
5446
|
+
return null;
|
|
5447
|
+
const m = OPEN_MARK.exec(ref.stem);
|
|
5448
|
+
return m ? m[1] === "repeat" ? "repeat" : "first" : null;
|
|
5449
|
+
}
|
|
5450
|
+
var placeOpenWord = (id) => {
|
|
5451
|
+
const open = placeOpen(id);
|
|
5452
|
+
return open ? OPEN_WORDS[open] : null;
|
|
5453
|
+
};
|
|
5454
|
+
function placeIdShownName(id, o = {}) {
|
|
5455
|
+
const p = placeParts(id);
|
|
5456
|
+
if (!p)
|
|
5457
|
+
return null;
|
|
5458
|
+
const component = (part) => ({ name: part.name, file: part.file ?? "" });
|
|
5459
|
+
const screen = p.screen.name === "(no screen)" ? "" : p.screen.name === "(outside app components)" ? screenName(p.screen.file ?? "") : componentShownName(p.screen.name, p.screen.file ?? "");
|
|
5460
|
+
if (p.kind === "screen" || p.kind === "launch")
|
|
5461
|
+
return screen + (p.kind === "screen" && typeof p.visit === "number" && p.visit > 1 ? ` (${ordinal(p.visit)} visit)` : "");
|
|
5462
|
+
return placeShownName({ kind: p.kind, target: p.outsideHost === null ? component(p.target) : null, parent: p.parent ? component(p.parent) : null, k: p.k, ...p.outsideHost === null ? {} : { host: p.outsideHost } }, { onScreen: o.onScreen === false ? undefined : screen });
|
|
5463
|
+
}
|
|
5464
|
+
var isLibraryPlace = (id) => placeParts(id)?.outsideHost != null;
|
|
5465
|
+
function stemName(stem, kind) {
|
|
5466
|
+
const shown = placeIdShownName(stem);
|
|
5467
|
+
if (shown !== null)
|
|
5468
|
+
return isLibraryPlace(stem) ? shown : `${KIND_WORD[kind]}: ${shown}`;
|
|
5469
|
+
if (kind === "journey")
|
|
5470
|
+
return PLACE_KIND_WORDS.journey;
|
|
5471
|
+
if (kind === "other")
|
|
5472
|
+
return words(stem);
|
|
5473
|
+
const rest = stem.slice(stem.indexOf(":") + 1).replace(kind === "press" || kind === "drag" ? OPEN_MARK : /$^/, "");
|
|
5474
|
+
if (kind === "background")
|
|
5475
|
+
return `${KIND_WORD.background}: ${BACKGROUND_SOURCES[rest] ?? words(rest)}`;
|
|
5476
|
+
if (kind === "launch")
|
|
5477
|
+
return `${KIND_WORD.launch}: ${screenName(rest)}`;
|
|
5478
|
+
if (kind !== "screen")
|
|
5479
|
+
return `${KIND_WORD[kind]}: ${words(rest)}`;
|
|
5480
|
+
const visitMark = /^(.*)#(\d+)$/.exec(rest);
|
|
5481
|
+
const visit = visitMark ? Number(visitMark[2]) : 1;
|
|
5482
|
+
return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
|
|
5483
|
+
}
|
|
5484
|
+
function windowName(id) {
|
|
5485
|
+
if (id === "flow")
|
|
5486
|
+
return PLACE_KIND_WORDS.journey;
|
|
5487
|
+
const ref = placeRef(id);
|
|
5488
|
+
const name = stemName(ref.stem, ref.kind);
|
|
5489
|
+
return ref.segment === "after3s" ? `${name} · ${PLACE_KIND_WORDS.after3s}` : name;
|
|
5490
|
+
}
|
|
5491
|
+
function placeKindWord(id) {
|
|
5492
|
+
if (id === "flow")
|
|
5493
|
+
return PLACE_KIND_WORDS.journey;
|
|
5494
|
+
const { kind } = placeRef(id);
|
|
5495
|
+
return kind === "other" ? "Window" : KIND_WORD[kind];
|
|
5496
|
+
}
|
|
5497
|
+
function windowQualified(id) {
|
|
5498
|
+
const word = placeOpenWord(id);
|
|
5499
|
+
const label = placeLabel(id);
|
|
5500
|
+
if (!word)
|
|
5501
|
+
return label;
|
|
5502
|
+
const after = placeRef(id).segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5503
|
+
return `${after ? label.slice(0, -after.length) : label} (${word})${after}`;
|
|
5504
|
+
}
|
|
5505
|
+
var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
|
|
5506
|
+
var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
|
|
5507
|
+
function placeLabel(id, screen) {
|
|
5508
|
+
if (id === "flow")
|
|
5509
|
+
return PLACE_KIND_WORDS.journey;
|
|
5510
|
+
const ref = placeRef(id);
|
|
5511
|
+
const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
|
|
5512
|
+
const shown = placeIdShownName(ref.stem);
|
|
5513
|
+
if (shown !== null)
|
|
5514
|
+
return isLibraryPlace(ref.stem) ? `${shown}${after}` : `${KIND_WORD[ref.kind]} · ${shown}${after}`;
|
|
5515
|
+
if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
|
|
5516
|
+
const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
|
|
5517
|
+
return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`;
|
|
5518
|
+
}
|
|
5519
|
+
const name = windowName(ref.stem);
|
|
5520
|
+
const colon = name.indexOf(": ");
|
|
5521
|
+
return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
|
|
5522
|
+
}
|
|
5523
|
+
// packages/present/dist/src/placeNames.js
|
|
5524
|
+
function shortestPaths(files) {
|
|
5525
|
+
const out = new Map;
|
|
5526
|
+
for (const file of files) {
|
|
5527
|
+
const parts = file.split("/");
|
|
5528
|
+
let shown = file;
|
|
5529
|
+
for (let k = 1;k <= parts.length; k++) {
|
|
5530
|
+
const tail = parts.slice(-k).join("/");
|
|
5531
|
+
if (!files.some((f) => f !== file && f.split("/").slice(-k).join("/") === tail)) {
|
|
5532
|
+
shown = tail;
|
|
5533
|
+
break;
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
out.set(file, shown);
|
|
5537
|
+
}
|
|
5538
|
+
return out;
|
|
5539
|
+
}
|
|
5123
5540
|
// packages/present/dist/src/stability.js
|
|
5124
5541
|
var UNSTABLE_WORDS = {
|
|
5125
5542
|
short: "main varies too much here",
|
|
5126
5543
|
label: `${STATE_WORDS["cant-tell"]} · main varies too much here`,
|
|
5127
5544
|
line: "Main varies too much here to tell",
|
|
5128
5545
|
ratioTip: "1.0 = main’s run-to-run noise alone would pass the warning step about once in 100 comparisons.",
|
|
5546
|
+
note: "This place varies a lot between runs. Worth checking why: it may be in the code.",
|
|
5129
5547
|
what: "Main’s own runs vary too much here to tell, so a comparison there would mostly measure noise. It is shown with its values, never judged, never blocks and is never counted. It is judged again once main settles."
|
|
5130
5548
|
};
|
|
5549
|
+
|
|
5550
|
+
// packages/present/dist/src/present.js
|
|
5551
|
+
var ASPECT_WORDS = {
|
|
5552
|
+
renderer: "React renderer",
|
|
5553
|
+
reactExport: "React export",
|
|
5554
|
+
causeSplit: "work split by its cause",
|
|
5555
|
+
windowModel: "window model",
|
|
5556
|
+
screenWindowModel: "screen window model",
|
|
5557
|
+
cpuSource: "CPU source",
|
|
5558
|
+
sdk: "SDK version",
|
|
5559
|
+
driver: "driver",
|
|
5560
|
+
driverVersion: "driver version",
|
|
5561
|
+
journeyVersion: "journey version",
|
|
5562
|
+
actionGc: "memory clean-up before each step",
|
|
5563
|
+
endGc: "memory clean-up at the journey end",
|
|
5564
|
+
cpuProfiling: "CPU profiling"
|
|
5565
|
+
};
|
|
5566
|
+
var measuredDifferentlyText = (aspects) => `measured differently (${aspects.map((a) => ASPECT_WORDS[a] ?? a).join(", ")})`;
|
|
5567
|
+
|
|
5568
|
+
// packages/present/dist/src/readWords.js
|
|
5569
|
+
function componentReadName(c) {
|
|
5570
|
+
const ref = (c.key ? parseComponentKey(c.key) : null) ?? parseComponentKey(c.name);
|
|
5571
|
+
return componentShownName(ref?.name ?? c.name, c.file ?? ref?.file ?? "");
|
|
5572
|
+
}
|
|
5573
|
+
function componentReadNames(cs) {
|
|
5574
|
+
const names = cs.map(componentReadName);
|
|
5575
|
+
const rawNames = cs.map((c) => (c.key ? parseComponentKey(c.key)?.name : undefined) ?? parseComponentKey(c.name)?.name ?? c.name);
|
|
5576
|
+
return cs.map((c, i) => {
|
|
5577
|
+
const equals = cs.filter((_, k) => rawNames[k] === rawNames[i]);
|
|
5578
|
+
const file = c.file ?? (c.key ? parseComponentKey(c.key)?.file : undefined);
|
|
5579
|
+
const files = equals.flatMap((x) => x.file ?? (x.key ? parseComponentKey(x.key)?.file ?? [] : []));
|
|
5580
|
+
return new Set(files).size > 1 && file ? `${names[i]} · ${shortestPaths(files).get(file)}` : names[i];
|
|
5581
|
+
});
|
|
5582
|
+
}
|
|
5583
|
+
function storedRefWords(ref, peers = []) {
|
|
5584
|
+
if (!ref.startsWith("b:component:"))
|
|
5585
|
+
return ref;
|
|
5586
|
+
const cut = ref.lastIndexOf(":");
|
|
5587
|
+
const key = ref.slice("b:component:".length, cut);
|
|
5588
|
+
const components = [ref, ...peers].filter((r) => r.startsWith("b:component:")).map((r) => {
|
|
5589
|
+
const componentKey = r.slice("b:component:".length, r.lastIndexOf(":"));
|
|
5590
|
+
return { key: componentKey, name: componentKey };
|
|
5591
|
+
});
|
|
5592
|
+
const name = componentReadNames(components)[0] ?? componentReadName({ key, name: key });
|
|
5593
|
+
return `${name}, ${measureWords(ref.slice(cut + 1))}`;
|
|
5594
|
+
}
|
|
5595
|
+
function resultContextLines(c, verbose = false, short = false) {
|
|
5596
|
+
if (!c)
|
|
5597
|
+
return [];
|
|
5598
|
+
const out = [];
|
|
5599
|
+
if (c.namesChanged)
|
|
5600
|
+
out.push(restartRowText(c.namesChanged.sdk, c.namesChanged.mainSessions, c.namesChanged.needed));
|
|
5601
|
+
for (const w of c.windows) {
|
|
5602
|
+
const shownPlace = `${windowQualified(w.place)}${verbose ? ` (${w.place})` : ""}`;
|
|
5603
|
+
if (w.slowestCommit)
|
|
5604
|
+
out.push(`${shownPlace}: ${MEASURES.commit_max_ms.label}: ${valueText("commit_max_ms", w.slowestCommit.main)} → ${valueText("commit_max_ms", w.slowestCommit.pr)}`);
|
|
5605
|
+
if (!w.cause)
|
|
5606
|
+
continue;
|
|
5607
|
+
const s = w.cause.subject;
|
|
5608
|
+
const subject = s.kind === "component" ? `${componentShownName(s.ref.name, s.ref.file)}${verbose ? ` (${s.ref.file})` : ""}` : placeShownName({ kind: placeParts(s.place)?.kind === "drag" ? "drag" : "press", target: s.target, parent: s.parent, k: s.k, ...s.host ? { host: s.host } : {} });
|
|
5609
|
+
out.push(`${shownPlace}: ${w.information || !w.tier ? informationLeadText(subject, w.cause.measure, w.cause.main, w.cause.pr) : screenLeadText(w.tier, subject, w.cause.measure, w.cause.main, w.cause.pr)}${w.cause.measuredDifferently?.length ? `; ${measuredDifferentlyText(w.cause.measuredDifferently)}` : ""}`);
|
|
5610
|
+
}
|
|
5611
|
+
const screens = new Map;
|
|
5612
|
+
for (const p of c.notInThisRun) {
|
|
5613
|
+
const parts = placeParts(p.place);
|
|
5614
|
+
const key = parts ? componentShownName(parts.screen.name, parts.screen.file ?? "") : "";
|
|
5615
|
+
screens.set(key, [...screens.get(key) ?? [], p]);
|
|
5616
|
+
}
|
|
5617
|
+
for (const [screen, rows] of screens) {
|
|
5618
|
+
const line = notInRunRowsText({ screens: rows.filter((p) => p.kind === "screen").length, places: rows.filter((p) => p.kind !== "screen").length }, c.page);
|
|
5619
|
+
out.push(`${screen ? `${screen}: ` : ""}${line.lead}${line.rest}`);
|
|
5620
|
+
if (rows.length > 3)
|
|
5621
|
+
out.push(` ${foldedText(rows.length, "places", c.page)}`);
|
|
5622
|
+
if (!short || rows.length <= 3)
|
|
5623
|
+
out.push(...rows.map((p) => ` ${windowQualified(p.place)}`));
|
|
5624
|
+
}
|
|
5625
|
+
const removed = new Map;
|
|
5626
|
+
for (const r of c.removed)
|
|
5627
|
+
removed.set(r.place, [...removed.get(r.place) ?? [], r]);
|
|
5628
|
+
for (const [place, rows] of removed) {
|
|
5629
|
+
const line = removedRowsText(rows.length, c.page);
|
|
5630
|
+
out.push(`${windowQualified(place)}: ${line.lead}${line.rest}`);
|
|
5631
|
+
if (rows.length > 3)
|
|
5632
|
+
out.push(` ${foldedText(rows.length, "components", c.page)}`);
|
|
5633
|
+
if (!short || rows.length <= 3)
|
|
5634
|
+
out.push(...rows.map((r) => ` ${r.ref.kind === "breakdown" ? componentReadName({ key: r.ref.key, name: r.ref.name }) : measureWords(r.ref.measure)}`));
|
|
5635
|
+
}
|
|
5636
|
+
return out;
|
|
5637
|
+
}
|
|
5638
|
+
function laneContextLines(context, journey, phone, verbose = false, short = false) {
|
|
5639
|
+
const lines = resultContextLines(context, verbose, short);
|
|
5640
|
+
return lines.length ? [`${journey} on ${phone}:`, ...lines.map((line) => ` ${line}`)] : [];
|
|
5641
|
+
}
|
|
5642
|
+
var exactComponentMs = (ms) => `${ms} ms`;
|
|
5643
|
+
var componentPageText = (page, count) => `Component rows: ${count ? `${page.offset + 1}–${page.offset + count}` : "0"} of ${page.total} stored${page.next_offset !== null ? `; next --page ${Math.floor(page.next_offset / page.limit) + 1} (MCP component_offset ${page.next_offset})` : ""}.`;
|
|
5644
|
+
var omissionText = (o) => `${o.run_id}: SDK omitted ${o.sdkOmitted ?? "unknown"}; Derivation left out ${o.omitted ?? "unknown"}${o.selfMs === null ? "" : ` · ${exactComponentMs(o.selfMs)}`}. Paging leaves recorded omissions unchanged.`;
|
|
5645
|
+
var READ_WORDS = {
|
|
5646
|
+
componentTime: "component render time",
|
|
5647
|
+
ownTime: "own time",
|
|
5648
|
+
detailUnavailable: "Only render time recorded; renders, parent and library detail unavailable.",
|
|
5649
|
+
parent: "Placed by",
|
|
5650
|
+
slowest: MEASURES.commit_max_ms.label,
|
|
5651
|
+
runA: "Run A",
|
|
5652
|
+
runB: "Run B",
|
|
5653
|
+
notMeasured: "Not measured",
|
|
5654
|
+
onlyA: "Only in run A",
|
|
5655
|
+
onlyB: "Only in run B"
|
|
5656
|
+
};
|
|
5657
|
+
var READ_USAGE = {
|
|
5658
|
+
listFindings: `What got ${STATE_WORDS.worse.toLowerCase()} (${STATE_WORDS.worse} / ${STATE_WORDS.warning}) or ${STATE_WORDS.better.toLowerCase()} in the app, worst first, as KeepSnappy measured it on real phones. The answer starts with whether and when the branch was measured and how its newest run set read: an empty list on a branch that was never measured, or whose newest run set was not judged, is not a clean result. Filter by a PR branch or main, journey, phone, and state (present by default; not-judged = the newest run set did not judge it, so it may still be there). Each finding says what changed, where, how sure, and has an id for get_finding. Page with offset.`,
|
|
5659
|
+
components: " --page N | --all (places or compare: a component page, or every stored component and place; --top-components sets the bounded page size)",
|
|
5660
|
+
verbose: " --verbose (include source files and raw identities in text; JSON is unchanged)",
|
|
5661
|
+
page: "--page must be a whole number from 1; use it only with places or compare.",
|
|
5662
|
+
all: "--all is for places or compare; do not combine it with --page, --offset or --short.",
|
|
5663
|
+
detailFlag: " --detail (places: include recorded run detail; also included by --verbose or component paging)",
|
|
5664
|
+
detail: "Include recorded per-run component and window detail.",
|
|
5665
|
+
compareTop: "--top-components must be a whole number from 1 to 200 for compare.",
|
|
5666
|
+
top: "--top-components must be a whole number from 0 to 20, or from 1 to 200 with --page or --all.",
|
|
5667
|
+
paging: "The API did not advance the component page; stopped without claiming the list is complete.",
|
|
5668
|
+
componentOffset: "Skip this many stored components per place.",
|
|
5669
|
+
componentLimit: "Component page size; the response gives stored totals and next offsets."
|
|
5670
|
+
};
|
|
5671
|
+
var componentListsText = (totals, shown) => `Stored component rows: ${shown.rows} of ${totals.rows} compared; ${shown.added} of ${totals.added} ${READ_WORDS.onlyB.toLowerCase()}; ${shown.removed} of ${totals.removed} ${READ_WORDS.onlyA.toLowerCase()}.`;
|
|
5672
|
+
var notMeasuredText = (side) => `${READ_WORDS.notMeasured} in run ${side.toUpperCase()}`;
|
|
5673
|
+
|
|
5674
|
+
// packages/present/dist/src/words.js
|
|
5675
|
+
var REASON_WORDS = {
|
|
5676
|
+
"no-baseline": "there are no main runs on this phone and journey yet",
|
|
5677
|
+
"too-few-runs": "there are too few valid runs on a side",
|
|
5678
|
+
"judged-measures-missing": "the judged measures are missing on a side",
|
|
5679
|
+
"measured-differently": "the sides were measured differently",
|
|
5680
|
+
"runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
|
|
5681
|
+
"facts-version-mismatch": "the sides were derived with different versions",
|
|
5682
|
+
"not-judged-on-simulators": "timing is not judged on simulators",
|
|
5683
|
+
"platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
|
|
5684
|
+
"not-enough-main": "main does not have enough sessions on this phone and journey yet"
|
|
5685
|
+
};
|
|
5686
|
+
var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
|
|
5687
|
+
// packages/present/dist/src/main-history.js
|
|
5688
|
+
var MAIN_TIER_WORDS = {
|
|
5689
|
+
same: "In norm",
|
|
5690
|
+
better: STATE_WORDS.better,
|
|
5691
|
+
cant: STATE_WORDS["cant-tell"],
|
|
5692
|
+
slight: STATE_WORDS.warning,
|
|
5693
|
+
worse: STATE_WORDS.worse
|
|
5694
|
+
};
|
|
5695
|
+
var MAIN_TIER_RESULT_WORDS = {
|
|
5696
|
+
same: STATE_WORDS["no-change"],
|
|
5697
|
+
better: STATE_WORDS.better,
|
|
5698
|
+
cant: STATE_WORDS["cant-tell"],
|
|
5699
|
+
slight: STATE_WORDS.warning,
|
|
5700
|
+
worse: STATE_WORDS.worse
|
|
5701
|
+
};
|
|
5702
|
+
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
5703
|
+
function mainCantWords(reason, config) {
|
|
5704
|
+
if (!reason)
|
|
5705
|
+
return "";
|
|
5706
|
+
return reason.kind === "noisy_main" ? "main’s runs vary too much there" : `main has ${reason.earlier_sessions}${config.baseline.sessions === undefined ? "" : ` of ${config.baseline.sessions}`} earlier sessions${reason.since_label ? ` on ${reason.since_label}` : ""}`;
|
|
5707
|
+
}
|
|
5708
|
+
var waitingSessionsWords = (sessions) => `${sessions} older main ${sessions === 1 ? "session waits" : "sessions wait"} for a reprocess`;
|
|
5709
|
+
function mainPlaceReasonWords(reason, config) {
|
|
5710
|
+
switch (reason.kind) {
|
|
5711
|
+
case "no_baseline":
|
|
5712
|
+
case "noisy_main":
|
|
5713
|
+
return mainCantWords(reason, config);
|
|
5714
|
+
case "higher":
|
|
5715
|
+
return "Higher than main’s earlier sessions";
|
|
5716
|
+
case "lower":
|
|
5717
|
+
return "Lower than main’s earlier sessions";
|
|
5718
|
+
case "missing_value":
|
|
5719
|
+
return REASON_WORDS["runs-unknown"];
|
|
5720
|
+
case "measured_differently":
|
|
5721
|
+
return REASON_WORDS["measured-differently"];
|
|
5722
|
+
case "core":
|
|
5723
|
+
return reason.codes.map((code) => REASON_WORDS[code]).join("; ");
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
var MAIN_SUMMARY_PENDING = "Runs received; the summary is not computed yet.";
|
|
5727
|
+
var SHORT_ASPECT_WORDS = {
|
|
5728
|
+
...ASPECT_WORDS,
|
|
5729
|
+
sdk: "SDK",
|
|
5730
|
+
renderer: "Renderer",
|
|
5731
|
+
endGc: "End GC",
|
|
5732
|
+
causeSplit: "Cause split",
|
|
5733
|
+
driver: "Driver",
|
|
5734
|
+
driverVersion: "Driver",
|
|
5735
|
+
journeyVersion: "Journey"
|
|
5736
|
+
};
|
|
5737
|
+
function mainSessionCantWords(places, reason, config) {
|
|
5738
|
+
return `${STATE_WORDS["cant-tell"]} at ${places} place${places === 1 ? "" : "s"}: ${mainCantWords(reason, config)}`;
|
|
5739
|
+
}
|
|
5740
|
+
var KIND_PLURAL = {
|
|
5741
|
+
screen: "screens",
|
|
5742
|
+
press: "taps",
|
|
5743
|
+
drag: "scrolls",
|
|
5744
|
+
launch: "launch screens",
|
|
5745
|
+
step: "test steps",
|
|
5746
|
+
background: "background work",
|
|
5747
|
+
other: "other places"
|
|
5748
|
+
};
|
|
5749
|
+
var listWords = (kinds) => {
|
|
5750
|
+
const words = kinds.map((k) => KIND_PLURAL[k] ?? "other places");
|
|
5751
|
+
return words.length < 2 ? words.join("") : `${words.slice(0, -1).join(", ")} and ${words.at(-1)}`;
|
|
5752
|
+
};
|
|
5753
|
+
function mainCutWords(v) {
|
|
5754
|
+
const parts = [];
|
|
5755
|
+
if (v.cuts.length)
|
|
5756
|
+
parts.push(`Starts a new ${v.cuts.join(" and ")} baseline.`);
|
|
5757
|
+
if (v.partial_render) {
|
|
5758
|
+
const cut = listWords(v.partial_render.cut);
|
|
5759
|
+
parts.push(`${cut[0].toUpperCase()}${cut.slice(1)} start a new render baseline; ${listWords(v.partial_render.kept)} keep theirs.`);
|
|
5760
|
+
}
|
|
5761
|
+
return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
|
|
5762
|
+
}
|
|
5763
|
+
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
5131
5764
|
// apps/agent/src/summary.ts
|
|
5132
5765
|
var fieldWord = (field) => measureDef(field)?.short ?? field;
|
|
5133
5766
|
function plainMetric(metric) {
|
|
@@ -5209,14 +5842,26 @@ function movedCounts(sc) {
|
|
|
5209
5842
|
}
|
|
5210
5843
|
var placeCount = (rows) => new Set(rows.map((i) => `${i.scenario}|${i.device_id ?? ""}|${i.finding ?? placeRef(i.step ?? "*").stem}`)).size;
|
|
5211
5844
|
var addCounts = (xs) => xs.reduce((a, c) => ({ worse: a.worse + c.worse, warnings: a.warnings + c.warnings, better: a.better + c.better, noChange: a.noChange + c.noChange, inconclusive: a.inconclusive + c.inconclusive }), { worse: 0, warnings: 0, better: 0, noChange: 0, inconclusive: 0 });
|
|
5212
|
-
var setupLine = (h) =>
|
|
5845
|
+
var setupLine = (h) => {
|
|
5846
|
+
const parts = [
|
|
5847
|
+
h?.measured_differently?.length ? `Measured differently (${h.measured_differently.join(", ")}) — ${NOT_BLOCKING}` : "",
|
|
5848
|
+
h?.waiting_for_reprocess ? waitingWords(h.waiting_for_reprocess) : ""
|
|
5849
|
+
].filter(Boolean);
|
|
5850
|
+
return parts.length ? parts.join("; ") : null;
|
|
5851
|
+
};
|
|
5213
5852
|
var keyHeader = (t) => ({ "x-keepsnappy-key": t.key });
|
|
5214
5853
|
async function createRunSet(t, body) {
|
|
5215
5854
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/run-sets", keyHeader(t), body);
|
|
5216
|
-
|
|
5855
|
+
if (typeof r.run_set_id !== "string" || !r.run_set_id)
|
|
5856
|
+
throw new UploadError("POST /v1/run-sets: response has no run_set_id (is the URL the KeepSnappy API?)");
|
|
5857
|
+
return { run_set_id: r.run_set_id, dashboard_url: String(r.dashboard_url ?? "") };
|
|
5217
5858
|
}
|
|
5218
5859
|
async function completeRunSet(t, id, verdicts) {
|
|
5219
|
-
|
|
5860
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}/complete`;
|
|
5861
|
+
const r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), { verdicts });
|
|
5862
|
+
if (!r.run_set_id)
|
|
5863
|
+
throw new UploadError(`POST ${path}: response has no run_set_id`);
|
|
5864
|
+
return r;
|
|
5220
5865
|
}
|
|
5221
5866
|
async function createIngestRun(t, body) {
|
|
5222
5867
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/runs", keyHeader(t), body);
|
|
@@ -5231,13 +5876,20 @@ async function finishIngestRun(t, runId, invalidReason, farmSession) {
|
|
|
5231
5876
|
});
|
|
5232
5877
|
}
|
|
5233
5878
|
async function getRunSetRuns(t, id) {
|
|
5234
|
-
|
|
5879
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}/runs`;
|
|
5880
|
+
const r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), undefined, "GET");
|
|
5881
|
+
if (!Array.isArray(r.runs))
|
|
5882
|
+
throw new UploadError(`GET ${path}: response has no runs`);
|
|
5883
|
+
return r;
|
|
5235
5884
|
}
|
|
5236
5885
|
var FEW_RUNS = "Few runs — result may be noisy";
|
|
5237
|
-
var word = (s, server,
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5886
|
+
var word = (s, server, blocking) => badgedWord(server || stateWord(s ?? "inconclusive"), blocking);
|
|
5887
|
+
var setWord = (v) => word(v?.state ?? null, v?.word, v?.blocking ?? v?.headline?.blocking);
|
|
5888
|
+
var scenarioWord = (sc) => word(sc.state, sc.word, sc.headline?.blocking);
|
|
5889
|
+
var notComputed = (s) => !s.verdicts || s.verdicts.state === null && !s.verdicts.blocking;
|
|
5890
|
+
var resultWord = (s) => setWord(s.verdicts);
|
|
5891
|
+
var UNCHECKED_GATE = "Verdict not computed yet (computed on demand): --fail-on-worse checked nothing";
|
|
5892
|
+
var uncheckedGate = (s) => notComputed(s) ? UNCHECKED_GATE : null;
|
|
5241
5893
|
var countsLine = (c) => countsWords(c);
|
|
5242
5894
|
var noCounts = (c) => !c.worse && !c.warnings && !c.better && !c.noChange && !c.inconclusive;
|
|
5243
5895
|
var change = (metric, a, b) => {
|
|
@@ -5250,7 +5902,7 @@ function formatRunSet(s, top = 5) {
|
|
|
5250
5902
|
const lines = [];
|
|
5251
5903
|
const where = `${s.branch}${s.commit_sha ? ` @ ${s.commit_sha.slice(0, 7)}` : ""}`;
|
|
5252
5904
|
const v = s.verdicts;
|
|
5253
|
-
lines.push(`KeepSnappy result for ${where}: ${
|
|
5905
|
+
lines.push(`KeepSnappy result for ${where}: ${setWord(v)}`);
|
|
5254
5906
|
if (setupLine(v?.headline))
|
|
5255
5907
|
lines.push(` ${setupLine(v?.headline)}`);
|
|
5256
5908
|
if (v?.low_data)
|
|
@@ -5264,13 +5916,14 @@ function formatRunSet(s, top = 5) {
|
|
|
5264
5916
|
const dev = sc.device ? `${sc.device.model}${sc.device.os ? ` (${sc.device.os})` : ""}` : "no device";
|
|
5265
5917
|
const what = sc.role === "comparison" ? "compared with the baseline branch" : sc.role === "history" ? "latest runs compared with earlier ones" : "not judged (no branch or flow name)";
|
|
5266
5918
|
lines.push("");
|
|
5267
|
-
lines.push(`${sc.scenario ?? "(no flow name)"} on ${dev}, ${what}: ${
|
|
5919
|
+
lines.push(`${sc.scenario ?? "(no flow name)"} on ${dev}, ${what}: ${scenarioWord(sc)}`);
|
|
5268
5920
|
if (setupLine(sc.headline) && (v?.scenarios.length ?? 0) > 1)
|
|
5269
5921
|
lines.push(` ${setupLine(sc.headline)}`);
|
|
5270
5922
|
if (sc.low_data && (v?.scenarios.length ?? 0) > 1)
|
|
5271
5923
|
lines.push(` ${FEW_RUNS}`);
|
|
5272
5924
|
if (sc.role !== "adhoc" && !noCounts(movedCounts(sc)))
|
|
5273
5925
|
lines.push(` ${countsLine(movedCounts(sc))}`);
|
|
5926
|
+
lines.push(...resultContextLines(sc.context).map((line) => ` ${line}`));
|
|
5274
5927
|
if (heatNote(sc))
|
|
5275
5928
|
lines.push(` ${heatNote(sc)}`);
|
|
5276
5929
|
const allMoved = movedItems(v, sc).sort((a, b) => Number(b.state === "worse") - Number(a.state === "worse") || Number(a.grade === "warn") - Number(b.grade === "warn"));
|
|
@@ -5302,12 +5955,13 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
5302
5955
|
}
|
|
5303
5956
|
const v = s.verdicts;
|
|
5304
5957
|
const heated = v.scenarios.flatMap((sc) => heatNote(sc) ?? []);
|
|
5958
|
+
const contextLines = v.scenarios.flatMap((sc) => laneContextLines(sc.context, sc.scenario ?? "-", sc.device?.model ?? "-"));
|
|
5305
5959
|
const rows = v.scenarios.map((sc) => {
|
|
5306
5960
|
const c = movedCounts(sc);
|
|
5307
|
-
return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${
|
|
5961
|
+
return `| ${sc.scenario ?? "-"} | ${sc.device ? sc.device.model : "-"} | ${scenarioWord(sc)}${sc.low_data ? ` (${FEW_RUNS})` : ""} | ${noCounts(c) ? "–" : `${c.worse} / ${c.warnings} / ${c.better} / ${c.noChange} / ${c.inconclusive}`} | ${sc.dashboard_url ? `[open](${sc.dashboard_url})` : "-"} |`;
|
|
5308
5962
|
});
|
|
5309
5963
|
return [
|
|
5310
|
-
`### KeepSnappy: ${
|
|
5964
|
+
`### KeepSnappy: ${setWord(v)}`,
|
|
5311
5965
|
"",
|
|
5312
5966
|
...setupLine(v.headline) ? [`${setupLine(v.headline)}.`, ""] : [],
|
|
5313
5967
|
`\`${s.branch}\`${s.commit_sha ? ` @ \`${s.commit_sha.slice(0, 7)}\`` : ""}, ${s.runs.valid}/${s.runs.total} runs used.${v.low_data ? ` ${FEW_RUNS}.` : ""}`,
|
|
@@ -5315,6 +5969,7 @@ Runs uploaded for \`${fallback.branch}\`. The result appears on the dashboard: [
|
|
|
5315
5969
|
`| Journey | Device | Result | ${STATE_WORDS.worse} / ${STATE_WORDS.warning} / ${STATE_WORDS.better} / ${STATE_WORDS["no-change"]} / ${STATE_WORDS["cant-tell"]} | Details |`,
|
|
5316
5970
|
"|---|---|---|---|---|",
|
|
5317
5971
|
...rows,
|
|
5972
|
+
...contextLines.length ? ["", ...contextLines] : [],
|
|
5318
5973
|
...heated.length ? ["", ...heated.map((note) => `${note}.`)] : [],
|
|
5319
5974
|
"",
|
|
5320
5975
|
`[Open the dashboard](${s.dashboard_url})`,
|
|
@@ -8827,15 +9482,18 @@ ${r.stderr}` : ""}`);
|
|
|
8827
9482
|
}
|
|
8828
9483
|
|
|
8829
9484
|
// apps/agent/src/run.ts
|
|
8830
|
-
|
|
9485
|
+
var matchesMetric = (prefix, metric) => metric === prefix || metric.startsWith(`${prefix}.`);
|
|
9486
|
+
function renameMetrics(rows, rename, warn = console.warn) {
|
|
9487
|
+
for (const key of Object.keys(rename)) {
|
|
9488
|
+
if (!rows.some((r) => [key, `step:${key}`].some((prefix) => matchesMetric(prefix, r.metric))))
|
|
9489
|
+
warn(`steps.rename: key "${key}" matches no metric.`);
|
|
9490
|
+
}
|
|
8831
9491
|
const pairs = Object.entries(rename).flatMap(([a, b]) => [[a, b], [`step:${a}`, `step:${b}`]]);
|
|
8832
9492
|
if (pairs.length === 0)
|
|
8833
9493
|
return [...rows];
|
|
8834
9494
|
return rows.map((r) => {
|
|
8835
9495
|
for (const [from, to] of pairs) {
|
|
8836
|
-
if (r.metric
|
|
8837
|
-
return { ...r, metric: to };
|
|
8838
|
-
if (r.metric.startsWith(`${from}.`))
|
|
9496
|
+
if (matchesMetric(from, r.metric))
|
|
8839
9497
|
return { ...r, metric: `${to}${r.metric.slice(from.length)}` };
|
|
8840
9498
|
}
|
|
8841
9499
|
return r;
|
|
@@ -8853,7 +9511,7 @@ async function keepsnappyRun(deps, source, opts) {
|
|
|
8853
9511
|
...opts.markers ? { markers: opts.markers } : {},
|
|
8854
9512
|
...opts.clockOffsetMs === undefined ? {} : { clockOffsetMs: opts.clockOffsetMs }
|
|
8855
9513
|
});
|
|
8856
|
-
const rows = renameMetrics(raw, opts.config.steps.rename);
|
|
9514
|
+
const rows = renameMetrics(raw, opts.config.steps.rename, deps.log);
|
|
8857
9515
|
await deps.fs.writeFile(join8(opts.outDir, "metrics.csv"), toCsv(rows));
|
|
8858
9516
|
const summary = summarize({ runs, outDir: opts.outDir });
|
|
8859
9517
|
return { runs, rows, summary };
|
|
@@ -9147,6 +9805,14 @@ ${outcome.summary}`);
|
|
|
9147
9805
|
...api.fetch ? { fetch: api.fetch } : {}
|
|
9148
9806
|
}).catch(infra(`results are saved in ${out}, but the upload failed`));
|
|
9149
9807
|
rt.print(`uploaded ${up.uploaded} runs to ${api.url}${up.skipped.length ? ` (${up.skipped.length} skipped: ${up.skipped.map((x) => `${x.run_id} ${x.why}`).join(", ")})` : ""}`);
|
|
9808
|
+
if (up.uploaded === 0) {
|
|
9809
|
+
if (runSet)
|
|
9810
|
+
await completeRunSet(api, runSet.run_set_id, "on-read").catch(infra("no run was uploaded, and the run set could not be closed"));
|
|
9811
|
+
rt.print("Blocked: no run sent any data. Check that the build carries the SDK and that the app reaches the API.");
|
|
9812
|
+
if (runSet)
|
|
9813
|
+
await writeResult(deps, out, rt, runSet, null, git.branch, true);
|
|
9814
|
+
return EXIT.infra;
|
|
9815
|
+
}
|
|
9150
9816
|
if (!runSet)
|
|
9151
9817
|
return EXIT.ok;
|
|
9152
9818
|
let summary = null;
|
|
@@ -9163,6 +9829,9 @@ ${formatRunSet(summary)}`);
|
|
|
9163
9829
|
rt.print(`--fail-on-worse: a journey is ${STATE_WORDS.worse.toLowerCase()} than its baseline and blocks (exit 3)`);
|
|
9164
9830
|
return EXIT.worse;
|
|
9165
9831
|
}
|
|
9832
|
+
const unchecked = o["fail-on-worse"] && summary ? uncheckedGate(summary) : null;
|
|
9833
|
+
if (unchecked)
|
|
9834
|
+
rt.print(unchecked);
|
|
9166
9835
|
return EXIT.ok;
|
|
9167
9836
|
}
|
|
9168
9837
|
async function replayFlow(config, scenario) {
|
|
@@ -9179,13 +9848,16 @@ var infra = (what) => (e) => {
|
|
|
9179
9848
|
throw new Error(`${what}: ${e.message}`);
|
|
9180
9849
|
throw e;
|
|
9181
9850
|
};
|
|
9182
|
-
async function writeResult(deps, out, rt, runSet, summary, branch) {
|
|
9851
|
+
async function writeResult(deps, out, rt, runSet, summary, branch, blocked = false) {
|
|
9183
9852
|
const url = summary?.dashboard_url ?? runSet.dashboard_url;
|
|
9184
|
-
await deps.fs.writeFile(join9(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: runSet.run_set_id, dashboard_url: url, state: summary?.verdicts?.state ?? null, summary }, null, 2)}
|
|
9853
|
+
await deps.fs.writeFile(join9(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: runSet.run_set_id, dashboard_url: url, state: blocked ? "blocked" : summary?.verdicts?.state ?? null, summary }, null, 2)}
|
|
9185
9854
|
`);
|
|
9186
9855
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
9187
9856
|
if (step)
|
|
9188
|
-
await deps.fs.appendFile(step,
|
|
9857
|
+
await deps.fs.appendFile(step, blocked ? `### KeepSnappy: Blocked
|
|
9858
|
+
|
|
9859
|
+
No run sent any data. [Open the dashboard](${url})
|
|
9860
|
+
` : markdownRunSet(summary, { dashboardUrl: url, branch }));
|
|
9189
9861
|
}
|
|
9190
9862
|
|
|
9191
9863
|
// apps/agent/src/doctor.ts
|
|
@@ -9272,10 +9944,20 @@ var nodeSpawner = {
|
|
|
9272
9944
|
let stdout = "";
|
|
9273
9945
|
let stderr = "";
|
|
9274
9946
|
let error;
|
|
9947
|
+
let timeout;
|
|
9948
|
+
let killTimeout;
|
|
9949
|
+
let closed = false;
|
|
9275
9950
|
const child = spawn(cmd, [...args], {
|
|
9276
|
-
env: { ...process.env, ...opts?.env }
|
|
9277
|
-
...opts?.timeoutMs ? { timeout: opts.timeoutMs } : {}
|
|
9951
|
+
env: { ...process.env, ...opts?.env }
|
|
9278
9952
|
});
|
|
9953
|
+
if (opts?.timeoutMs) {
|
|
9954
|
+
timeout = setTimeout(() => {
|
|
9955
|
+
error = `timed out after ${opts.timeoutMs}ms`;
|
|
9956
|
+
child.kill("SIGTERM");
|
|
9957
|
+
if (!closed)
|
|
9958
|
+
killTimeout = setTimeout(() => child.kill("SIGKILL"), opts.killAfterMs ?? 120000);
|
|
9959
|
+
}, opts.timeoutMs);
|
|
9960
|
+
}
|
|
9279
9961
|
child.stdout.on("data", (d) => {
|
|
9280
9962
|
stdout += d.toString();
|
|
9281
9963
|
opts?.onOutput?.(d.toString());
|
|
@@ -9286,8 +9968,11 @@ var nodeSpawner = {
|
|
|
9286
9968
|
});
|
|
9287
9969
|
child.on("error", (e) => error = e.message);
|
|
9288
9970
|
child.on("close", (code, signal) => {
|
|
9289
|
-
|
|
9290
|
-
|
|
9971
|
+
closed = true;
|
|
9972
|
+
clearTimeout(timeout);
|
|
9973
|
+
clearTimeout(killTimeout);
|
|
9974
|
+
if (signal)
|
|
9975
|
+
error = error ? `${error}; killed by ${signal}` : `killed by ${signal}`;
|
|
9291
9976
|
resolve({ code, stdout, stderr, ...error ? { error } : {} });
|
|
9292
9977
|
});
|
|
9293
9978
|
})
|
|
@@ -9330,7 +10015,7 @@ async function aa(deps, outDir, opts = {}) {
|
|
|
9330
10015
|
}
|
|
9331
10016
|
|
|
9332
10017
|
// apps/agent/src/farm.ts
|
|
9333
|
-
import { dirname as dirname3, join as
|
|
10018
|
+
import { dirname as dirname3, join as join13, resolve as resolve5 } from "node:path";
|
|
9334
10019
|
|
|
9335
10020
|
// packages/appium-driver/dist/src/links.js
|
|
9336
10021
|
var ALERT_POLL_MS = 250;
|
|
@@ -9628,18 +10313,13 @@ class WdError extends Error {
|
|
|
9628
10313
|
this.wdError = wdError;
|
|
9629
10314
|
}
|
|
9630
10315
|
}
|
|
9631
|
-
// packages/appium-driver/dist/src/session.js
|
|
9632
|
-
var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
9633
|
-
var FLUSH_MARGIN_MS = 30000;
|
|
9634
|
-
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
9635
|
-
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
9636
10316
|
// packages/appium-driver/dist/src/version.js
|
|
9637
10317
|
import { createHash } from "node:crypto";
|
|
9638
10318
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9639
10319
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
9640
10320
|
import { fileURLToPath } from "node:url";
|
|
9641
10321
|
function driverVersion() {
|
|
9642
|
-
const built = "0.1.0-alpha.
|
|
10322
|
+
const built = "0.1.0-alpha.17";
|
|
9643
10323
|
if (built)
|
|
9644
10324
|
return built;
|
|
9645
10325
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -9688,8 +10368,91 @@ function journeyVersion(modulePath) {
|
|
|
9688
10368
|
hash.update(key).update("\x00").update(text).update("\x00");
|
|
9689
10369
|
return hash.digest("hex").slice(0, 12);
|
|
9690
10370
|
}
|
|
10371
|
+
|
|
10372
|
+
// packages/appium-driver/dist/src/session.js
|
|
10373
|
+
var SDK_FLUSH_WORST_CASE_MS = 315000;
|
|
10374
|
+
var FLUSH_MARGIN_MS = 30000;
|
|
10375
|
+
var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
|
|
10376
|
+
var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
|
|
10377
|
+
// apps/agent/src/device-model.ts
|
|
10378
|
+
import { tmpdir } from "node:os";
|
|
10379
|
+
import { join as join12 } from "node:path";
|
|
10380
|
+
function parseDevicectlList(json) {
|
|
10381
|
+
let data;
|
|
10382
|
+
try {
|
|
10383
|
+
data = JSON.parse(json);
|
|
10384
|
+
} catch {
|
|
10385
|
+
return [];
|
|
10386
|
+
}
|
|
10387
|
+
const devices = Array.isArray(data.result?.devices) ? data.result.devices : [];
|
|
10388
|
+
return devices.flatMap((d) => {
|
|
10389
|
+
const hw = d.hardwareProperties ?? {};
|
|
10390
|
+
const conn = d.connectionProperties ?? {};
|
|
10391
|
+
if (typeof hw.productType !== "string" || !hw.productType)
|
|
10392
|
+
return [];
|
|
10393
|
+
const text = (v) => typeof v === "string" ? v : "";
|
|
10394
|
+
return [{ productType: hw.productType, udid: text(hw.udid), identifier: text(d.identifier), serial: text(hw.serialNumber), connected: conn.tunnelState === "connected" }];
|
|
10395
|
+
});
|
|
10396
|
+
}
|
|
10397
|
+
var parseSimulators = (json) => {
|
|
10398
|
+
try {
|
|
10399
|
+
const data = JSON.parse(json);
|
|
10400
|
+
return Object.values(data.devices ?? {}).flat();
|
|
10401
|
+
} catch {
|
|
10402
|
+
return [];
|
|
10403
|
+
}
|
|
10404
|
+
};
|
|
10405
|
+
var withSimulatorWord = (name) => /simulator/i.test(name) ? name : `${name} Simulator`;
|
|
10406
|
+
var simulatorModel = (s) => withSimulatorWord(s.name);
|
|
10407
|
+
async function androidModel(deps, id) {
|
|
10408
|
+
const r = await deps.spawner.run("adb", [...id ? ["-s", id] : [], "shell", "getprop", "ro.product.model"]);
|
|
10409
|
+
const model = r.code === 0 ? (r.stdout.split(`
|
|
10410
|
+
`)[0] ?? "").trim() : "";
|
|
10411
|
+
return model || null;
|
|
10412
|
+
}
|
|
10413
|
+
async function simulators(deps, id) {
|
|
10414
|
+
const r = await deps.spawner.run("xcrun", ["simctl", "list", "devices", ...id ? [] : ["booted"], "-j"]);
|
|
10415
|
+
if (r.code !== 0)
|
|
10416
|
+
return [];
|
|
10417
|
+
return parseSimulators(r.stdout).filter((s) => id ? s.udid === id : s.state === "Booted");
|
|
10418
|
+
}
|
|
10419
|
+
async function phones(deps) {
|
|
10420
|
+
const out = join12(tmpdir(), `keepsnappy-devicectl-${deps.clock.now()}.json`);
|
|
10421
|
+
try {
|
|
10422
|
+
const r = await deps.spawner.run("xcrun", ["devicectl", "list", "devices", "--quiet", "--json-output", out]);
|
|
10423
|
+
const text = r.code === 0 ? await deps.fs.readFile(out).catch(() => "") : "";
|
|
10424
|
+
return parseDevicectlList(text);
|
|
10425
|
+
} finally {
|
|
10426
|
+
await deps.spawner.run("rm", ["-f", out]);
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10429
|
+
async function iosModel(deps, id) {
|
|
10430
|
+
const sims = await simulators(deps, id);
|
|
10431
|
+
if (id) {
|
|
10432
|
+
if (sims.length === 1)
|
|
10433
|
+
return simulatorModel(sims[0]);
|
|
10434
|
+
const found = (await phones(deps)).filter((p) => [p.udid, p.identifier, p.serial].some((x) => x.toLowerCase() === id.toLowerCase()));
|
|
10435
|
+
return found.length === 1 ? found[0].productType : null;
|
|
10436
|
+
}
|
|
10437
|
+
const connected = (await phones(deps)).filter((p) => p.connected);
|
|
10438
|
+
if (sims.length + connected.length !== 1)
|
|
10439
|
+
return null;
|
|
10440
|
+
return sims[0] ? simulatorModel(sims[0]) : connected[0].productType;
|
|
10441
|
+
}
|
|
10442
|
+
async function detectDeviceModel(deps, q) {
|
|
10443
|
+
if (q.platform === "android")
|
|
10444
|
+
return androidModel(deps, q.deviceId);
|
|
10445
|
+
if (q.platform === "ios")
|
|
10446
|
+
return iosModel(deps, q.deviceId);
|
|
10447
|
+
return null;
|
|
10448
|
+
}
|
|
10449
|
+
async function detectSimulator(deps, udid) {
|
|
10450
|
+
const sims = await simulators(deps, udid);
|
|
10451
|
+
return sims.length === 1 ? simulatorModel(sims[0]) : null;
|
|
10452
|
+
}
|
|
10453
|
+
|
|
9691
10454
|
// apps/agent/src/slack.ts
|
|
9692
|
-
var COLOR = { error: "#
|
|
10455
|
+
var COLOR = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
9693
10456
|
var MAX_ROWS = 3;
|
|
9694
10457
|
var NAME_W = 30;
|
|
9695
10458
|
var COLUMN_GAP = 1;
|
|
@@ -9719,7 +10482,7 @@ var isBetter = (i) => i.state === "better";
|
|
|
9719
10482
|
var differs = (sc) => sc?.headline?.measured_differently ?? [];
|
|
9720
10483
|
var scenarioBlocks = (sc) => sc?.headline?.blocking !== false;
|
|
9721
10484
|
var isRed = (i, sc) => i.grade !== "warn" && scenarioBlocks(sc);
|
|
9722
|
-
var mark = (i, sc) => isBetter(i) ? "\uD83D\uDFE2" : isRed(i, sc) ? "\uD83D\uDD34" : "\uD83D\
|
|
10485
|
+
var mark = (i, sc) => isBetter(i) ? "\uD83D\uDFE2" : isRed(i, sc) ? "\uD83D\uDD34" : i.grade !== "warn" ? "⚪" : "\uD83D\uDFE1";
|
|
9723
10486
|
var fit = (s, w) => s.length > w ? `${s.slice(0, w - 1)}…` : s.padEnd(w);
|
|
9724
10487
|
var right = (s, w) => s.padStart(w);
|
|
9725
10488
|
var phone = (sc) => sc?.device ? `${sc.device.model}${sc.device.os ? ` · ${sc.device.os}` : ""}` : "unknown phone";
|
|
@@ -9780,26 +10543,29 @@ function slackMessage(s, ctx) {
|
|
|
9780
10543
|
const better = items.filter(isBetter).sort(bySize);
|
|
9781
10544
|
const commit = commitLine(ctx);
|
|
9782
10545
|
const heated = v.scenarios.filter((sc) => heatNote(sc));
|
|
10546
|
+
const contextLines = v.scenarios.flatMap((sc) => laneContextLines(sc.context, sc.scenario ?? "-", phone(sc))).map(context);
|
|
9783
10547
|
const heat = heated.map((sc) => context(heatNote(sc)));
|
|
9784
10548
|
const errorRows = worse.filter((i) => i.grade !== "warn");
|
|
9785
10549
|
const warningRows = worse.filter((i) => i.grade === "warn");
|
|
9786
10550
|
const errors = placeCount(errorRows);
|
|
9787
10551
|
const warns = placeCount(worse) - errors;
|
|
10552
|
+
const red = errorRows.some((i) => isRed(i, scenarioOf(i)));
|
|
10553
|
+
const notBlocking = errors > 0 && !red;
|
|
10554
|
+
const worseMark = red ? "\uD83D\uDD34" : warns > 0 ? "\uD83D\uDFE1" : "⚪";
|
|
10555
|
+
const worseColor = red ? COLOR.error : warns > 0 ? COLOR.warn : COLOR.none;
|
|
9788
10556
|
if (isPr) {
|
|
9789
10557
|
if (!worse.length) {
|
|
9790
10558
|
if (!heat.length)
|
|
9791
10559
|
return null;
|
|
9792
|
-
const head = `⚪ ${ctx.branch}: ${INFO_WORDS.heat}`;
|
|
9793
|
-
const blocks = [section(`*${head}*`), ...heat, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0]
|
|
10560
|
+
const head = `⚪ ${ctx.branch}: ${heat.length ? INFO_WORDS.heat : v.word ?? STATE_WORDS["no-change"]}`;
|
|
10561
|
+
const blocks = [section(`*${head}*`), ...heat, ...contextLines, ...commit ? [context(commit)] : [], section(`\uD83D\uDC49 <${heated[0]?.dashboard_url ?? s.dashboard_url}|View the comparison>`)];
|
|
9794
10562
|
return { text: head, attachments: [{ color: COLOR.none, blocks }] };
|
|
9795
10563
|
}
|
|
9796
|
-
const red = worse.some((i) => isRed(i, scenarioOf(i)));
|
|
9797
10564
|
const graded = worse.some((i) => i.grade);
|
|
9798
|
-
const notBlocking = errors > 0 && !red;
|
|
9799
10565
|
const places = (word, n) => `${word.toLowerCase()} in ${n} place${n > 1 ? "s" : ""}`;
|
|
9800
|
-
const counts = graded ? [errors ? `${places(STATE_WORDS.worse, errors)}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? places(STATE_WORDS.warning, warns) : ""].filter(Boolean).join(", ") : places(STATE_WORDS.worse, placeCount(worse))
|
|
9801
|
-
const head = `${
|
|
9802
|
-
const blocks = [section(`*${head}*`)];
|
|
10566
|
+
const counts = graded ? [errors ? `${places(STATE_WORDS.worse, errors)}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? places(STATE_WORDS.warning, warns) : ""].filter(Boolean).join(", ") : `${places(STATE_WORDS.worse, placeCount(worse))}${notBlocking ? ` (${NOT_BLOCKING})` : ""}`;
|
|
10567
|
+
const head = `${worseMark} ${ctx.branch}: ${counts}`;
|
|
10568
|
+
const blocks = [section(`*${head}*`), ...contextLines];
|
|
9803
10569
|
const groups = new Map;
|
|
9804
10570
|
for (const i of worse)
|
|
9805
10571
|
groups.set(scenarioOf(i), [...groups.get(scenarioOf(i)) ?? [], i]);
|
|
@@ -9814,7 +10580,8 @@ function slackMessage(s, ctx) {
|
|
|
9814
10580
|
const runs = nB && nC ? ` · ${nC} PR run${nC === 1 ? "" : "s"} vs ${nB} main run${nB === 1 ? "" : "s"}` : "";
|
|
9815
10581
|
const note = differs(sc).length ? `
|
|
9816
10582
|
_Measured differently (${differs(sc).join(", ")})${scenarioBlocks(sc) ? "" : ` — ${NOT_BLOCKING}`}_` : "";
|
|
9817
|
-
|
|
10583
|
+
const nonblocking = !scenarioBlocks(sc) && rows.some((i) => i.grade !== "warn") && !differs(sc).length ? ` · ${NOT_BLOCKING}` : "";
|
|
10584
|
+
blocks.push(section(`*${sc?.scenario ?? "journey"}*${nonblocking} · ${phone(sc)}${runs}${note}
|
|
9818
10585
|
${prTable(rows, sc)}`));
|
|
9819
10586
|
}
|
|
9820
10587
|
const more = worse.length - shown;
|
|
@@ -9822,18 +10589,18 @@ ${prTable(rows, sc)}`));
|
|
|
9822
10589
|
if (commit)
|
|
9823
10590
|
blocks.push(context(commit));
|
|
9824
10591
|
blocks.push(section(`\uD83D\uDC49 <${groups.keys().next().value?.dashboard_url ?? s.dashboard_url}|View the comparison>${more > 0 ? ` · ${more} more` : ""}`));
|
|
9825
|
-
return { text: head, attachments: [{ color:
|
|
10592
|
+
return { text: head, attachments: [{ color: worseColor, blocks }] };
|
|
9826
10593
|
}
|
|
9827
10594
|
const improved = better.length > 0 || v.state === "better";
|
|
9828
10595
|
if (!worse.length && !improved && !ctx.always && !heat.length)
|
|
9829
10596
|
return null;
|
|
9830
|
-
const parts = [errors ? `${errors} ${STATE_WORDS.worse.toLowerCase()}` : "", warns ? `${warns} ${STATE_WORDS.warning.toLowerCase()}` : "", better.length ? `${placeCount(better)} ${STATE_WORDS.better.toLowerCase()}` : ""].filter(Boolean);
|
|
10597
|
+
const parts = [errors ? `${errors} ${STATE_WORDS.worse.toLowerCase()}${notBlocking ? ` (${NOT_BLOCKING})` : ""}` : "", warns ? `${warns} ${STATE_WORDS.warning.toLowerCase()}` : "", better.length ? `${placeCount(better)} ${STATE_WORDS.better.toLowerCase()}` : ""].filter(Boolean);
|
|
9831
10598
|
const quiet = improved ? STATE_WORDS.better.toLowerCase() : (v.state === null || v.state === "inconclusive") && v.word ? v.word : STATE_WORDS["no-change"].toLowerCase();
|
|
9832
10599
|
const status = parts.length ? `${ctx.branch}: ${parts.join(", ")}` : !improved && heat.length && !ctx.always ? `${ctx.branch}: ${INFO_WORDS.heat}` : quiet.startsWith("Main: ") && ctx.branch === "main" ? quiet : `${ctx.branch}: ${quiet}`;
|
|
9833
|
-
const head = `${worse.length ?
|
|
10600
|
+
const head = `${worse.length ? worseMark : improved ? "\uD83D\uDFE2" : v.state === "inconclusive" && (ctx.always || !heat.length) ? "❔" : "⚪"} ${status}`;
|
|
9834
10601
|
const journeys = new Set(v.scenarios.map((sc) => `${sc.scenario}|${sc.device?.id}`)).size;
|
|
9835
10602
|
const blocks = [section(`*${head}*
|
|
9836
|
-
${journeys} journey${journeys === 1 ? "" : "s"} checked`)];
|
|
10603
|
+
${journeys} journey${journeys === 1 ? "" : "s"} checked`), ...contextLines];
|
|
9837
10604
|
let shown = 0;
|
|
9838
10605
|
for (const [title, list] of [[STATE_WORDS.worse, errorRows], [STATE_WORDS.warning, warningRows], [STATE_WORDS.better, better]]) {
|
|
9839
10606
|
for (const i of list) {
|
|
@@ -9841,7 +10608,7 @@ ${journeys} journey${journeys === 1 ? "" : "s"} checked`)];
|
|
|
9841
10608
|
break;
|
|
9842
10609
|
shown++;
|
|
9843
10610
|
const sc = scenarioOf(i);
|
|
9844
|
-
blocks.push(section(`*${title}
|
|
10611
|
+
blocks.push(section(`*${title}*${i.grade !== "warn" && isWorse(i) && !scenarioBlocks(sc) ? ` (${NOT_BLOCKING})` : ""} · *${sc?.scenario ?? "journey"}* · ${phone(sc)}
|
|
9845
10612
|
${mainCard(i)}`));
|
|
9846
10613
|
}
|
|
9847
10614
|
}
|
|
@@ -9850,7 +10617,7 @@ ${mainCard(i)}`));
|
|
|
9850
10617
|
if (commit)
|
|
9851
10618
|
blocks.push(context(commit));
|
|
9852
10619
|
blocks.push(section(`\uD83D\uDC49 <${s.dashboard_url}|Open KeepSnappy>${more > 0 ? ` · ${more} more changes` : ""}`));
|
|
9853
|
-
const color = worse.length ?
|
|
10620
|
+
const color = worse.length ? worseColor : improved ? COLOR.better : COLOR.none;
|
|
9854
10621
|
return { text: head, attachments: [{ color, blocks }] };
|
|
9855
10622
|
}
|
|
9856
10623
|
async function postToSlack(url, payload, fetchImpl = fetch) {
|
|
@@ -9869,17 +10636,23 @@ var LINKS_USAGE = `keepsnappy activation-link --scheme <app://> --token <run tok
|
|
|
9869
10636
|
flush-link --scheme <app://>
|
|
9870
10637
|
gc-link --scheme <app://> [--n 1]`;
|
|
9871
10638
|
function linksCommand(cmd, argv, rt) {
|
|
9872
|
-
const { values: o } =
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
10639
|
+
const { values: o } = (() => {
|
|
10640
|
+
try {
|
|
10641
|
+
return parseArgs({
|
|
10642
|
+
args: [...argv],
|
|
10643
|
+
options: {
|
|
10644
|
+
scheme: { type: "string" },
|
|
10645
|
+
token: { type: "string" },
|
|
10646
|
+
n: { type: "string" },
|
|
10647
|
+
"no-diag": { type: "boolean" },
|
|
10648
|
+
"no-profile": { type: "boolean" },
|
|
10649
|
+
set: { type: "string", multiple: true }
|
|
10650
|
+
}
|
|
10651
|
+
});
|
|
10652
|
+
} catch (e) {
|
|
10653
|
+
throw new UsageError([e.message, LINKS_USAGE]);
|
|
9881
10654
|
}
|
|
9882
|
-
});
|
|
10655
|
+
})();
|
|
9883
10656
|
if (!o.scheme)
|
|
9884
10657
|
throw new ConfigError([`--scheme is required (the app's URL scheme, e.g. myapp://)`]);
|
|
9885
10658
|
if (cmd === "flush-link") {
|
|
@@ -9997,9 +10770,20 @@ async function farmOpen(deps, f, rt) {
|
|
|
9997
10770
|
const git = gitContext({ ...f.branch ? { branch: f.branch } : {}, ...f.commit ? { commit: f.commit } : {} }, rt.env);
|
|
9998
10771
|
if (!git.branch)
|
|
9999
10772
|
problems.push("--branch is required outside GitHub Actions");
|
|
10000
|
-
|
|
10773
|
+
let provider = f["device-provider"] ?? "browserstack";
|
|
10774
|
+
let model = f["device-model"]?.trim();
|
|
10775
|
+
const simulator = f.platform === "ios" && provider !== "browserstack" && f["device-id"]?.trim() ? await detectSimulator(deps, f["device-id"].trim()) : null;
|
|
10776
|
+
if (simulator) {
|
|
10777
|
+
provider = "simulator";
|
|
10778
|
+
model = model ? withSimulatorWord(model) : simulator;
|
|
10779
|
+
}
|
|
10780
|
+
if (!model && ["local", "usb", "customer", "simulator", "emulator"].includes(provider)) {
|
|
10781
|
+
model = await detectDeviceModel(deps, { platform: f.platform, deviceId: f["device-id"]?.trim() }) ?? undefined;
|
|
10782
|
+
if (model)
|
|
10783
|
+
rt.print(`device model read from the device: ${model}`);
|
|
10784
|
+
}
|
|
10001
10785
|
if (!model)
|
|
10002
|
-
problems.push('--device-model is required: the journey history is keyed by the run set’s device (e.g. "
|
|
10786
|
+
problems.push('--device-model is required unless it can be read from a local device (--device-provider local|usb|customer|simulator|emulator, with --platform android|ios, adb or xcrun, and --device-id when more than one device is attached): the journey history is keyed by the run set’s device (e.g. "SM-S911B", "iPhone17,1")');
|
|
10003
10787
|
if (f.platform !== undefined && !["ios", "android", "web"].includes(f.platform))
|
|
10004
10788
|
problems.push("--platform must be ios, android or web");
|
|
10005
10789
|
const journeys = (f.journeys ?? "").split(",").map((j) => j.trim()).filter(Boolean);
|
|
@@ -10029,7 +10813,7 @@ async function farmOpen(deps, f, rt) {
|
|
|
10029
10813
|
}
|
|
10030
10814
|
const api = apiTarget(url, f, rt);
|
|
10031
10815
|
const now = rt.now ?? Date.now;
|
|
10032
|
-
const device = { model, os: f["device-os"] ?? null, provider
|
|
10816
|
+
const device = { model, os: f["device-os"] ?? null, provider };
|
|
10033
10817
|
const journeyInfo = await farmJourneys(deps, f, rt, journeys);
|
|
10034
10818
|
const withInstrument = { ...f, ...instrumentFlags(f) };
|
|
10035
10819
|
const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
|
|
@@ -10287,7 +11071,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
|
|
|
10287
11071
|
await deps.fs.writeFile(statePath, `${JSON.stringify(state2, null, 2)}
|
|
10288
11072
|
`);
|
|
10289
11073
|
await deps.fs.mkdir(out);
|
|
10290
|
-
await deps.fs.writeFile(
|
|
11074
|
+
await deps.fs.writeFile(join13(out, "keepsnappy-result.json"), `${JSON.stringify({ run_set_id: state.run_set_id, dashboard_url: summary.dashboard_url, state: state2.result.state, session: { status: session.status, message: session.message }, summary }, null, 2)}
|
|
10291
11075
|
`);
|
|
10292
11076
|
await githubOutputs(deps, rt, { state: state2.result.state ?? "none", dashboard_url: summary.dashboard_url, run_set_id: state.run_set_id });
|
|
10293
11077
|
const step = rt.env.GITHUB_STEP_SUMMARY;
|
|
@@ -10314,9 +11098,9 @@ ${tail.join(" ")}
|
|
|
10314
11098
|
...blocked ? { blocked: { reason: `No iteration sent any data${session.known ? ` (device session ${session.status})` : ""}.` } } : {},
|
|
10315
11099
|
always: envValue(rt.env, "SLACK_ALWAYS") === "1"
|
|
10316
11100
|
});
|
|
10317
|
-
if (!msg)
|
|
10318
|
-
rt.print(f.wait ?
|
|
10319
|
-
else {
|
|
11101
|
+
if (!msg) {
|
|
11102
|
+
rt.print(!f.wait ? "Slack: skipped, the message needs --wait" : notComputed(summary) ? "Slack: nothing to report, the verdict is not computed yet" : `Slack: nothing to report (${resultWord(summary).toLowerCase()})`);
|
|
11103
|
+
} else {
|
|
10320
11104
|
const r = await postToSlack(hook, msg, rt.fetch ?? fetch);
|
|
10321
11105
|
rt.print(r.ok ? "Slack: posted" : `Slack: not posted (${r.error}); the job is not affected`);
|
|
10322
11106
|
}
|
|
@@ -10327,17 +11111,20 @@ ${tail.join(" ")}
|
|
|
10327
11111
|
rt.print(`--fail-on-worse: a journey is ${STATE_WORDS.worse.toLowerCase()} than its baseline and blocks (exit 3)`);
|
|
10328
11112
|
return EXIT.worse;
|
|
10329
11113
|
}
|
|
11114
|
+
const unchecked = f["fail-on-worse"] ? uncheckedGate(summary) : null;
|
|
11115
|
+
if (unchecked)
|
|
11116
|
+
rt.print(unchecked);
|
|
10330
11117
|
return EXIT.ok;
|
|
10331
11118
|
}
|
|
10332
11119
|
|
|
10333
11120
|
// apps/agent/src/farm-run.ts
|
|
10334
11121
|
import { existsSync as existsSync2 } from "node:fs";
|
|
10335
11122
|
import { createRequire } from "node:module";
|
|
10336
|
-
import { dirname as dirname4, join as
|
|
11123
|
+
import { dirname as dirname4, join as join16, resolve as resolve6 } from "node:path";
|
|
10337
11124
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10338
11125
|
|
|
10339
11126
|
// apps/agent/src/app-source.ts
|
|
10340
|
-
import { join as
|
|
11127
|
+
import { join as join14 } from "node:path";
|
|
10341
11128
|
var EAS_CLI_VERSION = "24.6.0";
|
|
10342
11129
|
var isEas = (spec) => spec.startsWith("eas:");
|
|
10343
11130
|
|
|
@@ -10349,7 +11136,7 @@ class DefaultAppResolver {
|
|
|
10349
11136
|
this.env = env;
|
|
10350
11137
|
}
|
|
10351
11138
|
async resolve(spec, o) {
|
|
10352
|
-
if (spec.startsWith("bs://"))
|
|
11139
|
+
if (spec.startsWith("bs://") || spec.startsWith("custom_id:"))
|
|
10353
11140
|
return { app: spec, commit: null };
|
|
10354
11141
|
if (!isEas(spec)) {
|
|
10355
11142
|
if (!await this.deps.fs.exists(spec))
|
|
@@ -10373,7 +11160,7 @@ class DefaultAppResolver {
|
|
|
10373
11160
|
const url = build.artifacts?.buildUrl ?? build.artifacts?.applicationArchiveUrl;
|
|
10374
11161
|
if (!url)
|
|
10375
11162
|
throw new Error(`EAS build ${id} has no downloadable artifact (is it finished?)`);
|
|
10376
|
-
const file =
|
|
11163
|
+
const file = join14(o.outDir, `app-${id.slice(0, 8)}${build.platform === "ANDROID" ? ".apk" : ".ipa"}`);
|
|
10377
11164
|
await this.deps.fs.mkdir(o.outDir);
|
|
10378
11165
|
const dl = await this.deps.spawner.run("curl", ["-fsSL", "-o", file, url]);
|
|
10379
11166
|
if (dl.code !== 0)
|
|
@@ -10384,7 +11171,7 @@ class DefaultAppResolver {
|
|
|
10384
11171
|
|
|
10385
11172
|
// apps/agent/src/browserstack.ts
|
|
10386
11173
|
import { randomUUID } from "node:crypto";
|
|
10387
|
-
import { basename as basename2, join as
|
|
11174
|
+
import { basename as basename2, join as join15 } from "node:path";
|
|
10388
11175
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
10389
11176
|
var BASE = "https://api-cloud.browserstack.com/app-automate/maestro/v2";
|
|
10390
11177
|
var TERMINAL = new Set(["passed", "failed", "error", "timedout", "skipped", "stopped"]);
|
|
@@ -10455,7 +11242,7 @@ class BrowserStack {
|
|
|
10455
11242
|
}
|
|
10456
11243
|
}
|
|
10457
11244
|
async uploadApp(app, customId) {
|
|
10458
|
-
if (app.startsWith("bs://"))
|
|
11245
|
+
if (app.startsWith("bs://") || app.startsWith("custom_id:"))
|
|
10459
11246
|
return app;
|
|
10460
11247
|
const form = new FormData;
|
|
10461
11248
|
form.append("file", new Blob([await this.bytes(app)]), basename2(app));
|
|
@@ -10468,7 +11255,7 @@ class BrowserStack {
|
|
|
10468
11255
|
}
|
|
10469
11256
|
async runMaestro(b) {
|
|
10470
11257
|
const appUrl = await this.uploadApp(b.app, b.customId);
|
|
10471
|
-
const work =
|
|
11258
|
+
const work = join15(this.o.tmpDir ?? "/tmp", `keepsnappy-suite-${(this.o.uniqueId ?? randomUUID)()}`);
|
|
10472
11259
|
let suiteUrl;
|
|
10473
11260
|
try {
|
|
10474
11261
|
const zip = await this.zipSuite(b, work);
|
|
@@ -10501,17 +11288,17 @@ class BrowserStack {
|
|
|
10501
11288
|
return this.poll(buildId, b);
|
|
10502
11289
|
}
|
|
10503
11290
|
async zipSuite(b, work) {
|
|
10504
|
-
const zipPath =
|
|
10505
|
-
const stage =
|
|
11291
|
+
const zipPath = join15(work, "suite.zip");
|
|
11292
|
+
const stage = join15(work, "stage");
|
|
10506
11293
|
await this.deps.fs.mkdir(stage);
|
|
10507
11294
|
const cp = await this.deps.spawner.run("cp", ["-R", b.suiteDir.replace(/\/$/, ""), stage]);
|
|
10508
11295
|
if (cp.code !== 0)
|
|
10509
11296
|
throw new FarmApiError(`cannot copy the suite ${b.suiteDir}: ${cp.stderr || cp.error}`);
|
|
10510
11297
|
const folder = basename2(b.suiteDir.replace(/\/$/, ""));
|
|
10511
|
-
if (!await this.deps.fs.exists(
|
|
11298
|
+
if (!await this.deps.fs.exists(join15(stage, folder, "config.yaml")))
|
|
10512
11299
|
throw new FarmApiError(`${b.suiteDir} has no config.yaml: is that the Maestro workspace?`);
|
|
10513
|
-
const target =
|
|
10514
|
-
await this.deps.fs.mkdir(
|
|
11300
|
+
const target = join15(stage, folder, b.tokensPath);
|
|
11301
|
+
await this.deps.fs.mkdir(join15(target, ".."));
|
|
10515
11302
|
await this.deps.fs.writeFile(target, await this.deps.fs.readFile(b.tokensJs));
|
|
10516
11303
|
const include = SUITE_EXTENSIONS.flatMap((e) => ["-i", `${folder}/*${e}`, "-i", `${folder}/**/*${e}`]);
|
|
10517
11304
|
const z = await this.deps.spawner.run("sh", ["-c", 'cd "$1" && shift && zip -qr "$@"', "sh", stage, zipPath, folder, ...include, "-x", `${folder}/runs/*`, "-x", "*/node_modules/*"]);
|
|
@@ -10570,9 +11357,9 @@ class BrowserStackLocal {
|
|
|
10570
11357
|
const url = TUNNEL_ZIP[this.o.platform ?? process.platform];
|
|
10571
11358
|
if (!url)
|
|
10572
11359
|
throw new FarmApiError(`no BrowserStackLocal binary known for ${this.o.platform ?? process.platform}`);
|
|
10573
|
-
const dir =
|
|
10574
|
-
const bin =
|
|
10575
|
-
const zip =
|
|
11360
|
+
const dir = join15(this.o.dir, identifier);
|
|
11361
|
+
const bin = join15(dir, "BrowserStackLocal");
|
|
11362
|
+
const zip = join15(dir, "bs-local.zip");
|
|
10576
11363
|
await this.deps.fs.mkdir(dir);
|
|
10577
11364
|
for (const [cmd, args] of [
|
|
10578
11365
|
["curl", ["-fsSL", "-o", zip, url]],
|
|
@@ -10606,12 +11393,12 @@ ${r.stderr}`;
|
|
|
10606
11393
|
// apps/agent/src/farm-run.ts
|
|
10607
11394
|
function appiumDriverEntry(o = {}) {
|
|
10608
11395
|
const exists = o.exists ?? existsSync2;
|
|
10609
|
-
const bundled =
|
|
11396
|
+
const bundled = join16(o.selfDir ?? dirname4(fileURLToPath2(import.meta.url)), "appium-driver.js");
|
|
10610
11397
|
if (exists(bundled))
|
|
10611
11398
|
return bundled;
|
|
10612
11399
|
const pkgDir = dirname4(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
|
|
10613
|
-
const built =
|
|
10614
|
-
return exists(built) ? built :
|
|
11400
|
+
const built = join16(pkgDir, "dist", "src", "cli.js");
|
|
11401
|
+
return exists(built) ? built : join16(pkgDir, "src", "cli.ts");
|
|
10615
11402
|
}
|
|
10616
11403
|
var driverRuntime = (entry) => entry.endsWith(".ts") ? process.execPath : "node";
|
|
10617
11404
|
function nodeFarmRunPorts(deps, rt) {
|
|
@@ -10620,7 +11407,7 @@ function nodeFarmRunPorts(deps, rt) {
|
|
|
10620
11407
|
if (!user || !key)
|
|
10621
11408
|
throw new ConfigError(["BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY must be set"]);
|
|
10622
11409
|
const tmp = rt.env.RUNNER_TEMP ?? rt.env.TMPDIR ?? "/tmp";
|
|
10623
|
-
const dir =
|
|
11410
|
+
const dir = join16(tmp, "keepsnappy-bs-local");
|
|
10624
11411
|
return {
|
|
10625
11412
|
apps: new DefaultAppResolver(deps, rt.env),
|
|
10626
11413
|
provider: new BrowserStack(deps, { credentials: { user, key }, tmpDir: tmp, ...rt.fetch ? { fetch: rt.fetch } : {}, ...rt.sleep ? { sleep: rt.sleep } : {}, ...rt.now ? { now: rt.now } : {} }),
|
|
@@ -10636,7 +11423,7 @@ var RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
|
10636
11423
|
function planFarmRun(f) {
|
|
10637
11424
|
const problems = [];
|
|
10638
11425
|
if (!f.app)
|
|
10639
|
-
problems.push("--app <eas:<build id>|path to .ipa/.apk|bs://id> is required");
|
|
11426
|
+
problems.push("--app <eas:<build id>|path to .ipa/.apk|bs://id|custom_id:id> is required");
|
|
10640
11427
|
const platform = f.platform ?? "ios";
|
|
10641
11428
|
if (platform !== "ios" && platform !== "android")
|
|
10642
11429
|
problems.push("--platform must be ios or android");
|
|
@@ -10682,14 +11469,15 @@ function planFarmRun(f) {
|
|
|
10682
11469
|
problems.push("--driver-script and --journey-module are two ways to run Appium; pass one");
|
|
10683
11470
|
} else if (!f["driver-script"] && !f["journey-module"]) {
|
|
10684
11471
|
problems.push("--driver appium needs --journey-module <file> (the built-in driver) or --driver-script <file> (your own driver)");
|
|
10685
|
-
} else if (f["journey-module"] && platform !== "ios") {
|
|
10686
|
-
problems.push("--journey-module: the built-in Appium driver drives iOS only");
|
|
10687
11472
|
}
|
|
10688
11473
|
if (f["run-id"] !== undefined && !RUN_ID.test(f["run-id"]))
|
|
10689
11474
|
problems.push("--run-id: letters, digits, . _ - only (it names the tunnel and its folder)");
|
|
10690
11475
|
if (f["fail-on-worse"] && !f.wait)
|
|
10691
11476
|
problems.push("--fail-on-worse needs --wait");
|
|
10692
11477
|
const timeoutMinutes = Number(f["timeout-minutes"] ?? 80);
|
|
11478
|
+
const killAfterSeconds = Number(f["kill-after-seconds"] ?? 120);
|
|
11479
|
+
if (!Number.isFinite(killAfterSeconds) || killAfterSeconds <= 0)
|
|
11480
|
+
problems.push("--kill-after-seconds must be finite and > 0");
|
|
10693
11481
|
const pollSeconds = Number(f["poll-seconds"] ?? 20);
|
|
10694
11482
|
if (!(timeoutMinutes > 0))
|
|
10695
11483
|
problems.push("--timeout-minutes must be > 0");
|
|
@@ -10709,6 +11497,7 @@ function planFarmRun(f) {
|
|
|
10709
11497
|
driverArgs: f["driver-arg"] ?? [],
|
|
10710
11498
|
out: resolve6(f.out ?? "farm-run"),
|
|
10711
11499
|
timeoutMinutes,
|
|
11500
|
+
killAfterSeconds,
|
|
10712
11501
|
pollSeconds
|
|
10713
11502
|
};
|
|
10714
11503
|
}
|
|
@@ -10732,8 +11521,8 @@ function printPlan(p, f, rt) {
|
|
|
10732
11521
|
const line = (l) => rt.print(redact(l, secrets));
|
|
10733
11522
|
line("keepsnappy farm run: dry run, no request is made and nothing is written");
|
|
10734
11523
|
line(` 1. validate: ${p.journeys.join(", ")} x ${p.repeat} on ${p.device} (${p.platform}), driver ${p.driver}`);
|
|
10735
|
-
line(` 2. resolve the app: ${p.app.startsWith("eas:") ? `eas-cli build:view ${p.app.slice(4)}, download into ${p.out}` : p.app.startsWith("bs://")
|
|
10736
|
-
line(` 3. farm open -> ${
|
|
11524
|
+
line(` 2. resolve the app: ${p.app.startsWith("eas:") ? `eas-cli build:view ${p.app.slice(4)}, download into ${p.out}` : p.app.startsWith("bs://") || p.app.startsWith("custom_id:") ? `${p.app} passes through` : `${p.app} (local file)`}`);
|
|
11525
|
+
line(` 3. farm open -> ${join16(p.out, "keepsnappy-tokens.js")}, ${join16(p.out, "keepsnappy-farm.json")} (POST /v1/run-sets, then POST /v1/runs x ${p.journeys.length * p.repeat})`);
|
|
10737
11526
|
line(` 4. tunnel: ${p.ports.length ? `BrowserStackLocal for bs-local.com ports ${p.ports.join(", ")}, identifier = run id` : "none (no --tunnel-for)"}`);
|
|
10738
11527
|
if (p.driver === "maestro") {
|
|
10739
11528
|
line(` 5. maestro on BrowserStack: upload app, zip ${f["suite-dir"]} with the tokens at ${f["tokens-path"] ?? "flows/keepsnappy/tokens.js"}, upload suite, POST build (execute ${f.execute}), poll`);
|
|
@@ -10742,9 +11531,9 @@ function printPlan(p, f, rt) {
|
|
|
10742
11531
|
}
|
|
10743
11532
|
for (const [k, v] of p.env)
|
|
10744
11533
|
line(` env ${k}=${substitute(v, {})}`);
|
|
10745
|
-
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${
|
|
11534
|
+
line(` 6. farm close${f.wait ? " --wait" : ""}${f["fail-on-worse"] ? " --fail-on-worse" : ""} with ${join16(p.out, `browserstack.${p.platform}.json`)}`);
|
|
10746
11535
|
line(" 7. always: stop the tunnel, put the empty stub back over the tokens file");
|
|
10747
|
-
line(" exit: 0 any verdict, 1 infrastructure / driver failed / Blocked, 2 configuration, 3 --fail-on-worse");
|
|
11536
|
+
line(" exit: 0 any verdict, 1 infrastructure / driver failed / Blocked, 2 configuration, 3 --fail-on-worse (a computed blocking Worse only; no computed verdict: nothing checked)");
|
|
10748
11537
|
}
|
|
10749
11538
|
var ANNOTATION = /^::(error|warning|notice)( [^:]*)?::/;
|
|
10750
11539
|
async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
@@ -10766,13 +11555,13 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
10766
11555
|
timeoutMinutes: p.timeoutMinutes,
|
|
10767
11556
|
pollSeconds: p.pollSeconds,
|
|
10768
11557
|
onStarted: async (buildId) => {
|
|
10769
|
-
await deps.fs.writeFile(
|
|
11558
|
+
await deps.fs.writeFile(join16(ctx.out, `browserstack.${p.platform}.json`), `${JSON.stringify({ buildId, dashboard: `https://app-automate.browserstack.com/dashboard/v2/builds/${buildId}`, note: "written at submit; replaced by the full detail once the build ends", status: "unknown" }, null, 2)}
|
|
10770
11559
|
`);
|
|
10771
11560
|
rt.print(`build ${buildId} started: https://app-automate.browserstack.com/dashboard/v2/builds/${buildId}`);
|
|
10772
11561
|
},
|
|
10773
11562
|
onStatus: (buildId, status) => rt.print(`build ${buildId}: ${status}`)
|
|
10774
11563
|
});
|
|
10775
|
-
await deps.fs.writeFile(
|
|
11564
|
+
await deps.fs.writeFile(join16(ctx.out, `browserstack.${p.platform}.json`), `${redact(JSON.stringify(detail.detail, null, 2), secretsOf(p, f, rt.env))}
|
|
10776
11565
|
`);
|
|
10777
11566
|
return detail.status === "passed" ? { ok: true, message: null } : { ok: false, message: `the device build ${detail.buildId} ended ${detail.status}` };
|
|
10778
11567
|
}
|
|
@@ -10809,6 +11598,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
10809
11598
|
const r = await deps.spawner.run(f["driver-script"] ? "node" : driverRuntime(entry), args, {
|
|
10810
11599
|
env,
|
|
10811
11600
|
timeoutMs: p.timeoutMinutes * 60000,
|
|
11601
|
+
killAfterMs: p.killAfterSeconds * 1000,
|
|
10812
11602
|
onOutput: (chunk) => {
|
|
10813
11603
|
streamed = true;
|
|
10814
11604
|
const lines = (pending + chunk).split(`
|
|
@@ -10823,11 +11613,11 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
|
|
|
10823
11613
|
for (const l of `${r.stdout}${r.stderr}`.split(`
|
|
10824
11614
|
`))
|
|
10825
11615
|
emit(l);
|
|
10826
|
-
return r.code === 0 ? { ok: true, message: null } : { ok: false, message: `the driver script exited ${r.
|
|
11616
|
+
return r.code === 0 && !r.error ? { ok: true, message: null } : { ok: false, message: `the driver script exited ${r.error ?? r.code ?? "abnormally"}` };
|
|
10827
11617
|
}
|
|
10828
11618
|
async function farmRun(deps, f, rt, ports) {
|
|
10829
11619
|
const p = planFarmRun(f);
|
|
10830
|
-
const statePath =
|
|
11620
|
+
const statePath = join16(p.out, "keepsnappy-farm.json");
|
|
10831
11621
|
if (await deps.fs.exists(statePath))
|
|
10832
11622
|
throw new ConfigError([`${statePath} exists: a run set was opened here before; use another --out or remove ${p.out} (close it first with keepsnappy farm close)`]);
|
|
10833
11623
|
if (f["dry-run"]) {
|
|
@@ -10835,7 +11625,7 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10835
11625
|
return EXIT.ok;
|
|
10836
11626
|
}
|
|
10837
11627
|
const farm = ports ?? nodeFarmRunPorts(deps, rt);
|
|
10838
|
-
const tokensJs =
|
|
11628
|
+
const tokensJs = join16(p.out, "keepsnappy-tokens.js");
|
|
10839
11629
|
await deps.fs.mkdir(p.out);
|
|
10840
11630
|
const resolved = await farm.apps.resolve(p.app, { outDir: p.out });
|
|
10841
11631
|
const commit = f.commit ?? resolved.commit ?? undefined;
|
|
@@ -10866,9 +11656,9 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10866
11656
|
const runId = f["run-id"] ?? `keepsnappy-${p.platform}-${state.run_set_id.slice(0, 8)}`;
|
|
10867
11657
|
const ctx = { runId, out: p.out, tokensJs, runSetId: state.run_set_id, app: resolved.app };
|
|
10868
11658
|
try {
|
|
10869
|
-
await deps.spawner.run("rm", ["-f",
|
|
11659
|
+
await deps.spawner.run("rm", ["-f", join16(p.out, `browserstack.${p.platform}.json`)]);
|
|
10870
11660
|
if (p.ports.length) {
|
|
10871
|
-
tunnel = await farm.tunnel.open({ identifier: runId, ports: p.ports, logFile:
|
|
11661
|
+
tunnel = await farm.tunnel.open({ identifier: runId, ports: p.ports, logFile: join16(p.out, "browserstack-local.log") });
|
|
10872
11662
|
rt.print(`tunnel ${runId}: bs-local.com ports ${p.ports.join(", ")}`);
|
|
10873
11663
|
}
|
|
10874
11664
|
driver = await runDriver2(deps, rt, f, p, farm, { ...ctx, tunnelId: tunnel?.identifier ?? null });
|
|
@@ -10879,7 +11669,7 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10879
11669
|
rt.print(`device run failed: ${driver.message}; closing the run set with what arrived`);
|
|
10880
11670
|
closeCode = await farmClose(deps, {
|
|
10881
11671
|
state: statePath,
|
|
10882
|
-
"bs-build":
|
|
11672
|
+
"bs-build": join16(p.out, `browserstack.${p.platform}.json`),
|
|
10883
11673
|
out: p.out,
|
|
10884
11674
|
...f.wait ? { wait: true } : {},
|
|
10885
11675
|
...f["fail-on-worse"] ? { "fail-on-worse": true } : {},
|
|
@@ -10900,13 +11690,82 @@ async function farmRun(deps, f, rt, ports) {
|
|
|
10900
11690
|
}
|
|
10901
11691
|
|
|
10902
11692
|
// apps/agent/src/sourcemaps.ts
|
|
10903
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
10904
|
-
import { readFile as readFile4 } from "node:fs/promises";
|
|
10905
|
-
import { basename as basename3 } from "node:path";
|
|
11693
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
11694
|
+
import { access, readFile as readFile4, readdir as readdir2, rename, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
11695
|
+
import { basename as basename3, join as join17, resolve as resolve7 } from "node:path";
|
|
10906
11696
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
10907
11697
|
import { gzipSync } from "node:zlib";
|
|
10908
11698
|
var MAX_COMPLETE_ROUNDS = 20;
|
|
10909
11699
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11700
|
+
async function prepareComponents(projectRoot, options = {}) {
|
|
11701
|
+
const root = resolve7(projectRoot);
|
|
11702
|
+
const directory = join17(root, ".keepsnappy", "components");
|
|
11703
|
+
const configurations = await readdir2(directory, { withFileTypes: true }).catch((error) => {
|
|
11704
|
+
if (error.code === "ENOENT")
|
|
11705
|
+
return [];
|
|
11706
|
+
throw error;
|
|
11707
|
+
});
|
|
11708
|
+
const candidates = [];
|
|
11709
|
+
for (const entry of configurations) {
|
|
11710
|
+
if (!entry.isDirectory() || !/^[a-zA-Z0-9-]+$/.test(entry.name))
|
|
11711
|
+
continue;
|
|
11712
|
+
try {
|
|
11713
|
+
await access(join17(directory, entry.name, ".config.json"));
|
|
11714
|
+
candidates.push(entry.name);
|
|
11715
|
+
} catch (error) {
|
|
11716
|
+
if (error.code !== "ENOENT")
|
|
11717
|
+
throw error;
|
|
11718
|
+
}
|
|
11719
|
+
}
|
|
11720
|
+
candidates.sort();
|
|
11721
|
+
if (candidates.length === 0)
|
|
11722
|
+
return null;
|
|
11723
|
+
const selectedConfiguration = options.configuration ?? (candidates.length === 1 ? candidates[0] : undefined);
|
|
11724
|
+
if (!selectedConfiguration || !candidates.includes(selectedConfiguration)) {
|
|
11725
|
+
throw new ConfigError([`choose a component configuration with --components-config (${candidates.join(", ")})`]);
|
|
11726
|
+
}
|
|
11727
|
+
const selected = join17(directory, selectedConfiguration);
|
|
11728
|
+
const config = JSON.parse(await readFile4(join17(selected, ".config.json"), "utf8"));
|
|
11729
|
+
if (!isComponentConfiguration(config) || config.directory !== selectedConfiguration) {
|
|
11730
|
+
throw new ConfigError(["invalid KeepSnappy component fragment configuration"]);
|
|
11731
|
+
}
|
|
11732
|
+
const files = (await readdir2(selected)).filter((file) => file.endsWith(".json") && !file.startsWith(".")).sort();
|
|
11733
|
+
const components = [];
|
|
11734
|
+
const hashes = new Map;
|
|
11735
|
+
let matched = 0;
|
|
11736
|
+
for (const file of files) {
|
|
11737
|
+
const fragment = JSON.parse(await readFile4(join17(selected, file), "utf8"));
|
|
11738
|
+
if (!isComponentFragment(fragment))
|
|
11739
|
+
throw new ConfigError([`invalid KeepSnappy component fragment ${file}`]);
|
|
11740
|
+
if (!hashes.has(fragment.source)) {
|
|
11741
|
+
try {
|
|
11742
|
+
hashes.set(fragment.source, createHash2("sha256").update(await readFile4(resolve7(root, fragment.source))).digest("hex"));
|
|
11743
|
+
} catch (error) {
|
|
11744
|
+
if (error.code !== "ENOENT")
|
|
11745
|
+
throw error;
|
|
11746
|
+
hashes.set(fragment.source, null);
|
|
11747
|
+
}
|
|
11748
|
+
}
|
|
11749
|
+
if (hashes.get(fragment.source) !== fragment.hash)
|
|
11750
|
+
continue;
|
|
11751
|
+
matched++;
|
|
11752
|
+
components.push(...fragment.components);
|
|
11753
|
+
}
|
|
11754
|
+
if (files.length > 0 && matched === 0)
|
|
11755
|
+
options.warn?.("component fragments exist but none match the current sources");
|
|
11756
|
+
components.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : a.line - b.line);
|
|
11757
|
+
const file = join17(directory, "..", "components.json");
|
|
11758
|
+
const text = JSON.stringify(components);
|
|
11759
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11760
|
+
try {
|
|
11761
|
+
await writeFile2(temporary, text);
|
|
11762
|
+
await rename(temporary, file);
|
|
11763
|
+
} finally {
|
|
11764
|
+
await rm(temporary, { force: true });
|
|
11765
|
+
}
|
|
11766
|
+
const gzip = gzipSync(text);
|
|
11767
|
+
return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash2("sha256").update(gzip).digest("hex") };
|
|
11768
|
+
}
|
|
10910
11769
|
function stripSourcesContent(map) {
|
|
10911
11770
|
if (!isObject(map))
|
|
10912
11771
|
throw new ConfigError(["not a source map: the file is not a JSON object"]);
|
|
@@ -10982,21 +11841,35 @@ async function uploadSourcemaps(opts) {
|
|
|
10982
11841
|
}
|
|
10983
11842
|
return out;
|
|
10984
11843
|
}
|
|
10985
|
-
var SOURCEMAPS_USAGE =
|
|
11844
|
+
var SOURCEMAPS_USAGE = `keepsnappy sourcemaps upload --build <full commit sha|build id> --platform ios|android [--kind hermes|packager] [--flavour NAME] [--api URL] [--key-env KEEPSNAPPY_KEY] [--project-root PATH] [--components-config ID] <map files...>
|
|
11845
|
+
keepsnappy sourcemaps upload --eas (e2e EAS build: infer build, platform, map and kind; skips missing inputs and never fails the build)`;
|
|
10986
11846
|
async function sourcemapsCommand(args, rt) {
|
|
11847
|
+
try {
|
|
11848
|
+
return await uploadCommand(args, rt);
|
|
11849
|
+
} catch (error) {
|
|
11850
|
+
if (!args.includes("--eas"))
|
|
11851
|
+
throw error;
|
|
11852
|
+
rt.print("keepsnappy: source map upload failed (build not affected)");
|
|
11853
|
+
return 0;
|
|
11854
|
+
}
|
|
11855
|
+
}
|
|
11856
|
+
async function uploadCommand(args, rt) {
|
|
10987
11857
|
let parsed;
|
|
10988
11858
|
try {
|
|
10989
11859
|
parsed = parseArgs2({
|
|
10990
11860
|
args: [...args],
|
|
10991
11861
|
allowPositionals: true,
|
|
10992
11862
|
options: {
|
|
11863
|
+
eas: { type: "boolean" },
|
|
10993
11864
|
build: { type: "string" },
|
|
10994
11865
|
platform: { type: "string" },
|
|
10995
11866
|
kind: { type: "string" },
|
|
10996
11867
|
flavour: { type: "string" },
|
|
10997
11868
|
api: { type: "string" },
|
|
10998
11869
|
"ingest-url": { type: "string" },
|
|
10999
|
-
"key-env": { type: "string" }
|
|
11870
|
+
"key-env": { type: "string" },
|
|
11871
|
+
"project-root": { type: "string" },
|
|
11872
|
+
"components-config": { type: "string" }
|
|
11000
11873
|
}
|
|
11001
11874
|
});
|
|
11002
11875
|
} catch (e) {
|
|
@@ -11006,6 +11879,40 @@ async function sourcemapsCommand(args, rt) {
|
|
|
11006
11879
|
const [sub, ...files] = positionals;
|
|
11007
11880
|
if (sub !== "upload")
|
|
11008
11881
|
throw new ConfigError([SOURCEMAPS_USAGE]);
|
|
11882
|
+
const keyEnv = f["key-env"] ?? "KEEPSNAPPY_KEY";
|
|
11883
|
+
const key = projectKey(rt.env, keyEnv) || (f.eas && !f["key-env"] ? rt.env.KEEPSNAPPY_CI_KEY : undefined);
|
|
11884
|
+
const url = f.api ?? f["ingest-url"] ?? envValue(rt.env, "API") ?? (f.eas ? rt.env.EXPO_PUBLIC_KEEPSNAPPY_INGEST_URL ?? "" : DEFAULT_API_URL);
|
|
11885
|
+
let preparedFiles;
|
|
11886
|
+
if (f.eas) {
|
|
11887
|
+
const skip = (reason) => {
|
|
11888
|
+
rt.print(`keepsnappy: no source map upload (${reason})`);
|
|
11889
|
+
return 0;
|
|
11890
|
+
};
|
|
11891
|
+
if (rt.env.EAS_BUILD_PROFILE !== "e2e")
|
|
11892
|
+
return skip("profile is not e2e");
|
|
11893
|
+
f.platform ??= rt.env.EAS_BUILD_PLATFORM;
|
|
11894
|
+
if (f.platform !== "ios" && f.platform !== "android")
|
|
11895
|
+
return skip("platform must be ios or android");
|
|
11896
|
+
f.build ??= rt.env.EAS_BUILD_GIT_COMMIT_HASH;
|
|
11897
|
+
if (!key)
|
|
11898
|
+
return skip("key missing");
|
|
11899
|
+
if (!url)
|
|
11900
|
+
return skip("API URL missing");
|
|
11901
|
+
if (!f.build)
|
|
11902
|
+
return skip("commit missing");
|
|
11903
|
+
f.kind ??= f.platform === "ios" ? "packager" : "hermes";
|
|
11904
|
+
f.flavour ??= "e2e";
|
|
11905
|
+
if (files.length === 0)
|
|
11906
|
+
files.push(f.platform === "ios" ? "main.jsbundle.packager.map" : "android/app/build/generated/sourcemaps/react/release/index.android.bundle.map");
|
|
11907
|
+
preparedFiles = new Map;
|
|
11908
|
+
try {
|
|
11909
|
+
const read = rt.readFile ?? ((path) => readFile4(path, "utf8"));
|
|
11910
|
+
for (const file of files)
|
|
11911
|
+
preparedFiles.set(file, await read(file));
|
|
11912
|
+
} catch {
|
|
11913
|
+
return skip("map missing or unreadable");
|
|
11914
|
+
}
|
|
11915
|
+
}
|
|
11009
11916
|
const problems = [];
|
|
11010
11917
|
if (!f.build)
|
|
11011
11918
|
problems.push("--build <full commit sha (git rev-parse HEAD) or build id> is required");
|
|
@@ -11015,13 +11922,10 @@ async function sourcemapsCommand(args, rt) {
|
|
|
11015
11922
|
problems.push("--kind must be hermes or packager");
|
|
11016
11923
|
if (files.length === 0)
|
|
11017
11924
|
problems.push("give at least one source map file");
|
|
11018
|
-
const keyEnv = f["key-env"] ?? "KEEPSNAPPY_KEY";
|
|
11019
|
-
const key = projectKey(rt.env, keyEnv);
|
|
11020
11925
|
if (!key)
|
|
11021
11926
|
problems.push(`the project key (run scope) is missing: set the environment variable ${keyEnv}`);
|
|
11022
11927
|
if (problems.length)
|
|
11023
11928
|
throw new ConfigError(problems);
|
|
11024
|
-
const url = f.api ?? f["ingest-url"] ?? envValue(rt.env, "API") ?? DEFAULT_API_URL;
|
|
11025
11929
|
const done = await uploadSourcemaps({
|
|
11026
11930
|
url,
|
|
11027
11931
|
key,
|
|
@@ -11031,11 +11935,18 @@ async function sourcemapsCommand(args, rt) {
|
|
|
11031
11935
|
...f.kind ? { kind: f.kind } : {},
|
|
11032
11936
|
...f.flavour ? { flavour: f.flavour } : {},
|
|
11033
11937
|
...rt.fetch ? { fetch: rt.fetch } : {},
|
|
11034
|
-
...rt.readFile ? { readFile: rt.readFile } : {}
|
|
11938
|
+
...preparedFiles ? { readFile: async (path) => preparedFiles.get(path) } : rt.readFile ? { readFile: rt.readFile } : {}
|
|
11035
11939
|
});
|
|
11036
11940
|
for (const d of done) {
|
|
11037
11941
|
rt.print(`${d.file}: uploaded to build ${d.buildId} (${d.gzBytes} bytes gzip, sourcesContent removed from ${d.strippedSources} sources)`);
|
|
11038
11942
|
}
|
|
11943
|
+
try {
|
|
11944
|
+
const components = await prepareComponents(f["project-root"] ?? process.cwd(), { configuration: f["components-config"], warn: (message) => rt.print(`warning: ${message}`) });
|
|
11945
|
+
if (components)
|
|
11946
|
+
rt.print(`${components.file}: prepared ${components.count} components; upload is not yet supported by the API`);
|
|
11947
|
+
} catch (error) {
|
|
11948
|
+
rt.print(`warning: component map preparation skipped: ${error.message}`);
|
|
11949
|
+
}
|
|
11039
11950
|
return 0;
|
|
11040
11951
|
}
|
|
11041
11952
|
|
|
@@ -11043,36 +11954,105 @@ async function sourcemapsCommand(args, rt) {
|
|
|
11043
11954
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
11044
11955
|
|
|
11045
11956
|
// packages/mcp/dist/src/answer.js
|
|
11957
|
+
var SHORT_SESSIONS = 3;
|
|
11958
|
+
var SHORT_POINTS = 5;
|
|
11959
|
+
var SHORT_PLACES = 5;
|
|
11960
|
+
var SHORT_FORM_NOTES = {
|
|
11961
|
+
list_findings: "the how-sure lines, the place lines of findings that are not Worse and the lanes that were judged",
|
|
11962
|
+
get_finding: "the why, seen and layer lines, the component lines, the memory evidence lines other than the judged measure and the prompt",
|
|
11963
|
+
session_places: "the component lines",
|
|
11964
|
+
main_sessions: `the sessions older than the newest ${SHORT_SESSIONS}, the main changes of all but the newest session and, for one run_set, the places in norm`,
|
|
11965
|
+
main_vs_previous: `the movers after the first ${SHORT_PLACES} (every new, gone or multi-device place stays)`,
|
|
11966
|
+
place_trend: `the sessions older than the newest ${SHORT_SESSIONS} and the history restarts among them (unknown restarts always stay)`,
|
|
11967
|
+
window_history: `the runs older than the newest ${SHORT_POINTS}`,
|
|
11968
|
+
pr_status: "the per-journey dashboard links and the heat note",
|
|
11969
|
+
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}`
|
|
11970
|
+
};
|
|
11971
|
+
var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
11972
|
+
|
|
11973
|
+
class Rows {
|
|
11974
|
+
rows = [];
|
|
11975
|
+
add(short, ...texts) {
|
|
11976
|
+
for (const t of texts)
|
|
11977
|
+
for (const l of t.split(`
|
|
11978
|
+
`))
|
|
11979
|
+
this.rows.push({ text: l, short });
|
|
11980
|
+
return this;
|
|
11981
|
+
}
|
|
11982
|
+
both(...texts) {
|
|
11983
|
+
return this.add(true, ...texts);
|
|
11984
|
+
}
|
|
11985
|
+
full(...texts) {
|
|
11986
|
+
return this.add(false, ...texts);
|
|
11987
|
+
}
|
|
11988
|
+
detail(...texts) {
|
|
11989
|
+
for (const text of texts)
|
|
11990
|
+
for (const line of text.split(`
|
|
11991
|
+
`))
|
|
11992
|
+
this.rows.push({ text: line, short: false, notice: false });
|
|
11993
|
+
return this;
|
|
11994
|
+
}
|
|
11995
|
+
render(form, left = []) {
|
|
11996
|
+
if (form !== "short")
|
|
11997
|
+
return this.rows.map((r) => r.text).join(`
|
|
11998
|
+
`);
|
|
11999
|
+
const kept = [];
|
|
12000
|
+
for (const r of this.rows) {
|
|
12001
|
+
if (!r.short)
|
|
12002
|
+
continue;
|
|
12003
|
+
if (r.text === "" && (!kept.length || kept[kept.length - 1] === ""))
|
|
12004
|
+
continue;
|
|
12005
|
+
kept.push(r.text);
|
|
12006
|
+
}
|
|
12007
|
+
if (kept[kept.length - 1] === "")
|
|
12008
|
+
kept.pop();
|
|
12009
|
+
if (this.rows.some((r) => !r.short && r.notice !== false && r.text.trim() !== ""))
|
|
12010
|
+
kept.push(`Short form: ${andList(left.length ? left : ["other lines"])} left out; the full form shows them.`);
|
|
12011
|
+
return kept.join(`
|
|
12012
|
+
`);
|
|
12013
|
+
}
|
|
12014
|
+
}
|
|
11046
12015
|
var LABEL_W = 14;
|
|
11047
12016
|
var line = (label, text) => ` ${label.padEnd(LABEL_W)}${text}`;
|
|
11048
|
-
var
|
|
12017
|
+
var plural2 = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
|
|
12018
|
+
var promptText = (f) => f.what_to_do.prompt ?? "No prompt: this finding is an improvement.";
|
|
11049
12019
|
function formatFinding(f, opts = {}) {
|
|
11050
|
-
const
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`),
|
|
11059
|
-
line("Id", f.id)
|
|
11060
|
-
];
|
|
12020
|
+
const r = new Rows;
|
|
12021
|
+
r.both(f.answer.headline, line("What changed", f.answer.what_changed), line("Where", f.answer.where), line("How sure", f.answer.how_sure));
|
|
12022
|
+
if (f.lane)
|
|
12023
|
+
r.both(...laneContextLines(f.lane.context, f.lane.journey, f.lane.phone_name, opts.verbose, opts.form === "short"));
|
|
12024
|
+
r.full(line("Why", f.answer.why));
|
|
12025
|
+
r.both(line("What to do", f.answer.what_to_do));
|
|
12026
|
+
r.full(line("Seen", f.answer.lifecycle), line("Layer", `${f.layer.id} · ${f.layer.name} (${f.certificate ? "certified" : "uncertified"})${f.limits.length ? `; limits: ${f.limits.join("; ")}` : ""}`));
|
|
12027
|
+
r.both(line("Id", f.id));
|
|
11061
12028
|
if (f.why.components.length) {
|
|
11062
|
-
|
|
11063
|
-
|
|
11064
|
-
|
|
12029
|
+
r.full(line("Components", ""));
|
|
12030
|
+
const names = componentReadNames(f.why.components.map((c) => ({ key: c.key, name: c.component, ...c.file ? { file: c.file } : {} })));
|
|
12031
|
+
for (const [i, c] of f.why.components.entries()) {
|
|
12032
|
+
r.full(` ${names[i]}${opts.verbose && c.file ? ` (${c.file})` : ""}: ${c.self_ms_before ?? "–"} → ${c.self_ms_after} ms ${c.key ? READ_WORDS.componentTime : READ_WORDS.ownTime}, renders ${c.renders_before ?? "–"} → ${c.renders_after ?? "–"}${c.cause ? `; ${c.cause}` : ""}${c.flagged ? "" : " (not certain)"}`);
|
|
12033
|
+
}
|
|
12034
|
+
}
|
|
12035
|
+
const memory = f.why.memory ?? [];
|
|
12036
|
+
if (memory.length) {
|
|
12037
|
+
r.both(` Memory evidence: ${f.where.on === "main" ? "earlier → latest" : "main → PR"} (values are not a verdict)`);
|
|
12038
|
+
for (const m of memory) {
|
|
12039
|
+
const value = (n) => n === null ? "not stored" : fieldValueText(m.metric, n);
|
|
12040
|
+
r.add(m.metric === f.what.metric, ` ${m.label}: ${value(m.before)} → ${value(m.after)}${m.word ? ` (${m.word})` : ""}`);
|
|
11065
12041
|
}
|
|
11066
12042
|
}
|
|
11067
12043
|
if (opts.prompt)
|
|
11068
|
-
|
|
11069
|
-
return
|
|
11070
|
-
|
|
12044
|
+
r.full("", promptText(f));
|
|
12045
|
+
return r.render(opts.form, [
|
|
12046
|
+
"the why, seen and layer lines",
|
|
12047
|
+
...f.why.components.length ? [plural2(f.why.components.length, "component line")] : [],
|
|
12048
|
+
...memory.some((m) => m.metric !== f.what.metric) ? [plural2(memory.filter((m) => m.metric !== f.what.metric).length, "memory evidence line")] : [],
|
|
12049
|
+
...opts.prompt ? ["the prompt"] : []
|
|
12050
|
+
]);
|
|
11071
12051
|
}
|
|
11072
12052
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
11073
12053
|
var LANES_SHOWN = 8;
|
|
11074
12054
|
var STATE_WORDS2 = { present: "still present", resolved: "resolved", "not-judged": "not judged in the newest run set" };
|
|
11075
|
-
function
|
|
12055
|
+
function measuredLines(l, r, opts) {
|
|
11076
12056
|
const f = l.filters;
|
|
11077
12057
|
const scope = [f.branch ?? "all branches", f.journey, f.phone].filter(Boolean).join(" · ");
|
|
11078
12058
|
const m = l.measured;
|
|
@@ -11080,26 +12060,37 @@ function measuredLine(l) {
|
|
|
11080
12060
|
if (!n) {
|
|
11081
12061
|
const on = f.branch ? f.branch === l.baseline_branch ? f.branch : `branch ${f.branch}` : "any branch";
|
|
11082
12062
|
const narrow = [f.journey, f.phone ? `phone “${f.phone}”` : null].filter(Boolean).join(", ");
|
|
11083
|
-
|
|
12063
|
+
r.both(m.run_sets ? `${scope}: ${plural2(m.run_sets, "run set")} measured, but no verdict yet: nothing is known about its performance.` : `No KeepSnappy measurement on ${on}${narrow ? ` for ${narrow}` : ""}: nothing is known about its performance.`);
|
|
12064
|
+
return [];
|
|
11084
12065
|
}
|
|
11085
12066
|
const lane = (x) => {
|
|
11086
12067
|
const runs = x.runs.after !== null && x.runs.before !== null ? x.branch === l.baseline_branch ? `, ${x.runs.after} latest runs vs ${x.runs.before} earlier` : `, ${x.runs.after} branch runs vs ${x.runs.before} on ${l.baseline_branch}` : "";
|
|
11087
|
-
return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${runs}`;
|
|
12068
|
+
return `${x.commit?.sha ? `commit ${x.commit.sha.slice(0, 7)}` : "commit not recorded"} (${when(x.at)}): ${x.word}${runs}${x.waiting_for_reprocess ? `; ${waitingWords(x.waiting_for_reprocess)}` : ""}`;
|
|
11088
12069
|
};
|
|
11089
|
-
if (m.lanes.length === 1)
|
|
11090
|
-
|
|
12070
|
+
if (m.lanes.length === 1) {
|
|
12071
|
+
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
12072
|
+
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
12073
|
+
return [];
|
|
12074
|
+
}
|
|
11091
12075
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
12076
|
+
r.add(shown.some((x) => !x.judged || !!x.context), `${scope}: ${plural2(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
12077
|
+
for (const x of shown) {
|
|
12078
|
+
const keep = !x.judged || !!x.context;
|
|
12079
|
+
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
12080
|
+
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
12081
|
+
}
|
|
12082
|
+
if (m.lanes.length > shown.length)
|
|
12083
|
+
r.full(` … ${m.lanes.length - shown.length} more (--journey / --phone narrow it)`);
|
|
12084
|
+
const judgedLeft = shown.filter((x) => x.judged && !x.context).length;
|
|
11092
12085
|
return [
|
|
11093
|
-
|
|
11094
|
-
...
|
|
11095
|
-
|
|
11096
|
-
].join(`
|
|
11097
|
-
`);
|
|
12086
|
+
...judgedLeft ? [plural2(judgedLeft, "judged lane")] : [],
|
|
12087
|
+
...m.lanes.length > shown.length ? [plural2(m.lanes.length - shown.length, "lane") + " beyond the eighth"] : []
|
|
12088
|
+
];
|
|
11098
12089
|
}
|
|
11099
12090
|
function countsLine2(l) {
|
|
11100
12091
|
const c = l.counts;
|
|
11101
12092
|
const parts = [
|
|
11102
|
-
c.worse ? `${c.worse} ${STATE_WORDS.worse.toLowerCase()}${c.not_blocking ? ` (${c.not_blocking}
|
|
12093
|
+
c.worse ? `${c.worse} ${STATE_WORDS.worse.toLowerCase()}${c.not_blocking ? ` (${c.not_blocking} ${NOT_BLOCKING})` : ""}` : "",
|
|
11103
12094
|
c.warn ? `${c.warn} ${STATE_WORDS.warning.toLowerCase()}` : "",
|
|
11104
12095
|
c.better ? `${c.better} ${STATE_WORDS.better.toLowerCase()}` : ""
|
|
11105
12096
|
].filter(Boolean);
|
|
@@ -11108,31 +12099,135 @@ function countsLine2(l) {
|
|
|
11108
12099
|
const head = parts.length ? `${parts.join(", ")}${state === "all" ? "" : `, ${STATE_WORDS2[state] ?? state}`}` : state !== "present" && state !== "all" ? `No finding ${STATE_WORDS2[state] ?? state}` : unjudged.length === l.measured.lanes.length ? `No finding, but nothing was compared yet (${[...new Set(unjudged.map((x) => x.word))].join(", ")}): measure again before calling it clean` : `Nothing ${STATE_WORDS.worse.toLowerCase()}, nothing ${STATE_WORDS.warning.toLowerCase()}, nothing ${STATE_WORDS.better.toLowerCase()}`;
|
|
11109
12100
|
const heated = c.heat_runs ?? 0;
|
|
11110
12101
|
const more = [
|
|
11111
|
-
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${
|
|
12102
|
+
heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (run health, layer 3)` : "",
|
|
11112
12103
|
state === "present" && l.states.not_judged ? `${l.states.not_judged} more not judged in the newest run set (state not-judged)` : ""
|
|
11113
12104
|
].filter(Boolean);
|
|
11114
12105
|
const line = `${head}${more.length ? `; ${more.join("; ")}` : ""}.`;
|
|
11115
12106
|
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
11116
12107
|
}
|
|
11117
|
-
function formatFindingList(l) {
|
|
11118
|
-
const
|
|
12108
|
+
function formatFindingList(l, opts = {}) {
|
|
12109
|
+
const r = new Rows;
|
|
12110
|
+
const lanesLeft = measuredLines(l, r, opts);
|
|
11119
12111
|
if (l.measured.newest || l.total)
|
|
11120
|
-
|
|
12112
|
+
r.both(countsLine2(l));
|
|
11121
12113
|
l.findings.forEach((f, i) => {
|
|
11122
|
-
|
|
12114
|
+
r.both("", `${l.offset + i + 1}. ${f.answer.headline}`);
|
|
12115
|
+
r.add(isWorseState(f.state), ` ${f.answer.where}`);
|
|
12116
|
+
r.full(` ${f.answer.how_sure}`);
|
|
12117
|
+
r.both(` ${f.answer.lifecycle} Id ${f.id}`);
|
|
11123
12118
|
});
|
|
11124
12119
|
const shown = l.findings.length ? `Showing ${l.offset + 1}–${l.offset + l.findings.length} of ${l.total}` : "";
|
|
11125
12120
|
if (l.next_offset !== null)
|
|
11126
|
-
|
|
12121
|
+
r.both("", `${shown || `Showing none of ${l.total}`}; the rest from offset ${l.next_offset} (--offset ${l.next_offset}).`);
|
|
11127
12122
|
else if (l.offset > 0)
|
|
11128
|
-
|
|
12123
|
+
r.both("", shown ? `${shown}.` : `Offset ${l.offset} is past the last of ${l.total}.`);
|
|
11129
12124
|
if (l.findings.length)
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
12125
|
+
r.both("", "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt");
|
|
12126
|
+
const notWorse = l.findings.filter((f) => !isWorseState(f.state)).length;
|
|
12127
|
+
return r.render(opts.form, [...l.findings.length ? [plural2(l.findings.length, "how-sure line")] : [], ...notWorse ? [plural2(notWorse, "place line")] : [], ...lanesLeft]);
|
|
12128
|
+
}
|
|
12129
|
+
var STATUS_LEAD = {
|
|
12130
|
+
ok: "ok",
|
|
12131
|
+
warns: "nothing blocks, results to look at",
|
|
12132
|
+
blocks: "blocked",
|
|
12133
|
+
"cant-tell": "not ok yet, nothing was compared",
|
|
12134
|
+
"not-judged": "not ok yet, the newest run set was not judged",
|
|
12135
|
+
"never-measured": "not ok yet, never measured"
|
|
12136
|
+
};
|
|
12137
|
+
function formatBranchStatus(s, opts = {}) {
|
|
12138
|
+
const r = new Rows;
|
|
12139
|
+
r.both(`${s.branch}: ${STATUS_LEAD[s.result]}`, s.reason);
|
|
12140
|
+
if (s.lanes.length)
|
|
12141
|
+
r.both("");
|
|
12142
|
+
for (const l of s.lanes) {
|
|
12143
|
+
const shown = l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking);
|
|
12144
|
+
const sha = l.commit?.sha?.slice(0, 7);
|
|
12145
|
+
r.both(` ${l.journey_title ?? l.journey} on ${l.phone_name}: ${shown} · ${sha ? `commit ${sha}` : "commit not recorded"} (${when(l.at)})${l.in_newest_run_set ? "" : " · older run set"}${l.waiting_for_reprocess ? `; ${waitingWords(l.waiting_for_reprocess)}` : ""}`);
|
|
12146
|
+
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
12147
|
+
for (const f of l.worst)
|
|
12148
|
+
r.both(` ${f.id} ${f.headline}`);
|
|
12149
|
+
if (l.present_findings > l.worst.length)
|
|
12150
|
+
r.both(` … ${l.present_findings - l.worst.length} more: keepsnappy findings --branch ${s.branch}`);
|
|
12151
|
+
if (l.link)
|
|
12152
|
+
r.full(` ${l.link}`);
|
|
12153
|
+
}
|
|
12154
|
+
const links = s.lanes.filter((l) => l.link).length;
|
|
12155
|
+
const heated = s.counts.heat_runs ?? 0;
|
|
12156
|
+
const more = [
|
|
12157
|
+
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (information, never blocks)` : ""],
|
|
12158
|
+
[
|
|
12159
|
+
true,
|
|
12160
|
+
s.not_judged_findings ? `${plural2(s.not_judged_findings, "finding")} not judged in the newest run set; they may still be there: keepsnappy findings --branch ${s.branch} --state not-judged` : ""
|
|
12161
|
+
],
|
|
12162
|
+
[true, statusProblemsLine(s)],
|
|
12163
|
+
[true, s.problems?.unread_lanes ? `${unreadWords(s.problems.unread_lanes)} for problems: ${staleResult(s.problems.unread_lanes)}` : ""],
|
|
12164
|
+
[true, s.links.overview ? `Dashboard: ${s.links.overview}` : ""],
|
|
12165
|
+
[true, s.worst.length ? "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt" : ""]
|
|
12166
|
+
];
|
|
12167
|
+
const shownMore = more.filter(([, text]) => text);
|
|
12168
|
+
if (shownMore.length)
|
|
12169
|
+
r.both("");
|
|
12170
|
+
for (const [short, text] of shownMore)
|
|
12171
|
+
r.add(short, text);
|
|
12172
|
+
return r.render(opts.form, [...links ? [plural2(links, "dashboard link")] : [], ...heated ? ["the heat note"] : []]);
|
|
12173
|
+
}
|
|
12174
|
+
function statusProblemsLine(s) {
|
|
12175
|
+
const p = s.problems;
|
|
12176
|
+
if (!p?.count)
|
|
12177
|
+
return "";
|
|
12178
|
+
return `${plural2(p.places, "place")} over budget in this branch’s own runs (${p.far_count ? `${p.far_count} far over budget; ` : ""}not a regression, never blocks): list_problems, or keepsnappy problems --branch ${s.branch}`;
|
|
12179
|
+
}
|
|
12180
|
+
var unreadWords = (n) => `${n} journey × phone not read`;
|
|
12181
|
+
var staleResult = (n) => `${n === 1 ? "its stored result is" : "their stored results are"} not current`;
|
|
12182
|
+
function formatProblemList(l, opts = {}) {
|
|
12183
|
+
const r = new Rows;
|
|
12184
|
+
const b = l.filters.branch;
|
|
12185
|
+
const c = l.counts;
|
|
12186
|
+
const unread = c.unread_lanes ?? 0;
|
|
12187
|
+
const read = l.lanes.length - unread;
|
|
12188
|
+
if (!l.measured.run_sets) {
|
|
12189
|
+
r.both(`No KeepSnappy measurement on ${b === l.baseline_branch ? b : `branch ${b}`}: nothing is known about its problems.`);
|
|
12190
|
+
return r.render(opts.form);
|
|
12191
|
+
}
|
|
12192
|
+
if (!l.lanes.length) {
|
|
12193
|
+
r.both(`${b}: ${plural2(l.measured.run_sets, "run set")} measured, but no judgement yet to read problems from.`);
|
|
12194
|
+
return r.render(opts.form);
|
|
12195
|
+
}
|
|
12196
|
+
if (c.count)
|
|
12197
|
+
r.both(`${b}: ${plural2(c.places, "place")} over budget in ${b === l.baseline_branch ? "main’s" : "this branch’s"} own runs${c.far_count ? ` (${c.far_count} far over budget)` : ""}`);
|
|
12198
|
+
else if (read)
|
|
12199
|
+
r.both(`${b}: no problem over budget in the newest run set (${read} journey × phone read)`);
|
|
12200
|
+
else
|
|
12201
|
+
r.both(`${b}: no problem could be read: ${unread} journey × phone has a stored result that is not current, so nothing is known about its problems.`);
|
|
12202
|
+
r.both(l.note);
|
|
12203
|
+
if (read && unread)
|
|
12204
|
+
r.both(`${unreadWords(unread)}: ${staleResult(unread)}, so nothing is known about ${unread === 1 ? "its" : "their"} problems.`);
|
|
12205
|
+
l.problems.forEach((p, i) => {
|
|
12206
|
+
const keep = i < SHORT_PLACES;
|
|
12207
|
+
r.add(keep, "", `${l.offset + i + 1}. ${p.text}: ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
|
|
12208
|
+
r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}`);
|
|
12209
|
+
const top = p.top_components[0];
|
|
12210
|
+
const topName = componentReadNames(p.top_components)[0];
|
|
12211
|
+
const detail = [p.work, top ? `heaviest ${topName}${opts.verbose && top.key ? ` (${top.key})` : ""} ${top.self_text}` : null, p.new_component ? NEW_IN_PR_CHIP : null, p.range_text ? `${p.range_text} between runs` : null].filter(Boolean);
|
|
12212
|
+
if (detail.length)
|
|
12213
|
+
r.full(` ${detail.join("; ")}`);
|
|
12214
|
+
if (p.url)
|
|
12215
|
+
r.full(` ${p.url}`);
|
|
12216
|
+
});
|
|
12217
|
+
const shown = l.problems.length ? `Showing ${l.offset + 1}–${l.offset + l.problems.length} of ${l.total}` : "";
|
|
12218
|
+
if (l.next_offset !== null)
|
|
12219
|
+
r.both("", `${shown || `Showing none of ${l.total}`}; the rest from offset ${l.next_offset} (--offset ${l.next_offset}).`);
|
|
12220
|
+
else if (l.offset > 0)
|
|
12221
|
+
r.both("", shown ? `${shown}.` : `Offset ${l.offset} is past the last of ${l.total}.`);
|
|
12222
|
+
const detailLines = l.problems.some((p) => p.work || p.top_components.length || p.new_component || p.range_text);
|
|
12223
|
+
return r.render(opts.form, [
|
|
12224
|
+
...detailLines ? ["the work and component lines"] : [],
|
|
12225
|
+
...l.problems.some((p) => p.url) ? ["the dashboard links"] : [],
|
|
12226
|
+
...l.problems.length > SHORT_PLACES ? [`${plural2(l.problems.length - SHORT_PLACES, "problem")} beyond the fifth`] : []
|
|
12227
|
+
]);
|
|
11133
12228
|
}
|
|
11134
12229
|
var commitText = (c) => !c ? "commit not recorded" : c.title ? `“${c.title}”${c.author ? ` by ${c.author}` : ""}${c.sha ? ` (${c.sha.slice(0, 7)})` : ""}` : c.sha ? c.sha.slice(0, 7) : "commit not recorded";
|
|
11135
|
-
function formatCompare(r) {
|
|
12230
|
+
function formatCompare(r, opts = {}) {
|
|
11136
12231
|
const out = [
|
|
11137
12232
|
`Run A: ${r.a.external_id} on ${r.a.branch ?? "?"}, ${commitText(r.a.commit ? { sha: r.a.commit_sha, ...r.a.commit } : { sha: r.a.commit_sha, title: null, author: null })}`,
|
|
11138
12233
|
`Run B: ${r.b.external_id} on ${r.b.branch ?? "?"}, ${commitText(r.b.commit ? { sha: r.b.commit_sha, ...r.b.commit } : { sha: r.b.commit_sha, title: null, author: null })}`,
|
|
@@ -11142,37 +12237,151 @@ function formatCompare(r) {
|
|
|
11142
12237
|
out.push(`Measured differently: ${r.setup_differs.map((d) => `${d.field} ${JSON.stringify(d.a)} → ${JSON.stringify(d.b)}`).join(", ")}`);
|
|
11143
12238
|
for (const w of r.windows) {
|
|
11144
12239
|
const ms = w.metrics.filter((m) => m.a !== null || m.b !== null);
|
|
11145
|
-
|
|
12240
|
+
const c = w.component_page ?? w.components;
|
|
12241
|
+
if (!ms.length && !c)
|
|
11146
12242
|
continue;
|
|
11147
|
-
out.push("", w.window_name);
|
|
12243
|
+
out.push("", `${w.window_name}${opts.verbose ? ` (${w.window})` : ""}`);
|
|
12244
|
+
const singles = [...c?.added ?? [], ...c?.removed ?? [], ...w.component_page?.uncompared?.a ?? [], ...w.component_page?.uncompared?.b ?? []];
|
|
12245
|
+
const components = [...c?.rows ?? [], ...singles];
|
|
12246
|
+
const names = componentReadNames(components.map((row) => ({ key: row.key, name: row.component })));
|
|
12247
|
+
const shown = new Map(components.map((row, i) => [row.key, names[i]]));
|
|
12248
|
+
for (const row of c?.rows ?? [])
|
|
12249
|
+
out.push(` ${shown.get(row.key)}: ${row.self_ms_a === null ? "–" : exactComponentMs(row.self_ms_a)} → ${row.self_ms_b === null ? "–" : exactComponentMs(row.self_ms_b)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12250
|
+
for (const [label, rows] of [
|
|
12251
|
+
[READ_WORDS.onlyB, c?.added ?? []],
|
|
12252
|
+
[READ_WORDS.onlyA, c?.removed ?? []]
|
|
12253
|
+
]) {
|
|
12254
|
+
for (const row of rows)
|
|
12255
|
+
out.push(` ${shown.get(row.key)}: ${label}; ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12256
|
+
}
|
|
12257
|
+
const page = w.component_page;
|
|
12258
|
+
if (page) {
|
|
12259
|
+
for (const side of ["a", "b"]) {
|
|
12260
|
+
if (!page.measured[side])
|
|
12261
|
+
out.push(` ${notMeasuredText(side)}`);
|
|
12262
|
+
for (const row of page.uncompared?.[side] ?? [])
|
|
12263
|
+
out.push(` ${shown.get(row.key)}: ${side === "a" ? READ_WORDS.runA : READ_WORDS.runB} ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
12264
|
+
}
|
|
12265
|
+
out.push(` ${componentPageText(page.page, components.length)}`);
|
|
12266
|
+
} else if (c?.totals)
|
|
12267
|
+
out.push(` ${componentListsText(c.totals, { rows: c.rows.length, added: c.added?.length ?? 0, removed: c.removed?.length ?? 0 })}`);
|
|
12268
|
+
if (c?.omissions)
|
|
12269
|
+
for (const side of ["a", "b"]) {
|
|
12270
|
+
if (page && !page.measured[side])
|
|
12271
|
+
continue;
|
|
12272
|
+
const o = c.omissions[side];
|
|
12273
|
+
if (o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0)
|
|
12274
|
+
out.push(` ${omissionText({ run_id: side === "a" ? READ_WORDS.runA : READ_WORDS.runB, ...o })}`);
|
|
12275
|
+
}
|
|
11148
12276
|
for (const m of ms)
|
|
11149
12277
|
out.push(` ${m.label.padEnd(28)}${m.a_text.padStart(10)} → ${m.b_text}`);
|
|
11150
12278
|
}
|
|
11151
12279
|
return out.join(`
|
|
11152
12280
|
`);
|
|
11153
12281
|
}
|
|
11154
|
-
function formatHistory(h) {
|
|
11155
|
-
const
|
|
12282
|
+
function formatHistory(h, opts = {}) {
|
|
12283
|
+
const r = new Rows;
|
|
12284
|
+
r.both(`${h.window_name}${opts.verbose ? ` (${h.window})` : ""} in ${h.journey_title ?? h.journey} on ${h.branch}: ${h.points.length ? `${plural2(h.points.length, "run")}, oldest first` : h.note ?? "no runs"}`);
|
|
11156
12285
|
const ms = h.measures.slice(0, 4);
|
|
11157
12286
|
if (h.points.length) {
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
}
|
|
12287
|
+
r.both(` ${"when".padEnd(17)}${ms.map((m) => (m.label.length > 22 ? `${m.label.slice(0, 21)}…` : m.label).padStart(24)).join("")} commit`);
|
|
12288
|
+
h.points.forEach((p, i) => {
|
|
12289
|
+
r.add(i >= h.points.length - SHORT_POINTS, ` ${(p.at ?? "").slice(0, 16).replace("T", " ").padEnd(17)}${ms.map((m) => fieldValueText(m.metric, p.values[m.metric]).padStart(24)).join("")} ${commitText(p.commit)}`);
|
|
12290
|
+
});
|
|
11162
12291
|
}
|
|
11163
12292
|
if (h.findings.length)
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
12293
|
+
r.both("", "Findings on this window:", ...h.findings.map((f) => ` ${f.answer.headline} — ${f.where.branch} (${f.id})`));
|
|
12294
|
+
const older = Math.max(0, h.points.length - SHORT_POINTS);
|
|
12295
|
+
return r.render(opts.form, older ? [plural2(older, "older run")] : []);
|
|
11167
12296
|
}
|
|
11168
12297
|
function formatCommits(c) {
|
|
11169
12298
|
return [
|
|
11170
|
-
`${
|
|
12299
|
+
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) on ${c.branch}, oldest first:`,
|
|
11171
12300
|
...c.commits.map((x) => ` ${when(x.first_measured_at)} ${commitText(x)}${x.pull_request?.number ? `, PR #${x.pull_request.number}` : ""}${x.sessions > 1 ? `; ${x.sessions} sessions, the last ${when(x.last_measured_at)}` : ""}`),
|
|
11172
12301
|
c.note
|
|
11173
12302
|
].join(`
|
|
11174
12303
|
`);
|
|
11175
12304
|
}
|
|
12305
|
+
var list = (xs) => xs.join(", ");
|
|
12306
|
+
function versionText(label, counts) {
|
|
12307
|
+
const values = Object.keys(counts).sort((a, b) => a === "unknown" ? -1 : b === "unknown" ? 1 : a < b ? -1 : a > b ? 1 : 0);
|
|
12308
|
+
const name = (k) => k === "unknown" ? "not recorded" : k;
|
|
12309
|
+
if (values.length === 0)
|
|
12310
|
+
return "";
|
|
12311
|
+
if (values.length === 1)
|
|
12312
|
+
return `${label} ${name(values[0])}`;
|
|
12313
|
+
return `${label} mixed: ${values.map((k) => `${name(k)} ×${counts[k]}`).join(", ")}`;
|
|
12314
|
+
}
|
|
12315
|
+
function formatSessionPlaces(p, opts = {}) {
|
|
12316
|
+
const s = p.session;
|
|
12317
|
+
const sha = s.commit?.sha?.slice(0, 7);
|
|
12318
|
+
const commit = s.commit ? `${s.commit.title ? `“${s.commit.title}”${s.commit.author ? ` by ${s.commit.author}` : ""}` : sha ?? "commit not recorded"}${s.commit.title && sha ? ` (${sha})` : ""}` : "commit not recorded";
|
|
12319
|
+
const v = s.versions;
|
|
12320
|
+
const codes = Object.keys(s.runs.invalid_codes);
|
|
12321
|
+
const r = new Rows().both(`Session ${s.completed_at ? `${when(s.completed_at)} ` : ""}on ${s.branch}: ${commit}${s.pull_request?.number ? `, PR #${s.pull_request.number}` : ""}; ${s.journey.title ?? s.journey.key} on ${s.phone.model}${s.phone.os ? ` · ${s.phone.os}` : ""}${s.status === "processed" ? "" : ` (run set ${s.status})`}.`, `Runs: ${s.runs.valid} valid${s.runs.invalid ? `, ${s.runs.invalid} invalid${codes.length ? ` (${list(codes)})` : ""}` : ""}. ${[
|
|
12322
|
+
versionText("SDK", v.sdk),
|
|
12323
|
+
versionText("driver", v.driver),
|
|
12324
|
+
versionText("driver version", v.driver_version),
|
|
12325
|
+
versionText("app", v.app_build),
|
|
12326
|
+
versionText("journey version", v.journey_version),
|
|
12327
|
+
`derivation ${v.derivation}`,
|
|
12328
|
+
`stats ${v.stats}`
|
|
12329
|
+
].filter(Boolean).join(", ")}.`);
|
|
12330
|
+
if (s.not_computed)
|
|
12331
|
+
return r.both("Not computed: this session has no stored statistics yet; nothing is known about its places (not zero).").render(opts.form);
|
|
12332
|
+
if (s.stale)
|
|
12333
|
+
return r.both("Stale: its statistics are of another derivation or stats version and are not served until the next recompute rewrites them; nothing is known about its places (not zero).").render(opts.form);
|
|
12334
|
+
const by = p.filters.measure;
|
|
12335
|
+
const byLabel = p.places.flatMap((x) => x.measures).find((m) => m.measure === by)?.label ?? by;
|
|
12336
|
+
r.both(`Stored per-session statistics: no verdict, no comparison. Places by ${byLabel} (${p.filters.sort}), highest first${p.filters.window ? `, matching “${p.filters.window}”` : ""}${p.without_measure ? `; ${p.without_measure} of ${p.total} places ${p.without_measure === 1 ? "has" : "have"} no value for it` : ""}.`, "");
|
|
12337
|
+
let componentLines = 0;
|
|
12338
|
+
p.places.forEach((place, i) => {
|
|
12339
|
+
const m = place.measures.find((x) => x.measure === by);
|
|
12340
|
+
const value = m ? `${fieldValueText(by, m.median)} (p90 ${fieldValueText(by, m.p90)}, n ${m.n})` : `no ${by}`;
|
|
12341
|
+
const names = componentReadNames(place.components);
|
|
12342
|
+
const comps = place.components.map((c, i) => `${names[i]}${opts.verbose && c.file ? ` (${c.file})` : ""} ${valueText("self_ms", c.self_ms.median)} (p90 ${valueText("self_ms", c.self_ms.p90)}${c.renders_median !== null ? `, ${plural2(c.renders_median, "render")}` : ""})`);
|
|
12343
|
+
r.both(`${p.offset + i + 1}. ${place.window_name}${opts.verbose ? ` (${place.window})` : ""}: ${value}`);
|
|
12344
|
+
if (place.components_page)
|
|
12345
|
+
r.detail(` ${componentPageText(place.components_page, place.components.length)}`);
|
|
12346
|
+
for (const o of (place.omissions ?? []).filter((o) => o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0))
|
|
12347
|
+
r.detail(` ${omissionText(o)}`);
|
|
12348
|
+
const slowest = place.measures.find((m) => m.measure === "commit_max_ms");
|
|
12349
|
+
if (by !== "commit_max_ms" && slowest)
|
|
12350
|
+
r.detail(` ${READ_WORDS.slowest}: ${fieldValueText("commit_max_ms", slowest.median)}`);
|
|
12351
|
+
for (const detail of place.details ?? []) {
|
|
12352
|
+
if (detail.slowestCommit)
|
|
12353
|
+
r.detail(` ${detail.run_id}: ${READ_WORDS.slowest}: ${exactComponentMs(detail.slowestCommit.ms)}`);
|
|
12354
|
+
r.detail(` ${detail.run_id}: ${OUTSIDE_APP_NAME}: ${detail.outside ? exactComponentMs(detail.outside.selfMs) : "–"}; ${OTHER_RENDER_WORK_NAME}: ${detail.remainderMs === null ? "–" : exactComponentMs(detail.remainderMs)}`);
|
|
12355
|
+
for (const l of detail.outside?.rolledUp ?? [])
|
|
12356
|
+
r.detail(` ${libraryLineText(l)}`);
|
|
12357
|
+
if (detail.outside?.rolledUpRest)
|
|
12358
|
+
r.detail(` ${rolledUpRestText(detail.outside.rolledUpRest)}`);
|
|
12359
|
+
}
|
|
12360
|
+
for (const [ci, c] of place.components.entries()) {
|
|
12361
|
+
if (c.detail === "ms-only" && !c.runs?.length)
|
|
12362
|
+
r.detail(` ${names[ci]}: ${READ_WORDS.detailUnavailable}`);
|
|
12363
|
+
for (const d of c.runs ?? [])
|
|
12364
|
+
if (d.detail === "ms-only")
|
|
12365
|
+
r.detail(` ${names[ci]} · ${d.run_id}: ${d.selfMs === null ? "–" : exactComponentMs(d.selfMs)}; ${READ_WORDS.detailUnavailable}`);
|
|
12366
|
+
for (const d of c.runs ?? [])
|
|
12367
|
+
if (d.detail === "full") {
|
|
12368
|
+
if (d.parent)
|
|
12369
|
+
r.detail(` ${names[ci]} · ${d.run_id}: ${READ_WORDS.parent} ${componentReadName({ key: d.parent, name: d.parent })}`);
|
|
12370
|
+
if (d.rolledUpMs !== undefined && d.selfMs !== null)
|
|
12371
|
+
r.detail(` ${names[ci]} · ${d.run_id}: ${rolledUpText(d.rolledUpMs, d.selfMs)}`, ...(d.rolledUp ?? []).map((l) => ` ${libraryLineText(l)}`), ...d.rolledUpRest ? [` ${rolledUpRestText(d.rolledUpRest)}`] : []);
|
|
12372
|
+
}
|
|
12373
|
+
}
|
|
12374
|
+
if (comps.length) {
|
|
12375
|
+
r.full(` Components: ${list(comps)}`);
|
|
12376
|
+
componentLines++;
|
|
12377
|
+
}
|
|
12378
|
+
});
|
|
12379
|
+
if (!p.places.length)
|
|
12380
|
+
r.both(p.total ? `Offset ${p.offset} is past the last of ${p.total} places.` : "No place with stored statistics in this session matches.");
|
|
12381
|
+
else if (p.next_offset !== null)
|
|
12382
|
+
r.both("", `Showing ${p.offset + 1}–${p.offset + p.places.length} of ${p.total}; the rest from offset ${p.next_offset} (--offset ${p.next_offset}).`);
|
|
12383
|
+
return r.render(opts.form, componentLines ? [plural2(componentLines, "component line")] : []);
|
|
12384
|
+
}
|
|
11176
12385
|
function formatLayers(l) {
|
|
11177
12386
|
return [
|
|
11178
12387
|
l.note,
|
|
@@ -11182,6 +12391,106 @@ function formatLayers(l) {
|
|
|
11182
12391
|
].join(`
|
|
11183
12392
|
`);
|
|
11184
12393
|
}
|
|
12394
|
+
var MAIN_TIERS_WORST_FIRST = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
12395
|
+
function formatMainSessions(h, opts = {}) {
|
|
12396
|
+
const baseline = { baseline: { sessions: h.baseline_sessions } };
|
|
12397
|
+
const r = new Rows().both(`Main sessions: tiers are verdicts vs ${h.baseline_sessions === undefined ? "earlier sessions" : `the ${h.baseline_sessions} sessions before`}.`);
|
|
12398
|
+
if (!h.sessions.length)
|
|
12399
|
+
r.both("No main sessions measured for this journey and phone.");
|
|
12400
|
+
const recent = new Set(h.sessions.slice(-SHORT_SESSIONS));
|
|
12401
|
+
const latest = h.sessions[h.sessions.length - 1];
|
|
12402
|
+
let otherChanges = 0;
|
|
12403
|
+
for (const s of h.sessions) {
|
|
12404
|
+
const k = recent.has(s);
|
|
12405
|
+
r.add(k, `${when(s.at)} ${s.run_set_id} ${commitText(s.commit ? { ...s.commit, author: null } : null)} (${plural2(s.runs, "run")})`);
|
|
12406
|
+
if (!s.places)
|
|
12407
|
+
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
12408
|
+
else
|
|
12409
|
+
r.add(k, ` ${MAIN_TIERS_WORST_FIRST.map((tier) => `${MAIN_TIER_WORDS[tier]}: ${s.places[tier]}`).join("; ")}${s.cant_reason ? `; ${mainSessionCantWords(s.places.cant, s.cant_reason, baseline)}` : ""}`);
|
|
12410
|
+
if (s.waiting_sessions)
|
|
12411
|
+
r.add(k, ` ${waitingSessionsWords(s.waiting_sessions)}`);
|
|
12412
|
+
if (s.memory)
|
|
12413
|
+
r.add(k, ` Memory: ${MAIN_TIER_WORDS[s.memory.tier]}, ${s.memory.median} MB (n ${s.memory.n})${s.memory.cant_reason ? `; ${mainCantWords(s.memory.cant_reason, baseline)}` : ""}`);
|
|
12414
|
+
for (const v of h.versions.filter((v) => v.from_run_set_id === s.run_set_id))
|
|
12415
|
+
r.add(k, ` Version: ${v.label}; ${mainCutWords(v)}`);
|
|
12416
|
+
for (const c of h.changes.filter((c) => c.from_run_set_id === s.run_set_id)) {
|
|
12417
|
+
r.add(s === latest, ` Main change: ${windowQualified(c.place)}${opts.verbose ? ` (${c.place}; ${c.ref})` : ""} ${storedRefWords(c.ref, h.changes.filter((x) => x.place === c.place && x.from_run_set_id === c.from_run_set_id).map((x) => x.ref))}, ${c.before_median ?? "–"} → ${c.after_median ?? "–"}`);
|
|
12418
|
+
if (k && s !== latest)
|
|
12419
|
+
otherChanges++;
|
|
12420
|
+
}
|
|
12421
|
+
}
|
|
12422
|
+
const older = h.sessions.length - recent.size;
|
|
12423
|
+
return r.render(opts.form, [
|
|
12424
|
+
...older ? [plural2(older, "older session")] : [],
|
|
12425
|
+
...otherChanges ? [plural2(otherChanges, "main change") + " of the other recent sessions"] : []
|
|
12426
|
+
]);
|
|
12427
|
+
}
|
|
12428
|
+
function formatMainSessionPlaces(p, opts = {}) {
|
|
12429
|
+
const r = new Rows;
|
|
12430
|
+
if (p.status !== "ok")
|
|
12431
|
+
return r.both(`${MAIN_SUMMARY_PENDING}${p.summary_reason ? ` (${p.summary_reason})` : ""}`).render(opts.form);
|
|
12432
|
+
const baselineSessions = p.baseline_sessions;
|
|
12433
|
+
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
12434
|
+
for (const x of p.places)
|
|
12435
|
+
r.add(x.tier !== "same", ` ${windowQualified(x.place)}${opts.verbose ? ` (${x.place}; ${x.ref})` : ""} ${storedRefWords(x.ref, p.places.filter((row) => row.place === x.place).map((row) => row.ref))}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}`);
|
|
12436
|
+
const inNorm = p.places.filter((x) => x.tier === "same").length;
|
|
12437
|
+
return r.render(opts.form, inNorm ? [plural2(inNorm, "place") + " in norm"] : []);
|
|
12438
|
+
}
|
|
12439
|
+
var signed = (n) => `${n > 0 ? "+" : ""}${Number(n.toFixed(3))}`;
|
|
12440
|
+
var previousLine = (p, verbose = false, peers = []) => ` ${windowQualified(p.place)}${verbose ? ` (${p.place}; ${p.ref})` : ""} ${storedRefWords(p.ref, peers)}${p.devices ? ` [devices ${p.devices.before.join(", ") || "–"} → ${p.devices.after.join(", ") || "–"}]` : ""}: ${p.status === "compared" ? "" : `${p.status === "gone" ? p.ref.startsWith("b:component:") ? JOURNEY_ROW_WORDS.removed : JOURNEY_ROW_WORDS.notInRun : p.status === "new" ? JOURNEY_ROW_WORDS.new : p.status}; `}${p.before ?? "–"} → ${p.after ?? "–"}; delta ${p.delta === null ? "–" : signed(p.delta)}; ${p.percent_change === null ? "change undefined" : `${signed(p.percent_change)}%`}; n ${p.n.before ?? "–"} → ${p.n.after ?? "–"}`;
|
|
12441
|
+
function formatMainVsPrevious(m, opts = {}) {
|
|
12442
|
+
const r = new Rows().both(`Information only: main session ${m.run_set_id} vs ${m.previous_run_set_id ?? "no previous session"}.`, `Measure: ${m.measure}; ${m.status}.`);
|
|
12443
|
+
if (m.cut === "unknown")
|
|
12444
|
+
r.both("History restart: unknown; current summaries unavailable.");
|
|
12445
|
+
else if (m.cut)
|
|
12446
|
+
r.both(`History restart: ${m.cut.label}; ${m.cut.cuts.join(", ")}. Numbers cross this restart.`);
|
|
12447
|
+
if (m.not_computed.length)
|
|
12448
|
+
r.both(`not_computed: ${m.not_computed.join(", ")}`);
|
|
12449
|
+
if (m.stale.length)
|
|
12450
|
+
r.both(`stale: ${m.stale.join(", ")}`);
|
|
12451
|
+
if (m.journey)
|
|
12452
|
+
r.both("Whole journey:", previousLine(m.journey, opts.verbose));
|
|
12453
|
+
m.places.forEach((p, i) => r.add(i < SHORT_PLACES || p.status !== "compared", previousLine(p, opts.verbose, m.places.filter((row) => row.place === p.place).map((row) => row.ref))));
|
|
12454
|
+
if (m.next_offset !== null)
|
|
12455
|
+
r.both(`Showing ${m.offset + 1}–${m.offset + m.places.length} of ${m.total}; next offset ${m.next_offset}.`);
|
|
12456
|
+
const smaller = m.places.filter((p, i) => i >= SHORT_PLACES && p.status === "compared").length;
|
|
12457
|
+
return r.render(opts.form, smaller ? [plural2(smaller, "smaller mover")] : []);
|
|
12458
|
+
}
|
|
12459
|
+
function formatPlaceTrend(t, opts = {}) {
|
|
12460
|
+
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
12461
|
+
const newest = new Set(t.sessions.slice(-SHORT_SESSIONS).map((s) => s.run_set_id));
|
|
12462
|
+
const olderSessions = new Set;
|
|
12463
|
+
let olderRestarts = 0;
|
|
12464
|
+
for (const session of t.sessions) {
|
|
12465
|
+
const k = newest.has(session.run_set_id);
|
|
12466
|
+
if (session.restarts_known === false)
|
|
12467
|
+
r.both(`History restarts unknown: ${session.run_set_id} (no current main summary in the history window)`);
|
|
12468
|
+
for (const restart of t.restarts.filter((x) => x.from_run_set_id === session.run_set_id)) {
|
|
12469
|
+
r.add(k, `History restarts: ${restart.label} (${restart.cuts.join(", ")})`);
|
|
12470
|
+
if (!k)
|
|
12471
|
+
olderRestarts++;
|
|
12472
|
+
}
|
|
12473
|
+
for (const place of t.places) {
|
|
12474
|
+
const value = place.sessions.find((s) => s.run_set_id === session.run_set_id);
|
|
12475
|
+
if (!value)
|
|
12476
|
+
continue;
|
|
12477
|
+
const unit = place.unit ? ` ${place.unit}` : "";
|
|
12478
|
+
if (!k)
|
|
12479
|
+
olderSessions.add(session.run_set_id);
|
|
12480
|
+
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value.median}${unit}, MAD ${value.mad}${unit}, n=${value.n}`);
|
|
12481
|
+
}
|
|
12482
|
+
}
|
|
12483
|
+
if (!t.sessions.length)
|
|
12484
|
+
r.both("No main sessions.");
|
|
12485
|
+
if (t.not_computed.length)
|
|
12486
|
+
r.both(`Not computed: ${t.not_computed.join(", ")}`);
|
|
12487
|
+
if (t.stale.length)
|
|
12488
|
+
r.both(`Stale: ${t.stale.join(", ")}`);
|
|
12489
|
+
return r.render(opts.form, [
|
|
12490
|
+
...olderSessions.size ? [plural2(olderSessions.size, "older session")] : [],
|
|
12491
|
+
...olderRestarts ? [plural2(olderRestarts, "older restart marker")] : []
|
|
12492
|
+
]);
|
|
12493
|
+
}
|
|
11185
12494
|
|
|
11186
12495
|
// packages/mcp/dist/src/client.js
|
|
11187
12496
|
class ApiError extends Error {
|
|
@@ -11196,12 +12505,22 @@ class ApiError extends Error {
|
|
|
11196
12505
|
}
|
|
11197
12506
|
async function get(t, path, query = {}) {
|
|
11198
12507
|
const u = new URL(path, t.url.endsWith("/") ? t.url : `${t.url}/`);
|
|
11199
|
-
for (const [k, v] of Object.entries(query))
|
|
11200
|
-
if (v
|
|
12508
|
+
for (const [k, v] of Object.entries(query)) {
|
|
12509
|
+
if (Array.isArray(v))
|
|
12510
|
+
for (const value of v)
|
|
12511
|
+
u.searchParams.append(k, value);
|
|
12512
|
+
else if (v !== undefined && v !== "")
|
|
11201
12513
|
u.searchParams.set(k, String(v));
|
|
12514
|
+
}
|
|
11202
12515
|
const r = await (t.fetch ?? fetch)(u, { headers: { "x-keepsnappy-key": t.key, accept: "application/json" } });
|
|
11203
12516
|
const text = await r.text();
|
|
11204
|
-
|
|
12517
|
+
let body;
|
|
12518
|
+
try {
|
|
12519
|
+
body = text ? JSON.parse(text) : {};
|
|
12520
|
+
} catch {
|
|
12521
|
+
const page = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
12522
|
+
throw new ApiError(r.status, "not_json", `the API answered ${r.status} with a body that is not JSON${page ? `: ${page}` : ""}`);
|
|
12523
|
+
}
|
|
11205
12524
|
if (!r.ok) {
|
|
11206
12525
|
const code = typeof body.error === "string" ? body.error : String(r.status);
|
|
11207
12526
|
const message = typeof body.message === "string" ? body.message : code;
|
|
@@ -11211,31 +12530,60 @@ async function get(t, path, query = {}) {
|
|
|
11211
12530
|
return body;
|
|
11212
12531
|
}
|
|
11213
12532
|
var listFindings = (t, f = {}) => get(t, "v1/findings", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, state: f.state, limit: f.limit, offset: f.offset || undefined });
|
|
12533
|
+
var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
12534
|
+
var listProblems = (t, f = {}) => get(t, "v1/problems", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, limit: f.limit, offset: f.offset || undefined });
|
|
11214
12535
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
11215
|
-
var compareRuns = (t, a, b) => get(t, "v1/compare", { a, b });
|
|
12536
|
+
var compareRuns = (t, a, b, page = {}) => get(t, "v1/compare", { a, b, ...page });
|
|
11216
12537
|
var windowHistory = (t, q) => get(t, "v1/history", q);
|
|
11217
12538
|
var commitsBetween = (t, q) => get(t, "v1/commits", q);
|
|
11218
12539
|
var layers = (t) => get(t, "v1/layers");
|
|
12540
|
+
var sessionPlaces = (t, f) => get(t, "v1/sessions/places", {
|
|
12541
|
+
run_set: f.run_set,
|
|
12542
|
+
journey: f.journey,
|
|
12543
|
+
phone: f.phone,
|
|
12544
|
+
branch: f.branch,
|
|
12545
|
+
measure: f.measure,
|
|
12546
|
+
sort: f.sort,
|
|
12547
|
+
window: f.window,
|
|
12548
|
+
limit: f.limit,
|
|
12549
|
+
offset: f.offset || undefined,
|
|
12550
|
+
top_components: f.top_components,
|
|
12551
|
+
detail: f.detail,
|
|
12552
|
+
component_offset: f.component_offset,
|
|
12553
|
+
component_limit: f.component_limit
|
|
12554
|
+
});
|
|
12555
|
+
var SESSION_SORTS = ["median", "p90", "mad"];
|
|
12556
|
+
var mainSessions = (t, q) => get(t, "v1/main/sessions", { ...q });
|
|
12557
|
+
var mainSessionPlaces = (t, runSetId, q) => get(t, `v1/main/sessions/${encodeURIComponent(runSetId)}/places`, { ...q });
|
|
12558
|
+
var mainVsPrevious = (t, q) => {
|
|
12559
|
+
const { run_set, ...query } = q;
|
|
12560
|
+
return get(t, `v1/main/sessions/${encodeURIComponent(run_set ?? "latest")}/vs-previous`, query);
|
|
12561
|
+
};
|
|
12562
|
+
var placeTrend = (t, q) => get(t, "v1/places/trend", { ...q });
|
|
11219
12563
|
|
|
11220
12564
|
// apps/agent/src/findings.ts
|
|
11221
|
-
var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KEY] [--phone MODEL] [--state present|not-judged|resolved|all] [--limit N] [--offset N] [--json]
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
12565
|
+
var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KEY] [--phone MODEL] [--state present|not-judged|resolved|all] [--limit N] [--offset N] [--short] [--json]
|
|
12566
|
+
status --branch NAME [--short] [--json] (is my PR ok? one answer: blocked / nothing blocks / ok, per journey × phone the word, the ids of the worst findings, dashboard links; never measured and not judged are not ok)
|
|
12567
|
+
problems --branch NAME | --main [--journey KEY] [--phone MODEL] [--limit N] [--offset N] [--short] [--json]
|
|
12568
|
+
(the single problems "Over budget" / "Far over budget" in the branch's own runs, whatever main does: not a regression, never blocking)
|
|
12569
|
+
finding <id> [--prompt] [--short] [--json] (--prompt: only the hand-off for a coding agent; with --json the whole finding)
|
|
12570
|
+
compare <runA> <runB> [--json] (run ids or external ids; one run against one, no statistics)
|
|
12571
|
+
history <journey> <window> [--phone MODEL] [--short] [--json] (exact window id, e.g. "press:live.chat.send")
|
|
12572
|
+
commits <from> [<to>] [--branch NAME] [--json] (measured commits between two run sets or commit hashes)
|
|
12573
|
+
places [--run-set ID | --journey KEY [--phone MODEL] [--branch NAME]] [--measure ID] [--sort median|p90|mad] [--window TEXT] [--limit N] [--offset N] [--top-components N] [--short] [--json]
|
|
12574
|
+
(every place of one session from its stored statistics: no verdict; the newest closed run set of the journey when --run-set is not given)
|
|
12575
|
+
main --journey KEY --phone MODEL [--sessions N | --session ID] [--vs-previous] [--measure REF] [--limit N] [--offset N] [--short] [--json]
|
|
11226
12576
|
layers [--json]
|
|
12577
|
+
trend <journey> <window words...> --phone MODEL [--window PLACE ...] [--ref REF ...] [--sessions N] [--short] [--json]
|
|
12578
|
+
(the words after the journey are one window name; more windows with repeated --window)
|
|
12579
|
+
${READ_USAGE.components}
|
|
12580
|
+
${READ_USAGE.verbose}
|
|
12581
|
+
${READ_USAGE.detailFlag}
|
|
12582
|
+
--short (findings, status, problems, finding, history, places, main, trend: what decides, without the evidence detail; the same as the MCP tools' form short; --json is unchanged)
|
|
11227
12583
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API)`;
|
|
11228
|
-
|
|
11229
|
-
class UsageError extends ConfigError {
|
|
11230
|
-
constructor(lines) {
|
|
11231
|
-
super(lines);
|
|
11232
|
-
this.message = lines.join(`
|
|
11233
|
-
`);
|
|
11234
|
-
this.name = "UsageError";
|
|
11235
|
-
}
|
|
11236
|
-
}
|
|
11237
12584
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
11238
|
-
var
|
|
12585
|
+
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "history", "places", "main", "trend"]);
|
|
12586
|
+
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
11239
12587
|
async function findingsCommand(cmd, argv, rt) {
|
|
11240
12588
|
const args = (() => {
|
|
11241
12589
|
try {
|
|
@@ -11243,17 +12591,31 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11243
12591
|
args: argv,
|
|
11244
12592
|
allowPositionals: true,
|
|
11245
12593
|
options: {
|
|
12594
|
+
verbose: { type: "boolean" },
|
|
12595
|
+
detail: { type: "boolean" },
|
|
12596
|
+
page: { type: "string" },
|
|
12597
|
+
all: { type: "boolean" },
|
|
11246
12598
|
branch: { type: "string" },
|
|
11247
12599
|
main: { type: "boolean" },
|
|
11248
12600
|
journey: { type: "string" },
|
|
11249
12601
|
phone: { type: "string" },
|
|
11250
12602
|
state: { type: "string" },
|
|
12603
|
+
session: { type: "string" },
|
|
12604
|
+
"vs-previous": { type: "boolean" },
|
|
11251
12605
|
limit: { type: "string" },
|
|
11252
12606
|
offset: { type: "string" },
|
|
12607
|
+
"run-set": { type: "string" },
|
|
12608
|
+
measure: { type: "string" },
|
|
12609
|
+
sort: { type: "string" },
|
|
12610
|
+
"top-components": { type: "string" },
|
|
11253
12611
|
json: { type: "boolean" },
|
|
12612
|
+
short: { type: "boolean" },
|
|
11254
12613
|
prompt: { type: "boolean" },
|
|
11255
12614
|
api: { type: "string" },
|
|
11256
|
-
"key-env": { type: "string" }
|
|
12615
|
+
"key-env": { type: "string" },
|
|
12616
|
+
window: { type: "string", multiple: true },
|
|
12617
|
+
ref: { type: "string", multiple: true },
|
|
12618
|
+
sessions: { type: "string" }
|
|
11257
12619
|
}
|
|
11258
12620
|
});
|
|
11259
12621
|
} catch (e) {
|
|
@@ -11266,8 +12628,9 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11266
12628
|
if (!key)
|
|
11267
12629
|
throw new ConfigError([`a project key with read scope is needed: set the environment variable ${keyEnv}`]);
|
|
11268
12630
|
const t = { url: o.api ?? envValue(rt.env, "API") ?? DEFAULT_API_URL, key, ...rt.fetch ? { fetch: rt.fetch } : {} };
|
|
12631
|
+
const answerForm = { form: o.short ? "short" : "full", verbose: o.verbose };
|
|
11269
12632
|
const out = (v, text) => {
|
|
11270
|
-
rt.print(o.json ? JSON.stringify(v, null, 2) : text(v));
|
|
12633
|
+
rt.print(o.json ? JSON.stringify(v, null, 2) : text(v, answerForm));
|
|
11271
12634
|
return 0;
|
|
11272
12635
|
};
|
|
11273
12636
|
const need = (n, what) => {
|
|
@@ -11277,10 +12640,34 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11277
12640
|
const limit = o.limit === undefined ? undefined : Number(o.limit);
|
|
11278
12641
|
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 200))
|
|
11279
12642
|
throw new UsageError(["--limit must be a whole number from 1 to 200 (page with --offset)"]);
|
|
12643
|
+
if (o.short && !SHORT_COMMANDS.has(cmd))
|
|
12644
|
+
throw new UsageError([`--short is for ${[...SHORT_COMMANDS].join(", ")}; keepsnappy ${cmd} has only one form`]);
|
|
11280
12645
|
const offset = o.offset === undefined ? undefined : Number(o.offset);
|
|
11281
12646
|
if (offset !== undefined && (!Number.isInteger(offset) || offset < 0))
|
|
11282
12647
|
throw new UsageError(["--offset must be a whole number >= 0"]);
|
|
12648
|
+
const page = o.page === undefined ? undefined : Number(o.page);
|
|
12649
|
+
if (page !== undefined && (!["places", "compare"].includes(cmd) || !Number.isInteger(page) || page < 1))
|
|
12650
|
+
throw new UsageError([READ_USAGE.page]);
|
|
12651
|
+
if (o.all && (!["places", "compare"].includes(cmd) || page !== undefined || o.offset !== undefined || o.short))
|
|
12652
|
+
throw new UsageError([READ_USAGE.all]);
|
|
11283
12653
|
switch (cmd) {
|
|
12654
|
+
case "main": {
|
|
12655
|
+
if (!o.journey)
|
|
12656
|
+
throw new UsageError(["keepsnappy main needs --journey KEY"]);
|
|
12657
|
+
if (!o.phone)
|
|
12658
|
+
throw new UsageError(["keepsnappy main needs --phone MODEL (exact device model)"]);
|
|
12659
|
+
const sessions = o.sessions === undefined ? undefined : Number(o.sessions);
|
|
12660
|
+
if (sessions !== undefined && (!Number.isInteger(sessions) || sessions < 1 || sessions > 20))
|
|
12661
|
+
throw new UsageError(["--sessions must be a whole number from 1 to 20"]);
|
|
12662
|
+
if (sessions !== undefined && (o.session || o["vs-previous"]))
|
|
12663
|
+
throw new UsageError(["--sessions is for the session list"]);
|
|
12664
|
+
const lane = { journey: o.journey, phone: o.phone };
|
|
12665
|
+
if (o["vs-previous"])
|
|
12666
|
+
return out(await mainVsPrevious(t, { ...lane, run_set: o.session, measure: o.measure, limit, offset }), formatMainVsPrevious);
|
|
12667
|
+
if (o.session)
|
|
12668
|
+
return out(await mainSessionPlaces(t, o.session, lane), formatMainSessionPlaces);
|
|
12669
|
+
return out(await mainSessions(t, { ...lane, sessions }), formatMainSessions);
|
|
12670
|
+
}
|
|
11284
12671
|
case "findings": {
|
|
11285
12672
|
if (o.branch && o.main)
|
|
11286
12673
|
throw new UsageError(["give --branch or --main, not both"]);
|
|
@@ -11297,30 +12684,182 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
11297
12684
|
...offset ? { offset } : {}
|
|
11298
12685
|
}), formatFindingList);
|
|
11299
12686
|
}
|
|
12687
|
+
case "status":
|
|
12688
|
+
if (!o.branch)
|
|
12689
|
+
throw new UsageError(["keepsnappy status needs --branch NAME", FINDINGS_USAGE]);
|
|
12690
|
+
return out(await branchStatus(t, o.branch), formatBranchStatus);
|
|
12691
|
+
case "problems":
|
|
12692
|
+
if (!o.branch && !o.main)
|
|
12693
|
+
throw new UsageError(["keepsnappy problems needs --branch NAME or --main", FINDINGS_USAGE]);
|
|
12694
|
+
if (o.branch && o.main)
|
|
12695
|
+
throw new UsageError(["give --branch or --main, not both"]);
|
|
12696
|
+
return out(await listProblems(t, {
|
|
12697
|
+
...o.branch ? { branch: o.branch } : {},
|
|
12698
|
+
...o.main ? { main: true } : {},
|
|
12699
|
+
...o.journey ? { journey: o.journey } : {},
|
|
12700
|
+
...o.phone ? { phone: o.phone } : {},
|
|
12701
|
+
...limit ? { limit } : {},
|
|
12702
|
+
...offset ? { offset } : {}
|
|
12703
|
+
}), formatProblemList);
|
|
11300
12704
|
case "finding": {
|
|
11301
12705
|
need(1, "<id>");
|
|
11302
12706
|
const f = await getFinding(t, p[0]);
|
|
11303
12707
|
if (o.prompt && !o.json) {
|
|
11304
|
-
rt.print(f
|
|
12708
|
+
rt.print(promptText(f));
|
|
11305
12709
|
return 0;
|
|
11306
12710
|
}
|
|
11307
|
-
return out(f,
|
|
12711
|
+
return out(f, formatFinding);
|
|
11308
12712
|
}
|
|
11309
|
-
case "compare":
|
|
12713
|
+
case "compare": {
|
|
11310
12714
|
need(2, "<runA> <runB>");
|
|
11311
|
-
|
|
12715
|
+
const size = o["top-components"] === undefined ? 12 : Number(o["top-components"]);
|
|
12716
|
+
if (!Number.isInteger(size) || size < 1 || size > 200)
|
|
12717
|
+
throw new UsageError([READ_USAGE.compareTop]);
|
|
12718
|
+
const query = page || o.all || o["top-components"] !== undefined ? { component_offset: ((page ?? 1) - 1) * size, component_limit: size } : {};
|
|
12719
|
+
const compared = await compareRuns(t, p[0], p[1], query);
|
|
12720
|
+
return out(o.all ? await allCompare(t, compared, size) : compared, formatCompare);
|
|
12721
|
+
}
|
|
11312
12722
|
case "history":
|
|
11313
12723
|
need(2, "<journey> <window>");
|
|
11314
12724
|
return out(await windowHistory(t, { journey: p[0], window: p.slice(1).join(" "), ...o.phone ? { phone: o.phone } : {}, ...limit ? { limit } : {} }), formatHistory);
|
|
11315
12725
|
case "commits":
|
|
11316
12726
|
need(1, "<from>");
|
|
11317
12727
|
return out(await commitsBetween(t, { from: p[0], ...p[1] ? { to: p[1] } : {}, ...o.branch ? { branch: o.branch } : {} }), formatCommits);
|
|
12728
|
+
case "places": {
|
|
12729
|
+
const sort = o.sort;
|
|
12730
|
+
if (sort && !SESSION_SORTS.includes(sort))
|
|
12731
|
+
throw new UsageError([`--sort is ${SESSION_SORTS.join(", ")}`]);
|
|
12732
|
+
const top = o["top-components"] === undefined ? undefined : Number(o["top-components"]);
|
|
12733
|
+
if (top !== undefined && (!Number.isInteger(top) || top < (page || o.all ? 1 : 0) || top > (page || o.all ? 200 : 20)))
|
|
12734
|
+
throw new UsageError([READ_USAGE.top]);
|
|
12735
|
+
if ((o.window?.length ?? 0) > 1)
|
|
12736
|
+
throw new UsageError(["keepsnappy places takes one --window (more windows: keepsnappy trend)", FINDINGS_USAGE]);
|
|
12737
|
+
const query = {
|
|
12738
|
+
...o.verbose || o.detail ? { detail: true } : {},
|
|
12739
|
+
...o["run-set"] ? { run_set: o["run-set"] } : {},
|
|
12740
|
+
...o.journey ? { journey: o.journey } : {},
|
|
12741
|
+
...o.phone ? { phone: o.phone } : {},
|
|
12742
|
+
...o.branch ? { branch: o.branch } : {},
|
|
12743
|
+
...o.measure ? { measure: o.measure } : {},
|
|
12744
|
+
...sort ? { sort } : {},
|
|
12745
|
+
...o.window?.length ? { window: o.window[0] } : {},
|
|
12746
|
+
...limit ? { limit } : {},
|
|
12747
|
+
...offset ? { offset } : {},
|
|
12748
|
+
...page || o.all ? { component_offset: ((page ?? 1) - 1) * (top ?? 5), component_limit: top ?? 5 } : top !== undefined ? { top_components: top } : {}
|
|
12749
|
+
};
|
|
12750
|
+
return out(o.all ? await allSessionPlaces(t, query) : await sessionPlaces(t, query), formatSessionPlaces);
|
|
12751
|
+
}
|
|
11318
12752
|
case "layers":
|
|
11319
12753
|
return out(await layers(t), formatLayers);
|
|
12754
|
+
case "trend": {
|
|
12755
|
+
const journey = o.journey ?? p[0];
|
|
12756
|
+
const words = p.slice(o.journey ? 0 : 1).join(" ");
|
|
12757
|
+
const windows = [...words ? [words] : [], ...o.window ?? []];
|
|
12758
|
+
if (!journey || !windows.length || !o.phone)
|
|
12759
|
+
throw new UsageError(["keepsnappy trend needs <journey> <window> --phone MODEL", FINDINGS_USAGE]);
|
|
12760
|
+
const sessions = o.sessions === undefined ? undefined : Number(o.sessions);
|
|
12761
|
+
if (sessions !== undefined && (!Number.isInteger(sessions) || sessions < 1 || sessions > 30))
|
|
12762
|
+
throw new UsageError(["--sessions must be a whole number from 1 to 30"]);
|
|
12763
|
+
return out(await placeTrend(t, { journey, phone: o.phone, window: windows, ref: o.ref, sessions }), formatPlaceTrend);
|
|
12764
|
+
}
|
|
11320
12765
|
default:
|
|
11321
12766
|
throw new UsageError([FINDINGS_USAGE]);
|
|
11322
12767
|
}
|
|
11323
12768
|
}
|
|
12769
|
+
async function allSessionPlaces(target, query) {
|
|
12770
|
+
let first = await sessionPlaces(target, query);
|
|
12771
|
+
if (first.places.some((p) => !p.components_page))
|
|
12772
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12773
|
+
if (first.session.not_computed || first.session.stale)
|
|
12774
|
+
return first;
|
|
12775
|
+
const result = { ...first, places: [], offset: 0, next_offset: null };
|
|
12776
|
+
const pinned = { ...query, run_set: first.session.run_set_id, branch: undefined };
|
|
12777
|
+
const visited = new Set;
|
|
12778
|
+
for (;; ) {
|
|
12779
|
+
const places = first.places.map((p) => ({ ...p, components: [...p.components] }));
|
|
12780
|
+
let response = first;
|
|
12781
|
+
for (;; ) {
|
|
12782
|
+
const next = response.places.flatMap((p) => p.components_page?.next_offset ?? []);
|
|
12783
|
+
if (!next.length)
|
|
12784
|
+
break;
|
|
12785
|
+
const component_offset = Math.min(...next);
|
|
12786
|
+
const mark = `${first.offset}:${component_offset}`;
|
|
12787
|
+
if (visited.has(mark))
|
|
12788
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12789
|
+
visited.add(mark);
|
|
12790
|
+
response = await sessionPlaces(target, { ...pinned, offset: first.offset, component_offset });
|
|
12791
|
+
for (const p of places) {
|
|
12792
|
+
const more = response.places.find((x) => x.window === p.window);
|
|
12793
|
+
if (!more?.components_page)
|
|
12794
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12795
|
+
const keys = new Set(p.components.map((c) => c.key));
|
|
12796
|
+
p.components.push(...more.components.filter((c) => !keys.has(c.key)));
|
|
12797
|
+
p.components_page = more.components_page;
|
|
12798
|
+
}
|
|
12799
|
+
}
|
|
12800
|
+
result.places.push(...places.map((p) => ({ ...p, ...p.components_page ? { components_page: { ...p.components_page, offset: 0, limit: p.components.length, next_offset: null } } : {} })));
|
|
12801
|
+
if (places.some((p) => p.components_page && p.components.length !== p.components_page.total))
|
|
12802
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12803
|
+
if (first.next_offset === null) {
|
|
12804
|
+
if (result.places.length !== result.total)
|
|
12805
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12806
|
+
return result;
|
|
12807
|
+
}
|
|
12808
|
+
const mark = `places:${first.next_offset}`;
|
|
12809
|
+
if (visited.has(mark))
|
|
12810
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12811
|
+
visited.add(mark);
|
|
12812
|
+
first = await sessionPlaces(target, { ...pinned, offset: first.next_offset, component_offset: 0 });
|
|
12813
|
+
}
|
|
12814
|
+
}
|
|
12815
|
+
async function allCompare(target, result, size) {
|
|
12816
|
+
let response = result;
|
|
12817
|
+
const visited = new Set;
|
|
12818
|
+
for (;; ) {
|
|
12819
|
+
if (response.windows.some((w) => w.components && !w.component_page))
|
|
12820
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12821
|
+
const offsets = response.windows.flatMap((w) => w.component_page?.page.next_offset ?? []);
|
|
12822
|
+
if (!offsets.length)
|
|
12823
|
+
break;
|
|
12824
|
+
const offset = Math.min(...offsets);
|
|
12825
|
+
if (visited.has(offset))
|
|
12826
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12827
|
+
visited.add(offset);
|
|
12828
|
+
response = await compareRuns(target, result.a.id, result.b.id, { component_offset: offset, component_limit: size });
|
|
12829
|
+
for (const w of result.windows) {
|
|
12830
|
+
const c = w.component_page;
|
|
12831
|
+
if (!c && !w.components)
|
|
12832
|
+
continue;
|
|
12833
|
+
const more = response.windows.find((x) => x.window === w.window)?.component_page;
|
|
12834
|
+
if (!c || !more)
|
|
12835
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12836
|
+
const keys = new Set(c.rows.map((row) => row.key));
|
|
12837
|
+
c.rows.push(...more.rows.filter((row) => !keys.has(row.key)));
|
|
12838
|
+
for (const field of ["added", "removed"]) {
|
|
12839
|
+
const existing = c[field] ?? [];
|
|
12840
|
+
const known = new Set(existing.map((row) => row.key));
|
|
12841
|
+
c[field] = [...existing, ...(more[field] ?? []).filter((row) => !known.has(row.key))];
|
|
12842
|
+
}
|
|
12843
|
+
for (const side of ["a", "b"]) {
|
|
12844
|
+
if (!c.uncompared || !more.uncompared)
|
|
12845
|
+
continue;
|
|
12846
|
+
const known = new Set(c.uncompared[side].map((row) => row.key));
|
|
12847
|
+
c.uncompared[side].push(...more.uncompared[side].filter((row) => !known.has(row.key)));
|
|
12848
|
+
}
|
|
12849
|
+
c.page = more.page;
|
|
12850
|
+
}
|
|
12851
|
+
}
|
|
12852
|
+
for (const w of result.windows) {
|
|
12853
|
+
const c = w.component_page;
|
|
12854
|
+
if (!c)
|
|
12855
|
+
continue;
|
|
12856
|
+
const count = c.rows.length + (c.added?.length ?? 0) + (c.removed?.length ?? 0) + (c.uncompared?.a.length ?? 0) + (c.uncompared?.b.length ?? 0);
|
|
12857
|
+
if (count !== c.page.total)
|
|
12858
|
+
throw new UsageError([READ_USAGE.paging]);
|
|
12859
|
+
c.page = { ...c.page, offset: 0, limit: count, next_offset: null };
|
|
12860
|
+
}
|
|
12861
|
+
return result;
|
|
12862
|
+
}
|
|
11324
12863
|
|
|
11325
12864
|
// apps/agent/src/cli.ts
|
|
11326
12865
|
var USAGE = `keepsnappy <command>
|
|
@@ -11328,21 +12867,26 @@ var USAGE = `keepsnappy <command>
|
|
|
11328
12867
|
[--upload [--wait] [--fail-on-worse] [--ingest-url URL] [--commit SHA] [--branch NAME] [--base-commit SHA]]
|
|
11329
12868
|
[--device-model NAME --device-os VERSION] [--scenario NAME] [--replay <out dir>[:ARM] ...]
|
|
11330
12869
|
[--no-markers] [-- <e2e command...>]
|
|
12870
|
+
(--fail-on-worse: exit 3 only when the verdict computed on that run set is Worse and blocking; closing never computes one,
|
|
12871
|
+
so with no computed verdict it checks nothing: "Verdict not computed yet (computed on demand): --fail-on-worse checked nothing", exit 0)
|
|
11331
12872
|
measure --repeat N [--arms A=<app|bundle>,B=<app|bundle>] [--arm-env 'A:KEY=VAL' ...] [--bundle-id ID] [--platform ios-sim|ios-device]
|
|
11332
12873
|
[--device UDID] [--out DIR] [--timeout SEC] [--ri-capture observations.ndjson] [--mem-window NAME]
|
|
11333
12874
|
[--clock-offset-ms N] [--no-markers]
|
|
11334
12875
|
-- <test command...>
|
|
11335
|
-
farm open --api URL --device-model
|
|
12876
|
+
farm open --api URL [--device-model CODE] [--device-os V] [--device-id SERIAL|UDID] [--device-provider browserstack] [--platform ios|android] --journeys a,b [--repeat 5]
|
|
12877
|
+
(--device-model is read from the device with --platform and a local --device-provider: adb ro.product.model, devicectl machine id, simctl name; never for browserstack)
|
|
11336
12878
|
--tokens-js <file> --state <file> [--branch NAME --commit SHA --base-commit SHA] [--ttl-seconds 21600] [--key-env KEEPSNAPPY_KEY]
|
|
11337
12879
|
[--config keepsnappy.yml] [--flows <files|dirs|globs,...>] (journey titles: keepsnappy.yml flows.paths, else the flow's name: / top comment)
|
|
11338
12880
|
[--completion-marker] (the flow posts step run:complete at the end of each iteration; runs without it are invalid)
|
|
11339
12881
|
[--driver appium --journey-module FILE] [--driver-version V] [--app-build-id ID] [--harness-version V] [--journey-version V] [--flag-set LABEL] (setup fingerprint, plan 51)
|
|
11340
12882
|
[--sdk-args k=v&...] (over the run defaults profile=1 diag=1; tokens.js carries them to the flows' activation link)
|
|
11341
12883
|
farm close --state <file> [--wait [--fail-on-worse]] [--bs-build browserstack.ios.json] [--settle-seconds 60] [--max-wait-seconds 300] [--out DIR]
|
|
11342
|
-
|
|
12884
|
+
(--fail-on-worse as for run: with no verdict computed on the run set it checks nothing)
|
|
12885
|
+
farm run --app <eas:ID|file.ipa|file.apk|bs://ID|custom_id:ID> --journeys a,b [--repeat 5] --device "iPhone 15 Pro-17" [--platform ios|android] [--driver maestro|appium]
|
|
11343
12886
|
[--tunnel-for localhost:PORT ...] [--env KEY=VALUE ...] (values may use {tunnel:PORT} {run_id} {out} {tokens_js} {run_set_id})
|
|
11344
12887
|
maestro: --suite-dir DIR --execute FLOW [--tokens-path flows/keepsnappy/tokens.js] [--maestro-version V]
|
|
11345
|
-
appium: --journey-module FILE | --driver-script FILE [--driver-arg ARG ...]
|
|
12888
|
+
appium: --journey-module FILE | --driver-script FILE [--driver-arg ARG ...] (iOS and Android)
|
|
12889
|
+
[--timeout-minutes 80] [--kill-after-seconds 120] (Appium: SIGTERM at timeout, then SIGKILL after grace)
|
|
11346
12890
|
[--branch NAME --commit SHA] [--wait [--fail-on-worse]] [--out DIR] [--dry-run] (open + tunnel + device + close; docs/plans/54a)
|
|
11347
12891
|
extract <out-dir> --ri-capture observations.ndjson [--mem-window NAME]
|
|
11348
12892
|
aa <out-dir> [--reps N] [--boot N]
|
|
@@ -11380,73 +12924,80 @@ async function main2(argv) {
|
|
|
11380
12924
|
if (cmd && LINK_COMMANDS.has(cmd))
|
|
11381
12925
|
return linksCommand(cmd, rest, { print: console.log });
|
|
11382
12926
|
const deps = nodeDeps();
|
|
11383
|
-
const { values: o, positionals } =
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
12927
|
+
const { values: o, positionals } = (() => {
|
|
12928
|
+
try {
|
|
12929
|
+
return parseArgs4({
|
|
12930
|
+
args: rest,
|
|
12931
|
+
allowPositionals: true,
|
|
12932
|
+
options: {
|
|
12933
|
+
repeat: { type: "string" },
|
|
12934
|
+
arms: { type: "string" },
|
|
12935
|
+
"arm-env": { type: "string", multiple: true },
|
|
12936
|
+
platform: { type: "string" },
|
|
12937
|
+
device: { type: "string" },
|
|
12938
|
+
out: { type: "string" },
|
|
12939
|
+
"bundle-id": { type: "string" },
|
|
12940
|
+
timeout: { type: "string" },
|
|
12941
|
+
reps: { type: "string" },
|
|
12942
|
+
boot: { type: "string" },
|
|
12943
|
+
"ri-capture": { type: "string" },
|
|
12944
|
+
"mem-window": { type: "string" },
|
|
12945
|
+
"clock-offset-ms": { type: "string" },
|
|
12946
|
+
"no-markers": { type: "boolean" },
|
|
12947
|
+
config: { type: "string" },
|
|
12948
|
+
upload: { type: "boolean" },
|
|
12949
|
+
"ingest-url": { type: "string" },
|
|
12950
|
+
commit: { type: "string" },
|
|
12951
|
+
branch: { type: "string" },
|
|
12952
|
+
"base-commit": { type: "string" },
|
|
12953
|
+
wait: { type: "boolean" },
|
|
12954
|
+
"fail-on-worse": { type: "boolean" },
|
|
12955
|
+
scenario: { type: "string" },
|
|
12956
|
+
"device-model": { type: "string" },
|
|
12957
|
+
"device-os": { type: "string" },
|
|
12958
|
+
"device-id": { type: "string" },
|
|
12959
|
+
replay: { type: "string", multiple: true },
|
|
12960
|
+
api: { type: "string" },
|
|
12961
|
+
"key-env": { type: "string" },
|
|
12962
|
+
"device-provider": { type: "string" },
|
|
12963
|
+
journeys: { type: "string" },
|
|
12964
|
+
"tokens-js": { type: "string" },
|
|
12965
|
+
state: { type: "string" },
|
|
12966
|
+
"ttl-seconds": { type: "string" },
|
|
12967
|
+
"bs-build": { type: "string" },
|
|
12968
|
+
"settle-seconds": { type: "string" },
|
|
12969
|
+
"max-wait-seconds": { type: "string" },
|
|
12970
|
+
flows: { type: "string" },
|
|
12971
|
+
"completion-marker": { type: "boolean" },
|
|
12972
|
+
"driver-version": { type: "string" },
|
|
12973
|
+
"app-build-id": { type: "string" },
|
|
12974
|
+
"harness-version": { type: "string" },
|
|
12975
|
+
"journey-version": { type: "string" },
|
|
12976
|
+
"flag-set": { type: "string" },
|
|
12977
|
+
app: { type: "string" },
|
|
12978
|
+
driver: { type: "string" },
|
|
12979
|
+
"tunnel-for": { type: "string", multiple: true },
|
|
12980
|
+
env: { type: "string", multiple: true },
|
|
12981
|
+
"driver-script": { type: "string" },
|
|
12982
|
+
"journey-module": { type: "string" },
|
|
12983
|
+
"driver-arg": { type: "string", multiple: true },
|
|
12984
|
+
"suite-dir": { type: "string" },
|
|
12985
|
+
execute: { type: "string" },
|
|
12986
|
+
"tokens-path": { type: "string" },
|
|
12987
|
+
"maestro-version": { type: "string" },
|
|
12988
|
+
"timeout-minutes": { type: "string" },
|
|
12989
|
+
"kill-after-seconds": { type: "string" },
|
|
12990
|
+
"poll-seconds": { type: "string" },
|
|
12991
|
+
"custom-id": { type: "string" },
|
|
12992
|
+
"run-id": { type: "string" },
|
|
12993
|
+
"dry-run": { type: "boolean" },
|
|
12994
|
+
"sdk-args": { type: "string" }
|
|
12995
|
+
}
|
|
12996
|
+
});
|
|
12997
|
+
} catch (e) {
|
|
12998
|
+
throw new UsageError([e.message, "keepsnappy --help lists the commands and their flags"]);
|
|
11448
12999
|
}
|
|
11449
|
-
});
|
|
13000
|
+
})();
|
|
11450
13001
|
if (cmd === "doctor") {
|
|
11451
13002
|
console.log(formatChecks(await doctor(deps.spawner, process.version)));
|
|
11452
13003
|
return 0;
|
|
@@ -11466,16 +13017,16 @@ async function main2(argv) {
|
|
|
11466
13017
|
if (cmd === "measure") {
|
|
11467
13018
|
const repeat = Number(o.repeat);
|
|
11468
13019
|
if (!Number.isInteger(repeat) || repeat < 1)
|
|
11469
|
-
throw new
|
|
13020
|
+
throw new ConfigError(["--repeat N (>= 1) is required"]);
|
|
11470
13021
|
const platform = o.platform ?? "ios-sim";
|
|
11471
13022
|
if (platform !== "ios-sim" && platform !== "ios-device")
|
|
11472
|
-
throw new
|
|
13023
|
+
throw new ConfigError([`unknown --platform ${platform}`]);
|
|
11473
13024
|
if (platform === "ios-device" && !o.device)
|
|
11474
|
-
throw new
|
|
13025
|
+
throw new ConfigError(["--device <udid> is required for ios-device"]);
|
|
11475
13026
|
const arms = parseArmEnv(o["arm-env"], parseArms(o.arms));
|
|
11476
|
-
const out =
|
|
13027
|
+
const out = resolve8(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
11477
13028
|
const udid = o.device ?? "booted";
|
|
11478
|
-
const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid,
|
|
13029
|
+
const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve8(out, ".scratch"));
|
|
11479
13030
|
const installer = platform === "ios-sim" ? new IosSimulatorInstaller(deps, udid) : new IosDeviceInstaller(deps, udid);
|
|
11480
13031
|
const bundleId = await bundleIdOf(deps, arms[0]?.target, o["bundle-id"]);
|
|
11481
13032
|
let markers;
|
|
@@ -11494,7 +13045,7 @@ async function main2(argv) {
|
|
|
11494
13045
|
outDir: out,
|
|
11495
13046
|
testCommand: positionals,
|
|
11496
13047
|
bundleId,
|
|
11497
|
-
...o["ri-capture"] ? { ri: new FileRiSource(deps,
|
|
13048
|
+
...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve8(o["ri-capture"])) } : {},
|
|
11498
13049
|
...o["mem-window"] ? { memWindow: o["mem-window"] } : {},
|
|
11499
13050
|
...o.timeout ? { testTimeoutMs: Number(o.timeout) * 1000 } : {}
|
|
11500
13051
|
});
|
|
@@ -11505,18 +13056,18 @@ async function main2(argv) {
|
|
|
11505
13056
|
if (cmd === "extract") {
|
|
11506
13057
|
const dir = positionals[0];
|
|
11507
13058
|
if (!dir)
|
|
11508
|
-
throw new
|
|
11509
|
-
const ri = o["ri-capture"] ? new FileRiSource(deps,
|
|
11510
|
-
const rows = await extract(deps,
|
|
11511
|
-
console.log(`${rows.length} rows -> ${
|
|
13059
|
+
throw new ConfigError(["extract needs <out-dir>"]);
|
|
13060
|
+
const ri = o["ri-capture"] ? new FileRiSource(deps, resolve8(o["ri-capture"])) : undefined;
|
|
13061
|
+
const rows = await extract(deps, resolve8(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
|
|
13062
|
+
console.log(`${rows.length} rows -> ${resolve8(dir, "metrics.csv")}`);
|
|
11512
13063
|
return 0;
|
|
11513
13064
|
}
|
|
11514
13065
|
if (cmd === "aa") {
|
|
11515
13066
|
const dir = positionals[0];
|
|
11516
13067
|
if (!dir)
|
|
11517
|
-
throw new
|
|
13068
|
+
throw new ConfigError(["aa needs <out-dir>"]);
|
|
11518
13069
|
const config = { ...o.reps ? { reps: Number(o.reps) } : {}, ...o.boot ? { nBoot: Number(o.boot) } : {} };
|
|
11519
|
-
console.log((await aa(deps,
|
|
13070
|
+
console.log((await aa(deps, resolve8(dir), { config })).table);
|
|
11520
13071
|
return 0;
|
|
11521
13072
|
}
|
|
11522
13073
|
console.error(USAGE);
|