@hot-updater/plugin-insights 1.0.0-rc.20

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 (85) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +24 -0
  3. package/client/package.json +7 -0
  4. package/dist/client/eventId.cjs +181 -0
  5. package/dist/client/eventId.mjs +179 -0
  6. package/dist/client/index.cjs +234 -0
  7. package/dist/client/index.d.cts +42 -0
  8. package/dist/client/index.d.mts +42 -0
  9. package/dist/client/index.mjs +233 -0
  10. package/dist/client/sender.cjs +103 -0
  11. package/dist/client/sender.mjs +103 -0
  12. package/dist/client/state.cjs +121 -0
  13. package/dist/client/state.mjs +119 -0
  14. package/dist/internal.cjs +9 -0
  15. package/dist/internal.d.cts +5 -0
  16. package/dist/internal.d.mts +5 -0
  17. package/dist/internal.mjs +5 -0
  18. package/dist/server/contract.cjs +193 -0
  19. package/dist/server/contract.mjs +189 -0
  20. package/dist/server/domain.d.cts +149 -0
  21. package/dist/server/domain.d.mts +149 -0
  22. package/dist/server/errors.cjs +15 -0
  23. package/dist/server/errors.d.cts +6 -0
  24. package/dist/server/errors.d.mts +6 -0
  25. package/dist/server/errors.mjs +14 -0
  26. package/dist/server/eventInput.cjs +284 -0
  27. package/dist/server/eventInput.d.cts +6 -0
  28. package/dist/server/eventInput.d.mts +6 -0
  29. package/dist/server/eventInput.mjs +282 -0
  30. package/dist/server/eventRow.cjs +49 -0
  31. package/dist/server/eventRow.d.cts +64 -0
  32. package/dist/server/eventRow.d.mts +64 -0
  33. package/dist/server/eventRow.mjs +47 -0
  34. package/dist/server/index.cjs +128 -0
  35. package/dist/server/index.d.cts +394 -0
  36. package/dist/server/index.d.mts +394 -0
  37. package/dist/server/index.mjs +121 -0
  38. package/dist/server/model.cjs +17 -0
  39. package/dist/server/model.d.cts +10 -0
  40. package/dist/server/model.d.mts +10 -0
  41. package/dist/server/model.mjs +17 -0
  42. package/dist/server/modelTypes.d.cts +176 -0
  43. package/dist/server/modelTypes.d.mts +176 -0
  44. package/dist/server/overview.cjs +109 -0
  45. package/dist/server/overview.d.cts +15 -0
  46. package/dist/server/overview.d.mts +15 -0
  47. package/dist/server/overview.mjs +107 -0
  48. package/dist/server/provider.cjs +375 -0
  49. package/dist/server/provider.d.cts +6 -0
  50. package/dist/server/provider.d.mts +6 -0
  51. package/dist/server/provider.mjs +374 -0
  52. package/dist/server/queryInput.cjs +93 -0
  53. package/dist/server/queryInput.mjs +91 -0
  54. package/dist/server/reads.cjs +681 -0
  55. package/dist/server/reads.d.cts +60 -0
  56. package/dist/server/reads.d.mts +60 -0
  57. package/dist/server/reads.mjs +674 -0
  58. package/dist/server/recordEvent.cjs +319 -0
  59. package/dist/server/recordEvent.d.cts +17 -0
  60. package/dist/server/recordEvent.d.mts +17 -0
  61. package/dist/server/recordEvent.mjs +316 -0
  62. package/dist/server/routes.cjs +96 -0
  63. package/dist/server/routes.d.cts +36 -0
  64. package/dist/server/routes.d.mts +36 -0
  65. package/dist/server/routes.mjs +95 -0
  66. package/dist/server/schema.cjs +489 -0
  67. package/dist/server/schema.d.cts +706 -0
  68. package/dist/server/schema.d.mts +706 -0
  69. package/dist/server/schema.mjs +483 -0
  70. package/dist/server/testing/eventRow.d.mts +64 -0
  71. package/dist/server/testing/expectInsightsIndex.mjs +18 -0
  72. package/dist/server/testing/fixtures.d.mts +8 -0
  73. package/dist/server/testing/fixtures.mjs +25 -0
  74. package/dist/server/testing/index.d.mts +24 -0
  75. package/dist/server/testing/index.mjs +25 -0
  76. package/dist/server/testing/insightsModelTests.mjs +714 -0
  77. package/dist/server/testing/modelTypes.d.mts +176 -0
  78. package/dist/server/testing/setupInsightsHttpTestSuite.d.mts +11 -0
  79. package/dist/server/testing/setupInsightsHttpTestSuite.mjs +107 -0
  80. package/dist/server/testing/setupInsightsModelTestSuite.d.mts +11 -0
  81. package/dist/server/testing/setupInsightsModelTestSuite.mjs +31 -0
  82. package/dist/server/testing/types.d.mts +26 -0
  83. package/dist/server/types.d.cts +33 -0
  84. package/dist/server/types.d.mts +33 -0
  85. package/package.json +68 -0
