@hot-updater/firebase 1.0.0-rc.35 → 1.0.0-rc.37

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.35";
4527
+ const HOT_UPDATER_SERVER_VERSION = "1.0.0-rc.37";
4528
4528
  //#endregion
4529
4529
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
4530
4530
  /**
@@ -7681,1433 +7681,1560 @@ const apiKeys = (options = {}) => {
7681
7681
  return markOfficial$1(plugin);
7682
7682
  };
7683
7683
  //#endregion
7684
- //#region ../insights/dist/server/eventRow.mjs
7685
- const isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
7686
- const isString = (value) => typeof value === "string";
7687
- const isDatabaseHttpResponse = (value) => isDatabaseJsonObject(value) && (value.resource === "catalog" || value.resource === "artifact") && isString(value.path) && Number.isSafeInteger(value.status) && (value.body === null || isString(value.body)) && typeof value.body_truncated === "boolean" && Number.isSafeInteger(value.received_at_ms);
7688
- /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
7689
- const isDatabaseBundleEventFailure = (value) => isDatabaseJsonObject(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "error_message", isString) && isOptional(value, "error_stack", isString);
7690
- const isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "http_response", isDatabaseHttpResponse) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean");
7691
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7692
- const isIdentity$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
7693
- const isTextOrNull = (value) => value === null || typeof value === "string";
7694
- /** Each bundle event field and what it holds. */
7695
- const BUNDLE_EVENT_FIELDS = {
7696
- id: (value) => typeof value === "string",
7697
- type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UPDATE_FAILED" || value === "UNCHANGED",
7698
- install_id: isIdentity$1,
7699
- user_id: (value) => value === null || isIdentity$1(value),
7700
- from_bundle_id: isTextOrNull,
7701
- from_release_id: isTextOrNull,
7702
- to_release_id: isTextOrNull,
7703
- to_bundle_id: (value) => typeof value === "string",
7704
- platform: (value) => value === "ios" || value === "android",
7705
- app_version: (value) => typeof value === "string",
7706
- channel: (value) => typeof value === "string",
7707
- metadata: isDatabaseBundleEventMetadata,
7708
- received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
7684
+ //#region ../insights/dist/server/schema.mjs
7685
+ const HOUR_MS$1 = 36e5;
7686
+ const DAY_MS$1 = 864e5;
7687
+ const DEFAULT_INSIGHTS_RETENTION = {
7688
+ rawDays: 90,
7689
+ dailyDays: 400
7709
7690
  };
7710
- const updateStrategyOf = (row) => isRecord$2(row.metadata) ? row.metadata.update_strategy : void 0;
7691
+ const bucketRetention = (days) => ({
7692
+ field: "bucket_start_ms",
7693
+ days
7694
+ });
7695
+ /** Every kind of row the table keeps belongs in installation history. */
7696
+ const MOVEMENTS = /* @__PURE__ */ new Set([
7697
+ "UPDATE_DOWNLOADED",
7698
+ "UPDATE_APPLIED",
7699
+ "RECOVERED",
7700
+ "UPDATE_FAILED",
7701
+ "UNCHANGED"
7702
+ ]);
7703
+ /** A failed update check: it names no target bundle, so no bundle list shows it. */
7704
+ const isFailedCheck = (row) => row.type === "UPDATE_FAILED" && row.metadata?.failure?.stage === "check";
7705
+ const metadataOf = (row) => row.metadata ?? {};
7706
+ /** A download or apply that arrived after its target ran: it counts nothing. */
7707
+ const isLateEvent = (row) => metadataOf(row).late === true;
7711
7708
  /**
7712
- * A movement, or a failed update, names the bundle it left and how the
7713
- * device updates, and a failed update says what failed; an unchanged report
7714
- * names neither.
7709
+ * Whether a stored row launched its installation into a bundle release: an
7710
+ * apply, or a kept UNCHANGED row that moved the installation to another
7711
+ * bundle of a release, as when the apply report never arrived or a rollback
7712
+ * to the built-in bundle took it there. A first report, a release change of
7713
+ * the bundle already running, and a late row launch nothing.
7715
7714
  */
7716
- const hasEventInvariants = (row) => (row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED" || row.type === "UPDATE_FAILED") && typeof row.from_bundle_id === "string" && (updateStrategyOf(row) === "fingerprint" || updateStrategyOf(row) === "appVersion") && (row.type !== "UPDATE_FAILED" || isRecord$2(row.metadata) && isRecord$2(row.metadata.failure)) || row.type === "UNCHANGED" && row.from_bundle_id === null && updateStrategyOf(row) === null;
7715
+ const isLaunch = (row) => {
7716
+ if (isLateEvent(row)) return false;
7717
+ if (row.type === "UPDATE_APPLIED") return true;
7718
+ if (row.type !== "UNCHANGED") return false;
7719
+ const kinds = metadataOf(row).change?.kinds ?? [];
7720
+ return kinds.includes("bundle") && !kinds.includes("first_seen") && row.to_release_id !== null;
7721
+ };
7717
7722
  /**
7718
- * Throws unless `row` has exactly the bundle event fields, each of its type,
7719
- * and the invariants of its event type: `invalid-field` for an unknown field,
7720
- * `invalid-data` for anything else.
7723
+ * The bundle a stored row's bundle filter matches: `from:<bundle>` for a
7724
+ * recovery; `to:<bundle>` for a download, an apply, and an update failure;
7725
+ * `on:<bundle>` for an UNCHANGED row that launched a bundle (`isLaunch`), a
7726
+ * key no other row uses. None for a failed check, which targets no bundle, a
7727
+ * late row, or any other UNCHANGED row.
7721
7728
  */
7722
- const validateBundleEventFields = (row) => {
7723
- if (!isRecord$2(row)) throw new DatabaseAdapterInputError("invalid-data");
7724
- for (const field of Object.keys(row)) if (!Object.hasOwn(BUNDLE_EVENT_FIELDS, field)) throw new DatabaseAdapterInputError("invalid-field");
7725
- for (const [field, valid] of Object.entries(BUNDLE_EVENT_FIELDS)) if (!Object.hasOwn(row, field) || !valid(row[field])) throw new DatabaseAdapterInputError("invalid-data");
7726
- if (!hasEventInvariants(row)) throw new DatabaseAdapterInputError("invalid-data");
7727
- };
7728
- //#endregion
7729
- //#region ../insights/dist/server/contract.mjs
7730
- const encoder = new TextEncoder();
7731
- /** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
7732
- const compareInsightsText = (left, right) => {
7733
- const a = encoder.encode(left);
7734
- const b = encoder.encode(right);
7735
- for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
7736
- return a.length - b.length;
7737
- };
7738
- const isWellFormedText = (value) => {
7739
- for (const character of value) {
7740
- const point = character.codePointAt(0);
7741
- if (point >= 55296 && point <= 57343) return false;
7742
- }
7743
- return true;
7744
- };
7745
- const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
7746
- const isText$1 = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
7747
- const isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
7748
- const isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
7749
- const hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
7750
- const hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText$1(value.channel);
7751
- const isBundleFilter = (value, withKind = false) => {
7752
- if (!isRecord$2(value) || !hasScope(value)) return false;
7753
- const keys = [
7754
- "platform",
7755
- "channel",
7756
- "type",
7757
- ...withKind ? ["kind"] : []
7758
- ];
7759
- return value.type === "RECOVERED" ? isText$1(value.fromBundleId) && hasOnlyKeys(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UPDATE_FAILED") && isText$1(value.toBundleId) && hasOnlyKeys(value, [...keys, "toBundleId"]);
7760
- };
7761
- const isEventFilter = (value) => {
7762
- if (!isRecord$2(value)) return false;
7763
- if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
7764
- if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
7765
- return value.kind === "bundle" && isBundleFilter(value, true);
7766
- };
7767
- const validateRow = (model, row, result = false) => {
7768
- try {
7769
- validateBundleEventFields(row);
7770
- if (!isRecord$2(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || [...Object.values(row), ...isRecord$2(row.metadata) ? Object.values(row.metadata) : []].some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabaseAdapterInputError("invalid-data");
7771
- } catch (error) {
7772
- if (result) throw new DatabaseAdapterInputError("invalid-result");
7773
- throw error;
7774
- }
7775
- };
7776
- /** Throws `invalid-data` unless `row` is a complete, well-formed bundle event row. */
7777
- const assertBundleEventRow = (row) => validateRow("bundle_events", row);
7778
- /** Downloads, applies, recoveries, and failed updates belong in installation history. */
7779
- const isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED" || event.type === "UPDATE_FAILED";
7780
- const matchesInsightsEventFilter = (event, filter) => {
7781
- if (filter.kind === "all") return true;
7782
- if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
7783
- return event.type === filter.type && event.platform === filter.platform && event.channel === filter.channel && (filter.type === "RECOVERED" ? event.from_bundle_id === filter.fromBundleId : event.to_bundle_id === filter.toBundleId && event.metadata.failure?.stage !== "check");
7784
- };
7785
- const invalidQuery = () => {
7786
- throw new DatabaseAdapterInputError("invalid-query");
7787
- };
7788
- const invalidResult = () => {
7789
- throw new DatabaseAdapterInputError("invalid-result");
7790
- };
7791
- const validateCount = (count) => isTimestamp(count) ? count : invalidResult();
7792
- const isTimeRange = (value) => isRecord$2(value) && hasOnlyKeys(value, ["start", "end"]) && isTimestamp(value.start) && isTimestamp(value.end) && value.start < value.end;
7793
- const isReleaseReference = (value) => isRecord$2(value) && hasOnlyKeys(value, [
7794
- "releaseId",
7795
- "platform",
7796
- "channel"
7797
- ]) && isIdentity(value.releaseId) && hasScope(value);
7798
- const validateAggregateResult = (value) => {
7799
- const result = isRecord$2(value) ? value : invalidResult();
7800
- if (!isTimestamp(result.measuredAtMs)) invalidResult();
7801
- const coverage = result.coverage;
7802
- if (!isRecord$2(coverage) || coverage.kind !== "complete" && coverage.kind !== "partial" || coverage.sinceMs !== null && !isTimestamp(coverage.sinceMs)) invalidResult();
7803
- };
7804
- /** Validate custom and bundled providers at the same public boundary. */
7805
- const createValidatedInsightsModel = (model) => ({
7806
- async recordEvent(input) {
7807
- if (!isRecord$2(input) || !hasOnlyKeys(input, ["event"])) throw new DatabaseAdapterInputError("invalid-data");
7808
- validateRow("bundle_events", input.event);
7809
- await model.recordEvent(input);
7729
+ const bundleRefsOf = (row) => {
7730
+ if (isFailedCheck(row) || isLateEvent(row)) return [];
7731
+ if (row.type === "RECOVERED") return [`from:${row.from_bundle_id}`];
7732
+ if (row.type === "UNCHANGED") return isLaunch(row) ? [`on:${row.to_bundle_id}`] : [];
7733
+ return [`to:${row.to_bundle_id}`];
7734
+ };
7735
+ /** The key a bundle filter reads, as `bundleRefsOf` writes it. */
7736
+ const bundleRefOfFilter = (filter) => filter.type === "RECOVERED" ? `from:${filter.fromBundleId}` : filter.type === "UNCHANGED" ? `on:${filter.toBundleId}` : `to:${filter.toBundleId}`;
7737
+ /**
7738
+ * Downloads, applies, recoveries, update failures, and the UNCHANGED
7739
+ * reports that changed what their installation runs, whole. An UNCHANGED
7740
+ * report that changes nothing counts as a launch and moves its
7741
+ * installation's head, but no row keeps it.
7742
+ */
7743
+ const bundleEvents = (days) => defineTable({
7744
+ id: {
7745
+ type: "string",
7746
+ maxLength: 36
7810
7747
  },
7811
- async listEvents(input) {
7812
- if (!isRecord$2(input) || !hasOnlyKeys(input, [
7813
- "filter",
7814
- "sinceMs",
7815
- "beforeReceivedAtMs",
7816
- "after",
7817
- "limit"
7818
- ]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
7819
- const sinceMs = input.sinceMs ?? 0;
7820
- if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$2(input.after) || !hasOnlyKeys(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
7821
- const rows = await model.listEvents(input);
7822
- if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
7823
- let previous = input.after;
7824
- for (const row of rows) {
7825
- validateRow("bundle_events", row, true);
7826
- if (row.received_at_ms < sinceMs || row.received_at_ms >= input.beforeReceivedAtMs || !matchesInsightsEventFilter(row, input.filter) || previous !== void 0 && (row.received_at_ms > previous.receivedAtMs || row.received_at_ms === previous.receivedAtMs && row.id >= previous.id)) invalidResult();
7827
- previous = {
7828
- receivedAtMs: row.received_at_ms,
7829
- id: row.id
7830
- };
7831
- }
7832
- return rows;
7748
+ type: {
7749
+ type: "string",
7750
+ maxLength: 32
7833
7751
  },
7834
- async findLatestEvents(input) {
7835
- if (!isRecord$2(input)) invalidQuery();
7836
- if ("installId" in input) {
7837
- if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
7838
- } else if (!hasOnlyKeys(input, [
7839
- "userId",
7840
- "afterInstallId",
7841
- "limit"
7842
- ]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
7843
- const rows = await model.findLatestEvents(input);
7844
- if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
7845
- let previous = "installId" in input ? void 0 : input.afterInstallId;
7846
- for (const row of rows) {
7847
- validateRow("bundle_events", row, true);
7848
- if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
7849
- previous = row.install_id;
7850
- }
7851
- return rows;
7752
+ install_id: {
7753
+ type: "string",
7754
+ maxLength: 255
7852
7755
  },
7853
- async countLatestEvents(input) {
7854
- if (!isRecord$2(input) || !hasOnlyKeys(input, [
7855
- "platform",
7856
- "channel",
7857
- "sinceMs",
7858
- "bundle"
7859
- ]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundle !== void 0 && (!Array.isArray(input.bundle) || input.bundle.length < 1 || input.bundle.length > 2 || input.bundle.some((bundle) => !isRecord$2(bundle) || !hasOnlyKeys(bundle, [
7860
- "field",
7861
- "value",
7862
- "types"
7863
- ]) || bundle.field !== "from_bundle_id" && bundle.field !== "to_bundle_id" || !isText$1(bundle.value) || !Array.isArray(bundle.types) || bundle.types.length === 0 || bundle.types.some((type) => ![
7864
- "UNCHANGED",
7865
- "UPDATE_DOWNLOADED",
7866
- "UPDATE_APPLIED",
7867
- "RECOVERED"
7868
- ].includes(type))))) invalidQuery();
7869
- return validateCount(await model.countLatestEvents(input));
7756
+ user_id: {
7757
+ type: "string",
7758
+ maxLength: 255,
7759
+ required: false
7870
7760
  },
7871
- async countEvents(input) {
7872
- if (!isRecord$2(input) || !hasOnlyKeys(input, [
7873
- "filter",
7874
- "sinceMs",
7875
- "beforeReceivedAtMs"
7876
- ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
7877
- return validateCount(await model.countEvents(input));
7761
+ from_release_id: {
7762
+ type: "string",
7763
+ maxLength: 36,
7764
+ required: false
7878
7765
  },
7879
- async countEventSeries(input) {
7880
- if (!isRecord$2(input) || !hasOnlyKeys(input, [
7881
- "filter",
7882
- "timeRange",
7883
- "intervalMs"
7884
- ]) || !isBundleFilter(input.filter) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs)) invalidQuery();
7885
- const { start, end } = input.timeRange;
7886
- const { intervalMs } = input;
7887
- if (intervalMs === 0 || intervalMs % 36e5 !== 0 || start % 36e5 !== 0 || (end - start) % intervalMs !== 0 || end - start > 7776e6) invalidQuery();
7888
- const points = await model.countEventSeries(input);
7889
- if (!Array.isArray(points) || points.length !== (end - start) / intervalMs || points.some((point, index) => !isRecord$2(point) || !hasOnlyKeys(point, ["startMs", "events"]) || point.startMs !== start + index * intervalMs || !isTimestamp(point.events))) invalidResult();
7890
- return points;
7766
+ from_bundle_id: {
7767
+ type: "string",
7768
+ maxLength: 36,
7769
+ required: false
7891
7770
  },
7892
- async getReleaseActivity(input) {
7893
- if (!isRecord$2(input) || !hasOnlyKeys(input, ["releases"]) || !Array.isArray(input.releases) || input.releases.length < 1 || input.releases.length > 100 || !input.releases.every(isReleaseReference)) invalidQuery();
7894
- const result = await model.getReleaseActivity(input);
7895
- if (!isRecord$2(result) || !isTimestamp(result.measuredAtMs) || !Array.isArray(result.data) || result.data.some((item) => !isRecord$2(item) || !isReleaseReference(item.release) || !isRecord$2(item.metrics) || !isTimestamp(item.metrics.downloads) || !isTimestamp(item.metrics.applies) || !isTimestamp(item.metrics.failedLaunches))) invalidResult();
7896
- return result;
7771
+ to_release_id: {
7772
+ type: "string",
7773
+ maxLength: 36,
7774
+ required: false
7897
7775
  },
7898
- async getAppUsage(input) {
7899
- if (!isRecord$2(input) || !hasOnlyKeys(input, [
7900
- "channel",
7901
- "platform",
7902
- "appVersion",
7903
- "timeRange",
7904
- "intervalMs"
7905
- ]) || !isText$1(input.channel) || ![
7906
- "all",
7907
- "ios",
7908
- "android"
7909
- ].includes(input.platform) || input.appVersion !== void 0 && !isText$1(input.appVersion) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs) || input.intervalMs < 36e5) invalidQuery();
7910
- const result = await model.getAppUsage(input);
7911
- validateAggregateResult(result);
7912
- if (!isTimestamp(result.activeInstallations) || !Array.isArray(result.points) || !Array.isArray(result.appVersions) || !Array.isArray(result.versions) || !Array.isArray(result.platforms) || !Array.isArray(result.bundleDistribution)) invalidResult();
7913
- return result;
7776
+ to_bundle_id: {
7777
+ type: "string",
7778
+ maxLength: 36
7779
+ },
7780
+ platform: {
7781
+ type: "string",
7782
+ maxLength: 16
7783
+ },
7784
+ app_version: { type: "string" },
7785
+ channel: { type: "string" },
7786
+ metadata: { type: "json" },
7787
+ received_at_ms: { type: "integer" }
7788
+ }, {
7789
+ key: ["id"],
7790
+ derived: {
7791
+ /** The UTC day the event was received in. */
7792
+ day: {
7793
+ type: "integer",
7794
+ compute: (row) => row.received_at_ms - row.received_at_ms % DAY_MS$1
7795
+ },
7796
+ /** Every stored event belongs in installation history. */
7797
+ movement_install_id: {
7798
+ type: "string",
7799
+ compute: (row) => MOVEMENTS.has(row.type) ? row.install_id : null
7800
+ },
7801
+ /** The bundle a bundle filter matches (`bundleRefsOf`). */
7802
+ bundle_ref: {
7803
+ type: "string",
7804
+ multi: true,
7805
+ compute: (row) => bundleRefsOf(row)
7806
+ }
7807
+ },
7808
+ indexes: {
7809
+ movementsByInstall: {
7810
+ eq: ["movement_install_id"],
7811
+ sort: ["received_at_ms"]
7812
+ },
7813
+ byBundle: {
7814
+ eq: [
7815
+ "platform",
7816
+ "channel",
7817
+ "type",
7818
+ "bundle_ref",
7819
+ "day"
7820
+ ],
7821
+ sort: ["received_at_ms"]
7822
+ },
7823
+ /** Events of every scope by day, for the console's unfiltered event list. */
7824
+ byDay: {
7825
+ eq: ["day"],
7826
+ sort: ["received_at_ms"]
7827
+ }
7828
+ },
7829
+ retention: {
7830
+ field: "day",
7831
+ days
7914
7832
  }
7915
7833
  });
7916
- //#endregion
7917
- //#region ../insights/dist/server/model.mjs
7918
7834
  /**
7919
- * The plugin's API as `InsightsModel`, validated at its boundary: what the
7920
- * Insights routes, the console, and the e2e harness read through.
7835
+ * Each installation's latest event, whole, so reading it is one point read.
7836
+ * Its gauges count it in the UTC day of that event. An update failure moves
7837
+ * nothing, so no head holds one.
7921
7838
  */
7922
- const createInsightsModel = (api) => createValidatedInsightsModel({
7923
- recordEvent: ({ event }) => api.recordEvent(event),
7924
- listEvents: api.listEvents,
7925
- findLatestEvents: api.findLatestEvents,
7926
- countLatestEvents: api.countLatestEvents,
7927
- countEvents: api.countEvents,
7928
- countEventSeries: api.countEventSeries,
7929
- getReleaseActivity: api.getReleaseActivity,
7930
- getAppUsage: api.getAppUsage
7839
+ const bundleEventHeads = (days) => defineTable({
7840
+ install_id: {
7841
+ type: "string",
7842
+ maxLength: 255
7843
+ },
7844
+ id: {
7845
+ type: "string",
7846
+ maxLength: 36
7847
+ },
7848
+ type: {
7849
+ type: "string",
7850
+ maxLength: 32
7851
+ },
7852
+ user_id: {
7853
+ type: "string",
7854
+ maxLength: 255,
7855
+ required: false
7856
+ },
7857
+ from_release_id: {
7858
+ type: "string",
7859
+ maxLength: 36,
7860
+ required: false
7861
+ },
7862
+ from_bundle_id: {
7863
+ type: "string",
7864
+ maxLength: 36,
7865
+ required: false
7866
+ },
7867
+ to_release_id: {
7868
+ type: "string",
7869
+ maxLength: 36,
7870
+ required: false
7871
+ },
7872
+ to_bundle_id: {
7873
+ type: "string",
7874
+ maxLength: 36
7875
+ },
7876
+ platform: {
7877
+ type: "string",
7878
+ maxLength: 16
7879
+ },
7880
+ app_version: { type: "string" },
7881
+ channel: { type: "string" },
7882
+ metadata: { type: "json" },
7883
+ received_at_ms: { type: "integer" }
7884
+ }, {
7885
+ key: ["install_id"],
7886
+ indexes: { byUser: {
7887
+ eq: ["user_id"],
7888
+ sort: ["install_id"]
7889
+ } },
7890
+ retention: {
7891
+ field: "received_at_ms",
7892
+ days
7893
+ }
7931
7894
  });
7932
- //#endregion
7933
- //#region ../insights/dist/server/official.mjs
7934
- /**
7935
- * The brand of Hot Updater's own plugins, a registered symbol that
7936
- * `@hot-updater/server` reads to reserve the id "insights" for this plugin.
7937
- * Each official plugin package keeps this copy, so none imports another's
7938
- * internals for it.
7939
- */
7940
- const OFFICIAL = Symbol.for("@hot-updater/server/official-plugin");
7941
- /** Marks a plugin one of Hot Updater's own factories made. */
7942
- const markOfficial = (plugin) => Object.defineProperty(plugin, OFFICIAL, { value: true });
7943
- //#endregion
7944
- //#region ../insights/dist/server/errors.mjs
7945
7895
  /**
7946
- * What Insights' API throws for an input it refuses, such as an unknown
7947
- * event field or a malformed cursor; its routes answer 400 for it.
7896
+ * Shards are keyed by install id. Counters are blind increments and never
7897
+ * conflict, so they keep 8. Gauges and sketches are read, merged, and written
7898
+ * back. B3's rollout gate on PostgreSQL retried 2.4% of transactions at 8
7899
+ * shards and 1.3% at 16. DynamoDB's transactions take longer, so D8's gate
7900
+ * on DynamoDB Local retried 2–24% at 16, almost all on gauge rows, and 1–5%
7901
+ * with gauges at 32. Sketches stay at 16: each shard row carries 2 KB of
7902
+ * registers that every read merges.
7903
+ *
7904
+ * Every Insights aggregate is `batched`: on DynamoDB and Firestore, which
7905
+ * bill each write, an event's changes apply after it commits, merged with
7906
+ * other events' into one write per row, on one shard per writer.
7948
7907
  */
7949
- var InsightsBadRequestError = class extends Error {
7950
- name = "InsightsBadRequestError";
7908
+ const COUNTER_SHARDS = 8;
7909
+ const GAUGE_SHARDS = 32;
7910
+ const SKETCH_SHARDS = 16;
7911
+ /** Rows keyed by a hashed identity (scope and period) and a bucket. */
7912
+ const identityFields = {
7913
+ identity: {
7914
+ type: "string",
7915
+ maxLength: 32
7916
+ },
7917
+ bucket_start_ms: { type: "integer" }
7951
7918
  };
7952
- var InsightsPayloadTooLargeError = class extends Error {
7953
- maximumBytes;
7954
- name = "InsightsPayloadTooLargeError";
7955
- constructor(maximumBytes) {
7956
- super(`Event payload exceeds ${maximumBytes} bytes`);
7957
- this.maximumBytes = maximumBytes;
7958
- }
7919
+ const window = {
7920
+ eq: ["identity"],
7921
+ sort: ["bucket_start_ms"]
7959
7922
  };
7960
- //#endregion
7961
- //#region ../insights/dist/server/eventInput.mjs
7962
- const MAX_EVENT_STRING_LENGTH = 1024;
7963
- const MAX_IDENTITY_LENGTH$2 = 255;
7964
- const EVENT_BODY_MAX_BYTES = 16384;
7965
- function isRecord$1(value) {
7966
- return typeof value === "object" && value !== null && !Array.isArray(value);
7967
- }
7968
- function requireStringField(payload, key) {
7969
- const value = payload[key];
7970
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
7971
- return value;
7972
- }
7973
- function requireNullableStringField(payload, key) {
7974
- if (payload[key] === null) return null;
7975
- return requireStringField(payload, key);
7976
- }
7977
- function requireIdentityField(payload, key) {
7978
- const value = requireStringField(payload, key);
7979
- if (value.length > MAX_IDENTITY_LENGTH$2) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
7980
- return value;
7981
- }
7982
- /**
7983
- * The report's idempotency key, when the client sends one: it creates the
7984
- * UUIDv7 once and repeats it on every retry of that report.
7985
- */
7986
- function readEventId(payload) {
7987
- const value = payload.eventId;
7988
- if (value === void 0) return void 0;
7989
- if (!isUUIDv7(value)) throw new InsightsBadRequestError("Invalid event field: eventId");
7990
- return value;
7991
- }
7992
- /** One value of an open set: any other value reads as `unknown`. */
7993
- const oneOf = (values, value) => values.includes(value) ? value : "unknown";
7994
- const STAGES = [
7995
- "check",
7996
- "download",
7997
- "install"
7998
- ];
7999
- const REASONS = [
8000
- "network",
8001
- "http",
8002
- "invalid_response",
8003
- "hash_mismatch",
8004
- "signature",
8005
- "patch",
8006
- "extract",
8007
- "storage"
8008
- ];
8009
- const RESOURCES = [
8010
- "catalog",
8011
- "artifact",
8012
- "manifest",
8013
- "file",
8014
- "patch",
8015
- "archive"
8016
- ];
8017
- const TRANSPORTS = [
8018
- "timeout",
8019
- "dns",
8020
- "tls",
8021
- "connection",
8022
- "offline",
8023
- "cancelled"
8024
- ];
8025
- const DELIVERIES = [
8026
- "patch",
8027
- "manifest",
8028
- "archive"
7923
+ /** What a release or channel row counts in every period kind. */
7924
+ const WINDOW_COUNTERS = [
7925
+ "downloads",
7926
+ "failed_launches",
7927
+ "patch_downloads",
7928
+ "patch_fallbacks"
8029
7929
  ];
8030
- /** A storage error code: letters, digits, and `._-`. */
8031
- const CODE = /^[A-Za-z0-9._-]{1,64}$/;
8032
- const readCode = (value) => typeof value === "string" && CODE.test(value) ? value : void 0;
8033
- const readMetadata = (payload) => isRecord$1(payload.metadata) ? payload.metadata : {};
8034
7930
  /**
8035
- * What an UPDATE_FAILED report says failed. It never refuses the report: an
8036
- * unknown value of a set reads as `unknown`, a missing or malformed failure
8037
- * as an unknown stage and reason, and a malformed optional field is left out,
8038
- * as is a null one.
8039
- */
8040
- function readFailure(value) {
8041
- const failure = isRecord$1(value) ? value : {};
8042
- const httpStatus = failure.httpStatus;
8043
- const originCode = readCode(failure.originCode);
8044
- const errorMessage = typeof failure.errorMessage === "string" && failure.errorMessage.length <= 2048 ? failure.errorMessage : void 0;
8045
- const errorStack = typeof failure.errorStack === "string" && failure.errorStack.length <= 4096 ? failure.errorStack : void 0;
8046
- return {
8047
- stage: oneOf(STAGES, failure.stage),
8048
- reason: oneOf(REASONS, failure.reason),
8049
- ...failure.resource == null ? {} : { resource: oneOf(RESOURCES, failure.resource) },
8050
- ...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
8051
- ...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
8052
- ...originCode === void 0 ? {} : { originCode },
8053
- ...errorMessage === void 0 ? {} : { errorMessage },
8054
- ...errorStack === void 0 ? {} : { errorStack }
8055
- };
8056
- }
8057
- function readHttpResponse(value) {
8058
- if (!isRecord$1(value) || value.resource !== "catalog" && value.resource !== "artifact" || typeof value.path !== "string" || value.path.length > 1024 || !Number.isSafeInteger(value.status) || value.status < 100 || value.status > 599 || value.body !== null && (typeof value.body !== "string" || new TextEncoder().encode(JSON.stringify(value.body)).byteLength > 4096) || typeof value.bodyTruncated !== "boolean" || !Number.isSafeInteger(value.receivedAtMs) || value.receivedAtMs < 0) throw new InsightsBadRequestError("Invalid HTTP response details");
8059
- return {
8060
- resource: value.resource,
8061
- path: value.path,
8062
- status: value.status,
8063
- body: value.body,
8064
- bodyTruncated: value.bodyTruncated,
8065
- receivedAtMs: value.receivedAtMs
8066
- };
8067
- }
8068
- async function readBoundedText(request) {
8069
- const contentLength = request.headers.get("content-length");
8070
- const declaredByteLength = Number(contentLength);
8071
- if (contentLength !== null && Number.isSafeInteger(declaredByteLength) && declaredByteLength > 16384) throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
8072
- if (request.body === null) return "";
8073
- const reader = request.body.getReader();
8074
- const decoder = new TextDecoder();
8075
- let byteLength = 0;
8076
- let text = "";
8077
- while (true) {
8078
- const result = await reader.read();
8079
- if (result.done) break;
8080
- byteLength += result.value.byteLength;
8081
- if (byteLength > 16384) {
8082
- await reader.cancel();
8083
- throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
8084
- }
8085
- text += decoder.decode(result.value, { stream: true });
8086
- }
8087
- return text + decoder.decode();
8088
- }
8089
- async function parseJson(request) {
8090
- const text = await readBoundedText(request);
8091
- try {
8092
- return JSON.parse(text);
8093
- } catch (error) {
8094
- if (error instanceof SyntaxError) throw new InsightsBadRequestError("Invalid event payload");
8095
- throw error;
8096
- }
8097
- }
8098
- function requireEvent(payload) {
8099
- if (!isRecord$1(payload)) throw new InsightsBadRequestError("Invalid event payload");
8100
- const platform = requireStringField(payload, "platform");
8101
- if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
8102
- const eventId = readEventId(payload);
8103
- const metadata = readMetadata(payload);
8104
- const httpMetadata = metadata.httpResponse === void 0 ? {} : { httpResponse: readHttpResponse(metadata.httpResponse) };
8105
- const base = {
8106
- ...metadata.httpResponse === void 0 ? {} : { metadata: httpMetadata },
8107
- ...eventId === void 0 ? {} : { eventId },
8108
- installId: requireIdentityField(payload, "installId"),
8109
- toBundleId: requireStringField(payload, "toBundleId"),
8110
- ...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
8111
- platform,
8112
- appVersion: requireStringField(payload, "appVersion"),
8113
- channel: requireStringField(payload, "channel"),
8114
- cohort: requireStringField(payload, "cohort"),
8115
- fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
8116
- sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
8117
- fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
8118
- toReleaseId: requireNullableStringField(payload, "toReleaseId")
8119
- };
8120
- const type = requireStringField(payload, "type");
8121
- const movement = () => {
8122
- const updateStrategy = requireStringField(payload, "updateStrategy");
8123
- if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
8124
- return {
8125
- ...base,
8126
- fromBundleId: requireStringField(payload, "fromBundleId"),
8127
- updateStrategy
8128
- };
8129
- };
8130
- switch (type) {
8131
- case "UPDATE_DOWNLOADED": {
8132
- const delivery = metadata.delivery;
8133
- const read = {
8134
- ...httpMetadata,
8135
- ...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
8136
- ...metadata.patchFallback === true ? { patchFallback: true } : {}
8137
- };
8138
- return {
8139
- ...movement(),
8140
- type,
8141
- ...Object.keys(read).length === 0 ? {} : { metadata: read }
8142
- };
8143
- }
8144
- case "UPDATE_APPLIED":
8145
- case "RECOVERED": return {
8146
- ...movement(),
8147
- type
8148
- };
8149
- case "UPDATE_FAILED": return {
8150
- ...movement(),
8151
- type,
8152
- metadata: {
8153
- ...httpMetadata,
8154
- failure: readFailure(metadata.failure)
8155
- }
8156
- };
8157
- case "UNCHANGED":
8158
- if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
8159
- return {
8160
- ...base,
8161
- type,
8162
- fromBundleId: null,
8163
- updateStrategy: null
8164
- };
8165
- default: throw new InsightsBadRequestError("Invalid event field: type");
8166
- }
8167
- }
8168
- async function parseBundleEventRequest(request) {
8169
- return requireEvent(await parseJson(request));
8170
- }
8171
- function createBundleEventRow(input) {
8172
- input = requireEvent(input);
8173
- const base = {
8174
- app_version: input.appVersion,
8175
- channel: input.channel,
8176
- from_release_id: input.fromReleaseId,
8177
- id: input.eventId ?? createUUIDv7(),
8178
- install_id: input.installId,
8179
- platform: input.platform,
8180
- received_at_ms: Date.now(),
8181
- to_bundle_id: input.toBundleId,
8182
- to_release_id: input.toReleaseId,
8183
- user_id: input.userId ?? null
8184
- };
8185
- const response = input.metadata?.httpResponse;
8186
- const metadata = {
8187
- ...response ? { http_response: {
8188
- resource: response.resource,
8189
- path: response.path,
8190
- status: response.status,
8191
- body: response.body,
8192
- body_truncated: response.bodyTruncated,
8193
- received_at_ms: response.receivedAtMs
8194
- } } : {},
8195
- cohort: input.cohort,
8196
- fingerprint_hash: input.fingerprintHash,
8197
- sdk_version: input.sdkVersion ?? null,
8198
- update_strategy: input.updateStrategy
8199
- };
8200
- switch (input.type) {
8201
- case "UPDATE_DOWNLOADED": {
8202
- const { delivery, patchFallback } = input.metadata ?? {};
8203
- return {
8204
- ...base,
8205
- from_bundle_id: input.fromBundleId,
8206
- type: input.type,
8207
- metadata: {
8208
- ...metadata,
8209
- ...delivery === void 0 ? {} : { delivery },
8210
- ...patchFallback ? { patch_fallback: true } : {}
8211
- }
8212
- };
8213
- }
8214
- case "UPDATE_APPLIED":
8215
- case "RECOVERED": return {
8216
- ...base,
8217
- from_bundle_id: input.fromBundleId,
8218
- type: input.type,
8219
- metadata
8220
- };
8221
- case "UPDATE_FAILED": {
8222
- const { failure } = input.metadata;
8223
- return {
8224
- ...base,
8225
- from_bundle_id: input.fromBundleId,
8226
- type: input.type,
8227
- metadata: {
8228
- ...metadata,
8229
- failure: {
8230
- stage: failure.stage,
8231
- reason: failure.reason,
8232
- ...failure.errorMessage === void 0 ? {} : { error_message: failure.errorMessage },
8233
- ...failure.errorStack === void 0 ? {} : { error_stack: failure.errorStack },
8234
- ...failure.resource === void 0 ? {} : { resource: failure.resource },
8235
- ...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
8236
- ...failure.transport === void 0 ? {} : { transport: failure.transport },
8237
- ...failure.originCode === void 0 ? {} : { origin_code: failure.originCode }
8238
- }
8239
- }
8240
- };
8241
- }
8242
- case "UNCHANGED": return {
8243
- ...base,
8244
- from_bundle_id: null,
8245
- type: input.type,
8246
- metadata
8247
- };
8248
- }
8249
- }
8250
- //#endregion
8251
- //#region ../insights/dist/server/provider.mjs
8252
- const DEFAULT_PAGE_LIMIT = 50;
8253
- const MAX_PAGE_LIMIT$1 = 100;
8254
- const MAX_EVENT_ID_LENGTH = 1024;
8255
- const MAX_IDENTITY_LENGTH$1 = 255;
8256
- const MAX_CURSOR_LENGTH$1 = 8192;
8257
- const HOUR_MS$1 = 36e5;
8258
- const DAY_MS$1 = 24 * HOUR_MS$1;
8259
- const WINDOW_MS = {
8260
- "24h": 864e5,
8261
- "7d": 6048e5,
8262
- "30d": 2592e6
8263
- };
7931
+ * Hourly or daily counters: a release or channel row counts downloads,
7932
+ * launches that crashed and recovered (`failed_launches`), and downloads a
7933
+ * patch delivered and those that fell back from one.
7934
+ */
7935
+ const counters = (retention) => defineAggregate(identityFields, {
7936
+ key: ["identity", "bucket_start_ms"],
7937
+ counters: WINDOW_COUNTERS,
7938
+ shards: COUNTER_SHARDS,
7939
+ batched: true,
7940
+ indexes: { window },
7941
+ retention
7942
+ });
8264
7943
  /**
8265
- * The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
8266
- * bundle event list, and the range one covers without `sinceMs`: raw events
8267
- * are listed inside a time range, as analytics products list them. It is a
8268
- * duration, so a range that ends mid-day touches 91 UTC days. Installation
8269
- * history is one index range and reads back to `sinceMs`.
7944
+ * A release's counters since its first report, at bucket 0: the windowed
7945
+ * counters, its applies, which the bundle list reads, and its failed updates
7946
+ * (`failed_updates`), since no breakdown row outlives the raw period; windowed
7947
+ * reads sum `insights_failures` instead.
8270
7948
  */
8271
- const EVENT_LIST_RANGE_MS = 2160 * HOUR_MS$1;
8272
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8273
- const requireString = (value, label, maximumLength) => {
8274
- if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid ${label}.`);
8275
- return value;
8276
- };
8277
- const requireTimestamp = (value, label) => {
8278
- if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
8279
- return Number(value);
8280
- };
8281
- const readLimit = (value) => {
8282
- const limit = value ?? DEFAULT_PAGE_LIMIT;
8283
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT$1) throw new InsightsBadRequestError("Invalid page limit.");
8284
- return limit;
8285
- };
8286
- const encodeBase64Url = (value) => {
8287
- const bytes = new TextEncoder().encode(value);
8288
- let binary = "";
8289
- for (const byte of bytes) binary += String.fromCharCode(byte);
8290
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
8291
- };
8292
- const decodeBase64Url = (value) => {
8293
- if (value.length === 0 || value.length > MAX_CURSOR_LENGTH$1) throw new InsightsBadRequestError("Invalid Insights cursor.");
8294
- try {
8295
- const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
8296
- const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
8297
- const binary = atob(padded);
8298
- const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
8299
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
8300
- } catch {
8301
- throw new InsightsBadRequestError("Invalid Insights cursor.");
8302
- }
8303
- };
8304
- const encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
8305
- const decodeCursor = (value) => {
8306
- try {
8307
- return JSON.parse(decodeBase64Url(value));
8308
- } catch (error) {
8309
- if (error instanceof InsightsBadRequestError) throw error;
8310
- throw new InsightsBadRequestError("Invalid Insights cursor.");
8311
- }
8312
- };
8313
- const readScope$1 = (input) => {
8314
- if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
8315
- return {
8316
- platform: input.platform,
8317
- channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
8318
- };
8319
- };
8320
- const bundleFilter = (input) => {
8321
- const scope = readScope$1(input);
8322
- const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
8323
- switch (input.outcome) {
8324
- case "downloaded": return {
8325
- ...scope,
8326
- type: "UPDATE_DOWNLOADED",
8327
- toBundleId: bundleId
8328
- };
8329
- case "applied": return {
8330
- ...scope,
8331
- type: "UPDATE_APPLIED",
8332
- toBundleId: bundleId
8333
- };
8334
- case "recovered": return {
8335
- ...scope,
8336
- type: "RECOVERED",
8337
- fromBundleId: bundleId
8338
- };
8339
- case "failed": return {
8340
- ...scope,
8341
- type: "UPDATE_FAILED",
8342
- toBundleId: bundleId
8343
- };
8344
- default: throw new InsightsBadRequestError("Invalid Insights outcome.");
8345
- }
8346
- };
8347
- const sameFilter = (left, right) => {
8348
- if (!isRecord(left) || left.kind !== right.kind) return false;
8349
- switch (right.kind) {
8350
- case "all": return true;
8351
- case "installationMovement": return left.installId === right.installId;
8352
- case "bundle": return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
8353
- }
8354
- };
8355
- const readEventCursor = (value, filter) => {
8356
- const cursor = decodeCursor(value);
8357
- if (!isRecord(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord(cursor.filter) || !isRecord(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
8358
- if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
8359
- if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
8360
- return {
8361
- after: {
8362
- id: cursor.after.id,
8363
- receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
8364
- },
8365
- beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
8366
- kind: "events",
8367
- sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
8368
- filter,
8369
- version: 2
8370
- };
8371
- };
8372
- const readUserInstallationCursor = (value, userId) => {
8373
- const cursor = decodeCursor(value);
8374
- if (!isRecord(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
8375
- return {
8376
- afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH$1),
8377
- kind: "user-installations",
8378
- userId,
8379
- version: 1
8380
- };
8381
- };
8382
- const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
8383
- const isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
8384
- const assertEventRows = (rows, input) => {
8385
- if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
8386
- for (let index = 0; index < rows.length; index += 1) {
8387
- const row = rows[index];
8388
- const previous = rows[index - 1];
8389
- if (!row || typeof row.id !== "string" || !isUUIDv7(row.id) || !Number.isSafeInteger(row.received_at_ms) || row.received_at_ms >= input.beforeReceivedAtMs || row.received_at_ms < input.sinceMs || previous !== void 0 && compareEventNewest(previous, row) >= 0 || input.after !== void 0 && !isAfterEventCursor(row, input.after) || input.filter.kind === "installationMovement" && (row.install_id !== input.filter.installId || !isInsightsMovementEvent(row)) || input.filter.kind === "bundle" && (row.platform !== input.filter.platform || row.channel !== input.filter.channel || row.type !== input.filter.type || (input.filter.type === "RECOVERED" ? row.from_bundle_id !== input.filter.fromBundleId : row.to_bundle_id !== input.filter.toBundleId))) throw new Error("Insights database returned invalid event rows.");
8390
- }
8391
- };
8392
- /** An `UPDATE_FAILED` row's failure, as the event lists show it. */
8393
- const toFailure = (failure) => ({
8394
- stage: failure.stage,
8395
- reason: failure.reason,
8396
- ...failure.error_message === void 0 ? {} : { errorMessage: failure.error_message },
8397
- ...failure.error_stack === void 0 ? {} : { errorStack: failure.error_stack },
8398
- ...failure.resource === void 0 ? {} : { resource: failure.resource },
8399
- ...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
8400
- ...failure.transport === void 0 ? {} : { transport: failure.transport },
8401
- ...failure.origin_code === void 0 ? {} : { originCode: failure.origin_code }
7949
+ const lifetimeCounters = () => defineAggregate(identityFields, {
7950
+ key: ["identity", "bucket_start_ms"],
7951
+ counters: [
7952
+ ...WINDOW_COUNTERS,
7953
+ "applies",
7954
+ "failed_updates"
7955
+ ],
7956
+ shards: COUNTER_SHARDS,
7957
+ batched: true,
7958
+ indexes: { window }
8402
7959
  });
8403
- /** What a row's metadata adds for its type: a failure or a delivery. */
8404
- const eventDetails = ({ type, metadata }) => ({
8405
- ...metadata.http_response !== void 0 ? { httpResponse: {
8406
- resource: metadata.http_response.resource,
8407
- path: metadata.http_response.path,
8408
- status: metadata.http_response.status,
8409
- body: metadata.http_response.body,
8410
- bodyTruncated: metadata.http_response.body_truncated,
8411
- receivedAtMs: metadata.http_response.received_at_ms
8412
- } } : {},
8413
- ...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
8414
- ...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
8415
- ...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {}
7960
+ /**
7961
+ * Unique-installation sketches of one period kind: a usage row's active
7962
+ * installations (`activity_users`), and a `failure` or `check` row's
7963
+ * installations whose update or update check failed (`failed_users`).
7964
+ */
7965
+ const sketches = (retention) => defineAggregate(identityFields, {
7966
+ key: ["identity", "bucket_start_ms"],
7967
+ distinct: ["activity_users", "failed_users"],
7968
+ shards: SKETCH_SHARDS,
7969
+ batched: true,
7970
+ indexes: { window },
7971
+ retention
8416
7972
  });
8417
- const toEventHistoryRow = (row) => ({
8418
- ...row.to_release_id == null ? {} : { toReleaseId: row.to_release_id },
8419
- ...row.metadata.sdk_version == null ? {} : { sdkVersion: row.metadata.sdk_version },
8420
- appVersion: row.app_version,
8421
- channel: row.channel,
8422
- cohort: row.metadata.cohort,
8423
- fromBundleId: row.from_bundle_id,
8424
- id: row.id,
8425
- installId: row.install_id,
8426
- platform: row.platform,
8427
- receivedAtMs: row.received_at_ms,
8428
- toBundleId: row.to_bundle_id,
8429
- type: row.type,
8430
- userId: row.user_id,
8431
- ...eventDetails(row)
7973
+ /** Installations whose update to a release failed, since its first failure: kept, at bucket 0. */
7974
+ const lifetimeSketches = () => defineAggregate(identityFields, {
7975
+ key: ["identity", "bucket_start_ms"],
7976
+ distinct: ["failed_users"],
7977
+ shards: SKETCH_SHARDS,
7978
+ batched: true,
7979
+ indexes: { window }
8432
7980
  });
8433
- const toInstallationRow = (row) => ({
8434
- ...eventDetails(row).httpResponse ? { httpResponse: eventDetails(row).httpResponse } : {},
8435
- appVersion: row.app_version,
8436
- channel: row.channel,
8437
- cohort: row.metadata.cohort,
8438
- installId: row.install_id,
8439
- lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
8440
- pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
8441
- pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
8442
- latestStatus: row.type,
8443
- platform: row.platform,
8444
- receivedAtMs: row.received_at_ms,
8445
- userId: row.user_id
7981
+ /**
7982
+ * Update failures by what failed, a row an hour. `release_id` is the failed
7983
+ * update's target release, or empty for a failed check or an unknown target.
7984
+ * `stage` and `reason` say where and why it failed, and `detail` what else
7985
+ * the client knew, as the JSON array `[resource, httpStatus, originCode,
7986
+ * transport]`. Every index is in key order, so its counters stay blind
7987
+ * increments.
7988
+ */
7989
+ const insightsFailures = (retention) => defineAggregate({
7990
+ platform: {
7991
+ type: "string",
7992
+ maxLength: 16
7993
+ },
7994
+ channel: { type: "string" },
7995
+ bucket_start_ms: { type: "integer" },
7996
+ release_id: {
7997
+ type: "string",
7998
+ maxLength: 36
7999
+ },
8000
+ stage: {
8001
+ type: "string",
8002
+ maxLength: 16
8003
+ },
8004
+ reason: {
8005
+ type: "string",
8006
+ maxLength: 64
8007
+ },
8008
+ detail: {
8009
+ type: "string",
8010
+ maxLength: 160
8011
+ }
8012
+ }, {
8013
+ key: [
8014
+ "platform",
8015
+ "channel",
8016
+ "bucket_start_ms",
8017
+ "release_id",
8018
+ "stage",
8019
+ "reason",
8020
+ "detail"
8021
+ ],
8022
+ counters: ["events"],
8023
+ shards: COUNTER_SHARDS,
8024
+ batched: true,
8025
+ indexes: { byScope: {
8026
+ eq: ["platform", "channel"],
8027
+ sort: ["bucket_start_ms"]
8028
+ } },
8029
+ retention
8030
+ });
8031
+ /** Installations counted in the UTC day of their latest event, so reads sum whole UTC days. */
8032
+ const insightsDistribution = (retention) => defineAggregate({
8033
+ channel: { type: "string" },
8034
+ platform: {
8035
+ type: "string",
8036
+ maxLength: 16
8037
+ },
8038
+ app_version: { type: "string" },
8039
+ release_id: {
8040
+ type: "string",
8041
+ maxLength: 36
8042
+ },
8043
+ bucket_start_ms: { type: "integer" }
8044
+ }, {
8045
+ key: [
8046
+ "channel",
8047
+ "platform",
8048
+ "app_version",
8049
+ "release_id",
8050
+ "bucket_start_ms"
8051
+ ],
8052
+ gauges: ["latest_installations"],
8053
+ shards: GAUGE_SHARDS,
8054
+ batched: true,
8055
+ indexes: {
8056
+ byScope: {
8057
+ eq: ["channel", "platform"],
8058
+ sort: ["bucket_start_ms"]
8059
+ },
8060
+ byVersion: {
8061
+ eq: [
8062
+ "channel",
8063
+ "platform",
8064
+ "app_version"
8065
+ ],
8066
+ sort: ["bucket_start_ms"]
8067
+ }
8068
+ },
8069
+ retention
8446
8070
  });
8447
8071
  /**
8448
- * Where a list without `sinceMs` starts: 90 days before its cutoff for the
8449
- * global and bundle lists, at the first event for installation history.
8072
+ * The installations of `insights_distribution` that run their native build's
8073
+ * built-in bundle, by its ID (`builtin_bundle_id`), within the same rows:
8074
+ * reads move them out of the row of their release, or of no release.
8450
8075
  */
8451
- const defaultEventStart = (filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
8452
- /** Whether a cursor's row lies inside the list's range. */
8453
- const isInsideBounds = ({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs;
8454
- /** A page's range: its cursor's, which the request may repeat but not change. */
8455
- const readEventBounds = (input, cursor, filter) => {
8456
- const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
8457
- if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
8458
- const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? defaultEventStart(filter, beforeReceivedAtMs) : requireTimestamp(input.sinceMs, "event start"));
8459
- const bounds = {
8460
- beforeReceivedAtMs,
8461
- sinceMs
8462
- };
8463
- if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && !isInsideBounds(cursor, bounds)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
8464
- if (filter.kind !== "installationMovement" && beforeReceivedAtMs - sinceMs > 7776e6) throw new InsightsBadRequestError("Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.");
8465
- return bounds;
8466
- };
8467
- const pageEventRows = async (model, input, filter, map) => {
8468
- const limit = readLimit(input.limit);
8469
- const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
8470
- const bounds = readEventBounds(input, cursor, filter);
8471
- const query = {
8472
- filter,
8473
- ...bounds,
8474
- ...cursor === void 0 ? {} : { after: cursor.after },
8475
- limit
8476
- };
8477
- const rows = await model.listEvents(query);
8478
- assertEventRows(rows, query);
8479
- const last = rows.at(-1);
8480
- return {
8481
- beforeReceivedAtMs: bounds.beforeReceivedAtMs,
8482
- data: rows.map(map),
8483
- nextCursor: rows.length === limit && last ? encodeCursor({
8484
- after: {
8485
- id: last.id,
8486
- receivedAtMs: last.received_at_ms
8487
- },
8488
- ...bounds,
8489
- kind: "events",
8490
- filter,
8491
- version: 2
8492
- }) : null
8493
- };
8494
- };
8495
- const assertInstallationRows = (rows, input) => {
8496
- if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
8497
- for (let index = 0; index < rows.length; index += 1) {
8498
- const row = rows[index];
8499
- const previous = rows[index - 1];
8500
- if (!row || row.user_id !== input.userId || input.afterInstallId !== void 0 && compareInsightsText(row.install_id, input.afterInstallId) <= 0 || previous !== void 0 && compareInsightsText(previous.install_id, row.install_id) >= 0) throw new Error("Insights database returned invalid installation rows.");
8501
- }
8502
- };
8503
- const createInsightsProvider = (model) => Object.freeze({
8504
- async appendBundleEvent(input) {
8505
- const event = createBundleEventRow(input);
8506
- await model.recordEvent({ event });
8507
- },
8508
- listEvents(input) {
8509
- const filter = input.bundle === void 0 ? { kind: "all" } : {
8510
- kind: "bundle",
8511
- ...bundleFilter(input.bundle)
8512
- };
8513
- return pageEventRows(model, input, filter, toEventHistoryRow);
8076
+ const insightsBuiltinDistribution = (retention) => defineAggregate({
8077
+ channel: { type: "string" },
8078
+ platform: {
8079
+ type: "string",
8080
+ maxLength: 16
8514
8081
  },
8515
- async listInstallationEvents(input) {
8516
- if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
8517
- const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH$1);
8518
- return pageEventRows(model, input, {
8519
- kind: "installationMovement",
8520
- installId
8521
- }, (row) => toEventHistoryRow(row));
8082
+ app_version: { type: "string" },
8083
+ release_id: {
8084
+ type: "string",
8085
+ maxLength: 36
8522
8086
  },
8523
- async getInstallation({ installId }) {
8524
- const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH$1);
8525
- const rows = await model.findLatestEvents({ installId: normalizedInstallId });
8526
- const row = rows[0] ?? null;
8527
- if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
8528
- return row === null ? null : toInstallationRow(row);
8087
+ builtin_bundle_id: {
8088
+ type: "string",
8089
+ maxLength: 36
8529
8090
  },
8530
- async pageInstallationsByCurrentUserId(input) {
8531
- const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH$1);
8532
- const limit = readLimit(input.limit);
8533
- const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
8534
- const databaseInput = {
8535
- userId,
8536
- ...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
8537
- limit
8538
- };
8539
- const rows = await model.findLatestEvents(databaseInput);
8540
- assertInstallationRows(rows, databaseInput);
8541
- const last = rows.at(-1);
8542
- return {
8543
- data: rows.map(toInstallationRow),
8544
- nextCursor: rows.length === limit && last ? encodeCursor({
8545
- afterInstallId: last.install_id,
8546
- kind: "user-installations",
8547
- userId,
8548
- version: 1
8549
- }) : null
8550
- };
8091
+ bucket_start_ms: { type: "integer" }
8092
+ }, {
8093
+ key: [
8094
+ "channel",
8095
+ "platform",
8096
+ "app_version",
8097
+ "release_id",
8098
+ "builtin_bundle_id",
8099
+ "bucket_start_ms"
8100
+ ],
8101
+ gauges: ["latest_installations"],
8102
+ shards: GAUGE_SHARDS,
8103
+ batched: true,
8104
+ indexes: {
8105
+ byScope: {
8106
+ eq: ["channel", "platform"],
8107
+ sort: ["bucket_start_ms"]
8108
+ },
8109
+ byVersion: {
8110
+ eq: [
8111
+ "channel",
8112
+ "platform",
8113
+ "app_version"
8114
+ ],
8115
+ sort: ["bucket_start_ms"]
8116
+ }
8551
8117
  },
8552
- async getReportingOverview(input) {
8553
- const scope = readScope$1(input);
8554
- const { window } = input;
8555
- if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
8556
- const now = Date.now();
8557
- const beforeReceivedAtMs = now - now % HOUR_MS$1 + (now % HOUR_MS$1 === 0 ? 0 : HOUR_MS$1);
8558
- const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
8559
- const sinceMs = reach - reach % DAY_MS$1;
8560
- const measure = async (count) => {
8561
- const value = await count;
8562
- if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
8563
- return {
8564
- count: value,
8565
- measuredAtMs: Date.now()
8566
- };
8567
- };
8568
- const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
8569
- const reporting = measure(model.countLatestEvents({
8570
- ...scope,
8571
- sinceMs
8572
- }));
8573
- if (bundleId === void 0) return {
8574
- ...scope,
8575
- window,
8576
- sinceMs,
8577
- beforeReceivedAtMs,
8578
- reportingInstallations: await reporting
8579
- };
8580
- const countOutcome = (outcome) => measure(model.countEvents({
8581
- filter: bundleFilter({
8582
- ...scope,
8583
- bundleId,
8584
- outcome
8585
- }),
8586
- sinceMs,
8587
- beforeReceivedAtMs
8588
- }));
8589
- const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, failedReports] = await Promise.all([
8590
- reporting,
8591
- measure(model.countLatestEvents({
8592
- ...scope,
8593
- sinceMs,
8594
- bundle: [{
8595
- field: "from_bundle_id",
8596
- value: bundleId,
8597
- types: ["UPDATE_DOWNLOADED"]
8598
- }, {
8599
- field: "to_bundle_id",
8600
- value: bundleId,
8601
- types: [
8602
- "UNCHANGED",
8603
- "UPDATE_APPLIED",
8604
- "RECOVERED"
8605
- ]
8606
- }]
8607
- })),
8608
- countOutcome("downloaded"),
8609
- countOutcome("applied"),
8610
- countOutcome("recovered"),
8611
- countOutcome("failed")
8612
- ]);
8613
- return {
8614
- ...scope,
8615
- window,
8616
- sinceMs,
8617
- beforeReceivedAtMs,
8618
- reportingInstallations,
8619
- bundle: {
8620
- bundleId,
8621
- reportingInstallations: bundleInstallations,
8622
- downloadedReports,
8623
- appliedReports,
8624
- recoveredReports,
8625
- failedReports
8626
- }
8627
- };
8628
- }
8118
+ retention
8629
8119
  });
8630
- //#endregion
8631
- //#region ../insights/dist/server/schema.mjs
8632
- const HOUR_MS = 36e5;
8633
- const DAY_MS = 864e5;
8634
- const DEFAULT_INSIGHTS_RETENTION = {
8635
- rawDays: 90,
8636
- dailyDays: 400
8637
- };
8638
- const bucketRetention = (days) => ({
8639
- field: "bucket_start_ms",
8640
- days
8120
+ /** Latest events by the bundle they came from or went to, bucketed like the distribution. */
8121
+ const insightsLatestByBundle = (retention) => defineAggregate({
8122
+ platform: {
8123
+ type: "string",
8124
+ maxLength: 16
8125
+ },
8126
+ channel: { type: "string" },
8127
+ bundle_field: {
8128
+ type: "string",
8129
+ maxLength: 16
8130
+ },
8131
+ bundle_id: {
8132
+ type: "string",
8133
+ maxLength: 36
8134
+ },
8135
+ type: {
8136
+ type: "string",
8137
+ maxLength: 32
8138
+ },
8139
+ bucket_start_ms: { type: "integer" }
8140
+ }, {
8141
+ key: [
8142
+ "platform",
8143
+ "channel",
8144
+ "bundle_field",
8145
+ "bundle_id",
8146
+ "type",
8147
+ "bucket_start_ms"
8148
+ ],
8149
+ gauges: ["installations"],
8150
+ shards: GAUGE_SHARDS,
8151
+ batched: true,
8152
+ indexes: { byBundle: {
8153
+ eq: [
8154
+ "platform",
8155
+ "channel",
8156
+ "bundle_field",
8157
+ "bundle_id",
8158
+ "type"
8159
+ ],
8160
+ sort: ["bucket_start_ms"]
8161
+ } },
8162
+ retention
8641
8163
  });
8642
- const MOVEMENTS = /* @__PURE__ */ new Set([
8643
- "UPDATE_DOWNLOADED",
8644
- "UPDATE_APPLIED",
8645
- "RECOVERED",
8646
- "UPDATE_FAILED"
8647
- ]);
8648
- /** A failed update check: it names no target bundle, so no bundle list shows it. */
8649
- const isFailedCheck = (row) => row.type === "UPDATE_FAILED" && row.metadata?.failure?.stage === "check";
8650
8164
  /**
8651
- * Downloads, applies, recoveries, and update failures, whole. An UNCHANGED
8652
- * report counts as a launch and moves its installation's head, but no list
8653
- * shows it, so no row keeps it.
8165
+ * Stored events by outcome and the bundle a bundle filter matches, a row an
8166
+ * hour, which a failed check has none of; and every stored event, a row a UTC
8167
+ * day (`DAILY_EVENTS`). Bundle counts sum the hourly rows, and event lists
8168
+ * read either to skip days without events.
8654
8169
  */
8655
- const bundleEvents = (days) => defineTable({
8656
- id: {
8170
+ const insightsOutcomes = (retention) => defineAggregate({
8171
+ platform: {
8657
8172
  type: "string",
8658
- maxLength: 36
8173
+ maxLength: 16
8659
8174
  },
8175
+ channel: { type: "string" },
8660
8176
  type: {
8661
8177
  type: "string",
8662
8178
  maxLength: 32
8663
8179
  },
8664
- install_id: {
8180
+ bundle_ref: {
8665
8181
  type: "string",
8666
- maxLength: 255
8182
+ maxLength: 41
8667
8183
  },
8668
- user_id: {
8669
- type: "string",
8670
- maxLength: 255,
8671
- required: false
8184
+ bucket_start_ms: { type: "integer" }
8185
+ }, {
8186
+ key: [
8187
+ "platform",
8188
+ "channel",
8189
+ "type",
8190
+ "bundle_ref",
8191
+ "bucket_start_ms"
8192
+ ],
8193
+ counters: ["events"],
8194
+ shards: COUNTER_SHARDS,
8195
+ batched: true,
8196
+ indexes: { byRef: {
8197
+ eq: [
8198
+ "platform",
8199
+ "channel",
8200
+ "type",
8201
+ "bundle_ref"
8202
+ ],
8203
+ sort: ["bucket_start_ms"]
8204
+ } },
8205
+ retention
8206
+ });
8207
+ /**
8208
+ * The `insights_outcomes` identity that counts every stored event, for the
8209
+ * global event list. Its bucket is a UTC day, not an hour. Every event's
8210
+ * platform is ios or android, so no outcome row of a bundle filter shares it.
8211
+ */
8212
+ const DAILY_EVENTS = {
8213
+ platform: "*",
8214
+ channel: "",
8215
+ type: "",
8216
+ bundle_ref: ""
8217
+ };
8218
+ /**
8219
+ * Insights' tables, each expiring after its period. The periods are values
8220
+ * the engine applies at runtime; the tables and indexes are the same for
8221
+ * any periods.
8222
+ */
8223
+ const createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTION) => {
8224
+ const hourly = bucketRetention(rawDays);
8225
+ const daily = bucketRetention(dailyDays);
8226
+ return {
8227
+ bundle_events: bundleEvents(rawDays),
8228
+ bundle_event_heads: bundleEventHeads(dailyDays),
8229
+ /** Hourly counters and sketches: the raw period. */
8230
+ insights_overview: counters(hourly),
8231
+ insights_sketches: sketches(hourly),
8232
+ /** Daily counters and sketches: the daily period. */
8233
+ insights_overview_daily: counters(daily),
8234
+ insights_sketches_daily: sketches(daily),
8235
+ /** A release's counters since its first event, and its failed installations: kept. */
8236
+ insights_overview_lifetime: lifetimeCounters(),
8237
+ insights_sketches_lifetime: lifetimeSketches(),
8238
+ insights_distribution: insightsDistribution(daily),
8239
+ insights_builtin_distribution: insightsBuiltinDistribution(daily),
8240
+ insights_latest_by_bundle: insightsLatestByBundle(daily),
8241
+ insights_outcomes: insightsOutcomes(hourly),
8242
+ insights_failures: insightsFailures(hourly)
8243
+ };
8244
+ };
8245
+ createInsightsSchema();
8246
+ //#endregion
8247
+ //#region ../insights/dist/server/eventRow.mjs
8248
+ const CHANGE_KINDS = /* @__PURE__ */ new Set([
8249
+ "first_seen",
8250
+ "bundle",
8251
+ "release",
8252
+ "app_version",
8253
+ "channel",
8254
+ "native_build"
8255
+ ]);
8256
+ const isBundleEventChange = (value) => isDatabaseJsonObject(value) && Array.isArray(value.kinds) && value.kinds.length > 0 && value.kinds.every((kind) => CHANGE_KINDS.has(kind)) && (value.previous === null || isDatabaseJsonObject(value.previous) && typeof value.previous.bundle_id === "string" && (value.previous.release_id === null || typeof value.previous.release_id === "string") && typeof value.previous.app_version === "string" && typeof value.previous.channel === "string" && isOptional(value.previous, "min_bundle_id", isString));
8257
+ const isOptional = (value, key, valid) => !Object.hasOwn(value, key) || valid(value[key]);
8258
+ const isString = (value) => typeof value === "string";
8259
+ const isDatabaseHttpResponse = (value) => isDatabaseJsonObject(value) && (value.resource === "catalog" || value.resource === "artifact") && isString(value.path) && Number.isSafeInteger(value.status) && (value.body === null || isString(value.body)) && typeof value.body_truncated === "boolean" && Number.isSafeInteger(value.received_at_ms);
8260
+ /** An `UPDATE_FAILED` report's stored failure: a stage and a reason, and what else the client knew. */
8261
+ const isDatabaseBundleEventFailure = (value) => isDatabaseJsonObject(value) && isString(value.stage) && isString(value.reason) && isOptional(value, "resource", isString) && isOptional(value, "http_status", (status) => Number.isSafeInteger(status)) && isOptional(value, "transport", isString) && isOptional(value, "origin_code", isString) && isOptional(value, "error_message", isString) && isOptional(value, "error_stack", isString);
8262
+ const isDatabaseBundleEventMetadata = (value) => isDatabaseJsonObject(value) && typeof value.cohort === "string" && (value.update_strategy === null || value.update_strategy === "fingerprint" || value.update_strategy === "appVersion") && (value.fingerprint_hash === null || typeof value.fingerprint_hash === "string") && (value.sdk_version === null || typeof value.sdk_version === "string") && isOptional(value, "min_bundle_id", isString) && isOptional(value, "failure", isDatabaseBundleEventFailure) && isOptional(value, "http_response", isDatabaseHttpResponse) && isOptional(value, "delivery", isString) && isOptional(value, "patch_fallback", (flag) => typeof flag === "boolean") && isOptional(value, "change", isBundleEventChange) && isOptional(value, "late", (flag) => flag === true) && isOptional(value, "implied_download", (flag) => flag === true);
8263
+ const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8264
+ /**
8265
+ * The built-in bundle a report says its installation runs: the running
8266
+ * bundle, which a download leaves at its source, when it is the native
8267
+ * build's built-in bundle, whatever Release selected it. Null for any other
8268
+ * bundle, and for a report without the build's built-in bundle ID.
8269
+ */
8270
+ const runningBuiltinBundleId = (row) => {
8271
+ const running = row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id;
8272
+ const builtin = isRecord$2(row.metadata) ? row.metadata.min_bundle_id : null;
8273
+ return typeof builtin === "string" && running === builtin ? builtin : null;
8274
+ };
8275
+ const isIdentity$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
8276
+ const isTextOrNull = (value) => value === null || typeof value === "string";
8277
+ /** Each bundle event field and what it holds. */
8278
+ const BUNDLE_EVENT_FIELDS = {
8279
+ id: (value) => typeof value === "string",
8280
+ type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UPDATE_FAILED" || value === "UNCHANGED",
8281
+ install_id: isIdentity$1,
8282
+ user_id: (value) => value === null || isIdentity$1(value),
8283
+ from_bundle_id: isTextOrNull,
8284
+ from_release_id: isTextOrNull,
8285
+ to_release_id: isTextOrNull,
8286
+ to_bundle_id: (value) => typeof value === "string",
8287
+ platform: (value) => value === "ios" || value === "android",
8288
+ app_version: (value) => typeof value === "string",
8289
+ channel: (value) => typeof value === "string",
8290
+ metadata: isDatabaseBundleEventMetadata,
8291
+ received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
8292
+ };
8293
+ const hasMetadata = (row, key) => isRecord$2(row.metadata) && Object.hasOwn(row.metadata, key);
8294
+ const updateStrategyOf = (row) => isRecord$2(row.metadata) ? row.metadata.update_strategy : void 0;
8295
+ /**
8296
+ * A movement, or a failed update, names the bundle it left and how the
8297
+ * device updates, and a failed update says what failed; an unchanged report
8298
+ * names neither.
8299
+ */
8300
+ const hasEventInvariants = (row) => (row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED" || row.type === "UPDATE_FAILED") && typeof row.from_bundle_id === "string" && (updateStrategyOf(row) === "fingerprint" || updateStrategyOf(row) === "appVersion") && (row.type !== "UPDATE_FAILED" || isRecord$2(row.metadata) && isRecord$2(row.metadata.failure)) && !hasMetadata(row, "change") && (!hasMetadata(row, "late") || row.type === "UPDATE_DOWNLOADED" || row.type === "UPDATE_APPLIED") && (!hasMetadata(row, "implied_download") || row.type === "UPDATE_APPLIED" || row.type === "RECOVERED") || row.type === "UNCHANGED" && !hasMetadata(row, "late") && row.from_bundle_id === null && updateStrategyOf(row) === null;
8301
+ /**
8302
+ * Throws unless `row` has exactly the bundle event fields, each of its type,
8303
+ * and the invariants of its event type: `invalid-field` for an unknown field,
8304
+ * `invalid-data` for anything else.
8305
+ */
8306
+ const validateBundleEventFields = (row) => {
8307
+ if (!isRecord$2(row)) throw new DatabaseAdapterInputError("invalid-data");
8308
+ for (const field of Object.keys(row)) if (!Object.hasOwn(BUNDLE_EVENT_FIELDS, field)) throw new DatabaseAdapterInputError("invalid-field");
8309
+ for (const [field, valid] of Object.entries(BUNDLE_EVENT_FIELDS)) if (!Object.hasOwn(row, field) || !valid(row[field])) throw new DatabaseAdapterInputError("invalid-data");
8310
+ if (!hasEventInvariants(row)) throw new DatabaseAdapterInputError("invalid-data");
8311
+ };
8312
+ //#endregion
8313
+ //#region ../insights/dist/server/contract.mjs
8314
+ const encoder = new TextEncoder();
8315
+ /** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
8316
+ const compareInsightsText = (left, right) => {
8317
+ const a = encoder.encode(left);
8318
+ const b = encoder.encode(right);
8319
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
8320
+ return a.length - b.length;
8321
+ };
8322
+ const isWellFormedText = (value) => {
8323
+ for (const character of value) {
8324
+ const point = character.codePointAt(0);
8325
+ if (point >= 55296 && point <= 57343) return false;
8326
+ }
8327
+ return true;
8328
+ };
8329
+ const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
8330
+ const isText$1 = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
8331
+ const isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
8332
+ const isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
8333
+ const hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
8334
+ const hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText$1(value.channel);
8335
+ const isBundleFilter = (value, withKind = false) => {
8336
+ if (!isRecord$2(value) || !hasScope(value)) return false;
8337
+ const keys = [
8338
+ "platform",
8339
+ "channel",
8340
+ "type",
8341
+ ...withKind ? ["kind"] : []
8342
+ ];
8343
+ return value.type === "RECOVERED" ? isText$1(value.fromBundleId) && hasOnlyKeys(value, [...keys, "fromBundleId"]) : (value.type === "UPDATE_DOWNLOADED" || value.type === "UPDATE_APPLIED" || value.type === "UPDATE_FAILED" || value.type === "UNCHANGED") && isText$1(value.toBundleId) && hasOnlyKeys(value, [...keys, "toBundleId"]);
8344
+ };
8345
+ const isEventFilter = (value) => {
8346
+ if (!isRecord$2(value)) return false;
8347
+ if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
8348
+ if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
8349
+ return value.kind === "bundle" && isBundleFilter(value, true);
8350
+ };
8351
+ const validateRow = (model, row, result = false) => {
8352
+ try {
8353
+ validateBundleEventFields(row);
8354
+ if (!isRecord$2(row) || typeof row.id !== "string" || !isUUIDv7(row.id) || [...Object.values(row), ...isRecord$2(row.metadata) ? Object.values(row.metadata) : []].some((value) => typeof value === "string" && !isWellFormedText(value))) throw new DatabaseAdapterInputError("invalid-data");
8355
+ } catch (error) {
8356
+ if (result) throw new DatabaseAdapterInputError("invalid-result");
8357
+ throw error;
8358
+ }
8359
+ };
8360
+ /** Throws `invalid-data` unless `row` is a complete, well-formed bundle event row. */
8361
+ const assertBundleEventRow = (row) => validateRow("bundle_events", row);
8362
+ /**
8363
+ * Downloads, applies, recoveries, failed updates, and the UNCHANGED rows
8364
+ * kept for a change belong in installation history.
8365
+ */
8366
+ const isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED" || event.type === "UPDATE_FAILED" || event.type === "UNCHANGED";
8367
+ const matchesInsightsEventFilter = (event, filter) => {
8368
+ if (filter.kind === "all") return true;
8369
+ if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
8370
+ return event.type === filter.type && event.platform === filter.platform && event.channel === filter.channel && bundleRefsOf(event).includes(bundleRefOfFilter(filter));
8371
+ };
8372
+ const invalidQuery = () => {
8373
+ throw new DatabaseAdapterInputError("invalid-query");
8374
+ };
8375
+ const invalidResult = () => {
8376
+ throw new DatabaseAdapterInputError("invalid-result");
8377
+ };
8378
+ const validateCount = (count) => isTimestamp(count) ? count : invalidResult();
8379
+ const isTimeRange = (value) => isRecord$2(value) && hasOnlyKeys(value, ["start", "end"]) && isTimestamp(value.start) && isTimestamp(value.end) && value.start < value.end;
8380
+ const isReleaseReference = (value) => isRecord$2(value) && hasOnlyKeys(value, [
8381
+ "releaseId",
8382
+ "platform",
8383
+ "channel"
8384
+ ]) && isIdentity(value.releaseId) && hasScope(value);
8385
+ const validateAggregateResult = (value) => {
8386
+ const result = isRecord$2(value) ? value : invalidResult();
8387
+ if (!isTimestamp(result.measuredAtMs)) invalidResult();
8388
+ const coverage = result.coverage;
8389
+ if (!isRecord$2(coverage) || coverage.kind !== "complete" && coverage.kind !== "partial" || coverage.sinceMs !== null && !isTimestamp(coverage.sinceMs)) invalidResult();
8390
+ };
8391
+ /** Validate custom and bundled providers at the same public boundary. */
8392
+ const createValidatedInsightsModel = (model) => ({
8393
+ async recordEvent(input) {
8394
+ if (!isRecord$2(input) || !hasOnlyKeys(input, ["event"])) throw new DatabaseAdapterInputError("invalid-data");
8395
+ validateRow("bundle_events", input.event);
8396
+ await model.recordEvent(input);
8397
+ },
8398
+ async listEvents(input) {
8399
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
8400
+ "filter",
8401
+ "sinceMs",
8402
+ "beforeReceivedAtMs",
8403
+ "after",
8404
+ "limit"
8405
+ ]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
8406
+ const sinceMs = input.sinceMs ?? 0;
8407
+ if (sinceMs > input.beforeReceivedAtMs || input.after !== void 0 && (!isRecord$2(input.after) || !hasOnlyKeys(input.after, ["receivedAtMs", "id"]) || !isTimestamp(input.after.receivedAtMs) || typeof input.after.id !== "string" || !isUUIDv7(input.after.id) || input.after.receivedAtMs < sinceMs || input.after.receivedAtMs >= input.beforeReceivedAtMs)) invalidQuery();
8408
+ const rows = await model.listEvents(input);
8409
+ if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
8410
+ let previous = input.after;
8411
+ for (const row of rows) {
8412
+ validateRow("bundle_events", row, true);
8413
+ if (row.received_at_ms < sinceMs || row.received_at_ms >= input.beforeReceivedAtMs || !matchesInsightsEventFilter(row, input.filter) || previous !== void 0 && (row.received_at_ms > previous.receivedAtMs || row.received_at_ms === previous.receivedAtMs && row.id >= previous.id)) invalidResult();
8414
+ previous = {
8415
+ receivedAtMs: row.received_at_ms,
8416
+ id: row.id
8417
+ };
8418
+ }
8419
+ return rows;
8672
8420
  },
8673
- from_release_id: {
8674
- type: "string",
8675
- maxLength: 36,
8676
- required: false
8421
+ async findLatestEvents(input) {
8422
+ if (!isRecord$2(input)) invalidQuery();
8423
+ if ("installId" in input) {
8424
+ if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
8425
+ } else if (!hasOnlyKeys(input, [
8426
+ "userId",
8427
+ "afterInstallId",
8428
+ "limit"
8429
+ ]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
8430
+ const rows = await model.findLatestEvents(input);
8431
+ if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
8432
+ let previous = "installId" in input ? void 0 : input.afterInstallId;
8433
+ for (const row of rows) {
8434
+ validateRow("bundle_events", row, true);
8435
+ if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
8436
+ previous = row.install_id;
8437
+ }
8438
+ return rows;
8677
8439
  },
8678
- from_bundle_id: {
8679
- type: "string",
8680
- maxLength: 36,
8681
- required: false
8440
+ async countLatestEvents(input) {
8441
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
8442
+ "platform",
8443
+ "channel",
8444
+ "sinceMs",
8445
+ "bundle"
8446
+ ]) || !hasScope(input) || !isTimestamp(input.sinceMs) || input.bundle !== void 0 && (!Array.isArray(input.bundle) || input.bundle.length < 1 || input.bundle.length > 2 || input.bundle.some((bundle) => !isRecord$2(bundle) || !hasOnlyKeys(bundle, [
8447
+ "field",
8448
+ "value",
8449
+ "types"
8450
+ ]) || bundle.field !== "from_bundle_id" && bundle.field !== "to_bundle_id" || !isText$1(bundle.value) || !Array.isArray(bundle.types) || bundle.types.length === 0 || bundle.types.some((type) => ![
8451
+ "UNCHANGED",
8452
+ "UPDATE_DOWNLOADED",
8453
+ "UPDATE_APPLIED",
8454
+ "RECOVERED"
8455
+ ].includes(type))))) invalidQuery();
8456
+ return validateCount(await model.countLatestEvents(input));
8682
8457
  },
8683
- to_release_id: {
8684
- type: "string",
8685
- maxLength: 36,
8686
- required: false
8458
+ async countEvents(input) {
8459
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
8460
+ "filter",
8461
+ "sinceMs",
8462
+ "beforeReceivedAtMs"
8463
+ ]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
8464
+ return validateCount(await model.countEvents(input));
8687
8465
  },
8688
- to_bundle_id: {
8689
- type: "string",
8690
- maxLength: 36
8466
+ async countEventSeries(input) {
8467
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
8468
+ "filter",
8469
+ "timeRange",
8470
+ "intervalMs"
8471
+ ]) || !isBundleFilter(input.filter) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs)) invalidQuery();
8472
+ const { start, end } = input.timeRange;
8473
+ const { intervalMs } = input;
8474
+ if (intervalMs === 0 || intervalMs % 36e5 !== 0 || start % 36e5 !== 0 || (end - start) % intervalMs !== 0 || end - start > 7776e6) invalidQuery();
8475
+ const points = await model.countEventSeries(input);
8476
+ if (!Array.isArray(points) || points.length !== (end - start) / intervalMs || points.some((point, index) => !isRecord$2(point) || !hasOnlyKeys(point, ["startMs", "events"]) || point.startMs !== start + index * intervalMs || !isTimestamp(point.events))) invalidResult();
8477
+ return points;
8691
8478
  },
8692
- platform: {
8693
- type: "string",
8694
- maxLength: 16
8479
+ async getReleaseActivity(input) {
8480
+ if (!isRecord$2(input) || !hasOnlyKeys(input, ["releases"]) || !Array.isArray(input.releases) || input.releases.length < 1 || input.releases.length > 100 || !input.releases.every(isReleaseReference)) invalidQuery();
8481
+ const result = await model.getReleaseActivity(input);
8482
+ if (!isRecord$2(result) || !isTimestamp(result.measuredAtMs) || !Array.isArray(result.data) || result.data.some((item) => !isRecord$2(item) || !isReleaseReference(item.release) || !isRecord$2(item.metrics) || !isTimestamp(item.metrics.downloads) || !isTimestamp(item.metrics.applies) || !isTimestamp(item.metrics.failedLaunches))) invalidResult();
8483
+ return result;
8695
8484
  },
8696
- app_version: { type: "string" },
8697
- channel: { type: "string" },
8698
- metadata: { type: "json" },
8699
- received_at_ms: { type: "integer" }
8700
- }, {
8701
- key: ["id"],
8702
- derived: {
8703
- /** The UTC day the event was received in. */
8704
- day: {
8705
- type: "integer",
8706
- compute: (row) => row.received_at_ms - row.received_at_ms % DAY_MS
8707
- },
8708
- /** Set only for events that belong in installation history. */
8709
- movement_install_id: {
8710
- type: "string",
8711
- compute: (row) => MOVEMENTS.has(row.type) ? row.install_id : null
8712
- },
8713
- /**
8714
- * The bundle a bundle filter matches: `from:<bundle>` for RECOVERED,
8715
- * none for a failed check, else `to:<bundle>`.
8716
- */
8717
- bundle_ref: {
8718
- type: "string",
8719
- multi: true,
8720
- compute: (row) => isFailedCheck(row) ? [] : [row.type === "RECOVERED" && row.from_bundle_id !== null ? `from:${row.from_bundle_id}` : `to:${row.to_bundle_id}`]
8485
+ async getAppUsage(input) {
8486
+ if (!isRecord$2(input) || !hasOnlyKeys(input, [
8487
+ "channel",
8488
+ "platform",
8489
+ "appVersion",
8490
+ "timeRange",
8491
+ "intervalMs"
8492
+ ]) || !isText$1(input.channel) || ![
8493
+ "all",
8494
+ "ios",
8495
+ "android"
8496
+ ].includes(input.platform) || input.appVersion !== void 0 && !isText$1(input.appVersion) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs) || input.intervalMs < 36e5) invalidQuery();
8497
+ const result = await model.getAppUsage(input);
8498
+ validateAggregateResult(result);
8499
+ if (!isTimestamp(result.activeInstallations) || !Array.isArray(result.points) || !Array.isArray(result.appVersions) || !Array.isArray(result.versions) || !Array.isArray(result.platforms) || !Array.isArray(result.bundleDistribution)) invalidResult();
8500
+ return result;
8501
+ }
8502
+ });
8503
+ //#endregion
8504
+ //#region ../insights/dist/server/model.mjs
8505
+ /**
8506
+ * The plugin's API as `InsightsModel`, validated at its boundary: what the
8507
+ * Insights routes, the console, and the e2e harness read through.
8508
+ */
8509
+ const createInsightsModel = (api) => createValidatedInsightsModel({
8510
+ recordEvent: ({ event }) => api.recordEvent(event),
8511
+ listEvents: api.listEvents,
8512
+ findLatestEvents: api.findLatestEvents,
8513
+ countLatestEvents: api.countLatestEvents,
8514
+ countEvents: api.countEvents,
8515
+ countEventSeries: api.countEventSeries,
8516
+ getReleaseActivity: api.getReleaseActivity,
8517
+ getAppUsage: api.getAppUsage
8518
+ });
8519
+ //#endregion
8520
+ //#region ../insights/dist/server/official.mjs
8521
+ /**
8522
+ * The brand of Hot Updater's own plugins, a registered symbol that
8523
+ * `@hot-updater/server` reads to reserve the id "insights" for this plugin.
8524
+ * Each official plugin package keeps this copy, so none imports another's
8525
+ * internals for it.
8526
+ */
8527
+ const OFFICIAL = Symbol.for("@hot-updater/server/official-plugin");
8528
+ /** Marks a plugin one of Hot Updater's own factories made. */
8529
+ const markOfficial = (plugin) => Object.defineProperty(plugin, OFFICIAL, { value: true });
8530
+ //#endregion
8531
+ //#region ../insights/dist/server/errors.mjs
8532
+ /**
8533
+ * What Insights' API throws for an input it refuses, such as an unknown
8534
+ * event field or a malformed cursor; its routes answer 400 for it.
8535
+ */
8536
+ var InsightsBadRequestError = class extends Error {
8537
+ name = "InsightsBadRequestError";
8538
+ };
8539
+ var InsightsPayloadTooLargeError = class extends Error {
8540
+ maximumBytes;
8541
+ name = "InsightsPayloadTooLargeError";
8542
+ constructor(maximumBytes) {
8543
+ super(`Event payload exceeds ${maximumBytes} bytes`);
8544
+ this.maximumBytes = maximumBytes;
8545
+ }
8546
+ };
8547
+ //#endregion
8548
+ //#region ../insights/dist/server/eventInput.mjs
8549
+ const MAX_EVENT_STRING_LENGTH = 1024;
8550
+ const MAX_IDENTITY_LENGTH$2 = 255;
8551
+ const EVENT_BODY_MAX_BYTES = 16384;
8552
+ function isRecord$1(value) {
8553
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8554
+ }
8555
+ function requireStringField(payload, key) {
8556
+ const value = payload[key];
8557
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_EVENT_STRING_LENGTH || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
8558
+ return value;
8559
+ }
8560
+ function requireNullableStringField(payload, key) {
8561
+ if (payload[key] === null) return null;
8562
+ return requireStringField(payload, key);
8563
+ }
8564
+ function requireIdentityField(payload, key) {
8565
+ const value = requireStringField(payload, key);
8566
+ if (value.length > MAX_IDENTITY_LENGTH$2) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
8567
+ return value;
8568
+ }
8569
+ /**
8570
+ * The report's idempotency key, when the client sends one: it creates the
8571
+ * UUIDv7 once and repeats it on every retry of that report.
8572
+ */
8573
+ function readEventId(payload) {
8574
+ const value = payload.eventId;
8575
+ if (value === void 0) return void 0;
8576
+ if (!isUUIDv7(value)) throw new InsightsBadRequestError("Invalid event field: eventId");
8577
+ return value;
8578
+ }
8579
+ /**
8580
+ * The native build's built-in bundle ID: a bundle ID's length at most, and
8581
+ * left out when it is missing or malformed, so the report still records.
8582
+ */
8583
+ const readMinBundleId = (value) => typeof value === "string" && value.length > 0 && value.length <= 36 ? value : null;
8584
+ /** One value of an open set: any other value reads as `unknown`. */
8585
+ const oneOf = (values, value) => values.includes(value) ? value : "unknown";
8586
+ const STAGES = [
8587
+ "check",
8588
+ "download",
8589
+ "install"
8590
+ ];
8591
+ const REASONS = [
8592
+ "network",
8593
+ "http",
8594
+ "invalid_response",
8595
+ "hash_mismatch",
8596
+ "signature",
8597
+ "patch",
8598
+ "extract",
8599
+ "storage"
8600
+ ];
8601
+ const RESOURCES = [
8602
+ "catalog",
8603
+ "artifact",
8604
+ "manifest",
8605
+ "file",
8606
+ "patch",
8607
+ "archive"
8608
+ ];
8609
+ const TRANSPORTS = [
8610
+ "timeout",
8611
+ "dns",
8612
+ "tls",
8613
+ "connection",
8614
+ "offline",
8615
+ "cancelled"
8616
+ ];
8617
+ const DELIVERIES = [
8618
+ "patch",
8619
+ "manifest",
8620
+ "archive"
8621
+ ];
8622
+ /** A storage error code: letters, digits, and `._-`. */
8623
+ const CODE = /^[A-Za-z0-9._-]{1,64}$/;
8624
+ const readCode = (value) => typeof value === "string" && CODE.test(value) ? value : void 0;
8625
+ const readMetadata = (payload) => isRecord$1(payload.metadata) ? payload.metadata : {};
8626
+ /**
8627
+ * What an UPDATE_FAILED report says failed. It never refuses the report: an
8628
+ * unknown value of a set reads as `unknown`, a missing or malformed failure
8629
+ * as an unknown stage and reason, and a malformed optional field is left out,
8630
+ * as is a null one.
8631
+ */
8632
+ function readFailure(value) {
8633
+ const failure = isRecord$1(value) ? value : {};
8634
+ const httpStatus = failure.httpStatus;
8635
+ const originCode = readCode(failure.originCode);
8636
+ const errorMessage = typeof failure.errorMessage === "string" && failure.errorMessage.length <= 2048 ? failure.errorMessage : void 0;
8637
+ const errorStack = typeof failure.errorStack === "string" && failure.errorStack.length <= 4096 ? failure.errorStack : void 0;
8638
+ return {
8639
+ stage: oneOf(STAGES, failure.stage),
8640
+ reason: oneOf(REASONS, failure.reason),
8641
+ ...failure.resource == null ? {} : { resource: oneOf(RESOURCES, failure.resource) },
8642
+ ...Number.isSafeInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599 ? { httpStatus } : {},
8643
+ ...failure.transport == null ? {} : { transport: oneOf(TRANSPORTS, failure.transport) },
8644
+ ...originCode === void 0 ? {} : { originCode },
8645
+ ...errorMessage === void 0 ? {} : { errorMessage },
8646
+ ...errorStack === void 0 ? {} : { errorStack }
8647
+ };
8648
+ }
8649
+ function readHttpResponse(value) {
8650
+ if (!isRecord$1(value) || value.resource !== "catalog" && value.resource !== "artifact" || typeof value.path !== "string" || value.path.length > 1024 || !Number.isSafeInteger(value.status) || value.status < 100 || value.status > 599 || value.body !== null && (typeof value.body !== "string" || new TextEncoder().encode(JSON.stringify(value.body)).byteLength > 4096) || typeof value.bodyTruncated !== "boolean" || !Number.isSafeInteger(value.receivedAtMs) || value.receivedAtMs < 0) throw new InsightsBadRequestError("Invalid HTTP response details");
8651
+ return {
8652
+ resource: value.resource,
8653
+ path: value.path,
8654
+ status: value.status,
8655
+ body: value.body,
8656
+ bodyTruncated: value.bodyTruncated,
8657
+ receivedAtMs: value.receivedAtMs
8658
+ };
8659
+ }
8660
+ async function readBoundedText(request) {
8661
+ const contentLength = request.headers.get("content-length");
8662
+ const declaredByteLength = Number(contentLength);
8663
+ if (contentLength !== null && Number.isSafeInteger(declaredByteLength) && declaredByteLength > 16384) throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
8664
+ if (request.body === null) return "";
8665
+ const reader = request.body.getReader();
8666
+ const decoder = new TextDecoder();
8667
+ let byteLength = 0;
8668
+ let text = "";
8669
+ while (true) {
8670
+ const result = await reader.read();
8671
+ if (result.done) break;
8672
+ byteLength += result.value.byteLength;
8673
+ if (byteLength > 16384) {
8674
+ await reader.cancel();
8675
+ throw new InsightsPayloadTooLargeError(EVENT_BODY_MAX_BYTES);
8721
8676
  }
8722
- },
8723
- indexes: {
8724
- movementsByInstall: {
8725
- eq: ["movement_install_id"],
8726
- sort: ["received_at_ms"]
8727
- },
8728
- byBundle: {
8729
- eq: [
8730
- "platform",
8731
- "channel",
8732
- "type",
8733
- "bundle_ref",
8734
- "day"
8735
- ],
8736
- sort: ["received_at_ms"]
8737
- },
8738
- /** Events of every scope by day, for the console's unfiltered event list. */
8739
- byDay: {
8740
- eq: ["day"],
8741
- sort: ["received_at_ms"]
8677
+ text += decoder.decode(result.value, { stream: true });
8678
+ }
8679
+ return text + decoder.decode();
8680
+ }
8681
+ async function parseJson(request) {
8682
+ const text = await readBoundedText(request);
8683
+ try {
8684
+ return JSON.parse(text);
8685
+ } catch (error) {
8686
+ if (error instanceof SyntaxError) throw new InsightsBadRequestError("Invalid event payload");
8687
+ throw error;
8688
+ }
8689
+ }
8690
+ function requireEvent(payload) {
8691
+ if (!isRecord$1(payload)) throw new InsightsBadRequestError("Invalid event payload");
8692
+ const platform = requireStringField(payload, "platform");
8693
+ if (platform !== "ios" && platform !== "android") throw new InsightsBadRequestError("Invalid event field: platform");
8694
+ const eventId = readEventId(payload);
8695
+ const metadata = readMetadata(payload);
8696
+ const httpMetadata = metadata.httpResponse === void 0 ? {} : { httpResponse: readHttpResponse(metadata.httpResponse) };
8697
+ const base = {
8698
+ ...metadata.httpResponse === void 0 ? {} : { metadata: httpMetadata },
8699
+ ...eventId === void 0 ? {} : { eventId },
8700
+ installId: requireIdentityField(payload, "installId"),
8701
+ toBundleId: requireStringField(payload, "toBundleId"),
8702
+ ...payload.userId === void 0 ? {} : { userId: requireIdentityField(payload, "userId") },
8703
+ platform,
8704
+ appVersion: requireStringField(payload, "appVersion"),
8705
+ channel: requireStringField(payload, "channel"),
8706
+ cohort: requireStringField(payload, "cohort"),
8707
+ fingerprintHash: requireNullableStringField(payload, "fingerprintHash"),
8708
+ sdkVersion: payload.sdkVersion === void 0 ? null : requireNullableStringField(payload, "sdkVersion"),
8709
+ minBundleId: readMinBundleId(payload.minBundleId),
8710
+ fromReleaseId: requireNullableStringField(payload, "fromReleaseId"),
8711
+ toReleaseId: requireNullableStringField(payload, "toReleaseId")
8712
+ };
8713
+ const type = requireStringField(payload, "type");
8714
+ const movement = () => {
8715
+ const updateStrategy = requireStringField(payload, "updateStrategy");
8716
+ if (updateStrategy !== "fingerprint" && updateStrategy !== "appVersion") throw new InsightsBadRequestError("Invalid event field: updateStrategy");
8717
+ return {
8718
+ ...base,
8719
+ fromBundleId: requireStringField(payload, "fromBundleId"),
8720
+ updateStrategy
8721
+ };
8722
+ };
8723
+ switch (type) {
8724
+ case "UPDATE_DOWNLOADED": {
8725
+ const delivery = metadata.delivery;
8726
+ const read = {
8727
+ ...httpMetadata,
8728
+ ...delivery == null ? {} : { delivery: oneOf(DELIVERIES, delivery) },
8729
+ ...metadata.patchFallback === true ? { patchFallback: true } : {}
8730
+ };
8731
+ return {
8732
+ ...movement(),
8733
+ type,
8734
+ ...Object.keys(read).length === 0 ? {} : { metadata: read }
8735
+ };
8742
8736
  }
8743
- },
8744
- retention: {
8745
- field: "day",
8746
- days
8737
+ case "UPDATE_APPLIED":
8738
+ case "RECOVERED": return {
8739
+ ...movement(),
8740
+ type
8741
+ };
8742
+ case "UPDATE_FAILED": return {
8743
+ ...movement(),
8744
+ type,
8745
+ metadata: {
8746
+ ...httpMetadata,
8747
+ failure: readFailure(metadata.failure)
8748
+ }
8749
+ };
8750
+ case "UNCHANGED":
8751
+ if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
8752
+ return {
8753
+ ...base,
8754
+ type,
8755
+ fromBundleId: null,
8756
+ updateStrategy: null
8757
+ };
8758
+ default: throw new InsightsBadRequestError("Invalid event field: type");
8747
8759
  }
8748
- });
8749
- /**
8750
- * Each installation's latest event, whole, so reading it is one point read.
8751
- * Its gauges count it in the UTC day of that event. An update failure moves
8752
- * nothing, so no head holds one.
8753
- */
8754
- const bundleEventHeads = (days) => defineTable({
8755
- install_id: {
8756
- type: "string",
8757
- maxLength: 255
8758
- },
8759
- id: {
8760
- type: "string",
8761
- maxLength: 36
8762
- },
8763
- type: {
8764
- type: "string",
8765
- maxLength: 32
8766
- },
8767
- user_id: {
8768
- type: "string",
8769
- maxLength: 255,
8770
- required: false
8771
- },
8772
- from_release_id: {
8773
- type: "string",
8774
- maxLength: 36,
8775
- required: false
8776
- },
8777
- from_bundle_id: {
8778
- type: "string",
8779
- maxLength: 36,
8780
- required: false
8781
- },
8782
- to_release_id: {
8783
- type: "string",
8784
- maxLength: 36,
8785
- required: false
8786
- },
8787
- to_bundle_id: {
8788
- type: "string",
8789
- maxLength: 36
8790
- },
8791
- platform: {
8792
- type: "string",
8793
- maxLength: 16
8794
- },
8795
- app_version: { type: "string" },
8796
- channel: { type: "string" },
8797
- metadata: { type: "json" },
8798
- received_at_ms: { type: "integer" }
8799
- }, {
8800
- key: ["install_id"],
8801
- indexes: { byUser: {
8802
- eq: ["user_id"],
8803
- sort: ["install_id"]
8804
- } },
8805
- retention: {
8806
- field: "received_at_ms",
8807
- days
8760
+ }
8761
+ async function parseBundleEventRequest(request) {
8762
+ return requireEvent(await parseJson(request));
8763
+ }
8764
+ function createBundleEventRow(input) {
8765
+ input = requireEvent(input);
8766
+ const base = {
8767
+ app_version: input.appVersion,
8768
+ channel: input.channel,
8769
+ from_release_id: input.fromReleaseId,
8770
+ id: input.eventId ?? createUUIDv7(),
8771
+ install_id: input.installId,
8772
+ platform: input.platform,
8773
+ received_at_ms: Date.now(),
8774
+ to_bundle_id: input.toBundleId,
8775
+ to_release_id: input.toReleaseId,
8776
+ user_id: input.userId ?? null
8777
+ };
8778
+ const response = input.metadata?.httpResponse;
8779
+ const metadata = {
8780
+ ...response ? { http_response: {
8781
+ resource: response.resource,
8782
+ path: response.path,
8783
+ status: response.status,
8784
+ body: response.body,
8785
+ body_truncated: response.bodyTruncated,
8786
+ received_at_ms: response.receivedAtMs
8787
+ } } : {},
8788
+ cohort: input.cohort,
8789
+ fingerprint_hash: input.fingerprintHash,
8790
+ sdk_version: input.sdkVersion ?? null,
8791
+ ...input.minBundleId ? { min_bundle_id: input.minBundleId } : {},
8792
+ update_strategy: input.updateStrategy
8793
+ };
8794
+ switch (input.type) {
8795
+ case "UPDATE_DOWNLOADED": {
8796
+ const { delivery, patchFallback } = input.metadata ?? {};
8797
+ return {
8798
+ ...base,
8799
+ from_bundle_id: input.fromBundleId,
8800
+ type: input.type,
8801
+ metadata: {
8802
+ ...metadata,
8803
+ ...delivery === void 0 ? {} : { delivery },
8804
+ ...patchFallback ? { patch_fallback: true } : {}
8805
+ }
8806
+ };
8807
+ }
8808
+ case "UPDATE_APPLIED":
8809
+ case "RECOVERED": return {
8810
+ ...base,
8811
+ from_bundle_id: input.fromBundleId,
8812
+ type: input.type,
8813
+ metadata
8814
+ };
8815
+ case "UPDATE_FAILED": {
8816
+ const { failure } = input.metadata;
8817
+ return {
8818
+ ...base,
8819
+ from_bundle_id: input.fromBundleId,
8820
+ type: input.type,
8821
+ metadata: {
8822
+ ...metadata,
8823
+ failure: {
8824
+ stage: failure.stage,
8825
+ reason: failure.reason,
8826
+ ...failure.errorMessage === void 0 ? {} : { error_message: failure.errorMessage },
8827
+ ...failure.errorStack === void 0 ? {} : { error_stack: failure.errorStack },
8828
+ ...failure.resource === void 0 ? {} : { resource: failure.resource },
8829
+ ...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
8830
+ ...failure.transport === void 0 ? {} : { transport: failure.transport },
8831
+ ...failure.originCode === void 0 ? {} : { origin_code: failure.originCode }
8832
+ }
8833
+ }
8834
+ };
8835
+ }
8836
+ case "UNCHANGED": return {
8837
+ ...base,
8838
+ from_bundle_id: null,
8839
+ type: input.type,
8840
+ metadata
8841
+ };
8808
8842
  }
8809
- });
8843
+ }
8844
+ //#endregion
8845
+ //#region ../insights/dist/server/provider.mjs
8846
+ const DEFAULT_PAGE_LIMIT = 50;
8847
+ const MAX_PAGE_LIMIT$1 = 100;
8848
+ const MAX_EVENT_ID_LENGTH = 1024;
8849
+ const MAX_IDENTITY_LENGTH$1 = 255;
8850
+ const MAX_CURSOR_LENGTH$1 = 8192;
8851
+ const HOUR_MS = 36e5;
8852
+ const DAY_MS = 24 * HOUR_MS;
8853
+ const WINDOW_MS = {
8854
+ "24h": 864e5,
8855
+ "7d": 6048e5,
8856
+ "30d": 2592e6
8857
+ };
8810
8858
  /**
8811
- * Shards are keyed by install id. Counters are blind increments and never
8812
- * conflict, so they keep 8. Gauges and sketches are read, merged, and written
8813
- * back. B3's rollout gate on PostgreSQL retried 2.4% of transactions at 8
8814
- * shards and 1.3% at 16. DynamoDB's transactions take longer, so D8's gate
8815
- * on DynamoDB Local retried 2–24% at 16, almost all on gauge rows, and 1–5%
8816
- * with gauges at 32. Sketches stay at 16: each shard row carries 2 KB of
8817
- * registers that every read merges.
8818
- *
8819
- * Every Insights aggregate is `batched`: on DynamoDB and Firestore, which
8820
- * bill each write, an event's changes apply after it commits, merged with
8821
- * other events' into one write per row, on one shard per writer.
8859
+ * The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
8860
+ * bundle event list, and the range one covers without `sinceMs`: raw events
8861
+ * are listed inside a time range, as analytics products list them. It is a
8862
+ * duration, so a range that ends mid-day touches 91 UTC days. Installation
8863
+ * history is one index range and reads back to `sinceMs`.
8822
8864
  */
8823
- const COUNTER_SHARDS = 8;
8824
- const GAUGE_SHARDS = 32;
8825
- const SKETCH_SHARDS = 16;
8826
- /** Rows keyed by a hashed identity (scope and period) and a bucket. */
8827
- const identityFields = {
8828
- identity: {
8829
- type: "string",
8830
- maxLength: 32
8831
- },
8832
- bucket_start_ms: { type: "integer" }
8865
+ const EVENT_LIST_RANGE_MS = 2160 * HOUR_MS;
8866
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8867
+ const requireString = (value, label, maximumLength) => {
8868
+ if (typeof value !== "string" || value.length === 0 || value.length > maximumLength || new TextDecoder("utf-8", { ignoreBOM: true }).decode(new TextEncoder().encode(value)) !== value) throw new InsightsBadRequestError(`Invalid ${label}.`);
8869
+ return value;
8833
8870
  };
8834
- const window = {
8835
- eq: ["identity"],
8836
- sort: ["bucket_start_ms"]
8871
+ const requireTimestamp = (value, label) => {
8872
+ if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
8873
+ return Number(value);
8837
8874
  };
8838
- /** What a release or channel row counts in every period kind. */
8839
- const WINDOW_COUNTERS = [
8840
- "downloads",
8841
- "failed_launches",
8842
- "patch_downloads",
8843
- "patch_fallbacks"
8844
- ];
8845
- /**
8846
- * Hourly or daily counters: a release or channel row counts downloads,
8847
- * launches that crashed and recovered (`failed_launches`), and downloads a
8848
- * patch delivered and those that fell back from one.
8849
- */
8850
- const counters = (retention) => defineAggregate(identityFields, {
8851
- key: ["identity", "bucket_start_ms"],
8852
- counters: WINDOW_COUNTERS,
8853
- shards: COUNTER_SHARDS,
8854
- batched: true,
8855
- indexes: { window },
8856
- retention
8875
+ const readLimit = (value) => {
8876
+ const limit = value ?? DEFAULT_PAGE_LIMIT;
8877
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT$1) throw new InsightsBadRequestError("Invalid page limit.");
8878
+ return limit;
8879
+ };
8880
+ const encodeBase64Url = (value) => {
8881
+ const bytes = new TextEncoder().encode(value);
8882
+ let binary = "";
8883
+ for (const byte of bytes) binary += String.fromCharCode(byte);
8884
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
8885
+ };
8886
+ const decodeBase64Url = (value) => {
8887
+ if (value.length === 0 || value.length > MAX_CURSOR_LENGTH$1) throw new InsightsBadRequestError("Invalid Insights cursor.");
8888
+ try {
8889
+ const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
8890
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
8891
+ const binary = atob(padded);
8892
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
8893
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
8894
+ } catch {
8895
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
8896
+ }
8897
+ };
8898
+ const encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
8899
+ const decodeCursor = (value) => {
8900
+ try {
8901
+ return JSON.parse(decodeBase64Url(value));
8902
+ } catch (error) {
8903
+ if (error instanceof InsightsBadRequestError) throw error;
8904
+ throw new InsightsBadRequestError("Invalid Insights cursor.");
8905
+ }
8906
+ };
8907
+ const readScope$1 = (input) => {
8908
+ if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
8909
+ return {
8910
+ platform: input.platform,
8911
+ channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
8912
+ };
8913
+ };
8914
+ const bundleFilter = (input) => {
8915
+ const scope = readScope$1(input);
8916
+ const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
8917
+ switch (input.outcome) {
8918
+ case "downloaded": return {
8919
+ ...scope,
8920
+ type: "UPDATE_DOWNLOADED",
8921
+ toBundleId: bundleId
8922
+ };
8923
+ case "applied": return {
8924
+ ...scope,
8925
+ type: "UPDATE_APPLIED",
8926
+ toBundleId: bundleId
8927
+ };
8928
+ case "recovered": return {
8929
+ ...scope,
8930
+ type: "RECOVERED",
8931
+ fromBundleId: bundleId
8932
+ };
8933
+ case "failed": return {
8934
+ ...scope,
8935
+ type: "UPDATE_FAILED",
8936
+ toBundleId: bundleId
8937
+ };
8938
+ default: throw new InsightsBadRequestError("Invalid Insights outcome.");
8939
+ }
8940
+ };
8941
+ const sameFilter = (left, right) => {
8942
+ if (!isRecord(left) || left.kind !== right.kind) return false;
8943
+ switch (right.kind) {
8944
+ case "all": return true;
8945
+ case "installationMovement": return left.installId === right.installId;
8946
+ case "bundle": return left.platform === right.platform && left.channel === right.channel && left.type === right.type && (right.type === "RECOVERED" ? left.fromBundleId === right.fromBundleId : left.toBundleId === right.toBundleId);
8947
+ }
8948
+ };
8949
+ const readEventCursor = (value, filter) => {
8950
+ const cursor = decodeCursor(value);
8951
+ if (!isRecord(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord(cursor.filter) || !isRecord(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
8952
+ if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
8953
+ if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
8954
+ return {
8955
+ after: {
8956
+ id: cursor.after.id,
8957
+ receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
8958
+ },
8959
+ beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
8960
+ kind: "events",
8961
+ sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
8962
+ filter,
8963
+ version: 2
8964
+ };
8965
+ };
8966
+ const readUserInstallationCursor = (value, userId) => {
8967
+ const cursor = decodeCursor(value);
8968
+ if (!isRecord(cursor) || cursor.version !== 1 || cursor.kind !== "user-installations" || cursor.userId !== userId) throw new InsightsBadRequestError("Insights cursor does not match the requested user ID.");
8969
+ return {
8970
+ afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH$1),
8971
+ kind: "user-installations",
8972
+ userId,
8973
+ version: 1
8974
+ };
8975
+ };
8976
+ const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
8977
+ const isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
8978
+ const assertEventRows = (rows, input) => {
8979
+ if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
8980
+ for (let index = 0; index < rows.length; index += 1) {
8981
+ const row = rows[index];
8982
+ const previous = rows[index - 1];
8983
+ if (!row || typeof row.id !== "string" || !isUUIDv7(row.id) || !Number.isSafeInteger(row.received_at_ms) || row.received_at_ms >= input.beforeReceivedAtMs || row.received_at_ms < input.sinceMs || previous !== void 0 && compareEventNewest(previous, row) >= 0 || input.after !== void 0 && !isAfterEventCursor(row, input.after) || input.filter.kind === "installationMovement" && (row.install_id !== input.filter.installId || !isInsightsMovementEvent(row)) || input.filter.kind === "bundle" && (row.platform !== input.filter.platform || row.channel !== input.filter.channel || row.type !== input.filter.type || (input.filter.type === "RECOVERED" ? row.from_bundle_id !== input.filter.fromBundleId : row.to_bundle_id !== input.filter.toBundleId))) throw new Error("Insights database returned invalid event rows.");
8984
+ }
8985
+ };
8986
+ /** An `UPDATE_FAILED` row's failure, as the event lists show it. */
8987
+ const toFailure = (failure) => ({
8988
+ stage: failure.stage,
8989
+ reason: failure.reason,
8990
+ ...failure.error_message === void 0 ? {} : { errorMessage: failure.error_message },
8991
+ ...failure.error_stack === void 0 ? {} : { errorStack: failure.error_stack },
8992
+ ...failure.resource === void 0 ? {} : { resource: failure.resource },
8993
+ ...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
8994
+ ...failure.transport === void 0 ? {} : { transport: failure.transport },
8995
+ ...failure.origin_code === void 0 ? {} : { originCode: failure.origin_code }
8857
8996
  });
8858
- /**
8859
- * A release's counters since its first report, at bucket 0: the windowed
8860
- * counters, its applies, which the bundle list reads, and its failed updates
8861
- * (`failed_updates`), since no breakdown row outlives the raw period; windowed
8862
- * reads sum `insights_failures` instead.
8863
- */
8864
- const lifetimeCounters = () => defineAggregate(identityFields, {
8865
- key: ["identity", "bucket_start_ms"],
8866
- counters: [
8867
- ...WINDOW_COUNTERS,
8868
- "applies",
8869
- "failed_updates"
8870
- ],
8871
- shards: COUNTER_SHARDS,
8872
- batched: true,
8873
- indexes: { window }
8997
+ /** What a row's metadata adds for its type: a failure or a delivery. */
8998
+ const eventDetails = ({ type, metadata }) => ({
8999
+ ...metadata.http_response !== void 0 ? { httpResponse: {
9000
+ resource: metadata.http_response.resource,
9001
+ path: metadata.http_response.path,
9002
+ status: metadata.http_response.status,
9003
+ body: metadata.http_response.body,
9004
+ bodyTruncated: metadata.http_response.body_truncated,
9005
+ receivedAtMs: metadata.http_response.received_at_ms
9006
+ } } : {},
9007
+ ...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
9008
+ ...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
9009
+ ...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {},
9010
+ ...type === "UNCHANGED" && metadata.change !== void 0 ? { change: {
9011
+ kinds: metadata.change.kinds,
9012
+ ...metadata.change.previous === null ? {} : { previous: {
9013
+ bundleId: metadata.change.previous.bundle_id,
9014
+ releaseId: metadata.change.previous.release_id,
9015
+ appVersion: metadata.change.previous.app_version,
9016
+ channel: metadata.change.previous.channel,
9017
+ ...metadata.change.previous.min_bundle_id === void 0 ? {} : { minBundleId: metadata.change.previous.min_bundle_id }
9018
+ } }
9019
+ } } : {},
9020
+ ...metadata.late === true ? { late: true } : {},
9021
+ ...metadata.implied_download === true ? { impliedDownload: true } : {}
8874
9022
  });
8875
- /**
8876
- * Unique-installation sketches of one period kind: a usage row's active
8877
- * installations (`activity_users`), and a `failure` or `check` row's
8878
- * installations whose update or update check failed (`failed_users`).
8879
- */
8880
- const sketches = (retention) => defineAggregate(identityFields, {
8881
- key: ["identity", "bucket_start_ms"],
8882
- distinct: ["activity_users", "failed_users"],
8883
- shards: SKETCH_SHARDS,
8884
- batched: true,
8885
- indexes: { window },
8886
- retention
9023
+ const toEventHistoryRow = (row) => ({
9024
+ ...row.to_release_id == null ? {} : { toReleaseId: row.to_release_id },
9025
+ ...row.metadata.sdk_version == null ? {} : { sdkVersion: row.metadata.sdk_version },
9026
+ ...row.metadata.min_bundle_id === void 0 ? {} : { minBundleId: row.metadata.min_bundle_id },
9027
+ appVersion: row.app_version,
9028
+ channel: row.channel,
9029
+ cohort: row.metadata.cohort,
9030
+ fromBundleId: row.from_bundle_id,
9031
+ id: row.id,
9032
+ installId: row.install_id,
9033
+ platform: row.platform,
9034
+ receivedAtMs: row.received_at_ms,
9035
+ toBundleId: row.to_bundle_id,
9036
+ type: row.type,
9037
+ userId: row.user_id,
9038
+ ...eventDetails(row)
8887
9039
  });
8888
- /** Installations whose update to a release failed, since its first failure: kept, at bucket 0. */
8889
- const lifetimeSketches = () => defineAggregate(identityFields, {
8890
- key: ["identity", "bucket_start_ms"],
8891
- distinct: ["failed_users"],
8892
- shards: SKETCH_SHARDS,
8893
- batched: true,
8894
- indexes: { window }
9040
+ const toInstallationRow = (row) => ({
9041
+ ...eventDetails(row).httpResponse ? { httpResponse: eventDetails(row).httpResponse } : {},
9042
+ ...row.metadata.min_bundle_id === void 0 ? {} : { minBundleId: row.metadata.min_bundle_id },
9043
+ appVersion: row.app_version,
9044
+ channel: row.channel,
9045
+ cohort: row.metadata.cohort,
9046
+ installId: row.install_id,
9047
+ lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
9048
+ pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
9049
+ pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
9050
+ latestStatus: row.type,
9051
+ platform: row.platform,
9052
+ receivedAtMs: row.received_at_ms,
9053
+ userId: row.user_id
8895
9054
  });
8896
9055
  /**
8897
- * Update failures by what failed, a row an hour. `release_id` is the failed
8898
- * update's target release, or empty for a failed check or an unknown target.
8899
- * `stage` and `reason` say where and why it failed, and `detail` what else
8900
- * the client knew, as the JSON array `[resource, httpStatus, originCode,
8901
- * transport]`. Every index is in key order, so its counters stay blind
8902
- * increments.
9056
+ * Where a list without `sinceMs` starts: 90 days before its cutoff for the
9057
+ * global and bundle lists, at the first event for installation history.
8903
9058
  */
8904
- const insightsFailures = (retention) => defineAggregate({
8905
- platform: {
8906
- type: "string",
8907
- maxLength: 16
8908
- },
8909
- channel: { type: "string" },
8910
- bucket_start_ms: { type: "integer" },
8911
- release_id: {
8912
- type: "string",
8913
- maxLength: 36
8914
- },
8915
- stage: {
8916
- type: "string",
8917
- maxLength: 16
8918
- },
8919
- reason: {
8920
- type: "string",
8921
- maxLength: 64
8922
- },
8923
- detail: {
8924
- type: "string",
8925
- maxLength: 160
9059
+ const defaultEventStart = (filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
9060
+ /** Whether a cursor's row lies inside the list's range. */
9061
+ const isInsideBounds = ({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs;
9062
+ /** A page's range: its cursor's, which the request may repeat but not change. */
9063
+ const readEventBounds = (input, cursor, filter) => {
9064
+ const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
9065
+ if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
9066
+ const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? defaultEventStart(filter, beforeReceivedAtMs) : requireTimestamp(input.sinceMs, "event start"));
9067
+ const bounds = {
9068
+ beforeReceivedAtMs,
9069
+ sinceMs
9070
+ };
9071
+ if (sinceMs > beforeReceivedAtMs || input.sinceMs !== void 0 && input.sinceMs !== sinceMs || cursor !== void 0 && !isInsideBounds(cursor, bounds)) throw new InsightsBadRequestError("Insights cursor or range does not match the requested event start.");
9072
+ if (filter.kind !== "installationMovement" && beforeReceivedAtMs - sinceMs > 7776e6) throw new InsightsBadRequestError("Insights event lists cover at most 90 days: send a sinceMs no more than 90 days before beforeReceivedAtMs.");
9073
+ return bounds;
9074
+ };
9075
+ const pageEventRows = async (model, input, filter, map) => {
9076
+ const limit = readLimit(input.limit);
9077
+ const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
9078
+ const bounds = readEventBounds(input, cursor, filter);
9079
+ const query = {
9080
+ filter,
9081
+ ...bounds,
9082
+ ...cursor === void 0 ? {} : { after: cursor.after },
9083
+ limit
9084
+ };
9085
+ const rows = await model.listEvents(query);
9086
+ assertEventRows(rows, query);
9087
+ const last = rows.at(-1);
9088
+ return {
9089
+ beforeReceivedAtMs: bounds.beforeReceivedAtMs,
9090
+ data: rows.map(map),
9091
+ nextCursor: rows.length === limit && last ? encodeCursor({
9092
+ after: {
9093
+ id: last.id,
9094
+ receivedAtMs: last.received_at_ms
9095
+ },
9096
+ ...bounds,
9097
+ kind: "events",
9098
+ filter,
9099
+ version: 2
9100
+ }) : null
9101
+ };
9102
+ };
9103
+ const assertInstallationRows = (rows, input) => {
9104
+ if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
9105
+ for (let index = 0; index < rows.length; index += 1) {
9106
+ const row = rows[index];
9107
+ const previous = rows[index - 1];
9108
+ if (!row || row.user_id !== input.userId || input.afterInstallId !== void 0 && compareInsightsText(row.install_id, input.afterInstallId) <= 0 || previous !== void 0 && compareInsightsText(previous.install_id, row.install_id) >= 0) throw new Error("Insights database returned invalid installation rows.");
8926
9109
  }
8927
- }, {
8928
- key: [
8929
- "platform",
8930
- "channel",
8931
- "bucket_start_ms",
8932
- "release_id",
8933
- "stage",
8934
- "reason",
8935
- "detail"
8936
- ],
8937
- counters: ["events"],
8938
- shards: COUNTER_SHARDS,
8939
- batched: true,
8940
- indexes: { byScope: {
8941
- eq: ["platform", "channel"],
8942
- sort: ["bucket_start_ms"]
8943
- } },
8944
- retention
8945
- });
8946
- /** Installations counted in the UTC day of their latest event, so reads sum whole UTC days. */
8947
- const insightsDistribution = (retention) => defineAggregate({
8948
- channel: { type: "string" },
8949
- platform: {
8950
- type: "string",
8951
- maxLength: 16
8952
- },
8953
- app_version: { type: "string" },
8954
- release_id: {
8955
- type: "string",
8956
- maxLength: 36
8957
- },
8958
- bucket_start_ms: { type: "integer" }
8959
- }, {
8960
- key: [
8961
- "channel",
8962
- "platform",
8963
- "app_version",
8964
- "release_id",
8965
- "bucket_start_ms"
8966
- ],
8967
- gauges: ["latest_installations"],
8968
- shards: GAUGE_SHARDS,
8969
- batched: true,
8970
- indexes: {
8971
- byScope: {
8972
- eq: ["channel", "platform"],
8973
- sort: ["bucket_start_ms"]
8974
- },
8975
- byVersion: {
8976
- eq: [
8977
- "channel",
8978
- "platform",
8979
- "app_version"
8980
- ],
8981
- sort: ["bucket_start_ms"]
8982
- }
8983
- },
8984
- retention
8985
- });
8986
- /** Latest events by the bundle they came from or went to, bucketed like the distribution. */
8987
- const insightsLatestByBundle = (retention) => defineAggregate({
8988
- platform: {
8989
- type: "string",
8990
- maxLength: 16
8991
- },
8992
- channel: { type: "string" },
8993
- bundle_field: {
8994
- type: "string",
8995
- maxLength: 16
8996
- },
8997
- bundle_id: {
8998
- type: "string",
8999
- maxLength: 36
9110
+ };
9111
+ const createInsightsProvider = (model) => Object.freeze({
9112
+ async appendBundleEvent(input) {
9113
+ const event = createBundleEventRow(input);
9114
+ await model.recordEvent({ event });
9000
9115
  },
9001
- type: {
9002
- type: "string",
9003
- maxLength: 32
9116
+ listEvents(input) {
9117
+ const filter = input.bundle === void 0 ? { kind: "all" } : {
9118
+ kind: "bundle",
9119
+ ...bundleFilter(input.bundle)
9120
+ };
9121
+ return pageEventRows(model, input, filter, toEventHistoryRow);
9004
9122
  },
9005
- bucket_start_ms: { type: "integer" }
9006
- }, {
9007
- key: [
9008
- "platform",
9009
- "channel",
9010
- "bundle_field",
9011
- "bundle_id",
9012
- "type",
9013
- "bucket_start_ms"
9014
- ],
9015
- gauges: ["installations"],
9016
- shards: GAUGE_SHARDS,
9017
- batched: true,
9018
- indexes: { byBundle: {
9019
- eq: [
9020
- "platform",
9021
- "channel",
9022
- "bundle_field",
9023
- "bundle_id",
9024
- "type"
9025
- ],
9026
- sort: ["bucket_start_ms"]
9027
- } },
9028
- retention
9029
- });
9030
- /**
9031
- * Stored events by outcome and the bundle a bundle filter matches, a row an
9032
- * hour, which a failed check has none of; and every stored event, a row a UTC
9033
- * day (`DAILY_EVENTS`). Bundle counts sum the hourly rows, and event lists
9034
- * read either to skip days without events.
9035
- */
9036
- const insightsOutcomes = (retention) => defineAggregate({
9037
- platform: {
9038
- type: "string",
9039
- maxLength: 16
9123
+ async listInstallationEvents(input) {
9124
+ if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
9125
+ const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH$1);
9126
+ return pageEventRows(model, input, {
9127
+ kind: "installationMovement",
9128
+ installId
9129
+ }, (row) => toEventHistoryRow(row));
9040
9130
  },
9041
- channel: { type: "string" },
9042
- type: {
9043
- type: "string",
9044
- maxLength: 32
9131
+ async getInstallation({ installId }) {
9132
+ const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH$1);
9133
+ const rows = await model.findLatestEvents({ installId: normalizedInstallId });
9134
+ const row = rows[0] ?? null;
9135
+ if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
9136
+ return row === null ? null : toInstallationRow(row);
9045
9137
  },
9046
- bundle_ref: {
9047
- type: "string",
9048
- maxLength: 41
9138
+ async pageInstallationsByCurrentUserId(input) {
9139
+ const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH$1);
9140
+ const limit = readLimit(input.limit);
9141
+ const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
9142
+ const databaseInput = {
9143
+ userId,
9144
+ ...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
9145
+ limit
9146
+ };
9147
+ const rows = await model.findLatestEvents(databaseInput);
9148
+ assertInstallationRows(rows, databaseInput);
9149
+ const last = rows.at(-1);
9150
+ return {
9151
+ data: rows.map(toInstallationRow),
9152
+ nextCursor: rows.length === limit && last ? encodeCursor({
9153
+ afterInstallId: last.install_id,
9154
+ kind: "user-installations",
9155
+ userId,
9156
+ version: 1
9157
+ }) : null
9158
+ };
9049
9159
  },
9050
- bucket_start_ms: { type: "integer" }
9051
- }, {
9052
- key: [
9053
- "platform",
9054
- "channel",
9055
- "type",
9056
- "bundle_ref",
9057
- "bucket_start_ms"
9058
- ],
9059
- counters: ["events"],
9060
- shards: COUNTER_SHARDS,
9061
- batched: true,
9062
- indexes: { byRef: {
9063
- eq: [
9064
- "platform",
9065
- "channel",
9066
- "type",
9067
- "bundle_ref"
9068
- ],
9069
- sort: ["bucket_start_ms"]
9070
- } },
9071
- retention
9160
+ async getReportingOverview(input) {
9161
+ const scope = readScope$1(input);
9162
+ const { window } = input;
9163
+ if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
9164
+ const now = Date.now();
9165
+ const beforeReceivedAtMs = now - now % HOUR_MS + (now % HOUR_MS === 0 ? 0 : HOUR_MS);
9166
+ const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
9167
+ const sinceMs = reach - reach % DAY_MS;
9168
+ const measure = async (count) => {
9169
+ const value = await count;
9170
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
9171
+ return {
9172
+ count: value,
9173
+ measuredAtMs: Date.now()
9174
+ };
9175
+ };
9176
+ const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
9177
+ const reporting = measure(model.countLatestEvents({
9178
+ ...scope,
9179
+ sinceMs
9180
+ }));
9181
+ if (bundleId === void 0) return {
9182
+ ...scope,
9183
+ window,
9184
+ sinceMs,
9185
+ beforeReceivedAtMs,
9186
+ reportingInstallations: await reporting
9187
+ };
9188
+ const countOutcome = (outcome) => measure(model.countEvents({
9189
+ filter: bundleFilter({
9190
+ ...scope,
9191
+ bundleId,
9192
+ outcome
9193
+ }),
9194
+ sinceMs,
9195
+ beforeReceivedAtMs
9196
+ }));
9197
+ const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, failedReports] = await Promise.all([
9198
+ reporting,
9199
+ measure(model.countLatestEvents({
9200
+ ...scope,
9201
+ sinceMs,
9202
+ bundle: [{
9203
+ field: "from_bundle_id",
9204
+ value: bundleId,
9205
+ types: ["UPDATE_DOWNLOADED"]
9206
+ }, {
9207
+ field: "to_bundle_id",
9208
+ value: bundleId,
9209
+ types: [
9210
+ "UNCHANGED",
9211
+ "UPDATE_APPLIED",
9212
+ "RECOVERED"
9213
+ ]
9214
+ }]
9215
+ })),
9216
+ countOutcome("downloaded"),
9217
+ countOutcome("applied"),
9218
+ countOutcome("recovered"),
9219
+ countOutcome("failed")
9220
+ ]);
9221
+ return {
9222
+ ...scope,
9223
+ window,
9224
+ sinceMs,
9225
+ beforeReceivedAtMs,
9226
+ reportingInstallations,
9227
+ bundle: {
9228
+ bundleId,
9229
+ reportingInstallations: bundleInstallations,
9230
+ downloadedReports,
9231
+ appliedReports,
9232
+ recoveredReports,
9233
+ failedReports
9234
+ }
9235
+ };
9236
+ }
9072
9237
  });
9073
- /**
9074
- * The `insights_outcomes` identity that counts every stored event, for the
9075
- * global event list. Its bucket is a UTC day, not an hour. Every event's
9076
- * platform is ios or android, so no outcome row of a bundle filter shares it.
9077
- */
9078
- const DAILY_EVENTS = {
9079
- platform: "*",
9080
- channel: "",
9081
- type: "",
9082
- bundle_ref: ""
9083
- };
9084
- /**
9085
- * Insights' tables, each expiring after its period. The periods are values
9086
- * the engine applies at runtime; the tables and indexes are the same for
9087
- * any periods.
9088
- */
9089
- const createInsightsSchema = ({ rawDays, dailyDays } = DEFAULT_INSIGHTS_RETENTION) => {
9090
- const hourly = bucketRetention(rawDays);
9091
- const daily = bucketRetention(dailyDays);
9092
- return {
9093
- bundle_events: bundleEvents(rawDays),
9094
- bundle_event_heads: bundleEventHeads(dailyDays),
9095
- /** Hourly counters and sketches: the raw period. */
9096
- insights_overview: counters(hourly),
9097
- insights_sketches: sketches(hourly),
9098
- /** Daily counters and sketches: the daily period. */
9099
- insights_overview_daily: counters(daily),
9100
- insights_sketches_daily: sketches(daily),
9101
- /** A release's counters since its first event, and its failed installations: kept. */
9102
- insights_overview_lifetime: lifetimeCounters(),
9103
- insights_sketches_lifetime: lifetimeSketches(),
9104
- insights_distribution: insightsDistribution(daily),
9105
- insights_latest_by_bundle: insightsLatestByBundle(daily),
9106
- insights_outcomes: insightsOutcomes(hourly),
9107
- insights_failures: insightsFailures(hourly)
9108
- };
9109
- };
9110
- createInsightsSchema();
9111
9238
  //#endregion
9112
9239
  //#region ../insights/dist/server/overview.mjs
9113
9240
  const encodeIdentity = (identity) => [
@@ -9175,18 +9302,19 @@ const insightsOverviewDeltas = (event) => {
9175
9302
  bucketStartMs
9176
9303
  }, windowed);
9177
9304
  };
9178
- if (event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
9305
+ const implied = event.metadata.implied_download === true ? 1 : 0;
9306
+ if (!isLateEvent(event) && event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
9179
9307
  downloads: 1,
9180
9308
  applies: 0,
9181
9309
  failedLaunches: 0
9182
9310
  });
9183
- else if (event.type === "UPDATE_APPLIED") metric(event.to_release_id, {
9184
- downloads: 0,
9311
+ else if (!isLateEvent(event) && isLaunch(event)) metric(event.to_release_id, {
9312
+ downloads: implied,
9185
9313
  applies: 1,
9186
9314
  failedLaunches: 0
9187
9315
  });
9188
- else if (event.type === "RECOVERED") metric(event.from_release_id, {
9189
- downloads: 0,
9316
+ else if (!isLateEvent(event) && event.type === "RECOVERED") metric(event.from_release_id, {
9317
+ downloads: implied,
9190
9318
  applies: 0,
9191
9319
  failedLaunches: 1
9192
9320
  });
@@ -9210,8 +9338,8 @@ const insightsOverviewDeltas = (event) => {
9210
9338
  };
9211
9339
  //#endregion
9212
9340
  //#region ../insights/dist/server/recordEvent.mjs
9213
- const hourOf = (ms) => ms - ms % HOUR_MS;
9214
- const dayOf = (ms) => ms - ms % DAY_MS;
9341
+ const hourOf = (ms) => ms - ms % HOUR_MS$1;
9342
+ const dayOf = (ms) => ms - ms % DAY_MS$1;
9215
9343
  /** The `identity` of an overview or sketch row: its scope and period, hashed. */
9216
9344
  const insightsIdentity = (identity) => insightsOverviewId({
9217
9345
  ...identity,
@@ -9220,13 +9348,16 @@ const insightsIdentity = (identity) => insightsOverviewId({
9220
9348
  bucketStartMs: 0
9221
9349
  });
9222
9350
  /**
9223
- * The bucket a head's gauges count it in, the UTC day of its event, and the
9224
- * release its distribution row names: the one it runs, which a download
9225
- * leaves at its source.
9351
+ * The bucket a head's gauges count it in, the UTC day of its event, and what
9352
+ * its distribution row names: the release it runs, which a download leaves
9353
+ * at its source, and the built-in bundle, when it runs the native build's.
9354
+ * A head stored without the built-in bundle ID counts in no built-in row, so
9355
+ * taking it back moves none.
9226
9356
  */
9227
9357
  const gaugeSlot = (head) => ({
9228
9358
  bucket: dayOf(head.received_at_ms),
9229
- releaseId: head.type === "UPDATE_DOWNLOADED" ? head.from_release_id : head.to_release_id
9359
+ releaseId: head.type === "UPDATE_DOWNLOADED" ? head.from_release_id : head.to_release_id,
9360
+ builtinBundleId: runningBuiltinBundleId(head)
9230
9361
  });
9231
9362
  /**
9232
9363
  * The `insights_latest_by_bundle` field of a head's (from, to) pair. A head
@@ -9237,11 +9368,12 @@ const PAIR_FIELD = "from_to";
9237
9368
  /** The pair gauge's `bundle_id`: a hash of the two bundle ids, which fit no single 36-character column. */
9238
9369
  const bundlePairKey = (from, to) => insightsKey(`${from.length}:${from}${to.length}:${to}`);
9239
9370
  /**
9240
- * Moves a head's gauges: the distribution row, one row per bundle it
9241
- * references, and its pair; -1 takes back the head an event replaces.
9371
+ * Moves a head's gauges: the distribution row, and the built-in one when it
9372
+ * runs its build's built-in bundle, one row per bundle it references, and
9373
+ * its pair; -1 takes back the head an event replaces.
9242
9374
  */
9243
9375
  const countHead = (tx, head, delta) => {
9244
- const { bucket, releaseId } = gaugeSlot(head);
9376
+ const { bucket, releaseId, builtinBundleId } = gaugeSlot(head);
9245
9377
  const shardBy = head.install_id;
9246
9378
  tx.aggregate("insights_distribution", {
9247
9379
  channel: head.channel,
@@ -9250,6 +9382,14 @@ const countHead = (tx, head, delta) => {
9250
9382
  release_id: releaseId ?? "",
9251
9383
  bucket_start_ms: bucket
9252
9384
  }, { latest_installations: delta }, { shardBy });
9385
+ if (builtinBundleId !== null) tx.aggregate("insights_builtin_distribution", {
9386
+ channel: head.channel,
9387
+ platform: head.platform,
9388
+ app_version: head.app_version,
9389
+ release_id: releaseId ?? "",
9390
+ builtin_bundle_id: builtinBundleId,
9391
+ bucket_start_ms: bucket
9392
+ }, { latest_installations: delta }, { shardBy });
9253
9393
  for (const field of ["from_bundle_id", "to_bundle_id"]) {
9254
9394
  const bundleId = head[field];
9255
9395
  if (bundleId === null) continue;
@@ -9330,16 +9470,16 @@ const countEvent = (tx, event) => {
9330
9470
  }
9331
9471
  };
9332
9472
  /**
9333
- * A stored event's outcome rows: its bundle filter's hour, which a failed
9334
- * check has none of, and every stored event's UTC day.
9473
+ * A stored event's outcome rows: its bundle filter's hour, when it has one
9474
+ * (`bundleRefsOf`), and every stored event's UTC day.
9335
9475
  */
9336
9476
  const countOutcome = (tx, event) => {
9337
9477
  const shardBy = event.install_id;
9338
- if (!isFailedCheck(event)) tx.aggregate("insights_outcomes", {
9478
+ for (const bundleRef of bundleRefsOf(event)) tx.aggregate("insights_outcomes", {
9339
9479
  platform: event.platform,
9340
9480
  channel: event.channel,
9341
9481
  type: event.type,
9342
- bundle_ref: event.type === "RECOVERED" ? `from:${event.from_bundle_id}` : `to:${event.to_bundle_id}`,
9482
+ bundle_ref: bundleRef,
9343
9483
  bucket_start_ms: hourOf(event.received_at_ms)
9344
9484
  }, { events: 1 }, { shardBy });
9345
9485
  tx.aggregate("insights_outcomes", {
@@ -9443,6 +9583,126 @@ const countFailure = (tx, event) => {
9443
9583
  }, { events: 1 }, { shardBy });
9444
9584
  };
9445
9585
  const isNewer = (event, head) => event.received_at_ms !== head.received_at_ms ? event.received_at_ms > head.received_at_ms : compareUtf8(event.id, head.id) > 0;
9586
+ /** What a head says its installation runs: a download leaves it at the source. */
9587
+ const runningOf = (head) => head.type === "UPDATE_DOWNLOADED" ? {
9588
+ bundle_id: head.from_bundle_id,
9589
+ release_id: head.from_release_id
9590
+ } : {
9591
+ bundle_id: head.to_bundle_id,
9592
+ release_id: head.to_release_id
9593
+ };
9594
+ /** The native build a report or head came from, when its SDK says. */
9595
+ const minBundleIdOf = (row) => row.metadata?.min_bundle_id;
9596
+ /** When the client made a report, from the UUIDv7 it gave it. */
9597
+ const madeAt = (id) => isUUIDv7(id) ? extractTimestampFromUUIDv7(id) : null;
9598
+ /**
9599
+ * A report that arrived after its installation moved past it: a download or
9600
+ * an apply of the bundle the head already runs, or an UNCHANGED report of the
9601
+ * bundle an apply head left, made before that apply. A reload can cut one
9602
+ * runtime's report short and deliver it after the next runtime's. It stays
9603
+ * in history but moves and counts nothing.
9604
+ */
9605
+ const isLate = (event, head) => {
9606
+ if (head === null) return false;
9607
+ if (event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED") return event.to_bundle_id === runningOf(head).bundle_id;
9608
+ if (event.type === "UNCHANGED" && head.type === "UPDATE_APPLIED" && event.to_bundle_id === head.from_bundle_id) {
9609
+ const made = madeAt(event.id);
9610
+ const applied = madeAt(head.id);
9611
+ return made !== null && applied !== null && made < applied;
9612
+ }
9613
+ return false;
9614
+ };
9615
+ /**
9616
+ * What an UNCHANGED report changes against its installation's head: the
9617
+ * running bundle or release, the app version, the channel, or the native
9618
+ * build (when both name it), with the head's values before it; `first_seen`
9619
+ * when the installation has no head. A report the head already answers, one
9620
+ * older than the head, a user switch and any other event type change
9621
+ * nothing, so no row keeps them.
9622
+ */
9623
+ const changeOf = (event, head) => {
9624
+ if (event.type !== "UNCHANGED") return null;
9625
+ if (head === null) return {
9626
+ kinds: ["first_seen"],
9627
+ previous: null
9628
+ };
9629
+ if (!isNewer(event, head)) return null;
9630
+ const running = runningOf(head);
9631
+ const build = minBundleIdOf(event);
9632
+ const headBuild = minBundleIdOf(head);
9633
+ const kinds = [];
9634
+ if (event.to_bundle_id !== running.bundle_id) kinds.push("bundle");
9635
+ if (event.to_release_id !== running.release_id) kinds.push("release");
9636
+ if (event.app_version !== head.app_version) kinds.push("app_version");
9637
+ if (event.channel !== head.channel) kinds.push("channel");
9638
+ if (build !== void 0 && headBuild !== void 0 && build !== headBuild) kinds.push("native_build");
9639
+ return kinds.length === 0 ? null : {
9640
+ kinds,
9641
+ previous: {
9642
+ bundle_id: running.bundle_id,
9643
+ release_id: running.release_id,
9644
+ app_version: head.app_version,
9645
+ channel: head.channel,
9646
+ ...headBuild === void 0 ? {} : { min_bundle_id: headBuild }
9647
+ }
9648
+ };
9649
+ };
9650
+ /**
9651
+ * Whether a launch or a crash comes with no download report for its target:
9652
+ * its head shows neither that download nor the target already running. The
9653
+ * native build's built-in bundle is never downloaded, and without a head the
9654
+ * server knows nothing of the installation's past, so it implies nothing.
9655
+ */
9656
+ const impliesDownload = (event, head) => {
9657
+ const target = event.type === "RECOVERED" ? {
9658
+ bundle: event.from_bundle_id,
9659
+ release: event.from_release_id
9660
+ } : isLaunch(event) ? {
9661
+ bundle: event.to_bundle_id,
9662
+ release: event.to_release_id
9663
+ } : null;
9664
+ if (target === null || target.release === null || target.bundle === minBundleIdOf(event)) return false;
9665
+ if (head === null) return false;
9666
+ if (head.type === "UPDATE_DOWNLOADED" && head.to_bundle_id === target.bundle) return false;
9667
+ return runningOf(head).bundle_id !== target.bundle;
9668
+ };
9669
+ /**
9670
+ * The event as recorded, with what only the server sets: what a kept
9671
+ * UNCHANGED report changed, a late report's flag, and a download its launch
9672
+ * or crash implied.
9673
+ */
9674
+ const recordedOf = (event, head) => {
9675
+ const { change: _change, late: _late, implied_download: _implied, ...metadata } = event.metadata;
9676
+ const reported = {
9677
+ ...event,
9678
+ metadata
9679
+ };
9680
+ const late = isLate(reported, head);
9681
+ const change = late ? null : changeOf(reported, head);
9682
+ const withChange = change === null ? reported : {
9683
+ ...reported,
9684
+ metadata: {
9685
+ ...metadata,
9686
+ change
9687
+ }
9688
+ };
9689
+ return {
9690
+ recorded: late && reported.type !== "UNCHANGED" ? {
9691
+ ...reported,
9692
+ metadata: {
9693
+ ...metadata,
9694
+ late: true
9695
+ }
9696
+ } : !late && impliesDownload(withChange, head) ? {
9697
+ ...withChange,
9698
+ metadata: {
9699
+ ...withChange.metadata,
9700
+ implied_download: true
9701
+ }
9702
+ } : withChange,
9703
+ late
9704
+ };
9705
+ };
9446
9706
  /** What an UNCHANGED report must share with its installation's head to repeat it. */
9447
9707
  const REPEATED_FIELDS = [
9448
9708
  "channel",
@@ -9477,32 +9737,33 @@ const recordEvent = (db, event) => {
9477
9737
  const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
9478
9738
  if (existing !== null || previous?.id === event.id) return;
9479
9739
  if (previous !== null && repeatsHead(event, previous)) return;
9480
- if (event.type !== "UNCHANGED") {
9481
- tx.create("bundle_events", event);
9482
- countOutcome(tx, event);
9740
+ const { recorded, late } = recordedOf(event, previous);
9741
+ if (recorded.type !== "UNCHANGED" || recorded.metadata.change !== void 0 && !late) {
9742
+ tx.create("bundle_events", recorded);
9743
+ countOutcome(tx, recorded);
9483
9744
  }
9484
- if (event.type === "UPDATE_FAILED") {
9485
- countFailure(tx, event);
9745
+ if (recorded.type === "UPDATE_FAILED") {
9746
+ countFailure(tx, recorded);
9486
9747
  return;
9487
9748
  }
9488
- countEvent(tx, event);
9489
- if (previous !== null && !isNewer(event, previous)) return;
9490
- if (previous === null) tx.create("bundle_event_heads", event);
9749
+ countEvent(tx, recorded);
9750
+ if (previous !== null && (late || !isNewer(recorded, previous))) return;
9751
+ if (previous === null) tx.create("bundle_event_heads", recorded);
9491
9752
  else {
9492
- const { install_id: _, ...fields } = event;
9753
+ const { install_id: _, ...fields } = recorded;
9493
9754
  countHead(tx, previous, -1);
9494
9755
  tx.update("bundle_event_heads", previous, fields);
9495
9756
  }
9496
- countHead(tx, event, 1);
9757
+ countHead(tx, recorded, 1);
9497
9758
  });
9498
9759
  };
9499
9760
  //#endregion
9500
9761
  //#region ../insights/dist/server/reads.mjs
9501
9762
  const PAGE = 500;
9502
- const hourFloor = (ms) => ms - ms % HOUR_MS;
9503
- const hourCeil = (ms) => hourFloor(ms + HOUR_MS - 1);
9504
- const dayFloor = (ms) => ms - ms % DAY_MS;
9505
- const dayCeil = (ms) => dayFloor(ms + DAY_MS - 1);
9763
+ const hourFloor = (ms) => ms - ms % HOUR_MS$1;
9764
+ const hourCeil = (ms) => hourFloor(ms + HOUR_MS$1 - 1);
9765
+ const dayFloor = (ms) => ms - ms % DAY_MS$1;
9766
+ const dayCeil = (ms) => dayFloor(ms + DAY_MS$1 - 1);
9506
9767
  /** Every page of one read, in order. */
9507
9768
  const drain = async (read) => {
9508
9769
  const rows = [];
@@ -9531,12 +9792,11 @@ const EVENT_FIELDS = [
9531
9792
  ];
9532
9793
  /** A stored event or head, as the event row it holds. */
9533
9794
  const toEvent = (row) => Object.fromEntries(EVENT_FIELDS.map((field) => [field, row[field]]));
9534
- const bundleRef = (filter) => filter.type === "RECOVERED" ? `from:${filter.fromBundleId}` : `to:${filter.toBundleId}`;
9535
9795
  const scopeOf = (filter) => ({
9536
9796
  platform: filter.platform,
9537
9797
  channel: filter.channel,
9538
9798
  type: filter.type,
9539
- bundle_ref: bundleRef(filter)
9799
+ bundle_ref: bundleRefOfFilter(filter)
9540
9800
  });
9541
9801
  /** One UTC day of a global or bundle list, newest first. */
9542
9802
  const eventsOfDay = (db, filter, day, range, limit) => filter.kind === "all" ? db.findMany("bundle_events", {
@@ -9609,7 +9869,7 @@ const listEvents = async (db, input) => {
9609
9869
  while (day !== void 0 && day >= bottom && rows.length < limit) {
9610
9870
  const page = await eventsOfDay(db, filter, day, range, limit - rows.length);
9611
9871
  rows.push(...page.rows.map(toEvent));
9612
- day = page.rows.length > 0 ? day - DAY_MS : day > bottom ? await newestDayBelow(db, filter, day, since) : void 0;
9872
+ day = page.rows.length > 0 ? day - DAY_MS$1 : day > bottom ? await newestDayBelow(db, filter, day, since) : void 0;
9613
9873
  }
9614
9874
  return rows;
9615
9875
  };
@@ -9630,7 +9890,7 @@ const findLatestEvents = async (db, input) => {
9630
9890
  /** Raw events of one bundle filter over [from, to), which spans at most two partial hours. */
9631
9891
  const countRaw = async (db, filter, from, to) => {
9632
9892
  let total = 0;
9633
- for (let day = dayFloor(from); from < to && day < to; day += DAY_MS) {
9893
+ for (let day = dayFloor(from); from < to && day < to; day += DAY_MS$1) {
9634
9894
  const rows = await drain((page) => db.findMany("bundle_events", {
9635
9895
  index: "byBundle",
9636
9896
  where: {
@@ -9754,8 +10014,8 @@ const countLatestEvents = async (db, input) => {
9754
10014
  };
9755
10015
  /** The oldest whole hour and UTC day that hourly and daily rows still hold. */
9756
10016
  const retained = (now, { rawDays, dailyDays }) => ({
9757
- hour: hourCeil(now - rawDays * DAY_MS),
9758
- day: dayCeil(now - dailyDays * DAY_MS)
10017
+ hour: hourCeil(now - rawDays * DAY_MS$1),
10018
+ day: dayCeil(now - dailyDays * DAY_MS$1)
9759
10019
  });
9760
10020
  /**
9761
10021
  * Whether the rows a window reads still hold all of it; an older start is
@@ -9905,26 +10165,49 @@ const getAppUsage = async (db, input, now, retention) => {
9905
10165
  lt: timeRange.end
9906
10166
  };
9907
10167
  const distribution = [];
9908
- for (const platform of reported) distribution.push(...await drain((page) => input.appVersion === void 0 ? db.findAggregates("insights_distribution", {
9909
- index: "byScope",
9910
- where: {
9911
- channel: input.channel,
9912
- platform
9913
- },
9914
- range,
9915
- limit: PAGE,
9916
- ...page
9917
- }) : db.findAggregates("insights_distribution", {
9918
- index: "byVersion",
9919
- where: {
9920
- channel: input.channel,
9921
- platform,
9922
- app_version: input.appVersion
9923
- },
9924
- range,
9925
- limit: PAGE,
9926
- ...page
9927
- })));
10168
+ const builtinDistribution = [];
10169
+ for (const platform of reported) {
10170
+ distribution.push(...await drain((page) => input.appVersion === void 0 ? db.findAggregates("insights_distribution", {
10171
+ index: "byScope",
10172
+ where: {
10173
+ channel: input.channel,
10174
+ platform
10175
+ },
10176
+ range,
10177
+ limit: PAGE,
10178
+ ...page
10179
+ }) : db.findAggregates("insights_distribution", {
10180
+ index: "byVersion",
10181
+ where: {
10182
+ channel: input.channel,
10183
+ platform,
10184
+ app_version: input.appVersion
10185
+ },
10186
+ range,
10187
+ limit: PAGE,
10188
+ ...page
10189
+ })));
10190
+ builtinDistribution.push(...await drain((page) => input.appVersion === void 0 ? db.findAggregates("insights_builtin_distribution", {
10191
+ index: "byScope",
10192
+ where: {
10193
+ channel: input.channel,
10194
+ platform
10195
+ },
10196
+ range,
10197
+ limit: PAGE,
10198
+ ...page
10199
+ }) : db.findAggregates("insights_builtin_distribution", {
10200
+ index: "byVersion",
10201
+ where: {
10202
+ channel: input.channel,
10203
+ platform,
10204
+ app_version: input.appVersion
10205
+ },
10206
+ range,
10207
+ limit: PAGE,
10208
+ ...page
10209
+ })));
10210
+ }
9928
10211
  const points = [];
9929
10212
  for (let start = timeRange.start; start < timeRange.end; start += intervalMs) points.push({
9930
10213
  startMs: start,
@@ -9933,22 +10216,31 @@ const getAppUsage = async (db, input, now, retention) => {
9933
10216
  const versions = /* @__PURE__ */ new Map();
9934
10217
  const platforms = /* @__PURE__ */ new Map();
9935
10218
  const bundles = /* @__PURE__ */ new Map();
9936
- for (const row of distribution) {
9937
- const count = row.latest_installations;
9938
- versions.set(row.app_version, (versions.get(row.app_version) ?? 0) + count);
9939
- platforms.set(row.platform, (platforms.get(row.platform) ?? 0) + count);
9940
- const releaseId = row.release_id === "" ? null : row.release_id;
10219
+ const countBundle = (row, releaseId, builtinBundleId, count) => {
9941
10220
  const key = JSON.stringify([
9942
10221
  row.app_version,
9943
10222
  row.platform,
9944
- releaseId
10223
+ releaseId,
10224
+ builtinBundleId
9945
10225
  ]);
9946
10226
  bundles.set(key, {
9947
10227
  appVersion: row.app_version,
9948
10228
  platform: row.platform,
9949
10229
  releaseId,
10230
+ builtinBundleId,
9950
10231
  installations: (bundles.get(key)?.installations ?? 0) + count
9951
10232
  });
10233
+ };
10234
+ for (const row of distribution) {
10235
+ const count = row.latest_installations;
10236
+ versions.set(row.app_version, (versions.get(row.app_version) ?? 0) + count);
10237
+ platforms.set(row.platform, (platforms.get(row.platform) ?? 0) + count);
10238
+ countBundle(row, row.release_id || null, null, count);
10239
+ }
10240
+ for (const row of builtinDistribution) {
10241
+ const releaseId = row.release_id || null;
10242
+ countBundle(row, releaseId, null, -row.latest_installations);
10243
+ countBundle(row, releaseId, row.builtin_bundle_id, row.latest_installations);
9952
10244
  }
9953
10245
  const sortedVersions = byInstallations(versions);
9954
10246
  return {
@@ -9963,7 +10255,7 @@ const getAppUsage = async (db, input, now, retention) => {
9963
10255
  };
9964
10256
  };
9965
10257
  /** The longest period the failures read covers: the console's longest, 30 days. */
9966
- const UPDATE_FAILURES_RANGE_MS = 30 * DAY_MS;
10258
+ const UPDATE_FAILURES_RANGE_MS = 30 * DAY_MS$1;
9967
10259
  const isRange = (range) => range === void 0 || Number.isSafeInteger(range.start) && Number.isSafeInteger(range.end) && range.start >= 0 && range.start < range.end && range.end - range.start <= UPDATE_FAILURES_RANGE_MS;
9968
10260
  const isText = (value, maxLength) => typeof value === "string" && value.length > 0 && value.length <= maxLength;
9969
10261
  const failed = (...rows) => countDistinct(mergeDistinct(rows.flat().map((row) => row.failed_users)));