@gpc-cli/core 0.9.21 → 0.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/index.d.ts +51 -1
- package/dist/index.js +345 -56
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -242,11 +242,29 @@ function buildTestCase(item, commandName, index = 0) {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
const record = item;
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
const CANDIDATE_KEYS = [
|
|
246
|
+
"name",
|
|
247
|
+
"title",
|
|
248
|
+
"sku",
|
|
249
|
+
"id",
|
|
250
|
+
"reviewId",
|
|
251
|
+
"productId",
|
|
252
|
+
"packageName",
|
|
253
|
+
"track",
|
|
254
|
+
"trackId",
|
|
255
|
+
"versionCode",
|
|
256
|
+
"region",
|
|
257
|
+
"languageCode"
|
|
258
|
+
];
|
|
259
|
+
let resolvedName = `item-${index + 1}`;
|
|
260
|
+
for (const key of CANDIDATE_KEYS) {
|
|
261
|
+
const val = record[key];
|
|
262
|
+
if (val != null && val !== "" && val !== "-") {
|
|
263
|
+
resolvedName = String(val);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const name = escapeXml(resolvedName);
|
|
250
268
|
const classname = `gpc.${escapeXml(commandName)}`;
|
|
251
269
|
const breached = record["breached"];
|
|
252
270
|
if (breached === true) {
|
|
@@ -1329,8 +1347,8 @@ var ALL_IMAGE_TYPES = [
|
|
|
1329
1347
|
"tvBanner"
|
|
1330
1348
|
];
|
|
1331
1349
|
async function exportImages(client, packageName, dir, options) {
|
|
1332
|
-
const { mkdir:
|
|
1333
|
-
const { join:
|
|
1350
|
+
const { mkdir: mkdir6, writeFile: writeFile8 } = await import("fs/promises");
|
|
1351
|
+
const { join: join8 } = await import("path");
|
|
1334
1352
|
const edit = await client.edits.insert(packageName);
|
|
1335
1353
|
try {
|
|
1336
1354
|
let languages;
|
|
@@ -1361,12 +1379,12 @@ async function exportImages(client, packageName, dir, options) {
|
|
|
1361
1379
|
const batch = tasks.slice(i, i + concurrency);
|
|
1362
1380
|
const results = await Promise.all(
|
|
1363
1381
|
batch.map(async (task) => {
|
|
1364
|
-
const dirPath =
|
|
1365
|
-
await
|
|
1382
|
+
const dirPath = join8(dir, task.language, task.imageType);
|
|
1383
|
+
await mkdir6(dirPath, { recursive: true });
|
|
1366
1384
|
const response = await fetch(task.url);
|
|
1367
1385
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1368
|
-
const filePath =
|
|
1369
|
-
await
|
|
1386
|
+
const filePath = join8(dirPath, `${task.index}.png`);
|
|
1387
|
+
await writeFile8(filePath, buffer);
|
|
1370
1388
|
return buffer.length;
|
|
1371
1389
|
})
|
|
1372
1390
|
);
|
|
@@ -1404,6 +1422,7 @@ async function updateAppDetails(client, packageName, details) {
|
|
|
1404
1422
|
// src/commands/migrate.ts
|
|
1405
1423
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
|
|
1406
1424
|
import { join as join2 } from "path";
|
|
1425
|
+
var COMPLEX_RUBY_RE = /\b(begin|rescue|ensure|if |unless |case |while |until |for )\b/;
|
|
1407
1426
|
async function fileExists(path) {
|
|
1408
1427
|
try {
|
|
1409
1428
|
await access(path);
|
|
@@ -1419,7 +1438,8 @@ async function detectFastlane(cwd) {
|
|
|
1419
1438
|
hasMetadata: false,
|
|
1420
1439
|
hasGemfile: false,
|
|
1421
1440
|
lanes: [],
|
|
1422
|
-
metadataLanguages: []
|
|
1441
|
+
metadataLanguages: [],
|
|
1442
|
+
parseWarnings: []
|
|
1423
1443
|
};
|
|
1424
1444
|
const fastlaneDir = join2(cwd, "fastlane");
|
|
1425
1445
|
const hasFastlaneDir = await fileExists(fastlaneDir);
|
|
@@ -1435,13 +1455,20 @@ async function detectFastlane(cwd) {
|
|
|
1435
1455
|
const entries = await readdir2(metadataDir, { withFileTypes: true });
|
|
1436
1456
|
result.metadataLanguages = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1437
1457
|
} catch {
|
|
1458
|
+
result.parseWarnings.push("Could not read metadata directory \u2014 check permissions");
|
|
1438
1459
|
}
|
|
1439
1460
|
}
|
|
1440
1461
|
if (result.hasFastfile) {
|
|
1441
1462
|
try {
|
|
1442
1463
|
const content = await readFile3(fastfilePath, "utf-8");
|
|
1443
1464
|
result.lanes = parseFastfile(content);
|
|
1465
|
+
if (COMPLEX_RUBY_RE.test(content)) {
|
|
1466
|
+
result.parseWarnings.push(
|
|
1467
|
+
"Fastfile contains complex Ruby constructs (begin/rescue/if/unless/case). Lane detection may be incomplete \u2014 review MIGRATION.md and adjust manually."
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1444
1470
|
} catch {
|
|
1471
|
+
result.parseWarnings.push("Could not read Fastfile \u2014 check permissions");
|
|
1445
1472
|
}
|
|
1446
1473
|
}
|
|
1447
1474
|
if (result.hasAppfile) {
|
|
@@ -1451,6 +1478,7 @@ async function detectFastlane(cwd) {
|
|
|
1451
1478
|
result.packageName = parsed.packageName;
|
|
1452
1479
|
result.jsonKeyPath = parsed.jsonKeyPath;
|
|
1453
1480
|
} catch {
|
|
1481
|
+
result.parseWarnings.push("Could not read Appfile \u2014 check permissions");
|
|
1454
1482
|
}
|
|
1455
1483
|
}
|
|
1456
1484
|
return result;
|
|
@@ -1481,8 +1509,9 @@ function mapLaneToGpc(name, actions, body) {
|
|
|
1481
1509
|
const trackMatch = body.match(/track\s*:\s*["'](\w+)["']/);
|
|
1482
1510
|
const rolloutMatch = body.match(/rollout\s*:\s*["']?([\d.]+)["']?/);
|
|
1483
1511
|
if (rolloutMatch) {
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1512
|
+
const raw = parseFloat(rolloutMatch[1] ?? "0");
|
|
1513
|
+
const pct = raw > 1 ? Math.round(raw) : Math.round(raw * 100);
|
|
1514
|
+
return `gpc releases upload --rollout ${pct}${trackMatch ? ` --track ${trackMatch[1]}` : ""}`;
|
|
1486
1515
|
}
|
|
1487
1516
|
if (trackMatch) {
|
|
1488
1517
|
return `gpc releases upload --track ${trackMatch[1]}`;
|
|
@@ -1512,7 +1541,7 @@ function parseAppfile(content) {
|
|
|
1512
1541
|
function generateMigrationPlan(detection) {
|
|
1513
1542
|
const config = {};
|
|
1514
1543
|
const checklist = [];
|
|
1515
|
-
const warnings = [];
|
|
1544
|
+
const warnings = [...detection.parseWarnings];
|
|
1516
1545
|
if (detection.packageName) {
|
|
1517
1546
|
config["app"] = detection.packageName;
|
|
1518
1547
|
} else {
|
|
@@ -1521,44 +1550,61 @@ function generateMigrationPlan(detection) {
|
|
|
1521
1550
|
if (detection.jsonKeyPath) {
|
|
1522
1551
|
config["auth"] = { serviceAccount: detection.jsonKeyPath };
|
|
1523
1552
|
} else {
|
|
1524
|
-
checklist.push("Configure authentication: gpc auth
|
|
1553
|
+
checklist.push("Configure authentication: gpc auth login");
|
|
1525
1554
|
}
|
|
1526
1555
|
for (const lane of detection.lanes) {
|
|
1527
1556
|
if (lane.gpcEquivalent) {
|
|
1528
|
-
checklist.push(
|
|
1557
|
+
checklist.push(
|
|
1558
|
+
`Replace Fastlane lane "${lane.name}" with: ${lane.gpcEquivalent} <your.aab>`
|
|
1559
|
+
);
|
|
1529
1560
|
}
|
|
1530
1561
|
if (lane.actions.includes("capture_android_screenshots")) {
|
|
1531
1562
|
warnings.push(
|
|
1532
|
-
`Lane "${lane.name}" uses capture_android_screenshots which has no GPC equivalent.
|
|
1563
|
+
`Lane "${lane.name}" uses capture_android_screenshots which has no GPC equivalent. Use a separate screenshot tool or check gpc plugins list for community plugins.`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (lane.actions.length === 0 || lane.gpcEquivalent === void 0 && !lane.actions.includes("capture_android_screenshots")) {
|
|
1567
|
+
warnings.push(
|
|
1568
|
+
`Lane "${lane.name}" has no automatic GPC equivalent. Check \`gpc plugins list\` or the plugin SDK docs to build a custom command.`
|
|
1533
1569
|
);
|
|
1534
1570
|
}
|
|
1535
1571
|
}
|
|
1536
1572
|
if (detection.hasMetadata && detection.metadataLanguages.length > 0) {
|
|
1573
|
+
const langs = detection.metadataLanguages.slice(0, 3).join(", ");
|
|
1574
|
+
const more = detection.metadataLanguages.length > 3 ? ` (+${detection.metadataLanguages.length - 3} more)` : "";
|
|
1537
1575
|
checklist.push(
|
|
1538
|
-
`
|
|
1576
|
+
`Pull current metadata for ${detection.metadataLanguages.length} language(s) (${langs}${more}): gpc listings pull --dir fastlane/metadata/android`
|
|
1577
|
+
);
|
|
1578
|
+
checklist.push(
|
|
1579
|
+
"Review pulled metadata, then push back: gpc listings push --dir fastlane/metadata/android"
|
|
1539
1580
|
);
|
|
1540
|
-
checklist.push("Review and push metadata: gpc listings push --dir metadata");
|
|
1541
1581
|
}
|
|
1542
1582
|
checklist.push("Run gpc doctor to verify your setup");
|
|
1543
|
-
checklist.push("Test with --dry-run before making real changes");
|
|
1583
|
+
checklist.push("Test each command with --dry-run before making real changes");
|
|
1544
1584
|
if (detection.hasGemfile) {
|
|
1545
1585
|
checklist.push("Remove Fastlane from your Gemfile once migration is complete");
|
|
1546
1586
|
}
|
|
1547
|
-
if (detection.lanes.some(
|
|
1548
|
-
|
|
1587
|
+
if (detection.lanes.some(
|
|
1588
|
+
(l) => l.actions.includes("supply") || l.actions.includes("upload_to_play_store")
|
|
1589
|
+
)) {
|
|
1590
|
+
checklist.push("Update CI/CD pipelines to call gpc commands instead of Fastlane lanes");
|
|
1549
1591
|
}
|
|
1550
1592
|
return { config, checklist, warnings };
|
|
1551
1593
|
}
|
|
1552
1594
|
async function writeMigrationOutput(result, dir) {
|
|
1553
1595
|
await mkdir2(dir, { recursive: true });
|
|
1554
1596
|
const files = [];
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1597
|
+
if (Object.keys(result.config).length > 0) {
|
|
1598
|
+
const configPath = join2(dir, ".gpcrc.json");
|
|
1599
|
+
await writeFile2(configPath, JSON.stringify(result.config, null, 2) + "\n", "utf-8");
|
|
1600
|
+
files.push(configPath);
|
|
1601
|
+
}
|
|
1558
1602
|
const migrationPath = join2(dir, "MIGRATION.md");
|
|
1559
1603
|
const lines = [
|
|
1560
1604
|
"# Fastlane to GPC Migration",
|
|
1561
1605
|
"",
|
|
1606
|
+
"Generated by `gpc migrate fastlane`. Review and adjust before applying.",
|
|
1607
|
+
"",
|
|
1562
1608
|
"## Migration Checklist",
|
|
1563
1609
|
""
|
|
1564
1610
|
];
|
|
@@ -1570,7 +1616,7 @@ async function writeMigrationOutput(result, dir) {
|
|
|
1570
1616
|
lines.push("## Warnings");
|
|
1571
1617
|
lines.push("");
|
|
1572
1618
|
for (const warning of result.warnings) {
|
|
1573
|
-
lines.push(
|
|
1619
|
+
lines.push(`> \u26A0 ${warning}`);
|
|
1574
1620
|
}
|
|
1575
1621
|
}
|
|
1576
1622
|
lines.push("");
|
|
@@ -1581,8 +1627,11 @@ async function writeMigrationOutput(result, dir) {
|
|
|
1581
1627
|
lines.push("| `fastlane supply` | `gpc releases upload` / `gpc listings push` |");
|
|
1582
1628
|
lines.push("| `upload_to_play_store` | `gpc releases upload` |");
|
|
1583
1629
|
lines.push('| `supply(track: "internal")` | `gpc releases upload --track internal` |');
|
|
1584
|
-
lines.push('| `supply(rollout: "0.1")` | `gpc releases
|
|
1585
|
-
lines.push("| `
|
|
1630
|
+
lines.push('| `supply(rollout: "0.1")` | `gpc releases upload --rollout 10` |');
|
|
1631
|
+
lines.push("| `supply(skip_upload_aab: true)` | `gpc listings push` |");
|
|
1632
|
+
lines.push("| `capture_android_screenshots` | No equivalent \u2014 use separate tool |");
|
|
1633
|
+
lines.push("");
|
|
1634
|
+
lines.push("See the full migration guide: https://yasserstudio.github.io/gpc/migration/from-fastlane");
|
|
1586
1635
|
lines.push("");
|
|
1587
1636
|
await writeFile2(migrationPath, lines.join("\n"), "utf-8");
|
|
1588
1637
|
files.push(migrationPath);
|
|
@@ -1729,12 +1778,16 @@ var STANDARD_TRACKS = /* @__PURE__ */ new Set([
|
|
|
1729
1778
|
var TRACK_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_:-]*$/;
|
|
1730
1779
|
async function validatePreSubmission(options) {
|
|
1731
1780
|
const checks = [];
|
|
1781
|
+
const resultWarnings = [];
|
|
1732
1782
|
const fileResult = await validateUploadFile(options.filePath);
|
|
1733
1783
|
checks.push({
|
|
1734
1784
|
name: "file",
|
|
1735
1785
|
passed: fileResult.valid,
|
|
1736
|
-
message: fileResult.valid ? `Valid ${fileResult.fileType}
|
|
1786
|
+
message: fileResult.valid ? `Valid ${fileResult.fileType.toUpperCase()} (${formatSize3(fileResult.sizeBytes)})` : fileResult.errors.join("; ")
|
|
1737
1787
|
});
|
|
1788
|
+
for (const w of fileResult.warnings) {
|
|
1789
|
+
resultWarnings.push(w);
|
|
1790
|
+
}
|
|
1738
1791
|
if (options.mappingFile) {
|
|
1739
1792
|
try {
|
|
1740
1793
|
const stats = await stat5(options.mappingFile);
|
|
@@ -1786,7 +1839,8 @@ async function validatePreSubmission(options) {
|
|
|
1786
1839
|
}
|
|
1787
1840
|
return {
|
|
1788
1841
|
valid: checks.every((c) => c.passed),
|
|
1789
|
-
checks
|
|
1842
|
+
checks,
|
|
1843
|
+
warnings: resultWarnings
|
|
1790
1844
|
};
|
|
1791
1845
|
}
|
|
1792
1846
|
function formatSize3(bytes) {
|
|
@@ -1955,23 +2009,22 @@ var METRIC_SET_METRICS = {
|
|
|
1955
2009
|
function buildQuery(metricSet, options) {
|
|
1956
2010
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
1957
2011
|
const days = options?.days ?? 30;
|
|
1958
|
-
const
|
|
1959
|
-
end
|
|
1960
|
-
const start = new Date(
|
|
1961
|
-
start.setDate(start.getDate() - days);
|
|
2012
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2013
|
+
const end = new Date(Date.now() - DAY_MS);
|
|
2014
|
+
const start = new Date(Date.now() - DAY_MS - days * DAY_MS);
|
|
1962
2015
|
const query = {
|
|
1963
2016
|
metrics,
|
|
1964
2017
|
timelineSpec: {
|
|
1965
2018
|
aggregationPeriod: options?.aggregation ?? "DAILY",
|
|
1966
2019
|
startTime: {
|
|
1967
|
-
year: start.
|
|
1968
|
-
month: start.
|
|
1969
|
-
day: start.
|
|
2020
|
+
year: start.getUTCFullYear(),
|
|
2021
|
+
month: start.getUTCMonth() + 1,
|
|
2022
|
+
day: start.getUTCDate()
|
|
1970
2023
|
},
|
|
1971
2024
|
endTime: {
|
|
1972
|
-
year: end.
|
|
1973
|
-
month: end.
|
|
1974
|
-
day: end.
|
|
2025
|
+
year: end.getUTCFullYear(),
|
|
2026
|
+
month: end.getUTCMonth() + 1,
|
|
2027
|
+
day: end.getUTCDate()
|
|
1975
2028
|
}
|
|
1976
2029
|
}
|
|
1977
2030
|
};
|
|
@@ -2036,22 +2089,25 @@ async function searchVitalsErrors(reporting, packageName, options) {
|
|
|
2036
2089
|
return reporting.searchErrorIssues(packageName, options?.filter, options?.maxResults);
|
|
2037
2090
|
}
|
|
2038
2091
|
async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
const
|
|
2042
|
-
const
|
|
2043
|
-
currentStart
|
|
2044
|
-
const previousEnd = new Date(
|
|
2045
|
-
|
|
2046
|
-
const previousStart = new Date(previousEnd);
|
|
2047
|
-
previousStart.setDate(previousStart.getDate() - days);
|
|
2092
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2093
|
+
const nowMs = Date.now();
|
|
2094
|
+
const baseMs = nowMs - 2 * DAY_MS;
|
|
2095
|
+
const currentEnd = new Date(baseMs);
|
|
2096
|
+
const currentStart = new Date(baseMs - days * DAY_MS);
|
|
2097
|
+
const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
|
|
2098
|
+
const previousStart = new Date(baseMs - days * DAY_MS - DAY_MS - days * DAY_MS);
|
|
2048
2099
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
2100
|
+
const toApiDate2 = (d) => ({
|
|
2101
|
+
year: d.getUTCFullYear(),
|
|
2102
|
+
month: d.getUTCMonth() + 1,
|
|
2103
|
+
day: d.getUTCDate()
|
|
2104
|
+
});
|
|
2049
2105
|
const makeQuery = (start, end) => ({
|
|
2050
2106
|
metrics,
|
|
2051
2107
|
timelineSpec: {
|
|
2052
2108
|
aggregationPeriod: "DAILY",
|
|
2053
|
-
startTime:
|
|
2054
|
-
endTime:
|
|
2109
|
+
startTime: toApiDate2(start),
|
|
2110
|
+
endTime: toApiDate2(end)
|
|
2055
2111
|
}
|
|
2056
2112
|
});
|
|
2057
2113
|
const [currentResult, previousResult] = await Promise.all([
|
|
@@ -2798,10 +2854,12 @@ function formatNotes(commits, maxLength) {
|
|
|
2798
2854
|
${bullets}`);
|
|
2799
2855
|
}
|
|
2800
2856
|
let text = sections.join("\n\n");
|
|
2857
|
+
let truncated = false;
|
|
2801
2858
|
if (text.length > maxLength) {
|
|
2802
2859
|
text = text.slice(0, maxLength - 3) + "...";
|
|
2860
|
+
truncated = true;
|
|
2803
2861
|
}
|
|
2804
|
-
return text;
|
|
2862
|
+
return { text, truncated };
|
|
2805
2863
|
}
|
|
2806
2864
|
async function gitExec(args) {
|
|
2807
2865
|
try {
|
|
@@ -2855,17 +2913,19 @@ async function generateNotesFromGit(options) {
|
|
|
2855
2913
|
language,
|
|
2856
2914
|
text: "No changes since last release.",
|
|
2857
2915
|
commitCount: 0,
|
|
2858
|
-
since
|
|
2916
|
+
since,
|
|
2917
|
+
truncated: false
|
|
2859
2918
|
};
|
|
2860
2919
|
}
|
|
2861
2920
|
const subjects = logOutput.split("\n").filter((line) => line.length > 0);
|
|
2862
2921
|
const commits = subjects.map(parseConventionalCommit);
|
|
2863
|
-
const text = formatNotes(commits, maxLength);
|
|
2922
|
+
const { text, truncated } = formatNotes(commits, maxLength);
|
|
2864
2923
|
return {
|
|
2865
2924
|
language,
|
|
2866
2925
|
text,
|
|
2867
2926
|
commitCount: subjects.length,
|
|
2868
|
-
since
|
|
2927
|
+
since,
|
|
2928
|
+
truncated
|
|
2869
2929
|
};
|
|
2870
2930
|
}
|
|
2871
2931
|
|
|
@@ -3912,6 +3972,230 @@ function compareBundles(before, after) {
|
|
|
3912
3972
|
categoryDeltas
|
|
3913
3973
|
};
|
|
3914
3974
|
}
|
|
3975
|
+
|
|
3976
|
+
// src/commands/status.ts
|
|
3977
|
+
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
3978
|
+
import { join as join7 } from "path";
|
|
3979
|
+
import { getCacheDir } from "@gpc-cli/config";
|
|
3980
|
+
var DEFAULT_TTL_SECONDS = 3600;
|
|
3981
|
+
function cacheFilePath(packageName) {
|
|
3982
|
+
return join7(getCacheDir(), `status-${packageName}.json`);
|
|
3983
|
+
}
|
|
3984
|
+
async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
3985
|
+
try {
|
|
3986
|
+
const raw = await readFile10(cacheFilePath(packageName), "utf-8");
|
|
3987
|
+
const entry = JSON.parse(raw);
|
|
3988
|
+
const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
|
|
3989
|
+
if (age > (entry.ttl ?? ttlSeconds)) return null;
|
|
3990
|
+
return { ...entry.data, cached: true };
|
|
3991
|
+
} catch {
|
|
3992
|
+
return null;
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
3996
|
+
try {
|
|
3997
|
+
const dir = getCacheDir();
|
|
3998
|
+
await mkdir5(dir, { recursive: true });
|
|
3999
|
+
const entry = {
|
|
4000
|
+
fetchedAt: data.fetchedAt,
|
|
4001
|
+
ttl: ttlSeconds,
|
|
4002
|
+
data
|
|
4003
|
+
};
|
|
4004
|
+
await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 });
|
|
4005
|
+
} catch {
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
var METRIC_SET_METRICS2 = {
|
|
4009
|
+
crashRateMetricSet: ["crashRate", "userPerceivedCrashRate", "distinctUsers"],
|
|
4010
|
+
anrRateMetricSet: ["anrRate", "userPerceivedAnrRate", "distinctUsers"],
|
|
4011
|
+
slowStartRateMetricSet: ["slowStartRate", "distinctUsers"],
|
|
4012
|
+
slowRenderingRateMetricSet: ["slowRenderingRate", "distinctUsers"]
|
|
4013
|
+
};
|
|
4014
|
+
var DEFAULT_THRESHOLDS = {
|
|
4015
|
+
crashRate: 0.02,
|
|
4016
|
+
anrRate: 0.01,
|
|
4017
|
+
slowStartRate: 0.05,
|
|
4018
|
+
slowRenderingRate: 0.1
|
|
4019
|
+
};
|
|
4020
|
+
var WARN_MARGIN = 0.2;
|
|
4021
|
+
function toApiDate(d) {
|
|
4022
|
+
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
4023
|
+
}
|
|
4024
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
4025
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4026
|
+
const baseMs = Date.now() - 2 * DAY_MS;
|
|
4027
|
+
const end = new Date(baseMs);
|
|
4028
|
+
const start = new Date(baseMs - days * DAY_MS);
|
|
4029
|
+
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
4030
|
+
const query = {
|
|
4031
|
+
metrics,
|
|
4032
|
+
timelineSpec: {
|
|
4033
|
+
aggregationPeriod: "DAILY",
|
|
4034
|
+
startTime: toApiDate(start),
|
|
4035
|
+
endTime: toApiDate(end)
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
const result = await reporting.queryMetricSet(packageName, metricSet, query);
|
|
4039
|
+
if (!result.rows || result.rows.length === 0) return void 0;
|
|
4040
|
+
const values = result.rows.map((row) => {
|
|
4041
|
+
const firstKey = Object.keys(row.metrics)[0];
|
|
4042
|
+
return firstKey ? Number(row.metrics[firstKey]?.decimalValue?.value) : NaN;
|
|
4043
|
+
}).filter((v) => !isNaN(v));
|
|
4044
|
+
if (values.length === 0) return void 0;
|
|
4045
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4046
|
+
}
|
|
4047
|
+
function toVitalMetric(value, threshold) {
|
|
4048
|
+
if (value === void 0) {
|
|
4049
|
+
return { value: void 0, threshold, status: "unknown" };
|
|
4050
|
+
}
|
|
4051
|
+
if (value > threshold) return { value, threshold, status: "breach" };
|
|
4052
|
+
if (value > threshold * (1 - WARN_MARGIN)) return { value, threshold, status: "warn" };
|
|
4053
|
+
return { value, threshold, status: "ok" };
|
|
4054
|
+
}
|
|
4055
|
+
function computeReviewSentiment(reviews, windowDays) {
|
|
4056
|
+
const now = Date.now();
|
|
4057
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4058
|
+
const windowMs = windowDays * DAY_MS;
|
|
4059
|
+
const prevWindowStart = now - 2 * windowMs;
|
|
4060
|
+
const curWindowStart = now - windowMs;
|
|
4061
|
+
const current = reviews.filter((r) => {
|
|
4062
|
+
const uc = r.comments?.[0]?.userComment;
|
|
4063
|
+
if (!uc) return false;
|
|
4064
|
+
const ts = Number(uc.lastModified.seconds) * 1e3;
|
|
4065
|
+
return ts >= curWindowStart;
|
|
4066
|
+
});
|
|
4067
|
+
const previous = reviews.filter((r) => {
|
|
4068
|
+
const uc = r.comments?.[0]?.userComment;
|
|
4069
|
+
if (!uc) return false;
|
|
4070
|
+
const ts = Number(uc.lastModified.seconds) * 1e3;
|
|
4071
|
+
return ts >= prevWindowStart && ts < curWindowStart;
|
|
4072
|
+
});
|
|
4073
|
+
const avgRating = (items) => {
|
|
4074
|
+
const ratings = items.map((r) => r.comments?.[0]?.userComment?.starRating).filter((v) => v !== void 0 && v > 0);
|
|
4075
|
+
if (ratings.length === 0) return void 0;
|
|
4076
|
+
return Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length * 10) / 10;
|
|
4077
|
+
};
|
|
4078
|
+
const positiveCount = current.filter(
|
|
4079
|
+
(r) => (r.comments?.[0]?.userComment?.starRating ?? 0) >= 4
|
|
4080
|
+
).length;
|
|
4081
|
+
const positivePercent = current.length > 0 ? Math.round(positiveCount / current.length * 100) : void 0;
|
|
4082
|
+
return {
|
|
4083
|
+
windowDays,
|
|
4084
|
+
averageRating: avgRating(current),
|
|
4085
|
+
previousAverageRating: avgRating(previous),
|
|
4086
|
+
totalNew: current.length,
|
|
4087
|
+
positivePercent
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
4091
|
+
const days = options.days ?? 7;
|
|
4092
|
+
const thresholds = {
|
|
4093
|
+
crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
|
|
4094
|
+
anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
|
|
4095
|
+
slowStartRate: options.vitalThresholds?.slowStartRate ?? DEFAULT_THRESHOLDS.slowStartRate,
|
|
4096
|
+
slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
|
|
4097
|
+
};
|
|
4098
|
+
const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
|
|
4099
|
+
getReleasesStatus(client, packageName),
|
|
4100
|
+
queryVitalForStatus(reporting, packageName, "crashRateMetricSet", days),
|
|
4101
|
+
queryVitalForStatus(reporting, packageName, "anrRateMetricSet", days),
|
|
4102
|
+
queryVitalForStatus(reporting, packageName, "slowStartRateMetricSet", days),
|
|
4103
|
+
queryVitalForStatus(reporting, packageName, "slowRenderingRateMetricSet", days),
|
|
4104
|
+
listReviews(client, packageName, { maxResults: 500 })
|
|
4105
|
+
]);
|
|
4106
|
+
const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
|
|
4107
|
+
const releases = rawReleases.map((r) => ({
|
|
4108
|
+
track: r.track,
|
|
4109
|
+
versionCode: r.versionCodes[r.versionCodes.length - 1] ?? "\u2014",
|
|
4110
|
+
status: r.status,
|
|
4111
|
+
userFraction: r.userFraction ?? null
|
|
4112
|
+
}));
|
|
4113
|
+
const crashValue = crashesResult.status === "fulfilled" ? crashesResult.value : void 0;
|
|
4114
|
+
const anrValue = anrResult.status === "fulfilled" ? anrResult.value : void 0;
|
|
4115
|
+
const slowStartValue = slowStartResult.status === "fulfilled" ? slowStartResult.value : void 0;
|
|
4116
|
+
const slowRenderValue = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : void 0;
|
|
4117
|
+
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
|
|
4118
|
+
const reviews = computeReviewSentiment(rawReviews, 30);
|
|
4119
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4120
|
+
return {
|
|
4121
|
+
packageName,
|
|
4122
|
+
fetchedAt,
|
|
4123
|
+
cached: false,
|
|
4124
|
+
releases,
|
|
4125
|
+
vitals: {
|
|
4126
|
+
windowDays: days,
|
|
4127
|
+
crashes: toVitalMetric(crashValue, thresholds.crashRate),
|
|
4128
|
+
anr: toVitalMetric(anrValue, thresholds.anrRate),
|
|
4129
|
+
slowStarts: toVitalMetric(slowStartValue, thresholds.slowStartRate),
|
|
4130
|
+
slowRender: toVitalMetric(slowRenderValue, thresholds.slowRenderingRate)
|
|
4131
|
+
},
|
|
4132
|
+
reviews
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
function vitalIndicator(metric) {
|
|
4136
|
+
if (metric.status === "unknown") return "?";
|
|
4137
|
+
if (metric.status === "breach") return "\u2717";
|
|
4138
|
+
if (metric.status === "warn") return "\u26A0";
|
|
4139
|
+
return "\u2713";
|
|
4140
|
+
}
|
|
4141
|
+
function formatVitalValue(metric) {
|
|
4142
|
+
if (metric.value === void 0) return "n/a";
|
|
4143
|
+
return `${(metric.value * 100).toFixed(2)}%`;
|
|
4144
|
+
}
|
|
4145
|
+
function formatFraction(fraction) {
|
|
4146
|
+
if (fraction === null) return "\u2014";
|
|
4147
|
+
return `${Math.round(fraction * 100)}%`;
|
|
4148
|
+
}
|
|
4149
|
+
function formatRating(rating) {
|
|
4150
|
+
if (rating === void 0) return "n/a";
|
|
4151
|
+
return `\u2605 ${rating.toFixed(1)}`;
|
|
4152
|
+
}
|
|
4153
|
+
function formatTrend(current, previous) {
|
|
4154
|
+
if (current === void 0 || previous === void 0) return "";
|
|
4155
|
+
if (current > previous) return ` \u2191 from ${previous.toFixed(1)}`;
|
|
4156
|
+
if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
|
|
4157
|
+
return "";
|
|
4158
|
+
}
|
|
4159
|
+
function formatStatusTable(status) {
|
|
4160
|
+
const lines = [];
|
|
4161
|
+
const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
|
|
4162
|
+
lines.push(`App: ${status.packageName}${cachedLabel}`);
|
|
4163
|
+
lines.push("");
|
|
4164
|
+
lines.push("RELEASES");
|
|
4165
|
+
if (status.releases.length === 0) {
|
|
4166
|
+
lines.push(" No releases found.");
|
|
4167
|
+
} else {
|
|
4168
|
+
const trackW = Math.max(10, ...status.releases.map((r) => r.track.length));
|
|
4169
|
+
const versionW = Math.max(7, ...status.releases.map((r) => r.versionCode.length));
|
|
4170
|
+
const statusW = Math.max(8, ...status.releases.map((r) => r.status.length));
|
|
4171
|
+
for (const r of status.releases) {
|
|
4172
|
+
lines.push(
|
|
4173
|
+
` ${r.track.padEnd(trackW)} ${r.versionCode.padEnd(versionW)} ${r.status.padEnd(statusW)} ${formatFraction(r.userFraction)}`
|
|
4174
|
+
);
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
lines.push("");
|
|
4178
|
+
lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
|
|
4179
|
+
const { crashes, anr, slowStarts, slowRender } = status.vitals;
|
|
4180
|
+
lines.push(
|
|
4181
|
+
` crashes ${formatVitalValue(crashes).padEnd(8)} ${vitalIndicator(crashes)} anr ${formatVitalValue(anr).padEnd(8)} ${vitalIndicator(anr)}`
|
|
4182
|
+
);
|
|
4183
|
+
lines.push(
|
|
4184
|
+
` slow starts ${formatVitalValue(slowStarts).padEnd(8)} ${vitalIndicator(slowStarts)} slow render ${formatVitalValue(slowRender).padEnd(8)} ${vitalIndicator(slowRender)}`
|
|
4185
|
+
);
|
|
4186
|
+
lines.push("");
|
|
4187
|
+
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4188
|
+
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4189
|
+
const trend = formatTrend(averageRating, previousAverageRating);
|
|
4190
|
+
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
4191
|
+
lines.push(
|
|
4192
|
+
` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`
|
|
4193
|
+
);
|
|
4194
|
+
return lines.join("\n");
|
|
4195
|
+
}
|
|
4196
|
+
function statusHasBreach(status) {
|
|
4197
|
+
return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
|
|
4198
|
+
}
|
|
3915
4199
|
export {
|
|
3916
4200
|
ApiError,
|
|
3917
4201
|
ConfigError,
|
|
@@ -3981,9 +4265,11 @@ export {
|
|
|
3981
4265
|
formatJunit,
|
|
3982
4266
|
formatOutput,
|
|
3983
4267
|
formatSlackPayload,
|
|
4268
|
+
formatStatusTable,
|
|
3984
4269
|
generateMigrationPlan,
|
|
3985
4270
|
generateNotesFromGit,
|
|
3986
4271
|
getAppInfo,
|
|
4272
|
+
getAppStatus,
|
|
3987
4273
|
getCountryAvailability,
|
|
3988
4274
|
getDataSafety,
|
|
3989
4275
|
getDeviceTier,
|
|
@@ -4034,6 +4320,7 @@ export {
|
|
|
4034
4320
|
listTracks,
|
|
4035
4321
|
listUsers,
|
|
4036
4322
|
listVoidedPurchases,
|
|
4323
|
+
loadStatusCache,
|
|
4037
4324
|
migratePrices,
|
|
4038
4325
|
parseAppfile,
|
|
4039
4326
|
parseFastfile,
|
|
@@ -4055,11 +4342,13 @@ export {
|
|
|
4055
4342
|
revokeSubscriptionPurchase,
|
|
4056
4343
|
safePath,
|
|
4057
4344
|
safePathWithin,
|
|
4345
|
+
saveStatusCache,
|
|
4058
4346
|
scaffoldPlugin,
|
|
4059
4347
|
searchAuditEvents,
|
|
4060
4348
|
searchVitalsErrors,
|
|
4061
4349
|
sendWebhook,
|
|
4062
4350
|
sortResults,
|
|
4351
|
+
statusHasBreach,
|
|
4063
4352
|
syncInAppProducts,
|
|
4064
4353
|
updateAppDetails,
|
|
4065
4354
|
updateDataSafety,
|