@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.
- package/dist/server/contract.cjs +13 -32
- package/dist/server/contract.mjs +13 -32
- package/dist/server/index.cjs +3 -29
- package/dist/server/index.d.cts +6 -72
- package/dist/server/index.d.mts +6 -72
- package/dist/server/index.mjs +4 -30
- package/dist/server/model.cjs +2 -2
- package/dist/server/model.mjs +2 -2
- package/dist/server/modelTypes.d.cts +28 -51
- package/dist/server/modelTypes.d.mts +28 -51
- package/dist/server/overview.cjs +6 -10
- package/dist/server/overview.mjs +6 -10
- package/dist/server/reads.cjs +34 -104
- package/dist/server/reads.mjs +34 -104
- package/dist/server/recordEvent.cjs +9 -60
- package/dist/server/recordEvent.mjs +9 -60
- package/dist/server/schema.cjs +9 -87
- package/dist/server/schema.d.cts +4 -136
- package/dist/server/schema.d.mts +4 -136
- package/dist/server/schema.mjs +9 -87
- package/package.json +5 -5
|
@@ -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
|
-
|
|
102
|
+
interface InsightsGetReleaseActivityInput {
|
|
103
|
+
/** 1 to 100 releases. */
|
|
98
104
|
readonly releases: readonly ReleaseReference[];
|
|
99
|
-
|
|
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
|
|
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
|
-
/**
|
|
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,
|
|
183
|
+
export { InsightsBundleEventFilter, InsightsCountEventSeriesInput, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsEventSeriesPoint, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
|
package/dist/server/overview.cjs
CHANGED
|
@@ -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
|
|
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,
|
package/dist/server/overview.mjs
CHANGED
|
@@ -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
|
|
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,
|
package/dist/server/reads.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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;
|
package/dist/server/reads.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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,
|
|
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.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
295
|
-
|
|
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);
|