eas-cli 18.6.0 → 18.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -91
- package/build/channel/insights/formatInsights.d.ts +47 -0
- package/build/channel/insights/formatInsights.js +108 -0
- package/build/commands/build/dev.d.ts +1 -0
- package/build/commands/build/dev.js +10 -2
- package/build/commands/channel/insights.d.ts +18 -0
- package/build/commands/channel/insights.js +71 -0
- package/build/commands/observe/events.d.ts +1 -0
- package/build/commands/observe/events.js +17 -4
- package/build/commands/observe/metrics.d.ts +1 -0
- package/build/commands/observe/metrics.js +18 -6
- package/build/commands/observe/versions.d.ts +1 -0
- package/build/commands/observe/versions.js +17 -4
- package/build/commands/update/insights.d.ts +19 -0
- package/build/commands/update/insights.js +75 -0
- package/build/commands/update/view.d.ts +4 -0
- package/build/commands/update/view.js +47 -2
- package/build/credentials/ios/appstore/capabilityIdentifiers.js +28 -3
- package/build/graphql/client.d.ts +13 -0
- package/build/graphql/client.js +36 -1
- package/build/graphql/generated.d.ts +193 -0
- package/build/graphql/generated.js +8 -2
- package/build/graphql/queries/ChannelInsightsQuery.d.ts +12 -0
- package/build/graphql/queries/ChannelInsightsQuery.js +81 -0
- package/build/graphql/queries/UpdateInsightsQuery.d.ts +10 -0
- package/build/graphql/queries/UpdateInsightsQuery.js +53 -0
- package/build/graphql/types/Observe.js +1 -0
- package/build/insights/formatTimespan.d.ts +7 -0
- package/build/insights/formatTimespan.js +15 -0
- package/build/insights/timeRange.d.ts +10 -0
- package/build/insights/timeRange.js +10 -0
- package/build/metadata/apple/tasks/previews.js +41 -15
- package/build/observe/formatEvents.d.ts +3 -0
- package/build/observe/formatEvents.js +4 -0
- package/build/observe/formatMetrics.d.ts +3 -2
- package/build/observe/formatMetrics.js +16 -27
- package/build/observe/formatVersions.js +2 -8
- package/build/update/insights/formatInsights.d.ts +34 -0
- package/build/update/insights/formatInsights.js +128 -0
- package/build/utils/renderTextTable.d.ts +6 -0
- package/build/utils/renderTextTable.js +23 -0
- package/oclif.manifest.json +773 -469
- package/package.json +5 -5
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UpdateInsightsQuery = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
|
|
6
|
+
const client_1 = require("../client");
|
|
7
|
+
exports.UpdateInsightsQuery = {
|
|
8
|
+
async viewUpdateGroupInsightsAsync(graphqlClient, { groupId, startTime, endTime }) {
|
|
9
|
+
const data = await (0, client_1.withUpgradeRequiredErrorHandlingAsync)(graphqlClient
|
|
10
|
+
.query((0, graphql_tag_1.default) `
|
|
11
|
+
query ViewUpdateGroupInsights($groupId: ID!, $timespan: InsightsTimespan!) {
|
|
12
|
+
updatesByGroup(group: $groupId) {
|
|
13
|
+
id
|
|
14
|
+
platform
|
|
15
|
+
insights {
|
|
16
|
+
id
|
|
17
|
+
totalUniqueUsers(timespan: $timespan)
|
|
18
|
+
cumulativeAverageMetrics {
|
|
19
|
+
launchAssetCount
|
|
20
|
+
averageUpdatePayloadBytes
|
|
21
|
+
}
|
|
22
|
+
cumulativeMetrics(timespan: $timespan) {
|
|
23
|
+
metricsAtLastTimestamp {
|
|
24
|
+
totalInstalls
|
|
25
|
+
totalFailedInstalls
|
|
26
|
+
}
|
|
27
|
+
data {
|
|
28
|
+
labels
|
|
29
|
+
installsDataset {
|
|
30
|
+
id
|
|
31
|
+
label
|
|
32
|
+
cumulative
|
|
33
|
+
difference
|
|
34
|
+
}
|
|
35
|
+
failedInstallsDataset {
|
|
36
|
+
id
|
|
37
|
+
label
|
|
38
|
+
cumulative
|
|
39
|
+
difference
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`, { groupId, timespan: { start: startTime, end: endTime } }, { additionalTypenames: ['Update', 'UpdateInsights'] })
|
|
47
|
+
.toPromise(), { featureName: 'EAS Update insights' });
|
|
48
|
+
if (data.updatesByGroup.length === 0) {
|
|
49
|
+
throw new Error(`Could not find any updates with group ID: "${groupId}"`);
|
|
50
|
+
}
|
|
51
|
+
return data.updatesByGroup;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatTimespan = formatTimespan;
|
|
4
|
+
exports.toDateOnly = toDateOnly;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const dateformat_1 = tslib_1.__importDefault(require("dateformat"));
|
|
7
|
+
function formatTimespan(timespan) {
|
|
8
|
+
if (timespan.daysBack) {
|
|
9
|
+
return `last ${timespan.daysBack} day${timespan.daysBack === 1 ? '' : 's'}`;
|
|
10
|
+
}
|
|
11
|
+
return `${toDateOnly(timespan.startTime)} to ${toDateOnly(timespan.endTime)}`;
|
|
12
|
+
}
|
|
13
|
+
function toDateOnly(isoTimestamp) {
|
|
14
|
+
return (0, dateformat_1.default)(new Date(isoTimestamp), 'UTC:yyyy-mm-dd');
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INSIGHTS_DEFAULT_DAYS_BACK = void 0;
|
|
4
|
+
exports.resolveInsightsTimeRange = resolveInsightsTimeRange;
|
|
5
|
+
const startAndEndTime_1 = require("../observe/startAndEndTime");
|
|
6
|
+
exports.INSIGHTS_DEFAULT_DAYS_BACK = 7;
|
|
7
|
+
function resolveInsightsTimeRange(flags) {
|
|
8
|
+
const days = flags.days ?? (flags.start ? undefined : exports.INSIGHTS_DEFAULT_DAYS_BACK);
|
|
9
|
+
return (0, startAndEndTime_1.resolveTimeRange)({ ...flags, days });
|
|
10
|
+
}
|
|
@@ -95,19 +95,45 @@ class PreviewsTask extends task_1.AppleTask {
|
|
|
95
95
|
}
|
|
96
96
|
for (const localeCode of locales) {
|
|
97
97
|
const previews = config.getPreviews(localeCode);
|
|
98
|
-
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
98
|
+
const existingSets = context.previewSets.get(localeCode);
|
|
101
99
|
const localization = context.versionLocales.find(l => l.attributes.locale === localeCode);
|
|
102
100
|
if (!localization) {
|
|
103
101
|
log_1.default.warn((0, chalk_1.default) `{yellow Skipping video previews for ${localeCode} - locale not found}`);
|
|
104
102
|
continue;
|
|
105
103
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
// Upload/sync configured previews
|
|
105
|
+
if (previews) {
|
|
106
|
+
for (const [previewType, previewConfig] of Object.entries(previews)) {
|
|
107
|
+
if (!previewConfig) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!apple_utils_1.ALL_PREVIEW_TYPES.includes(previewType)) {
|
|
111
|
+
const strippedType = previewType.replace(/^APP_/, '');
|
|
112
|
+
const suggestion = apple_utils_1.ALL_PREVIEW_TYPES.includes(strippedType)
|
|
113
|
+
? (0, chalk_1.default) ` Did you mean {bold ${strippedType}}? Preview types don't use the "APP_" prefix (that's only for screenshots).`
|
|
114
|
+
: '';
|
|
115
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Unknown preview type {bold ${previewType}} for ${localeCode}, skipping.${suggestion}}`);
|
|
116
|
+
log_1.default.warn((0, chalk_1.default) `{yellow Valid preview types: ${apple_utils_1.ALL_PREVIEW_TYPES.join(', ')}}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), existingSets);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Delete remote previews for types no longer in config
|
|
123
|
+
if (existingSets) {
|
|
124
|
+
for (const [previewType, previewSet] of existingSets) {
|
|
125
|
+
if (previews?.[previewType]) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const existingPreviews = previewSet.attributes.appPreviews || [];
|
|
129
|
+
for (const preview of existingPreviews) {
|
|
130
|
+
await (0, log_2.logAsync)(() => preview.deleteAsync(), {
|
|
131
|
+
pending: `Deleting video preview ${chalk_1.default.bold(preview.attributes.fileName)} (${localeCode})...`,
|
|
132
|
+
success: `Deleted video preview ${chalk_1.default.bold(preview.attributes.fileName)} (${localeCode})`,
|
|
133
|
+
failure: `Failed deleting video preview ${chalk_1.default.bold(preview.attributes.fileName)} (${localeCode})`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
109
136
|
}
|
|
110
|
-
await syncPreviewSetAsync(context.projectDir, localization, previewType, normalizePreviewConfig(previewConfig), context.previewSets.get(localeCode));
|
|
111
137
|
}
|
|
112
138
|
}
|
|
113
139
|
}
|
|
@@ -156,15 +182,15 @@ async function syncPreviewSetAsync(projectDir, localization, previewType, previe
|
|
|
156
182
|
log_1.default.log((0, chalk_1.default) `{dim Preview ${fileName} already exists, skipping upload}`);
|
|
157
183
|
return;
|
|
158
184
|
}
|
|
159
|
-
// Delete existing previews
|
|
185
|
+
// Delete all existing previews before uploading the new one.
|
|
186
|
+
// Apple limits each set to 3 previews, and we manage one preview per set,
|
|
187
|
+
// so we need to clean up stale entries to avoid "Too many app previews" errors.
|
|
160
188
|
for (const preview of existingPreviews) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
167
|
-
}
|
|
189
|
+
await (0, log_2.logAsync)(() => preview.deleteAsync(), {
|
|
190
|
+
pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
|
|
191
|
+
success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
|
|
192
|
+
failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
|
|
193
|
+
});
|
|
168
194
|
}
|
|
169
195
|
// Upload new preview
|
|
170
196
|
await (0, log_2.logAsync)(() => apple_utils_1.AppPreview.uploadAsync(localization.context, {
|
|
@@ -23,6 +23,9 @@ function formatDate(isoString) {
|
|
|
23
23
|
day: 'numeric',
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
+
function resolveCustomParams(event) {
|
|
27
|
+
return event.customParams ?? null;
|
|
28
|
+
}
|
|
26
29
|
function buildObserveEventsTable(events, pageInfo, options) {
|
|
27
30
|
if (events.length === 0) {
|
|
28
31
|
return chalk_1.default.yellow('No events found.');
|
|
@@ -90,6 +93,7 @@ function buildObserveEventsJson(events, pageInfo) {
|
|
|
90
93
|
sessionId: event.sessionId ?? null,
|
|
91
94
|
easClientId: event.easClientId,
|
|
92
95
|
timestamp: event.timestamp,
|
|
96
|
+
customParams: resolveCustomParams(event),
|
|
93
97
|
})),
|
|
94
98
|
pageInfo: {
|
|
95
99
|
hasNextPage: pageInfo.hasNextPage,
|
|
@@ -22,17 +22,18 @@ export type MetricValuesJson = Partial<Record<StatisticKey, number | null>>;
|
|
|
22
22
|
export interface ObserveMetricsVersionResult {
|
|
23
23
|
appVersion: string;
|
|
24
24
|
platform: AppPlatform;
|
|
25
|
+
buildNumbers: string[];
|
|
26
|
+
updateIds: string[];
|
|
25
27
|
metrics: Record<string, MetricValuesJson>;
|
|
26
28
|
}
|
|
27
29
|
export interface ObserveMetricsJsonOutput {
|
|
28
30
|
versions: ObserveMetricsVersionResult[];
|
|
29
31
|
totalEventCounts: Record<string, Record<string, number>>;
|
|
30
32
|
}
|
|
31
|
-
export declare function buildObserveMetricsJson(metricsMap: ObserveMetricsMap, metricNames: string[], stats: StatisticKey[], totalEventCounts?: Map<string, number
|
|
33
|
+
export declare function buildObserveMetricsJson(metricsMap: ObserveMetricsMap, metricNames: string[], stats: StatisticKey[], totalEventCounts?: Map<string, number>, buildNumbersMap?: BuildNumbersMap, updateIdsMap?: UpdateIdsMap): ObserveMetricsJsonOutput;
|
|
32
34
|
export declare function buildObserveMetricsTable(metricsMap: ObserveMetricsMap, metricNames: string[], stats: StatisticKey[], options?: {
|
|
33
35
|
daysBack?: number;
|
|
34
36
|
buildNumbersMap?: BuildNumbersMap;
|
|
35
|
-
updateIdsMap?: UpdateIdsMap;
|
|
36
37
|
totalEventCounts?: Map<string, number>;
|
|
37
38
|
}): string;
|
|
38
39
|
export {};
|
|
@@ -9,6 +9,7 @@ const tslib_1 = require("tslib");
|
|
|
9
9
|
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
10
10
|
const errors_1 = require("../commandUtils/errors");
|
|
11
11
|
const platform_1 = require("../platform");
|
|
12
|
+
const renderTextTable_1 = tslib_1.__importDefault(require("../utils/renderTextTable"));
|
|
12
13
|
const metricNames_1 = require("./metricNames");
|
|
13
14
|
exports.STAT_ALIASES = {
|
|
14
15
|
min: 'min',
|
|
@@ -65,7 +66,7 @@ function parseMetricsKey(key) {
|
|
|
65
66
|
platform: key.slice(lastColon + 1),
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
|
-
function buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCounts) {
|
|
69
|
+
function buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCounts, buildNumbersMap, updateIdsMap) {
|
|
69
70
|
const versions = [];
|
|
70
71
|
for (const [key, versionMetrics] of metricsMap) {
|
|
71
72
|
const { appVersion, platform } = parseMetricsKey(key);
|
|
@@ -78,7 +79,13 @@ function buildObserveMetricsJson(metricsMap, metricNames, stats, totalEventCount
|
|
|
78
79
|
}
|
|
79
80
|
metrics[metricName] = statValues;
|
|
80
81
|
}
|
|
81
|
-
versions.push({
|
|
82
|
+
versions.push({
|
|
83
|
+
appVersion,
|
|
84
|
+
platform,
|
|
85
|
+
buildNumbers: buildNumbersMap?.get(key) ?? [],
|
|
86
|
+
updateIds: updateIdsMap?.get(key) ?? [],
|
|
87
|
+
metrics,
|
|
88
|
+
});
|
|
82
89
|
}
|
|
83
90
|
// Group total event counts by metric → platform
|
|
84
91
|
const counts = {};
|
|
@@ -104,19 +111,6 @@ function buildTimeRangeDescription(daysBack) {
|
|
|
104
111
|
}
|
|
105
112
|
return '';
|
|
106
113
|
}
|
|
107
|
-
function renderTable(headers, rows, footerRow) {
|
|
108
|
-
const allRows = footerRow ? [...rows, footerRow] : rows;
|
|
109
|
-
const colWidths = headers.map((h, i) => Math.max(h.length, ...allRows.map(r => (r[i] ?? '').length)));
|
|
110
|
-
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
111
|
-
const separatorLine = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
112
|
-
const dataLines = rows.map(row => row.map((cell, i) => cell.padEnd(colWidths[i])).join(' '));
|
|
113
|
-
const lines = [chalk_1.default.bold(headerLine), separatorLine, ...dataLines];
|
|
114
|
-
if (footerRow) {
|
|
115
|
-
lines.push(separatorLine);
|
|
116
|
-
lines.push(footerRow.map((cell, i) => cell.padEnd(colWidths[i])).join(' '));
|
|
117
|
-
}
|
|
118
|
-
return lines.join('\n');
|
|
119
|
-
}
|
|
120
114
|
function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
|
|
121
115
|
const { versions: results } = buildObserveMetricsJson(metricsMap, metricNames, stats);
|
|
122
116
|
if (results.length === 0) {
|
|
@@ -154,23 +148,18 @@ function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
|
|
|
154
148
|
}
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
|
-
|
|
158
|
-
const hasUpdates = options?.updateIdsMap
|
|
159
|
-
? Array.from(options.updateIdsMap.values()).some(ids => ids.length > 0)
|
|
160
|
-
: false;
|
|
161
|
-
const headers = ['App Version', ...(hasUpdates ? ['Updates'] : []), ...metricHeaders];
|
|
151
|
+
const headers = ['App Version', ...metricHeaders];
|
|
162
152
|
const sections = [chalk_1.default.bold(summaryLine)];
|
|
163
153
|
for (const [platform, platformResults] of byPlatform) {
|
|
164
154
|
sections.push('');
|
|
165
155
|
sections.push(chalk_1.default.bold(platform_1.appPlatformDisplayNames[platform]));
|
|
166
|
-
const rows =
|
|
156
|
+
const rows = [];
|
|
157
|
+
for (const result of platformResults) {
|
|
167
158
|
const key = makeMetricsKey(result.appVersion, result.platform);
|
|
168
159
|
const buildNumbers = options?.buildNumbersMap?.get(key);
|
|
169
160
|
const versionLabel = buildNumbers?.length
|
|
170
161
|
? `${result.appVersion} (${buildNumbers.join(', ')})`
|
|
171
162
|
: result.appVersion;
|
|
172
|
-
const updateIds = options?.updateIdsMap?.get(key);
|
|
173
|
-
const updatesLabel = updateIds?.length ? updateIds.join(', ') : '';
|
|
174
163
|
const metricCells = [];
|
|
175
164
|
for (const m of metricNames) {
|
|
176
165
|
const values = result.metrics[m];
|
|
@@ -187,8 +176,8 @@ function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
|
|
|
187
176
|
}
|
|
188
177
|
}
|
|
189
178
|
}
|
|
190
|
-
|
|
191
|
-
}
|
|
179
|
+
rows.push([versionLabel, ...metricCells]);
|
|
180
|
+
}
|
|
192
181
|
let footerRow;
|
|
193
182
|
if (options?.totalEventCounts) {
|
|
194
183
|
const countCells = [];
|
|
@@ -197,10 +186,10 @@ function buildObserveMetricsTable(metricsMap, metricNames, stats, options) {
|
|
|
197
186
|
countCells.push(count != null ? count.toLocaleString() : '-');
|
|
198
187
|
}
|
|
199
188
|
if (countCells.some(c => c !== '-')) {
|
|
200
|
-
footerRow = ['Total events', ...
|
|
189
|
+
footerRow = ['Total events', ...countCells];
|
|
201
190
|
}
|
|
202
191
|
}
|
|
203
|
-
sections.push(
|
|
192
|
+
sections.push((0, renderTextTable_1.default)(headers, rows, footerRow));
|
|
204
193
|
}
|
|
205
194
|
return sections.join('\n');
|
|
206
195
|
}
|
|
@@ -5,6 +5,7 @@ exports.buildObserveVersionsTable = buildObserveVersionsTable;
|
|
|
5
5
|
const tslib_1 = require("tslib");
|
|
6
6
|
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
7
|
const platform_1 = require("../platform");
|
|
8
|
+
const renderTextTable_1 = tslib_1.__importDefault(require("../utils/renderTextTable"));
|
|
8
9
|
function formatDate(isoString) {
|
|
9
10
|
const date = new Date(isoString);
|
|
10
11
|
return date.toLocaleDateString('en-US', {
|
|
@@ -56,13 +57,6 @@ function buildObserveVersionsJson(results) {
|
|
|
56
57
|
}
|
|
57
58
|
return output;
|
|
58
59
|
}
|
|
59
|
-
function renderTable(headers, rows) {
|
|
60
|
-
const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => r[i].length)));
|
|
61
|
-
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
62
|
-
const separatorLine = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
63
|
-
const dataLines = rows.map(row => row.map((cell, i) => cell.padEnd(colWidths[i])).join(' '));
|
|
64
|
-
return [chalk_1.default.bold(headerLine), separatorLine, ...dataLines].join('\n');
|
|
65
|
-
}
|
|
66
60
|
function buildObserveVersionsTable(results) {
|
|
67
61
|
const hasAnyVersions = results.some(r => r.appVersions.length > 0);
|
|
68
62
|
if (!hasAnyVersions) {
|
|
@@ -86,7 +80,7 @@ function buildObserveVersionsTable(results) {
|
|
|
86
80
|
String(version.buildNumbers.length),
|
|
87
81
|
String(version.updates.length),
|
|
88
82
|
]);
|
|
89
|
-
sections.push(
|
|
83
|
+
sections.push((0, renderTextTable_1.default)(headers, rows));
|
|
90
84
|
}
|
|
91
85
|
return sections.join('\n');
|
|
92
86
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { UpdateWithInsightsObject } from '../../graphql/queries/UpdateInsightsQuery';
|
|
2
|
+
export interface UpdateInsightsTimespan {
|
|
3
|
+
startTime: string;
|
|
4
|
+
endTime: string;
|
|
5
|
+
daysBack?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface UpdateInsightsDailyEntry {
|
|
8
|
+
date: string;
|
|
9
|
+
installs: number;
|
|
10
|
+
failedInstalls: number;
|
|
11
|
+
}
|
|
12
|
+
export interface UpdateInsightsPlatformSummary {
|
|
13
|
+
platform: string;
|
|
14
|
+
updateId: string;
|
|
15
|
+
totalUniqueUsers: number;
|
|
16
|
+
totalInstalls: number;
|
|
17
|
+
totalFailedInstalls: number;
|
|
18
|
+
crashRatePercent: number;
|
|
19
|
+
launchAssetCount: number;
|
|
20
|
+
averageUpdatePayloadBytes: number;
|
|
21
|
+
daily: UpdateInsightsDailyEntry[];
|
|
22
|
+
}
|
|
23
|
+
export interface UpdateInsightsSummary {
|
|
24
|
+
groupId: string;
|
|
25
|
+
startTime: string;
|
|
26
|
+
endTime: string;
|
|
27
|
+
daysBack?: number;
|
|
28
|
+
platforms: UpdateInsightsPlatformSummary[];
|
|
29
|
+
}
|
|
30
|
+
export declare function toUpdateInsightsSummary(groupId: string, updates: UpdateWithInsightsObject[], timespan: UpdateInsightsTimespan): UpdateInsightsSummary;
|
|
31
|
+
export declare function buildUpdateInsightsJson(summary: UpdateInsightsSummary): object;
|
|
32
|
+
export declare function buildUpdateInsightsTable(summary: UpdateInsightsSummary): string;
|
|
33
|
+
export declare function formatPercent(value: number): string;
|
|
34
|
+
export declare function formatBytes(bytes: number): string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toUpdateInsightsSummary = toUpdateInsightsSummary;
|
|
4
|
+
exports.buildUpdateInsightsJson = buildUpdateInsightsJson;
|
|
5
|
+
exports.buildUpdateInsightsTable = buildUpdateInsightsTable;
|
|
6
|
+
exports.formatPercent = formatPercent;
|
|
7
|
+
exports.formatBytes = formatBytes;
|
|
8
|
+
const tslib_1 = require("tslib");
|
|
9
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
10
|
+
const formatTimespan_1 = require("../../insights/formatTimespan");
|
|
11
|
+
const formatFields_1 = tslib_1.__importDefault(require("../../utils/formatFields"));
|
|
12
|
+
const renderTextTable_1 = tslib_1.__importDefault(require("../../utils/renderTextTable"));
|
|
13
|
+
function toUpdateInsightsSummary(groupId, updates, timespan) {
|
|
14
|
+
const platforms = updates
|
|
15
|
+
.map(toPlatformSummary)
|
|
16
|
+
.sort((a, b) => a.platform.localeCompare(b.platform));
|
|
17
|
+
return {
|
|
18
|
+
groupId,
|
|
19
|
+
startTime: timespan.startTime,
|
|
20
|
+
endTime: timespan.endTime,
|
|
21
|
+
daysBack: timespan.daysBack,
|
|
22
|
+
platforms,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function toPlatformSummary(update) {
|
|
26
|
+
const { insights } = update;
|
|
27
|
+
const { totalInstalls, totalFailedInstalls } = insights.cumulativeMetrics.metricsAtLastTimestamp;
|
|
28
|
+
const denom = totalInstalls + totalFailedInstalls;
|
|
29
|
+
const crashRatePercent = denom === 0 ? 0 : (totalFailedInstalls / denom) * 100;
|
|
30
|
+
const { labels, installsDataset, failedInstallsDataset } = insights.cumulativeMetrics.data;
|
|
31
|
+
const daily = labels.map((date, i) => ({
|
|
32
|
+
date,
|
|
33
|
+
installs: installsDataset.difference[i] ?? 0,
|
|
34
|
+
failedInstalls: failedInstallsDataset.difference[i] ?? 0,
|
|
35
|
+
}));
|
|
36
|
+
return {
|
|
37
|
+
platform: update.platform,
|
|
38
|
+
updateId: update.id,
|
|
39
|
+
totalUniqueUsers: insights.totalUniqueUsers,
|
|
40
|
+
totalInstalls,
|
|
41
|
+
totalFailedInstalls,
|
|
42
|
+
crashRatePercent,
|
|
43
|
+
launchAssetCount: insights.cumulativeAverageMetrics.launchAssetCount,
|
|
44
|
+
averageUpdatePayloadBytes: insights.cumulativeAverageMetrics.averageUpdatePayloadBytes,
|
|
45
|
+
daily,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function buildUpdateInsightsJson(summary) {
|
|
49
|
+
return {
|
|
50
|
+
groupId: summary.groupId,
|
|
51
|
+
timespan: {
|
|
52
|
+
start: summary.startTime,
|
|
53
|
+
end: summary.endTime,
|
|
54
|
+
...(summary.daysBack !== undefined ? { daysBack: summary.daysBack } : {}),
|
|
55
|
+
},
|
|
56
|
+
platforms: summary.platforms.map(p => ({
|
|
57
|
+
platform: p.platform,
|
|
58
|
+
updateId: p.updateId,
|
|
59
|
+
totals: {
|
|
60
|
+
uniqueUsers: p.totalUniqueUsers,
|
|
61
|
+
installs: p.totalInstalls,
|
|
62
|
+
failedInstalls: p.totalFailedInstalls,
|
|
63
|
+
crashRatePercent: p.crashRatePercent,
|
|
64
|
+
},
|
|
65
|
+
payload: {
|
|
66
|
+
launchAssetCount: p.launchAssetCount,
|
|
67
|
+
averageUpdatePayloadBytes: p.averageUpdatePayloadBytes,
|
|
68
|
+
},
|
|
69
|
+
daily: p.daily,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function buildUpdateInsightsTable(summary) {
|
|
74
|
+
const sections = [];
|
|
75
|
+
sections.push(chalk_1.default.bold('Update group insights:'));
|
|
76
|
+
sections.push((0, formatFields_1.default)([
|
|
77
|
+
{ label: 'Group ID', value: summary.groupId },
|
|
78
|
+
{ label: 'Time range', value: (0, formatTimespan_1.formatTimespan)(summary) },
|
|
79
|
+
{ label: 'Platforms', value: summary.platforms.map(p => p.platform).join(', ') || 'N/A' },
|
|
80
|
+
]));
|
|
81
|
+
const dailyHeader = summary.daysBack ? ` (last ${summary.daysBack} days)` : '';
|
|
82
|
+
for (const platform of summary.platforms) {
|
|
83
|
+
sections.push('');
|
|
84
|
+
sections.push(chalk_1.default.bold(`${chalk_1.default.cyan(platform.platform)}:`));
|
|
85
|
+
sections.push((0, formatFields_1.default)([
|
|
86
|
+
{ label: 'Update ID', value: platform.updateId },
|
|
87
|
+
{ label: 'Launches', value: platform.totalInstalls.toLocaleString() },
|
|
88
|
+
{ label: 'Failed launches', value: platform.totalFailedInstalls.toLocaleString() },
|
|
89
|
+
{ label: 'Crash rate', value: formatPercent(platform.crashRatePercent) },
|
|
90
|
+
{ label: 'Unique users', value: platform.totalUniqueUsers.toLocaleString() },
|
|
91
|
+
{ label: 'Launch assets', value: platform.launchAssetCount.toLocaleString() },
|
|
92
|
+
{ label: 'Avg payload size', value: formatBytes(platform.averageUpdatePayloadBytes) },
|
|
93
|
+
]));
|
|
94
|
+
if (platform.daily.length > 0) {
|
|
95
|
+
sections.push('');
|
|
96
|
+
sections.push(chalk_1.default.bold(` Daily breakdown${dailyHeader}:`));
|
|
97
|
+
sections.push('');
|
|
98
|
+
sections.push(indent(renderDailyTable(platform.daily), 2));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return sections.join('\n');
|
|
102
|
+
}
|
|
103
|
+
function formatPercent(value) {
|
|
104
|
+
return `${value.toFixed(2)}%`;
|
|
105
|
+
}
|
|
106
|
+
function formatBytes(bytes) {
|
|
107
|
+
if (bytes < 1024) {
|
|
108
|
+
return `${bytes} B`;
|
|
109
|
+
}
|
|
110
|
+
if (bytes < 1024 * 1024) {
|
|
111
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
112
|
+
}
|
|
113
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
114
|
+
}
|
|
115
|
+
function renderDailyTable(rows) {
|
|
116
|
+
return (0, renderTextTable_1.default)(['Date', 'Launches', 'Crashes'], rows.map(r => [
|
|
117
|
+
(0, formatTimespan_1.toDateOnly)(r.date),
|
|
118
|
+
r.installs.toLocaleString(),
|
|
119
|
+
r.failedInstalls.toLocaleString(),
|
|
120
|
+
]));
|
|
121
|
+
}
|
|
122
|
+
function indent(text, spaces) {
|
|
123
|
+
const pad = ' '.repeat(spaces);
|
|
124
|
+
return text
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map(line => (line.length > 0 ? pad + line : line))
|
|
127
|
+
.join('\n');
|
|
128
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a simple column-aligned text table with a bold header row, a dashed
|
|
3
|
+
* separator, the data rows, and an optional footer row (preceded by its own
|
|
4
|
+
* separator). Columns are padded to the widest cell in that column.
|
|
5
|
+
*/
|
|
6
|
+
export default function renderTextTable(headers: string[], rows: string[][], footerRow?: string[]): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = renderTextTable;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
6
|
+
/**
|
|
7
|
+
* Render a simple column-aligned text table with a bold header row, a dashed
|
|
8
|
+
* separator, the data rows, and an optional footer row (preceded by its own
|
|
9
|
+
* separator). Columns are padded to the widest cell in that column.
|
|
10
|
+
*/
|
|
11
|
+
function renderTextTable(headers, rows, footerRow) {
|
|
12
|
+
const allRows = footerRow ? [...rows, footerRow] : rows;
|
|
13
|
+
const colWidths = headers.map((h, i) => Math.max(h.length, ...allRows.map(r => (r[i] ?? '').length)));
|
|
14
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
15
|
+
const separatorLine = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
16
|
+
const dataLines = rows.map(row => row.map((cell, i) => (cell ?? '').padEnd(colWidths[i])).join(' '));
|
|
17
|
+
const lines = [chalk_1.default.bold(headerLine), separatorLine, ...dataLines];
|
|
18
|
+
if (footerRow) {
|
|
19
|
+
lines.push(separatorLine);
|
|
20
|
+
lines.push(footerRow.map((cell, i) => (cell ?? '').padEnd(colWidths[i])).join(' '));
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|