@@ -0,0 +1,394 @@
1
+ import { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, BundleEventRow, BundleEventRowBase, DatabaseBundleEventMetadata } from "./eventRow.mjs";
2
+ import { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference } from "./modelTypes.mjs";
3
+ import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.mjs";
4
+ import { InsightsEventPageInput, InsightsInstallationEventPageInput, InsightsProvider, InsightsUserInstallationPageInput } from "./types.mjs";
5
+ import { DEFAULT_INSIGHTS_RETENTION, InsightsRetention, InsightsSchema, createInsightsSchema, insightsSchema } from "./schema.mjs";
6
+ import { InsightsFailureBreakdown, InsightsUpdateFailures, InsightsUpdateFailuresInput } from "./reads.mjs";
7
+ import { createInsightsModel } from "./model.mjs";
8
+ import { createInsightsProvider } from "./provider.mjs";
9
+ import { InsightsIdentityParts, insightsIdentity } from "./recordEvent.mjs";
10
+ import { HotUpdaterDatabase, PluginEndpoint } from "@hot-updater/plugin-core/server-plugin";
11
+ //#region src/server/index.d.ts
12
+ declare const createInsightsApi: (db: HotUpdaterDatabase<InsightsSchema>, now: () => number, retention: InsightsRetention) => {
13
+ /** How long this plugin keeps its rows. */
14
+ retention: InsightsRetention;
15
+ /** Records one validated event row; a repeated id changes nothing. */
16
+ recordEvent: (event: BundleEventRow) => Promise<void>;
17
+ listEvents: (input: InsightsListEventsInput) => Promise<readonly BundleEventRow[]>;
18
+ findLatestEvents: (input: InsightsFindLatestEventsInput) => Promise<readonly BundleEventRow[]>;
19
+ countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
20
+ countEvents: (input: InsightsCountEventsInput) => Promise<number>;
21
+ getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
22
+ getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
23
+ /** A release's or a channel's update failures, and their breakdown over a time range. */
24
+ getUpdateFailures: (input: InsightsUpdateFailuresInput) => Promise<InsightsUpdateFailures>;
25
+ };
26
+ type InsightsApi = ReturnType<typeof createInsightsApi>;
27
+ interface InsightsOptions {
28
+ /**
29
+ * How long to keep rows, in whole days: `rawDays` (90 by default) for raw
30
+ * events and hourly rollups, `dailyDays` (400 by default) for daily
31
+ * rollups and each installation's latest event. Each is at least 1, and
32
+ * `dailyDays` at least `rawDays`.
33
+ */
34
+ readonly retention?: Partial<InsightsRetention>;
35
+ }
36
+ /**
37
+ * Insights: bundle lifecycle events and update failures, each
38
+ * installation's latest event, and the counters, gauges, and sketches its
39
+ * reads come from, each kept for its retention. It serves `POST /events` to
40
+ * clients, and the Insights reads, update failures, and retention to admins.
41
+ */
42
+ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plugin-core/server-plugin").HotUpdaterPlugin<"insights", {
43
+ readonly bundle_events: import("@hot-updater/plugin-core/server-plugin").TableDefinition<{
44
+ readonly id: {
45
+ readonly type: "string";
46
+ readonly maxLength: 36;
47
+ };
48
+ readonly type: {
49
+ readonly type: "string";
50
+ readonly maxLength: 32;
51
+ };
52
+ readonly install_id: {
53
+ readonly type: "string";
54
+ readonly maxLength: 255;
55
+ };
56
+ readonly user_id: {
57
+ readonly type: "string";
58
+ readonly maxLength: 255;
59
+ readonly required: false;
60
+ };
61
+ readonly from_release_id: {
62
+ readonly type: "string";
63
+ readonly maxLength: 36;
64
+ readonly required: false;
65
+ };
66
+ readonly from_bundle_id: {
67
+ readonly type: "string";
68
+ readonly maxLength: 36;
69
+ readonly required: false;
70
+ };
71
+ readonly to_release_id: {
72
+ readonly type: "string";
73
+ readonly maxLength: 36;
74
+ readonly required: false;
75
+ };
76
+ readonly to_bundle_id: {
77
+ readonly type: "string";
78
+ readonly maxLength: 36;
79
+ };
80
+ readonly platform: {
81
+ readonly type: "string";
82
+ readonly maxLength: 16;
83
+ };
84
+ readonly app_version: {
85
+ readonly type: "string";
86
+ };
87
+ readonly channel: {
88
+ readonly type: "string";
89
+ };
90
+ readonly metadata: {
91
+ readonly type: "json";
92
+ };
93
+ readonly received_at_ms: {
94
+ readonly type: "integer";
95
+ };
96
+ }, {
97
+ day: "integer";
98
+ movement_install_id: "string";
99
+ bundle_ref: "string";
100
+ }, {
101
+ readonly recent: {
102
+ readonly eq: readonly ["channel", "platform", "day"];
103
+ readonly sort: readonly ["received_at_ms"];
104
+ };
105
+ readonly movementsByInstall: {
106
+ readonly eq: readonly ["movement_install_id"];
107
+ readonly sort: readonly ["received_at_ms"];
108
+ };
109
+ readonly byBundle: {
110
+ readonly eq: readonly ["platform", "channel", "type", "bundle_ref", "day"];
111
+ readonly sort: readonly ["received_at_ms"];
112
+ };
113
+ readonly byDay: {
114
+ readonly eq: readonly ["day"];
115
+ readonly sort: readonly ["received_at_ms"];
116
+ };
117
+ }, readonly ["id"]>;
118
+ readonly bundle_event_heads: import("@hot-updater/plugin-core/server-plugin").TableDefinition<{
119
+ readonly install_id: {
120
+ readonly type: "string";
121
+ readonly maxLength: 255;
122
+ };
123
+ readonly id: {
124
+ readonly type: "string";
125
+ readonly maxLength: 36;
126
+ };
127
+ readonly type: {
128
+ readonly type: "string";
129
+ readonly maxLength: 32;
130
+ };
131
+ readonly user_id: {
132
+ readonly type: "string";
133
+ readonly maxLength: 255;
134
+ readonly required: false;
135
+ };
136
+ readonly from_release_id: {
137
+ readonly type: "string";
138
+ readonly maxLength: 36;
139
+ readonly required: false;
140
+ };
141
+ readonly from_bundle_id: {
142
+ readonly type: "string";
143
+ readonly maxLength: 36;
144
+ readonly required: false;
145
+ };
146
+ readonly to_release_id: {
147
+ readonly type: "string";
148
+ readonly maxLength: 36;
149
+ readonly required: false;
150
+ };
151
+ readonly to_bundle_id: {
152
+ readonly type: "string";
153
+ readonly maxLength: 36;
154
+ };
155
+ readonly platform: {
156
+ readonly type: "string";
157
+ readonly maxLength: 16;
158
+ };
159
+ readonly app_version: {
160
+ readonly type: "string";
161
+ };
162
+ readonly channel: {
163
+ readonly type: "string";
164
+ };
165
+ readonly metadata: {
166
+ readonly type: "json";
167
+ };
168
+ readonly received_at_ms: {
169
+ readonly type: "integer";
170
+ };
171
+ }, Readonly<Record<string, import("@hot-updater/plugin-core/server-plugin").FieldType>>, {
172
+ readonly byUser: {
173
+ readonly eq: readonly ["user_id"];
174
+ readonly sort: readonly ["install_id"];
175
+ };
176
+ }, readonly ["install_id"]>;
177
+ readonly insights_overview: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
178
+ readonly identity: {
179
+ readonly type: "string";
180
+ readonly maxLength: 32;
181
+ };
182
+ readonly bucket_start_ms: {
183
+ readonly type: "integer";
184
+ };
185
+ }, "downloads" | "launches" | "failed_launches" | "failed_updates" | "patch_downloads" | "patch_fallbacks", {
186
+ readonly window: {
187
+ readonly eq: readonly ["identity"];
188
+ readonly sort: readonly ["bucket_start_ms"];
189
+ };
190
+ }, never, readonly ["identity", "bucket_start_ms"]>;
191
+ readonly insights_sketches: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
192
+ readonly identity: {
193
+ readonly type: "string";
194
+ readonly maxLength: 32;
195
+ };
196
+ readonly bucket_start_ms: {
197
+ readonly type: "integer";
198
+ };
199
+ }, never, {
200
+ readonly window: {
201
+ readonly eq: readonly ["identity"];
202
+ readonly sort: readonly ["bucket_start_ms"];
203
+ };
204
+ }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
205
+ readonly insights_overview_daily: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
206
+ readonly identity: {
207
+ readonly type: "string";
208
+ readonly maxLength: 32;
209
+ };
210
+ readonly bucket_start_ms: {
211
+ readonly type: "integer";
212
+ };
213
+ }, "downloads" | "launches" | "failed_launches" | "failed_updates" | "patch_downloads" | "patch_fallbacks", {
214
+ readonly window: {
215
+ readonly eq: readonly ["identity"];
216
+ readonly sort: readonly ["bucket_start_ms"];
217
+ };
218
+ }, never, readonly ["identity", "bucket_start_ms"]>;
219
+ readonly insights_sketches_daily: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
220
+ readonly identity: {
221
+ readonly type: "string";
222
+ readonly maxLength: 32;
223
+ };
224
+ readonly bucket_start_ms: {
225
+ readonly type: "integer";
226
+ };
227
+ }, never, {
228
+ readonly window: {
229
+ readonly eq: readonly ["identity"];
230
+ readonly sort: readonly ["bucket_start_ms"];
231
+ };
232
+ }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
233
+ readonly insights_overview_lifetime: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
234
+ readonly identity: {
235
+ readonly type: "string";
236
+ readonly maxLength: 32;
237
+ };
238
+ readonly bucket_start_ms: {
239
+ readonly type: "integer";
240
+ };
241
+ }, "downloads" | "launches" | "failed_launches" | "failed_updates" | "patch_downloads" | "patch_fallbacks", {
242
+ readonly window: {
243
+ readonly eq: readonly ["identity"];
244
+ readonly sort: readonly ["bucket_start_ms"];
245
+ };
246
+ }, never, readonly ["identity", "bucket_start_ms"]>;
247
+ readonly insights_sketches_lifetime: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
248
+ readonly identity: {
249
+ readonly type: "string";
250
+ readonly maxLength: 32;
251
+ };
252
+ readonly bucket_start_ms: {
253
+ readonly type: "integer";
254
+ };
255
+ }, never, {
256
+ readonly window: {
257
+ readonly eq: readonly ["identity"];
258
+ readonly sort: readonly ["bucket_start_ms"];
259
+ };
260
+ }, "failed_users", readonly ["identity", "bucket_start_ms"]>;
261
+ readonly insights_distribution: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
262
+ readonly channel: {
263
+ readonly type: "string";
264
+ };
265
+ readonly platform: {
266
+ readonly type: "string";
267
+ readonly maxLength: 16;
268
+ };
269
+ readonly app_version: {
270
+ readonly type: "string";
271
+ };
272
+ readonly release_id: {
273
+ readonly type: "string";
274
+ readonly maxLength: 36;
275
+ };
276
+ readonly bucket_start_ms: {
277
+ readonly type: "integer";
278
+ };
279
+ }, "latest_installations", {
280
+ readonly byScope: {
281
+ readonly eq: readonly ["channel", "platform"];
282
+ readonly sort: readonly ["bucket_start_ms"];
283
+ };
284
+ readonly byVersion: {
285
+ readonly eq: readonly ["channel", "platform", "app_version"];
286
+ readonly sort: readonly ["bucket_start_ms"];
287
+ };
288
+ }, never, readonly ["channel", "platform", "app_version", "release_id", "bucket_start_ms"]>;
289
+ readonly insights_latest_by_bundle: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
290
+ readonly platform: {
291
+ readonly type: "string";
292
+ readonly maxLength: 16;
293
+ };
294
+ readonly channel: {
295
+ readonly type: "string";
296
+ };
297
+ readonly bundle_field: {
298
+ readonly type: "string";
299
+ readonly maxLength: 16;
300
+ };
301
+ readonly bundle_id: {
302
+ readonly type: "string";
303
+ readonly maxLength: 36;
304
+ };
305
+ readonly type: {
306
+ readonly type: "string";
307
+ readonly maxLength: 32;
308
+ };
309
+ readonly bucket_start_ms: {
310
+ readonly type: "integer";
311
+ };
312
+ }, "installations", {
313
+ readonly byBundle: {
314
+ readonly eq: readonly ["platform", "channel", "bundle_field", "bundle_id", "type"];
315
+ readonly sort: readonly ["bucket_start_ms"];
316
+ };
317
+ }, never, readonly ["platform", "channel", "bundle_field", "bundle_id", "type", "bucket_start_ms"]>;
318
+ readonly insights_outcomes: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
319
+ readonly platform: {
320
+ readonly type: "string";
321
+ readonly maxLength: 16;
322
+ };
323
+ readonly channel: {
324
+ readonly type: "string";
325
+ };
326
+ readonly type: {
327
+ readonly type: "string";
328
+ readonly maxLength: 32;
329
+ };
330
+ readonly bundle_ref: {
331
+ readonly type: "string";
332
+ readonly maxLength: 41;
333
+ };
334
+ readonly bucket_start_ms: {
335
+ readonly type: "integer";
336
+ };
337
+ }, "events", {
338
+ readonly byRef: {
339
+ readonly eq: readonly ["platform", "channel", "type", "bundle_ref"];
340
+ readonly sort: readonly ["bucket_start_ms"];
341
+ };
342
+ }, never, readonly ["platform", "channel", "type", "bundle_ref", "bucket_start_ms"]>;
343
+ readonly insights_failures: import("@hot-updater/plugin-core/server-plugin").AggregateDefinition<{
344
+ readonly platform: {
345
+ readonly type: "string";
346
+ readonly maxLength: 16;
347
+ };
348
+ readonly channel: {
349
+ readonly type: "string";
350
+ };
351
+ readonly bucket_start_ms: {
352
+ readonly type: "integer";
353
+ };
354
+ readonly release_id: {
355
+ readonly type: "string";
356
+ readonly maxLength: 36;
357
+ };
358
+ readonly stage: {
359
+ readonly type: "string";
360
+ readonly maxLength: 16;
361
+ };
362
+ readonly reason: {
363
+ readonly type: "string";
364
+ readonly maxLength: 64;
365
+ };
366
+ readonly detail: {
367
+ readonly type: "string";
368
+ readonly maxLength: 160;
369
+ };
370
+ }, "events", {
371
+ readonly byScope: {
372
+ readonly eq: readonly ["platform", "channel"];
373
+ readonly sort: readonly ["bucket_start_ms"];
374
+ };
375
+ }, never, readonly ["platform", "channel", "bucket_start_ms", "release_id", "stage", "reason", "detail"]>;
376
+ }, {
377
+ api: {
378
+ /** How long this plugin keeps its rows. */
379
+ retention: InsightsRetention;
380
+ /** Records one validated event row; a repeated id changes nothing. */
381
+ recordEvent: (event: BundleEventRow) => Promise<void>;
382
+ listEvents: (input: InsightsListEventsInput) => Promise<readonly BundleEventRow[]>;
383
+ findLatestEvents: (input: InsightsFindLatestEventsInput) => Promise<readonly BundleEventRow[]>;
384
+ countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
385
+ countEvents: (input: InsightsCountEventsInput) => Promise<number>;
386
+ getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
387
+ getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
388
+ /** A release's or a channel's update failures, and their breakdown over a time range. */
389
+ getUpdateFailures: (input: InsightsUpdateFailuresInput) => Promise<InsightsUpdateFailures>;
390
+ };
391
+ endpoints: PluginEndpoint[];
392
+ }, {}>;
393
+ //#endregion
394
+ export { type ActiveInstallationWindow, type BundleEventFailure, type BundleEventFailureInput, type BundleEventFailureReason, type BundleEventFailureStage, type BundleEventRow, type BundleEventRowBase, type CreateBundleEventRequest, type CreateBundleEventRequestBase, type CursorPage, DEFAULT_INSIGHTS_RETENTION, type DatabaseBundleEventMetadata, type EventCursorPage, type EventHistoryRow, InsightsApi, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsIdentityParts, type InsightsInstallationEventPageInput, type InsightsListEventsInput, type InsightsModel, InsightsOptions, type InsightsProvider, type InsightsRecordEventInput, type InsightsRetention, type InsightsSchema, type InsightsScope, type InsightsTimeRange, type InsightsUpdateFailures, type InsightsUpdateFailuresInput, type InsightsUserInstallationPageInput, type InstallationHistoryRow, type InstallationRow, type ReleaseActivityMetrics, type ReleaseReference, type ReportingOverview, createInsightsModel, createInsightsProvider, createInsightsSchema, insights, insightsIdentity, insightsSchema };
@@ -0,0 +1,121 @@
1
+ import { createInsightsModel } from "./model.mjs";
2
+ import { createInsightsProvider } from "./provider.mjs";
3
+ import { DEFAULT_INSIGHTS_RETENTION, createInsightsSchema, insightsSchema } from "./schema.mjs";
4
+ import { insightsIdentity, recordEvent } from "./recordEvent.mjs";
5
+ import { countEvents, countLatestEvents, findLatestEvents, getAppUsage, getReleaseActivity, getUpdateFailures, listEvents } from "./reads.mjs";
6
+ import { INSIGHTS_ROUTES, createInsightsRouteHandlers } from "./routes.mjs";
7
+ import { DatabaseAdapterInputError } from "@hot-updater/plugin-core";
8
+ import { markOfficial } from "@hot-updater/plugin-core/internal";
9
+ import { HotUpdaterConfigError, definePlugin, isDatabaseBusyError } from "@hot-updater/plugin-core/server-plugin";
10
+ //#region src/server/index.ts
11
+ const createInsightsApi = (db, now, retention) => ({
12
+ /** How long this plugin keeps its rows. */
13
+ retention,
14
+ /** Records one validated event row; a repeated id changes nothing. */
15
+ recordEvent: (event) => recordEvent(db, event),
16
+ listEvents: (input) => listEvents(db, input),
17
+ findLatestEvents: (input) => findLatestEvents(db, input),
18
+ countLatestEvents: (input) => countLatestEvents(db, input),
19
+ countEvents: (input) => countEvents(db, input),
20
+ getReleaseActivity: (input) => getReleaseActivity(db, input, now, retention),
21
+ getAppUsage: (input) => getAppUsage(db, input, now, retention),
22
+ /** A release's or a channel's update failures, and their breakdown over a time range. */
23
+ getUpdateFailures: (input) => getUpdateFailures(db, input, now, retention)
24
+ });
25
+ /** `GET /failures`'s query: a scope, an optional release, and a time range or none. */
26
+ const readFailuresQuery = (url) => {
27
+ const query = url.searchParams;
28
+ const time = (name) => {
29
+ const value = query.get(name);
30
+ return value === null ? void 0 : Number(value);
31
+ };
32
+ const [start, end] = [time("start"), time("end")];
33
+ const platform = query.get("platform");
34
+ const releaseId = query.get("releaseId");
35
+ if (platform !== "ios" && platform !== "android" || start === void 0 !== (end === void 0)) throw new DatabaseAdapterInputError("invalid-query");
36
+ return {
37
+ platform,
38
+ channel: query.get("channel") ?? "",
39
+ ...releaseId === null ? {} : { releaseId },
40
+ ...start === void 0 ? {} : { timeRange: {
41
+ start,
42
+ end
43
+ } }
44
+ };
45
+ };
46
+ /**
47
+ * The admin route of the update failures read: a release's (`releaseId`)
48
+ * since its first report, or over `start`–`end`, or a channel's over a range,
49
+ * which covers at most 30 days.
50
+ */
51
+ const failuresEndpoint = (api) => ({
52
+ access: "admin",
53
+ method: "GET",
54
+ path: "/failures",
55
+ handler: async (request) => {
56
+ try {
57
+ return Response.json(await api.getUpdateFailures(readFailuresQuery(new URL(request.url))), { headers: { "cache-control": "private, no-store" } });
58
+ } catch (error) {
59
+ if (error instanceof DatabaseAdapterInputError) return Response.json({ error: "Send platform and channel, and releaseId or start and end in epoch milliseconds, at most 30 days apart." }, { status: 400 });
60
+ if (!isDatabaseBusyError(error)) throw error;
61
+ return Response.json({ error: "Service unavailable" }, {
62
+ status: 503,
63
+ headers: { "retry-after": "5" }
64
+ });
65
+ }
66
+ }
67
+ });
68
+ const retentionOf = ({ retention }) => {
69
+ const rawDays = retention?.rawDays ?? 90;
70
+ const dailyDays = retention?.dailyDays ?? 400;
71
+ const whole = (days) => Number.isSafeInteger(days) && days >= 1;
72
+ if (!whole(rawDays) || !whole(dailyDays) || dailyDays < rawDays) throw new HotUpdaterConfigError(`insights({ retention }) takes whole days of at least 1, with dailyDays at least rawDays; got rawDays ${rawDays} and dailyDays ${dailyDays}.`);
73
+ return {
74
+ rawDays,
75
+ dailyDays
76
+ };
77
+ };
78
+ /**
79
+ * Insights: bundle lifecycle events and update failures, each
80
+ * installation's latest event, and the counters, gauges, and sketches its
81
+ * reads come from, each kept for its retention. It serves `POST /events` to
82
+ * clients, and the Insights reads, update failures, and retention to admins.
83
+ */
84
+ const insights = (options = {}) => {
85
+ const retention = retentionOf(options);
86
+ const plugin = definePlugin({
87
+ id: "insights",
88
+ namespace: false,
89
+ schemaVersion: "1.2.0",
90
+ schema: createInsightsSchema(retention),
91
+ init: ({ db, now }) => {
92
+ const api = createInsightsApi(db, now, retention);
93
+ const routes = createInsightsRouteHandlers(createInsightsProvider(createInsightsModel(api)));
94
+ return {
95
+ api,
96
+ endpoints: [
97
+ ...INSIGHTS_ROUTES.map(({ access, method, path, handler }) => ({
98
+ access,
99
+ method,
100
+ path,
101
+ handler: (request, params) => routes[handler](params, request)
102
+ })),
103
+ failuresEndpoint(api),
104
+ {
105
+ access: "admin",
106
+ method: "GET",
107
+ path: "/retention",
108
+ handler: async () => Response.json(retention)
109
+ }
110
+ ]
111
+ };
112
+ },
113
+ cli: { clientPlugin: {
114
+ module: "@hot-updater/react-native/plugins/insights",
115
+ name: "insights"
116
+ } }
117
+ });
118
+ return markOfficial(plugin);
119
+ };
120
+ //#endregion
121
+ export { DEFAULT_INSIGHTS_RETENTION, createInsightsModel, createInsightsProvider, createInsightsSchema, insights, insightsIdentity, insightsSchema };
@@ -0,0 +1,17 @@
1
+ const require_contract = require("./contract.cjs");
2
+ //#region src/server/model.ts
3
+ /**
4
+ * The plugin's API as `InsightsModel`, validated at its boundary: what the
5
+ * Insights routes, the console, and the e2e harness read through.
6
+ */
7
+ const createInsightsModel = (api) => require_contract.createValidatedInsightsModel({
8
+ recordEvent: ({ event }) => api.recordEvent(event),
9
+ listEvents: api.listEvents,
10
+ findLatestEvents: api.findLatestEvents,
11
+ countLatestEvents: api.countLatestEvents,
12
+ countEvents: api.countEvents,
13
+ getReleaseActivity: api.getReleaseActivity,
14
+ getAppUsage: api.getAppUsage
15
+ });
16
+ //#endregion
17
+ exports.createInsightsModel = createInsightsModel;
@@ -0,0 +1,10 @@
1
+ import { InsightsModel } from "./modelTypes.cjs";
2
+ import { InsightsApi } from "./index.cjs";
3
+ //#region src/server/model.d.ts
4
+ /**
5
+ * The plugin's API as `InsightsModel`, validated at its boundary: what the
6
+ * Insights routes, the console, and the e2e harness read through.
7
+ */
8
+ declare const createInsightsModel: (api: InsightsApi) => InsightsModel;
9
+ //#endregion
10
+ export { createInsightsModel };
@@ -0,0 +1,10 @@
1
+ import { InsightsModel } from "./modelTypes.mjs";
2
+ import { InsightsApi } from "./index.mjs";
3
+ //#region src/server/model.d.ts
4
+ /**
5
+ * The plugin's API as `InsightsModel`, validated at its boundary: what the
6
+ * Insights routes, the console, and the e2e harness read through.
7
+ */
8
+ declare const createInsightsModel: (api: InsightsApi) => InsightsModel;
9
+ //#endregion
10
+ export { createInsightsModel };
@@ -0,0 +1,17 @@
1
+ import { createValidatedInsightsModel } from "./contract.mjs";
2
+ //#region src/server/model.ts
3
+ /**
4
+ * The plugin's API as `InsightsModel`, validated at its boundary: what the
5
+ * Insights routes, the console, and the e2e harness read through.
6
+ */
7
+ const createInsightsModel = (api) => createValidatedInsightsModel({
8
+ recordEvent: ({ event }) => api.recordEvent(event),
9
+ listEvents: api.listEvents,
10
+ findLatestEvents: api.findLatestEvents,
11
+ countLatestEvents: api.countLatestEvents,
12
+ countEvents: api.countEvents,
13
+ getReleaseActivity: api.getReleaseActivity,
14
+ getAppUsage: api.getAppUsage
15
+ });
16
+ //#endregion
17
+ export { createInsightsModel };