@hot-updater/server 1.0.0-rc.0 → 1.0.0-rc.1

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.
Files changed (75) hide show
  1. package/dist/adapters/drizzle.cjs +3 -3
  2. package/dist/adapters/drizzle.mjs +3 -3
  3. package/dist/createHotUpdaterCore.cjs +6 -6
  4. package/dist/createHotUpdaterCore.d.cts +7 -7
  5. package/dist/createHotUpdaterCore.d.mts +7 -7
  6. package/dist/createHotUpdaterCore.mjs +6 -6
  7. package/dist/db/types.cjs +1 -1
  8. package/dist/db/types.mjs +1 -1
  9. package/dist/handler.cjs +5 -5
  10. package/dist/handler.mjs +5 -5
  11. package/dist/index.cjs +2 -2
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.mts +4 -4
  14. package/dist/index.mjs +2 -2
  15. package/dist/{analytics → insights}/bounded/activeOverview.cjs +1 -1
  16. package/dist/{analytics → insights}/bounded/activeOverview.mjs +1 -1
  17. package/dist/{analytics → insights}/bounded/installationSearch.cjs +1 -1
  18. package/dist/{analytics → insights}/bounded/installationSearch.mjs +1 -1
  19. package/dist/{analytics → insights}/bounded/persistence.cjs +1 -1
  20. package/dist/{analytics → insights}/bounded/persistence.mjs +1 -1
  21. package/dist/{analytics → insights}/bounded/provider.cjs +9 -9
  22. package/dist/insights/bounded/provider.d.cts +7 -0
  23. package/dist/insights/bounded/provider.d.mts +7 -0
  24. package/dist/{analytics → insights}/bounded/provider.mjs +10 -10
  25. package/dist/{analytics → insights}/bounded/scan.cjs +15 -15
  26. package/dist/{analytics → insights}/bounded/scan.mjs +16 -16
  27. package/dist/{analytics → insights}/domain.d.cts +12 -12
  28. package/dist/{analytics → insights}/domain.d.mts +12 -12
  29. package/dist/insights/errors.cjs +22 -0
  30. package/dist/insights/errors.mjs +20 -0
  31. package/dist/{analytics → insights}/eventInput.cjs +10 -10
  32. package/dist/{analytics → insights}/eventInput.mjs +11 -11
  33. package/dist/insights/persistence.d.cts +6 -0
  34. package/dist/insights/persistence.d.mts +6 -0
  35. package/dist/{analytics → insights}/queryInput.cjs +9 -9
  36. package/dist/{analytics → insights}/queryInput.mjs +10 -10
  37. package/dist/{analytics → insights}/routes.cjs +16 -16
  38. package/dist/{analytics → insights}/routes.mjs +16 -16
  39. package/dist/{analytics → insights}/types.d.cts +6 -6
  40. package/dist/{analytics → insights}/types.d.mts +6 -6
  41. package/dist/package.cjs +1 -1
  42. package/dist/package.mjs +1 -1
  43. package/package.json +4 -4
  44. package/src/adapters/drizzle.ts +3 -3
  45. package/src/apiKeys.integration.spec.ts +2 -2
  46. package/src/createHotUpdaterCore.ts +13 -13
  47. package/src/db/types.spec.ts +3 -3
  48. package/src/db/types.ts +7 -7
  49. package/src/handler-standalone.integration.spec.ts +1 -1
  50. package/src/handler.ts +12 -12
  51. package/src/index.ts +3 -3
  52. package/src/{analytics → insights}/bounded/provider.spec.ts +21 -21
  53. package/src/{analytics → insights}/bounded/provider.ts +18 -18
  54. package/src/{analytics → insights}/bounded/scan.ts +26 -26
  55. package/src/{analytics → insights}/domain.ts +10 -10
  56. package/src/insights/errors.ts +18 -0
  57. package/src/{analytics → insights}/eventInput.ts +11 -11
  58. package/src/{analytics/analytics.integration.spec.ts → insights/insights.integration.spec.ts} +6 -6
  59. package/src/insights/persistence.ts +10 -0
  60. package/src/{analytics → insights}/queryInput.ts +11 -11
  61. package/src/{analytics → insights}/routes.ts +18 -18
  62. package/src/{analytics → insights}/types.ts +7 -7
  63. package/dist/analytics/bounded/provider.d.cts +0 -7
  64. package/dist/analytics/bounded/provider.d.mts +0 -7
  65. package/dist/analytics/errors.cjs +0 -22
  66. package/dist/analytics/errors.mjs +0 -20
  67. package/dist/analytics/persistence.d.cts +0 -6
  68. package/dist/analytics/persistence.d.mts +0 -6
  69. package/src/analytics/errors.ts +0 -18
  70. package/src/analytics/persistence.ts +0 -10
  71. /package/src/{analytics → insights}/bounded/activeOverview.spec.ts +0 -0
  72. /package/src/{analytics → insights}/bounded/activeOverview.ts +0 -0
  73. /package/src/{analytics → insights}/bounded/installationSearch.spec.ts +0 -0
  74. /package/src/{analytics → insights}/bounded/installationSearch.ts +0 -0
  75. /package/src/{analytics → insights}/bounded/persistence.ts +0 -0
