@hot-updater/plugin-insights 1.0.0-rc.29 → 1.0.0-rc.31

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.
@@ -150,21 +150,23 @@ const createValidatedInsightsModel = (model) => ({
150
150
  ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
151
151
  return validateCount(await model.countEvents(input));
152
152
  },
153
- async getReleaseActivity(input) {
153
+ async countEventSeries(input) {
154
154
  if (!require_eventRow.isRecord(input) || !hasOnlyKeys(input, [
155
- "releases",
156
- "scope",
157
- "timeRange"
158
- ])) invalidQuery();
159
- const releases = input.releases;
160
- const scope = input.scope;
161
- const timeRange = input.timeRange;
162
- const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 || isTimeRange(timeRange));
163
- const scopeQuery = releases === void 0 && require_eventRow.isRecord(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
164
- if (!releaseQuery && !scopeQuery) invalidQuery();
155
+ "filter",
156
+ "timeRange",
157
+ "intervalMs"
158
+ ]) || !isBundleFilter(input.filter) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs)) invalidQuery();
159
+ const { start, end } = input.timeRange;
160
+ const { intervalMs } = input;
161
+ if (intervalMs === 0 || intervalMs % 36e5 !== 0 || start % 36e5 !== 0 || (end - start) % intervalMs !== 0 || end - start > 7776e6) invalidQuery();
162
+ const points = await model.countEventSeries(input);
163
+ if (!Array.isArray(points) || points.length !== (end - start) / intervalMs || points.some((point, index) => !require_eventRow.isRecord(point) || !hasOnlyKeys(point, ["startMs", "events"]) || point.startMs !== start + index * intervalMs || !isTimestamp(point.events))) invalidResult();
164
+ return points;
165
+ },
166
+ async getReleaseActivity(input) {
167
+ if (!require_eventRow.isRecord(input) || !hasOnlyKeys(input, ["releases"]) || !Array.isArray(input.releases) || input.releases.length < 1 || input.releases.length > 100 || !input.releases.every(isReleaseReference)) invalidQuery();
165
168
  const result = await model.getReleaseActivity(input);
166
- validateAggregateResult(result);
167
- if (!Array.isArray(result.data)) invalidResult();
169
+ if (!require_eventRow.isRecord(result) || !isTimestamp(result.measuredAtMs) || !Array.isArray(result.data) || result.data.some((item) => !require_eventRow.isRecord(item) || !isReleaseReference(item.release) || !require_eventRow.isRecord(item.metrics) || !isTimestamp(item.metrics.downloads) || !isTimestamp(item.metrics.launches) || !isTimestamp(item.metrics.failedLaunches))) invalidResult();
168
170
  return result;
169
171
  },
170
172
  async getAppUsage(input) {
@@ -150,21 +150,23 @@ const createValidatedInsightsModel = (model) => ({
150
150
  ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
151
151
  return validateCount(await model.countEvents(input));
152
152
  },
153
- async getReleaseActivity(input) {
153
+ async countEventSeries(input) {
154
154
  if (!isRecord(input) || !hasOnlyKeys(input, [
155
- "releases",
156
- "scope",
157
- "timeRange"
158
- ])) invalidQuery();
159
- const releases = input.releases;
160
- const scope = input.scope;
161
- const timeRange = input.timeRange;
162
- const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 || isTimeRange(timeRange));
163
- const scopeQuery = releases === void 0 && isRecord(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
164
- if (!releaseQuery && !scopeQuery) invalidQuery();
155
+ "filter",
156
+ "timeRange",
157
+ "intervalMs"
158
+ ]) || !isBundleFilter(input.filter) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs)) invalidQuery();
159
+ const { start, end } = input.timeRange;
160
+ const { intervalMs } = input;
161
+ if (intervalMs === 0 || intervalMs % 36e5 !== 0 || start % 36e5 !== 0 || (end - start) % intervalMs !== 0 || end - start > 7776e6) invalidQuery();
162
+ const points = await model.countEventSeries(input);
163
+ if (!Array.isArray(points) || points.length !== (end - start) / intervalMs || points.some((point, index) => !isRecord(point) || !hasOnlyKeys(point, ["startMs", "events"]) || point.startMs !== start + index * intervalMs || !isTimestamp(point.events))) invalidResult();
164
+ return points;
165
+ },
166
+ async getReleaseActivity(input) {
167
+ if (!isRecord(input) || !hasOnlyKeys(input, ["releases"]) || !Array.isArray(input.releases) || input.releases.length < 1 || input.releases.length > 100 || !input.releases.every(isReleaseReference)) invalidQuery();
165
168
  const result = await model.getReleaseActivity(input);
166
- validateAggregateResult(result);
167
- if (!Array.isArray(result.data)) invalidResult();
169
+ if (!isRecord(result) || !isTimestamp(result.measuredAtMs) || !Array.isArray(result.data) || result.data.some((item) => !isRecord(item) || !isReleaseReference(item.release) || !isRecord(item.metrics) || !isTimestamp(item.metrics.downloads) || !isTimestamp(item.metrics.launches) || !isTimestamp(item.metrics.failedLaunches))) invalidResult();
168
170
  return result;
169
171
  },
