@hot-updater/firebase 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.
@@ -4524,7 +4524,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
4524
4524
  };
4525
4525
  //#endregion
4526
4526
  //#region ../../packages/server/dist/version.mjs
4527
- const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.29";
4527
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.30";
4528
4528
  //#endregion
4529
4529
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4530
4530
  /**
@@ -7880,19 +7880,40 @@ const createValidatedInsightsModel = (model) => ({
7880
7880
  if (!isRecord$2(input) || !hasOnlyKeys(input, [
7881
7881
  "releases",
7882
7882
  "scope",
7883
- "timeRange"
7883
+ "timeRange",
7884
+ "intervalMs"
7884
7885
  ])) invalidQuery();
7885
7886
  const releases = input.releases;
7886
7887
  const scope = input.scope;
7887
7888
  const timeRange = input.timeRange;
7888
- const releaseQuery = Array.isArray(releases) && releases.length > 0 && releases.length <= 100 && releases.every(isReleaseReference) && scope === void 0 && (timeRange === void 0 || isTimeRange(timeRange));
7889
- const scopeQuery = releases === void 0 && isRecord$2(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
7889
+ const intervalMs = input.intervalMs;
7890
+ 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));
7891
+ const scopeQuery = releases === void 0 && intervalMs === void 0 && isRecord$2(scope) && hasOnlyKeys(scope, ["platform", "channel"]) && hasScope(scope) && isTimeRange(timeRange);
7890
7892
  if (!releaseQuery && !scopeQuery) invalidQuery();
7891
7893
  const result = await model.getReleaseActivity(input);
7892
7894
  validateAggregateResult(result);
7893
7895
  if (!Array.isArray(result.data)) invalidResult();
7894
7896
  return result;
7895
7897
  },
7898
+ async getDistributionHistory(input) {
7899
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
7900
+ "channel",
7901
+ "platform",
7902
+ "timeRange"
7903
+ ]) || !hasScope(input) || !isTimeRange(input.timeRange) || input.timeRange.start % 864e5 !== 0 || input.timeRange.end % 864e5 !== 0 || input.timeRange.end - input.timeRange.start > 26784e5) invalidQuery();
7904
+ const result = await model.getDistributionHistory(input);
7905
+ validateAggregateResult(result);
7906
+ if (!Array.isArray(result.points)) invalidResult();
7907
+ for (const point of result.points) {
7908
+ if (!isTimestamp(point.startMs) || !Array.isArray(point.bundles)) invalidResult();
7909
+ for (const bundle of point.bundles) if (!isText$1(bundle.appVersion) || bundle.releaseId !== null && !isText$1(bundle.releaseId) || ![
7910
+ "release",
7911
+ "builtin",
7912
+ "unknown"
7913
+ ].includes(bundle.bundleKind) || !isTimestamp(bundle.installations)) invalidResult();
7914
+ }
7915
+ return result;
7916
+ },
7896
7917
  async getAppUsage(input) {
7897
7918
  if (!isRecord$2(input) || !hasOnlyKeys(input, [
7898
7919
  "channel",
@@ -7924,7 +7945,8 @@ const createInsightsModel = (api) => createValidatedInsightsModel({
7924
7945
  countLatestEvents: api.countLatestEvents,
7925
7946
  countEvents: api.countEvents,
7926
7947
  getReleaseActivity: api.getReleaseActivity,
7927
- getAppUsage: api.getAppUsage
7948
+ getAppUsage: api.getAppUsage,
7949
+ getDistributionHistory: api.getDistributionHistory
7928
7950
  });
7929
7951
  //#endregion
7930
7952
  //#region ../insights/dist/server/official.mjs
@@ -9006,6 +9028,71 @@ const insightsDistribution = (retention) => defineAggregate({
9006
9028
  },
9007
9029
  retention
9008
9030
  });
9031
+ /** The last running bundle observed per installation and UTC day. */
9032
+ const bundleDailyHeads = (retention) => defineTable({
9033
+ install_id: {
9034
+ type: "string",
9035
+ maxLength: 255
9036
+ },
9037
+ bucket_start_ms: { type: "integer" },
9038
+ id: {
9039
+ type: "string",
9040
+ maxLength: 36
9041
+ },
9042
+ received_at_ms: { type: "integer" },
9043
+ channel: { type: "string" },
9044
+ platform: {
9045
+ type: "string",
9046
+ maxLength: 16
9047
+ },
9048
+ app_version: { type: "string" },
9049
+ release_id: {
9050
+ type: "string",
9051
+ maxLength: 36
9052
+ },
9053
+ bundle_kind: {
9054
+ type: "string",
9055
+ maxLength: 16
9056
+ }
9057
+ }, {
9058
+ key: ["install_id", "bucket_start_ms"],
9059
+ retention
9060
+ });
9061
+ /** Daily observations stay in their day when an installation reports again. */
9062
+ const insightsDistributionHistory = (retention) => defineAggregate({
9063
+ channel: { type: "string" },
9064
+ platform: {
9065
+ type: "string",
9066
+ maxLength: 16
9067
+ },
9068
+ app_version: { type: "string" },
9069
+ release_id: {
9070
+ type: "string",
9071
+ maxLength: 36
9072
+ },
9073
+ bundle_kind: {
9074
+ type: "string",
9075
+ maxLength: 16
9076
+ },
9077
+ bucket_start_ms: { type: "integer" }
9078
+ }, {
9079
+ key: [
9080
+ "channel",
9081
+ "platform",
9082
+ "app_version",
9083
+ "release_id",
9084
+ "bundle_kind",
9085
+ "bucket_start_ms"
9086
+ ],
9087
+ gauges: ["installations"],
9088
+ shards: GAUGE_SHARDS,
9089
+ batched: true,
9090
+ indexes: { byScope: {
9091
+ eq: ["channel", "platform"],
9092
+ sort: ["bucket_start_ms"]
9093
+ } },
9094
+ retention
9095
+ });
9009
9096
  /** Latest events by the bundle they came from or went to, bucketed like the distribution. */
