@hot-updater/aws 1.0.0-rc.29 → 1.0.0-rc.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -224,9 +224,13 @@ interface ReleaseActivityMetrics {
224
224
  readonly failedLaunches: number;
225
225
  /** Omitted for lifetime-only bundle-row reads. */
226
226
  readonly uniqueUsers?: number;
227
- /** Daily points, present only for period reads. */
227
+ /**
228
+ * Points of each UTC day, or of each `intervalMs` from the period's start
229
+ * on a release read that asks for one; present only for period reads.
230
+ */
228
231
  readonly series?: readonly {
229
232
  readonly startMs: number;
233
+ readonly downloads: number;
230
234
  readonly launches: number;
231
235
  readonly failedLaunches: number;
232
236
  }[];
@@ -234,10 +238,17 @@ interface ReleaseActivityMetrics {
234
238
  type InsightsGetReleaseActivityInput = {
235
239
  readonly releases: readonly ReleaseReference[];
236
240
  readonly timeRange?: InsightsTimeRange;
241
+ /**
242
+ * Whole hours each series point spans, from `timeRange.start`, with
243
+ * every point present; omitted, the series has a point per UTC day
244
+ * with reports. Only with a `timeRange`.
245
+ */
246
+ readonly intervalMs?: number;
237
247
  readonly scope?: never;
238
248
  } | {
239
249
  readonly scope: InsightsScope;
240
250
  readonly timeRange: InsightsTimeRange;
251
+ readonly intervalMs?: never;
241
252
  readonly releases?: never;
242
253
  };
243
254
  interface InsightsGetReleaseActivityResult {
@@ -280,6 +291,23 @@ interface InsightsGetAppUsageResult {
280
291
  }[];
281
292
  readonly measuredAtMs: number;
282
293
  }
294
+ /** Whole UTC days, at most 31, including the current unfinished day. */
295
+ interface InsightsGetDistributionHistoryInput extends InsightsScope {
296
+ readonly timeRange: InsightsTimeRange;
297
+ }
298
+ interface InsightsGetDistributionHistoryResult {
299
+ readonly coverage: InsightsCoverage;
300
+ readonly measuredAtMs: number;
301
+ readonly points: readonly {
302
+ readonly startMs: number;
303
+ readonly bundles: readonly {
304
+ readonly appVersion: string;
305
+ readonly releaseId: string | null;
306
+ readonly bundleKind: "release" | "builtin" | "unknown";
307
+ readonly installations: number;
308
+ }[];
309
+ }[];
310
+ }
283
311
  //#endregion
284
312
  //#region ../insights/dist/server/schema.d.mts
285
313
  //#region src/server/schema.d.ts
@@ -621,6 +649,68 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
621
649
  readonly sort: readonly ["bucket_start_ms"];
622
650
  };
623
651
  }, never, readonly ["channel", "platform", "app_version", "release_id", "bucket_start_ms"]>;
