@hot-updater/plugin-insights 1.0.0-rc.35 → 1.0.0-rc.37
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/dist/client/index.cjs +2 -1
- package/dist/client/index.mjs +2 -1
- package/dist/server/contract.cjs +8 -4
- package/dist/server/contract.mjs +8 -4
- package/dist/server/domain.d.cts +40 -4
- package/dist/server/domain.d.mts +40 -4
- package/dist/server/eventInput.cjs +7 -0
- package/dist/server/eventInput.mjs +7 -0
- package/dist/server/eventRow.cjs +24 -2
- package/dist/server/eventRow.d.cts +35 -1
- package/dist/server/eventRow.d.mts +35 -1
- package/dist/server/eventRow.mjs +24 -3
- package/dist/server/index.cjs +1 -1
- package/dist/server/index.d.cts +34 -2
- package/dist/server/index.d.mts +34 -2
- package/dist/server/index.mjs +1 -1
- package/dist/server/modelTypes.d.cts +6 -2
- package/dist/server/modelTypes.d.mts +6 -2
- package/dist/server/overview.cjs +7 -5
- package/dist/server/overview.d.cts +3 -3
- package/dist/server/overview.d.mts +3 -3
- package/dist/server/overview.mjs +7 -5
- package/dist/server/provider.cjs +15 -1
- package/dist/server/provider.mjs +15 -1
- package/dist/server/reads.cjs +60 -29
- package/dist/server/reads.mjs +60 -29
- package/dist/server/recordEvent.cjs +156 -22
- package/dist/server/recordEvent.mjs +157 -23
- package/dist/server/schema.cjs +96 -10
- package/dist/server/schema.d.cts +64 -0
- package/dist/server/schema.d.mts +64 -0
- package/dist/server/schema.mjs +93 -11
- package/package.json +5 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
const require_eventRow = require("./eventRow.cjs");
|
|
2
|
+
const require_schema = require("./schema.cjs");
|
|
1
3
|
const require_contract = require("./contract.cjs");
|
|
2
4
|
const require_overview = require("./overview.cjs");
|
|
3
|
-
const require_schema = require("./schema.cjs");
|
|
4
5
|
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
5
6
|
//#region src/server/recordEvent.ts
|
|
6
7
|
const hourOf = (ms) => ms - ms % require_schema.HOUR_MS;
|
|
@@ -13,13 +14,16 @@ const insightsIdentity = (identity) => require_overview.insightsOverviewId({
|
|
|
13
14
|
bucketStartMs: 0
|
|
14
15
|
});
|
|
15
16
|
/**
|
|
16
|
-
* The bucket a head's gauges count it in, the UTC day of its event, and
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* The bucket a head's gauges count it in, the UTC day of its event, and what
|
|
18
|
+
* its distribution row names: the release it runs, which a download leaves
|
|
19
|
+
* at its source, and the built-in bundle, when it runs the native build's.
|
|
20
|
+
* A head stored without the built-in bundle ID counts in no built-in row, so
|
|
21
|
+
* taking it back moves none.
|
|
19
22
|
*/
|
|
20
23
|
const gaugeSlot = (head) => ({
|
|
21
24
|
bucket: dayOf(head.received_at_ms),
|
|
22
|
-
releaseId: head.type === "UPDATE_DOWNLOADED" ? head.from_release_id : head.to_release_id
|
|
25
|
+
releaseId: head.type === "UPDATE_DOWNLOADED" ? head.from_release_id : head.to_release_id,
|
|
26
|
+
builtinBundleId: require_eventRow.runningBuiltinBundleId(head)
|
|
23
27
|
});
|
|
24
28
|
/**
|
|
25
29
|
* The `insights_latest_by_bundle` field of a head's (from, to) pair. A head
|
|
@@ -30,11 +34,12 @@ const PAIR_FIELD = "from_to";
|
|
|
30
34
|
/** The pair gauge's `bundle_id`: a hash of the two bundle ids, which fit no single 36-character column. */
|
|
31
35
|
const bundlePairKey = (from, to) => require_overview.insightsKey(`${from.length}:${from}${to.length}:${to}`);
|
|
32
36
|
/**
|
|
33
|
-
* Moves a head's gauges: the distribution row,
|
|
34
|
-
*
|
|
37
|
+
* Moves a head's gauges: the distribution row, and the built-in one when it
|
|
38
|
+
* runs its build's built-in bundle, one row per bundle it references, and
|
|
39
|
+
* its pair; -1 takes back the head an event replaces.
|
|
35
40
|
*/
|
|
36
41
|
const countHead = (tx, head, delta) => {
|
|
37
|
-
const { bucket, releaseId } = gaugeSlot(head);
|
|
42
|
+
const { bucket, releaseId, builtinBundleId } = gaugeSlot(head);
|
|
38
43
|
const shardBy = head.install_id;
|
|
39
44
|
tx.aggregate("insights_distribution", {
|
|
40
45
|
channel: head.channel,
|
|
@@ -43,6 +48,14 @@ const countHead = (tx, head, delta) => {
|
|
|
43
48
|
release_id: releaseId ?? "",
|
|
44
49
|
bucket_start_ms: bucket
|
|
45
50
|
}, { latest_installations: delta }, { shardBy });
|
|
51
|
+
if (builtinBundleId !== null) tx.aggregate("insights_builtin_distribution", {
|
|
52
|
+
channel: head.channel,
|
|
53
|
+
platform: head.platform,
|
|
54
|
+
app_version: head.app_version,
|
|
55
|
+
release_id: releaseId ?? "",
|
|
56
|
+
builtin_bundle_id: builtinBundleId,
|
|
57
|
+
bucket_start_ms: bucket
|
|
58
|
+
}, { latest_installations: delta }, { shardBy });
|
|
46
59
|
for (const field of ["from_bundle_id", "to_bundle_id"]) {
|
|
47
60
|
const bundleId = head[field];
|
|
48
61
|
if (bundleId === null) continue;
|
|
@@ -123,16 +136,16 @@ const countEvent = (tx, event) => {
|
|
|
123
136
|
}
|
|
124
137
|
};
|
|
125
138
|
/**
|
|
126
|
-
* A stored event's outcome rows: its bundle filter's hour,
|
|
127
|
-
*
|
|
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.
|
|
128
141
|
*/
|
|
129
142
|
const countOutcome = (tx, event) => {
|
|
130
143
|
const shardBy = event.install_id;
|
|
131
|
-
|
|
144
|
+
for (const bundleRef of require_schema.bundleRefsOf(event)) tx.aggregate("insights_outcomes", {
|
|
132
145
|
platform: event.platform,
|
|
133
146
|
channel: event.channel,
|
|
134
147
|
type: event.type,
|
|
135
|
-
bundle_ref:
|
|
148
|
+
bundle_ref: bundleRef,
|
|
136
149
|
bucket_start_ms: hourOf(event.received_at_ms)
|
|
137
150
|
}, { events: 1 }, { shardBy });
|
|
138
151
|
tx.aggregate("insights_outcomes", {
|
|
@@ -236,6 +249,126 @@ const countFailure = (tx, event) => {
|
|
|
236
249
|
}, { events: 1 }, { shardBy });
|
|
237
250
|
};
|
|
238
251
|
const isNewer = (event, head) => event.received_at_ms !== head.received_at_ms ? event.received_at_ms > head.received_at_ms : (0, _hot_updater_plugin_core.compareUtf8)(event.id, head.id) > 0;
|
|
252
|
+
/** What a head says its installation runs: a download leaves it at the source. */
|
|
253
|
+
const runningOf = (head) => head.type === "UPDATE_DOWNLOADED" ? {
|
|
254
|
+
bundle_id: head.from_bundle_id,
|
|
255
|
+
release_id: head.from_release_id
|
|
256
|
+
} : {
|
|
257
|
+
bundle_id: head.to_bundle_id,
|
|
258
|
+
release_id: head.to_release_id
|
|
259
|
+
};
|
|
260
|
+
/** The native build a report or head came from, when its SDK says. */
|
|
261
|
+
const minBundleIdOf = (row) => row.metadata?.min_bundle_id;
|
|
262
|
+
/** When the client made a report, from the UUIDv7 it gave it. */
|
|
263
|
+
const madeAt = (id) => (0, _hot_updater_plugin_core.isUUIDv7)(id) ? (0, _hot_updater_plugin_core.extractTimestampFromUUIDv7)(id) : null;
|
|
264
|
+
/**
|
|
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.
|
|
270
|
+
*/
|
|
271
|
+
const isLate = (event, head) => {
|
|
272
|
+
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) {
|
|
275
|
+
const made = madeAt(event.id);
|
|
276
|
+
const applied = madeAt(head.id);
|
|
277
|
+
return made !== null && applied !== null && made < applied;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* What an UNCHANGED report changes against its installation's head: the
|
|
283
|
+
* running bundle or release, the app version, the channel, or the native
|
|
284
|
+
* build (when both name it), with the head's values before it; `first_seen`
|
|
285
|
+
* when the installation has no head. A report the head already answers, one
|
|
286
|
+
* older than the head, a user switch and any other event type change
|
|
287
|
+
* nothing, so no row keeps them.
|
|
288
|
+
*/
|
|
289
|
+
const changeOf = (event, head) => {
|
|
290
|
+
if (event.type !== "UNCHANGED") return null;
|
|
291
|
+
if (head === null) return {
|
|
292
|
+
kinds: ["first_seen"],
|
|
293
|
+
previous: null
|
|
294
|
+
};
|
|
295
|
+
if (!isNewer(event, head)) return null;
|
|
296
|
+
const running = runningOf(head);
|
|
297
|
+
const build = minBundleIdOf(event);
|
|
298
|
+
const headBuild = minBundleIdOf(head);
|
|
299
|
+
const kinds = [];
|
|
300
|
+
if (event.to_bundle_id !== running.bundle_id) kinds.push("bundle");
|
|
301
|
+
if (event.to_release_id !== running.release_id) kinds.push("release");
|
|
302
|
+
if (event.app_version !== head.app_version) kinds.push("app_version");
|
|
303
|
+
if (event.channel !== head.channel) kinds.push("channel");
|
|
304
|
+
if (build !== void 0 && headBuild !== void 0 && build !== headBuild) kinds.push("native_build");
|
|
305
|
+
return kinds.length === 0 ? null : {
|
|
306
|
+
kinds,
|
|
307
|
+
previous: {
|
|
308
|
+
bundle_id: running.bundle_id,
|
|
309
|
+
release_id: running.release_id,
|
|
310
|
+
app_version: head.app_version,
|
|
311
|
+
channel: head.channel,
|
|
312
|
+
...headBuild === void 0 ? {} : { min_bundle_id: headBuild }
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Whether a launch or a crash comes with no download report for its target:
|
|
318
|
+
* its head shows neither that download nor the target already running. The
|
|
319
|
+
* native build's built-in bundle is never downloaded, and without a head the
|
|
320
|
+
* server knows nothing of the installation's past, so it implies nothing.
|
|
321
|
+
*/
|
|
322
|
+
const impliesDownload = (event, head) => {
|
|
323
|
+
const target = event.type === "RECOVERED" ? {
|
|
324
|
+
bundle: event.from_bundle_id,
|
|
325
|
+
release: event.from_release_id
|
|
326
|
+
} : require_schema.isLaunch(event) ? {
|
|
327
|
+
bundle: event.to_bundle_id,
|
|
328
|
+
release: event.to_release_id
|
|
329
|
+
} : null;
|
|
330
|
+
if (target === null || target.release === null || target.bundle === minBundleIdOf(event)) return false;
|
|
331
|
+
if (head === null) return false;
|
|
332
|
+
if (head.type === "UPDATE_DOWNLOADED" && head.to_bundle_id === target.bundle) return false;
|
|
333
|
+
return runningOf(head).bundle_id !== target.bundle;
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* The event as recorded, with what only the server sets: what a kept
|
|
337
|
+
* UNCHANGED report changed, a late report's flag, and a download its launch
|
|
338
|
+
* or crash implied.
|
|
339
|
+
*/
|
|
340
|
+
const recordedOf = (event, head) => {
|
|
341
|
+
const { change: _change, late: _late, implied_download: _implied, ...metadata } = event.metadata;
|
|
342
|
+
const reported = {
|
|
343
|
+
...event,
|
|
344
|
+
metadata
|
|
345
|
+
};
|
|
346
|
+
const late = isLate(reported, head);
|
|
347
|
+
const change = late ? null : changeOf(reported, head);
|
|
348
|
+
const withChange = change === null ? reported : {
|
|
349
|
+
...reported,
|
|
350
|
+
metadata: {
|
|
351
|
+
...metadata,
|
|
352
|
+
change
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
recorded: late && reported.type !== "UNCHANGED" ? {
|
|
357
|
+
...reported,
|
|
358
|
+
metadata: {
|
|
359
|
+
...metadata,
|
|
360
|
+
late: true
|
|
361
|
+
}
|
|
362
|
+
} : !late && impliesDownload(withChange, head) ? {
|
|
363
|
+
...withChange,
|
|
364
|
+
metadata: {
|
|
365
|
+
...withChange.metadata,
|
|
366
|
+
implied_download: true
|
|
367
|
+
}
|
|
368
|
+
} : withChange,
|
|
369
|
+
late
|
|
370
|
+
};
|
|
371
|
+
};
|
|
239
372
|
/** What an UNCHANGED report must share with its installation's head to repeat it. */
|
|
240
373
|
const REPEATED_FIELDS = [
|
|
241
374
|
"channel",
|
|
@@ -270,23 +403,24 @@ const recordEvent = (db, event) => {
|
|
|
270
403
|
const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
|
|
271
404
|
if (existing !== null || previous?.id === event.id) return;
|
|
272
405
|
if (previous !== null && repeatsHead(event, previous)) return;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
406
|
+
const { recorded, late } = recordedOf(event, previous);
|
|
407
|
+
if (recorded.type !== "UNCHANGED" || recorded.metadata.change !== void 0 && !late) {
|
|
408
|
+
tx.create("bundle_events", recorded);
|
|
409
|
+
countOutcome(tx, recorded);
|
|
276
410
|
}
|
|
277
|
-
if (
|
|
278
|
-
countFailure(tx,
|
|
411
|
+
if (recorded.type === "UPDATE_FAILED") {
|
|
412
|
+
countFailure(tx, recorded);
|
|
279
413
|
return;
|
|
280
414
|
}
|
|
281
|
-
countEvent(tx,
|
|
282
|
-
if (previous !== null && !isNewer(
|
|
283
|
-
if (previous === null) tx.create("bundle_event_heads",
|
|
415
|
+
countEvent(tx, recorded);
|
|
416
|
+
if (previous !== null && (late || !isNewer(recorded, previous))) return;
|
|
417
|
+
if (previous === null) tx.create("bundle_event_heads", recorded);
|
|
284
418
|
else {
|
|
285
|
-
const { install_id: _, ...fields } =
|
|
419
|
+
const { install_id: _, ...fields } = recorded;
|
|
286
420
|
countHead(tx, previous, -1);
|
|
287
421
|
tx.update("bundle_event_heads", previous, fields);
|
|
288
422
|
}
|
|
289
|
-
countHead(tx,
|
|
423
|
+
countHead(tx, recorded, 1);
|
|
290
424
|
});
|
|
291
425
|
};
|
|
292
426
|
//#endregion
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { runningBuiltinBundleId } from "./eventRow.mjs";
|
|
2
|
+
import { DAILY_EVENTS, DAY_MS, HOUR_MS, bundleRefsOf, isLaunch } from "./schema.mjs";
|
|
1
3
|
import { assertBundleEventRow } from "./contract.mjs";
|
|
2
4
|
import { insightsKey, insightsOverviewDeltas, insightsOverviewId } from "./overview.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { addDistinct, compareUtf8 } from "@hot-updater/plugin-core";
|
|
5
|
+
import { addDistinct, compareUtf8, extractTimestampFromUUIDv7, isUUIDv7 } from "@hot-updater/plugin-core";
|
|
5
6
|
//#region src/server/recordEvent.ts
|
|
6
7
|
const hourOf = (ms) => ms - ms % HOUR_MS;
|
|
7
8
|
const dayOf = (ms) => ms - ms % DAY_MS;
|
|
@@ -13,13 +14,16 @@ const insightsIdentity = (identity) => insightsOverviewId({
|
|
|
13
14
|
bucketStartMs: 0
|
|
14
15
|
});
|
|
15
16
|
/**
|
|
16
|
-
* The bucket a head's gauges count it in, the UTC day of its event, and
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* The bucket a head's gauges count it in, the UTC day of its event, and what
|
|
18
|
+
* its distribution row names: the release it runs, which a download leaves
|
|
19
|
+
* at its source, and the built-in bundle, when it runs the native build's.
|
|
20
|
+
* A head stored without the built-in bundle ID counts in no built-in row, so
|
|
21
|
+
* taking it back moves none.
|
|
19
22
|
*/
|
|
20
23
|
const gaugeSlot = (head) => ({
|
|
21
24
|
bucket: dayOf(head.received_at_ms),
|
|
22
|
-
releaseId: head.type === "UPDATE_DOWNLOADED" ? head.from_release_id : head.to_release_id
|
|
25
|
+
releaseId: head.type === "UPDATE_DOWNLOADED" ? head.from_release_id : head.to_release_id,
|
|
26
|
+
builtinBundleId: runningBuiltinBundleId(head)
|
|
23
27
|
});
|
|
24
28
|
/**
|
|
25
29
|
* The `insights_latest_by_bundle` field of a head's (from, to) pair. A head
|
|
@@ -30,11 +34,12 @@ const PAIR_FIELD = "from_to";
|
|
|
30
34
|
/** The pair gauge's `bundle_id`: a hash of the two bundle ids, which fit no single 36-character column. */
|
|
31
35
|
const bundlePairKey = (from, to) => insightsKey(`${from.length}:${from}${to.length}:${to}`);
|
|
32
36
|
/**
|
|
33
|
-
* Moves a head's gauges: the distribution row,
|
|
34
|
-
*
|
|
37
|
+
* Moves a head's gauges: the distribution row, and the built-in one when it
|
|
38
|
+
* runs its build's built-in bundle, one row per bundle it references, and
|
|
39
|
+
* its pair; -1 takes back the head an event replaces.
|
|
35
40
|
*/
|
|
36
41
|
const countHead = (tx, head, delta) => {
|
|
37
|
-
const { bucket, releaseId } = gaugeSlot(head);
|
|
42
|
+
const { bucket, releaseId, builtinBundleId } = gaugeSlot(head);
|
|
38
43
|
const shardBy = head.install_id;
|
|
39
44
|
tx.aggregate("insights_distribution", {
|
|
40
45
|
channel: head.channel,
|
|
@@ -43,6 +48,14 @@ const countHead = (tx, head, delta) => {
|
|
|
43
48
|
release_id: releaseId ?? "",
|
|
44
49
|
bucket_start_ms: bucket
|
|
45
50
|
}, { latest_installations: delta }, { shardBy });
|
|
51
|
+
if (builtinBundleId !== null) tx.aggregate("insights_builtin_distribution", {
|
|
52
|
+
channel: head.channel,
|
|
53
|
+
platform: head.platform,
|
|
54
|
+
app_version: head.app_version,
|
|
55
|
+
release_id: releaseId ?? "",
|
|
56
|
+
builtin_bundle_id: builtinBundleId,
|
|
57
|
+
bucket_start_ms: bucket
|
|
58
|
+
}, { latest_installations: delta }, { shardBy });
|
|
46
59
|
for (const field of ["from_bundle_id", "to_bundle_id"]) {
|
|
47
60
|
const bundleId = head[field];
|
|
48
61
|
if (bundleId === null) continue;
|
|
@@ -123,16 +136,16 @@ const countEvent = (tx, event) => {
|
|
|
123
136
|
}
|
|
124
137
|
};
|
|
125
138
|
/**
|
|
126
|
-
* A stored event's outcome rows: its bundle filter's hour,
|
|
127
|
-
*
|
|
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.
|
|
128
141
|
*/
|
|
129
142
|
const countOutcome = (tx, event) => {
|
|
130
143
|
const shardBy = event.install_id;
|
|
131
|
-
|
|
144
|
+
for (const bundleRef of bundleRefsOf(event)) tx.aggregate("insights_outcomes", {
|
|
132
145
|
platform: event.platform,
|
|
133
146
|
channel: event.channel,
|
|
134
147
|
type: event.type,
|
|
135
|
-
bundle_ref:
|
|
148
|
+
bundle_ref: bundleRef,
|
|
136
149
|
bucket_start_ms: hourOf(event.received_at_ms)
|
|
137
150
|
}, { events: 1 }, { shardBy });
|
|
138
151
|
tx.aggregate("insights_outcomes", {
|
|
@@ -236,6 +249,126 @@ const countFailure = (tx, event) => {
|
|
|
236
249
|
}, { events: 1 }, { shardBy });
|
|
237
250
|
};
|
|
238
251
|
const isNewer = (event, head) => event.received_at_ms !== head.received_at_ms ? event.received_at_ms > head.received_at_ms : compareUtf8(event.id, head.id) > 0;
|
|
252
|
+
/** What a head says its installation runs: a download leaves it at the source. */
|
|
253
|
+
const runningOf = (head) => head.type === "UPDATE_DOWNLOADED" ? {
|
|
254
|
+
bundle_id: head.from_bundle_id,
|
|
255
|
+
release_id: head.from_release_id
|
|
256
|
+
} : {
|
|
257
|
+
bundle_id: head.to_bundle_id,
|
|
258
|
+
release_id: head.to_release_id
|
|
259
|
+
};
|
|
260
|
+
/** The native build a report or head came from, when its SDK says. */
|
|
261
|
+
const minBundleIdOf = (row) => row.metadata?.min_bundle_id;
|
|
262
|
+
/** When the client made a report, from the UUIDv7 it gave it. */
|
|
263
|
+
const madeAt = (id) => isUUIDv7(id) ? extractTimestampFromUUIDv7(id) : null;
|
|
264
|
+
/**
|
|
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.
|
|
270
|
+
*/
|
|
271
|
+
const isLate = (event, head) => {
|
|
272
|
+
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) {
|
|
275
|
+
const made = madeAt(event.id);
|
|
276
|
+
const applied = madeAt(head.id);
|
|
277
|
+
return made !== null && applied !== null && made < applied;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* What an UNCHANGED report changes against its installation's head: the
|
|
283
|
+
* running bundle or release, the app version, the channel, or the native
|
|
284
|
+
* build (when both name it), with the head's values before it; `first_seen`
|
|
285
|
+
* when the installation has no head. A report the head already answers, one
|
|
286
|
+
* older than the head, a user switch and any other event type change
|
|
287
|
+
* nothing, so no row keeps them.
|
|
288
|
+
*/
|
|
289
|
+
const changeOf = (event, head) => {
|
|
290
|
+
if (event.type !== "UNCHANGED") return null;
|
|
291
|
+
if (head === null) return {
|
|
292
|
+
kinds: ["first_seen"],
|
|
293
|
+
previous: null
|
|
294
|
+
};
|
|
295
|
+
if (!isNewer(event, head)) return null;
|
|
296
|
+
const running = runningOf(head);
|
|
297
|
+
const build = minBundleIdOf(event);
|
|
298
|
+
const headBuild = minBundleIdOf(head);
|
|
299
|
+
const kinds = [];
|
|
300
|
+
if (event.to_bundle_id !== running.bundle_id) kinds.push("bundle");
|
|
301
|
+
if (event.to_release_id !== running.release_id) kinds.push("release");
|
|
302
|
+
if (event.app_version !== head.app_version) kinds.push("app_version");
|
|
303
|
+
if (event.channel !== head.channel) kinds.push("channel");
|
|
304
|
+
if (build !== void 0 && headBuild !== void 0 && build !== headBuild) kinds.push("native_build");
|
|
305
|
+
return kinds.length === 0 ? null : {
|
|
306
|
+
kinds,
|
|
307
|
+
previous: {
|
|
308
|
+
bundle_id: running.bundle_id,
|
|
309
|
+
release_id: running.release_id,
|
|
310
|
+
app_version: head.app_version,
|
|
311
|
+
channel: head.channel,
|
|
312
|
+
...headBuild === void 0 ? {} : { min_bundle_id: headBuild }
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Whether a launch or a crash comes with no download report for its target:
|
|
318
|
+
* its head shows neither that download nor the target already running. The
|
|
319
|
+
* native build's built-in bundle is never downloaded, and without a head the
|
|
320
|
+
* server knows nothing of the installation's past, so it implies nothing.
|
|
321
|
+
*/
|
|
322
|
+
const impliesDownload = (event, head) => {
|
|
323
|
+
const target = event.type === "RECOVERED" ? {
|
|
324
|
+
bundle: event.from_bundle_id,
|
|
325
|
+
release: event.from_release_id
|
|
326
|
+
} : isLaunch(event) ? {
|
|
327
|
+
bundle: event.to_bundle_id,
|
|
328
|
+
release: event.to_release_id
|
|
329
|
+
} : null;
|
|
330
|
+
if (target === null || target.release === null || target.bundle === minBundleIdOf(event)) return false;
|
|
331
|
+
if (head === null) return false;
|
|
332
|
+
if (head.type === "UPDATE_DOWNLOADED" && head.to_bundle_id === target.bundle) return false;
|
|
333
|
+
return runningOf(head).bundle_id !== target.bundle;
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* The event as recorded, with what only the server sets: what a kept
|
|
337
|
+
* UNCHANGED report changed, a late report's flag, and a download its launch
|
|
338
|
+
* or crash implied.
|
|
339
|
+
*/
|
|
340
|
+
const recordedOf = (event, head) => {
|
|
341
|
+
const { change: _change, late: _late, implied_download: _implied, ...metadata } = event.metadata;
|
|
342
|
+
const reported = {
|
|
343
|
+
...event,
|
|
344
|
+
metadata
|
|
345
|
+
};
|
|
346
|
+
const late = isLate(reported, head);
|
|
347
|
+
const change = late ? null : changeOf(reported, head);
|
|
348
|
+
const withChange = change === null ? reported : {
|
|
349
|
+
...reported,
|
|
350
|
+
metadata: {
|
|
351
|
+
...metadata,
|
|
352
|
+
change
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
recorded: late && reported.type !== "UNCHANGED" ? {
|
|
357
|
+
...reported,
|
|
358
|
+
metadata: {
|
|
359
|
+
...metadata,
|
|
360
|
+
late: true
|
|
361
|
+
}
|
|
362
|
+
} : !late && impliesDownload(withChange, head) ? {
|
|
363
|
+
...withChange,
|
|
364
|
+
metadata: {
|
|
365
|
+
...withChange.metadata,
|
|
366
|
+
implied_download: true
|
|
367
|
+
}
|
|
368
|
+
} : withChange,
|
|
369
|
+
late
|
|
370
|
+
};
|
|
371
|
+
};
|
|
239
372
|
/** What an UNCHANGED report must share with its installation's head to repeat it. */
|
|
240
373
|
const REPEATED_FIELDS = [
|
|
241
374
|
"channel",
|
|
@@ -270,23 +403,24 @@ const recordEvent = (db, event) => {
|
|
|
270
403
|
const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
|
|
271
404
|
if (existing !== null || previous?.id === event.id) return;
|
|
272
405
|
if (previous !== null && repeatsHead(event, previous)) return;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
406
|
+
const { recorded, late } = recordedOf(event, previous);
|
|
407
|
+
if (recorded.type !== "UNCHANGED" || recorded.metadata.change !== void 0 && !late) {
|
|
408
|
+
tx.create("bundle_events", recorded);
|
|
409
|
+
countOutcome(tx, recorded);
|
|
276
410
|
}
|
|
277
|
-
if (
|
|
278
|
-
countFailure(tx,
|
|
411
|
+
if (recorded.type === "UPDATE_FAILED") {
|
|
412
|
+
countFailure(tx, recorded);
|
|
279
413
|
return;
|
|
280
414
|
}
|
|
281
|
-
countEvent(tx,
|
|
282
|
-
if (previous !== null && !isNewer(
|
|
283
|
-
if (previous === null) tx.create("bundle_event_heads",
|
|
415
|
+
countEvent(tx, recorded);
|
|
416
|
+
if (previous !== null && (late || !isNewer(recorded, previous))) return;
|
|
417
|
+
if (previous === null) tx.create("bundle_event_heads", recorded);
|
|
284
418
|
else {
|
|
285
|
-
const { install_id: _, ...fields } =
|
|
419
|
+
const { install_id: _, ...fields } = recorded;
|
|
286
420
|
countHead(tx, previous, -1);
|
|
287
421
|
tx.update("bundle_event_heads", previous, fields);
|
|
288
422
|
}
|
|
289
|
-
countHead(tx,
|
|
423
|
+
countHead(tx, recorded, 1);
|
|
290
424
|
});
|
|
291
425
|
};
|
|
292
426
|
//#endregion
|
package/dist/server/schema.cjs
CHANGED
|
@@ -10,18 +10,53 @@ const bucketRetention = (days) => ({
|
|
|
10
10
|
field: "bucket_start_ms",
|
|
11
11
|
days
|
|
12
12
|
});
|
|
13
|
+
/** Every kind of row the table keeps belongs in installation history. */
|
|
13
14
|
const MOVEMENTS = /* @__PURE__ */ new Set([
|
|
14
15
|
"UPDATE_DOWNLOADED",
|
|
15
16
|
"UPDATE_APPLIED",
|
|
16
17
|
"RECOVERED",
|
|
17
|
-
"UPDATE_FAILED"
|
|
18
|
+
"UPDATE_FAILED",
|
|
19
|
+
"UNCHANGED"
|
|
18
20
|
]);
|
|
19
21
|
/** A failed update check: it names no target bundle, so no bundle list shows it. */
|
|
20
22
|
const isFailedCheck = (row) => row.type === "UPDATE_FAILED" && row.metadata?.failure?.stage === "check";
|
|
23
|
+
const metadataOf = (row) => row.metadata ?? {};
|
|
24
|
+
/** A download or apply that arrived after its target ran: it counts nothing. */
|
|
25
|
+
const isLateEvent = (row) => metadataOf(row).late === true;
|
|
21
26
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* Whether a stored row launched its installation into a bundle release: an
|
|
28
|
+
* apply, or a kept UNCHANGED row that moved the installation to another
|
|
29
|
+
* bundle of a release, as when the apply report never arrived or a rollback
|
|
30
|
+
* to the built-in bundle took it there. A first report, a release change of
|
|
31
|
+
* the bundle already running, and a late row launch nothing.
|
|
32
|
+
*/
|
|
33
|
+
const isLaunch = (row) => {
|
|
34
|
+
if (isLateEvent(row)) return false;
|
|
35
|
+
if (row.type === "UPDATE_APPLIED") return true;
|
|
36
|
+
if (row.type !== "UNCHANGED") return false;
|
|
37
|
+
const kinds = metadataOf(row).change?.kinds ?? [];
|
|
38
|
+
return kinds.includes("bundle") && !kinds.includes("first_seen") && row.to_release_id !== null;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The bundle a stored row's bundle filter matches: `from:<bundle>` for a
|
|
42
|
+
* recovery; `to:<bundle>` for a download, an apply, and an update failure;
|
|
43
|
+
* `on:<bundle>` for an UNCHANGED row that launched a bundle (`isLaunch`), a
|
|
44
|
+
* key no other row uses. None for a failed check, which targets no bundle, a
|
|
45
|
+
* late row, or any other UNCHANGED row.
|
|
46
|
+
*/
|
|
47
|
+
const bundleRefsOf = (row) => {
|
|
48
|
+
if (isFailedCheck(row) || isLateEvent(row)) return [];
|
|
49
|
+
if (row.type === "RECOVERED") return [`from:${row.from_bundle_id}`];
|
|
50
|
+
if (row.type === "UNCHANGED") return isLaunch(row) ? [`on:${row.to_bundle_id}`] : [];
|
|
51
|
+
return [`to:${row.to_bundle_id}`];
|
|
52
|
+
};
|
|
53
|
+
/** The key a bundle filter reads, as `bundleRefsOf` writes it. */
|
|
54
|
+
const bundleRefOfFilter = (filter) => filter.type === "RECOVERED" ? `from:${filter.fromBundleId}` : filter.type === "UNCHANGED" ? `on:${filter.toBundleId}` : `to:${filter.toBundleId}`;
|
|
55
|
+
/**
|
|
56
|
+
* Downloads, applies, recoveries, update failures, and the UNCHANGED
|
|
57
|
+
* reports that changed what their installation runs, whole. An UNCHANGED
|
|
58
|
+
* report that changes nothing counts as a launch and moves its
|
|
59
|
+
* installation's head, but no row keeps it.
|
|
25
60
|
*/
|
|
26
61
|
const bundleEvents = (days) => (0, _hot_updater_plugin_core.defineTable)({
|
|
27
62
|
id: {
|
|
@@ -76,19 +111,16 @@ const bundleEvents = (days) => (0, _hot_updater_plugin_core.defineTable)({
|
|
|
76
111
|
type: "integer",
|
|
77
112
|
compute: (row) => row.received_at_ms - row.received_at_ms % DAY_MS
|
|
78
113
|
},
|
|
79
|
-
/**
|
|
114
|
+
/** Every stored event belongs in installation history. */
|
|
80
115
|
movement_install_id: {
|
|
81
116
|
type: "string",
|
|
82
117
|
compute: (row) => MOVEMENTS.has(row.type) ? row.install_id : null
|
|
83
118
|
},
|
|
84
|
-
/**
|
|
85
|
-
* The bundle a bundle filter matches: `from:<bundle>` for RECOVERED,
|
|
86
|
-
* none for a failed check, else `to:<bundle>`.
|
|
87
|
-
*/
|
|
119
|
+
/** The bundle a bundle filter matches (`bundleRefsOf`). */
|
|
88
120
|
bundle_ref: {
|
|
89
121
|
type: "string",
|
|
90
122
|
multi: true,
|
|
91
|
-
compute: (row) =>
|
|
123
|
+
compute: (row) => bundleRefsOf(row)
|
|
92
124
|
}
|
|
93
125
|
},
|
|
94
126
|
indexes: {
|
|
@@ -354,6 +386,55 @@ const insightsDistribution = (retention) => (0, _hot_updater_plugin_core.defineA
|
|
|
354
386
|
},
|
|
355
387
|
retention
|
|
356
388
|
});
|
|
389
|
+
/**
|
|
390
|
+
* The installations of `insights_distribution` that run their native build's
|
|
391
|
+
* built-in bundle, by its ID (`builtin_bundle_id`), within the same rows:
|
|
392
|
+
* reads move them out of the row of their release, or of no release.
|
|
393
|
+
*/
|
|
394
|
+
const insightsBuiltinDistribution = (retention) => (0, _hot_updater_plugin_core.defineAggregate)({
|
|
395
|
+
channel: { type: "string" },
|
|
396
|
+
platform: {
|
|
397
|
+
type: "string",
|
|
398
|
+
maxLength: 16
|
|
399
|
+
},
|
|
400
|
+
app_version: { type: "string" },
|
|
401
|
+
release_id: {
|
|
402
|
+
type: "string",
|
|
403
|
+
maxLength: 36
|
|
404
|
+
},
|
|
405
|
+
builtin_bundle_id: {
|
|
406
|
+
type: "string",
|
|
407
|
+
maxLength: 36
|
|
408
|
+
},
|
|
409
|
+
bucket_start_ms: { type: "integer" }
|
|
410
|
+
}, {
|
|
411
|
+
key: [
|
|
412
|
+
"channel",
|
|
413
|
+
"platform",
|
|
414
|
+
"app_version",
|
|
415
|
+
"release_id",
|
|
416
|
+
"builtin_bundle_id",
|
|
417
|
+
"bucket_start_ms"
|
|
418
|
+
],
|
|
419
|
+
gauges: ["latest_installations"],
|
|
420
|
+
shards: GAUGE_SHARDS,
|
|
421
|
+
batched: true,
|
|
422
|
+
indexes: {
|
|
423
|
+
byScope: {
|
|
424
|
+
eq: ["channel", "platform"],
|
|
425
|
+
sort: ["bucket_start_ms"]
|
|
426
|
+
},
|
|
427
|
+
byVersion: {
|
|
428
|
+
eq: [
|
|
429
|
+
"channel",
|
|
430
|
+
"platform",
|
|
431
|
+
"app_version"
|
|
432
|
+
],
|
|
433
|
+
sort: ["bucket_start_ms"]
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
retention
|
|
437
|
+
});
|
|
357
438
|
/** Latest events by the bundle they came from or went to, bucketed like the distribution. */
|
|
358
439
|
const insightsLatestByBundle = (retention) => (0, _hot_updater_plugin_core.defineAggregate)({
|
|
359
440
|
platform: {
|
|
@@ -473,6 +554,7 @@ const createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTIO
|
|
|
473
554
|
insights_overview_lifetime: lifetimeCounters(),
|
|
474
555
|
insights_sketches_lifetime: lifetimeSketches(),
|
|
475
556
|
insights_distribution: insightsDistribution(daily),
|
|
557
|
+
insights_builtin_distribution: insightsBuiltinDistribution(daily),
|
|
476
558
|
insights_latest_by_bundle: insightsLatestByBundle(daily),
|
|
477
559
|
insights_outcomes: insightsOutcomes(hourly),
|
|
478
560
|
insights_failures: insightsFailures(hourly)
|
|
@@ -484,6 +566,10 @@ exports.DAILY_EVENTS = DAILY_EVENTS;
|
|
|
484
566
|
exports.DAY_MS = DAY_MS;
|
|
485
567
|
exports.DEFAULT_INSIGHTS_RETENTION = DEFAULT_INSIGHTS_RETENTION;
|
|
486
568
|
exports.HOUR_MS = HOUR_MS;
|
|
569
|
+
exports.bundleRefOfFilter = bundleRefOfFilter;
|
|
570
|
+
exports.bundleRefsOf = bundleRefsOf;
|
|
487
571
|
exports.createInsightsSchema = createInsightsSchema;
|
|
488
572
|
exports.insightsSchema = insightsSchema;
|
|
489
573
|
exports.isFailedCheck = isFailedCheck;
|
|
574
|
+
exports.isLateEvent = isLateEvent;
|
|
575
|
+
exports.isLaunch = isLaunch;
|