@hot-updater/plugin-insights 1.0.0-rc.30 → 1.0.0-rc.32

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.
@@ -59,6 +59,19 @@ interface InsightsCountEventsInput {
59
59
  readonly sinceMs: number;
60
60
  readonly beforeReceivedAtMs: number;
61
61
  }
62
+ /** One bundle filter's stored events in each interval of a time range. */
63
+ interface InsightsCountEventSeriesInput {
64
+ readonly filter: InsightsBundleEventFilter;
65
+ /** Whole UTC hours, at most 90 days, that a whole number of intervals fill. */
66
+ readonly timeRange: InsightsTimeRange;
67
+ /** Whole hours each point spans, from `timeRange.start`. */
68
+ readonly intervalMs: number;
69
+ }
70
+ /** Stored events in [startMs, startMs + intervalMs). */
71
+ interface InsightsEventSeriesPoint {
72
+ readonly startMs: number;
73
+ readonly events: number;
74
+ }
62
75
  interface ReleaseReference {
63
76
  readonly releaseId: string;
64
77
  readonly platform: "ios" | "android";
@@ -77,44 +90,22 @@ type InsightsCoverage = {
77
90
  readonly kind: "partial";
78
91
  readonly sinceMs: number | null;
79
92
  };
93
+ /**
94
+ * A release's counts since its first report, from its lifetime counters,
95
+ * which are kept.
96
+ */
80
97
  interface ReleaseActivityMetrics {
81
98
  readonly downloads: number;
82
99
  readonly launches: number;
83
100
  readonly failedLaunches: number;
84
- /** Omitted for lifetime-only bundle-row reads. */
85
- readonly uniqueUsers?: number;
86
- /**
87
- * Points of each UTC day, or of each `intervalMs` from the period's start
88
- * on a release read that asks for one; present only for period reads.
89
- */
90
- readonly series?: readonly {
91
- readonly startMs: number;
92
- readonly downloads: number;
93
- readonly launches: number;
94
- readonly failedLaunches: number;
95
- }[];
96
101
  }
97
- type InsightsGetReleaseActivityInput = {
102
+ interface InsightsGetReleaseActivityInput {
103
+ /** 1 to 100 releases. */
98
104
  readonly releases: readonly ReleaseReference[];
99
- readonly timeRange?: InsightsTimeRange;
100
- /**
101
- * Whole hours each series point spans, from `timeRange.start`, with
102
- * every point present; omitted, the series has a point per UTC day
103
- * with reports. Only with a `timeRange`.
104
- */
105
- readonly intervalMs?: number;
106
- readonly scope?: never;
107
- } | {
108
- readonly scope: InsightsScope;
109
- readonly timeRange: InsightsTimeRange;
110
- readonly intervalMs?: never;
111
- readonly releases?: never;
112
- };
105
+ }
113
106
  interface InsightsGetReleaseActivityResult {
114
- readonly coverage: InsightsCoverage;
115
107
  readonly data: readonly {
116
- readonly release?: ReleaseReference;
117
- readonly scope?: InsightsScope;
108
+ readonly release: ReleaseReference;
118
109
  readonly metrics: ReleaseActivityMetrics;
119
110
  }[];
120
111
  readonly measuredAtMs: number;
@@ -150,23 +141,6 @@ interface InsightsGetAppUsageResult {
150
141
  }[];
151
142
  readonly measuredAtMs: number;
152
143
  }
153
- /** Whole UTC days, at most 31, including the current unfinished day. */
154
- interface InsightsGetDistributionHistoryInput extends InsightsScope {
155
- readonly timeRange: InsightsTimeRange;
156
- }
157
- interface InsightsGetDistributionHistoryResult {
158
- readonly coverage: InsightsCoverage;
159
- readonly measuredAtMs: number;
160
- readonly points: readonly {
161
- readonly startMs: number;
162
- readonly bundles: readonly {
163
- readonly appVersion: string;
164
- readonly releaseId: string | null;
165
- readonly bundleKind: "release" | "builtin" | "unknown";
166
- readonly installations: number;
167
- }[];
168
- }[];
169
- }
170
144
  interface InsightsModel {
171
145
  /**
172
146
  * Persist the immutable event once. A private latest-event index, if used,
@@ -195,12 +169,15 @@ interface InsightsModel {
195
169
  countLatestEvents(input: InsightsCountLatestEventsInput): Promise<number>;
196
170
  /** Count accepted reports in [sinceMs, beforeReceivedAtMs), across all pages. */
197
171
  countEvents(input: InsightsCountEventsInput): Promise<number>;
198
- /** Read maintained release/scope summaries without scanning raw events. */
172
+ /**
173
+ * A bundle filter's stored events in each interval, every interval
174
+ * present in order, from the hourly counts that `countEvents` reads.
175
+ */
176
+ countEventSeries(input: InsightsCountEventSeriesInput): Promise<readonly InsightsEventSeriesPoint[]>;
177
+ /** Releases' lifetime counters, without scanning raw events. */
199
178
  getReleaseActivity(input: InsightsGetReleaseActivityInput): Promise<InsightsGetReleaseActivityResult>;
200
- /** Daily last-observed running bundles, one per reporting installation. No historical backfill. */
201
- getDistributionHistory(input: InsightsGetDistributionHistoryInput): Promise<InsightsGetDistributionHistoryResult>;
202
179
  /** Read maintained App usage summaries and latest-report distribution. */
203
180
  getAppUsage(input: InsightsGetAppUsageInput): Promise<InsightsGetAppUsageResult>;
204
181
  }
205
182
  //#endregion
206
- export { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetDistributionHistoryInput, InsightsGetDistributionHistoryResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
183
+ export { InsightsBundleEventFilter, InsightsCountEventSeriesInput, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsEventSeriesPoint, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
@@ -33,12 +33,14 @@ const insightsOverviewDeltas = (event) => {
33
33
  downloads: (previous?.downloads ?? 0) + values.downloads,
34
34
  launches: (previous?.launches ?? 0) + values.launches,
35
35
  failedLaunches: (previous?.failedLaunches ?? 0) + values.failedLaunches,
36
- launchIdentity: previous?.launchIdentity ?? values.launchIdentity,
37
36
  activityIdentity: previous?.activityIdentity ?? values.activityIdentity
38
37
  });
39
38
  };
40
39
  const metric = (releaseId, values) => {
41
- const launchIdentity = values.launches > 0 ? event.install_id : void 0;
40
+ const windowed = {
41
+ ...values,
42
+ launches: 0
43
+ };
42
44
  if (releaseId !== null) for (const [periodKind, start] of [["lifetime", 0], ["hour", bucketStartMs]]) append({
43
45
  scopeKind: "release",
44
46
  releaseKind: "specific",
@@ -49,10 +51,7 @@ const insightsOverviewDeltas = (event) => {
49
51
  appVersion: "",
50
52
  periodKind,
51
53
  bucketStartMs: start
52
- }, {
53
- ...values,
54
- ...periodKind === "hour" ? { launchIdentity } : {}
55
- });
54
+ }, periodKind === "lifetime" ? values : windowed);
56
55
  append({
57
56
  scopeKind: "channel",
58
57
  releaseKind: "all",
@@ -63,10 +62,7 @@ const insightsOverviewDeltas = (event) => {
63
62
  appVersion: "",
64
63
  periodKind: "hour",
65
64
  bucketStartMs
66
- }, {
67
- ...values,
68
- launchIdentity
69
- });
65
+ }, windowed);
70
66
  };
