@hot-updater/server 1.0.0-rc.1 → 1.0.0-rc.2
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/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/adapters/databaseConstraintErrors.cjs +0 -1
- package/dist/adapters/databasePluginUtils.cjs +0 -1
- package/dist/adapters/drizzle.cjs +10 -3
- package/dist/adapters/drizzle.d.cts +0 -1
- package/dist/adapters/drizzle.d.mts +0 -1
- package/dist/adapters/drizzle.mjs +11 -3
- package/dist/adapters/drizzleCrud.cjs +62 -1
- package/dist/adapters/drizzleCrud.mjs +61 -2
- package/dist/adapters/drizzleLazyDB.cjs +6 -1
- package/dist/adapters/drizzleLazyDB.mjs +6 -1
- package/dist/adapters/drizzleQuery.cjs +3 -2
- package/dist/adapters/drizzleQuery.mjs +3 -1
- package/dist/adapters/insightsTransaction.cjs +22 -0
- package/dist/adapters/insightsTransaction.mjs +22 -0
- package/dist/adapters/kysely.cjs +2 -1
- package/dist/adapters/kysely.d.cts +0 -1
- package/dist/adapters/kysely.d.mts +0 -1
- package/dist/adapters/kysely.mjs +3 -1
- package/dist/adapters/kyselyCrud.cjs +54 -3
- package/dist/adapters/kyselyCrud.mjs +54 -3
- package/dist/adapters/kyselyQuery.cjs +2 -2
- package/dist/adapters/kyselyQuery.mjs +2 -1
- package/dist/adapters/mongodb.cjs +27 -1
- package/dist/adapters/mongodb.d.cts +1 -1
- package/dist/adapters/mongodb.d.mts +1 -1
- package/dist/adapters/mongodb.mjs +28 -1
- package/dist/adapters/mongodbCollections.cjs +2 -0
- package/dist/adapters/mongodbCollections.mjs +2 -0
- package/dist/adapters/mongodbQuery.cjs +21 -4
- package/dist/adapters/mongodbQuery.mjs +21 -5
- package/dist/adapters/mongodbReads.cjs +91 -29
- package/dist/adapters/mongodbReads.mjs +92 -29
- package/dist/adapters/mongodbWrites.cjs +16 -2
- package/dist/adapters/mongodbWrites.mjs +18 -3
- package/dist/adapters/prisma.cjs +55 -7
- package/dist/adapters/prisma.d.cts +0 -1
- package/dist/adapters/prisma.d.mts +0 -1
- package/dist/adapters/prisma.mjs +56 -7
- package/dist/adapters/prismaQuery.cjs +0 -1
- package/dist/adapters/prismaRows.cjs +22 -1
- package/dist/adapters/prismaRows.mjs +22 -1
- package/dist/apiKeys.cjs +6 -2
- package/dist/apiKeys.d.cts +0 -1
- package/dist/apiKeys.d.mts +0 -1
- package/dist/apiKeys.mjs +3 -3
- package/dist/createHotUpdaterCore.cjs +24 -9
- package/dist/createHotUpdaterCore.d.cts +1 -1
- package/dist/createHotUpdaterCore.d.mts +1 -1
- package/dist/createHotUpdaterCore.mjs +23 -9
- package/dist/db/createBundleDiff.cjs +2 -2
- package/dist/db/createBundleDiff.d.cts +1 -5
- package/dist/db/createBundleDiff.d.mts +1 -5
- package/dist/db/databasePluginCore.cjs +0 -1
- package/dist/db/fixedMigrator.cjs +2 -2
- package/dist/db/fixedMigrator.mjs +2 -2
- package/dist/db/fixedMigratorMongo.cjs +6 -4
- package/dist/db/fixedMigratorMongo.mjs +6 -4
- package/dist/db/index.d.cts +0 -1
- package/dist/db/index.d.mts +0 -1
- package/dist/db/releaseCatalog.cjs +0 -1
- package/dist/db/releaseCatalog.d.cts +0 -1
- package/dist/db/releaseCatalog.d.mts +0 -1
- package/dist/db/schema/insightsCollation.cjs +14 -0
- package/dist/db/schema/insightsCollation.mjs +14 -0
- package/dist/db/schema/sql.cjs +11 -7
- package/dist/db/schema/sql.mjs +8 -8
- package/dist/db/schemaGenerators.cjs +28 -9
- package/dist/db/schemaGenerators.mjs +28 -9
- package/dist/db/schemaReadiness.cjs +2 -0
- package/dist/db/schemaReadiness.d.cts +1 -0
- package/dist/db/schemaReadiness.d.mts +1 -0
- package/dist/db/schemaReadiness.mjs +2 -0
- package/dist/db/sqlMigrationExecution.cjs +1 -1
- package/dist/db/sqlMigrationExecution.mjs +1 -0
- package/dist/db/types.cjs +3 -1
- package/dist/db/types.d.cts +3 -6
- package/dist/db/types.d.mts +3 -6
- package/dist/db/types.mjs +2 -2
- package/dist/db/updateArtifacts.cjs +0 -1
- package/dist/handler.d.cts +1 -2
- package/dist/handler.d.mts +1 -2
- package/dist/handlerReleaseCatalogRoutes.cjs +0 -1
- package/dist/handlerReleaseManagementRoutes.cjs +0 -1
- package/dist/handlerTypes.d.cts +0 -1
- package/dist/handlerTypes.d.mts +0 -1
- package/dist/handlerVersionRoutes.d.cts +1 -0
- package/dist/handlerVersionRoutes.d.mts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/insights/domain.d.cts +37 -69
- package/dist/insights/domain.d.mts +37 -69
- package/dist/insights/errors.cjs +1 -8
- package/dist/insights/errors.mjs +2 -8
- package/dist/insights/eventInput.cjs +49 -5
- package/dist/insights/eventInput.mjs +48 -6
- package/dist/insights/provider.cjs +314 -0
- package/dist/insights/provider.d.cts +6 -0
- package/dist/insights/provider.d.mts +6 -0
- package/dist/insights/provider.mjs +314 -0
- package/dist/insights/queryInput.cjs +78 -43
- package/dist/insights/queryInput.mjs +76 -40
- package/dist/insights/routes.cjs +20 -25
- package/dist/insights/routes.mjs +22 -27
- package/dist/insights/types.d.cts +27 -12
- package/dist/insights/types.d.mts +27 -12
- package/dist/node.d.cts +0 -1
- package/dist/node.d.mts +0 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/schema/index.cjs +17 -3
- package/dist/schema/index.mjs +4 -4
- package/dist/schema/v1_0_0.cjs +72 -30
- package/dist/schema/v1_0_0.mjs +72 -31
- package/dist/storageAccess.cjs +0 -1
- package/dist/types/index.d.cts +0 -1
- package/dist/types/index.d.mts +0 -1
- package/package.json +7 -7
- package/src/adapters/databasePluginTestDatabase.ts +14 -0
- package/src/adapters/drizzle.spec.ts +99 -0
- package/src/adapters/drizzle.ts +21 -3
- package/src/adapters/drizzleCrud.ts +154 -2
- package/src/adapters/drizzleLazyDB.ts +17 -3
- package/src/adapters/insightsTransaction.ts +32 -0
- package/src/adapters/kysely.spec.ts +29 -0
- package/src/adapters/kysely.ts +10 -1
- package/src/adapters/kyselyCrud.ts +121 -0
- package/src/adapters/mongodb.integration.spec.ts +210 -0
- package/src/adapters/mongodb.spec.ts +69 -1
- package/src/adapters/mongodb.ts +66 -2
- package/src/adapters/mongodbCollections.ts +5 -0
- package/src/adapters/mongodbQuery.ts +32 -3
- package/src/adapters/mongodbReads.ts +69 -0
- package/src/adapters/mongodbTestClient.ts +41 -6
- package/src/adapters/mongodbTestFilter.ts +4 -1
- package/src/adapters/mongodbWrites.ts +27 -0
- package/src/adapters/prisma.spec.ts +97 -0
- package/src/adapters/prisma.ts +103 -3
- package/src/adapters/prismaRows.ts +37 -1
- package/src/adapters/prismaTestClient.ts +22 -5
- package/src/apiKeys.integration.spec.ts +1 -1
- package/src/createHotUpdaterCore.ts +21 -7
- package/src/db/databasePluginUpdateCheck.bench.ts +3 -0
- package/src/db/fixedMigrator.spec.ts +65 -0
- package/src/db/fixedMigratorMongo.spec.ts +56 -9
- package/src/db/fixedMigratorMongo.ts +10 -10
- package/src/db/index.spec.ts +8 -1
- package/src/db/schema/insightsCollation.ts +21 -0
- package/src/db/schema/sql.ts +8 -5
- package/src/db/schemaGenerators.ts +41 -13
- package/src/db/types.spec.ts +8 -0
- package/src/db/types.ts +10 -4
- package/src/handler-standalone.integration.spec.ts +5 -7
- package/src/index.ts +2 -2
- package/src/insights/domain.ts +39 -75
- package/src/insights/errors.ts +0 -7
- package/src/insights/eventInput.ts +60 -3
- package/src/insights/insights.integration.spec.ts +254 -17
- package/src/insights/provider.spec.ts +385 -0
- package/src/insights/provider.ts +591 -0
- package/src/insights/queryInput.ts +112 -73
- package/src/insights/routes.ts +37 -56
- package/src/insights/types.ts +47 -36
- package/src/schema/dsl-all-versions.spec.ts +3 -1
- package/src/schema/v1_0_0.spec.ts +77 -3
- package/src/schema/v1_0_0.ts +94 -30
- package/dist/insights/bounded/activeOverview.cjs +0 -87
- package/dist/insights/bounded/activeOverview.mjs +0 -85
- package/dist/insights/bounded/installationSearch.cjs +0 -42
- package/dist/insights/bounded/installationSearch.mjs +0 -42
- package/dist/insights/bounded/persistence.cjs +0 -39
- package/dist/insights/bounded/persistence.mjs +0 -38
- package/dist/insights/bounded/provider.cjs +0 -170
- package/dist/insights/bounded/provider.d.cts +0 -7
- package/dist/insights/bounded/provider.d.mts +0 -7
- package/dist/insights/bounded/provider.mjs +0 -170
- package/dist/insights/bounded/scan.cjs +0 -144
- package/dist/insights/bounded/scan.mjs +0 -138
- package/dist/insights/persistence.d.cts +0 -6
- package/dist/insights/persistence.d.mts +0 -6
- package/src/insights/bounded/activeOverview.spec.ts +0 -189
- package/src/insights/bounded/activeOverview.ts +0 -148
- package/src/insights/bounded/installationSearch.spec.ts +0 -130
- package/src/insights/bounded/installationSearch.ts +0 -69
- package/src/insights/bounded/persistence.ts +0 -43
- package/src/insights/bounded/provider.spec.ts +0 -219
- package/src/insights/bounded/provider.ts +0 -229
- package/src/insights/bounded/scan.ts +0 -235
- package/src/insights/persistence.ts +0 -10
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { collectActiveInstallationOverview } from "./activeOverview.mjs";
|
|
2
|
-
import { INSIGHTS_SCAN_MAX_ROWS, collectEventActivity, compareCodePoints, compareEventNewest, materializeActiveRows, materializeEventRows, materializeRowsForWindow } from "./scan.mjs";
|
|
3
|
-
import { searchEventInstallations } from "./installationSearch.mjs";
|
|
4
|
-
import { createBundleEventRow } from "./persistence.mjs";
|
|
5
|
-
//#region src/insights/bounded/provider.ts
|
|
6
|
-
const isTransitionEventRow = (row) => row.type === "UPDATE_APPLIED" || row.type === "RECOVERED";
|
|
7
|
-
const toHistoryRow = (row) => ({
|
|
8
|
-
id: row.id,
|
|
9
|
-
type: row.type,
|
|
10
|
-
fromBundleId: row.from_bundle_id,
|
|
11
|
-
toBundleId: row.to_bundle_id,
|
|
12
|
-
username: row.username,
|
|
13
|
-
userId: row.user_id,
|
|
14
|
-
platform: row.platform,
|
|
15
|
-
appVersion: row.app_version,
|
|
16
|
-
channel: row.channel,
|
|
17
|
-
cohort: row.cohort,
|
|
18
|
-
receivedAtMs: row.received_at_ms
|
|
19
|
-
});
|
|
20
|
-
const isInstalledForBundle = (row, bundleId) => row.type === "UPDATE_APPLIED" && row.to_bundle_id === bundleId;
|
|
21
|
-
const isRecoveredFromBundle = (row, bundleId) => row.type === "RECOVERED" && row.from_bundle_id === bundleId;
|
|
22
|
-
const countDistinctInstallations = (rows) => new Set(rows.map(({ install_id }) => install_id)).size;
|
|
23
|
-
const getInsightsResult = async (persistence, bundleId, window, limit, offset) => {
|
|
24
|
-
const scope = {
|
|
25
|
-
persistence,
|
|
26
|
-
cutoffMs: Date.now()
|
|
27
|
-
};
|
|
28
|
-
const rows = (await materializeRowsForWindow(scope, window)).filter(isTransitionEventRow);
|
|
29
|
-
const installedRows = rows.filter((row) => isInstalledForBundle(row, bundleId));
|
|
30
|
-
const recoveredRows = rows.filter((row) => isRecoveredFromBundle(row, bundleId));
|
|
31
|
-
const installed = collectEventActivity({
|
|
32
|
-
rows: installedRows,
|
|
33
|
-
window,
|
|
34
|
-
cutoffMs: scope.cutoffMs
|
|
35
|
-
});
|
|
36
|
-
const recovered = collectEventActivity({
|
|
37
|
-
rows: recoveredRows,
|
|
38
|
-
window,
|
|
39
|
-
cutoffMs: scope.cutoffMs
|
|
40
|
-
});
|
|
41
|
-
const recentRows = [...installedRows, ...recoveredRows].sort(compareEventNewest);
|
|
42
|
-
return {
|
|
43
|
-
summary: {
|
|
44
|
-
installed: installed.summary,
|
|
45
|
-
recovered: recovered.summary
|
|
46
|
-
},
|
|
47
|
-
series: {
|
|
48
|
-
installed: installed.series,
|
|
49
|
-
recovered: recovered.series
|
|
50
|
-
},
|
|
51
|
-
cohorts: {
|
|
52
|
-
installed: installed.cohorts,
|
|
53
|
-
recovered: recovered.cohorts
|
|
54
|
-
},
|
|
55
|
-
recentEvents: {
|
|
56
|
-
data: recentRows.slice(offset, offset + limit).map(toHistoryRow),
|
|
57
|
-
pagination: {
|
|
58
|
-
total: recentRows.length,
|
|
59
|
-
limit,
|
|
60
|
-
offset
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
const getInsightsSummaries = async (persistence, bundleIds, window) => {
|
|
66
|
-
const normalizedBundleIds = [...new Set(bundleIds)];
|
|
67
|
-
if (normalizedBundleIds.length === 0) return [];
|
|
68
|
-
const requestedBundleIds = new Set(normalizedBundleIds);
|
|
69
|
-
const installedByBundleId = /* @__PURE__ */ new Map();
|
|
70
|
-
const recoveredByBundleId = /* @__PURE__ */ new Map();
|
|
71
|
-
const rows = (await materializeRowsForWindow({
|
|
72
|
-
persistence,
|
|
73
|
-
cutoffMs: Date.now()
|
|
74
|
-
}, window)).filter(isTransitionEventRow);
|
|
75
|
-
for (const row of rows) {
|
|
76
|
-
const bundleId = row.type === "UPDATE_APPLIED" ? row.to_bundle_id : row.from_bundle_id;
|
|
77
|
-
if (!requestedBundleIds.has(bundleId)) continue;
|
|
78
|
-
const counts = row.type === "UPDATE_APPLIED" ? installedByBundleId : recoveredByBundleId;
|
|
79
|
-
const installIds = counts.get(bundleId) ?? /* @__PURE__ */ new Set();
|
|
80
|
-
installIds.add(row.install_id);
|
|
81
|
-
counts.set(bundleId, installIds);
|
|
82
|
-
}
|
|
83
|
-
return normalizedBundleIds.map((bundleId) => ({
|
|
84
|
-
bundleId,
|
|
85
|
-
installed: installedByBundleId.get(bundleId)?.size ?? 0,
|
|
86
|
-
recovered: recoveredByBundleId.get(bundleId)?.size ?? 0
|
|
87
|
-
}));
|
|
88
|
-
};
|
|
89
|
-
const createInsightsProvider = (persistence) => Object.freeze({
|
|
90
|
-
mode: "bounded",
|
|
91
|
-
maxMatchingRows: INSIGHTS_SCAN_MAX_ROWS,
|
|
92
|
-
async appendBundleEvent(input) {
|
|
93
|
-
await persistence.append(createBundleEventRow(input));
|
|
94
|
-
},
|
|
95
|
-
async getBundleEventSummary(bundleId) {
|
|
96
|
-
const rows = (await materializeEventRows({
|
|
97
|
-
persistence,
|
|
98
|
-
cutoffMs: Date.now()
|
|
99
|
-
})).filter(isTransitionEventRow);
|
|
100
|
-
return {
|
|
101
|
-
installed: countDistinctInstallations(rows.filter((row) => isInstalledForBundle(row, bundleId))),
|
|
102
|
-
recovered: countDistinctInstallations(rows.filter((row) => isRecoveredFromBundle(row, bundleId)))
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
getBundleEventSummaries(bundleIds, window) {
|
|
106
|
-
return getInsightsSummaries(persistence, bundleIds, window);
|
|
107
|
-
},
|
|
108
|
-
getBundleEventInsights(bundleId, window, limit, offset) {
|
|
109
|
-
return getInsightsResult(persistence, bundleId, window, limit, offset);
|
|
110
|
-
},
|
|
111
|
-
async getBundleEventOverview() {
|
|
112
|
-
const rows = await materializeEventRows({
|
|
113
|
-
persistence,
|
|
114
|
-
cutoffMs: Date.now()
|
|
115
|
-
});
|
|
116
|
-
const latestByInstall = /* @__PURE__ */ new Map();
|
|
117
|
-
for (const row of rows) {
|
|
118
|
-
const current = latestByInstall.get(row.install_id);
|
|
119
|
-
if (current === void 0 || compareEventNewest(row, current) < 0) latestByInstall.set(row.install_id, row);
|
|
120
|
-
}
|
|
121
|
-
const counts = /* @__PURE__ */ new Map();
|
|
122
|
-
for (const row of latestByInstall.values()) counts.set(row.to_bundle_id, (counts.get(row.to_bundle_id) ?? 0) + 1);
|
|
123
|
-
return {
|
|
124
|
-
trackedInstallations: latestByInstall.size,
|
|
125
|
-
bundles: [...counts].map(([bundleId, installations]) => ({
|
|
126
|
-
bundleId,
|
|
127
|
-
installations
|
|
128
|
-
})).sort((left, right) => right.installations - left.installations || compareCodePoints(left.bundleId, right.bundleId))
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
async getActiveInstallationOverview(input) {
|
|
132
|
-
const asOfMs = Date.now();
|
|
133
|
-
return collectActiveInstallationOverview({
|
|
134
|
-
rows: await materializeActiveRows({
|
|
135
|
-
persistence,
|
|
136
|
-
cutoffMs: asOfMs
|
|
137
|
-
}, input.window),
|
|
138
|
-
asOfMs,
|
|
139
|
-
window: input.window,
|
|
140
|
-
...input.userId === void 0 ? {} : { userId: input.userId }
|
|
141
|
-
});
|
|
142
|
-
},
|
|
143
|
-
async searchInstallations(query, limit, offset) {
|
|
144
|
-
return searchEventInstallations({
|
|
145
|
-
rows: await materializeEventRows({
|
|
146
|
-
persistence,
|
|
147
|
-
cutoffMs: Date.now()
|
|
148
|
-
}),
|
|
149
|
-
query,
|
|
150
|
-
limit,
|
|
151
|
-
offset
|
|
152
|
-
});
|
|
153
|
-
},
|
|
154
|
-
async getInstallationHistory(installId, limit, offset) {
|
|
155
|
-
const ordered = (await materializeEventRows({
|
|
156
|
-
persistence,
|
|
157
|
-
cutoffMs: Date.now()
|
|
158
|
-
})).filter((row) => row.install_id === installId && isTransitionEventRow(row)).toSorted(compareEventNewest);
|
|
159
|
-
return {
|
|
160
|
-
data: ordered.slice(offset, offset + limit).map(toHistoryRow),
|
|
161
|
-
pagination: {
|
|
162
|
-
total: ordered.length,
|
|
163
|
-
limit,
|
|
164
|
-
offset
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
//#endregion
|
|
170
|
-
export { createInsightsProvider };
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
const require_errors = require("../errors.cjs");
|
|
2
|
-
const require_activeOverview = require("./activeOverview.cjs");
|
|
3
|
-
//#region src/insights/bounded/scan.ts
|
|
4
|
-
const INSIGHTS_SCAN_MAX_ROWS = 5e4;
|
|
5
|
-
const INSIGHTS_MATERIALIZATION_LIMIT = INSIGHTS_SCAN_MAX_ROWS + 1;
|
|
6
|
-
const INSIGHTS_SCAN_PAGE_SIZE = 1e3;
|
|
7
|
-
const INSIGHTS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
|
|
8
|
-
const compareCodePoints = (left, right) => {
|
|
9
|
-
if (left < right) return -1;
|
|
10
|
-
if (left > right) return 1;
|
|
11
|
-
return 0;
|
|
12
|
-
};
|
|
13
|
-
const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareCodePoints(right.id, left.id);
|
|
14
|
-
const compareEventOldest = (left, right) => left.received_at_ms - right.received_at_ms || compareCodePoints(left.id, right.id);
|
|
15
|
-
const materializeEventRows = async (scope) => {
|
|
16
|
-
const rows = [];
|
|
17
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
18
|
-
let after = scope.lowerBoundMs === void 0 ? void 0 : {
|
|
19
|
-
receivedAtMs: scope.lowerBoundMs,
|
|
20
|
-
id: INSIGHTS_LOWER_BOUND_ID
|
|
21
|
-
};
|
|
22
|
-
while (rows.length < INSIGHTS_MATERIALIZATION_LIMIT) {
|
|
23
|
-
const limit = Math.min(INSIGHTS_SCAN_PAGE_SIZE, INSIGHTS_MATERIALIZATION_LIMIT - rows.length);
|
|
24
|
-
const page = await scope.persistence.scan({
|
|
25
|
-
beforeReceivedAtMs: scope.cutoffMs,
|
|
26
|
-
...after === void 0 ? {} : { after },
|
|
27
|
-
limit
|
|
28
|
-
});
|
|
29
|
-
if (page.length === 0) break;
|
|
30
|
-
if (page.length > limit) throw new InsightsPersistenceOrderError();
|
|
31
|
-
let previous = after === void 0 ? void 0 : {
|
|
32
|
-
received_at_ms: after.receivedAtMs,
|
|
33
|
-
id: after.id
|
|
34
|
-
};
|
|
35
|
-
for (const row of page) {
|
|
36
|
-
if (row.received_at_ms >= scope.cutoffMs || previous !== void 0 && compareEventOldest(previous, row) >= 0 || seenIds.has(row.id)) throw new InsightsPersistenceOrderError();
|
|
37
|
-
seenIds.add(row.id);
|
|
38
|
-
rows.push(row);
|
|
39
|
-
previous = row;
|
|
40
|
-
}
|
|
41
|
-
const last = page.at(-1);
|
|
42
|
-
if (last !== void 0) after = {
|
|
43
|
-
receivedAtMs: last.received_at_ms,
|
|
44
|
-
id: last.id
|
|
45
|
-
};
|
|
46
|
-
if (rows.length > 5e4) throw new require_errors.InsightsScanLimitExceededError(INSIGHTS_SCAN_MAX_ROWS);
|
|
47
|
-
}
|
|
48
|
-
return rows;
|
|
49
|
-
};
|
|
50
|
-
var InsightsPersistenceOrderError = class extends Error {
|
|
51
|
-
name = "InsightsPersistenceOrderError";
|
|
52
|
-
constructor() {
|
|
53
|
-
super("Insights persistence did not advance its scan cursor.");
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const materializeActiveRows = async (scope, window) => {
|
|
57
|
-
const definition = require_activeOverview.getActiveWindowDefinition(window);
|
|
58
|
-
const durationMs = definition.bucketCount * definition.bucketSizeMs;
|
|
59
|
-
return (await materializeEventRows({
|
|
60
|
-
...scope,
|
|
61
|
-
lowerBoundMs: scope.cutoffMs - durationMs
|
|
62
|
-
})).filter((row) => row.received_at_ms >= scope.cutoffMs - durationMs && require_activeOverview.ACTIVE_BUNDLE_EVENT_TYPES.includes(row.type));
|
|
63
|
-
};
|
|
64
|
-
const startOfUtcHour = (value) => {
|
|
65
|
-
const date = new Date(value);
|
|
66
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
|
|
67
|
-
};
|
|
68
|
-
const startOfUtcDay = (value) => {
|
|
69
|
-
const date = new Date(value);
|
|
70
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
71
|
-
};
|
|
72
|
-
const getWindowRange = (window, now) => {
|
|
73
|
-
if (window === "24h") return {
|
|
74
|
-
sizeMs: 3600 * 1e3,
|
|
75
|
-
rangeStart: startOfUtcHour(now) - 1380 * 60 * 1e3
|
|
76
|
-
};
|
|
77
|
-
const days = window === "7d" ? 7 : 30;
|
|
78
|
-
return {
|
|
79
|
-
sizeMs: 1440 * 60 * 1e3,
|
|
80
|
-
rangeStart: startOfUtcDay(now) - (days - 1) * 24 * 60 * 60 * 1e3
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
const materializeRowsForWindow = async (scope, window) => {
|
|
84
|
-
if (window === "all") return materializeEventRows(scope);
|
|
85
|
-
const range = getWindowRange(window, scope.cutoffMs);
|
|
86
|
-
return (await materializeEventRows({
|
|
87
|
-
...scope,
|
|
88
|
-
lowerBoundMs: range.rangeStart
|
|
89
|
-
})).filter(({ received_at_ms }) => received_at_ms >= range.rangeStart);
|
|
90
|
-
};
|
|
91
|
-
const bucketStart = (receivedAtMs, sizeMs) => sizeMs === 3600 * 1e3 ? startOfUtcHour(receivedAtMs) : startOfUtcDay(receivedAtMs);
|
|
92
|
-
const createSeries = (request) => {
|
|
93
|
-
const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
|
|
94
|
-
const sizeMs = range?.sizeMs ?? 1440 * 60 * 1e3;
|
|
95
|
-
const installIdsByBucket = /* @__PURE__ */ new Map();
|
|
96
|
-
let oldestMs = request.cutoffMs;
|
|
97
|
-
for (const row of request.rows) {
|
|
98
|
-
oldestMs = Math.min(oldestMs, row.received_at_ms);
|
|
99
|
-
const start = bucketStart(row.received_at_ms, sizeMs);
|
|
100
|
-
const installIds = installIdsByBucket.get(start) ?? /* @__PURE__ */ new Set();
|
|
101
|
-
installIds.add(row.install_id);
|
|
102
|
-
installIdsByBucket.set(start, installIds);
|
|
103
|
-
}
|
|
104
|
-
const first = range?.rangeStart ?? startOfUtcDay(oldestMs);
|
|
105
|
-
const last = bucketStart(request.cutoffMs, sizeMs);
|
|
106
|
-
return Array.from({ length: Math.floor((last - first) / sizeMs) + 1 }, (_, index) => {
|
|
107
|
-
const start = first + index * sizeMs;
|
|
108
|
-
return {
|
|
109
|
-
bucketStartMs: start,
|
|
110
|
-
value: installIdsByBucket.get(start)?.size ?? 0
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
const collectEventActivity = (request) => {
|
|
115
|
-
const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
|
|
116
|
-
const rows = range === void 0 ? request.rows : request.rows.filter(({ received_at_ms }) => received_at_ms >= range.rangeStart && received_at_ms < request.cutoffMs);
|
|
117
|
-
const installs = /* @__PURE__ */ new Set();
|
|
118
|
-
const installsByCohort = /* @__PURE__ */ new Map();
|
|
119
|
-
for (const row of rows) {
|
|
120
|
-
installs.add(row.install_id);
|
|
121
|
-
const cohort = installsByCohort.get(row.cohort) ?? /* @__PURE__ */ new Set();
|
|
122
|
-
cohort.add(row.install_id);
|
|
123
|
-
installsByCohort.set(row.cohort, cohort);
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
summary: installs.size,
|
|
127
|
-
cohorts: [...installsByCohort].sort(([left], [right]) => compareCodePoints(left, right)).map(([cohort, installIds]) => ({
|
|
128
|
-
cohort,
|
|
129
|
-
value: installIds.size
|
|
130
|
-
})),
|
|
131
|
-
series: createSeries({
|
|
132
|
-
...request,
|
|
133
|
-
rows
|
|
134
|
-
})
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
//#endregion
|
|
138
|
-
exports.INSIGHTS_SCAN_MAX_ROWS = INSIGHTS_SCAN_MAX_ROWS;
|
|
139
|
-
exports.collectEventActivity = collectEventActivity;
|
|
140
|
-
exports.compareCodePoints = compareCodePoints;
|
|
141
|
-
exports.compareEventNewest = compareEventNewest;
|
|
142
|
-
exports.materializeActiveRows = materializeActiveRows;
|
|
143
|
-
exports.materializeEventRows = materializeEventRows;
|
|
144
|
-
exports.materializeRowsForWindow = materializeRowsForWindow;
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { InsightsScanLimitExceededError } from "../errors.mjs";
|
|
2
|
-
import { ACTIVE_BUNDLE_EVENT_TYPES, getActiveWindowDefinition } from "./activeOverview.mjs";
|
|
3
|
-
//#region src/insights/bounded/scan.ts
|
|
4
|
-
const INSIGHTS_SCAN_MAX_ROWS = 5e4;
|
|
5
|
-
const INSIGHTS_MATERIALIZATION_LIMIT = INSIGHTS_SCAN_MAX_ROWS + 1;
|
|
6
|
-
const INSIGHTS_SCAN_PAGE_SIZE = 1e3;
|
|
7
|
-
const INSIGHTS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
|
|
8
|
-
const compareCodePoints = (left, right) => {
|
|
9
|
-
if (left < right) return -1;
|
|
10
|
-
if (left > right) return 1;
|
|
11
|
-
return 0;
|
|
12
|
-
};
|
|
13
|
-
const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareCodePoints(right.id, left.id);
|
|
14
|
-
const compareEventOldest = (left, right) => left.received_at_ms - right.received_at_ms || compareCodePoints(left.id, right.id);
|
|
15
|
-
const materializeEventRows = async (scope) => {
|
|
16
|
-
const rows = [];
|
|
17
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
18
|
-
let after = scope.lowerBoundMs === void 0 ? void 0 : {
|
|
19
|
-
receivedAtMs: scope.lowerBoundMs,
|
|
20
|
-
id: INSIGHTS_LOWER_BOUND_ID
|
|
21
|
-
};
|
|
22
|
-
while (rows.length < INSIGHTS_MATERIALIZATION_LIMIT) {
|
|
23
|
-
const limit = Math.min(INSIGHTS_SCAN_PAGE_SIZE, INSIGHTS_MATERIALIZATION_LIMIT - rows.length);
|
|
24
|
-
const page = await scope.persistence.scan({
|
|
25
|
-
beforeReceivedAtMs: scope.cutoffMs,
|
|
26
|
-
...after === void 0 ? {} : { after },
|
|
27
|
-
limit
|
|
28
|
-
});
|
|
29
|
-
if (page.length === 0) break;
|
|
30
|
-
if (page.length > limit) throw new InsightsPersistenceOrderError();
|
|
31
|
-
let previous = after === void 0 ? void 0 : {
|
|
32
|
-
received_at_ms: after.receivedAtMs,
|
|
33
|
-
id: after.id
|
|
34
|
-
};
|
|
35
|
-
for (const row of page) {
|
|
36
|
-
if (row.received_at_ms >= scope.cutoffMs || previous !== void 0 && compareEventOldest(previous, row) >= 0 || seenIds.has(row.id)) throw new InsightsPersistenceOrderError();
|
|
37
|
-
seenIds.add(row.id);
|
|
38
|
-
rows.push(row);
|
|
39
|
-
previous = row;
|
|
40
|
-
}
|
|
41
|
-
const last = page.at(-1);
|
|
42
|
-
if (last !== void 0) after = {
|
|
43
|
-
receivedAtMs: last.received_at_ms,
|
|
44
|
-
id: last.id
|
|
45
|
-
};
|
|
46
|
-
if (rows.length > 5e4) throw new InsightsScanLimitExceededError(INSIGHTS_SCAN_MAX_ROWS);
|
|
47
|
-
}
|
|
48
|
-
return rows;
|
|
49
|
-
};
|
|
50
|
-
var InsightsPersistenceOrderError = class extends Error {
|
|
51
|
-
name = "InsightsPersistenceOrderError";
|
|
52
|
-
constructor() {
|
|
53
|
-
super("Insights persistence did not advance its scan cursor.");
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
const materializeActiveRows = async (scope, window) => {
|
|
57
|
-
const definition = getActiveWindowDefinition(window);
|
|
58
|
-
const durationMs = definition.bucketCount * definition.bucketSizeMs;
|
|
59
|
-
return (await materializeEventRows({
|
|
60
|
-
...scope,
|
|
61
|
-
lowerBoundMs: scope.cutoffMs - durationMs
|
|
62
|
-
})).filter((row) => row.received_at_ms >= scope.cutoffMs - durationMs && ACTIVE_BUNDLE_EVENT_TYPES.includes(row.type));
|
|
63
|
-
};
|
|
64
|
-
const startOfUtcHour = (value) => {
|
|
65
|
-
const date = new Date(value);
|
|
66
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
|
|
67
|
-
};
|
|
68
|
-
const startOfUtcDay = (value) => {
|
|
69
|
-
const date = new Date(value);
|
|
70
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
71
|
-
};
|
|
72
|
-
const getWindowRange = (window, now) => {
|
|
73
|
-
if (window === "24h") return {
|
|
74
|
-
sizeMs: 3600 * 1e3,
|
|
75
|
-
rangeStart: startOfUtcHour(now) - 1380 * 60 * 1e3
|
|
76
|
-
};
|
|
77
|
-
const days = window === "7d" ? 7 : 30;
|
|
78
|
-
return {
|
|
79
|
-
sizeMs: 1440 * 60 * 1e3,
|
|
80
|
-
rangeStart: startOfUtcDay(now) - (days - 1) * 24 * 60 * 60 * 1e3
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
const materializeRowsForWindow = async (scope, window) => {
|
|
84
|
-
if (window === "all") return materializeEventRows(scope);
|
|
85
|
-
const range = getWindowRange(window, scope.cutoffMs);
|
|
86
|
-
return (await materializeEventRows({
|
|
87
|
-
...scope,
|
|
88
|
-
lowerBoundMs: range.rangeStart
|
|
89
|
-
})).filter(({ received_at_ms }) => received_at_ms >= range.rangeStart);
|
|
90
|
-
};
|
|
91
|
-
const bucketStart = (receivedAtMs, sizeMs) => sizeMs === 3600 * 1e3 ? startOfUtcHour(receivedAtMs) : startOfUtcDay(receivedAtMs);
|
|
92
|
-
const createSeries = (request) => {
|
|
93
|
-
const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
|
|
94
|
-
const sizeMs = range?.sizeMs ?? 1440 * 60 * 1e3;
|
|
95
|
-
const installIdsByBucket = /* @__PURE__ */ new Map();
|
|
96
|
-
let oldestMs = request.cutoffMs;
|
|
97
|
-
for (const row of request.rows) {
|
|
98
|
-
oldestMs = Math.min(oldestMs, row.received_at_ms);
|
|
99
|
-
const start = bucketStart(row.received_at_ms, sizeMs);
|
|
100
|
-
const installIds = installIdsByBucket.get(start) ?? /* @__PURE__ */ new Set();
|
|
101
|
-
installIds.add(row.install_id);
|
|
102
|
-
installIdsByBucket.set(start, installIds);
|
|
103
|
-
}
|
|
104
|
-
const first = range?.rangeStart ?? startOfUtcDay(oldestMs);
|
|
105
|
-
const last = bucketStart(request.cutoffMs, sizeMs);
|
|
106
|
-
return Array.from({ length: Math.floor((last - first) / sizeMs) + 1 }, (_, index) => {
|
|
107
|
-
const start = first + index * sizeMs;
|
|
108
|
-
return {
|
|
109
|
-
bucketStartMs: start,
|
|
110
|
-
value: installIdsByBucket.get(start)?.size ?? 0
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
const collectEventActivity = (request) => {
|
|
115
|
-
const range = request.window === "all" ? void 0 : getWindowRange(request.window, request.cutoffMs);
|
|
116
|
-
const rows = range === void 0 ? request.rows : request.rows.filter(({ received_at_ms }) => received_at_ms >= range.rangeStart && received_at_ms < request.cutoffMs);
|
|
117
|
-
const installs = /* @__PURE__ */ new Set();
|
|
118
|
-
const installsByCohort = /* @__PURE__ */ new Map();
|
|
119
|
-
for (const row of rows) {
|
|
120
|
-
installs.add(row.install_id);
|
|
121
|
-
const cohort = installsByCohort.get(row.cohort) ?? /* @__PURE__ */ new Set();
|
|
122
|
-
cohort.add(row.install_id);
|
|
123
|
-
installsByCohort.set(row.cohort, cohort);
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
summary: installs.size,
|
|
127
|
-
cohorts: [...installsByCohort].sort(([left], [right]) => compareCodePoints(left, right)).map(([cohort, installIds]) => ({
|
|
128
|
-
cohort,
|
|
129
|
-
value: installIds.size
|
|
130
|
-
})),
|
|
131
|
-
series: createSeries({
|
|
132
|
-
...request,
|
|
133
|
-
rows
|
|
134
|
-
})
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
//#endregion
|
|
138
|
-
export { INSIGHTS_SCAN_MAX_ROWS, collectEventActivity, compareCodePoints, compareEventNewest, materializeActiveRows, materializeEventRows, materializeRowsForWindow };
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import type { BundleEventPersistenceRow } from "../persistence";
|
|
4
|
-
import { collectActiveInstallationOverview } from "./activeOverview";
|
|
5
|
-
|
|
6
|
-
const HOUR_MS = 60 * 60 * 1_000;
|
|
7
|
-
const DAY_MS = 24 * HOUR_MS;
|
|
8
|
-
|
|
9
|
-
type EventRowOptions = {
|
|
10
|
-
readonly id: string;
|
|
11
|
-
readonly receivedAtMs: number;
|
|
12
|
-
readonly installId?: string;
|
|
13
|
-
readonly userId?: string | null;
|
|
14
|
-
readonly bundleId?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type SeriesWindow = {
|
|
18
|
-
readonly startMs: number;
|
|
19
|
-
readonly bucketCount: number;
|
|
20
|
-
readonly bucketSizeMs: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function eventRow(options: EventRowOptions): BundleEventPersistenceRow {
|
|
24
|
-
return {
|
|
25
|
-
id: options.id,
|
|
26
|
-
type: "UPDATE_APPLIED",
|
|
27
|
-
install_id: options.installId ?? `install-${options.id}`,
|
|
28
|
-
user_id: options.userId ?? null,
|
|
29
|
-
username: null,
|
|
30
|
-
from_release_id: null,
|
|
31
|
-
from_bundle_id: "old",
|
|
32
|
-
to_release_id: null,
|
|
33
|
-
to_bundle_id: options.bundleId ?? "new",
|
|
34
|
-
platform: "ios",
|
|
35
|
-
app_version: "1.0.0",
|
|
36
|
-
channel: "production",
|
|
37
|
-
cohort: "default",
|
|
38
|
-
update_strategy: "fingerprint",
|
|
39
|
-
fingerprint_hash: null,
|
|
40
|
-
sdk_version: null,
|
|
41
|
-
received_at_ms: options.receivedAtMs,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function expectedSeries(
|
|
46
|
-
window: SeriesWindow,
|
|
47
|
-
values: Readonly<Record<number, number>>,
|
|
48
|
-
): { readonly bucketStartMs: number; readonly value: number }[] {
|
|
49
|
-
return Array.from({ length: window.bucketCount }, (_, index) => ({
|
|
50
|
-
bucketStartMs: window.startMs + index * window.bucketSizeMs,
|
|
51
|
-
value: values[index] ?? 0,
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
describe("collectActiveInstallationOverview", () => {
|
|
56
|
-
it("uses the strict window and latest installation rows for a user", () => {
|
|
57
|
-
const asOfMs = 100 * HOUR_MS;
|
|
58
|
-
const windowStartMs = asOfMs - 24 * HOUR_MS;
|
|
59
|
-
const window = {
|
|
60
|
-
startMs: windowStartMs,
|
|
61
|
-
bucketCount: 24,
|
|
62
|
-
bucketSizeMs: HOUR_MS,
|
|
63
|
-
} as const;
|
|
64
|
-
const rows = [
|
|
65
|
-
eventRow({
|
|
66
|
-
id: "a-start",
|
|
67
|
-
receivedAtMs: windowStartMs,
|
|
68
|
-
installId: "install-a",
|
|
69
|
-
userId: "user-a",
|
|
70
|
-
bundleId: "bundle-b",
|
|
71
|
-
}),
|
|
72
|
-
eventRow({
|
|
73
|
-
id: "a-latest",
|
|
74
|
-
receivedAtMs: asOfMs - 1,
|
|
75
|
-
installId: "install-a",
|
|
76
|
-
userId: "user-a",
|
|
77
|
-
bundleId: "bundle-a",
|
|
78
|
-
}),
|
|
79
|
-
eventRow({
|
|
80
|
-
id: "before-window",
|
|
81
|
-
receivedAtMs: windowStartMs - 1,
|
|
82
|
-
userId: "user-a",
|
|
83
|
-
}),
|
|
84
|
-
eventRow({
|
|
85
|
-
id: "at-cutoff",
|
|
86
|
-
receivedAtMs: asOfMs,
|
|
87
|
-
userId: "user-a",
|
|
88
|
-
}),
|
|
89
|
-
eventRow({
|
|
90
|
-
id: "tie-a",
|
|
91
|
-
receivedAtMs: asOfMs - HOUR_MS,
|
|
92
|
-
installId: "install-tie",
|
|
93
|
-
userId: "user-a",
|
|
94
|
-
bundleId: "bundle-b",
|
|
95
|
-
}),
|
|
96
|
-
eventRow({
|
|
97
|
-
id: "tie-z",
|
|
98
|
-
receivedAtMs: asOfMs - HOUR_MS,
|
|
99
|
-
installId: "install-tie",
|
|
100
|
-
userId: "user-a",
|
|
101
|
-
bundleId: "bundle-c",
|
|
102
|
-
}),
|
|
103
|
-
eventRow({
|
|
104
|
-
id: "changed-user-old",
|
|
105
|
-
receivedAtMs: windowStartMs,
|
|
106
|
-
installId: "install-changed-user",
|
|
107
|
-
userId: "user-a",
|
|
108
|
-
}),
|
|
109
|
-
eventRow({
|
|
110
|
-
id: "changed-user-new",
|
|
111
|
-
receivedAtMs: asOfMs - 1,
|
|
112
|
-
installId: "install-changed-user",
|
|
113
|
-
userId: "user-b",
|
|
114
|
-
}),
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
expect(
|
|
118
|
-
collectActiveInstallationOverview({
|
|
119
|
-
rows,
|
|
120
|
-
asOfMs,
|
|
121
|
-
window: "24h",
|
|
122
|
-
userId: "user-a",
|
|
123
|
-
}),
|
|
124
|
-
).toEqual({
|
|
125
|
-
asOfMs,
|
|
126
|
-
window: "24h",
|
|
127
|
-
activeInstallations: 2,
|
|
128
|
-
series: expectedSeries(window, { 0: 1, 23: 2 }),
|
|
129
|
-
bundleSeries: [
|
|
130
|
-
{
|
|
131
|
-
bundleId: "bundle-a",
|
|
132
|
-
series: expectedSeries(window, { 23: 1 }),
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
bundleId: "bundle-b",
|
|
136
|
-
series: expectedSeries(window, { 0: 1 }),
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
bundleId: "bundle-c",
|
|
140
|
-
series: expectedSeries(window, { 23: 1 }),
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
bundles: [
|
|
144
|
-
{ bundleId: "bundle-a", installations: 1 },
|
|
145
|
-
{ bundleId: "bundle-c", installations: 1 },
|
|
146
|
-
],
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("includes installations with any user when the filter is omitted", () => {
|
|
151
|
-
const asOfMs = 10 * DAY_MS;
|
|
152
|
-
const windowStartMs = asOfMs - 7 * DAY_MS;
|
|
153
|
-
const window = {
|
|
154
|
-
startMs: windowStartMs,
|
|
155
|
-
bucketCount: 7,
|
|
156
|
-
bucketSizeMs: DAY_MS,
|
|
157
|
-
} as const;
|
|
158
|
-
const rows = [
|
|
159
|
-
eventRow({
|
|
160
|
-
id: "user-a",
|
|
161
|
-
receivedAtMs: windowStartMs,
|
|
162
|
-
userId: "user-a",
|
|
163
|
-
bundleId: "bundle-b",
|
|
164
|
-
}),
|
|
165
|
-
eventRow({
|
|
166
|
-
id: "anonymous",
|
|
167
|
-
receivedAtMs: asOfMs - 1,
|
|
168
|
-
bundleId: "bundle-a",
|
|
169
|
-
}),
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
const result = collectActiveInstallationOverview({
|
|
173
|
-
rows,
|
|
174
|
-
asOfMs,
|
|
175
|
-
window: "7d",
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
expect(result.activeInstallations).toBe(2);
|
|
179
|
-
expect(result.series).toEqual(expectedSeries(window, { 0: 1, 6: 1 }));
|
|
180
|
-
expect(result.bundles).toEqual([
|
|
181
|
-
{ bundleId: "bundle-a", installations: 1 },
|
|
182
|
-
{ bundleId: "bundle-b", installations: 1 },
|
|
183
|
-
]);
|
|
184
|
-
expect(result.bundleSeries.map(({ bundleId }) => bundleId)).toEqual([
|
|
185
|
-
"bundle-a",
|
|
186
|
-
"bundle-b",
|
|
187
|
-
]);
|
|
188
|
-
});
|
|
189
|
-
});
|