652
+ readonly bundle_daily_heads: import("@hot-updater/plugin-core").TableDefinition<{
653
+ readonly install_id: {
654
+ readonly type: "string";
655
+ readonly maxLength: 255;
656
+ };
657
+ readonly bucket_start_ms: {
658
+ readonly type: "integer";
659
+ };
660
+ readonly id: {
661
+ readonly type: "string";
662
+ readonly maxLength: 36;
663
+ };
664
+ readonly received_at_ms: {
665
+ readonly type: "integer";
666
+ };
667
+ readonly channel: {
668
+ readonly type: "string";
669
+ };
670
+ readonly platform: {
671
+ readonly type: "string";
672
+ readonly maxLength: 16;
673
+ };
674
+ readonly app_version: {
675
+ readonly type: "string";
676
+ };
677
+ readonly release_id: {
678
+ readonly type: "string";
679
+ readonly maxLength: 36;
680
+ };
681
+ readonly bundle_kind: {
682
+ readonly type: "string";
683
+ readonly maxLength: 16;
684
+ };
685
+ }, Readonly<Record<string, import("@hot-updater/plugin-core").FieldType>>, {}, readonly ["install_id", "bucket_start_ms"]>;
686
+ readonly insights_distribution_history: import("@hot-updater/plugin-core").AggregateDefinition<{
687
+ readonly channel: {
688
+ readonly type: "string";
689
+ };
690
+ readonly platform: {
691
+ readonly type: "string";
692
+ readonly maxLength: 16;
693
+ };
694
+ readonly app_version: {
695
+ readonly type: "string";
696
+ };
697
+ readonly release_id: {
698
+ readonly type: "string";
699
+ readonly maxLength: 36;
700
+ };
701
+ readonly bundle_kind: {
702
+ readonly type: "string";
703
+ readonly maxLength: 16;
704
+ };
705
+ readonly bucket_start_ms: {
706
+ readonly type: "integer";
707
+ };
708
+ }, "installations", {
709
+ readonly byScope: {
710
+ readonly eq: readonly ["channel", "platform"];
711
+ readonly sort: readonly ["bucket_start_ms"];
712
+ };
713
+ }, never, readonly ["channel", "platform", "app_version", "release_id", "bundle_kind", "bucket_start_ms"]>;
624
714
  readonly insights_latest_by_bundle: import("@hot-updater/plugin-core").AggregateDefinition<{
625
715
  readonly platform: {
626
716
  readonly type: "string";
@@ -717,6 +807,7 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
717
807
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
718
808
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
719
809
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
810
+ getDistributionHistory: (input: InsightsGetDistributionHistoryInput) => Promise<InsightsGetDistributionHistoryResult>;
720
811
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
721
812
  getUpdateFailures: (input: InsightsUpdateFailuresInput) => Promise<InsightsUpdateFailures>;
722
813
  };
package/dist/index.d.mts CHANGED
@@ -224,9 +224,13 @@ interface ReleaseActivityMetrics {
224
224
  readonly failedLaunches: number;
225
225
  /** Omitted for lifetime-only bundle-row reads. */
226
226
  readonly uniqueUsers?: number;
227
- /** Daily points, present only for period reads. */
227
+ /**
228
+ * Points of each UTC day, or of each `intervalMs` from the period's start
229
+ * on a release read that asks for one; present only for period reads.
230
+ */
228
231
  readonly series?: readonly {
229
232
  readonly startMs: number;
233
+ readonly downloads: number;
230
234
  readonly launches: number;
231
235
  readonly failedLaunches: number;
232
236
  }[];
@@ -234,10 +238,17 @@ interface ReleaseActivityMetrics {
234
238
  type InsightsGetReleaseActivityInput = {
235
239
  readonly releases: readonly ReleaseReference[];
236
240
  readonly timeRange?: InsightsTimeRange;
241
+ /**
242
+ * Whole hours each series point spans, from `timeRange.start`, with
243
+ * every point present; omitted, the series has a point per UTC day
244
+ * with reports. Only with a `timeRange`.
245
+ */
246
+ readonly intervalMs?: number;
237
247
  readonly scope?: never;
238
248
  } | {
239
249
  readonly scope: InsightsScope;
240
250
  readonly timeRange: InsightsTimeRange;
251
+ readonly intervalMs?: never;
241
252
  readonly releases?: never;
242
253
  };
243
254
  interface InsightsGetReleaseActivityResult {
@@ -280,6 +291,23 @@ interface InsightsGetAppUsageResult {
280
291
  }[];
281
292
  readonly measuredAtMs: number;
282
293
  }
294
+ /** Whole UTC days, at most 31, including the current unfinished day. */
295
+ interface InsightsGetDistributionHistoryInput extends InsightsScope {
296
+ readonly timeRange: InsightsTimeRange;
297
+ }
298
+ interface InsightsGetDistributionHistoryResult {
299
+ readonly coverage: InsightsCoverage;
300
+ readonly measuredAtMs: number;
301
+ readonly points: readonly {
302
+ readonly startMs: number;
303
+ readonly bundles: readonly {
304
+ readonly appVersion: string;
305
+ readonly releaseId: string | null;
306
+ readonly bundleKind: "release" | "builtin" | "unknown";
307
+ readonly installations: number;
308
+ }[];
309
+ }[];
310
+ }
283
311
  //#endregion
284
312
  //#region ../insights/dist/server/schema.d.mts
285
313
  //#region src/server/schema.d.ts
@@ -621,6 +649,68 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
621
649
  readonly sort: readonly ["bucket_start_ms"];
622
650
  };
623
651
  }, never, readonly ["channel", "platform", "app_version", "release_id", "bucket_start_ms"]>;
652
+ readonly bundle_daily_heads: import("@hot-updater/plugin-core").TableDefinition<{
653
+ readonly install_id: {
654
+ readonly type: "string";
655
+ readonly maxLength: 255;
656
+ };
657
+ readonly bucket_start_ms: {
658
+ readonly type: "integer";
659
+ };
660
+ readonly id: {
661
+ readonly type: "string";
662
+ readonly maxLength: 36;
663
+ };
664
+ readonly received_at_ms: {
665
+ readonly type: "integer";
666
+ };
667
+ readonly channel: {
668
+ readonly type: "string";
669
+ };
670
+ readonly platform: {
671
+ readonly type: "string";
672
+ readonly maxLength: 16;
673
+ };
674
+ readonly app_version: {
675
+ readonly type: "string";
676
+ };
677
+ readonly release_id: {
678
+ readonly type: "string";
679
+ readonly maxLength: 36;
680
+ };
681
+ readonly bundle_kind: {
682
+ readonly type: "string";
683
+ readonly maxLength: 16;
684
+ };
685
+ }, Readonly<Record<string, import("@hot-updater/plugin-core").FieldType>>, {}, readonly ["install_id", "bucket_start_ms"]>;
686
+ readonly insights_distribution_history: import("@hot-updater/plugin-core").AggregateDefinition<{
687
+ readonly channel: {
688
+ readonly type: "string";
689
+ };
690
+ readonly platform: {
691
+ readonly type: "string";
692
+ readonly maxLength: 16;
693
+ };
694
+ readonly app_version: {
695
+ readonly type: "string";
696
+ };
697
+ readonly release_id: {
698
+ readonly type: "string";
699
+ readonly maxLength: 36;
700
+ };
701
+ readonly bundle_kind: {
702
+ readonly type: "string";
703
+ readonly maxLength: 16;
704
+ };
705
+ readonly bucket_start_ms: {
706
+ readonly type: "integer";
707
+ };
708
+ }, "installations", {
709
+ readonly byScope: {
710
+ readonly eq: readonly ["channel", "platform"];
711
+ readonly sort: readonly ["bucket_start_ms"];
712
+ };
713
+ }, never, readonly ["channel", "platform", "app_version", "release_id", "bundle_kind", "bucket_start_ms"]>;
624
714
  readonly insights_latest_by_bundle: import("@hot-updater/plugin-core").AggregateDefinition<{
625
715
  readonly platform: {
626
716
  readonly type: "string";
@@ -717,6 +807,7 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
717
807
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
718
808
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
719
809
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
810
+ getDistributionHistory: (input: InsightsGetDistributionHistoryInput) => Promise<InsightsGetDistributionHistoryResult>;
720
811
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
721
812
  getUpdateFailures: (input: InsightsUpdateFailuresInput) => Promise<InsightsUpdateFailures>;
722
813
  };
@@ -4570,7 +4570,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4570
4570
  };
4571
4571
  //#endregion
4572
4572
  //#region ../../packages/server/dist/version.mjs
4573
- const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.29";
4573
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.30";
4574
4574
  //#endregion
4575
4575
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4576
4576
  /**
@@ -9986,19 +9986,40 @@ const createValidatedInsightsModel = (model) => ({
9986
9986
  if (!isRecord$2(input) || !hasOnlyKeys(input, [
9987
9987
  "releases",
9988
9988
  "scope",
9989
- "timeRange"
9989
+ "timeRange",
9990
+ "intervalMs"
9990
9991
  ])) invalidQuery();
9991
9992
  const releases = input.releases;
9992
9993
  const scope = input.scope;
9993
9994
  const timeRange = input.timeRange;
9994
- const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 || isTimeRange(timeRange));
9995
- const scopeQuery = releases === void 0 && isRecord$2(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
9995
+ const intervalMs = input.intervalMs;
9996
+ const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 ? intervalMs === void 0 : isTimeRange(timeRange) && (intervalMs === void 0 || isTimestamp(intervalMs) && intervalMs > 0 && intervalMs % 36e5 === 0 && (timeRange.end - timeRange.start) / intervalMs <= 8784));
9997
+ const scopeQuery = releases === void 0 && intervalMs === void 0 && isRecord$2(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
9996
9998
  if (!releaseQuery && !scopeQuery) invalidQuery();
9997
9999
  const result = await model.getReleaseActivity(input);
9998
10000
  validateAggregateResult(result);
9999
10001
  if (!Array.isArray(result.data)) invalidResult();
10000
10002
  return result;
10001
10003
  },
10004
+ async getDistributionHistory(input) {
10005
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
10006
+ "channel",
10007
+ "platform",
10008
+ "timeRange"
10009
+ ]) || !hasScope(input) || !isTimeRange(input.timeRange) || input.timeRange.start % 864e5 !== 0 || input.timeRange.end % 864e5 !== 0 || input.timeRange.end - input.timeRange.start > 26784e5) invalidQuery();
10010
+ const result = await model.getDistributionHistory(input);
10011
+ validateAggregateResult(result);
10012
+ if (!Array.isArray(result.points)) invalidResult();
10013
+ for (const point of result.points) {
10014
+ if (!isTimestamp(point.startMs) || !Array.isArray(point.bundles)) invalidResult();
10015
+ for (const bundle of point.bundles) if (!isText$1(bundle.appVersion) || bundle.releaseId !== null && !isText$1(bundle.releaseId) || ![
10016
+ "release",
10017
+ "builtin",
10018
+ "unknown"
10019
+ ].includes(bundle.bundleKind) || !isTimestamp(bundle.installations)) invalidResult();
10020
+ }
10021
+ return result;
10022
+ },
10002
10023
  async getAppUsage(input) {
10003
10024
  if (!isRecord$2(input) || !hasOnlyKeys(input, [
10004
10025
  "channel",
@@ -10030,7 +10051,8 @@ const createInsightsModel = (api) => createValidatedInsightsModel({
10030
10051
  countLatestEvents: api.countLatestEvents,
10031
10052
  countEvents: api.countEvents,
10032
10053
  getReleaseActivity: api.getReleaseActivity,
10033
- getAppUsage: api.getAppUsage
10054
+ getAppUsage: api.getAppUsage,
10055
+ getDistributionHistory: api.getDistributionHistory
10034
10056
  });
10035
10057
  //#endregion
10036
10058
  //#region ../insights/dist/server/official.mjs
@@ -11112,6 +11134,71 @@ const insightsDistribution = (retention) => defineAggregate({
11112
11134
  },
11113
11135
  retention
11114
11136
  });
11137
+ /** The last running bundle observed per installation and UTC day. */
11138
+ const bundleDailyHeads = (retention) => defineTable({
11139
+ install_id: {
11140
+ type: "string",
11141
+ maxLength: 255
11142
+ },
11143
+ bucket_start_ms: { type: "integer" },
11144
+ id: {
11145
+ type: "string",
11146
+ maxLength: 36
11147
+ },
11148
+ received_at_ms: { type: "integer" },
11149
+ channel: { type: "string" },
11150
+ platform: {
11151
+ type: "string",
11152
+ maxLength: 16
11153
+ },
11154
+ app_version: { type: "string" },
11155
+ release_id: {
11156
+ type: "string",
11157
+ maxLength: 36
11158
+ },
11159
+ bundle_kind: {
11160
+ type: "string",
11161
+ maxLength: 16
11162
+ }
11163
+ }, {
11164
+ key: ["install_id", "bucket_start_ms"],
11165
+ retention
11166
+ });
11167
+ /** Daily observations stay in their day when an installation reports again. */
11168
+ const insightsDistributionHistory = (retention) => defineAggregate({
11169
+ channel: { type: "string" },
11170
+ platform: {
11171
+ type: "string",
11172
+ maxLength: 16
11173
+ },
11174
+ app_version: { type: "string" },
11175
+ release_id: {
11176
+ type: "string",
11177
+ maxLength: 36
11178
+ },
11179
+ bundle_kind: {
11180
+ type: "string",
11181
+ maxLength: 16
11182
+ },
11183
+ bucket_start_ms: { type: "integer" }
11184
+ }, {
11185
+ key: [
11186
+ "channel",
11187
+ "platform",
11188
+ "app_version",
11189
+ "release_id",
11190
+ "bundle_kind",
11191
+ "bucket_start_ms"
11192
+ ],
11193
+ gauges: ["installations"],
11194
+ shards: GAUGE_SHARDS,
11195
+ batched: true,
11196
+ indexes: { byScope: {
11197
+ eq: ["channel", "platform"],
11198
+ sort: ["bucket_start_ms"]
11199
+ } },
11200
+ retention
11201
+ });
11115
11202
  /** Latest events by the bundle they came from or went to, bucketed like the distribution. */
11116
11203
  const insightsLatestByBundle = (retention) => defineAggregate({
11117
11204
  platform: {
@@ -11231,6 +11318,8 @@ const createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTIO
11231
11318
  insights_overview_lifetime: counters(),
11232
11319
  insights_sketches_lifetime: lifetimeSketches(),
11233
11320
  insights_distribution: insightsDistribution(daily),
11321
+ bundle_daily_heads: bundleDailyHeads(daily),
11322
+ insights_distribution_history: insightsDistributionHistory(daily),
11234
11323
  insights_latest_by_bundle: insightsLatestByBundle(daily),
11235
11324
  insights_outcomes: insightsOutcomes(hourly),
11236
11325
  insights_failures: insightsFailures(hourly)
@@ -11618,21 +11707,66 @@ const REPEATED_FIELDS = [
11618
11707
  const repeatsHead = (event, head) => event.type === "UNCHANGED" && head.type !== "UPDATE_DOWNLOADED" && dayOf(event.received_at_ms) === dayOf(head.received_at_ms) && REPEATED_FIELDS.every((field) => event[field] === head[field]);
11619
11708
  /**
11620
11709
  * Records one event in one transaction: one batch read of the event and its
11621
- * installation's head, one of the gauge and sketch rows it changes, then one
11710
+ * installation's latest and daily heads, then the gauge and sketch rows it
11711
+ * changes and one
11622
11712
  * write. On a database that batches aggregates (DynamoDB, Firestore), the
11623
11713
  * write holds the event's rows and one log row instead, and a compaction
11624
11714
  * reads and writes the aggregate rows. A stored event's id changes nothing
11625
11715
  * when repeated, whichever installation sends it, and neither does the id
11626
11716
  * of the installation's head. An UNCHANGED report that repeats its head on
11627
- * the same UTC day writes nothing at all. An older event still counts in its
11717
+ * the same UTC day only advances its daily observation. An older event counts in its
11628
11718
  * own hour but never replaces the head. An update failure is stored and
11629
11719
  * counted, but changes what no installation runs, so it moves no head.
11630
11720
  */
11631
11721
  const recordEvent = (db, event) => {
11632
11722
  assertBundleEventRow(event);
11633
11723
  return db.transaction(async (tx) => {
11634
- const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
11635
- if (existing !== null || previous?.id === event.id) return;
11724
+ const dailyKey = {
11725
+ install_id: event.install_id,
11726
+ bucket_start_ms: dayOf(event.received_at_ms)
11727
+ };
11728
+ const [existing, previous, daily] = await Promise.all([
11729
+ tx.findOne("bundle_events", { id: event.id }),
11730
+ tx.findOne("bundle_event_heads", { install_id: event.install_id }),
11731
+ event.type === "UPDATE_FAILED" ? null : tx.findOne("bundle_daily_heads", dailyKey)
11732
+ ]);
11733
+ if (existing !== null || previous?.id === event.id || daily?.id === event.id) return;
11734
+ if (event.type !== "UPDATE_FAILED" && (daily === null || isNewer(event, daily))) {
11735
+ const downloaded = event.type === "UPDATE_DOWNLOADED";
11736
+ const releaseId = downloaded ? event.from_release_id : event.to_release_id;
11737
+ const bundleId = downloaded ? event.from_bundle_id : event.to_bundle_id;
11738
+ const next = {
11739
+ ...dailyKey,
11740
+ id: event.id,
11741
+ received_at_ms: event.received_at_ms,
11742
+ channel: event.channel,
11743
+ platform: event.platform,
11744
+ app_version: event.app_version,
11745
+ release_id: releaseId ?? "",
11746
+ bundle_kind: bundleId === "00000000-0000-0000-0000-000000000000" ? "builtin" : releaseId === null ? "unknown" : "release"
11747
+ };
11748
+ const count = (head, delta) => {
11749
+ tx.aggregate("insights_distribution_history", {
11750
+ channel: head.channel,
11751
+ platform: head.platform,
11752
+ app_version: head.app_version,
11753
+ release_id: head.release_id,
11754
+ bundle_kind: head.bundle_kind,
11755
+ bucket_start_ms: head.bucket_start_ms
11756
+ }, { installations: delta }, { shardBy: head.install_id });
11757
+ };
11758
+ if (daily !== null) {
11759
+ if (daily.channel !== next.channel || daily.platform !== next.platform || daily.app_version !== next.app_version || daily.release_id !== next.release_id || daily.bundle_kind !== next.bundle_kind) {
11760
+ count(daily, -1);
11761
+ count(next, 1);
11762
+ }
11763
+ const { install_id: _, bucket_start_ms: __, ...fields } = next;
11764
+ tx.update("bundle_daily_heads", daily, fields);
11765
+ } else {
11766
+ tx.create("bundle_daily_heads", next);
11767
+ count(next, 1);
11768
+ }
11769
+ }
11636
11770
  if (previous !== null && repeatsHead(event, previous)) return;
11637
11771
  if (event.type !== "UNCHANGED") {
11638
11772
  tx.create("bundle_events", event);
@@ -11998,19 +12132,30 @@ const lifetimeMetrics = async (db, release) => {
11998
12132
  failedLaunches: lifetime?.failed_launches ?? 0
11999
12133
  };
12000
12134
  };
12001
- /** Counters, unique users, and a per-UTC-day series over a window. */
12002
- const rangedMetrics = async (db, parts, users, range, days, hour) => {
12135
+ /**
12136
+ * Counters, unique users, and a series over a window: a point per UTC day
12137
+ * with rows, or, with `intervalMs`, every point of that span from the
12138
+ * window's start.
12139
+ */
12140
+ const rangedMetrics = async (db, parts, users, range, days, hour, intervalMs) => {
12003
12141
  const [counters, sketches] = await Promise.all([counterRows(db, parts, range, days, hour), sketchRows(db, users.parts, range, days, hour)]);
12004
12142
  const series = /* @__PURE__ */ new Map();
12143
+ if (intervalMs !== void 0) for (let start = range.start; start < range.end; start += intervalMs) series.set(start, {
12144
+ downloads: 0,
12145
+ launches: 0,
12146
+ failedLaunches: 0
12147
+ });
12005
12148
  for (const row of counters) {
12006
- const day = dayFloor(row.bucket_start_ms);
12007
- const point = series.get(day) ?? {
12149
+ const start = intervalMs === void 0 ? dayFloor(row.bucket_start_ms) : row.bucket_start_ms - (row.bucket_start_ms - range.start) % intervalMs;
12150
+ const point = series.get(start) ?? {
12151
+ downloads: 0,
12008
12152
  launches: 0,
12009
12153
  failedLaunches: 0
12010
12154
  };
12155
+ point.downloads += row.downloads;
12011
12156
  point.launches += row.launches;
12012
12157
  point.failedLaunches += row.failed_launches;
12013
- series.set(day, point);
12158
+ series.set(start, point);
12014
12159
  }
12015
12160
  const total = (field) => counters.reduce((sum, row) => sum + row[field], 0);
12016
12161
  return {
@@ -12056,7 +12201,7 @@ const getReleaseActivity = async (db, input, now, retention) => {
12056
12201
  metrics: input.timeRange === void 0 ? await lifetimeMetrics(db, release) : await rangedMetrics(db, releaseParts(release), {
12057
12202
  parts: releaseParts(release),
12058
12203
  field: "launch_users"
12059
- }, input.timeRange, false, kept.hour)
12204
+ }, input.timeRange, false, kept.hour, input.intervalMs)
12060
12205
  })));
12061
12206
  const oldest = kept[input.scope === void 0 ? "hour" : "day"];
12062
12207
  return {
@@ -12322,6 +12467,43 @@ const getUpdateFailures = async (db, input, now, retention) => {
12322
12467
  }
12323
12468
  };
12324
12469
  };
12470
+ /** Reads daily gauges, never raw events or mutable latest-state distribution. */
12471
+ const getDistributionHistory = async (db, input, now, retention) => {
12472
+ const at = now();
12473
+ const { start, end } = input.timeRange;
12474
+ const points = /* @__PURE__ */ new Map();
12475
+ for (let day = start; day < end; day += DAY_MS) points.set(day, []);
12476
+ const rows = await drain((page) => db.findAggregates("insights_distribution_history", {
12477
+ index: "byScope",
12478
+ where: {
12479
+ channel: input.channel,
12480
+ platform: input.platform
12481
+ },
12482
+ range: {
12483
+ gte: start,
12484
+ lt: end
12485
+ },
12486
+ limit: PAGE,
12487
+ ...page
12488
+ }));
12489
+ for (const row of rows) {
12490
+ if (row.installations <= 0) continue;
12491
+ points.get(row.bucket_start_ms).push({
12492
+ appVersion: row.app_version,
12493
+ releaseId: row.release_id || null,
12494
+ bundleKind: row.bundle_kind,
12495
+ installations: row.installations
12496
+ });
12497
+ }
12498
+ return {
12499
+ coverage: coverageOf(start, retained(at, retention).day),
12500
+ measuredAtMs: at,
12501
+ points: [...points].map(([startMs, bundles]) => ({
12502
+ startMs,
12503
+ bundles
12504
+ }))
12505
+ };
12506
+ };
12325
12507
  //#endregion
12326
12508
  //#region ../insights/dist/server/queryInput.mjs
12327
12509
  const MAX_PAGE_LIMIT = 100;
@@ -12513,6 +12695,7 @@ const createInsightsApi = (db, now, retention) => ({
12513
12695
  countLatestEvents: (input) => countLatestEvents(db, input),
12514
12696
  countEvents: (input) => countEvents(db, input),
12515
12697
  getReleaseActivity: (input) => getReleaseActivity(db, input, now, retention),
12698
+ getDistributionHistory: (input) => getDistributionHistory(db, input, now, retention),
12516
12699
  getAppUsage: (input) => getAppUsage(db, input, now, retention),
12517
12700
  /** A release's or a channel's update failures, and their breakdown over a time range. */
12518
12701
  getUpdateFailures: (input) => getUpdateFailures(db, input, now, retention)
@@ -12581,7 +12764,7 @@ const insights = (options = {}) => {
12581
12764
  const plugin = definePlugin({
12582
12765
  id: "insights",
12583
12766
  namespace: false,
12584
- schemaVersion: "1.2.0",
12767
+ schemaVersion: "1.3.0",
12585
12768
  schema: createInsightsSchema(retention),
12586
12769
  init: ({ db, now }) => {
12587
12770
  const api = createInsightsApi(db, now, retention);
@@ -12596,6 +12779,32 @@ const insights = (options = {}) => {
12596
12779
  handler: (request, params) => routes[handler](params, request)
12597
12780
  })),
12598
12781
  failuresEndpoint(api),
12782
+ {
12783
+ access: "admin",
12784
+ method: "GET",
12785
+ path: "/distribution-history",
12786
+ handler: async (request) => {
12787
+ const query = new URL(request.url).searchParams;
12788
+ try {
12789
+ const result = await createInsightsModel(api).getDistributionHistory({
12790
+ platform: query.get("platform"),
12791
+ channel: query.get("channel") ?? "",
12792
+ timeRange: {
12793
+ start: Number(query.get("start") ?? NaN),
12794
+ end: Number(query.get("end") ?? NaN)
12795
+ }
12796
+ });
12797
+ return Response.json(result, { headers: { "cache-control": "private, no-store" } });
12798
+ } catch (error) {
12799
+ if (error instanceof DatabaseAdapterInputError) return Response.json({ error: "Choose a platform, channel, and up to 31 whole UTC days." }, { status: 400 });
12800
+ if (!isDatabaseBusyError(error)) throw error;
12801
+ return Response.json({ error: "Service unavailable" }, {
12802
+ status: 503,
12803
+ headers: { "retry-after": "5" }
12804
+ });
12805
+ }
12806
+ }
12807
+ },
12599
12808
  {
12600
12809
  access: "admin",
12601
12810
  method: "GET",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/aws",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.29",
4
+ "version": "1.0.0-rc.30",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -47,7 +47,7 @@
47
47
  "mime": "^4.0.4",
48
48
  "@hot-updater/protocol": "1.0.0-rc.28",
49
49
  "@hot-updater/mock": "1.0.0-rc.28",
50
- "@hot-updater/test-utils": "1.0.0-rc.28"
50
+ "@hot-updater/test-utils": "1.0.0-rc.29"
51
51
  },
52
52
  "dependencies": {
53
53
  "@aws-sdk/client-cloudfront": "3.1066.0",
@@ -62,9 +62,9 @@
62
62
  "@aws-sdk/lib-storage": "3.1066.0",
63
63
  "@aws-sdk/lib-dynamodb": "3.1066.0",
64
64
  "hono": "4.12.34",
65
- "@hot-updater/cli-tools": "1.0.0-rc.29",
65
+ "@hot-updater/cli-tools": "1.0.0-rc.30",
66
66
  "@hot-updater/plugin-core": "1.0.0-rc.28",
67
- "@hot-updater/server": "1.0.0-rc.29"
67
+ "@hot-updater/server": "1.0.0-rc.30"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "tsdown",