170
172
  async getAppUsage(input) {
@@ -18,7 +18,8 @@ const createInsightsApi = (db, now, retention) => ({
18
18
  findLatestEvents: (input) => require_reads.findLatestEvents(db, input),
19
19
  countLatestEvents: (input) => require_reads.countLatestEvents(db, input),
20
20
  countEvents: (input) => require_reads.countEvents(db, input),
21
- getReleaseActivity: (input) => require_reads.getReleaseActivity(db, input, now, retention),
21
+ countEventSeries: (input) => require_reads.countEventSeries(db, input),
22
+ getReleaseActivity: (input) => require_reads.getReleaseActivity(db, input, now),
22
23
  getAppUsage: (input) => require_reads.getAppUsage(db, input, now, retention),
23
24
  /** A release's or a channel's update failures, and their breakdown over a time range. */
24
25
  getUpdateFailures: (input) => require_reads.getUpdateFailures(db, input, now, retention)
@@ -1,5 +1,5 @@
1
1
  import { BundleEventFailure, BundleEventFailureReason, BundleEventFailureStage, BundleEventRow, BundleEventRowBase, DatabaseBundleEventMetadata } from "./eventRow.cjs";
2
- import { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference } from "./modelTypes.cjs";
2
+ import { InsightsBundleEventFilter, InsightsCountEventSeriesInput, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsEventSeriesPoint, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference } from "./modelTypes.cjs";
3
3
  import { DEFAULT_INSIGHTS_RETENTION, InsightsRetention, InsightsSchema, createInsightsSchema, insightsSchema } from "./schema.cjs";
4
4
  import { InsightsFailureBreakdown, InsightsUpdateFailures, InsightsUpdateFailuresInput } from "./reads.cjs";
5
5
  import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InsightsHttpResponse, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.cjs";
@@ -19,6 +19,7 @@ declare const createInsightsApi: (db: HotUpdaterDatabase<InsightsSchema>, now: (
19
19
  findLatestEvents: (input: InsightsFindLatestEventsInput) => Promise<readonly BundleEventRow[]>;
20
20
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
21
21
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
22
+ countEventSeries: (input: InsightsCountEventSeriesInput) => Promise<readonly InsightsEventSeriesPoint[]>;
22
23
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
23
24
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
24
25
  /** A release's or a channel's update failures, and their breakdown over a time range. */
@@ -99,10 +100,6 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
99
100
  movement_install_id: "string";
100
101
  bundle_ref: "string";
101
102
  }, {
102
- readonly recent: {
103
- readonly eq: readonly ["channel", "platform", "day"];
104
- readonly sort: readonly ["received_at_ms"];
105
- };
106
103
  readonly movementsByInstall: {
107
104
  readonly eq: readonly ["movement_install_id"];
108
105
  readonly sort: readonly ["received_at_ms"];
@@ -202,7 +199,7 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
202
199
  readonly eq: readonly ["identity"];
203
200
  readonly sort: readonly ["bucket_start_ms"];
204
201
  };
205
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
202
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
206
203
  readonly insights_overview_daily: import("@hot-updater/plugin-core").AggregateDefinition<{
207
204
  readonly identity: {
208
205
  readonly type: "string";
@@ -230,7 +227,7 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
230
227
  readonly eq: readonly ["identity"];
231
228
  readonly sort: readonly ["bucket_start_ms"];
232
229
  };
233
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
230
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
234
231
  readonly insights_overview_lifetime: import("@hot-updater/plugin-core").AggregateDefinition<{
235
232
  readonly identity: {
236
233
  readonly type: "string";
@@ -384,6 +381,7 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
384
381
  findLatestEvents: (input: InsightsFindLatestEventsInput) => Promise<readonly BundleEventRow[]>;
385
382
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
386
383
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
384
+ countEventSeries: (input: InsightsCountEventSeriesInput) => Promise<readonly InsightsEventSeriesPoint[]>;
387
385
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
388
386
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
389
387
  /** A release's or a channel's update failures, and their breakdown over a time range. */
@@ -392,4 +390,4 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
392
390
  endpoints: PluginEndpoint[];
393
391
  }, {}>;
394
392
  //#endregion
395
- 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, InsightsBadRequestError, 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 InsightsHttpResponse, 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 };
393
+ 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, InsightsBadRequestError, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventSeriesInput, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsEventSeriesPoint, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsHttpResponse, 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 };
@@ -1,5 +1,5 @@
1
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";
2
+ import { InsightsBundleEventFilter, InsightsCountEventSeriesInput, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsEventSeriesPoint, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference } from "./modelTypes.mjs";
3
3
  import { DEFAULT_INSIGHTS_RETENTION, InsightsRetention, InsightsSchema, createInsightsSchema, insightsSchema } from "./schema.mjs";
4
4
  import { InsightsFailureBreakdown, InsightsUpdateFailures, InsightsUpdateFailuresInput } from "./reads.mjs";
5
5
  import { ActiveInstallationWindow, BundleEventFailureInput, CreateBundleEventRequest, CreateBundleEventRequestBase, CursorPage, EventCursorPage, EventHistoryRow, InsightsBundleSelection, InsightsCountMeasurement, InsightsHttpResponse, InstallationHistoryRow, InstallationRow, ReportingOverview } from "./domain.mjs";
@@ -19,6 +19,7 @@ declare const createInsightsApi: (db: HotUpdaterDatabase<InsightsSchema>, now: (
19
19
  findLatestEvents: (input: InsightsFindLatestEventsInput) => Promise<readonly BundleEventRow[]>;
20
20
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
21
21
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
22
+ countEventSeries: (input: InsightsCountEventSeriesInput) => Promise<readonly InsightsEventSeriesPoint[]>;
22
23
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
23
24
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
24
25
  /** A release's or a channel's update failures, and their breakdown over a time range. */
@@ -99,10 +100,6 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
99
100
  movement_install_id: "string";
100
101
  bundle_ref: "string";
101
102
  }, {
102
- readonly recent: {
103
- readonly eq: readonly ["channel", "platform", "day"];
104
- readonly sort: readonly ["received_at_ms"];
105
- };
106
103
  readonly movementsByInstall: {
107
104
  readonly eq: readonly ["movement_install_id"];
108
105
  readonly sort: readonly ["received_at_ms"];
@@ -202,7 +199,7 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
202
199
  readonly eq: readonly ["identity"];
203
200
  readonly sort: readonly ["bucket_start_ms"];
204
201
  };
205
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
202
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
206
203
  readonly insights_overview_daily: import("@hot-updater/plugin-core").AggregateDefinition<{
207
204
  readonly identity: {
208
205
  readonly type: "string";
@@ -230,7 +227,7 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
230
227
  readonly eq: readonly ["identity"];
231
228
  readonly sort: readonly ["bucket_start_ms"];
232
229
  };
233
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
230
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
234
231
  readonly insights_overview_lifetime: import("@hot-updater/plugin-core").AggregateDefinition<{
235
232
  readonly identity: {
236
233
  readonly type: "string";
@@ -384,6 +381,7 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
384
381
  findLatestEvents: (input: InsightsFindLatestEventsInput) => Promise<readonly BundleEventRow[]>;
385
382
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
386
383
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
384
+ countEventSeries: (input: InsightsCountEventSeriesInput) => Promise<readonly InsightsEventSeriesPoint[]>;
387
385
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
388
386
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
389
387
  /** A release's or a channel's update failures, and their breakdown over a time range. */
@@ -392,4 +390,4 @@ declare const insights: (options?: InsightsOptions) => import("@hot-updater/plug
392
390
  endpoints: PluginEndpoint[];
393
391
  }, {}>;
394
392
  //#endregion
395
- 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, InsightsBadRequestError, 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 InsightsHttpResponse, 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 };
393
+ 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, InsightsBadRequestError, type InsightsBundleEventFilter, type InsightsBundleSelection, type InsightsCountEventSeriesInput, type InsightsCountEventsInput, type InsightsCountLatestEventsInput, type InsightsCountMeasurement, type InsightsCoverage, type InsightsEventCursor, type InsightsEventFilter, type InsightsEventPageInput, type InsightsEventSeriesPoint, type InsightsFailureBreakdown, type InsightsFindLatestEventsInput, type InsightsGetAppUsageInput, type InsightsGetAppUsageResult, type InsightsGetReleaseActivityInput, type InsightsGetReleaseActivityResult, type InsightsHttpResponse, 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 };
@@ -4,7 +4,7 @@ import { InsightsBadRequestError } from "./errors.mjs";
4
4
  import { createInsightsProvider } from "./provider.mjs";
5
5
  import { DEFAULT_INSIGHTS_RETENTION, createInsightsSchema, insightsSchema } from "./schema.mjs";
6
6
  import { insightsIdentity, recordEvent } from "./recordEvent.mjs";
7
- import { countEvents, countLatestEvents, findLatestEvents, getAppUsage, getReleaseActivity, getUpdateFailures, listEvents } from "./reads.mjs";
7
+ import { countEventSeries, countEvents, countLatestEvents, findLatestEvents, getAppUsage, getReleaseActivity, getUpdateFailures, listEvents } from "./reads.mjs";
8
8
  import { INSIGHTS_ROUTES, createInsightsRouteHandlers } from "./routes.mjs";
9
9
  import { DatabaseAdapterInputError, HotUpdaterConfigError, definePlugin, isDatabaseBusyError } from "@hot-updater/plugin-core";
10
10
  //#region src/server/index.ts
@@ -17,7 +17,8 @@ const createInsightsApi = (db, now, retention) => ({
17
17
  findLatestEvents: (input) => findLatestEvents(db, input),
18
18
  countLatestEvents: (input) => countLatestEvents(db, input),
19
19
  countEvents: (input) => countEvents(db, input),
20
- getReleaseActivity: (input) => getReleaseActivity(db, input, now, retention),
20
+ countEventSeries: (input) => countEventSeries(db, input),
21
+ getReleaseActivity: (input) => getReleaseActivity(db, input, now),
21
22
  getAppUsage: (input) => getAppUsage(db, input, now, retention),
22
23
  /** A release's or a channel's update failures, and their breakdown over a time range. */
23
24
  getUpdateFailures: (input) => getUpdateFailures(db, input, now, retention)
@@ -10,6 +10,7 @@ const createInsightsModel = (api) => require_contract.createValidatedInsightsMod
10
10
  findLatestEvents: api.findLatestEvents,
11
11
  countLatestEvents: api.countLatestEvents,
12
12
  countEvents: api.countEvents,
13
+ countEventSeries: api.countEventSeries,
13
14
  getReleaseActivity: api.getReleaseActivity,
14
15
  getAppUsage: api.getAppUsage
15
16
  });
@@ -10,6 +10,7 @@ const createInsightsModel = (api) => createValidatedInsightsModel({
10
10
  findLatestEvents: api.findLatestEvents,
11
11
  countLatestEvents: api.countLatestEvents,
12
12
  countEvents: api.countEvents,
13
+ countEventSeries: api.countEventSeries,
13
14
  getReleaseActivity: api.getReleaseActivity,
14
15
  getAppUsage: api.getAppUsage
15
16
  });
@@ -59,6 +59,19 @@ interface InsightsCountEventsInput {
59
59
  readonly sinceMs: number;
60
60
  readonly beforeReceivedAtMs: number;
61
61
  }
62
+ /** One bundle filter's stored events in each interval of a time range. */
63
+ interface InsightsCountEventSeriesInput {
64
+ readonly filter: InsightsBundleEventFilter;
65
+ /** Whole UTC hours, at most 90 days, that a whole number of intervals fill. */
66
+ readonly timeRange: InsightsTimeRange;
67
+ /** Whole hours each point spans, from `timeRange.start`. */
68
+ readonly intervalMs: number;
69
+ }
70
+ /** Stored events in [startMs, startMs + intervalMs). */
71
+ interface InsightsEventSeriesPoint {
72
+ readonly startMs: number;
73
+ readonly events: number;
74
+ }
62
75
  interface ReleaseReference {
63
76
  readonly releaseId: string;
64
77
  readonly platform: "ios" | "android";
@@ -77,33 +90,22 @@ type InsightsCoverage = {
77
90
  readonly kind: "partial";
78
91
  readonly sinceMs: number | null;
79
92
  };
93
+ /**
94
+ * A release's counts since its first report, from its lifetime counters,
95
+ * which are kept.
96
+ */
80
97
  interface ReleaseActivityMetrics {
81
98
  readonly downloads: number;
82
99
  readonly launches: number;
83
100
  readonly failedLaunches: number;
84
- /** Omitted for lifetime-only bundle-row reads. */
85
- readonly uniqueUsers?: number;
86
- /** Daily points, present only for period reads. */
87
- readonly series?: readonly {
88
- readonly startMs: number;
89
- readonly launches: number;
90
- readonly failedLaunches: number;
91
- }[];
92
101
  }
93
- type InsightsGetReleaseActivityInput = {
102
+ interface InsightsGetReleaseActivityInput {
103
+ /** 1 to 100 releases. */
94
104
  readonly releases: readonly ReleaseReference[];
95
- readonly timeRange?: InsightsTimeRange;
96
- readonly scope?: never;
97
- } | {
98
- readonly scope: InsightsScope;
99
- readonly timeRange: InsightsTimeRange;
100
- readonly releases?: never;
101
- };
105
+ }
102
106
  interface InsightsGetReleaseActivityResult {
103
- readonly coverage: InsightsCoverage;
104
107
  readonly data: readonly {
105
- readonly release?: ReleaseReference;
106
- readonly scope?: InsightsScope;
108
+ readonly release: ReleaseReference;
107
109
  readonly metrics: ReleaseActivityMetrics;
108
110
  }[];
109
111
  readonly measuredAtMs: number;
@@ -167,10 +169,15 @@ interface InsightsModel {
167
169
  countLatestEvents(input: InsightsCountLatestEventsInput): Promise<number>;
168
170
  /** Count accepted reports in [sinceMs, beforeReceivedAtMs), across all pages. */
169
171
  countEvents(input: InsightsCountEventsInput): Promise<number>;
170
- /** Read maintained release/scope summaries without scanning raw events. */
172
+ /**
173
+ * A bundle filter's stored events in each interval, every interval
174
+ * present in order, from the hourly counts that `countEvents` reads.
175
+ */
176
+ countEventSeries(input: InsightsCountEventSeriesInput): Promise<readonly InsightsEventSeriesPoint[]>;
177
+ /** Releases' lifetime counters, without scanning raw events. */
171
178
  getReleaseActivity(input: InsightsGetReleaseActivityInput): Promise<InsightsGetReleaseActivityResult>;
172
179
  /** Read maintained App usage summaries and latest-report distribution. */
173
180
  getAppUsage(input: InsightsGetAppUsageInput): Promise<InsightsGetAppUsageResult>;
174
181
  }
175
182
  //#endregion
176
- export { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
183
+ export { InsightsBundleEventFilter, InsightsCountEventSeriesInput, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsEventSeriesPoint, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
@@ -59,6 +59,19 @@ interface InsightsCountEventsInput {
59
59
  readonly sinceMs: number;
60
60
  readonly beforeReceivedAtMs: number;
61
61
  }
62
+ /** One bundle filter's stored events in each interval of a time range. */
63
+ interface InsightsCountEventSeriesInput {
64
+ readonly filter: InsightsBundleEventFilter;
65
+ /** Whole UTC hours, at most 90 days, that a whole number of intervals fill. */
66
+ readonly timeRange: InsightsTimeRange;
67
+ /** Whole hours each point spans, from `timeRange.start`. */
68
+ readonly intervalMs: number;
69
+ }
70
+ /** Stored events in [startMs, startMs + intervalMs). */
71
+ interface InsightsEventSeriesPoint {
72
+ readonly startMs: number;
73
+ readonly events: number;
74
+ }
62
75
  interface ReleaseReference {
63
76
  readonly releaseId: string;
64
77
  readonly platform: "ios" | "android";
@@ -77,33 +90,22 @@ type InsightsCoverage = {
77
90
  readonly kind: "partial";
78
91
  readonly sinceMs: number | null;
79
92
  };
93
+ /**
94
+ * A release's counts since its first report, from its lifetime counters,
95
+ * which are kept.
96
+ */
80
97
  interface ReleaseActivityMetrics {
81
98
  readonly downloads: number;
82
99
  readonly launches: number;
83
100
  readonly failedLaunches: number;
84
- /** Omitted for lifetime-only bundle-row reads. */
85
- readonly uniqueUsers?: number;
86
- /** Daily points, present only for period reads. */
87
- readonly series?: readonly {
88
- readonly startMs: number;
89
- readonly launches: number;
90
- readonly failedLaunches: number;
91
- }[];
92
101
  }
93
- type InsightsGetReleaseActivityInput = {
102
+ interface InsightsGetReleaseActivityInput {
103
+ /** 1 to 100 releases. */
94
104
  readonly releases: readonly ReleaseReference[];
95
- readonly timeRange?: InsightsTimeRange;
96
- readonly scope?: never;
97
- } | {
98
- readonly scope: InsightsScope;
99
- readonly timeRange: InsightsTimeRange;
100
- readonly releases?: never;
101
- };
105
+ }
102
106
  interface InsightsGetReleaseActivityResult {
103
- readonly coverage: InsightsCoverage;
104
107
  readonly data: readonly {
105
- readonly release?: ReleaseReference;
106
- readonly scope?: InsightsScope;
108
+ readonly release: ReleaseReference;
107
109
  readonly metrics: ReleaseActivityMetrics;
108
110
  }[];
109
111
  readonly measuredAtMs: number;
@@ -167,10 +169,15 @@ interface InsightsModel {
167
169
  countLatestEvents(input: InsightsCountLatestEventsInput): Promise<number>;
168
170
  /** Count accepted reports in [sinceMs, beforeReceivedAtMs), across all pages. */
169
171
  countEvents(input: InsightsCountEventsInput): Promise<number>;
170
- /** Read maintained release/scope summaries without scanning raw events. */
172
+ /**
173
+ * A bundle filter's stored events in each interval, every interval
174
+ * present in order, from the hourly counts that `countEvents` reads.
175
+ */
176
+ countEventSeries(input: InsightsCountEventSeriesInput): Promise<readonly InsightsEventSeriesPoint[]>;
177
+ /** Releases' lifetime counters, without scanning raw events. */
171
178
  getReleaseActivity(input: InsightsGetReleaseActivityInput): Promise<InsightsGetReleaseActivityResult>;
172
179
  /** Read maintained App usage summaries and latest-report distribution. */
173
180
  getAppUsage(input: InsightsGetAppUsageInput): Promise<InsightsGetAppUsageResult>;
174
181
  }
175
182
  //#endregion
176
- export { InsightsBundleEventFilter, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
183
+ export { InsightsBundleEventFilter, InsightsCountEventSeriesInput, InsightsCountEventsInput, InsightsCountLatestEventsInput, InsightsCoverage, InsightsEventCursor, InsightsEventFilter, InsightsEventSeriesPoint, InsightsFindLatestEventsInput, InsightsGetAppUsageInput, InsightsGetAppUsageResult, InsightsGetReleaseActivityInput, InsightsGetReleaseActivityResult, InsightsListEventsInput, InsightsModel, InsightsRecordEventInput, InsightsScope, InsightsTimeRange, ReleaseActivityMetrics, ReleaseReference };
@@ -33,12 +33,14 @@ const insightsOverviewDeltas = (event) => {
33
33
  downloads: (previous?.downloads ?? 0) + values.downloads,
34
34
  launches: (previous?.launches ?? 0) + values.launches,
35
35
  failedLaunches: (previous?.failedLaunches ?? 0) + values.failedLaunches,
36
- launchIdentity: previous?.launchIdentity ?? values.launchIdentity,
37
36
  activityIdentity: previous?.activityIdentity ?? values.activityIdentity
38
37
  });
39
38
  };
40
39
  const metric = (releaseId, values) => {
41
- const launchIdentity = values.launches > 0 ? event.install_id : void 0;
40
+ const windowed = {
41
+ ...values,
42
+ launches: 0
43
+ };
42
44
  if (releaseId !== null) for (const [periodKind, start] of [["lifetime", 0], ["hour", bucketStartMs]]) append({
43
45
  scopeKind: "release",
44
46
  releaseKind: "specific",
@@ -49,10 +51,7 @@ const insightsOverviewDeltas = (event) => {
49
51
  appVersion: "",
50
52
  periodKind,
51
53
  bucketStartMs: start
52
- }, {
53
- ...values,
54
- ...periodKind === "hour" ? { launchIdentity } : {}
55
- });
54
+ }, periodKind === "lifetime" ? values : windowed);
56
55
  append({
57
56
  scopeKind: "channel",
58
57
  releaseKind: "all",
@@ -63,10 +62,7 @@ const insightsOverviewDeltas = (event) => {
63
62
  appVersion: "",
64
63
  periodKind: "hour",
65
64
  bucketStartMs
66
- }, {
67
- ...values,
68
- launchIdentity
69
- });
65
+ }, windowed);
70
66
  };
71
67
  if (event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
72
68
  downloads: 1,
@@ -33,12 +33,14 @@ const insightsOverviewDeltas = (event) => {
33
33
  downloads: (previous?.downloads ?? 0) + values.downloads,
34
34
  launches: (previous?.launches ?? 0) + values.launches,
35
35
  failedLaunches: (previous?.failedLaunches ?? 0) + values.failedLaunches,
36
- launchIdentity: previous?.launchIdentity ?? values.launchIdentity,
37
36
  activityIdentity: previous?.activityIdentity ?? values.activityIdentity
38
37
  });
39
38
  };
40
39
  const metric = (releaseId, values) => {
41
- const launchIdentity = values.launches > 0 ? event.install_id : void 0;
40
+ const windowed = {
41
+ ...values,
42
+ launches: 0
43
+ };
42
44
  if (releaseId !== null) for (const [periodKind, start] of [["lifetime", 0], ["hour", bucketStartMs]]) append({
43
45
  scopeKind: "release",
44
46
  releaseKind: "specific",
@@ -49,10 +51,7 @@ const insightsOverviewDeltas = (event) => {
49
51
  appVersion: "",
50
52
  periodKind,
51
53
  bucketStartMs: start
52
- }, {
53
- ...values,
54
- ...periodKind === "hour" ? { launchIdentity } : {}
55
- });
54
+ }, periodKind === "lifetime" ? values : windowed);
56
55
  append({
57
56
  scopeKind: "channel",
58
57
  releaseKind: "all",
@@ -63,10 +62,7 @@ const insightsOverviewDeltas = (event) => {
63
62
  appVersion: "",
64
63
  periodKind: "hour",
65
64
  bucketStartMs
66
- }, {
67
- ...values,
68
- launchIdentity
69
- });
65
+ }, windowed);
70
66
  };
71
67
  if (event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
72
68
  downloads: 1,
@@ -171,6 +171,30 @@ const countEvents = async (db, input) => {
171
171
  }));
172
172
  return await countRaw(db, filter, sinceMs, start) + hours.reduce((sum, row) => sum + row.events, 0) + await countRaw(db, filter, end, beforeReceivedAtMs);
173
173
  };
174
+ /**
175
+ * A bundle filter's stored events in each interval of whole hours: one read
176
+ * of its hourly counts, every interval present.
177
+ */
178
+ const countEventSeries = async (db, input) => {
179
+ const { filter, intervalMs } = input;
180
+ const { start, end } = input.timeRange;
181
+ const events = Array.from({ length: (end - start) / intervalMs }, () => 0);
182
+ const hours = await drain((page) => db.findAggregates("insights_outcomes", {
183
+ index: "byRef",
184
+ where: scopeOf(filter),
185
+ range: {
186
+ gte: start,
187
+ lt: end
188
+ },
189
+ limit: PAGE,
190
+ ...page
191
+ }));
192
+ for (const row of hours) events[Math.floor((row.bucket_start_ms - start) / intervalMs)] += row.events;
193
+ return events.map((count, index) => ({
194
+ startMs: start + index * intervalMs,
195
+ events: count
196
+ }));
197
+ };
174
198
  /** A bundle filter's distinct (field, value, type) predicates. */
175
199
  const latestPredicates = (bundle) => {
176
200
  if (bundle === void 0) return void 0;
@@ -242,7 +266,7 @@ const retained = (now, { rawDays, dailyDays }) => ({
242
266
  * Whether the rows a window reads still hold all of it; an older start is
243
267
  * partial from `oldest`, the first bucket its rows keep.
244
268
  */
245
- const coverageOf = (start, oldest) => start === void 0 || start >= oldest ? {
269
+ const coverageOf = (start, oldest) => start >= oldest ? {
246
270
  kind: "complete",
247
271
  sinceMs: 0
248
272
  } : {
@@ -345,32 +369,6 @@ const lifetimeMetrics = async (db, release) => {
345
369
  failedLaunches: lifetime?.failed_launches ?? 0
346
370
  };
347
371
  };
348
- /** Counters, unique users, and a per-UTC-day series over a window. */
349
- const rangedMetrics = async (db, parts, users, range, days, hour) => {
350
- const [counters, sketches] = await Promise.all([counterRows(db, parts, range, days, hour), sketchRows(db, users.parts, range, days, hour)]);
351
- const series = /* @__PURE__ */ new Map();
352
- for (const row of counters) {
353
- const day = dayFloor(row.bucket_start_ms);
354
- const point = series.get(day) ?? {
355
- launches: 0,
356
- failedLaunches: 0
357
- };
358
- point.launches += row.launches;
359
- point.failedLaunches += row.failed_launches;
360
- series.set(day, point);
361
- }
362
- const total = (field) => counters.reduce((sum, row) => sum + row[field], 0);
363
- return {
364
- downloads: total("downloads"),
365
- launches: total("launches"),
366
- failedLaunches: total("failed_launches"),
367
- uniqueUsers: (0, _hot_updater_plugin_core.countDistinct)((0, _hot_updater_plugin_core.mergeDistinct)(sketches.map((row) => row[users.field]))),
368
- series: [...series].sort(([left], [right]) => left - right).map(([startMs, point]) => ({
369
- startMs,
370
- ...point
371
- }))
372
- };
373
- };
374
372
  /** A channel and platform's usage rows, of every app version or of one. */
375
373
  const usageParts = (channel, platform, appVersion) => ({
376
374
  scopeKind: "usage",
@@ -381,35 +379,15 @@ const usageParts = (channel, platform, appVersion) => ({
381
379
  appVersionKind: appVersion === void 0 ? "all" : "specific",
382
380
  appVersion: appVersion ?? ""
383
381
  });
384
- const getReleaseActivity = async (db, input, now, retention) => {
385
- const at = now();
386
- const kept = retained(at, retention);
387
- const data = input.scope !== void 0 ? [{
388
- scope: input.scope,
389
- metrics: await rangedMetrics(db, {
390
- scopeKind: "channel",
391
- releaseKind: "all",
392
- releaseId: "",
393
- channel: input.scope.channel,
394
- platform: input.scope.platform,
395
- appVersionKind: "all",
396
- appVersion: ""
397
- }, {
398
- parts: usageParts(input.scope.channel, input.scope.platform),
399
- field: "activity_users"
400
- }, input.timeRange, true, kept.hour)
401
- }] : await Promise.all(input.releases.map(async (release) => ({
402
- release,
403
- metrics: input.timeRange === void 0 ? await lifetimeMetrics(db, release) : await rangedMetrics(db, releaseParts(release), {
404
- parts: releaseParts(release),
405
- field: "launch_users"
406
- }, input.timeRange, false, kept.hour)
407
- })));
408
- const oldest = kept[input.scope === void 0 ? "hour" : "day"];
382
+ /** Each release's lifetime counters, which are kept: one logical row apiece. */
383
+ const getReleaseActivity = async (db, input, now) => {
384
+ const measuredAtMs = now();
409
385
  return {
410
- coverage: coverageOf(input.timeRange?.start, oldest),
411
- data,
412
- measuredAtMs: at
386
+ data: await Promise.all(input.releases.map(async (release) => ({
387
+ release,
388
+ metrics: await lifetimeMetrics(db, release)
389
+ }))),
390
+ measuredAtMs
413
391
  };
414
392
  };
415
393
  /**
@@ -671,6 +649,7 @@ const getUpdateFailures = async (db, input, now, retention) => {
671
649
  };
672
650
  //#endregion
673
651
  exports.UPDATE_FAILURES_RANGE_MS = UPDATE_FAILURES_RANGE_MS;
652
+ exports.countEventSeries = countEventSeries;
674
653
  exports.countEvents = countEvents;
675
654
  exports.countLatestEvents = countLatestEvents;
676
655
  exports.findLatestEvents = findLatestEvents;
@@ -171,6 +171,30 @@ const countEvents = async (db, input) => {
171
171
  }));
172
172
  return await countRaw(db, filter, sinceMs, start) + hours.reduce((sum, row) => sum + row.events, 0) + await countRaw(db, filter, end, beforeReceivedAtMs);
173
173
  };
174
+ /**
175
+ * A bundle filter's stored events in each interval of whole hours: one read
176
+ * of its hourly counts, every interval present.
177
+ */
178
+ const countEventSeries = async (db, input) => {
179
+ const { filter, intervalMs } = input;
180
+ const { start, end } = input.timeRange;
181
+ const events = Array.from({ length: (end - start) / intervalMs }, () => 0);
182
+ const hours = await drain((page) => db.findAggregates("insights_outcomes", {
183
+ index: "byRef",
184
+ where: scopeOf(filter),
185
+ range: {
186
+ gte: start,
187
+ lt: end
188
+ },
189
+ limit: PAGE,
190
+ ...page
191
+ }));
192
+ for (const row of hours) events[Math.floor((row.bucket_start_ms - start) / intervalMs)] += row.events;
193
+ return events.map((count, index) => ({
194
+ startMs: start + index * intervalMs,
195
+ events: count
196
+ }));
197
+ };
174
198
  /** A bundle filter's distinct (field, value, type) predicates. */
175
199
  const latestPredicates = (bundle) => {
176
200
  if (bundle === void 0) return void 0;
@@ -242,7 +266,7 @@ const retained = (now, { rawDays, dailyDays }) => ({
242
266
  * Whether the rows a window reads still hold all of it; an older start is
243
267
  * partial from `oldest`, the first bucket its rows keep.
244
268
  */
245
- const coverageOf = (start, oldest) => start === void 0 || start >= oldest ? {
269
+ const coverageOf = (start, oldest) => start >= oldest ? {
246
270
  kind: "complete",
247
271
  sinceMs: 0
248
272
  } : {
@@ -345,32 +369,6 @@ const lifetimeMetrics = async (db, release) => {
345
369
  failedLaunches: lifetime?.failed_launches ?? 0
346
370
  };
347
371
  };
348
- /** Counters, unique users, and a per-UTC-day series over a window. */
349
- const rangedMetrics = async (db, parts, users, range, days, hour) => {
350
- const [counters, sketches] = await Promise.all([counterRows(db, parts, range, days, hour), sketchRows(db, users.parts, range, days, hour)]);
351
- const series = /* @__PURE__ */ new Map();
352
- for (const row of counters) {
353
- const day = dayFloor(row.bucket_start_ms);
354
- const point = series.get(day) ?? {
355
- launches: 0,
356
- failedLaunches: 0
357
- };
358
- point.launches += row.launches;
359
- point.failedLaunches += row.failed_launches;
360
- series.set(day, point);
361
- }
362
- const total = (field) => counters.reduce((sum, row) => sum + row[field], 0);
363
- return {
364
- downloads: total("downloads"),
365
- launches: total("launches"),
366
- failedLaunches: total("failed_launches"),
367
- uniqueUsers: countDistinct(mergeDistinct(sketches.map((row) => row[users.field]))),
368
- series: [...series].sort(([left], [right]) => left - right).map(([startMs, point]) => ({
369
- startMs,
370
- ...point
371
- }))
372
- };
373
- };
374
372
  /** A channel and platform's usage rows, of every app version or of one. */
375
373
  const usageParts = (channel, platform, appVersion) => ({
376
374
  scopeKind: "usage",
@@ -381,35 +379,15 @@ const usageParts = (channel, platform, appVersion) => ({
381
379
  appVersionKind: appVersion === void 0 ? "all" : "specific",
382
380
  appVersion: appVersion ?? ""
383
381
  });
384
- const getReleaseActivity = async (db, input, now, retention) => {
385
- const at = now();
386
- const kept = retained(at, retention);
387
- const data = input.scope !== void 0 ? [{
388
- scope: input.scope,
389
- metrics: await rangedMetrics(db, {
390
- scopeKind: "channel",
391
- releaseKind: "all",
392
- releaseId: "",
393
- channel: input.scope.channel,
394
- platform: input.scope.platform,
395
- appVersionKind: "all",
396
- appVersion: ""
397
- }, {
398
- parts: usageParts(input.scope.channel, input.scope.platform),
399
- field: "activity_users"
400
- }, input.timeRange, true, kept.hour)
401
- }] : await Promise.all(input.releases.map(async (release) => ({
402
- release,
403
- metrics: input.timeRange === void 0 ? await lifetimeMetrics(db, release) : await rangedMetrics(db, releaseParts(release), {
404
- parts: releaseParts(release),
405
- field: "launch_users"
406
- }, input.timeRange, false, kept.hour)
407
- })));
408
- const oldest = kept[input.scope === void 0 ? "hour" : "day"];
382
+ /** Each release's lifetime counters, which are kept: one logical row apiece. */
383
+ const getReleaseActivity = async (db, input, now) => {
384
+ const measuredAtMs = now();
409
385
  return {
410
- coverage: coverageOf(input.timeRange?.start, oldest),
411
- data,
412
- measuredAtMs: at
386
+ data: await Promise.all(input.releases.map(async (release) => ({
387
+ release,
388
+ metrics: await lifetimeMetrics(db, release)
389
+ }))),
390
+ measuredAtMs
413
391
  };
414
392
  };
415
393
  /**
@@ -670,4 +648,4 @@ const getUpdateFailures = async (db, input, now, retention) => {
670
648
  };
671
649
  };
672
650
  //#endregion
673
- export { UPDATE_FAILURES_RANGE_MS, countEvents, countLatestEvents, findLatestEvents, getAppUsage, getReleaseActivity, getUpdateFailures, listEvents };
651
+ export { UPDATE_FAILURES_RANGE_MS, countEventSeries, countEvents, countLatestEvents, findLatestEvents, getAppUsage, getReleaseActivity, getUpdateFailures, listEvents };
@@ -89,11 +89,10 @@ const patchCounters = (event) => event.type !== "UPDATE_DOWNLOADED" ? {} : {
89
89
  };
90
90
  /**
91
91
  * Counters and sketches for one event: release, channel, and usage rows, with
92
- * day rollups for channel and usage. Usage rows are written for the event's
93
- * platform only, since a read for every platform merges the ios and android
94
- * sketches; a channel's active installations come from its usage rows, so
95
- * channel rows keep no sketch of their own. A download's patch counters go
96
- * in the rows that count it.
92
+ * day rollups for channel and usage. Only usage rows keep a sketch, of active
93
+ * installations, and only for the event's platform, since a read for every
94
+ * platform merges the ios and android sketches. A download's patch counters
95
+ * go in the rows that count it.
97
96
  */
98
97
  const countEvent = (tx, event) => {
99
98
  const shardBy = event.install_id;
@@ -119,11 +118,7 @@ const countEvent = (tx, event) => {
119
118
  ...delta.downloads === 0 ? {} : patchCounters(event)
120
119
  };
121
120
  if (Object.keys(counters).length > 0) tx.aggregate(models.counters, key, counters, { shardBy });
122
- const sketches = {
123
- ...delta.launchIdentity === void 0 || parts.scopeKind === "channel" ? {} : { launch_users: (0, _hot_updater_plugin_core.addDistinct)(null, delta.launchIdentity) },
124
- ...delta.activityIdentity === void 0 ? {} : { activity_users: (0, _hot_updater_plugin_core.addDistinct)(null, delta.activityIdentity) }
125
- };
126
- if (models.sketches !== void 0 && Object.keys(sketches).length > 0) tx.aggregate(models.sketches, key, sketches, { shardBy });
121
+ if (models.sketches !== void 0 && delta.activityIdentity !== void 0) tx.aggregate(models.sketches, key, { activity_users: (0, _hot_updater_plugin_core.addDistinct)(null, delta.activityIdentity) }, { shardBy });
127
122
  }
128
123
  }
129
124
  };
@@ -89,11 +89,10 @@ const patchCounters = (event) => event.type !== "UPDATE_DOWNLOADED" ? {} : {
89
89
  };
90
90
  /**
91
91
  * Counters and sketches for one event: release, channel, and usage rows, with
92
- * day rollups for channel and usage. Usage rows are written for the event's
93
- * platform only, since a read for every platform merges the ios and android
94
- * sketches; a channel's active installations come from its usage rows, so
95
- * channel rows keep no sketch of their own. A download's patch counters go
96
- * in the rows that count it.
92
+ * day rollups for channel and usage. Only usage rows keep a sketch, of active
93
+ * installations, and only for the event's platform, since a read for every
94
+ * platform merges the ios and android sketches. A download's patch counters
95
+ * go in the rows that count it.
97
96
  */
98
97
  const countEvent = (tx, event) => {
99
98
  const shardBy = event.install_id;
@@ -119,11 +118,7 @@ const countEvent = (tx, event) => {
119
118
  ...delta.downloads === 0 ? {} : patchCounters(event)
120
119
  };
121
120
  if (Object.keys(counters).length > 0) tx.aggregate(models.counters, key, counters, { shardBy });
122
- const sketches = {
123
- ...delta.launchIdentity === void 0 || parts.scopeKind === "channel" ? {} : { launch_users: addDistinct(null, delta.launchIdentity) },
124
- ...delta.activityIdentity === void 0 ? {} : { activity_users: addDistinct(null, delta.activityIdentity) }
125
- };
126
- if (models.sketches !== void 0 && Object.keys(sketches).length > 0) tx.aggregate(models.sketches, key, sketches, { shardBy });
121
+ if (models.sketches !== void 0 && delta.activityIdentity !== void 0) tx.aggregate(models.sketches, key, { activity_users: addDistinct(null, delta.activityIdentity) }, { shardBy });
127
122
  }
128
123
  }
129
124
  };
@@ -92,14 +92,6 @@ const bundleEvents = (days) => (0, _hot_updater_plugin_core.defineTable)({
92
92
  }
93
93
  },
94
94
  indexes: {
95
- recent: {
96
- eq: [
97
- "channel",
98
- "platform",
99
- "day"
100
- ],
101
- sort: ["received_at_ms"]
102
- },
103
95
  movementsByInstall: {
104
96
  eq: ["movement_install_id"],
105
97
  sort: ["received_at_ms"]
@@ -216,10 +208,11 @@ const window = {
216
208
  };
217
209
  /**
218
210
  * Counters of one period kind: hourly, daily, or lifetime at bucket 0. A
219
- * release or channel row counts downloads, launches, and failed launches,
220
- * and downloads a patch delivered and those that fell back from one. A
221
- * release's lifetime row counts its failed updates (`failed_updates`), since
222
- * no breakdown row outlives the raw period; windowed reads sum
211
+ * release or channel row counts downloads and failed launches, and downloads
212
+ * a patch delivered and those that fell back from one. Only a release's
213
+ * lifetime row counts launches, which the bundle list reads and no windowed
214
+ * read sums. It also counts the release's failed updates (`failed_updates`),
215
+ * since no breakdown row outlives the raw period; windowed reads sum
223
216
  * `insights_failures` instead.
224
217
  */
225
218
  const counters = (retention) => (0, _hot_updater_plugin_core.defineAggregate)(identityFields, {
@@ -238,17 +231,13 @@ const counters = (retention) => (0, _hot_updater_plugin_core.defineAggregate)(id
238
231
  ...retention === void 0 ? {} : { retention }
239
232
  });
240
233
  /**
241
- * Unique-installation sketches of one period kind. Rows of a failure
242
- * identity (`failure` and `check` scopes) keep only `failed_users`, so a
243
- * release's launch rows never grow by a failure's registers.
234
+ * Unique-installation sketches of one period kind: a usage row's active
235
+ * installations (`activity_users`), and a `failure` or `check` row's
236
+ * installations whose update or update check failed (`failed_users`).
244
237
  */
245
238
  const sketches = (retention) => (0, _hot_updater_plugin_core.defineAggregate)(identityFields, {
246
239
  key: ["identity", "bucket_start_ms"],
247
- distinct: [
248
- "launch_users",
249
- "activity_users",
250
- "failed_users"
251
- ],
240
+ distinct: ["activity_users", "failed_users"],
252
241
  shards: SKETCH_SHARDS,
253
242
  batched: true,
254
243
  indexes: { window },
@@ -71,10 +71,6 @@ declare const createInsightsSchema: ({ rawDays, dailyDays }?: InsightsRetention)
71
71
  movement_install_id: "string";
72
72
  bundle_ref: "string";
73
73
  }, {
74
- readonly recent: {
75
- readonly eq: readonly ["channel", "platform", "day"];
76
- readonly sort: readonly ["received_at_ms"];
77
- };
78
74
  readonly movementsByInstall: {
79
75
  readonly eq: readonly ["movement_install_id"];
80
76
  readonly sort: readonly ["received_at_ms"];
@@ -176,7 +172,7 @@ declare const createInsightsSchema: ({ rawDays, dailyDays }?: InsightsRetention)
176
172
  readonly eq: readonly ["identity"];
177
173
  readonly sort: readonly ["bucket_start_ms"];
178
174
  };
179
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
175
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
180
176
  /** Daily counters and sketches: the daily period. */
181
177
  readonly insights_overview_daily: import("@hot-updater/plugin-core").AggregateDefinition<{
182
178
  readonly identity: {
@@ -205,7 +201,7 @@ declare const createInsightsSchema: ({ rawDays, dailyDays }?: InsightsRetention)
205
201
  readonly eq: readonly ["identity"];
206
202
  readonly sort: readonly ["bucket_start_ms"];
207
203
  };
208
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
204
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
209
205
  /** A release's counters since its first event, and its failed installations: kept. */
210
206
  readonly insights_overview_lifetime: import("@hot-updater/plugin-core").AggregateDefinition<{
211
207
  readonly identity: {
@@ -410,10 +406,6 @@ declare const insightsSchema: {
410
406
  movement_install_id: "string";
411
407
  bundle_ref: "string";
412
408
  }, {
413
- readonly recent: {
414
- readonly eq: readonly ["channel", "platform", "day"];
415
- readonly sort: readonly ["received_at_ms"];
416
- };
417
409
  readonly movementsByInstall: {
418
410
  readonly eq: readonly ["movement_install_id"];
419
411
  readonly sort: readonly ["received_at_ms"];
@@ -515,7 +507,7 @@ declare const insightsSchema: {
515
507
  readonly eq: readonly ["identity"];
516
508
  readonly sort: readonly ["bucket_start_ms"];
517
509
  };
518
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
510
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
519
511
  /** Daily counters and sketches: the daily period. */
520
512
  readonly insights_overview_daily: import("@hot-updater/plugin-core").AggregateDefinition<{
521
513
  readonly identity: {
@@ -544,7 +536,7 @@ declare const insightsSchema: {
544
536
  readonly eq: readonly ["identity"];
545
537
  readonly sort: readonly ["bucket_start_ms"];
546
538
  };
547
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
539
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
548
540
  /** A release's counters since its first event, and its failed installations: kept. */
549
541
  readonly insights_overview_lifetime: import("@hot-updater/plugin-core").AggregateDefinition<{
550
542
  readonly identity: {
@@ -71,10 +71,6 @@ declare const createInsightsSchema: ({ rawDays, dailyDays }?: InsightsRetention)
71
71
  movement_install_id: "string";
72
72
  bundle_ref: "string";
73
73
  }, {
74
- readonly recent: {
75
- readonly eq: readonly ["channel", "platform", "day"];
76
- readonly sort: readonly ["received_at_ms"];
77
- };
78
74
  readonly movementsByInstall: {
79
75
  readonly eq: readonly ["movement_install_id"];
80
76
  readonly sort: readonly ["received_at_ms"];
@@ -176,7 +172,7 @@ declare const createInsightsSchema: ({ rawDays, dailyDays }?: InsightsRetention)
176
172
  readonly eq: readonly ["identity"];
177
173
  readonly sort: readonly ["bucket_start_ms"];
178
174
  };
179
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
175
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
180
176
  /** Daily counters and sketches: the daily period. */
181
177
  readonly insights_overview_daily: import("@hot-updater/plugin-core").AggregateDefinition<{
182
178
  readonly identity: {
@@ -205,7 +201,7 @@ declare const createInsightsSchema: ({ rawDays, dailyDays }?: InsightsRetention)
205
201
  readonly eq: readonly ["identity"];
206
202
  readonly sort: readonly ["bucket_start_ms"];
207
203
  };
208
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
204
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
209
205
  /** A release's counters since its first event, and its failed installations: kept. */
210
206
  readonly insights_overview_lifetime: import("@hot-updater/plugin-core").AggregateDefinition<{
211
207
  readonly identity: {
@@ -410,10 +406,6 @@ declare const insightsSchema: {
410
406
  movement_install_id: "string";
411
407
  bundle_ref: "string";
412
408
  }, {
413
- readonly recent: {
414
- readonly eq: readonly ["channel", "platform", "day"];
415
- readonly sort: readonly ["received_at_ms"];
416
- };
417
409
  readonly movementsByInstall: {
418
410
  readonly eq: readonly ["movement_install_id"];
419
411
  readonly sort: readonly ["received_at_ms"];
@@ -515,7 +507,7 @@ declare const insightsSchema: {
515
507
  readonly eq: readonly ["identity"];
516
508
  readonly sort: readonly ["bucket_start_ms"];
517
509
  };
518
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
510
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
519
511
  /** Daily counters and sketches: the daily period. */
520
512
  readonly insights_overview_daily: import("@hot-updater/plugin-core").AggregateDefinition<{
521
513
  readonly identity: {
@@ -544,7 +536,7 @@ declare const insightsSchema: {
544
536
  readonly eq: readonly ["identity"];
545
537
  readonly sort: readonly ["bucket_start_ms"];
546
538
  };
547
- }, "launch_users" | "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
539
+ }, "activity_users" | "failed_users", readonly ["identity", "bucket_start_ms"]>;
548
540
  /** A release's counters since its first event, and its failed installations: kept. */
549
541
  readonly insights_overview_lifetime: import("@hot-updater/plugin-core").AggregateDefinition<{
550
542
  readonly identity: {
@@ -92,14 +92,6 @@ const bundleEvents = (days) => defineTable({
92
92
  }
93
93
  },
94
94
  indexes: {
95
- recent: {
96
- eq: [
97
- "channel",
98
- "platform",
99
- "day"
100
- ],
101
- sort: ["received_at_ms"]
102
- },
103
95
  movementsByInstall: {
104
96
  eq: ["movement_install_id"],
105
97
  sort: ["received_at_ms"]
@@ -216,10 +208,11 @@ const window = {
216
208
  };
217
209
  /**
218
210
  * Counters of one period kind: hourly, daily, or lifetime at bucket 0. A
219
- * release or channel row counts downloads, launches, and failed launches,
220
- * and downloads a patch delivered and those that fell back from one. A
221
- * release's lifetime row counts its failed updates (`failed_updates`), since
222
- * no breakdown row outlives the raw period; windowed reads sum
211
+ * release or channel row counts downloads and failed launches, and downloads
212
+ * a patch delivered and those that fell back from one. Only a release's
213
+ * lifetime row counts launches, which the bundle list reads and no windowed
214
+ * read sums. It also counts the release's failed updates (`failed_updates`),
215
+ * since no breakdown row outlives the raw period; windowed reads sum
223
216
  * `insights_failures` instead.
224
217
  */
225
218
  const counters = (retention) => defineAggregate(identityFields, {
@@ -238,17 +231,13 @@ const counters = (retention) => defineAggregate(identityFields, {
238
231
  ...retention === void 0 ? {} : { retention }
239
232
  });
240
233
  /**
241
- * Unique-installation sketches of one period kind. Rows of a failure
242
- * identity (`failure` and `check` scopes) keep only `failed_users`, so a
243
- * release's launch rows never grow by a failure's registers.
234
+ * Unique-installation sketches of one period kind: a usage row's active
235
+ * installations (`activity_users`), and a `failure` or `check` row's
236
+ * installations whose update or update check failed (`failed_users`).
244
237
  */
245
238
  const sketches = (retention) => defineAggregate(identityFields, {
246
239
  key: ["identity", "bucket_start_ms"],
247
- distinct: [
248
- "launch_users",
249
- "activity_users",
250
- "failed_users"
251
- ],
240
+ distinct: ["activity_users", "failed_users"],
252
241
  shards: SKETCH_SHARDS,
253
242
  batched: true,
254
243
  indexes: { window },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/plugin-insights",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.29",
4
+ "version": "1.0.0-rc.31",
5
5
  "description": "Hot Updater's Insights plugin: launch, download, and update failure events, for the server and the React Native app",
6
6
  "exports": {
7
7
  "./client": {
@@ -47,7 +47,7 @@
47
47
  "@types/node": "^20",
48
48
  "vitest": "4.1.4",
49
49
  "@hot-updater/plugin-core": "1.0.0-rc.28",
50
- "@hot-updater/test-utils": "1.0.0-rc.28"
50
+ "@hot-updater/test-utils": "1.0.0-rc.30"
51
51
  },
52
52
  "nx": {
53
53
  "implicitDependencies": [