71
67
  if (event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
72
68
  downloads: 1,
@@ -33,12 +33,14 @@ const insightsOverviewDeltas = (event) => {
33
33
  downloads: (previous?.downloads ?? 0) + values.downloads,
34
34
  launches: (previous?.launches ?? 0) + values.launches,
35
35
  failedLaunches: (previous?.failedLaunches ?? 0) + values.failedLaunches,
36
- launchIdentity: previous?.launchIdentity ?? values.launchIdentity,
37
36
  activityIdentity: previous?.activityIdentity ?? values.activityIdentity
38
37
  });
39
38
  };
40
39
  const metric = (releaseId, values) => {
41
- const launchIdentity = values.launches > 0 ? event.install_id : void 0;
40
+ const windowed = {
41
+ ...values,
42
+ launches: 0
43
+ };
42
44
  if (releaseId !== null) for (const [periodKind, start] of [["lifetime", 0], ["hour", bucketStartMs]]) append({
43
45
  scopeKind: "release",
44
46
  releaseKind: "specific",
@@ -49,10 +51,7 @@ const insightsOverviewDeltas = (event) => {
49
51
  appVersion: "",
50
52
  periodKind,
51
53
  bucketStartMs: start
52
- }, {
53
- ...values,
54
- ...periodKind === "hour" ? { launchIdentity } : {}
55
- });
54
+ }, periodKind === "lifetime" ? values : windowed);
56
55
  append({
57
56
  scopeKind: "channel",
58
57
  releaseKind: "all",
@@ -63,10 +62,7 @@ const insightsOverviewDeltas = (event) => {
63
62
  appVersion: "",
64
63
  periodKind: "hour",
65
64
  bucketStartMs
66
- }, {
67
- ...values,
68
- launchIdentity
69
- });
65
+ }, windowed);
70
66
  };
