@hot-updater/plugin-insights 1.0.0-rc.37 → 1.0.0-rc.39

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.
@@ -123,7 +123,7 @@ type EventHistoryRow = {
123
123
  readonly patchFallback?: true;
124
124
  /** `UNCHANGED`: what the report changed, the reason it was kept. */
125
125
  readonly change?: EventHistoryChange;
126
- /** A download or apply that arrived after its target already ran: it counted nothing. */
126
+ /** A download or apply of a target already run or already downloaded: it counted nothing. */
127
127
  readonly late?: true;
128
128
  /** A launch or crash whose download report never arrived, counted with it. */
129
129
  readonly impliedDownload?: true;
@@ -179,6 +179,10 @@ type ReportingOverview = InsightsScope & {
179
179
  readonly bundle?: {
180
180
  readonly bundleId: string;
181
181
  readonly reportingInstallations: InsightsCountMeasurement;
182
+ /**
183
+ * Download reports of the bundle, and in whole hours the downloads a
184
+ * launch or crash implied when its download report never arrived.
185
+ */
182
186
  readonly downloadedReports: InsightsCountMeasurement;
183
187
  readonly appliedReports: InsightsCountMeasurement;
184
188
  readonly recoveredReports: InsightsCountMeasurement;
@@ -123,7 +123,7 @@ type EventHistoryRow = {
123
123
  readonly patchFallback?: true;
124
124
  /** `UNCHANGED`: what the report changed, the reason it was kept. */
125
125
  readonly change?: EventHistoryChange;
126
- /** A download or apply that arrived after its target already ran: it counted nothing. */
126
+ /** A download or apply of a target already run or already downloaded: it counted nothing. */
127
127
  readonly late?: true;
128
128
  /** A launch or crash whose download report never arrived, counted with it. */
129
129
  readonly impliedDownload?: true;
@@ -179,6 +179,10 @@ type ReportingOverview = InsightsScope & {
179
179
  readonly bundle?: {
180
180
  readonly bundleId: string;
181
181
  readonly reportingInstallations: InsightsCountMeasurement;
182
+ /**
183
+ * Download reports of the bundle, and in whole hours the downloads a
184
+ * launch or crash implied when its download report never arrived.
185
+ */
182
186
  readonly downloadedReports: InsightsCountMeasurement;
183
187
  readonly appliedReports: InsightsCountMeasurement;
184
188
  readonly recoveredReports: InsightsCountMeasurement;
@@ -62,9 +62,10 @@ type DatabaseBundleEventMetadata = DatabaseJsonObject & {
62
62
  /** A kept `UNCHANGED` row: what it changed, which the server sets. */
63
63
  readonly change?: BundleEventChange;
64
64
  /**
65
- * A download or apply that arrived after its installation already ran its
66
- * target, as when a reload cut the first runtime's report short: the
67
- * server keeps it in history, but it moves and counts nothing.
65
+ * A download or apply of a target its installation already ran, as when
66
+ * a reload cut the first runtime's report short, or a download that
67
+ * repeats the pending one: the server keeps it in history, but it moves
68
+ * and counts nothing.
68
69
  */
69
70
  readonly late?: true;
70
71
  /**
@@ -62,9 +62,10 @@ type DatabaseBundleEventMetadata = DatabaseJsonObject & {
62
62
  /** A kept `UNCHANGED` row: what it changed, which the server sets. */
63
63
  readonly change?: BundleEventChange;
64
64
  /**
65
- * A download or apply that arrived after its installation already ran its
66
- * target, as when a reload cut the first runtime's report short: the
67
- * server keeps it in history, but it moves and counts nothing.
65
+ * A download or apply of a target its installation already ran, as when
66
+ * a reload cut the first runtime's report short, or a download that
67
+ * repeats the pending one: the server keeps it in history, but it moves
68
+ * and counts nothing.
68
69
  */
69
70
  readonly late?: true;
70
71
  /**
@@ -152,7 +152,11 @@ const countRaw = async (db, filter, from, to) => {
152
152
  }
153
153
  return total;
154
154
  };
155
- /** Whole hours from outcome counters; the partial hours at either edge from raw events. */
155
+ /**
156
+ * Whole hours from outcome counters; the partial hours at either edge from
157
+ * raw events. A download filter's whole hours count the downloads a launch
158
+ * or crash implied too, which its raw events do not hold.
159
+ */
156
160
  const countEvents = async (db, input) => {
157
161
  const { filter, sinceMs, beforeReceivedAtMs } = input;
158
162
  const start = hourCeil(sinceMs);
@@ -171,8 +175,9 @@ const countEvents = async (db, input) => {
171
175
  return await countRaw(db, filter, sinceMs, start) + hours.reduce((sum, row) => sum + row.events, 0) + await countRaw(db, filter, end, beforeReceivedAtMs);
172
176
  };
173
177
  /**
174
- * A bundle filter's stored events in each interval of whole hours: one read
175
- * of its hourly counts, every interval present.
178
+ * A bundle filter's stored events in each interval of whole hours, and for a
179
+ * download filter the downloads a launch or crash implied: one read of its
180
+ * hourly counts, every interval present.
176
181
  */
177
182
  const countEventSeries = async (db, input) => {
178
183
  const { filter, intervalMs } = input;
@@ -152,7 +152,11 @@ const countRaw = async (db, filter, from, to) => {
152
152
  }
153
153
  return total;
154
154
  };
155
- /** Whole hours from outcome counters; the partial hours at either edge from raw events. */
155
+ /**
156
+ * Whole hours from outcome counters; the partial hours at either edge from
157
+ * raw events. A download filter's whole hours count the downloads a launch
158
+ * or crash implied too, which its raw events do not hold.
159
+ */
156
160
  const countEvents = async (db, input) => {
157
161
  const { filter, sinceMs, beforeReceivedAtMs } = input;
158
162
  const start = hourCeil(sinceMs);
@@ -171,8 +175,9 @@ const countEvents = async (db, input) => {
171
175
  return await countRaw(db, filter, sinceMs, start) + hours.reduce((sum, row) => sum + row.events, 0) + await countRaw(db, filter, end, beforeReceivedAtMs);
172
176
  };
173
177
  /**
174
- * A bundle filter's stored events in each interval of whole hours: one read
175
- * of its hourly counts, every interval present.
178
+ * A bundle filter's stored events in each interval of whole hours, and for a
179
+ * download filter the downloads a launch or crash implied: one read of its
180
+ * hourly counts, every interval present.
176
181
  */
177
182
  const countEventSeries = async (db, input) => {
178
183
  const { filter, intervalMs } = input;
@@ -137,17 +137,25 @@ const countEvent = (tx, event) => {
137
137
  };
138
138
  /**
139
139
  * A stored event's outcome rows: its bundle filter's hour, when it has one
140
- * (`bundleRefsOf`), and every stored event's UTC day.
140
+ * (`bundleRefsOf`), and every stored event's UTC day. A download that a
141
+ * launch or crash implied counts in its bundle's download hour too, in the
142
+ * hour of that launch or crash, so the download series covers what the
143
+ * release's downloads count; the launch or crash is its row.
141
144
  */
142
145
  const countOutcome = (tx, event) => {
143
146
  const shardBy = event.install_id;
144
- for (const bundleRef of require_schema.bundleRefsOf(event)) tx.aggregate("insights_outcomes", {
147
+ const hour = (type, bundleRef) => tx.aggregate("insights_outcomes", {
145
148
  platform: event.platform,
146
149
  channel: event.channel,
147
- type: event.type,
150
+ type,
148
151
  bundle_ref: bundleRef,
149
152
  bucket_start_ms: hourOf(event.received_at_ms)
150
153
  }, { events: 1 }, { shardBy });
154
+ for (const bundleRef of require_schema.bundleRefsOf(event)) hour(event.type, bundleRef);
155
+ if (event.metadata.implied_download === true) hour("UPDATE_DOWNLOADED", require_schema.bundleRefOfFilter({
156
+ type: "UPDATE_DOWNLOADED",
157
+ toBundleId: event.type === "RECOVERED" ? event.from_bundle_id : event.to_bundle_id
158
+ }));
151
159
  tx.aggregate("insights_outcomes", {
152
160
  ...require_schema.DAILY_EVENTS,
153
161
  bucket_start_ms: dayOf(event.received_at_ms)
@@ -262,19 +270,25 @@ const minBundleIdOf = (row) => row.metadata?.min_bundle_id;
262
270
  /** When the client made a report, from the UUIDv7 it gave it. */
263
271
  const madeAt = (id) => (0, _hot_updater_plugin_core.isUUIDv7)(id) ? (0, _hot_updater_plugin_core.extractTimestampFromUUIDv7)(id) : null;
264
272
  /**
265
- * A report that arrived after its installation moved past it: a download or
266
- * an apply of the bundle the head already runs, or an UNCHANGED report of the
267
- * bundle an apply head left, made before that apply. A reload can cut one
268
- * runtime's report short and deliver it after the next runtime's. It stays
269
- * in history but moves and counts nothing.
273
+ * A report its installation already moved past, which stays in history but
274
+ * moves and counts nothing:
275
+ * - a download or an apply of the bundle the head already runs, or a
276
+ * download that repeats the head's pending one, from the same bundle to
277
+ * the same bundle: the installation's download or launch already counted;
278
+ * - an UNCHANGED report made, by its UUIDv7, before the head's report, even
279
+ * after a later report replaced the apply it preceded: a reload can cut
280
+ * one runtime's report short and deliver it after the next runtime's.
281
+ *
282
+ * Report IDs are made on the device, so a device whose clock jumps back has
283
+ * its launch reports judged late until its clock passes its latest report.
270
284
  */
271
285
  const isLate = (event, head) => {
272
286
  if (head === null) return false;
273
- if (event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED") return event.to_bundle_id === runningOf(head).bundle_id;
274
- if (event.type === "UNCHANGED" && head.type === "UPDATE_APPLIED" && event.to_bundle_id === head.from_bundle_id) {
287
+ if (event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED") return event.to_bundle_id === runningOf(head).bundle_id || event.type === "UPDATE_DOWNLOADED" && head.type === "UPDATE_DOWNLOADED" && event.from_bundle_id === head.from_bundle_id && event.to_bundle_id === head.to_bundle_id;
288
+ if (event.type === "UNCHANGED") {
275
289
  const made = madeAt(event.id);
276
- const applied = madeAt(head.id);
277
- return made !== null && applied !== null && made < applied;
290
+ const latest = madeAt(head.id);
291
+ return made !== null && latest !== null && made < latest;
278
292
  }
279
293
  return false;
280
294
  };
@@ -1,5 +1,5 @@
1
1
  import { runningBuiltinBundleId } from "./eventRow.mjs";
2
- import { DAILY_EVENTS, DAY_MS, HOUR_MS, bundleRefsOf, isLaunch } from "./schema.mjs";
2
+ import { DAILY_EVENTS, DAY_MS, HOUR_MS, bundleRefOfFilter, bundleRefsOf, isLaunch } from "./schema.mjs";
3
3
  import { assertBundleEventRow } from "./contract.mjs";
4
4
  import { insightsKey, insightsOverviewDeltas, insightsOverviewId } from "./overview.mjs";
5
5
  import { addDistinct, compareUtf8, extractTimestampFromUUIDv7, isUUIDv7 } from "@hot-updater/plugin-core";
@@ -137,17 +137,25 @@ const countEvent = (tx, event) => {
137
137
  };
138
138
  /**
139
139
  * A stored event's outcome rows: its bundle filter's hour, when it has one
140
- * (`bundleRefsOf`), and every stored event's UTC day.
140
+ * (`bundleRefsOf`), and every stored event's UTC day. A download that a
141
+ * launch or crash implied counts in its bundle's download hour too, in the
142
+ * hour of that launch or crash, so the download series covers what the
143
+ * release's downloads count; the launch or crash is its row.
141
144
  */
142
145
  const countOutcome = (tx, event) => {
143
146
  const shardBy = event.install_id;
144
- for (const bundleRef of bundleRefsOf(event)) tx.aggregate("insights_outcomes", {
147
+ const hour = (type, bundleRef) => tx.aggregate("insights_outcomes", {
145
148
  platform: event.platform,
146
149
  channel: event.channel,
147
- type: event.type,
150
+ type,
148
151
  bundle_ref: bundleRef,
149
152
  bucket_start_ms: hourOf(event.received_at_ms)
150
153
  }, { events: 1 }, { shardBy });
154
+ for (const bundleRef of bundleRefsOf(event)) hour(event.type, bundleRef);
155
+ if (event.metadata.implied_download === true) hour("UPDATE_DOWNLOADED", bundleRefOfFilter({
156
+ type: "UPDATE_DOWNLOADED",
157
+ toBundleId: event.type === "RECOVERED" ? event.from_bundle_id : event.to_bundle_id
158
+ }));
151
159
  tx.aggregate("insights_outcomes", {
152
160
  ...DAILY_EVENTS,
153
161
  bucket_start_ms: dayOf(event.received_at_ms)
@@ -262,19 +270,25 @@ const minBundleIdOf = (row) => row.metadata?.min_bundle_id;
262
270
  /** When the client made a report, from the UUIDv7 it gave it. */
263
271
  const madeAt = (id) => isUUIDv7(id) ? extractTimestampFromUUIDv7(id) : null;
264
272
  /**
265
- * A report that arrived after its installation moved past it: a download or
266
- * an apply of the bundle the head already runs, or an UNCHANGED report of the
267
- * bundle an apply head left, made before that apply. A reload can cut one
268
- * runtime's report short and deliver it after the next runtime's. It stays
269
- * in history but moves and counts nothing.
273
+ * A report its installation already moved past, which stays in history but
274
+ * moves and counts nothing:
275
+ * - a download or an apply of the bundle the head already runs, or a
276
+ * download that repeats the head's pending one, from the same bundle to
277
+ * the same bundle: the installation's download or launch already counted;
278
+ * - an UNCHANGED report made, by its UUIDv7, before the head's report, even
279
+ * after a later report replaced the apply it preceded: a reload can cut
280
+ * one runtime's report short and deliver it after the next runtime's.
281
+ *
282
+ * Report IDs are made on the device, so a device whose clock jumps back has
283
+ * its launch reports judged late until its clock passes its latest report.
270
284
  */
271
285
  const isLate = (event, head) => {
272
286
  if (head === null) return false;
273
- if (event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED") return event.to_bundle_id === runningOf(head).bundle_id;
274
- if (event.type === "UNCHANGED" && head.type === "UPDATE_APPLIED" && event.to_bundle_id === head.from_bundle_id) {
287
+ if (event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED") return event.to_bundle_id === runningOf(head).bundle_id || event.type === "UPDATE_DOWNLOADED" && head.type === "UPDATE_DOWNLOADED" && event.from_bundle_id === head.from_bundle_id && event.to_bundle_id === head.to_bundle_id;
288
+ if (event.type === "UNCHANGED") {
275
289
  const made = madeAt(event.id);
276
- const applied = madeAt(head.id);
277
- return made !== null && applied !== null && made < applied;
290
+ const latest = madeAt(head.id);
291
+ return made !== null && latest !== null && made < latest;
278
292
  }
279
293
  return false;
280
294
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/plugin-insights",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.37",
4
+ "version": "1.0.0-rc.39",
5
5
  "description": "Hot Updater's Insights plugin: launch, download, and update failure events, for the server and the React Native app",
6
6
  "exports": {
7
7
  "./client": {
@@ -33,10 +33,10 @@
33
33
  "package.json"
34
34
  ],
35
35
  "dependencies": {
36
- "@hot-updater/protocol": "1.0.0-rc.37"
36
+ "@hot-updater/protocol": "1.0.0-rc.39"
37
37
  },
38
38
  "peerDependencies": {
39
- "@hot-updater/plugin-core": "^1.0.0-rc.37"
39
+ "@hot-updater/plugin-core": "^1.0.0-rc.39"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@hot-updater/plugin-core": {
@@ -46,8 +46,8 @@
46
46
  "devDependencies": {
47
47
  "@types/node": "^20",
48
48
  "vitest": "4.1.4",
49
- "@hot-updater/test-utils": "1.0.0-rc.37",
50
- "@hot-updater/plugin-core": "1.0.0-rc.37"
49
+ "@hot-updater/plugin-core": "1.0.0-rc.39",
50
+ "@hot-updater/test-utils": "1.0.0-rc.39"
51
51
  },
52
52
  "nx": {
53
53
  "implicitDependencies": [