@hot-updater/server 1.0.0-rc.0 → 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 +11 -4
- package/dist/adapters/drizzle.d.cts +0 -1
- package/dist/adapters/drizzle.d.mts +0 -1
- package/dist/adapters/drizzle.mjs +12 -4
- 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 +27 -12
- package/dist/createHotUpdaterCore.d.cts +8 -8
- package/dist/createHotUpdaterCore.d.mts +8 -8
- package/dist/createHotUpdaterCore.mjs +26 -12
- 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.cjs +5 -5
- package/dist/handler.d.cts +1 -2
- package/dist/handler.d.mts +1 -2
- package/dist/handler.mjs +5 -5
- 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 +2 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +2 -2
- package/dist/insights/domain.d.cts +89 -0
- package/dist/insights/domain.d.mts +89 -0
- package/dist/insights/errors.cjs +15 -0
- package/dist/insights/errors.mjs +14 -0
- package/dist/{analytics → insights}/eventInput.cjs +58 -14
- package/dist/{analytics → insights}/eventInput.mjs +58 -16
- 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 +93 -0
- package/dist/insights/queryInput.mjs +91 -0
- package/dist/insights/routes.cjs +58 -0
- package/dist/insights/routes.mjs +56 -0
- package/dist/insights/types.d.cts +32 -0
- package/dist/insights/types.d.mts +32 -0
- 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 +22 -4
- 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 +3 -3
- package/src/createHotUpdaterCore.ts +30 -16
- 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 +11 -3
- package/src/db/types.ts +13 -7
- package/src/handler-standalone.integration.spec.ts +6 -8
- package/src/handler.ts +12 -12
- package/src/index.ts +3 -3
- package/src/insights/domain.ts +103 -0
- package/src/insights/errors.ts +11 -0
- package/src/{analytics → insights}/eventInput.ts +71 -14
- package/src/insights/insights.integration.spec.ts +381 -0
- package/src/insights/provider.spec.ts +385 -0
- package/src/insights/provider.ts +591 -0
- package/src/insights/queryInput.ts +141 -0
- package/src/insights/routes.ts +104 -0
- package/src/insights/types.ts +55 -0
- 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/analytics/bounded/activeOverview.cjs +0 -87
- package/dist/analytics/bounded/activeOverview.mjs +0 -85
- package/dist/analytics/bounded/installationSearch.cjs +0 -42
- package/dist/analytics/bounded/installationSearch.mjs +0 -42
- package/dist/analytics/bounded/persistence.cjs +0 -39
- package/dist/analytics/bounded/persistence.mjs +0 -38
- package/dist/analytics/bounded/provider.cjs +0 -170
- package/dist/analytics/bounded/provider.d.cts +0 -7
- package/dist/analytics/bounded/provider.d.mts +0 -7
- package/dist/analytics/bounded/provider.mjs +0 -170
- package/dist/analytics/bounded/scan.cjs +0 -144
- package/dist/analytics/bounded/scan.mjs +0 -138
- package/dist/analytics/domain.d.cts +0 -121
- package/dist/analytics/domain.d.mts +0 -121
- package/dist/analytics/errors.cjs +0 -22
- package/dist/analytics/errors.mjs +0 -20
- package/dist/analytics/persistence.d.cts +0 -6
- package/dist/analytics/persistence.d.mts +0 -6
- package/dist/analytics/queryInput.cjs +0 -58
- package/dist/analytics/queryInput.mjs +0 -55
- package/dist/analytics/routes.cjs +0 -63
- package/dist/analytics/routes.mjs +0 -61
- package/dist/analytics/types.d.cts +0 -17
- package/dist/analytics/types.d.mts +0 -17
- package/src/analytics/analytics.integration.spec.ts +0 -144
- package/src/analytics/bounded/activeOverview.spec.ts +0 -189
- package/src/analytics/bounded/activeOverview.ts +0 -148
- package/src/analytics/bounded/installationSearch.spec.ts +0 -130
- package/src/analytics/bounded/installationSearch.ts +0 -69
- package/src/analytics/bounded/persistence.ts +0 -43
- package/src/analytics/bounded/provider.spec.ts +0 -219
- package/src/analytics/bounded/provider.ts +0 -229
- package/src/analytics/bounded/scan.ts +0 -235
- package/src/analytics/domain.ts +0 -139
- package/src/analytics/errors.ts +0 -18
- package/src/analytics/persistence.ts +0 -10
- package/src/analytics/queryInput.ts +0 -102
- package/src/analytics/routes.ts +0 -123
- package/src/analytics/types.ts +0 -44
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActiveInstallationWindow,
|
|
3
|
+
InsightsBundleSelection,
|
|
4
|
+
InsightsScope,
|
|
5
|
+
} from "./domain";
|
|
6
|
+
import { InsightsBadRequestError } from "./errors";
|
|
7
|
+
import type {
|
|
8
|
+
InsightsEventPageInput,
|
|
9
|
+
InsightsUserInstallationPageInput,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
const MAX_PAGE_LIMIT = 100;
|
|
13
|
+
const MAX_IDENTITY_LENGTH = 255;
|
|
14
|
+
const MAX_CURSOR_LENGTH = 8 * 1_024;
|
|
15
|
+
|
|
16
|
+
const readSingle = (url: URL, key: string): string | undefined => {
|
|
17
|
+
const values = url.searchParams.getAll(key);
|
|
18
|
+
if (values.length > 1) {
|
|
19
|
+
throw new InsightsBadRequestError(`Duplicate '${key}' query parameter.`);
|
|
20
|
+
}
|
|
21
|
+
return values[0];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const readPageLimit = (url: URL): number | undefined => {
|
|
25
|
+
const value = readSingle(url, "limit");
|
|
26
|
+
if (value === undefined) return undefined;
|
|
27
|
+
const limit = Number(value);
|
|
28
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) {
|
|
29
|
+
throw new InsightsBadRequestError("Invalid 'limit' query parameter.");
|
|
30
|
+
}
|
|
31
|
+
return limit;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const readCursor = (url: URL): string | undefined => {
|
|
35
|
+
const cursor = readSingle(url, "cursor");
|
|
36
|
+
if (
|
|
37
|
+
cursor !== undefined &&
|
|
38
|
+
(cursor.length === 0 || cursor.length > MAX_CURSOR_LENGTH)
|
|
39
|
+
) {
|
|
40
|
+
throw new InsightsBadRequestError("Invalid 'cursor' query parameter.");
|
|
41
|
+
}
|
|
42
|
+
return cursor;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const readId = (
|
|
46
|
+
url: URL,
|
|
47
|
+
key: string,
|
|
48
|
+
maximumLength = MAX_IDENTITY_LENGTH,
|
|
49
|
+
): string => {
|
|
50
|
+
const value = readSingle(url, key);
|
|
51
|
+
if (
|
|
52
|
+
value === undefined ||
|
|
53
|
+
value.length === 0 ||
|
|
54
|
+
value.length > maximumLength
|
|
55
|
+
) {
|
|
56
|
+
throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const readTimestamp = (url: URL, key: string): number | undefined => {
|
|
62
|
+
const raw = readSingle(url, key);
|
|
63
|
+
if (raw === undefined) return undefined;
|
|
64
|
+
const value = Number(raw);
|
|
65
|
+
if (!raw.length || !Number.isSafeInteger(value) || value < 0) {
|
|
66
|
+
throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const readScope = (url: URL): InsightsScope => {
|
|
72
|
+
const platform = readSingle(url, "platform");
|
|
73
|
+
if (platform !== "ios" && platform !== "android") {
|
|
74
|
+
throw new InsightsBadRequestError("Invalid 'platform' query parameter.");
|
|
75
|
+
}
|
|
76
|
+
return { platform, channel: readId(url, "channel", 1_024) };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const parseEventPageInput = (
|
|
80
|
+
request: Request,
|
|
81
|
+
): InsightsEventPageInput => {
|
|
82
|
+
const url = new URL(request.url);
|
|
83
|
+
const bundleFields = ["bundleId", "outcome", "platform", "channel"];
|
|
84
|
+
let bundle: InsightsBundleSelection | undefined;
|
|
85
|
+
if (bundleFields.some((key) => url.searchParams.has(key))) {
|
|
86
|
+
const outcome = readSingle(url, "outcome");
|
|
87
|
+
if (
|
|
88
|
+
outcome !== "applied" &&
|
|
89
|
+
outcome !== "recovered" &&
|
|
90
|
+
outcome !== "adopted"
|
|
91
|
+
) {
|
|
92
|
+
throw new InsightsBadRequestError("Invalid 'outcome' query parameter.");
|
|
93
|
+
}
|
|
94
|
+
bundle = {
|
|
95
|
+
...readScope(url),
|
|
96
|
+
bundleId: readId(url, "bundleId", 1_024),
|
|
97
|
+
outcome,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
beforeReceivedAtMs: readTimestamp(url, "beforeReceivedAtMs"),
|
|
102
|
+
sinceMs: readTimestamp(url, "sinceMs"),
|
|
103
|
+
cursor: readCursor(url),
|
|
104
|
+
limit: readPageLimit(url),
|
|
105
|
+
...(bundle === undefined ? {} : { bundle }),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const parseUserInstallationPageInput = (
|
|
110
|
+
request: Request,
|
|
111
|
+
): InsightsUserInstallationPageInput => {
|
|
112
|
+
const url = new URL(request.url);
|
|
113
|
+
const cursor = readCursor(url);
|
|
114
|
+
const limit = readPageLimit(url);
|
|
115
|
+
return {
|
|
116
|
+
userId: readId(url, "userId"),
|
|
117
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
118
|
+
...(limit === undefined ? {} : { limit }),
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const parseReportingOverviewInput = (
|
|
123
|
+
request: Request,
|
|
124
|
+
): InsightsScope & {
|
|
125
|
+
readonly window: ActiveInstallationWindow;
|
|
126
|
+
readonly bundleId?: string;
|
|
127
|
+
} => {
|
|
128
|
+
const url = new URL(request.url);
|
|
129
|
+
const window = readSingle(url, "window") ?? "30d";
|
|
130
|
+
if (window !== "24h" && window !== "7d" && window !== "30d") {
|
|
131
|
+
throw new InsightsBadRequestError("Invalid 'window' query parameter.");
|
|
132
|
+
}
|
|
133
|
+
const bundleId = readSingle(url, "bundleId");
|
|
134
|
+
return {
|
|
135
|
+
...readScope(url),
|
|
136
|
+
window,
|
|
137
|
+
...(bundleId === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { bundleId: readId(url, "bundleId", 1_024) }),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { RouteHandler } from "../handlerTypes";
|
|
2
|
+
import {
|
|
3
|
+
InsightsBadRequestError,
|
|
4
|
+
InsightsPayloadTooLargeError,
|
|
5
|
+
} from "./errors";
|
|
6
|
+
import { parseBundleEventRequest } from "./eventInput";
|
|
7
|
+
import {
|
|
8
|
+
parseReportingOverviewInput,
|
|
9
|
+
parseEventPageInput,
|
|
10
|
+
parseUserInstallationPageInput,
|
|
11
|
+
} from "./queryInput";
|
|
12
|
+
import type { InsightsProvider } from "./types";
|
|
13
|
+
|
|
14
|
+
const json = (body: unknown, status: number): Response =>
|
|
15
|
+
Response.json(body, {
|
|
16
|
+
headers: { "cache-control": "private, no-store" },
|
|
17
|
+
status,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const requireParam = (
|
|
21
|
+
params: Readonly<Record<string, string>>,
|
|
22
|
+
key: string,
|
|
23
|
+
): string => {
|
|
24
|
+
const value = params[key];
|
|
25
|
+
if (value === undefined || value.length === 0) {
|
|
26
|
+
throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return decodeURIComponent(value);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new InsightsBadRequestError(`Invalid route parameter: ${key}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const run = async (operation: () => Promise<Response>): Promise<Response> => {
|
|
36
|
+
try {
|
|
37
|
+
return await operation();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof InsightsBadRequestError) {
|
|
40
|
+
return json({ error: error.message }, 400);
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof InsightsPayloadTooLargeError) {
|
|
43
|
+
return json({ error: error.message }, 413);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const query = (operation: () => Promise<unknown>): Promise<Response> =>
|
|
50
|
+
run(async () => json(await operation(), 200));
|
|
51
|
+
|
|
52
|
+
export const createInsightsRouteHandlers = (
|
|
53
|
+
provider: InsightsProvider,
|
|
54
|
+
): Record<string, RouteHandler> => ({
|
|
55
|
+
appendBundleEvent: async (_params, request) =>
|
|
56
|
+
run(async () => {
|
|
57
|
+
await provider.appendBundleEvent(await parseBundleEventRequest(request));
|
|
58
|
+
return new Response(null, { status: 204 });
|
|
59
|
+
}),
|
|
60
|
+
getReportingOverview: (_params, request) =>
|
|
61
|
+
query(() =>
|
|
62
|
+
provider.getReportingOverview(parseReportingOverviewInput(request)),
|
|
63
|
+
),
|
|
64
|
+
getInstallation: (params) =>
|
|
65
|
+
run(async () => {
|
|
66
|
+
const installation = await provider.getInstallation({
|
|
67
|
+
installId: requireParam(params, "installId"),
|
|
68
|
+
});
|
|
69
|
+
return installation === null
|
|
70
|
+
? json({ error: "Installation not found" }, 404)
|
|
71
|
+
: json(installation, 200);
|
|
72
|
+
}),
|
|
73
|
+
listEvents: (_params, request) =>
|
|
74
|
+
query(() => provider.listEvents(parseEventPageInput(request))),
|
|
75
|
+
listInstallationEvents: (params, request) =>
|
|
76
|
+
query(() =>
|
|
77
|
+
provider.listInstallationEvents({
|
|
78
|
+
...parseEventPageInput(request),
|
|
79
|
+
installId: requireParam(params, "installId"),
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
pageInstallationsByCurrentUserId: (_params, request) =>
|
|
83
|
+
query(() =>
|
|
84
|
+
provider.pageInstallationsByCurrentUserId(
|
|
85
|
+
parseUserInstallationPageInput(request),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const registerInsightsClientRoutes = (
|
|
91
|
+
add: (method: string, path: string, handler: string) => void,
|
|
92
|
+
): void => {
|
|
93
|
+
add("POST", "/events", "appendBundleEvent");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const registerInsightsAdminRoutes = (
|
|
97
|
+
add: (method: string, path: string, handler: string) => void,
|
|
98
|
+
): void => {
|
|
99
|
+
add("GET", "/events", "listEvents");
|
|
100
|
+
add("GET", "/overview", "getReportingOverview");
|
|
101
|
+
add("GET", "/installations", "pageInstallationsByCurrentUserId");
|
|
102
|
+
add("GET", "/installations/:installId/events", "listInstallationEvents");
|
|
103
|
+
add("GET", "/installations/:installId", "getInstallation");
|
|
104
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ReportingOverview,
|
|
3
|
+
InsightsBundleSelection,
|
|
4
|
+
InsightsScope,
|
|
5
|
+
ActiveInstallationWindow,
|
|
6
|
+
CreateBundleEventRequest,
|
|
7
|
+
CursorPage,
|
|
8
|
+
EventCursorPage,
|
|
9
|
+
EventHistoryRow,
|
|
10
|
+
InstallationHistoryRow,
|
|
11
|
+
InstallationRow,
|
|
12
|
+
} from "./domain";
|
|
13
|
+
|
|
14
|
+
export type InsightsEventPageInput = {
|
|
15
|
+
readonly beforeReceivedAtMs?: number;
|
|
16
|
+
readonly sinceMs?: number;
|
|
17
|
+
readonly bundle?: InsightsBundleSelection;
|
|
18
|
+
readonly cursor?: string;
|
|
19
|
+
readonly limit?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type InsightsInstallationEventPageInput = Omit<
|
|
23
|
+
InsightsEventPageInput,
|
|
24
|
+
"bundle"
|
|
25
|
+
> & {
|
|
26
|
+
readonly installId: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type InsightsUserInstallationPageInput = {
|
|
30
|
+
readonly userId: string;
|
|
31
|
+
readonly cursor?: string;
|
|
32
|
+
readonly limit?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type InsightsProvider = {
|
|
36
|
+
appendBundleEvent(input: CreateBundleEventRequest): Promise<void>;
|
|
37
|
+
listEvents(
|
|
38
|
+
input: InsightsEventPageInput,
|
|
39
|
+
): Promise<EventCursorPage<EventHistoryRow>>;
|
|
40
|
+
listInstallationEvents(
|
|
41
|
+
input: InsightsInstallationEventPageInput,
|
|
42
|
+
): Promise<EventCursorPage<InstallationHistoryRow>>;
|
|
43
|
+
getInstallation(input: {
|
|
44
|
+
readonly installId: string;
|
|
45
|
+
}): Promise<InstallationRow | null>;
|
|
46
|
+
pageInstallationsByCurrentUserId(
|
|
47
|
+
input: InsightsUserInstallationPageInput,
|
|
48
|
+
): Promise<CursorPage<InstallationRow>>;
|
|
49
|
+
getReportingOverview(
|
|
50
|
+
input: InsightsScope & {
|
|
51
|
+
readonly window: ActiveInstallationWindow;
|
|
52
|
+
readonly bundleId?: string;
|
|
53
|
+
},
|
|
54
|
+
): Promise<ReportingOverview>;
|
|
55
|
+
};
|
|
@@ -4,6 +4,7 @@ import { hotUpdaterSchemaVersions } from "./index";
|
|
|
4
4
|
import { createSettingsTable } from "./settings";
|
|
5
5
|
import {
|
|
6
6
|
bundlePatchesV100,
|
|
7
|
+
bundleInstallationsV100,
|
|
7
8
|
bundlesV100,
|
|
8
9
|
channelsV100,
|
|
9
10
|
releaseCatalogsV100,
|
|
@@ -17,6 +18,7 @@ describe("versioned schema DSL", () => {
|
|
|
17
18
|
expect(channelsV100.dsl).toBe("table");
|
|
18
19
|
expect(bundlesV100.dsl).toBe("table");
|
|
19
20
|
expect(bundlePatchesV100.dsl).toBe("table");
|
|
21
|
+
expect(bundleInstallationsV100.dsl).toBe("table");
|
|
20
22
|
expect(releasesV100.dsl).toBe("table");
|
|
21
23
|
expect(releaseCatalogsV100.dsl).toBe("table");
|
|
22
24
|
expect(createSettingsTable("1.0.0").dsl).toBe("table");
|
|
@@ -27,7 +29,7 @@ describe("versioned schema DSL", () => {
|
|
|
27
29
|
}
|
|
28
30
|
});
|
|
29
31
|
|
|
30
|
-
it("registers only schema 1.0.0", () => {
|
|
32
|
+
it("registers only the initial schema 1.0.0", () => {
|
|
31
33
|
expect(hotUpdaterSchemaVersions.map((item) => item.version)).toEqual([
|
|
32
34
|
"1.0.0",
|
|
33
35
|
]);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../db/schemaGenerators";
|
|
8
8
|
import {
|
|
9
9
|
bundleEventsV100,
|
|
10
|
+
bundleInstallationsV100,
|
|
10
11
|
bundlePatchesV100,
|
|
11
12
|
bundlesV100,
|
|
12
13
|
releaseCatalogsV100,
|
|
@@ -57,7 +58,7 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
57
58
|
const patchSize = bundlePatchesV100.columns.find(
|
|
58
59
|
({ ormName }) => ormName === "byte_size",
|
|
59
60
|
);
|
|
60
|
-
const sql = createTableSql("postgresql").join("\n");
|
|
61
|
+
const sql = createTableSql("postgresql", "foreign-keys", v1_0_0).join("\n");
|
|
61
62
|
const prisma = generatePrismaSchema("postgresql", v1_0_0);
|
|
62
63
|
const drizzle = generateDrizzleSchema("postgresql", v1_0_0);
|
|
63
64
|
|
|
@@ -96,6 +97,79 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
96
97
|
).toBeUndefined();
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
it("stores one current row per installation with the required lookup indexes", () => {
|
|
101
|
+
expect(
|
|
102
|
+
bundleInstallationsV100.columns.find(
|
|
103
|
+
({ ormName }) => ormName === "install_id",
|
|
104
|
+
)?.primaryKey,
|
|
105
|
+
).toBe(true);
|
|
106
|
+
expect(bundleInstallationsV100.indexes).toEqual(
|
|
107
|
+
expect.arrayContaining([
|
|
108
|
+
{
|
|
109
|
+
columns: ["user_id", "install_id"],
|
|
110
|
+
name: "bundle_installations_user_id_idx",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
columns: ["received_at_ms"],
|
|
114
|
+
name: "bundle_installations_received_at_idx",
|
|
115
|
+
},
|
|
116
|
+
]),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const sql = createTableSql("postgresql", "foreign-keys", v1_0_0).join("\n");
|
|
120
|
+
const prisma = generatePrismaSchema("postgresql", v1_0_0);
|
|
121
|
+
const drizzle = generateDrizzleSchema("postgresql", v1_0_0);
|
|
122
|
+
expect(sql).toContain("create table bundle_installations");
|
|
123
|
+
expect(sql).toContain(
|
|
124
|
+
'install_id varchar(255) collate "C" primary key not null',
|
|
125
|
+
);
|
|
126
|
+
expect(sql).toContain(
|
|
127
|
+
"create index bundle_installations_user_id_idx on bundle_installations(user_id, install_id)",
|
|
128
|
+
);
|
|
129
|
+
expect(sql).toContain(
|
|
130
|
+
"create index bundle_installations_received_at_idx on bundle_installations(received_at_ms)",
|
|
131
|
+
);
|
|
132
|
+
expect(sql).toContain(
|
|
133
|
+
"create index bundle_installations_bundle_idx on bundle_installations(platform, channel, to_bundle_id, received_at_ms)",
|
|
134
|
+
);
|
|
135
|
+
expect(sql).toContain(
|
|
136
|
+
"create index bundle_events_from_bundle_idx on bundle_events(type, platform, channel, from_bundle_id, received_at_ms, id)",
|
|
137
|
+
);
|
|
138
|
+
expect(prisma).toContain("initial Prisma SQL migration");
|
|
139
|
+
expect(prisma).toContain(
|
|
140
|
+
'alter table bundle_installations alter column install_id type varchar(255) collate "C"',
|
|
141
|
+
);
|
|
142
|
+
expect(prisma).toContain("model bundle_installations {");
|
|
143
|
+
expect(prisma).toContain("install_id String @db.VarChar(255) @id");
|
|
144
|
+
expect(drizzle).toContain("export const bundle_installations = pgTable(");
|
|
145
|
+
expect(drizzle).toContain(
|
|
146
|
+
'index("bundle_installations_user_id_idx").on(table.user_id, table.install_id)',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("keeps identity indexes valid on MySQL and MSSQL", () => {
|
|
151
|
+
const mysql = createTableSql("mysql", "foreign-keys", v1_0_0).join("\n");
|
|
152
|
+
const mssql = createTableSql("mssql", "foreign-keys", v1_0_0).join("\n");
|
|
153
|
+
|
|
154
|
+
expect(mysql).toContain(
|
|
155
|
+
"install_id varchar(255) character set utf8mb4 collate utf8mb4_0900_bin not null",
|
|
156
|
+
);
|
|
157
|
+
expect(mysql).toContain("user_id varchar(255)");
|
|
158
|
+
expect(mssql).toContain("install_id nvarchar(255) not null");
|
|
159
|
+
expect(mssql).toContain("user_id nvarchar(255)");
|
|
160
|
+
|
|
161
|
+
for (const sql of [mysql, mssql]) {
|
|
162
|
+
expect(sql).toContain(
|
|
163
|
+
"create index bundle_events_install_idx on bundle_events(install_id, type, received_at_ms, id)",
|
|
164
|
+
);
|
|
165
|
+
expect(sql).toContain(
|
|
166
|
+
"create index bundle_installations_user_id_idx on bundle_installations(user_id, install_id)",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
expect(mssql).not.toContain("install_id nvarchar(max)");
|
|
170
|
+
expect(mssql).not.toContain("user_id nvarchar(max)");
|
|
171
|
+
});
|
|
172
|
+
|
|
99
173
|
it("generates nullable source and artifact relations with the intended deletion rules", () => {
|
|
100
174
|
const prisma = generatePrismaSchema("postgresql", v1_0_0);
|
|
101
175
|
const drizzle = generateDrizzleSchema("postgresql", v1_0_0);
|
|
@@ -111,7 +185,7 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
111
185
|
});
|
|
112
186
|
|
|
113
187
|
it("creates both projection tables and their constraints from empty", () => {
|
|
114
|
-
const sql = createTableSql("postgresql").join("\n");
|
|
188
|
+
const sql = createTableSql("postgresql", "foreign-keys", v1_0_0).join("\n");
|
|
115
189
|
|
|
116
190
|
expect(sql).toContain("create table releases");
|
|
117
191
|
expect(sql).toContain("create table release_catalogs");
|
|
@@ -124,7 +198,7 @@ describe("v1.0.0 Release Catalog schema", () => {
|
|
|
124
198
|
});
|
|
125
199
|
|
|
126
200
|
it("uses byte-bounded MySQL catalog keys", () => {
|
|
127
|
-
const sql = createTableSql("mysql").join("\n");
|
|
201
|
+
const sql = createTableSql("mysql", "foreign-keys", v1_0_0).join("\n");
|
|
128
202
|
|
|
129
203
|
expect(sql).toContain(
|
|
130
204
|
"scope_key varchar(2048) character set ascii collate ascii_bin",
|
package/src/schema/v1_0_0.ts
CHANGED
|
@@ -366,21 +366,37 @@ export const releaseCatalogsV100 = table(
|
|
|
366
366
|
},
|
|
367
367
|
);
|
|
368
368
|
|
|
369
|
+
const insightsTextCollations = {
|
|
370
|
+
postgresql: '"C"',
|
|
371
|
+
mysql: "utf8mb4_0900_bin",
|
|
372
|
+
} as const;
|
|
373
|
+
const insightsProviders = [
|
|
374
|
+
"postgresql",
|
|
375
|
+
"mysql",
|
|
376
|
+
"sqlite",
|
|
377
|
+
"cockroachdb",
|
|
378
|
+
"mongodb",
|
|
379
|
+
] as const;
|
|
380
|
+
|
|
369
381
|
export const bundleEventsV100 = table(
|
|
370
382
|
"bundle_events",
|
|
371
383
|
{
|
|
372
384
|
id: idColumn("id", "uuid"),
|
|
373
|
-
type:
|
|
374
|
-
install_id:
|
|
375
|
-
|
|
385
|
+
type: column("type", varchar(32)),
|
|
386
|
+
install_id: column("install_id", varchar(255)).collate(
|
|
387
|
+
insightsTextCollations,
|
|
388
|
+
),
|
|
389
|
+
user_id: column("user_id", varchar(255))
|
|
390
|
+
.collate(insightsTextCollations)
|
|
391
|
+
.nullable(),
|
|
376
392
|
username: stringColumn("username").nullable(),
|
|
377
393
|
from_release_id: uuid("from_release_id").nullable(),
|
|
378
394
|
from_bundle_id: uuid("from_bundle_id").nullable(),
|
|
379
395
|
to_release_id: uuid("to_release_id").nullable(),
|
|
380
396
|
to_bundle_id: uuid("to_bundle_id"),
|
|
381
|
-
platform: stringColumn("platform"),
|
|
397
|
+
platform: stringColumn("platform").collate(insightsTextCollations),
|
|
382
398
|
app_version: stringColumn("app_version"),
|
|
383
|
-
channel: stringColumn("channel"),
|
|
399
|
+
channel: stringColumn("channel").collate(insightsTextCollations),
|
|
384
400
|
cohort: stringColumn("cohort"),
|
|
385
401
|
update_strategy: stringColumn("update_strategy").nullable(),
|
|
386
402
|
fingerprint_hash: stringColumn("fingerprint_hash").nullable(),
|
|
@@ -392,35 +408,27 @@ export const bundleEventsV100 = table(
|
|
|
392
408
|
index("bundle_events_received_at_idx", ["received_at_ms", "id"]),
|
|
393
409
|
index("bundle_events_install_idx", [
|
|
394
410
|
"install_id",
|
|
395
|
-
"received_at_ms",
|
|
396
|
-
"id",
|
|
397
|
-
]),
|
|
398
|
-
index("bundle_events_user_id_idx", ["user_id", "received_at_ms", "id"]),
|
|
399
|
-
index("bundle_events_username_idx", ["username", "received_at_ms", "id"]),
|
|
400
|
-
index("bundle_events_to_bundle_idx", [
|
|
401
|
-
"type",
|
|
402
|
-
"to_bundle_id",
|
|
403
|
-
"received_at_ms",
|
|
404
|
-
"id",
|
|
405
|
-
]),
|
|
406
|
-
index("bundle_events_from_bundle_idx", [
|
|
407
|
-
"type",
|
|
408
|
-
"from_bundle_id",
|
|
409
|
-
"received_at_ms",
|
|
410
|
-
"id",
|
|
411
|
-
]),
|
|
412
|
-
index("bundle_events_to_release_idx", [
|
|
413
411
|
"type",
|
|
414
|
-
"to_release_id",
|
|
415
|
-
"received_at_ms",
|
|
416
|
-
"id",
|
|
417
|
-
]),
|
|
418
|
-
index("bundle_events_from_release_idx", [
|
|
419
|
-
"type",
|
|
420
|
-
"from_release_id",
|
|
421
412
|
"received_at_ms",
|
|
422
413
|
"id",
|
|
423
414
|
]),
|
|
415
|
+
index(
|
|
416
|
+
"bundle_events_from_bundle_idx",
|
|
417
|
+
[
|
|
418
|
+
"type",
|
|
419
|
+
"platform",
|
|
420
|
+
"channel",
|
|
421
|
+
"from_bundle_id",
|
|
422
|
+
"received_at_ms",
|
|
423
|
+
"id",
|
|
424
|
+
],
|
|
425
|
+
insightsProviders,
|
|
426
|
+
),
|
|
427
|
+
index(
|
|
428
|
+
"bundle_events_to_bundle_idx",
|
|
429
|
+
["type", "platform", "channel", "to_bundle_id", "received_at_ms", "id"],
|
|
430
|
+
insightsProviders,
|
|
431
|
+
),
|
|
424
432
|
],
|
|
425
433
|
checks: [
|
|
426
434
|
check({
|
|
@@ -449,6 +457,61 @@ export const bundleEventsV100 = table(
|
|
|
449
457
|
},
|
|
450
458
|
);
|
|
451
459
|
|
|
460
|
+
export const bundleInstallationsV100 = table(
|
|
461
|
+
"bundle_installations",
|
|
462
|
+
{
|
|
463
|
+
install_id: idColumn("install_id", varchar(255)).collate(
|
|
464
|
+
insightsTextCollations,
|
|
465
|
+
),
|
|
466
|
+
id: uuid("id"),
|
|
467
|
+
user_id: column("user_id", varchar(255))
|
|
468
|
+
.collate(insightsTextCollations)
|
|
469
|
+
.nullable(),
|
|
470
|
+
username: stringColumn("username").nullable(),
|
|
471
|
+
to_bundle_id: uuid("to_bundle_id"),
|
|
472
|
+
type: column("type", varchar(32)),
|
|
473
|
+
platform: stringColumn("platform").collate(insightsTextCollations),
|
|
474
|
+
app_version: stringColumn("app_version"),
|
|
475
|
+
channel: stringColumn("channel").collate(insightsTextCollations),
|
|
476
|
+
cohort: stringColumn("cohort"),
|
|
477
|
+
received_at_ms: float("received_at_ms"),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
indexes: [
|
|
481
|
+
index("bundle_installations_user_id_idx", ["user_id", "install_id"]),
|
|
482
|
+
index("bundle_installations_received_at_idx", ["received_at_ms"]),
|
|
483
|
+
index(
|
|
484
|
+
"bundle_installations_scope_idx",
|
|
485
|
+
["platform", "channel", "received_at_ms"],
|
|
486
|
+
insightsProviders,
|
|
487
|
+
),
|
|
488
|
+
index(
|
|
489
|
+
"bundle_installations_bundle_idx",
|
|
490
|
+
["platform", "channel", "to_bundle_id", "received_at_ms"],
|
|
491
|
+
insightsProviders,
|
|
492
|
+
),
|
|
493
|
+
],
|
|
494
|
+
checks: [
|
|
495
|
+
check({
|
|
496
|
+
name: "bundle_installations_type_check",
|
|
497
|
+
expression:
|
|
498
|
+
"type in ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')",
|
|
499
|
+
sqliteInline: true,
|
|
500
|
+
}),
|
|
501
|
+
check({
|
|
502
|
+
name: "bundle_installations_platform_check",
|
|
503
|
+
expression: "platform in ('ios', 'android')",
|
|
504
|
+
sqliteInline: true,
|
|
505
|
+
}),
|
|
506
|
+
check({
|
|
507
|
+
name: "bundle_installations_received_at_check",
|
|
508
|
+
expression: "received_at_ms >= 0",
|
|
509
|
+
sqliteInline: true,
|
|
510
|
+
}),
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
|
|
452
515
|
export const v1_0_0 = schema({
|
|
453
516
|
version: "1.0.0",
|
|
454
517
|
settingsTable: HOT_UPDATER_SETTINGS_TABLE,
|
|
@@ -459,6 +522,7 @@ export const v1_0_0 = schema({
|
|
|
459
522
|
releasesV100,
|
|
460
523
|
releaseCatalogsV100,
|
|
461
524
|
bundleEventsV100,
|
|
525
|
+
bundleInstallationsV100,
|
|
462
526
|
apiKeysV100,
|
|
463
527
|
createSettingsTable("1.0.0"),
|
|
464
528
|
],
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
//#region src/analytics/bounded/activeOverview.ts
|
|
2
|
-
const HOUR_MS = 3600 * 1e3;
|
|
3
|
-
const DAY_MS = 24 * HOUR_MS;
|
|
4
|
-
const ACTIVE_BUNDLE_EVENT_TYPES = [
|
|
5
|
-
"UPDATE_APPLIED",
|
|
6
|
-
"RECOVERED",
|
|
7
|
-
"RELEASE_ADOPTED",
|
|
8
|
-
"UNCHANGED"
|
|
9
|
-
];
|
|
10
|
-
const activeWindowDefinitions = {
|
|
11
|
-
"24h": {
|
|
12
|
-
bucketCount: 24,
|
|
13
|
-
bucketSizeMs: HOUR_MS
|
|
14
|
-
},
|
|
15
|
-
"7d": {
|
|
16
|
-
bucketCount: 7,
|
|
17
|
-
bucketSizeMs: DAY_MS
|
|
18
|
-
},
|
|
19
|
-
"30d": {
|
|
20
|
-
bucketCount: 30,
|
|
21
|
-
bucketSizeMs: DAY_MS
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
function getActiveWindowDefinition(window) {
|
|
25
|
-
return activeWindowDefinitions[window];
|
|
26
|
-
}
|
|
27
|
-
function compareCodePoints(left, right) {
|
|
28
|
-
if (left < right) return -1;
|
|
29
|
-
if (left > right) return 1;
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
function isNewer(candidate, current) {
|
|
33
|
-
return candidate.received_at_ms > current.received_at_ms || candidate.received_at_ms === current.received_at_ms && compareCodePoints(candidate.id, current.id) > 0;
|
|
34
|
-
}
|
|
35
|
-
function collectActiveInstallationOverview(request) {
|
|
36
|
-
const definition = getActiveWindowDefinition(request.window);
|
|
37
|
-
const windowStartMs = request.asOfMs - definition.bucketCount * definition.bucketSizeMs;
|
|
38
|
-
const rows = request.rows.filter((row) => row.received_at_ms >= windowStartMs && row.received_at_ms < request.asOfMs);
|
|
39
|
-
const latestByInstall = /* @__PURE__ */ new Map();
|
|
40
|
-
for (const row of rows) {
|
|
41
|
-
const current = latestByInstall.get(row.install_id);
|
|
42
|
-
if (current === void 0 || isNewer(row, current)) latestByInstall.set(row.install_id, row);
|
|
43
|
-
}
|
|
44
|
-
const selectedRows = [...latestByInstall.values()].filter((row) => request.userId === void 0 || row.user_id === request.userId);
|
|
45
|
-
const selectedInstallIds = new Set(selectedRows.map((row) => row.install_id));
|
|
46
|
-
const bundleCounts = /* @__PURE__ */ new Map();
|
|
47
|
-
for (const row of selectedRows) bundleCounts.set(row.to_bundle_id, (bundleCounts.get(row.to_bundle_id) ?? 0) + 1);
|
|
48
|
-
const latestByBucket = Array.from({ length: definition.bucketCount }, () => /* @__PURE__ */ new Map());
|
|
49
|
-
for (const row of rows) {
|
|
50
|
-
if (!selectedInstallIds.has(row.install_id)) continue;
|
|
51
|
-
const bucket = latestByBucket[Math.floor((row.received_at_ms - windowStartMs) / definition.bucketSizeMs)];
|
|
52
|
-
if (bucket === void 0) continue;
|
|
53
|
-
const current = bucket.get(row.install_id);
|
|
54
|
-
if (current === void 0 || isNewer(row, current)) bucket.set(row.install_id, row);
|
|
55
|
-
}
|
|
56
|
-
const bundleCountsByBucket = latestByBucket.map((bucket) => {
|
|
57
|
-
const counts = /* @__PURE__ */ new Map();
|
|
58
|
-
for (const row of bucket.values()) counts.set(row.to_bundle_id, (counts.get(row.to_bundle_id) ?? 0) + 1);
|
|
59
|
-
return counts;
|
|
60
|
-
});
|
|
61
|
-
const bundleObservationTotals = /* @__PURE__ */ new Map();
|
|
62
|
-
for (const counts of bundleCountsByBucket) for (const [bundleId, count] of counts) bundleObservationTotals.set(bundleId, (bundleObservationTotals.get(bundleId) ?? 0) + count);
|
|
63
|
-
return {
|
|
64
|
-
asOfMs: request.asOfMs,
|
|
65
|
-
window: request.window,
|
|
66
|
-
activeInstallations: selectedInstallIds.size,
|
|
67
|
-
series: latestByBucket.map((bucket, index) => ({
|
|
68
|
-
bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
|
|
69
|
-
value: bucket.size
|
|
70
|
-
})),
|
|
71
|
-
bundleSeries: [...bundleObservationTotals].sort(([leftId, leftTotal], [rightId, rightTotal]) => rightTotal - leftTotal || compareCodePoints(leftId, rightId)).map(([bundleId]) => ({
|
|
72
|
-
bundleId,
|
|
73
|
-
series: bundleCountsByBucket.map((counts, index) => ({
|
|
74
|
-
bucketStartMs: windowStartMs + index * definition.bucketSizeMs,
|
|
75
|
-
value: counts.get(bundleId) ?? 0
|
|
76
|
-
}))
|
|
77
|
-
})),
|
|
78
|
-
bundles: [...bundleCounts].map(([bundleId, installations]) => ({
|
|
79
|
-
bundleId,
|
|
80
|
-
installations
|
|
81
|
-
})).sort((left, right) => right.installations - left.installations || compareCodePoints(left.bundleId, right.bundleId))
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
//#endregion
|
|
85
|
-
exports.ACTIVE_BUNDLE_EVENT_TYPES = ACTIVE_BUNDLE_EVENT_TYPES;
|
|
86
|
-
exports.collectActiveInstallationOverview = collectActiveInstallationOverview;
|
|
87
|
-
exports.getActiveWindowDefinition = getActiveWindowDefinition;
|