71
67
  if (event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
72
68
  downloads: 1,
@@ -171,6 +171,30 @@ const countEvents = async (db, input) => {
171
171
  }));
172
172
  return await countRaw(db, filter, sinceMs, start) + hours.reduce((sum, row) => sum + row.events, 0) + await countRaw(db, filter, end, beforeReceivedAtMs);
173
173
  };
174
+ /**
175
+ * A bundle filter's stored events in each interval of whole hours: one read
176
+ * of its hourly counts, every interval present.
177
+ */
178
+ const countEventSeries = async (db, input) => {
179
+ const { filter, intervalMs } = input;
180
+ const { start, end } = input.timeRange;
181
+ const events = Array.from({ length: (end - start) / intervalMs }, () => 0);
182
+ const hours = await drain((page) => db.findAggregates("insights_outcomes", {
183
+ index: "byRef",
184
+ where: scopeOf(filter),
185
+ range: {
186
+ gte: start,
187
+ lt: end
188
+ },
189
+ limit: PAGE,
190
+ ...page
191
+ }));
192
+ for (const row of hours) events[Math.floor((row.bucket_start_ms - start) / intervalMs)] += row.events;
193
+ return events.map((count, index) => ({
194
+ startMs: start + index * intervalMs,
195
+ events: count
196
+ }));
197
+ };
174
198
  /** A bundle filter's distinct (field, value, type) predicates. */