9010
9097
  const insightsLatestByBundle = (retention) => defineAggregate({
9011
9098
  platform: {
@@ -9125,6 +9212,8 @@ const createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTIO
9125
9212
  insights_overview_lifetime: counters(),
9126
9213
  insights_sketches_lifetime: lifetimeSketches(),
9127
9214
  insights_distribution: insightsDistribution(daily),
9215
+ bundle_daily_heads: bundleDailyHeads(daily),
9216
+ insights_distribution_history: insightsDistributionHistory(daily),
9128
9217
  insights_latest_by_bundle: insightsLatestByBundle(daily),
9129
9218
  insights_outcomes: insightsOutcomes(hourly),
9130
9219
  insights_failures: insightsFailures(hourly)
@@ -9512,21 +9601,66 @@ const REPEATED_FIELDS = [
9512
9601
  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]);
9513
9602
  /**
9514
9603
  * Records one event in one transaction: one batch read of the event and its
9515
- * installation's head, one of the gauge and sketch rows it changes, then one
9604
+ * installation's latest and daily heads, then the gauge and sketch rows it
9605
+ * changes and one
9516
9606
  * write. On a database that batches aggregates (DynamoDB, Firestore), the
9517
9607
  * write holds the event's rows and one log row instead, and a compaction
9518
9608
  * reads and writes the aggregate rows. A stored event's id changes nothing
9519
9609
  * when repeated, whichever installation sends it, and neither does the id
9520
9610
  * of the installation's head. An UNCHANGED report that repeats its head on
9521
- * the same UTC day writes nothing at all. An older event still counts in its
9611
+ * the same UTC day only advances its daily observation. An older event counts in its
9522
9612
  * own hour but never replaces the head. An update failure is stored and
9523
9613
  * counted, but changes what no installation runs, so it moves no head.
9524
9614
  */
9525
9615
  const recordEvent = (db, event) => {
9526
9616
  assertBundleEventRow(event);
9527
9617
  return db.transaction(async (tx) => {
9528
- const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
9529
- if (existing !== null || previous?.id === event.id) return;
9618
+ const dailyKey = {
9619
+ install_id: event.install_id,
9620
+ bucket_start_ms: dayOf(event.received_at_ms)
9621
+ };
9622
+ const [existing, previous, daily] = await Promise.all([
9623
+ tx.findOne("bundle_events", { id: event.id }),
9624
+ tx.findOne("bundle_event_heads", { install_id: event.install_id }),
9625
+ event.type === "UPDATE_FAILED" ? null : tx.findOne("bundle_daily_heads", dailyKey)
9626
+ ]);
9627
+ if (existing !== null || previous?.id === event.id || daily?.id === event.id) return;
9628
+ if (event.type !== "UPDATE_FAILED" && (daily === null || isNewer(event, daily))) {
9629
+ const downloaded = event.type === "UPDATE_DOWNLOADED";
9630
+ const releaseId = downloaded ? event.from_release_id : event.to_release_id;
9631
+ const bundleId = downloaded ? event.from_bundle_id : event.to_bundle_id;
9632
+ const next = {
9633
+ ...dailyKey,
9634
+ id: event.id,
9635
+ received_at_ms: event.received_at_ms,
9636
+ channel: event.channel,
9637
+ platform: event.platform,
9638
+ app_version: event.app_version,
9639
+ release_id: releaseId ?? "",
9640
+ bundle_kind: bundleId === "00000000-0000-0000-0000-000000000000" ? "builtin" : releaseId === null ? "unknown" : "release"
9641
+ };
9642
+ const count = (head, delta) => {
9643
+ tx.aggregate("insights_distribution_history", {
9644
+ channel: head.channel,
9645
+ platform: head.platform,
9646
+ app_version: head.app_version,
9647
+ release_id: head.release_id,
9648
+ bundle_kind: head.bundle_kind,
9649
+ bucket_start_ms: head.bucket_start_ms
9650
+ }, { installations: delta }, { shardBy: head.install_id });
9651
+ };
9652
+ if (daily !== null) {
9653
+ 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) {
9654
+ count(daily, -1);
9655
+ count(next, 1);
9656
+ }
9657
+ const { install_id: _, bucket_start_ms: __, ...fields } = next;
9658
+ tx.update("bundle_daily_heads", daily, fields);
9659
+ } else {
9660
+ tx.create("bundle_daily_heads", next);
9661
+ count(next, 1);
9662
+ }
9663
+ }
9530
9664
  if (previous !== null && repeatsHead(event, previous)) return;
9531
9665
  if (event.type !== "UNCHANGED") {
9532
9666
  tx.create("bundle_events", event);
@@ -9892,19 +10026,30 @@ const lifetimeMetrics = async (db, release) => {
9892
10026
  failedLaunches: lifetime?.failed_launches ?? 0
9893
10027
  };
9894
10028
  };
9895
- /** Counters, unique users, and a per-UTC-day series over a window. */
9896
- const rangedMetrics = async (db, parts, users, range, days, hour) => {
10029
+ /**
10030
+ * Counters, unique users, and a series over a window: a point per UTC day
10031
+ * with rows, or, with `intervalMs`, every point of that span from the
10032
+ * window's start.
10033
+ */
10034
+ const rangedMetrics = async (db, parts, users, range, days, hour, intervalMs) => {
9897
10035
  const [counters, sketches] = await Promise.all([counterRows(db, parts, range, days, hour), sketchRows(db, users.parts, range, days, hour)]);
9898
10036
  const series = /* @__PURE__ */ new Map();
10037
+ if (intervalMs !== void 0) for (let start = range.start; start < range.end; start += intervalMs) series.set(start, {
10038
+ downloads: 0,
10039
+ launches: 0,
10040
+ failedLaunches: 0
10041
+ });
9899
10042
  for (const row of counters) {
9900
- const day = dayFloor(row.bucket_start_ms);
9901
- const point = series.get(day) ?? {
10043
+ const start = intervalMs === void 0 ? dayFloor(row.bucket_start_ms) : row.bucket_start_ms - (row.bucket_start_ms - range.start) % intervalMs;
10044
+ const point = series.get(start) ?? {
10045
+ downloads: 0,
9902
10046
  launches: 0,
9903
10047
  failedLaunches: 0
9904
10048
  };
10049
+ point.downloads += row.downloads;
9905
10050
  point.launches += row.launches;
9906
10051
  point.failedLaunches += row.failed_launches;
9907
- series.set(day, point);
10052
+ series.set(start, point);
9908
10053
  }
9909
10054
  const total = (field) => counters.reduce((sum, row) => sum + row[field], 0);
9910
10055
  return {
@@ -9950,7 +10095,7 @@ const getReleaseActivity = async (db, input, now, retention) => {
9950
10095
  metrics: input.timeRange === void 0 ? await lifetimeMetrics(db, release) : await rangedMetrics(db, releaseParts(release), {
9951
10096
  parts: releaseParts(release),
9952
10097
  field: "launch_users"
9953
- }, input.timeRange, false, kept.hour)
10098
+ }, input.timeRange, false, kept.hour, input.intervalMs)
9954
10099
  })));
9955
10100
  const oldest = kept[input.scope === void 0 ? "hour" : "day"];
9956
10101
  return {
@@ -10216,6 +10361,43 @@ const getUpdateFailures = async (db, input, now, retention) => {
10216
10361
  }
10217
10362
  };
10218
10363
  };
10364
+ /** Reads daily gauges, never raw events or mutable latest-state distribution. */
10365
+ const getDistributionHistory = async (db, input, now, retention) => {
10366
+ const at = now();
10367
+ const { start, end } = input.timeRange;
10368
+ const points = /* @__PURE__ */ new Map();
10369
+ for (let day = start; day < end; day += DAY_MS) points.set(day, []);
10370
+ const rows = await drain((page) => db.findAggregates("insights_distribution_history", {
10371
+ index: "byScope",
10372
+ where: {
10373
+ channel: input.channel,
10374
+ platform: input.platform
10375
+ },
10376
+ range: {
10377
+ gte: start,
10378
+ lt: end
10379
+ },
10380
+ limit: PAGE,
10381
+ ...page
10382
+ }));
10383
+ for (const row of rows) {
10384
+ if (row.installations <= 0) continue;
10385
+ points.get(row.bucket_start_ms).push({
10386
+ appVersion: row.app_version,
10387
+ releaseId: row.release_id || null,
10388
+ bundleKind: row.bundle_kind,
10389
+ installations: row.installations
10390
+ });
10391
+ }
10392
+ return {
10393
+ coverage: coverageOf(start, retained(at, retention).day),
10394
+ measuredAtMs: at,
10395
+ points: [...points].map(([startMs, bundles]) => ({
10396
+ startMs,
10397
+ bundles
10398
+ }))
10399
+ };
10400
+ };
10219
10401
  //#endregion
10220
10402
  //#region ../insights/dist/server/queryInput.mjs
10221
10403
  const MAX_PAGE_LIMIT = 100;
@@ -10407,6 +10589,7 @@ const createInsightsApi = (db, now, retention) => ({
10407
10589
  countLatestEvents: (input) => countLatestEvents(db, input),
10408
10590
  countEvents: (input) => countEvents(db, input),
10409
10591
  getReleaseActivity: (input) => getReleaseActivity(db, input, now, retention),
10592
+ getDistributionHistory: (input) => getDistributionHistory(db, input, now, retention),
10410
10593
  getAppUsage: (input) => getAppUsage(db, input, now, retention),
10411
10594
  /** A release's or a channel's update failures, and their breakdown over a time range. */
10412
10595
  getUpdateFailures: (input) => getUpdateFailures(db, input, now, retention)
@@ -10475,7 +10658,7 @@ const insights = (options = {}) => {
10475
10658
  const plugin = definePlugin({
10476
10659
  id: "insights",
10477
10660
  namespace: false,
10478
- schemaVersion: "1.2.0",
10661
+ schemaVersion: "1.3.0",
10479
10662
  schema: createInsightsSchema(retention),
10480
10663
  init: ({ db, now }) => {
10481
10664
  const api = createInsightsApi(db, now, retention);
@@ -10490,6 +10673,32 @@ const insights = (options = {}) => {
10490
10673
  handler: (request, params) => routes[handler](params, request)
10491
10674
  })),
10492
10675
  failuresEndpoint(api),
10676
+ {
10677
+ access: "admin",
10678
+ method: "GET",
10679
+ path: "/distribution-history",
10680
+ handler: async (request) => {
10681
+ const query = new URL(request.url).searchParams;
10682
+ try {
10683
+ const result = await createInsightsModel(api).getDistributionHistory({
10684
+ platform: query.get("platform"),
10685
+ channel: query.get("channel") ?? "",
10686
+ timeRange: {
10687
+ start: Number(query.get("start") ?? NaN),
10688
+ end: Number(query.get("end") ?? NaN)
10689
+ }
10690
+ });
10691
+ return Response.json(result, { headers: { "cache-control": "private, no-store" } });
10692
+ } catch (error) {
10693
+ if (error instanceof DatabaseAdapterInputError) return Response.json({ error: "Choose a platform, channel, and up to 31 whole UTC days." }, { status: 400 });
10694
+ if (!isDatabaseBusyError(error)) throw error;
10695
+ return Response.json({ error: "Service unavailable" }, {
10696
+ status: 503,
10697
+ headers: { "retry-after": "5" }
10698
+ });
10699
+ }
10700
+ }
10701
+ },
10493
10702
  {
10494
10703
  access: "admin",
10495
10704
  method: "GET",
package/dist/index.d.cts CHANGED
@@ -191,9 +191,13 @@ interface ReleaseActivityMetrics {
191
191
  readonly failedLaunches: number;
192
192
  /** Omitted for lifetime-only bundle-row reads. */
193
193
  readonly uniqueUsers?: number;
194
- /** Daily points, present only for period reads. */
194
+ /**
195
+ * Points of each UTC day, or of each `intervalMs` from the period's start
196
+ * on a release read that asks for one; present only for period reads.
197
+ */
195
198
  readonly series?: readonly {
196
199
  readonly startMs: number;
200
+ readonly downloads: number;
197
201
  readonly launches: number;
198
202
  readonly failedLaunches: number;
199
203
  }[];
@@ -201,10 +205,17 @@ interface ReleaseActivityMetrics {
201
205
  type InsightsGetReleaseActivityInput = {
202
206
  readonly releases: readonly ReleaseReference[];
203
207
  readonly timeRange?: InsightsTimeRange;
208
+ /**
209
+ * Whole hours each series point spans, from `timeRange.start`, with
210
+ * every point present; omitted, the series has a point per UTC day
211
+ * with reports. Only with a `timeRange`.
212
+ */
213
+ readonly intervalMs?: number;
204
214
  readonly scope?: never;
205
215
  } | {
206
216
  readonly scope: InsightsScope;
207
217
  readonly timeRange: InsightsTimeRange;
218
+ readonly intervalMs?: never;
208
219
  readonly releases?: never;
209
220
  };
210
221
  interface InsightsGetReleaseActivityResult {
@@ -247,6 +258,23 @@ interface InsightsGetAppUsageResult {
247
258
  }[];
248
259
  readonly measuredAtMs: number;
249
260
  }
261
+ /** Whole UTC days, at most 31, including the current unfinished day. */
262
+ interface InsightsGetDistributionHistoryInput extends InsightsScope {
263
+ readonly timeRange: InsightsTimeRange;
264
+ }
265
+ interface InsightsGetDistributionHistoryResult {
266
+ readonly coverage: InsightsCoverage;
267
+ readonly measuredAtMs: number;
268
+ readonly points: readonly {
269
+ readonly startMs: number;
270
+ readonly bundles: readonly {
271
+ readonly appVersion: string;
272
+ readonly releaseId: string | null;
273
+ readonly bundleKind: "release" | "builtin" | "unknown";
274
+ readonly installations: number;
275
+ }[];
276
+ }[];
277
+ }
250
278
  //#endregion
251
279
  //#region ../insights/dist/server/schema.d.mts
252
280
  //#region src/server/schema.d.ts
@@ -588,6 +616,68 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
588
616
  readonly sort: readonly ["bucket_start_ms"];
589
617
  };
590
618
  }, never, readonly ["channel", "platform", "app_version", "release_id", "bucket_start_ms"]>;
619
+ readonly bundle_daily_heads: import("@hot-updater/plugin-core").TableDefinition<{
620
+ readonly install_id: {
621
+ readonly type: "string";
622
+ readonly maxLength: 255;
623
+ };
624
+ readonly bucket_start_ms: {
625
+ readonly type: "integer";
626
+ };
627
+ readonly id: {
628
+ readonly type: "string";
629
+ readonly maxLength: 36;
630
+ };
631
+ readonly received_at_ms: {
632
+ readonly type: "integer";
633
+ };
634
+ readonly channel: {
635
+ readonly type: "string";
636
+ };
637
+ readonly platform: {
638
+ readonly type: "string";
639
+ readonly maxLength: 16;
640
+ };
641
+ readonly app_version: {
642
+ readonly type: "string";
643
+ };
644
+ readonly release_id: {
645
+ readonly type: "string";
646
+ readonly maxLength: 36;
647
+ };
648
+ readonly bundle_kind: {
649
+ readonly type: "string";
650
+ readonly maxLength: 16;
651
+ };
652
+ }, Readonly<Record<string, import("@hot-updater/plugin-core").FieldType>>, {}, readonly ["install_id", "bucket_start_ms"]>;
653
+ readonly insights_distribution_history: import("@hot-updater/plugin-core").AggregateDefinition<{
654
+ readonly channel: {
655
+ readonly type: "string";
656
+ };
657
+ readonly platform: {
658
+ readonly type: "string";
659
+ readonly maxLength: 16;
660
+ };
661
+ readonly app_version: {
662
+ readonly type: "string";
663
+ };
664
+ readonly release_id: {
665
+ readonly type: "string";
666
+ readonly maxLength: 36;
667
+ };
668
+ readonly bundle_kind: {
669
+ readonly type: "string";
670
+ readonly maxLength: 16;
671
+ };
672
+ readonly bucket_start_ms: {
673
+ readonly type: "integer";
674
+ };
675
+ }, "installations", {
676
+ readonly byScope: {
677
+ readonly eq: readonly ["channel", "platform"];
678
+ readonly sort: readonly ["bucket_start_ms"];
679
+ };
680
+ }, never, readonly ["channel", "platform", "app_version", "release_id", "bundle_kind", "bucket_start_ms"]>;
591
681
  readonly insights_latest_by_bundle: import("@hot-updater/plugin-core").AggregateDefinition<{
592
682
  readonly platform: {
593
683
  readonly type: "string";
@@ -684,6 +774,7 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
684
774
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
685
775
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
686
776
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
777
+ getDistributionHistory: (input: InsightsGetDistributionHistoryInput) => Promise<InsightsGetDistributionHistoryResult>;
687
778
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
688
779
  getUpdateFailures: (input: InsightsUpdateFailuresInput) => Promise<InsightsUpdateFailures>;
689
780
  };
package/dist/index.d.mts CHANGED
@@ -191,9 +191,13 @@ interface ReleaseActivityMetrics {
191
191
  readonly failedLaunches: number;
192
192
  /** Omitted for lifetime-only bundle-row reads. */
193
193
  readonly uniqueUsers?: number;
194
- /** Daily points, present only for period reads. */
194
+ /**
195
+ * Points of each UTC day, or of each `intervalMs` from the period's start
196
+ * on a release read that asks for one; present only for period reads.
197
+ */
195
198
  readonly series?: readonly {
196
199
  readonly startMs: number;
200
+ readonly downloads: number;
197
201
  readonly launches: number;
198
202
  readonly failedLaunches: number;
199
203
  }[];
@@ -201,10 +205,17 @@ interface ReleaseActivityMetrics {
201
205
  type InsightsGetReleaseActivityInput = {
202
206
  readonly releases: readonly ReleaseReference[];
203
207
  readonly timeRange?: InsightsTimeRange;
208
+ /**
209
+ * Whole hours each series point spans, from `timeRange.start`, with
210
+ * every point present; omitted, the series has a point per UTC day
211
+ * with reports. Only with a `timeRange`.
212
+ */
213
+ readonly intervalMs?: number;
204
214
  readonly scope?: never;
205
215
  } | {
206
216
  readonly scope: InsightsScope;
207
217
  readonly timeRange: InsightsTimeRange;
218
+ readonly intervalMs?: never;
208
219
  readonly releases?: never;
209
220
  };
210
221
  interface InsightsGetReleaseActivityResult {
@@ -247,6 +258,23 @@ interface InsightsGetAppUsageResult {
247
258
  }[];
248
259
  readonly measuredAtMs: number;
249
260
  }
261
+ /** Whole UTC days, at most 31, including the current unfinished day. */
262
+ interface InsightsGetDistributionHistoryInput extends InsightsScope {
263
+ readonly timeRange: InsightsTimeRange;
264
+ }
265
+ interface InsightsGetDistributionHistoryResult {
266
+ readonly coverage: InsightsCoverage;
267
+ readonly measuredAtMs: number;
268
+ readonly points: readonly {
269
+ readonly startMs: number;
270
+ readonly bundles: readonly {
271
+ readonly appVersion: string;
272
+ readonly releaseId: string | null;
273
+ readonly bundleKind: "release" | "builtin" | "unknown";
274
+ readonly installations: number;
275
+ }[];
276
+ }[];
277
+ }
250
278
  //#endregion
251
279
  //#region ../insights/dist/server/schema.d.mts
252
280
  //#region src/server/schema.d.ts
@@ -588,6 +616,68 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
588
616
  readonly sort: readonly ["bucket_start_ms"];
589
617
  };
590
618
  }, never, readonly ["channel", "platform", "app_version", "release_id", "bucket_start_ms"]>;
619
+ readonly bundle_daily_heads: import("@hot-updater/plugin-core").TableDefinition<{
620
+ readonly install_id: {
621
+ readonly type: "string";
622
+ readonly maxLength: 255;
623
+ };
624
+ readonly bucket_start_ms: {
625
+ readonly type: "integer";
626
+ };
627
+ readonly id: {
628
+ readonly type: "string";
629
+ readonly maxLength: 36;
630
+ };
631
+ readonly received_at_ms: {
632
+ readonly type: "integer";
633
+ };
634
+ readonly channel: {
635
+ readonly type: "string";
636
+ };
637
+ readonly platform: {
638
+ readonly type: "string";
639
+ readonly maxLength: 16;
640
+ };
641
+ readonly app_version: {
642
+ readonly type: "string";
643
+ };
644
+ readonly release_id: {
645
+ readonly type: "string";
646
+ readonly maxLength: 36;
647
+ };
648
+ readonly bundle_kind: {
649
+ readonly type: "string";
650
+ readonly maxLength: 16;
651
+ };
652
+ }, Readonly<Record<string, import("@hot-updater/plugin-core").FieldType>>, {}, readonly ["install_id", "bucket_start_ms"]>;
653
+ readonly insights_distribution_history: import("@hot-updater/plugin-core").AggregateDefinition<{
654
+ readonly channel: {
655
+ readonly type: "string";
656
+ };
657
+ readonly platform: {
658
+ readonly type: "string";
659
+ readonly maxLength: 16;
660
+ };
661
+ readonly app_version: {
662
+ readonly type: "string";
663
+ };
664
+ readonly release_id: {
665
+ readonly type: "string";
666
+ readonly maxLength: 36;
667
+ };
668
+ readonly bundle_kind: {
669
+ readonly type: "string";
670
+ readonly maxLength: 16;
671
+ };
672
+ readonly bucket_start_ms: {
673
+ readonly type: "integer";
674
+ };
675
+ }, "installations", {
676
+ readonly byScope: {
677
+ readonly eq: readonly ["channel", "platform"];
678
+ readonly sort: readonly ["bucket_start_ms"];
679
+ };
680
+ }, never, readonly ["channel", "platform", "app_version", "release_id", "bundle_kind", "bucket_start_ms"]>;
591
681
  readonly insights_latest_by_bundle: import("@hot-updater/plugin-core").AggregateDefinition<{
592
682
  readonly platform: {
593
683
  readonly type: "string";
@@ -684,6 +774,7 @@ declare const plugins: readonly [import("@hot-updater/plugin-core").HotUpdaterPl
684
774
  countLatestEvents: (input: InsightsCountLatestEventsInput) => Promise<number>;
685
775
  countEvents: (input: InsightsCountEventsInput) => Promise<number>;
686
776
  getReleaseActivity: (input: InsightsGetReleaseActivityInput) => Promise<InsightsGetReleaseActivityResult>;
777
+ getDistributionHistory: (input: InsightsGetDistributionHistoryInput) => Promise<InsightsGetDistributionHistoryResult>;
687
778
  getAppUsage: (input: InsightsGetAppUsageInput) => Promise<InsightsGetAppUsageResult>;
688
779
  getUpdateFailures: (input: InsightsUpdateFailuresInput) => Promise<InsightsUpdateFailures>;
689
780
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/firebase",
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
  "types": "dist/index.d.cts",
@@ -37,10 +37,10 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "hono": "4.12.34",
40
- "@hot-updater/cli-tools": "1.0.0-rc.29",
41
- "@hot-updater/protocol": "1.0.0-rc.28",
40
+ "@hot-updater/cli-tools": "1.0.0-rc.30",
42
41
  "@hot-updater/plugin-core": "1.0.0-rc.28",
43
- "@hot-updater/server": "1.0.0-rc.29"
42
+ "@hot-updater/server": "1.0.0-rc.30",
43
+ "@hot-updater/protocol": "1.0.0-rc.28"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -55,8 +55,8 @@
55
55
  "firebase-tools": "15.29.0",
56
56
  "fkill": "^9.0.0",
57
57
  "mime": "^4.0.4",
58
- "@hot-updater/mock": "1.0.0-rc.28",
59
- "@hot-updater/test-utils": "1.0.0-rc.28"
58
+ "@hot-updater/test-utils": "1.0.0-rc.29",
59
+ "@hot-updater/mock": "1.0.0-rc.28"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "firebase-admin": "*",