@@ -1,17 +1,17 @@
1
1
  import type {
2
- BundleEventAnalyticsWindow,
2
+ BundleEventInsightsWindow,
3
3
  InstallationHistoryRow,
4
4
  } from "../domain";
5
5
  import type {
6
- AnalyticsPersistence,
6
+ InsightsPersistence,
7
7
  BundleEventPersistenceRow,
8
8
  } from "../persistence";
9
- import type { AnalyticsProvider } from "../types";
9
+ import type { InsightsProvider } from "../types";
10
10
  import { collectActiveInstallationOverview } from "./activeOverview";
11
11
  import { searchEventInstallations } from "./installationSearch";
12
12
  import { createBundleEventRow } from "./persistence";
13
13
  import {
14
- ANALYTICS_SCAN_MAX_ROWS,
14
+ INSIGHTS_SCAN_MAX_ROWS,
15
15
  collectEventActivity,
16
16
  compareCodePoints,
17
17
  compareEventNewest,
@@ -57,10 +57,10 @@ const countDistinctInstallations = (
57
57
  rows: readonly BundleEventPersistenceRow[],
58
58
  ): number => new Set(rows.map(({ install_id }) => install_id)).size;
59
59
 
60
- const getAnalyticsResult = async (
61
- persistence: AnalyticsPersistence,
60
+ const getInsightsResult = async (
61
+ persistence: InsightsPersistence,
62
62
  bundleId: string,
63
- window: BundleEventAnalyticsWindow,
63
+ window: BundleEventInsightsWindow,
64
64
  limit: number,
65
65
  offset: number,
66
66
  ) => {
@@ -98,10 +98,10 @@ const getAnalyticsResult = async (
98
98
  };
99
99
  };
100
100
 
101
- const getAnalyticsSummaries = async (
102
- persistence: AnalyticsPersistence,
101
+ const getInsightsSummaries = async (
102
+ persistence: InsightsPersistence,
103
103
  bundleIds: readonly string[],
104
- window: BundleEventAnalyticsWindow,
104
+ window: BundleEventInsightsWindow,
105
105
  ) => {
106
106
  const normalizedBundleIds = [...new Set(bundleIds)];
107
107
  if (normalizedBundleIds.length === 0) return [];
@@ -135,12 +135,12 @@ const getAnalyticsSummaries = async (
135
135
  }));
136
136
  };
137
137
 
138
- export const createAnalyticsProvider = (
139
- persistence: AnalyticsPersistence,
140
- ): AnalyticsProvider =>
138
+ export const createInsightsProvider = (
139
+ persistence: InsightsPersistence,
140
+ ): InsightsProvider =>
141
141
  Object.freeze({
142
142
  mode: "bounded",
143
- maxMatchingRows: ANALYTICS_SCAN_MAX_ROWS,
143
+ maxMatchingRows: INSIGHTS_SCAN_MAX_ROWS,
144
144
  async appendBundleEvent(input) {
145
145
  await persistence.append(createBundleEventRow(input));
146
146
  },
@@ -158,10 +158,10 @@ export const createAnalyticsProvider = (
158
158
  };
159
159
  },
160
160
  getBundleEventSummaries(bundleIds, window) {
161
- return getAnalyticsSummaries(persistence, bundleIds, window);
161
+ return getInsightsSummaries(persistence, bundleIds, window);
162
162
  },
163
- getBundleEventAnalytics(bundleId, window, limit, offset) {
164
- return getAnalyticsResult(persistence, bundleId, window, limit, offset);
163
+ getBundleEventInsights(bundleId, window, limit, offset) {
164
+ return getInsightsResult(persistence, bundleId, window, limit, offset);
165
165
  },
166
166
  async getBundleEventOverview() {
167
167
  const rows = await materializeEventRows({
@@ -226,4 +226,4 @@ export const createAnalyticsProvider = (
226
226
  pagination: { total: ordered.length, limit, offset },
227
227
  };
228
228
  },
229
- } satisfies AnalyticsProvider);
229
+ } satisfies InsightsProvider);
@@ -1,10 +1,10 @@
1
1
  import type {
2
2
  ActiveInstallationWindow,
3
- BundleEventAnalyticsWindow,
3
+ BundleEventInsightsWindow,
4
4
  } from "../domain";
5
- import { AnalyticsScanLimitExceededError } from "../errors";
5
+ import { InsightsScanLimitExceededError } from "../errors";
6
6
  import type {
7
- AnalyticsPersistence,
7
+ InsightsPersistence,
8
8
  BundleEventPersistenceRow,
9
9
  } from "../persistence";
10
10
  import {
@@ -12,20 +12,20 @@ import {
12
12
  getActiveWindowDefinition,
13
13
  } from "./activeOverview";
14
14
 
15
- export const ANALYTICS_SCAN_MAX_ROWS = 50_000;
16
- export const ANALYTICS_MATERIALIZATION_LIMIT = ANALYTICS_SCAN_MAX_ROWS + 1;
17
- export const ANALYTICS_SCAN_PAGE_SIZE = 1_000;
18
- const ANALYTICS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
15
+ export const INSIGHTS_SCAN_MAX_ROWS = 50_000;
16
+ export const INSIGHTS_MATERIALIZATION_LIMIT = INSIGHTS_SCAN_MAX_ROWS + 1;
17
+ export const INSIGHTS_SCAN_PAGE_SIZE = 1_000;
18
+ const INSIGHTS_LOWER_BOUND_ID = "00000000-0000-0000-0000-000000000000";
19
19
 
20
- export type AnalyticsScanScope = {
21
- readonly persistence: AnalyticsPersistence;
20
+ export type InsightsScanScope = {
21
+ readonly persistence: InsightsPersistence;
22
22
  readonly cutoffMs: number;
23
23
  readonly lowerBoundMs?: number;
24
24
  };
25
25
 
26
26
  type EventActivityRequest = {
27
27
  readonly rows: readonly BundleEventPersistenceRow[];
28
- readonly window: BundleEventAnalyticsWindow;
28
+ readonly window: BundleEventInsightsWindow;
29
29
  readonly cutoffMs: number;
30
30
  };
31
31
 
@@ -50,18 +50,18 @@ const compareEventOldest = (
50
50
  compareCodePoints(left.id, right.id);
51
51
 
52
52
  export const materializeEventRows = async (
53
- scope: AnalyticsScanScope,
53
+ scope: InsightsScanScope,
54
54
  ): Promise<readonly BundleEventPersistenceRow[]> => {
55
55
  const rows: BundleEventPersistenceRow[] = [];
56
56
  const seenIds = new Set<string>();
57
57
  let after =
58
58
  scope.lowerBoundMs === undefined
59
59
  ? undefined
60
- : { receivedAtMs: scope.lowerBoundMs, id: ANALYTICS_LOWER_BOUND_ID };
61
- while (rows.length < ANALYTICS_MATERIALIZATION_LIMIT) {
60
+ : { receivedAtMs: scope.lowerBoundMs, id: INSIGHTS_LOWER_BOUND_ID };
61
+ while (rows.length < INSIGHTS_MATERIALIZATION_LIMIT) {
62
62
  const limit = Math.min(
63
- ANALYTICS_SCAN_PAGE_SIZE,
64
- ANALYTICS_MATERIALIZATION_LIMIT - rows.length,
63
+ INSIGHTS_SCAN_PAGE_SIZE,
64
+ INSIGHTS_MATERIALIZATION_LIMIT - rows.length,
65
65
  );
66
66
  const page = await scope.persistence.scan({
67
67
  beforeReceivedAtMs: scope.cutoffMs,
@@ -69,7 +69,7 @@ export const materializeEventRows = async (
69
69
  limit,
70
70
  });
71
71
  if (page.length === 0) break;
72
- if (page.length > limit) throw new AnalyticsPersistenceOrderError();
72
+ if (page.length > limit) throw new InsightsPersistenceOrderError();
73
73
  let previous =
74
74
  after === undefined
75
75
  ? undefined
@@ -80,7 +80,7 @@ export const materializeEventRows = async (
80
80
  (previous !== undefined && compareEventOldest(previous, row) >= 0) ||
81
81
  seenIds.has(row.id)
82
82
  ) {
83
- throw new AnalyticsPersistenceOrderError();
83
+ throw new InsightsPersistenceOrderError();
84
84
  }
85
85
  seenIds.add(row.id);
86
86
  rows.push(row);
@@ -90,23 +90,23 @@ export const materializeEventRows = async (
90
90
  if (last !== undefined) {
91
91
  after = { receivedAtMs: last.received_at_ms, id: last.id };
92
92
  }
93
- if (rows.length > ANALYTICS_SCAN_MAX_ROWS) {
94
- throw new AnalyticsScanLimitExceededError(ANALYTICS_SCAN_MAX_ROWS);
93
+ if (rows.length > INSIGHTS_SCAN_MAX_ROWS) {
94
+ throw new InsightsScanLimitExceededError(INSIGHTS_SCAN_MAX_ROWS);
95
95
  }
96
96
  }
97
97
  return rows;
98
98
  };
99
99
 
100
- export class AnalyticsPersistenceOrderError extends Error {
101
- readonly name = "AnalyticsPersistenceOrderError";
100
+ export class InsightsPersistenceOrderError extends Error {
101
+ readonly name = "InsightsPersistenceOrderError";
102
102
 
103
103
  constructor() {
104
- super("Analytics persistence did not advance its scan cursor.");
104
+ super("Insights persistence did not advance its scan cursor.");
105
105
  }
106
106
  }
107
107
 
108
108
  export const materializeActiveRows = async (
109
- scope: AnalyticsScanScope,
109
+ scope: InsightsScanScope,
110
110
  window: ActiveInstallationWindow,
111
111
  ): Promise<readonly BundleEventPersistenceRow[]> => {
112
112
  const definition = getActiveWindowDefinition(window);
@@ -139,7 +139,7 @@ const startOfUtcDay = (value: number): number => {
139
139
  };
140
140
 
141
141
  export const getWindowRange = (
142
- window: Exclude<BundleEventAnalyticsWindow, "all">,
142
+ window: Exclude<BundleEventInsightsWindow, "all">,
143
143
  now: number,
144
144
  ): { readonly sizeMs: number; readonly rangeStart: number } => {
145
145
  if (window === "24h") {
@@ -156,8 +156,8 @@ export const getWindowRange = (
156
156
  };
157
157
 
158
158
  export const materializeRowsForWindow = async (
159
- scope: AnalyticsScanScope,
160
- window: BundleEventAnalyticsWindow,
159
+ scope: InsightsScanScope,
160
+ window: BundleEventInsightsWindow,
161
161
  ): Promise<readonly BundleEventPersistenceRow[]> => {
162
162
  if (window === "all") return materializeEventRows(scope);
163
163
  const range = getWindowRange(window, scope.cutoffMs);
@@ -44,7 +44,7 @@ export type BundleEventSummaryByBundle = BundleEventSummary & {
44
44
  readonly bundleId: string;
45
45
  };
46
46
 
47
- export type BundleEventAnalyticsWindow = "24h" | "7d" | "30d" | "all";
47
+ export type BundleEventInsightsWindow = "24h" | "7d" | "30d" | "all";
48
48
  export type ActiveInstallationWindow = "24h" | "7d" | "30d";
49
49
 
50
50
  export type InstallationSearchRow = {
@@ -87,25 +87,25 @@ export type OffsetPaginationResult<TData> = {
87
87
  };
88
88
  };
89
89
 
90
- export type AnalyticsSeriesPoint = {
90
+ export type InsightsSeriesPoint = {
91
91
  readonly bucketStartMs: number;
92
92
  readonly value: number;
93
93
  };
94
94
 
95
- export type AnalyticsCohortPoint = {
95
+ export type InsightsCohortPoint = {
96
96
  readonly cohort: string;
97
97
  readonly value: number;
98
98
  };
99
99
 
100
- export type BundleEventAnalyticsResult = {
100
+ export type BundleEventInsightsResult = {
101
101
  readonly summary: BundleEventSummary;
102
102
  readonly series: {
103
- readonly installed: readonly AnalyticsSeriesPoint[];
104
- readonly recovered: readonly AnalyticsSeriesPoint[];
103
+ readonly installed: readonly InsightsSeriesPoint[];
104
+ readonly recovered: readonly InsightsSeriesPoint[];
105
105
  };
106
106
  readonly cohorts: {
107
- readonly installed: readonly AnalyticsCohortPoint[];
108
- readonly recovered: readonly AnalyticsCohortPoint[];
107
+ readonly installed: readonly InsightsCohortPoint[];
108
+ readonly recovered: readonly InsightsCohortPoint[];
109
109
  };
110
110
  readonly recentEvents: OffsetPaginationResult<InstallationHistoryRow>;
111
111
  };
@@ -122,10 +122,10 @@ export type ActiveInstallationOverview = {
122
122
  readonly asOfMs: number;
123
123
  readonly window: ActiveInstallationWindow;
124
124
  readonly activeInstallations: number;
125
- readonly series: readonly AnalyticsSeriesPoint[];
125
+ readonly series: readonly InsightsSeriesPoint[];
126
126
  readonly bundleSeries: readonly {
127
127
  readonly bundleId: string;
128
- readonly series: readonly AnalyticsSeriesPoint[];
128
+ readonly series: readonly InsightsSeriesPoint[];
129
129
  }[];
130
130
  readonly bundles: readonly {
131
131
  readonly bundleId: string;
@@ -0,0 +1,18 @@
1
+ export class InsightsScanLimitExceededError extends Error {
2
+ constructor(readonly limit: number) {
3
+ super(`Insights event scan exceeded ${limit} rows.`);
4
+ this.name = "InsightsScanLimitExceededError";
5
+ }
6
+ }
7
+
8
+ export class InsightsBadRequestError extends Error {
9
+ readonly name = "InsightsBadRequestError";
10
+ }
11
+
12
+ export class InsightsPayloadTooLargeError extends Error {
13
+ readonly name = "InsightsPayloadTooLargeError";
14
+
15
+ constructor(readonly maximumBytes: number) {
16
+ super(`Event payload exceeds ${maximumBytes} bytes`);
17
+ }
18
+ }
@@ -3,8 +3,8 @@ import type {
3
3
  CreateBundleEventRequestBase,
4
4
  } from "./domain";
5
5
  import {
6
- AnalyticsBadRequestError,
7
- AnalyticsPayloadTooLargeError,
6
+ InsightsBadRequestError,
7
+ InsightsPayloadTooLargeError,
8
8
  } from "./errors";
9
9
 
10
10
  const MAX_EVENT_STRING_LENGTH = 1_024;
@@ -42,7 +42,7 @@ function requireStringField(
42
42
  value.length === 0 ||
43
43
  value.length > MAX_EVENT_STRING_LENGTH
44
44
  ) {
45
- throw new AnalyticsBadRequestError(`Invalid event field: ${key}`);
45
+ throw new InsightsBadRequestError(`Invalid event field: ${key}`);
46
46
  }
47
47
  return value;
48
48
  }
@@ -63,7 +63,7 @@ async function readBoundedText(request: Request): Promise<string> {
63
63
  Number.isSafeInteger(declaredByteLength) &&
64
64
  declaredByteLength > EVENT_BODY_MAX_BYTES
65
65
  ) {
66
- throw new AnalyticsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
66
+ throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
67
67
  }
68
68
  if (request.body === null) return "";
69
69
  const reader = request.body.getReader();
@@ -76,7 +76,7 @@ async function readBoundedText(request: Request): Promise<string> {
76
76
  byteLength += result.value.byteLength;
77
77
  if (byteLength > EVENT_BODY_MAX_BYTES) {
78
78
  await reader.cancel();
79
- throw new AnalyticsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
79
+ throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
80
80
  }
81
81
  text += decoder.decode(result.value, { stream: true });
82
82
  }
@@ -89,7 +89,7 @@ async function parseJson(request: Request): Promise<unknown> {
89
89
  return JSON.parse(text);
90
90
  } catch (error) {
91
91
  if (error instanceof SyntaxError) {
92
- throw new AnalyticsBadRequestError("Invalid event payload");
92
+ throw new InsightsBadRequestError("Invalid event payload");
93
93
  }
94
94
  throw error;
95
95
  }
@@ -100,11 +100,11 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
100
100
  !isRecord(payload) ||
101
101
  Object.keys(payload).some((key) => !eventKeys.has(key))
102
102
  ) {
103
- throw new AnalyticsBadRequestError("Invalid event payload");
103
+ throw new InsightsBadRequestError("Invalid event payload");
104
104
  }
105
105
  const platform = requireStringField(payload, "platform");
106
106
  if (platform !== "ios" && platform !== "android") {
107
- throw new AnalyticsBadRequestError("Invalid event field: platform");
107
+ throw new InsightsBadRequestError("Invalid event field: platform");
108
108
  }
109
109
  const base: CreateBundleEventRequestBase = {
110
110
  installId: requireStringField(payload, "installId"),
@@ -134,7 +134,7 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
134
134
  case "RELEASE_ADOPTED": {
135
135
  const updateStrategy = requireStringField(payload, "updateStrategy");
136
136
  if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") {
137
- throw new AnalyticsBadRequestError(
137
+ throw new InsightsBadRequestError(
138
138
  "Invalid event field: updateStrategy",
139
139
  );
140
140
  }
@@ -147,7 +147,7 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
147
147
  }
148
148
  case "UNCHANGED":
149
149
  if (payload.fromBundleId !== null || payload.updateStrategy !== null) {
150
- throw new AnalyticsBadRequestError("Invalid unchanged event shape");
150
+ throw new InsightsBadRequestError("Invalid unchanged event shape");
151
151
  }
152
152
  return {
153
153
  ...base,
@@ -156,7 +156,7 @@ function requireEvent(payload: unknown): CreateBundleEventRequest {
156
156
  updateStrategy: null,
157
157
  };
158
158
  default:
159
- throw new AnalyticsBadRequestError("Invalid event field: type");
159
+ throw new InsightsBadRequestError("Invalid event field: type");
160
160
  }
161
161
  }
162
162
 
@@ -32,12 +32,12 @@ afterEach(() => {
32
32
  vi.useRealTimers();
33
33
  });
34
34
 
35
- describe("createHotUpdater Analytics", () => {
36
- it("always persists events and serves queries without a server Analytics flag", async () => {
35
+ describe("createHotUpdater Insights", () => {
36
+ it("always persists events and serves queries without a server Insights flag", async () => {
37
37
  vi.useFakeTimers();
38
38
  vi.setSystemTime(new Date("2026-08-12T00:00:00.000Z"));
39
39
  const database = createInMemoryDatabasePlugin();
40
- const append = vi.spyOn(database.models.analytics, "append");
40
+ const append = vi.spyOn(database.models.insights, "append");
41
41
  const hotUpdater = createHotUpdater({
42
42
  database,
43
43
  clientAccess: { type: "public" },
@@ -58,7 +58,7 @@ describe("createHotUpdater Analytics", () => {
58
58
  type: "UNCHANGED",
59
59
  }),
60
60
  );
61
- expect(hotUpdater.analytics).toMatchObject({
61
+ expect(hotUpdater.insights).toMatchObject({
62
62
  mode: "bounded",
63
63
  maxMatchingRows: 50_000,
64
64
  });
@@ -69,7 +69,7 @@ describe("createHotUpdater Analytics", () => {
69
69
  });
70
70
  });
71
71
 
72
- it("checks the official schema before Analytics reads and writes", async () => {
72
+ it("checks the official schema before Insights reads and writes", async () => {
73
73
  const database = createSchemaManagedDatabase(
74
74
  "kysely",
75
75
  HOT_UPDATER_SCHEMA_VERSION,
@@ -81,7 +81,7 @@ describe("createHotUpdater Analytics", () => {
81
81
  });
82
82
 
83
83
  expect((await hotUpdater.handlers.client(eventRequest())).status).toBe(204);
84
- await hotUpdater.analytics?.getBundleEventOverview();
84
+ await hotUpdater.insights?.getBundleEventOverview();
85
85
 
86
86
  expect(createMigrator).toHaveBeenCalledOnce();
87
87
  });
@@ -0,0 +1,10 @@
1
+ import type {
2
+ InsightsScanCursor,
3
+ InsightsScanInput,
4
+ InsightsModel,
5
+ BundleEventRow,
6
+ } from "@hot-updater/plugin-core";
7
+
8
+ export type BundleEventPersistenceRow = BundleEventRow;
9
+ export type InsightsPersistence = InsightsModel;
10
+ export type { InsightsScanCursor, InsightsScanInput };
@@ -1,8 +1,8 @@
1
1
  import type {
2
2
  ActiveInstallationInput,
3
- BundleEventAnalyticsWindow,
3
+ BundleEventInsightsWindow,
4
4
  } from "./domain";
5
- import { AnalyticsBadRequestError } from "./errors";
5
+ import { InsightsBadRequestError } from "./errors";
6
6
 
7
7
  type IntegerBounds = {
8
8
  readonly defaultValue: number;
@@ -26,8 +26,8 @@ export type PaginationInput = {
26
26
  readonly offset: number;
27
27
  };
28
28
 
29
- export type AnalyticsQueryInput = PaginationInput & {
30
- readonly window: BundleEventAnalyticsWindow;
29
+ export type InsightsQueryInput = PaginationInput & {
30
+ readonly window: BundleEventInsightsWindow;
31
31
  };
32
32
 
33
33
  function parseInteger(url: URL, key: string, bounds: IntegerBounds): number {
@@ -39,7 +39,7 @@ function parseInteger(url: URL, key: string, bounds: IntegerBounds): number {
39
39
  parsed < bounds.minimum ||
40
40
  (bounds.maximum !== undefined && parsed > bounds.maximum)
41
41
  ) {
42
- throw new AnalyticsBadRequestError(`Invalid '${key}' query parameter.`);
42
+ throw new InsightsBadRequestError(`Invalid '${key}' query parameter.`);
43
43
  }
44
44
  return parsed;
45
45
  }
@@ -52,7 +52,7 @@ export const parsePagination = (request: Request): PaginationInput => {
52
52
  };
53
53
  };
54
54
 
55
- export const parseAnalyticsQuery = (request: Request): AnalyticsQueryInput => {
55
+ export const parseInsightsQuery = (request: Request): InsightsQueryInput => {
56
56
  const url = new URL(request.url);
57
57
  const window = url.searchParams.get("window") ?? "24h";
58
58
  if (
@@ -61,7 +61,7 @@ export const parseAnalyticsQuery = (request: Request): AnalyticsQueryInput => {
61
61
  window !== "30d" &&
62
62
  window !== "all"
63
63
  ) {
64
- throw new AnalyticsBadRequestError("Invalid 'window' query parameter.");
64
+ throw new InsightsBadRequestError("Invalid 'window' query parameter.");
65
65
  }
66
66
  return { ...parsePagination(request), window };
67
67
  };
@@ -72,22 +72,22 @@ export const parseActiveInstallationInput = (
72
72
  const url = new URL(request.url);
73
73
  const windows = url.searchParams.getAll("window");
74
74
  if (windows.length > 1) {
75
- throw new AnalyticsBadRequestError("Duplicate 'window' query parameter.");
75
+ throw new InsightsBadRequestError("Duplicate 'window' query parameter.");
76
76
  }
77
77
  const window = windows[0] ?? "30d";
78
78
  if (window !== "24h" && window !== "7d" && window !== "30d") {
79
- throw new AnalyticsBadRequestError("Invalid 'window' query parameter.");
79
+ throw new InsightsBadRequestError("Invalid 'window' query parameter.");
80
80
  }
81
81
  const userIds = url.searchParams.getAll("userId");
82
82
  if (userIds.length > 1) {
83
- throw new AnalyticsBadRequestError("Duplicate 'userId' query parameter.");
83
+ throw new InsightsBadRequestError("Duplicate 'userId' query parameter.");
84
84
  }
85
85
  const userId = userIds[0];
86
86
  if (
87
87
  userId !== undefined &&
88
88
  (userId.length === 0 || userId.length > MAX_USER_ID_LENGTH)
89
89
  ) {
90
- throw new AnalyticsBadRequestError("Invalid 'userId' query parameter.");
90
+ throw new InsightsBadRequestError("Invalid 'userId' query parameter.");
91
91
  }
92
92
  return userId === undefined ? { window } : { window, userId };
93
93
  };
@@ -1,17 +1,17 @@
1
1
  import type { RouteHandler } from "../handlerTypes";
2
2
  import {
3
- AnalyticsBadRequestError,
4
- AnalyticsPayloadTooLargeError,
5
- AnalyticsScanLimitExceededError,
3
+ InsightsBadRequestError,
4
+ InsightsPayloadTooLargeError,
5
+ InsightsScanLimitExceededError,
6
6
  } from "./errors";
7
7
  import { parseBundleEventRequest } from "./eventInput";
8
8
  import {
9
9
  parseActiveInstallationInput,
10
- parseAnalyticsQuery,
10
+ parseInsightsQuery,
11
11
  parsePagination,
12
12
  parseSearchInput,
13
13
  } from "./queryInput";
14
- import type { AnalyticsProvider } from "./types";
14
+ import type { InsightsProvider } from "./types";
15
15
 
16
16
  const json = (body: unknown, status: number): Response =>
17
17
  Response.json(body, {
@@ -25,7 +25,7 @@ const requireParam = (
25
25
  ): string => {
26
26
  const value = params[key];
27
27
  if (value === undefined || value.length === 0) {
28
- throw new AnalyticsBadRequestError(`Missing route parameter: ${key}`);
28
+ throw new InsightsBadRequestError(`Missing route parameter: ${key}`);
29
29
  }
30
30
  return value;
31
31
  };
@@ -34,17 +34,17 @@ const run = async (operation: () => Promise<Response>): Promise<Response> => {
34
34
  try {
35
35
  return await operation();
36
36
  } catch (error) {
37
- if (error instanceof AnalyticsBadRequestError) {
37
+ if (error instanceof InsightsBadRequestError) {
38
38
  return json({ error: error.message }, 400);
39
39
  }
40
- if (error instanceof AnalyticsPayloadTooLargeError) {
40
+ if (error instanceof InsightsPayloadTooLargeError) {
41
41
  return json({ error: error.message }, 413);
42
42
  }
43
- if (error instanceof AnalyticsScanLimitExceededError) {
43
+ if (error instanceof InsightsScanLimitExceededError) {
44
44
  return json(
45
45
  {
46
46
  error: {
47
- code: "ANALYTICS_SCAN_LIMIT_EXCEEDED",
47
+ code: "INSIGHTS_SCAN_LIMIT_EXCEEDED",
48
48
  limit: error.limit,
49
49
  },
50
50
  },
@@ -58,8 +58,8 @@ const run = async (operation: () => Promise<Response>): Promise<Response> => {
58
58
  const query = (operation: () => Promise<unknown>): Promise<Response> =>
59
59
  run(async () => json(await operation(), 200));
60
60
 
61
- export const createAnalyticsRouteHandlers = (
62
- provider: AnalyticsProvider,
61
+ export const createInsightsRouteHandlers = (
62
+ provider: InsightsProvider,
63
63
  ): Record<string, RouteHandler> => ({
64
64
  appendBundleEvent: async (_params, request) =>
65
65
  run(async () => {
@@ -68,10 +68,10 @@ export const createAnalyticsRouteHandlers = (
68
68
  }),
69
69
  getBundleEventSummary: (params) =>
70
70
  query(() => provider.getBundleEventSummary(requireParam(params, "id"))),
71
- getBundleEventAnalytics: (params, request) =>
71
+ getBundleEventInsights: (params, request) =>
72
72
  query(() => {
73
- const input = parseAnalyticsQuery(request);
74
- return provider.getBundleEventAnalytics(
73
+ const input = parseInsightsQuery(request);
74
+ return provider.getBundleEventInsights(
75
75
  requireParam(params, "id"),
76
76
  input.window,
77
77
  input.limit,
@@ -105,17 +105,17 @@ export const createAnalyticsRouteHandlers = (
105
105
  }),
106
106
  });
107
107
 
108
- export const registerAnalyticsClientRoutes = (
108
+ export const registerInsightsClientRoutes = (
109
109
  add: (method: string, path: string, handler: string) => void,
110
110
  ): void => {
111
111
  add("POST", "/events", "appendBundleEvent");
112
112
  };
113
113
 
114
- export const registerAnalyticsAdminRoutes = (
114
+ export const registerInsightsAdminRoutes = (
115
115
  add: (method: string, path: string, handler: string) => void,
116
116
  ): void => {
117
117
  add("GET", "/bundles/:id/events/summary", "getBundleEventSummary");
118
- add("GET", "/bundles/:id/events/analytics", "getBundleEventAnalytics");
118
+ add("GET", "/bundles/:id/events/insights", "getBundleEventInsights");
119
119
  add("GET", "/installations/overview", "getBundleEventOverview");
120
120
  add("GET", "/installations/active", "getActiveInstallationOverview");
121
121
  add("GET", "/installations", "searchInstallations");
@@ -1,8 +1,8 @@
1
1
  import type {
2
2
  ActiveInstallationInput,
3
3
  ActiveInstallationOverview,
4
- BundleEventAnalyticsResult,
5
- BundleEventAnalyticsWindow,
4
+ BundleEventInsightsResult,
5
+ BundleEventInsightsWindow,
6
6
  BundleEventOverview,
7
7
  BundleEventSummary,
8
8
  BundleEventSummaryByBundle,
@@ -12,21 +12,21 @@ import type {
12
12
  OffsetPaginationResult,
13
13
  } from "./domain";
14
14
 
15
- export type AnalyticsProvider = {
15
+ export type InsightsProvider = {
16
16
  readonly mode: "bounded";
17
17
  readonly maxMatchingRows: number;
18
18
  appendBundleEvent(input: CreateBundleEventRequest): Promise<void>;
19
19
  getBundleEventSummary(bundleId: string): Promise<BundleEventSummary>;
20
20
  getBundleEventSummaries(
21
21
  bundleIds: readonly string[],
22
- window: BundleEventAnalyticsWindow,
22
+ window: BundleEventInsightsWindow,
23
23
  ): Promise<readonly BundleEventSummaryByBundle[]>;
24
- getBundleEventAnalytics(
24
+ getBundleEventInsights(
25
25
  bundleId: string,
26
- window: BundleEventAnalyticsWindow,
26
+ window: BundleEventInsightsWindow,
27
27
  limit: number,
28
28
  offset: number,
29
- ): Promise<BundleEventAnalyticsResult>;
29
+ ): Promise<BundleEventInsightsResult>;
30
30
  getBundleEventOverview(): Promise<BundleEventOverview>;
31
31
  getActiveInstallationOverview(
32
32
  input: ActiveInstallationInput,
@@ -1,7 +0,0 @@
1
- import { AnalyticsProvider } from "../types.cjs";
2
- import { AnalyticsPersistence } from "../persistence.cjs";
3
-
4
- //#region src/analytics/bounded/provider.d.ts
5
- declare const createAnalyticsProvider: (persistence: AnalyticsPersistence) => AnalyticsProvider;
6
- //#endregion
7
- export { createAnalyticsProvider };
@@ -1,7 +0,0 @@
1
- import { AnalyticsProvider } from "../types.mjs";
2
- import { AnalyticsPersistence } from "../persistence.mjs";
3
-
4
- //#region src/analytics/bounded/provider.d.ts
5
- declare const createAnalyticsProvider: (persistence: AnalyticsPersistence) => AnalyticsProvider;
6
- //#endregion
7
- export { createAnalyticsProvider };