eas-cli 18.7.0 → 18.8.1

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.
Files changed (49) hide show
  1. package/README.md +147 -90
  2. package/build/build/utils/url.d.ts +6 -0
  3. package/build/build/utils/url.js +9 -0
  4. package/build/channel/insights/formatInsights.d.ts +47 -0
  5. package/build/channel/insights/formatInsights.js +108 -0
  6. package/build/commands/channel/insights.d.ts +18 -0
  7. package/build/commands/channel/insights.js +71 -0
  8. package/build/commands/observe/events.d.ts +1 -0
  9. package/build/commands/observe/events.js +17 -4
  10. package/build/commands/observe/metrics.d.ts +1 -0
  11. package/build/commands/observe/metrics.js +18 -6
  12. package/build/commands/observe/versions.d.ts +1 -0
  13. package/build/commands/observe/versions.js +17 -4
  14. package/build/commands/simulator/start.d.ts +16 -0
  15. package/build/commands/simulator/start.js +203 -0
  16. package/build/commands/update/insights.d.ts +19 -0
  17. package/build/commands/update/insights.js +75 -0
  18. package/build/commands/update/view.d.ts +4 -0
  19. package/build/commands/update/view.js +47 -2
  20. package/build/credentials/ios/appstore/capabilityIdentifiers.js +28 -3
  21. package/build/graphql/client.d.ts +13 -0
  22. package/build/graphql/client.js +36 -1
  23. package/build/graphql/generated.d.ts +318 -0
  24. package/build/graphql/generated.js +21 -3
  25. package/build/graphql/mutations/DeviceRunSessionMutation.d.ts +5 -0
  26. package/build/graphql/mutations/DeviceRunSessionMutation.js +34 -0
  27. package/build/graphql/queries/ChannelInsightsQuery.d.ts +12 -0
  28. package/build/graphql/queries/ChannelInsightsQuery.js +81 -0
  29. package/build/graphql/queries/DeviceRunSessionQuery.d.ts +5 -0
  30. package/build/graphql/queries/DeviceRunSessionQuery.js +28 -0
  31. package/build/graphql/queries/UpdateInsightsQuery.d.ts +10 -0
  32. package/build/graphql/queries/UpdateInsightsQuery.js +53 -0
  33. package/build/graphql/types/Observe.js +1 -0
  34. package/build/insights/formatTimespan.d.ts +7 -0
  35. package/build/insights/formatTimespan.js +15 -0
  36. package/build/insights/timeRange.d.ts +10 -0
  37. package/build/insights/timeRange.js +10 -0
  38. package/build/metadata/apple/tasks/previews.js +41 -15
  39. package/build/observe/formatEvents.d.ts +3 -0
  40. package/build/observe/formatEvents.js +4 -0
  41. package/build/observe/formatMetrics.d.ts +3 -2
  42. package/build/observe/formatMetrics.js +16 -27
  43. package/build/observe/formatVersions.js +2 -8
  44. package/build/update/insights/formatInsights.d.ts +34 -0
  45. package/build/update/insights/formatInsights.js +128 -0
  46. package/build/utils/renderTextTable.d.ts +6 -0
  47. package/build/utils/renderTextTable.js +23 -0
  48. package/oclif.manifest.json +995 -593
  49. package/package.json +5 -5
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChannelInsightsQuery = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
6
+ const errors_1 = require("../../channel/errors");
7
+ const client_1 = require("../client");
8
+ exports.ChannelInsightsQuery = {
9
+ async viewChannelRuntimeInsightsAsync(graphqlClient, { appId, channelName, runtimeVersion, startTime, endTime, }) {
10
+ const data = await (0, client_1.withUpgradeRequiredErrorHandlingAsync)(graphqlClient
11
+ .query((0, graphql_tag_1.default) `
12
+ query ViewChannelRuntimeInsightsOnApp(
13
+ $appId: String!
14
+ $channelName: String!
15
+ $runtimeVersion: String!
16
+ $timespan: InsightsTimespan!
17
+ ) {
18
+ app {
19
+ byId(appId: $appId) {
20
+ id
21
+ updateChannelByName(name: $channelName) {
22
+ id
23
+ name
24
+ runtimeInsights {
25
+ id
26
+ embeddedUpdateTotalUniqueUsers(runtimeVersion: $runtimeVersion, timespan: $timespan)
27
+ mostPopularUpdates(runtimeVersion: $runtimeVersion, timespan: $timespan) {
28
+ id
29
+ group
30
+ message
31
+ runtimeVersion
32
+ platform
33
+ insights {
34
+ id
35
+ totalUniqueUsers(timespan: $timespan)
36
+ }
37
+ }
38
+ uniqueUsersOverTime(runtimeVersion: $runtimeVersion, timespan: $timespan) {
39
+ data {
40
+ labels
41
+ datasets {
42
+ id
43
+ label
44
+ data
45
+ }
46
+ }
47
+ }
48
+ cumulativeMetricsOverTime(runtimeVersion: $runtimeVersion, timespan: $timespan) {
49
+ data {
50
+ labels
51
+ datasets {
52
+ id
53
+ label
54
+ data
55
+ }
56
+ }
57
+ metricsAtLastTimestamp {
58
+ id
59
+ label
60
+ data
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ `, {
69
+ appId,
70
+ channelName,
71
+ runtimeVersion,
72
+ timespan: { start: startTime, end: endTime },
73
+ }, { additionalTypenames: ['UpdateChannel', 'Update', 'UpdateChannelRuntimeInsights'] })
74
+ .toPromise(), { featureName: 'EAS Update channel insights' });
75
+ const updateChannel = data.app.byId.updateChannelByName;
76
+ if (!updateChannel) {
77
+ throw new errors_1.ChannelNotFoundError(`Could not find channel with the name ${channelName}`);
78
+ }
79
+ return updateChannel.runtimeInsights;
80
+ },
81
+ };
@@ -0,0 +1,5 @@
1
+ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
2
+ import { DeviceRunSessionByIdQuery } from '../generated';
3
+ export declare const DeviceRunSessionQuery: {
4
+ byIdAsync(graphqlClient: ExpoGraphqlClient, deviceRunSessionId: string): Promise<DeviceRunSessionByIdQuery["deviceRunSessions"]["byId"]>;
5
+ };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeviceRunSessionQuery = 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.DeviceRunSessionQuery = {
8
+ async byIdAsync(graphqlClient, deviceRunSessionId) {
9
+ const data = await (0, client_1.withErrorHandlingAsync)(graphqlClient
10
+ .query((0, graphql_tag_1.default) `
11
+ query DeviceRunSessionByIdQuery($deviceRunSessionId: ID!) {
12
+ deviceRunSessions {
13
+ byId(deviceRunSessionId: $deviceRunSessionId) {
14
+ id
15
+ status
16
+ turtleJobRun {
17
+ id
18
+ status
19
+ logFileUrls
20
+ }
21
+ }
22
+ }
23
+ }
24
+ `, { deviceRunSessionId }, { requestPolicy: 'network-only' })
25
+ .toPromise());
26
+ return data.deviceRunSessions.byId;
27
+ },
28
+ };
@@ -0,0 +1,10 @@
1
+ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
2
+ import { ViewUpdateGroupInsightsQuery } from '../generated';
3
+ export type UpdateWithInsightsObject = ViewUpdateGroupInsightsQuery['updatesByGroup'][number];
4
+ export declare const UpdateInsightsQuery: {
5
+ viewUpdateGroupInsightsAsync(graphqlClient: ExpoGraphqlClient, { groupId, startTime, endTime }: {
6
+ groupId: string;
7
+ startTime: string;
8
+ endTime: string;
9
+ }): Promise<UpdateWithInsightsObject[]>;
10
+ };
@@ -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
+ };
@@ -35,6 +35,7 @@ exports.AppObserveEventFragmentNode = (0, graphql_tag_1.default) `
35
35
  countryCode
36
36
  sessionId
37
37
  easClientId
38
+ customParams
38
39
  }
39
40
  `;
