@gpc-cli/core 0.9.21 → 0.9.22
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 +323 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -244,7 +244,7 @@ function buildTestCase(item, commandName, index = 0) {
|
|
|
244
244
|
const record = item;
|
|
245
245
|
const name = escapeXml(
|
|
246
246
|
String(
|
|
247
|
-
record["name"] ?? record["title"] ?? record["sku"] ?? record["id"] ?? record["productId"] ?? record["packageName"] ?? record["trackId"] ?? record["region"] ?? record["languageCode"] ?? `item-${index + 1}`
|
|
247
|
+
record["name"] ?? record["title"] ?? record["sku"] ?? record["id"] ?? record["reviewId"] ?? record["productId"] ?? record["packageName"] ?? record["track"] ?? record["trackId"] ?? record["versionCode"] ?? record["region"] ?? record["languageCode"] ?? `item-${index + 1}`
|
|
248
248
|
)
|
|
249
249
|
);
|
|
250
250
|
const classname = `gpc.${escapeXml(commandName)}`;
|
|
@@ -1329,8 +1329,8 @@ var ALL_IMAGE_TYPES = [
|
|
|
1329
1329
|
"tvBanner"
|
|
1330
1330
|
];
|
|
1331
1331
|
async function exportImages(client, packageName, dir, options) {
|
|
1332
|
-
const { mkdir:
|
|
1333
|
-
const { join:
|
|
1332
|
+
const { mkdir: mkdir6, writeFile: writeFile8 } = await import("fs/promises");
|
|
1333
|
+
const { join: join8 } = await import("path");
|
|
1334
1334
|
const edit = await client.edits.insert(packageName);
|
|
1335
1335
|
try {
|
|
1336
1336
|
let languages;
|
|
@@ -1361,12 +1361,12 @@ async function exportImages(client, packageName, dir, options) {
|
|
|
1361
1361
|
const batch = tasks.slice(i, i + concurrency);
|
|
1362
1362
|
const results = await Promise.all(
|
|
1363
1363
|
batch.map(async (task) => {
|
|
1364
|
-
const dirPath =
|
|
1365
|
-
await
|
|
1364
|
+
const dirPath = join8(dir, task.language, task.imageType);
|
|
1365
|
+
await mkdir6(dirPath, { recursive: true });
|
|
1366
1366
|
const response = await fetch(task.url);
|
|
1367
1367
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1368
|
-
const filePath =
|
|
1369
|
-
await
|
|
1368
|
+
const filePath = join8(dirPath, `${task.index}.png`);
|
|
1369
|
+
await writeFile8(filePath, buffer);
|
|
1370
1370
|
return buffer.length;
|
|
1371
1371
|
})
|
|
1372
1372
|
);
|
|
@@ -1404,6 +1404,7 @@ async function updateAppDetails(client, packageName, details) {
|
|
|
1404
1404
|
// src/commands/migrate.ts
|
|
1405
1405
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
|
|
1406
1406
|
import { join as join2 } from "path";
|
|
1407
|
+
var COMPLEX_RUBY_RE = /\b(begin|rescue|ensure|if |unless |case |while |until |for )\b/;
|
|
1407
1408
|
async function fileExists(path) {
|
|
1408
1409
|
try {
|
|
1409
1410
|
await access(path);
|
|
@@ -1419,7 +1420,8 @@ async function detectFastlane(cwd) {
|
|
|
1419
1420
|
hasMetadata: false,
|
|
1420
1421
|
hasGemfile: false,
|
|
1421
1422
|
lanes: [],
|
|
1422
|
-
metadataLanguages: []
|
|
1423
|
+
metadataLanguages: [],
|
|
1424
|
+
parseWarnings: []
|
|
1423
1425
|
};
|
|
1424
1426
|
const fastlaneDir = join2(cwd, "fastlane");
|
|
1425
1427
|
const hasFastlaneDir = await fileExists(fastlaneDir);
|
|
@@ -1435,13 +1437,20 @@ async function detectFastlane(cwd) {
|
|
|
1435
1437
|
const entries = await readdir2(metadataDir, { withFileTypes: true });
|
|
1436
1438
|
result.metadataLanguages = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1437
1439
|
} catch {
|
|
1440
|
+
result.parseWarnings.push("Could not read metadata directory \u2014 check permissions");
|
|
1438
1441
|
}
|
|
1439
1442
|
}
|
|
1440
1443
|
if (result.hasFastfile) {
|
|
1441
1444
|
try {
|
|
1442
1445
|
const content = await readFile3(fastfilePath, "utf-8");
|
|
1443
1446
|
result.lanes = parseFastfile(content);
|
|
1447
|
+
if (COMPLEX_RUBY_RE.test(content)) {
|
|
1448
|
+
result.parseWarnings.push(
|
|
1449
|
+
"Fastfile contains complex Ruby constructs (begin/rescue/if/unless/case). Lane detection may be incomplete \u2014 review MIGRATION.md and adjust manually."
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1444
1452
|
} catch {
|
|
1453
|
+
result.parseWarnings.push("Could not read Fastfile \u2014 check permissions");
|
|
1445
1454
|
}
|
|
1446
1455
|
}
|
|
1447
1456
|
if (result.hasAppfile) {
|
|
@@ -1451,6 +1460,7 @@ async function detectFastlane(cwd) {
|
|
|
1451
1460
|
result.packageName = parsed.packageName;
|
|
1452
1461
|
result.jsonKeyPath = parsed.jsonKeyPath;
|
|
1453
1462
|
} catch {
|
|
1463
|
+
result.parseWarnings.push("Could not read Appfile \u2014 check permissions");
|
|
1454
1464
|
}
|
|
1455
1465
|
}
|
|
1456
1466
|
return result;
|
|
@@ -1481,8 +1491,9 @@ function mapLaneToGpc(name, actions, body) {
|
|
|
1481
1491
|
const trackMatch = body.match(/track\s*:\s*["'](\w+)["']/);
|
|
1482
1492
|
const rolloutMatch = body.match(/rollout\s*:\s*["']?([\d.]+)["']?/);
|
|
1483
1493
|
if (rolloutMatch) {
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1494
|
+
const raw = parseFloat(rolloutMatch[1] ?? "0");
|
|
1495
|
+
const pct = raw > 1 ? Math.round(raw) : Math.round(raw * 100);
|
|
1496
|
+
return `gpc releases upload --rollout ${pct}${trackMatch ? ` --track ${trackMatch[1]}` : ""}`;
|
|
1486
1497
|
}
|
|
1487
1498
|
if (trackMatch) {
|
|
1488
1499
|
return `gpc releases upload --track ${trackMatch[1]}`;
|
|
@@ -1512,7 +1523,7 @@ function parseAppfile(content) {
|
|
|
1512
1523
|
function generateMigrationPlan(detection) {
|
|
1513
1524
|
const config = {};
|
|
1514
1525
|
const checklist = [];
|
|
1515
|
-
const warnings = [];
|
|
1526
|
+
const warnings = [...detection.parseWarnings];
|
|
1516
1527
|
if (detection.packageName) {
|
|
1517
1528
|
config["app"] = detection.packageName;
|
|
1518
1529
|
} else {
|
|
@@ -1521,44 +1532,61 @@ function generateMigrationPlan(detection) {
|
|
|
1521
1532
|
if (detection.jsonKeyPath) {
|
|
1522
1533
|
config["auth"] = { serviceAccount: detection.jsonKeyPath };
|
|
1523
1534
|
} else {
|
|
1524
|
-
checklist.push("Configure authentication: gpc auth
|
|
1535
|
+
checklist.push("Configure authentication: gpc auth login");
|
|
1525
1536
|
}
|
|
1526
1537
|
for (const lane of detection.lanes) {
|
|
1527
1538
|
if (lane.gpcEquivalent) {
|
|
1528
|
-
checklist.push(
|
|
1539
|
+
checklist.push(
|
|
1540
|
+
`Replace Fastlane lane "${lane.name}" with: ${lane.gpcEquivalent} <your.aab>`
|
|
1541
|
+
);
|
|
1529
1542
|
}
|
|
1530
1543
|
if (lane.actions.includes("capture_android_screenshots")) {
|
|
1531
1544
|
warnings.push(
|
|
1532
|
-
`Lane "${lane.name}" uses capture_android_screenshots which has no GPC equivalent.
|
|
1545
|
+
`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.`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (lane.actions.length === 0 || lane.gpcEquivalent === void 0 && !lane.actions.includes("capture_android_screenshots")) {
|
|
1549
|
+
warnings.push(
|
|
1550
|
+
`Lane "${lane.name}" has no automatic GPC equivalent. Check \`gpc plugins list\` or the plugin SDK docs to build a custom command.`
|
|
1533
1551
|
);
|
|
1534
1552
|
}
|
|
1535
1553
|
}
|
|
1536
1554
|
if (detection.hasMetadata && detection.metadataLanguages.length > 0) {
|
|
1555
|
+
const langs = detection.metadataLanguages.slice(0, 3).join(", ");
|
|
1556
|
+
const more = detection.metadataLanguages.length > 3 ? ` (+${detection.metadataLanguages.length - 3} more)` : "";
|
|
1557
|
+
checklist.push(
|
|
1558
|
+
`Pull current metadata for ${detection.metadataLanguages.length} language(s) (${langs}${more}): gpc listings pull --dir fastlane/metadata/android`
|
|
1559
|
+
);
|
|
1537
1560
|
checklist.push(
|
|
1538
|
-
|
|
1561
|
+
"Review pulled metadata, then push back: gpc listings push --dir fastlane/metadata/android"
|
|
1539
1562
|
);
|
|
1540
|
-
checklist.push("Review and push metadata: gpc listings push --dir metadata");
|
|
1541
1563
|
}
|
|
1542
1564
|
checklist.push("Run gpc doctor to verify your setup");
|
|
1543
|
-
checklist.push("Test with --dry-run before making real changes");
|
|
1565
|
+
checklist.push("Test each command with --dry-run before making real changes");
|
|
1544
1566
|
if (detection.hasGemfile) {
|
|
1545
1567
|
checklist.push("Remove Fastlane from your Gemfile once migration is complete");
|
|
1546
1568
|
}
|
|
1547
|
-
if (detection.lanes.some(
|
|
1548
|
-
|
|
1569
|
+
if (detection.lanes.some(
|
|
1570
|
+
(l) => l.actions.includes("supply") || l.actions.includes("upload_to_play_store")
|
|
1571
|
+
)) {
|
|
1572
|
+
checklist.push("Update CI/CD pipelines to call gpc commands instead of Fastlane lanes");
|
|
1549
1573
|
}
|
|
1550
1574
|
return { config, checklist, warnings };
|
|
1551
1575
|
}
|
|
1552
1576
|
async function writeMigrationOutput(result, dir) {
|
|
1553
1577
|
await mkdir2(dir, { recursive: true });
|
|
1554
1578
|
const files = [];
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1579
|
+
if (Object.keys(result.config).length > 0) {
|
|
1580
|
+
const configPath = join2(dir, ".gpcrc.json");
|
|
1581
|
+
await writeFile2(configPath, JSON.stringify(result.config, null, 2) + "\n", "utf-8");
|
|
1582
|
+
files.push(configPath);
|
|
1583
|
+
}
|
|
1558
1584
|
const migrationPath = join2(dir, "MIGRATION.md");
|
|
1559
1585
|
const lines = [
|
|
1560
1586
|
"# Fastlane to GPC Migration",
|
|
1561
1587
|
"",
|
|
1588
|
+
"Generated by `gpc migrate fastlane`. Review and adjust before applying.",
|
|
1589
|
+
"",
|
|
1562
1590
|
"## Migration Checklist",
|
|
1563
1591
|
""
|
|
1564
1592
|
];
|
|
@@ -1570,7 +1598,7 @@ async function writeMigrationOutput(result, dir) {
|
|
|
1570
1598
|
lines.push("## Warnings");
|
|
1571
1599
|
lines.push("");
|
|
1572
1600
|
for (const warning of result.warnings) {
|
|
1573
|
-
lines.push(
|
|
1601
|
+
lines.push(`> \u26A0 ${warning}`);
|
|
1574
1602
|
}
|
|
1575
1603
|
}
|
|
1576
1604
|
lines.push("");
|
|
@@ -1581,8 +1609,11 @@ async function writeMigrationOutput(result, dir) {
|
|
|
1581
1609
|
lines.push("| `fastlane supply` | `gpc releases upload` / `gpc listings push` |");
|
|
1582
1610
|
lines.push("| `upload_to_play_store` | `gpc releases upload` |");
|
|
1583
1611
|
lines.push('| `supply(track: "internal")` | `gpc releases upload --track internal` |');
|
|
1584
|
-
lines.push('| `supply(rollout: "0.1")` | `gpc releases
|
|
1585
|
-
lines.push("| `
|
|
1612
|
+
lines.push('| `supply(rollout: "0.1")` | `gpc releases upload --rollout 10` |');
|
|
1613
|
+
lines.push("| `supply(skip_upload_aab: true)` | `gpc listings push` |");
|
|
1614
|
+
lines.push("| `capture_android_screenshots` | No equivalent \u2014 use separate tool |");
|
|
1615
|
+
lines.push("");
|
|
1616
|
+
lines.push("See the full migration guide: https://yasserstudio.github.io/gpc/migration/from-fastlane");
|
|
1586
1617
|
lines.push("");
|
|
1587
1618
|
await writeFile2(migrationPath, lines.join("\n"), "utf-8");
|
|
1588
1619
|
files.push(migrationPath);
|
|
@@ -1729,12 +1760,16 @@ var STANDARD_TRACKS = /* @__PURE__ */ new Set([
|
|
|
1729
1760
|
var TRACK_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_:-]*$/;
|
|
1730
1761
|
async function validatePreSubmission(options) {
|
|
1731
1762
|
const checks = [];
|
|
1763
|
+
const resultWarnings = [];
|
|
1732
1764
|
const fileResult = await validateUploadFile(options.filePath);
|
|
1733
1765
|
checks.push({
|
|
1734
1766
|
name: "file",
|
|
1735
1767
|
passed: fileResult.valid,
|
|
1736
|
-
message: fileResult.valid ? `Valid ${fileResult.fileType}
|
|
1768
|
+
message: fileResult.valid ? `Valid ${fileResult.fileType.toUpperCase()} (${formatSize3(fileResult.sizeBytes)})` : fileResult.errors.join("; ")
|
|
1737
1769
|
});
|
|
1770
|
+
for (const w of fileResult.warnings) {
|
|
1771
|
+
resultWarnings.push(w);
|
|
1772
|
+
}
|
|
1738
1773
|
if (options.mappingFile) {
|
|
1739
1774
|
try {
|
|
1740
1775
|
const stats = await stat5(options.mappingFile);
|
|
@@ -1786,7 +1821,8 @@ async function validatePreSubmission(options) {
|
|
|
1786
1821
|
}
|
|
1787
1822
|
return {
|
|
1788
1823
|
valid: checks.every((c) => c.passed),
|
|
1789
|
-
checks
|
|
1824
|
+
checks,
|
|
1825
|
+
warnings: resultWarnings
|
|
1790
1826
|
};
|
|
1791
1827
|
}
|
|
1792
1828
|
function formatSize3(bytes) {
|
|
@@ -1955,23 +1991,22 @@ var METRIC_SET_METRICS = {
|
|
|
1955
1991
|
function buildQuery(metricSet, options) {
|
|
1956
1992
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
1957
1993
|
const days = options?.days ?? 30;
|
|
1958
|
-
const
|
|
1959
|
-
end
|
|
1960
|
-
const start = new Date(
|
|
1961
|
-
start.setDate(start.getDate() - days);
|
|
1994
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1995
|
+
const end = new Date(Date.now() - DAY_MS);
|
|
1996
|
+
const start = new Date(Date.now() - DAY_MS - days * DAY_MS);
|
|
1962
1997
|
const query = {
|
|
1963
1998
|
metrics,
|
|
1964
1999
|
timelineSpec: {
|
|
1965
2000
|
aggregationPeriod: options?.aggregation ?? "DAILY",
|
|
1966
2001
|
startTime: {
|
|
1967
|
-
year: start.
|
|
1968
|
-
month: start.
|
|
1969
|
-
day: start.
|
|
2002
|
+
year: start.getUTCFullYear(),
|
|
2003
|
+
month: start.getUTCMonth() + 1,
|
|
2004
|
+
day: start.getUTCDate()
|
|
1970
2005
|
},
|
|
1971
2006
|
endTime: {
|
|
1972
|
-
year: end.
|
|
1973
|
-
month: end.
|
|
1974
|
-
day: end.
|
|
2007
|
+
year: end.getUTCFullYear(),
|
|
2008
|
+
month: end.getUTCMonth() + 1,
|
|
2009
|
+
day: end.getUTCDate()
|
|
1975
2010
|
}
|
|
1976
2011
|
}
|
|
1977
2012
|
};
|
|
@@ -2036,22 +2071,25 @@ async function searchVitalsErrors(reporting, packageName, options) {
|
|
|
2036
2071
|
return reporting.searchErrorIssues(packageName, options?.filter, options?.maxResults);
|
|
2037
2072
|
}
|
|
2038
2073
|
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);
|
|
2074
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2075
|
+
const nowMs = Date.now();
|
|
2076
|
+
const baseMs = nowMs - 2 * DAY_MS;
|
|
2077
|
+
const currentEnd = new Date(baseMs);
|
|
2078
|
+
const currentStart = new Date(baseMs - days * DAY_MS);
|
|
2079
|
+
const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
|
|
2080
|
+
const previousStart = new Date(baseMs - days * DAY_MS - DAY_MS - days * DAY_MS);
|
|
2048
2081
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
2082
|
+
const toApiDate2 = (d) => ({
|
|
2083
|
+
year: d.getUTCFullYear(),
|
|
2084
|
+
month: d.getUTCMonth() + 1,
|
|
2085
|
+
day: d.getUTCDate()
|
|
2086
|
+
});
|
|
2049
2087
|
const makeQuery = (start, end) => ({
|
|
2050
2088
|
metrics,
|
|
2051
2089
|
timelineSpec: {
|
|
2052
2090
|
aggregationPeriod: "DAILY",
|
|
2053
|
-
startTime:
|
|
2054
|
-
endTime:
|
|
2091
|
+
startTime: toApiDate2(start),
|
|
2092
|
+
endTime: toApiDate2(end)
|
|
2055
2093
|
}
|
|
2056
2094
|
});
|
|
2057
2095
|
const [currentResult, previousResult] = await Promise.all([
|
|
@@ -2798,10 +2836,12 @@ function formatNotes(commits, maxLength) {
|
|
|
2798
2836
|
${bullets}`);
|
|
2799
2837
|
}
|
|
2800
2838
|
let text = sections.join("\n\n");
|
|
2839
|
+
let truncated = false;
|
|
2801
2840
|
if (text.length > maxLength) {
|
|
2802
2841
|
text = text.slice(0, maxLength - 3) + "...";
|
|
2842
|
+
truncated = true;
|
|
2803
2843
|
}
|
|
2804
|
-
return text;
|
|
2844
|
+
return { text, truncated };
|
|
2805
2845
|
}
|
|
2806
2846
|
async function gitExec(args) {
|
|
2807
2847
|
try {
|
|
@@ -2855,17 +2895,19 @@ async function generateNotesFromGit(options) {
|
|
|
2855
2895
|
language,
|
|
2856
2896
|
text: "No changes since last release.",
|
|
2857
2897
|
commitCount: 0,
|
|
2858
|
-
since
|
|
2898
|
+
since,
|
|
2899
|
+
truncated: false
|
|
2859
2900
|
};
|
|
2860
2901
|
}
|
|
2861
2902
|
const subjects = logOutput.split("\n").filter((line) => line.length > 0);
|
|
2862
2903
|
const commits = subjects.map(parseConventionalCommit);
|
|
2863
|
-
const text = formatNotes(commits, maxLength);
|
|
2904
|
+
const { text, truncated } = formatNotes(commits, maxLength);
|
|
2864
2905
|
return {
|
|
2865
2906
|
language,
|
|
2866
2907
|
text,
|
|
2867
2908
|
commitCount: subjects.length,
|
|
2868
|
-
since
|
|
2909
|
+
since,
|
|
2910
|
+
truncated
|
|
2869
2911
|
};
|
|
2870
2912
|
}
|
|
2871
2913
|
|
|
@@ -3912,6 +3954,230 @@ function compareBundles(before, after) {
|
|
|
3912
3954
|
categoryDeltas
|
|
3913
3955
|
};
|
|
3914
3956
|
}
|
|
3957
|
+
|
|
3958
|
+
// src/commands/status.ts
|
|
3959
|
+
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile7 } from "fs/promises";
|
|
3960
|
+
import { join as join7 } from "path";
|
|
3961
|
+
import { getCacheDir } from "@gpc-cli/config";
|
|
3962
|
+
var DEFAULT_TTL_SECONDS = 3600;
|
|
3963
|
+
function cacheFilePath(packageName) {
|
|
3964
|
+
return join7(getCacheDir(), `status-${packageName}.json`);
|
|
3965
|
+
}
|
|
3966
|
+
async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
3967
|
+
try {
|
|
3968
|
+
const raw = await readFile10(cacheFilePath(packageName), "utf-8");
|
|
3969
|
+
const entry = JSON.parse(raw);
|
|
3970
|
+
const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
|
|
3971
|
+
if (age > (entry.ttl ?? ttlSeconds)) return null;
|
|
3972
|
+
return { ...entry.data, cached: true };
|
|
3973
|
+
} catch {
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
async function saveStatusCache(packageName, data, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
3978
|
+
try {
|
|
3979
|
+
const dir = getCacheDir();
|
|
3980
|
+
await mkdir5(dir, { recursive: true });
|
|
3981
|
+
const entry = {
|
|
3982
|
+
fetchedAt: data.fetchedAt,
|
|
3983
|
+
ttl: ttlSeconds,
|
|
3984
|
+
data
|
|
3985
|
+
};
|
|
3986
|
+
await writeFile7(cacheFilePath(packageName), JSON.stringify(entry, null, 2), { encoding: "utf-8", mode: 384 });
|
|
3987
|
+
} catch {
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
var METRIC_SET_METRICS2 = {
|
|
3991
|
+
crashRateMetricSet: ["crashRate", "userPerceivedCrashRate", "distinctUsers"],
|
|
3992
|
+
anrRateMetricSet: ["anrRate", "userPerceivedAnrRate", "distinctUsers"],
|
|
3993
|
+
slowStartRateMetricSet: ["slowStartRate", "distinctUsers"],
|
|
3994
|
+
slowRenderingRateMetricSet: ["slowRenderingRate", "distinctUsers"]
|
|
3995
|
+
};
|
|
3996
|
+
var DEFAULT_THRESHOLDS = {
|
|
3997
|
+
crashRate: 0.02,
|
|
3998
|
+
anrRate: 0.01,
|
|
3999
|
+
slowStartRate: 0.05,
|
|
4000
|
+
slowRenderingRate: 0.1
|
|
4001
|
+
};
|
|
4002
|
+
var WARN_MARGIN = 0.2;
|
|
4003
|
+
function toApiDate(d) {
|
|
4004
|
+
return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
|
4005
|
+
}
|
|
4006
|
+
async function queryVitalForStatus(reporting, packageName, metricSet, days) {
|
|
4007
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4008
|
+
const baseMs = Date.now() - 2 * DAY_MS;
|
|
4009
|
+
const end = new Date(baseMs);
|
|
4010
|
+
const start = new Date(baseMs - days * DAY_MS);
|
|
4011
|
+
const metrics = METRIC_SET_METRICS2[metricSet] ?? ["distinctUsers"];
|
|
4012
|
+
const query = {
|
|
4013
|
+
metrics,
|
|
4014
|
+
timelineSpec: {
|
|
4015
|
+
aggregationPeriod: "DAILY",
|
|
4016
|
+
startTime: toApiDate(start),
|
|
4017
|
+
endTime: toApiDate(end)
|
|
4018
|
+
}
|
|
4019
|
+
};
|
|
4020
|
+
const result = await reporting.queryMetricSet(packageName, metricSet, query);
|
|
4021
|
+
if (!result.rows || result.rows.length === 0) return void 0;
|
|
4022
|
+
const values = result.rows.map((row) => {
|
|
4023
|
+
const firstKey = Object.keys(row.metrics)[0];
|
|
4024
|
+
return firstKey ? Number(row.metrics[firstKey]?.decimalValue?.value) : NaN;
|
|
4025
|
+
}).filter((v) => !isNaN(v));
|
|
4026
|
+
if (values.length === 0) return void 0;
|
|
4027
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4028
|
+
}
|
|
4029
|
+
function toVitalMetric(value, threshold) {
|
|
4030
|
+
if (value === void 0) {
|
|
4031
|
+
return { value: void 0, threshold, status: "unknown" };
|
|
4032
|
+
}
|
|
4033
|
+
if (value > threshold) return { value, threshold, status: "breach" };
|
|
4034
|
+
if (value > threshold * (1 - WARN_MARGIN)) return { value, threshold, status: "warn" };
|
|
4035
|
+
return { value, threshold, status: "ok" };
|
|
4036
|
+
}
|
|
4037
|
+
function computeReviewSentiment(reviews, windowDays) {
|
|
4038
|
+
const now = Date.now();
|
|
4039
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4040
|
+
const windowMs = windowDays * DAY_MS;
|
|
4041
|
+
const prevWindowStart = now - 2 * windowMs;
|
|
4042
|
+
const curWindowStart = now - windowMs;
|
|
4043
|
+
const current = reviews.filter((r) => {
|
|
4044
|
+
const uc = r.comments?.[0]?.userComment;
|
|
4045
|
+
if (!uc) return false;
|
|
4046
|
+
const ts = Number(uc.lastModified.seconds) * 1e3;
|
|
4047
|
+
return ts >= curWindowStart;
|
|
4048
|
+
});
|
|
4049
|
+
const previous = reviews.filter((r) => {
|
|
4050
|
+
const uc = r.comments?.[0]?.userComment;
|
|
4051
|
+
if (!uc) return false;
|
|
4052
|
+
const ts = Number(uc.lastModified.seconds) * 1e3;
|
|
4053
|
+
return ts >= prevWindowStart && ts < curWindowStart;
|
|
4054
|
+
});
|
|
4055
|
+
const avgRating = (items) => {
|
|
4056
|
+
const ratings = items.map((r) => r.comments?.[0]?.userComment?.starRating).filter((v) => v !== void 0 && v > 0);
|
|
4057
|
+
if (ratings.length === 0) return void 0;
|
|
4058
|
+
return Math.round(ratings.reduce((a, b) => a + b, 0) / ratings.length * 10) / 10;
|
|
4059
|
+
};
|
|
4060
|
+
const positiveCount = current.filter(
|
|
4061
|
+
(r) => (r.comments?.[0]?.userComment?.starRating ?? 0) >= 4
|
|
4062
|
+
).length;
|
|
4063
|
+
const positivePercent = current.length > 0 ? Math.round(positiveCount / current.length * 100) : void 0;
|
|
4064
|
+
return {
|
|
4065
|
+
windowDays,
|
|
4066
|
+
averageRating: avgRating(current),
|
|
4067
|
+
previousAverageRating: avgRating(previous),
|
|
4068
|
+
totalNew: current.length,
|
|
4069
|
+
positivePercent
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
async function getAppStatus(client, reporting, packageName, options = {}) {
|
|
4073
|
+
const days = options.days ?? 7;
|
|
4074
|
+
const thresholds = {
|
|
4075
|
+
crashRate: options.vitalThresholds?.crashRate ?? DEFAULT_THRESHOLDS.crashRate,
|
|
4076
|
+
anrRate: options.vitalThresholds?.anrRate ?? DEFAULT_THRESHOLDS.anrRate,
|
|
4077
|
+
slowStartRate: options.vitalThresholds?.slowStartRate ?? DEFAULT_THRESHOLDS.slowStartRate,
|
|
4078
|
+
slowRenderingRate: options.vitalThresholds?.slowRenderingRate ?? DEFAULT_THRESHOLDS.slowRenderingRate
|
|
4079
|
+
};
|
|
4080
|
+
const [releasesResult, crashesResult, anrResult, slowStartResult, slowRenderResult, reviewsResult] = await Promise.allSettled([
|
|
4081
|
+
getReleasesStatus(client, packageName),
|
|
4082
|
+
queryVitalForStatus(reporting, packageName, "crashRateMetricSet", days),
|
|
4083
|
+
queryVitalForStatus(reporting, packageName, "anrRateMetricSet", days),
|
|
4084
|
+
queryVitalForStatus(reporting, packageName, "slowStartRateMetricSet", days),
|
|
4085
|
+
queryVitalForStatus(reporting, packageName, "slowRenderingRateMetricSet", days),
|
|
4086
|
+
listReviews(client, packageName, { maxResults: 500 })
|
|
4087
|
+
]);
|
|
4088
|
+
const rawReleases = releasesResult.status === "fulfilled" ? releasesResult.value : [];
|
|
4089
|
+
const releases = rawReleases.map((r) => ({
|
|
4090
|
+
track: r.track,
|
|
4091
|
+
versionCode: r.versionCodes[r.versionCodes.length - 1] ?? "\u2014",
|
|
4092
|
+
status: r.status,
|
|
4093
|
+
userFraction: r.userFraction ?? null
|
|
4094
|
+
}));
|
|
4095
|
+
const crashValue = crashesResult.status === "fulfilled" ? crashesResult.value : void 0;
|
|
4096
|
+
const anrValue = anrResult.status === "fulfilled" ? anrResult.value : void 0;
|
|
4097
|
+
const slowStartValue = slowStartResult.status === "fulfilled" ? slowStartResult.value : void 0;
|
|
4098
|
+
const slowRenderValue = slowRenderResult.status === "fulfilled" ? slowRenderResult.value : void 0;
|
|
4099
|
+
const rawReviews = reviewsResult.status === "fulfilled" ? reviewsResult.value : [];
|
|
4100
|
+
const reviews = computeReviewSentiment(rawReviews, 30);
|
|
4101
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4102
|
+
return {
|
|
4103
|
+
packageName,
|
|
4104
|
+
fetchedAt,
|
|
4105
|
+
cached: false,
|
|
4106
|
+
releases,
|
|
4107
|
+
vitals: {
|
|
4108
|
+
windowDays: days,
|
|
4109
|
+
crashes: toVitalMetric(crashValue, thresholds.crashRate),
|
|
4110
|
+
anr: toVitalMetric(anrValue, thresholds.anrRate),
|
|
4111
|
+
slowStarts: toVitalMetric(slowStartValue, thresholds.slowStartRate),
|
|
4112
|
+
slowRender: toVitalMetric(slowRenderValue, thresholds.slowRenderingRate)
|
|
4113
|
+
},
|
|
4114
|
+
reviews
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
function vitalIndicator(metric) {
|
|
4118
|
+
if (metric.status === "unknown") return "?";
|
|
4119
|
+
if (metric.status === "breach") return "\u2717";
|
|
4120
|
+
if (metric.status === "warn") return "\u26A0";
|
|
4121
|
+
return "\u2713";
|
|
4122
|
+
}
|
|
4123
|
+
function formatVitalValue(metric) {
|
|
4124
|
+
if (metric.value === void 0) return "n/a";
|
|
4125
|
+
return `${(metric.value * 100).toFixed(2)}%`;
|
|
4126
|
+
}
|
|
4127
|
+
function formatFraction(fraction) {
|
|
4128
|
+
if (fraction === null) return "\u2014";
|
|
4129
|
+
return `${Math.round(fraction * 100)}%`;
|
|
4130
|
+
}
|
|
4131
|
+
function formatRating(rating) {
|
|
4132
|
+
if (rating === void 0) return "n/a";
|
|
4133
|
+
return `\u2605 ${rating.toFixed(1)}`;
|
|
4134
|
+
}
|
|
4135
|
+
function formatTrend(current, previous) {
|
|
4136
|
+
if (current === void 0 || previous === void 0) return "";
|
|
4137
|
+
if (current > previous) return ` \u2191 from ${previous.toFixed(1)}`;
|
|
4138
|
+
if (current < previous) return ` \u2193 from ${previous.toFixed(1)}`;
|
|
4139
|
+
return "";
|
|
4140
|
+
}
|
|
4141
|
+
function formatStatusTable(status) {
|
|
4142
|
+
const lines = [];
|
|
4143
|
+
const cachedLabel = status.cached ? ` (cached ${new Date(status.fetchedAt).toLocaleTimeString()})` : ` (fetched ${new Date(status.fetchedAt).toLocaleTimeString()})`;
|
|
4144
|
+
lines.push(`App: ${status.packageName}${cachedLabel}`);
|
|
4145
|
+
lines.push("");
|
|
4146
|
+
lines.push("RELEASES");
|
|
4147
|
+
if (status.releases.length === 0) {
|
|
4148
|
+
lines.push(" No releases found.");
|
|
4149
|
+
} else {
|
|
4150
|
+
const trackW = Math.max(10, ...status.releases.map((r) => r.track.length));
|
|
4151
|
+
const versionW = Math.max(7, ...status.releases.map((r) => r.versionCode.length));
|
|
4152
|
+
const statusW = Math.max(8, ...status.releases.map((r) => r.status.length));
|
|
4153
|
+
for (const r of status.releases) {
|
|
4154
|
+
lines.push(
|
|
4155
|
+
` ${r.track.padEnd(trackW)} ${r.versionCode.padEnd(versionW)} ${r.status.padEnd(statusW)} ${formatFraction(r.userFraction)}`
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
lines.push("");
|
|
4160
|
+
lines.push(`VITALS (last ${status.vitals.windowDays} days)`);
|
|
4161
|
+
const { crashes, anr, slowStarts, slowRender } = status.vitals;
|
|
4162
|
+
lines.push(
|
|
4163
|
+
` crashes ${formatVitalValue(crashes).padEnd(8)} ${vitalIndicator(crashes)} anr ${formatVitalValue(anr).padEnd(8)} ${vitalIndicator(anr)}`
|
|
4164
|
+
);
|
|
4165
|
+
lines.push(
|
|
4166
|
+
` slow starts ${formatVitalValue(slowStarts).padEnd(8)} ${vitalIndicator(slowStarts)} slow render ${formatVitalValue(slowRender).padEnd(8)} ${vitalIndicator(slowRender)}`
|
|
4167
|
+
);
|
|
4168
|
+
lines.push("");
|
|
4169
|
+
lines.push(`REVIEWS (last ${status.reviews.windowDays} days)`);
|
|
4170
|
+
const { averageRating, previousAverageRating, totalNew, positivePercent } = status.reviews;
|
|
4171
|
+
const trend = formatTrend(averageRating, previousAverageRating);
|
|
4172
|
+
const positiveStr = positivePercent !== void 0 ? ` ${positivePercent}% positive` : "";
|
|
4173
|
+
lines.push(
|
|
4174
|
+
` ${formatRating(averageRating)} ${totalNew} new${positiveStr}${trend}`
|
|
4175
|
+
);
|
|
4176
|
+
return lines.join("\n");
|
|
4177
|
+
}
|
|
4178
|
+
function statusHasBreach(status) {
|
|
4179
|
+
return status.vitals.crashes.status === "breach" || status.vitals.anr.status === "breach" || status.vitals.slowStarts.status === "breach" || status.vitals.slowRender.status === "breach";
|
|
4180
|
+
}
|
|
3915
4181
|
export {
|
|
3916
4182
|
ApiError,
|
|
3917
4183
|
ConfigError,
|
|
@@ -3981,9 +4247,11 @@ export {
|
|
|
3981
4247
|
formatJunit,
|
|
3982
4248
|
formatOutput,
|
|
3983
4249
|
formatSlackPayload,
|
|
4250
|
+
formatStatusTable,
|
|
3984
4251
|
generateMigrationPlan,
|
|
3985
4252
|
generateNotesFromGit,
|
|
3986
4253
|
getAppInfo,
|
|
4254
|
+
getAppStatus,
|
|
3987
4255
|
getCountryAvailability,
|
|
3988
4256
|
getDataSafety,
|
|
3989
4257
|
getDeviceTier,
|
|
@@ -4034,6 +4302,7 @@ export {
|
|
|
4034
4302
|
listTracks,
|
|
4035
4303
|
listUsers,
|
|
4036
4304
|
listVoidedPurchases,
|
|
4305
|
+
loadStatusCache,
|
|
4037
4306
|
migratePrices,
|
|
4038
4307
|
parseAppfile,
|
|
4039
4308
|
parseFastfile,
|
|
@@ -4055,11 +4324,13 @@ export {
|
|
|
4055
4324
|
revokeSubscriptionPurchase,
|
|
4056
4325
|
safePath,
|
|
4057
4326
|
safePathWithin,
|
|
4327
|
+
saveStatusCache,
|
|
4058
4328
|
scaffoldPlugin,
|
|
4059
4329
|
searchAuditEvents,
|
|
4060
4330
|
searchVitalsErrors,
|
|
4061
4331
|
sendWebhook,
|
|
4062
4332
|
sortResults,
|
|
4333
|
+
statusHasBreach,
|
|
4063
4334
|
syncInAppProducts,
|
|
4064
4335
|
updateAppDetails,
|
|
4065
4336
|
updateDataSafety,
|