175
199
  const latestPredicates = (bundle) => {
176
200
  if (bundle === void 0) return void 0;
@@ -242,7 +266,7 @@ const retained = (now, { rawDays, dailyDays }) => ({
242
266
  * Whether the rows a window reads still hold all of it; an older start is
243
267
  * partial from `oldest`, the first bucket its rows keep.
244
268
  */
245
- const coverageOf = (start, oldest) => start === void 0 || start >= oldest ? {
269
+ const coverageOf = (start, oldest) => start >= oldest ? {
246
270
  kind: "complete",
247
271
  sinceMs: 0
248
272
  } : {
@@ -345,43 +369,6 @@ const lifetimeMetrics = async (db, release) => {
345
369
  failedLaunches: lifetime?.failed_launches ?? 0
346
370
  };
347
371
  };
348
- /**
349
- * Counters, unique users, and a series over a window: a point per UTC day
350
- * with rows, or, with `intervalMs`, every point of that span from the
351
- * window's start.
352
- */
353
- const rangedMetrics = async (db, parts, users, range, days, hour, intervalMs) => {
354
- const [counters, sketches] = await Promise.all([counterRows(db, parts, range, days, hour), sketchRows(db, users.parts, range, days, hour)]);
355
- const series = /* @__PURE__ */ new Map();
356
- if (intervalMs !== void 0) for (let start = range.start; start < range.end; start += intervalMs) series.set(start, {
357
- downloads: 0,
358
- launches: 0,
359
- failedLaunches: 0
360
- });
361
- for (const row of counters) {
362
- const start = intervalMs === void 0 ? dayFloor(row.bucket_start_ms) : row.bucket_start_ms - (row.bucket_start_ms - range.start) % intervalMs;
363
- const point = series.get(start) ?? {
364
- downloads: 0,
365
- launches: 0,
366
- failedLaunches: 0
367
- };
368
- point.downloads += row.downloads;
369
- point.launches += row.launches;
370
- point.failedLaunches += row.failed_launches;
371
- series.set(start, point);
372
- }
373
- const total = (field) => counters.reduce((sum, row) => sum + row[field], 0);
374
- return {
375
- downloads: total("downloads"),
376
- launches: total("launches"),
377
- failedLaunches: total("failed_launches"),
378
- uniqueUsers: (0, _hot_updater_plugin_core.countDistinct)((0, _hot_updater_plugin_core.mergeDistinct)(sketches.map((row) => row[users.field]))),
379
- series: [...series].sort(([left], [right]) => left - right).map(([startMs, point]) => ({
380
- startMs,
381
- ...point
382
- }))
383
- };
384
- };
385
372
  /** A channel and platform's usage rows, of every app version or of one. */
386
373
  const usageParts = (channel, platform, appVersion) => ({
387
374
  scopeKind: "usage",
@@ -392,35 +379,15 @@ const usageParts = (channel, platform, appVersion) => ({
392
379
  appVersionKind: appVersion === void 0 ? "all" : "specific",
393
380
  appVersion: appVersion ?? ""
394
381
  });
395
- const getReleaseActivity = async (db, input, now, retention) => {
396
- const at = now();
397
- const kept = retained(at, retention);
398
- const data = input.scope !== void 0 ? [{
399
- scope: input.scope,
400
- metrics: await rangedMetrics(db, {
401
- scopeKind: "channel",
402
- releaseKind: "all",
403
- releaseId: "",
404
- channel: input.scope.channel,
405
- platform: input.scope.platform,
406
- appVersionKind: "all",
407
- appVersion: ""
408
- }, {
409
- parts: usageParts(input.scope.channel, input.scope.platform),
410
- field: "activity_users"
411
- }, input.timeRange, true, kept.hour)
412
- }] : await Promise.all(input.releases.map(async (release) => ({
413
- release,
414
- metrics: input.timeRange === void 0 ? await lifetimeMetrics(db, release) : await rangedMetrics(db, releaseParts(release), {
415
- parts: releaseParts(release),
416
- field: "launch_users"
417
- }, input.timeRange, false, kept.hour, input.intervalMs)
418
- })));
419
- const oldest = kept[input.scope === void 0 ? "hour" : "day"];
382
+ /** Each release's lifetime counters, which are kept: one logical row apiece. */
383
+ const getReleaseActivity = async (db, input, now) => {
384
+ const measuredAtMs = now();
420
385
  return {
421
- coverage: coverageOf(input.timeRange?.start, oldest),
422
- data,
423
- measuredAtMs: at
386
+ data: await Promise.all(input.releases.map(async (release) => ({
387
+ release,
388
+ metrics: await lifetimeMetrics(db, release)
389
+ }))),
390
+ measuredAtMs
424
391
  };
425
392
  };
426
393
  /**
@@ -680,50 +647,13 @@ const getUpdateFailures = async (db, input, now, retention) => {
680
647
  }
681
648
  };
682
649
  };
683
- /** Reads daily gauges, never raw events or mutable latest-state distribution. */
684
- const getDistributionHistory = async (db, input, now, retention) => {
685
- const at = now();
686
- const { start, end } = input.timeRange;
687
- const points = /* @__PURE__ */ new Map();
688
- for (let day = start; day < end; day += require_schema.DAY_MS) points.set(day, []);
689
- const rows = await drain((page) => db.findAggregates("insights_distribution_history", {
690
- index: "byScope",
691
- where: {
692
- channel: input.channel,
693
- platform: input.platform
694
- },
695
- range: {
696
- gte: start,
697
- lt: end
698
- },
699
- limit: PAGE,
700
- ...page
701
- }));
702
- for (const row of rows) {
703
- if (row.installations <= 0) continue;
704
- points.get(row.bucket_start_ms).push({
705
- appVersion: row.app_version,
706
- releaseId: row.release_id || null,
707
- bundleKind: row.bundle_kind,
708
- installations: row.installations
709
- });
710
- }
711
- return {
712
- coverage: coverageOf(start, retained(at, retention).day),
713
- measuredAtMs: at,
714
- points: [...points].map(([startMs, bundles]) => ({
715
- startMs,
716
- bundles
717
- }))
718
- };
719
- };
720
650
  //#endregion
721
651
  exports.UPDATE_FAILURES_RANGE_MS = UPDATE_FAILURES_RANGE_MS;
652
+ exports.countEventSeries = countEventSeries;
722
653
  exports.countEvents = countEvents;
723
654
  exports.countLatestEvents = countLatestEvents;
724
655
  exports.findLatestEvents = findLatestEvents;
725
656
  exports.getAppUsage = getAppUsage;
726
- exports.getDistributionHistory = getDistributionHistory;
727
657
  exports.getReleaseActivity = getReleaseActivity;
728
658
  exports.getUpdateFailures = getUpdateFailures;
729
659
  exports.listEvents = listEvents;
@@ -171,6 +171,30 @@ const countEvents = async (db, input) => {
171
171
  }));
172
172
  return await countRaw(db, filter, sinceMs, start) + hours.reduce((sum, row) => sum + row.events, 0) + await countRaw(db, filter, end, beforeReceivedAtMs);
173
173
  };
174
+ /**
175
+ * A bundle filter's stored events in each interval of whole hours: one read
176
+ * of its hourly counts, every interval present.
177
+ */
178
+ const countEventSeries = async (db, input) => {
179
+ const { filter, intervalMs } = input;
180
+ const { start, end } = input.timeRange;
181
+ const events = Array.from({ length: (end - start) / intervalMs }, () => 0);
182
+ const hours = await drain((page) => db.findAggregates("insights_outcomes", {
183
+ index: "byRef",
184
+ where: scopeOf(filter),
185
+ range: {
186
+ gte: start,
187
+ lt: end
188
+ },
189
+ limit: PAGE,
190
+ ...page
191
+ }));
192
+ for (const row of hours) events[Math.floor((row.bucket_start_ms - start) / intervalMs)] += row.events;
193
+ return events.map((count, index) => ({
194
+ startMs: start + index * intervalMs,
195
+ events: count
196
+ }));
197
+ };
174
198
  /** A bundle filter's distinct (field, value, type) predicates. */
175
199
  const latestPredicates = (bundle) => {
176
200
  if (bundle === void 0) return void 0;
@@ -242,7 +266,7 @@ const retained = (now, { rawDays, dailyDays }) => ({
242
266
  * Whether the rows a window reads still hold all of it; an older start is
243
267
  * partial from `oldest`, the first bucket its rows keep.
244
268
  */
245
- const coverageOf = (start, oldest) => start === void 0 || start >= oldest ? {
269
+ const coverageOf = (start, oldest) => start >= oldest ? {
246
270
  kind: "complete",
247
271
  sinceMs: 0
248
272
  } : {
@@ -345,43 +369,6 @@ const lifetimeMetrics = async (db, release) => {
345
369
  failedLaunches: lifetime?.failed_launches ?? 0
346
370
  };
347
371
  };
348
- /**
349
- * Counters, unique users, and a series over a window: a point per UTC day
350
- * with rows, or, with `intervalMs`, every point of that span from the
351
- * window's start.
352
- */
353
- const rangedMetrics = async (db, parts, users, range, days, hour, intervalMs) => {
354
- const [counters, sketches] = await Promise.all([counterRows(db, parts, range, days, hour), sketchRows(db, users.parts, range, days, hour)]);
355
- const series = /* @__PURE__ */ new Map();
356
- if (intervalMs !== void 0) for (let start = range.start; start < range.end; start += intervalMs) series.set(start, {
357
- downloads: 0,
358
- launches: 0,
359
- failedLaunches: 0
360
- });
361
- for (const row of counters) {
362
- const start = intervalMs === void 0 ? dayFloor(row.bucket_start_ms) : row.bucket_start_ms - (row.bucket_start_ms - range.start) % intervalMs;
363
- const point = series.get(start) ?? {
364
- downloads: 0,
365
- launches: 0,
366
- failedLaunches: 0
367
- };
368
- point.downloads += row.downloads;
369
- point.launches += row.launches;
370
- point.failedLaunches += row.failed_launches;
371
- series.set(start, point);
372
- }
373
- const total = (field) => counters.reduce((sum, row) => sum + row[field], 0);
374
- return {
375
- downloads: total("downloads"),
376
- launches: total("launches"),
377
- failedLaunches: total("failed_launches"),
378
- uniqueUsers: countDistinct(mergeDistinct(sketches.map((row) => row[users.field]))),
379
- series: [...series].sort(([left], [right]) => left - right).map(([startMs, point]) => ({
380
- startMs,
381
- ...point
382
- }))
383
- };
384
- };
385
372
  /** A channel and platform's usage rows, of every app version or of one. */
386
373
  const usageParts = (channel, platform, appVersion) => ({
387
374
  scopeKind: "usage",
@@ -392,35 +379,15 @@ const usageParts = (channel, platform, appVersion) => ({
392
379
  appVersionKind: appVersion === void 0 ? "all" : "specific",
393
380
  appVersion: appVersion ?? ""
394
381
  });
395
- const getReleaseActivity = async (db, input, now, retention) => {
396
- const at = now();
397
- const kept = retained(at, retention);
398
- const data = input.scope !== void 0 ? [{
399
- scope: input.scope,
400
- metrics: await rangedMetrics(db, {
401
- scopeKind: "channel",
402
- releaseKind: "all",
403
- releaseId: "",
404
- channel: input.scope.channel,
405
- platform: input.scope.platform,
406
- appVersionKind: "all",
407
- appVersion: ""
408
- }, {
409
- parts: usageParts(input.scope.channel, input.scope.platform),
410
- field: "activity_users"
411
- }, input.timeRange, true, kept.hour)
412
- }] : await Promise.all(input.releases.map(async (release) => ({
413
- release,
414
- metrics: input.timeRange === void 0 ? await lifetimeMetrics(db, release) : await rangedMetrics(db, releaseParts(release), {
415
- parts: releaseParts(release),
416
- field: "launch_users"
417
- }, input.timeRange, false, kept.hour, input.intervalMs)
418
- })));
419
- const oldest = kept[input.scope === void 0 ? "hour" : "day"];
382
+ /** Each release's lifetime counters, which are kept: one logical row apiece. */
383
+ const getReleaseActivity = async (db, input, now) => {
384
+ const measuredAtMs = now();
420
385
  return {
421
- coverage: coverageOf(input.timeRange?.start, oldest),
422
- data,
423
- measuredAtMs: at
386
+ data: await Promise.all(input.releases.map(async (release) => ({
387
+ release,
388
+ metrics: await lifetimeMetrics(db, release)
389
+ }))),
390
+ measuredAtMs
424
391
  };
425
392
  };
426
393
  /**
@@ -680,42 +647,5 @@ const getUpdateFailures = async (db, input, now, retention) => {
680
647
  }
681
648
  };
682
649
  };
683
- /** Reads daily gauges, never raw events or mutable latest-state distribution. */
684
- const getDistributionHistory = async (db, input, now, retention) => {
685
- const at = now();
686
- const { start, end } = input.timeRange;
687
- const points = /* @__PURE__ */ new Map();
688
- for (let day = start; day < end; day += DAY_MS) points.set(day, []);
689
- const rows = await drain((page) => db.findAggregates("insights_distribution_history", {
690
- index: "byScope",
691
- where: {
692
- channel: input.channel,
693
- platform: input.platform
694
- },
695
- range: {
696
- gte: start,
697
- lt: end
698
- },
699
- limit: PAGE,
700
- ...page
701
- }));
702
- for (const row of rows) {
703
- if (row.installations <= 0) continue;
704
- points.get(row.bucket_start_ms).push({
705
- appVersion: row.app_version,
706
- releaseId: row.release_id || null,
707
- bundleKind: row.bundle_kind,
708
- installations: row.installations
709
- });
710
- }
711
- return {
712
- coverage: coverageOf(start, retained(at, retention).day),
713
- measuredAtMs: at,
714
- points: [...points].map(([startMs, bundles]) => ({
715
- startMs,
716
- bundles
717
- }))
718
- };
719
- };
720
650
  //#endregion
721
- export { UPDATE_FAILURES_RANGE_MS, countEvents, countLatestEvents, findLatestEvents, getAppUsage, getDistributionHistory, getReleaseActivity, getUpdateFailures, listEvents };
651
+ export { UPDATE_FAILURES_RANGE_MS, countEventSeries, countEvents, countLatestEvents, findLatestEvents, getAppUsage, getReleaseActivity, getUpdateFailures, listEvents };
@@ -2,7 +2,6 @@ const require_contract = require("./contract.cjs");
2
2
  const require_overview = require("./overview.cjs");
3
3
  const require_schema = require("./schema.cjs");
4
4
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
5
- let _hot_updater_protocol = require("@hot-updater/protocol");
6
5
  //#region src/server/recordEvent.ts
7
6
  const hourOf = (ms) => ms - ms % require_schema.HOUR_MS;
8
7
  const dayOf = (ms) => ms - ms % require_schema.DAY_MS;
@@ -90,11 +89,10 @@ const patchCounters = (event) => event.type !== "UPDATE_DOWNLOADED" ? {} : {
90
89
  };
91
90
  /**
92
91
  * Counters and sketches for one event: release, channel, and usage rows, with
93
- * day rollups for channel and usage. Usage rows are written for the event's
94
- * platform only, since a read for every platform merges the ios and android
95
- * sketches; a channel's active installations come from its usage rows, so
96
- * channel rows keep no sketch of their own. A download's patch counters go
97
- * in the rows that count it.
92
+ * day rollups for channel and usage. Only usage rows keep a sketch, of active
93
+ * installations, and only for the event's platform, since a read for every
94
+ * platform merges the ios and android sketches. A download's patch counters
95
+ * go in the rows that count it.
98
96
  */
99
97
  const countEvent = (tx, event) => {
100
98
  const shardBy = event.install_id;
@@ -120,11 +118,7 @@ const countEvent = (tx, event) => {
120
118
  ...delta.downloads === 0 ? {} : patchCounters(event)
121
119
  };
122
120
  if (Object.keys(counters).length > 0) tx.aggregate(models.counters, key, counters, { shardBy });
123
- const sketches = {
124
- ...delta.launchIdentity === void 0 || parts.scopeKind === "channel" ? {} : { launch_users: (0, _hot_updater_plugin_core.addDistinct)(null, delta.launchIdentity) },
125
- ...delta.activityIdentity === void 0 ? {} : { activity_users: (0, _hot_updater_plugin_core.addDistinct)(null, delta.activityIdentity) }
126
- };
127
- if (models.sketches !== void 0 && Object.keys(sketches).length > 0) tx.aggregate(models.sketches, key, sketches, { shardBy });
121
+ if (models.sketches !== void 0 && delta.activityIdentity !== void 0) tx.aggregate(models.sketches, key, { activity_users: (0, _hot_updater_plugin_core.addDistinct)(null, delta.activityIdentity) }, { shardBy });
128
122
  }
129
123
  }
130
124
  };
@@ -277,66 +271,21 @@ const REPEATED_FIELDS = [
277
271
  const repeatsHead = (event, head) => event.type === "UNCHANGED" && head.type !== "UPDATE_DOWNLOADED" && dayOf(event.received_at_ms) === dayOf(head.received_at_ms) && REPEATED_FIELDS.every((field) => event[field] === head[field]);
278
272
  /**
279
273
  * Records one event in one transaction: one batch read of the event and its
280
- * installation's latest and daily heads, then the gauge and sketch rows it
281
- * changes and one
274
+ * installation's head, one of the gauge and sketch rows it changes, then one
282
275
  * write. On a database that batches aggregates (DynamoDB, Firestore), the
283
276
  * write holds the event's rows and one log row instead, and a compaction
284
277
  * reads and writes the aggregate rows. A stored event's id changes nothing
285
278
  * when repeated, whichever installation sends it, and neither does the id
286
279
  * of the installation's head. An UNCHANGED report that repeats its head on
287
- * the same UTC day only advances its daily observation. An older event counts in its
280
+ * the same UTC day writes nothing at all. An older event still counts in its
288
281
  * own hour but never replaces the head. An update failure is stored and
289
282
  * counted, but changes what no installation runs, so it moves no head.
290
283
  */
291
284
  const recordEvent = (db, event) => {
292
285
  require_contract.assertBundleEventRow(event);
293
286
  return db.transaction(async (tx) => {
294
- const dailyKey = {
295
- install_id: event.install_id,
296
- bucket_start_ms: dayOf(event.received_at_ms)
297
- };
298
- const [existing, previous, daily] = await Promise.all([
299
- tx.findOne("bundle_events", { id: event.id }),
300
- tx.findOne("bundle_event_heads", { install_id: event.install_id }),
301
- event.type === "UPDATE_FAILED" ? null : tx.findOne("bundle_daily_heads", dailyKey)
302
- ]);
303
- if (existing !== null || previous?.id === event.id || daily?.id === event.id) return;
304
- if (event.type !== "UPDATE_FAILED" && (daily === null || isNewer(event, daily))) {
305
- const downloaded = event.type === "UPDATE_DOWNLOADED";
306
- const releaseId = downloaded ? event.from_release_id : event.to_release_id;
307
- const bundleId = downloaded ? event.from_bundle_id : event.to_bundle_id;
308
- const next = {
309
- ...dailyKey,
310
- id: event.id,
311
- received_at_ms: event.received_at_ms,
312
- channel: event.channel,
313
- platform: event.platform,
314
- app_version: event.app_version,
315
- release_id: releaseId ?? "",
316
- bundle_kind: bundleId === _hot_updater_protocol.NIL_UUID ? "builtin" : releaseId === null ? "unknown" : "release"
317
- };
318
- const count = (head, delta) => {
319
- tx.aggregate("insights_distribution_history", {
320
- channel: head.channel,
321
- platform: head.platform,
322
- app_version: head.app_version,
323
- release_id: head.release_id,
324
- bundle_kind: head.bundle_kind,
325
- bucket_start_ms: head.bucket_start_ms
326
- }, { installations: delta }, { shardBy: head.install_id });
327
- };
328
- if (daily !== null) {
329
- if (daily.channel !== next.channel || daily.platform !== next.platform || daily.app_version !== next.app_version || daily.release_id !== next.release_id || daily.bundle_kind !== next.bundle_kind) {
330
- count(daily, -1);
331
- count(next, 1);
332
- }
333
- const { install_id: _, bucket_start_ms: __, ...fields } = next;
334
- tx.update("bundle_daily_heads", daily, fields);
335
- } else {
336
- tx.create("bundle_daily_heads", next);
337
- count(next, 1);
338
- }
339
- }
287
+ const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
288
+ if (existing !== null || previous?.id === event.id) return;
340
289
  if (previous !== null && repeatsHead(event, previous)) return;
341
290
  if (event.type !== "UNCHANGED") {
342
291
  tx.create("bundle_events", event);