40
41
  exports.AppObserveAppVersionFragmentNode = (0, graphql_tag_1.default) `
@@ -0,0 +1,7 @@
1
+ export interface InsightsTimespanFields {
2
+ startTime: string;
3
+ endTime: string;
4
+ daysBack?: number;
5
+ }
6
+ export declare function formatTimespan(timespan: InsightsTimespanFields): string;
7
+ export declare function toDateOnly(isoTimestamp: string): string;
@@ -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
+ export declare const INSIGHTS_DEFAULT_DAYS_BACK = 7;
2
+ export declare function resolveInsightsTimeRange(flags: {
3
+ days?: number;
4
+ start?: string;
5
+ end?: string;
6
+ }): {
7
+ daysBack?: number;
8
+ startTime: string;
9
+ endTime: string;
10
+ };
@@ -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
- if (!previews || Object.keys(previews).length === 0) {
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
- for (const [previewType, previewConfig] of Object.entries(previews)) {
107
- if (!previewConfig) {
108
- continue;
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 that don't match
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
- if (preview.attributes.fileName !== fileName) {
162
- await (0, log_2.logAsync)(() => preview.deleteAsync(), {
163
- pending: `Deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})...`,
164
- success: `Deleted old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
165
- failure: `Failed deleting old preview ${chalk_1.default.bold(preview.attributes.fileName)} (${locale})`,
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, {
@@ -13,6 +13,9 @@ export interface ObserveEventJson {
13
13
  sessionId: string | null;
14
14
  easClientId: string;
15
15
  timestamp: string;
16
+ customParams: {
17
+ [key: string]: any;
18
+ } | null;
16
19
  }
17
20
  export interface BuildEventsTableOptions {
18
21
  metricName: string;
@@ -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>): ObserveMetricsJsonOutput;
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({ appVersion, platform, metrics });
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
- // Check if any version has updates
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 = platformResults.map(result => {
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
- return [versionLabel, ...(hasUpdates ? [updatesLabel] : []), ...metricCells];
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', ...(hasUpdates ? [''] : []), ...countCells];
189
+ footerRow = ['Total events', ...countCells];
201
190
  }
202
191
  }
203
- sections.push(renderTable(headers, rows, footerRow));
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(renderTable(headers, rows));
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
+ }