@hot-updater/firebase 1.0.0-rc.36 → 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.
- package/dist/firebase/functions/index.cjs +1621 -1443
- package/dist/index.d.cts +35 -2
- package/dist/index.d.mts +35 -2
- package/package.json +7 -7
|
@@ -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.
|
|
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,1503 +7681,1560 @@ const apiKeys = (options = {}) => {
|
|
|
7681
7681
|
return markOfficial$1(plugin);
|
|
7682
7682
|
};
|
|
7683
7683
|
//#endregion
|
|
7684
|
-
//#region ../insights/dist/server/
|
|
7685
|
-
const
|
|
7686
|
-
const
|
|
7687
|
-
const
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
const
|
|
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
|
|
7690
|
+
};
|
|
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;
|
|
7692
7708
|
/**
|
|
7693
|
-
*
|
|
7694
|
-
*
|
|
7695
|
-
*
|
|
7696
|
-
* bundle
|
|
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.
|
|
7697
7714
|
*/
|
|
7698
|
-
const
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
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;
|
|
7702
7721
|
};
|
|
7703
|
-
const isIdentity$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 255;
|
|
7704
|
-
const isTextOrNull = (value) => value === null || typeof value === "string";
|
|
7705
|
-
/** Each bundle event field and what it holds. */
|
|
7706
|
-
const BUNDLE_EVENT_FIELDS = {
|
|
7707
|
-
id: (value) => typeof value === "string",
|
|
7708
|
-
type: (value) => value === "UPDATE_DOWNLOADED" || value === "UPDATE_APPLIED" || value === "RECOVERED" || value === "UPDATE_FAILED" || value === "UNCHANGED",
|
|
7709
|
-
install_id: isIdentity$1,
|
|
7710
|
-
user_id: (value) => value === null || isIdentity$1(value),
|
|
7711
|
-
from_bundle_id: isTextOrNull,
|
|
7712
|
-
from_release_id: isTextOrNull,
|
|
7713
|
-
to_release_id: isTextOrNull,
|
|
7714
|
-
to_bundle_id: (value) => typeof value === "string",
|
|
7715
|
-
platform: (value) => value === "ios" || value === "android",
|
|
7716
|
-
app_version: (value) => typeof value === "string",
|
|
7717
|
-
channel: (value) => typeof value === "string",
|
|
7718
|
-
metadata: isDatabaseBundleEventMetadata,
|
|
7719
|
-
received_at_ms: (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
7720
|
-
};
|
|
7721
|
-
const updateStrategyOf = (row) => isRecord$2(row.metadata) ? row.metadata.update_strategy : void 0;
|
|
7722
7722
|
/**
|
|
7723
|
-
*
|
|
7724
|
-
*
|
|
7725
|
-
*
|
|
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.
|
|
7726
7728
|
*/
|
|
7727
|
-
const
|
|
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}`;
|
|
7728
7737
|
/**
|
|
7729
|
-
*
|
|
7730
|
-
*
|
|
7731
|
-
*
|
|
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.
|
|
7732
7742
|
*/
|
|
7733
|
-
const
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
if (!hasEventInvariants(row)) throw new DatabaseAdapterInputError("invalid-data");
|
|
7738
|
-
};
|
|
7739
|
-
//#endregion
|
|
7740
|
-
//#region ../insights/dist/server/contract.mjs
|
|
7741
|
-
const encoder = new TextEncoder();
|
|
7742
|
-
/** Exact UTF-8 byte ordering, without case folding or Unicode normalization. */
|
|
7743
|
-
const compareInsightsText = (left, right) => {
|
|
7744
|
-
const a = encoder.encode(left);
|
|
7745
|
-
const b = encoder.encode(right);
|
|
7746
|
-
for (let index = 0; index < Math.min(a.length, b.length); index += 1) if (a[index] !== b[index]) return a[index] - b[index];
|
|
7747
|
-
return a.length - b.length;
|
|
7748
|
-
};
|
|
7749
|
-
const isWellFormedText = (value) => {
|
|
7750
|
-
for (const character of value) {
|
|
7751
|
-
const point = character.codePointAt(0);
|
|
7752
|
-
if (point >= 55296 && point <= 57343) return false;
|
|
7753
|
-
}
|
|
7754
|
-
return true;
|
|
7755
|
-
};
|
|
7756
|
-
const isIdentity = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && isWellFormedText(value);
|
|
7757
|
-
const isText$1 = (value) => typeof value === "string" && value.length > 0 && isWellFormedText(value);
|
|
7758
|
-
const isTimestamp = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
7759
|
-
const isLimit = (value) => isTimestamp(value) && value >= 1 && value <= 101;
|
|
7760
|
-
const hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
7761
|
-
const hasScope = (value) => (value.platform === "ios" || value.platform === "android") && isText$1(value.channel);
|
|
7762
|
-
const isBundleFilter = (value, withKind = false) => {
|
|
7763
|
-
if (!isRecord$2(value) || !hasScope(value)) return false;
|
|
7764
|
-
const keys = [
|
|
7765
|
-
"platform",
|
|
7766
|
-
"channel",
|
|
7767
|
-
"type",
|
|
7768
|
-
...withKind ? ["kind"] : []
|
|
7769
|
-
];
|
|
7770
|
-
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"]);
|
|
7771
|
-
};
|
|
7772
|
-
const isEventFilter = (value) => {
|
|
7773
|
-
if (!isRecord$2(value)) return false;
|
|
7774
|
-
if (value.kind === "all") return hasOnlyKeys(value, ["kind"]);
|
|
7775
|
-
if (value.kind === "installationMovement") return isIdentity(value.installId) && hasOnlyKeys(value, ["kind", "installId"]);
|
|
7776
|
-
return value.kind === "bundle" && isBundleFilter(value, true);
|
|
7777
|
-
};
|
|
7778
|
-
const validateRow = (model, row, result = false) => {
|
|
7779
|
-
try {
|
|
7780
|
-
validateBundleEventFields(row);
|
|
7781
|
-
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");
|
|
7782
|
-
} catch (error) {
|
|
7783
|
-
if (result) throw new DatabaseAdapterInputError("invalid-result");
|
|
7784
|
-
throw error;
|
|
7785
|
-
}
|
|
7786
|
-
};
|
|
7787
|
-
/** Throws `invalid-data` unless `row` is a complete, well-formed bundle event row. */
|
|
7788
|
-
const assertBundleEventRow = (row) => validateRow("bundle_events", row);
|
|
7789
|
-
/** Downloads, applies, recoveries, and failed updates belong in installation history. */
|
|
7790
|
-
const isInsightsMovementEvent = (event) => event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_APPLIED" || event.type === "RECOVERED" || event.type === "UPDATE_FAILED";
|
|
7791
|
-
const matchesInsightsEventFilter = (event, filter) => {
|
|
7792
|
-
if (filter.kind === "all") return true;
|
|
7793
|
-
if (filter.kind === "installationMovement") return event.install_id === filter.installId && isInsightsMovementEvent(event);
|
|
7794
|
-
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");
|
|
7795
|
-
};
|
|
7796
|
-
const invalidQuery = () => {
|
|
7797
|
-
throw new DatabaseAdapterInputError("invalid-query");
|
|
7798
|
-
};
|
|
7799
|
-
const invalidResult = () => {
|
|
7800
|
-
throw new DatabaseAdapterInputError("invalid-result");
|
|
7801
|
-
};
|
|
7802
|
-
const validateCount = (count) => isTimestamp(count) ? count : invalidResult();
|
|
7803
|
-
const isTimeRange = (value) => isRecord$2(value) && hasOnlyKeys(value, ["start", "end"]) && isTimestamp(value.start) && isTimestamp(value.end) && value.start < value.end;
|
|
7804
|
-
const isReleaseReference = (value) => isRecord$2(value) && hasOnlyKeys(value, [
|
|
7805
|
-
"releaseId",
|
|
7806
|
-
"platform",
|
|
7807
|
-
"channel"
|
|
7808
|
-
]) && isIdentity(value.releaseId) && hasScope(value);
|
|
7809
|
-
const validateAggregateResult = (value) => {
|
|
7810
|
-
const result = isRecord$2(value) ? value : invalidResult();
|
|
7811
|
-
if (!isTimestamp(result.measuredAtMs)) invalidResult();
|
|
7812
|
-
const coverage = result.coverage;
|
|
7813
|
-
if (!isRecord$2(coverage) || coverage.kind !== "complete" && coverage.kind !== "partial" || coverage.sinceMs !== null && !isTimestamp(coverage.sinceMs)) invalidResult();
|
|
7814
|
-
};
|
|
7815
|
-
/** Validate custom and bundled providers at the same public boundary. */
|
|
7816
|
-
const createValidatedInsightsModel = (model) => ({
|
|
7817
|
-
async recordEvent(input) {
|
|
7818
|
-
if (!isRecord$2(input) || !hasOnlyKeys(input, ["event"])) throw new DatabaseAdapterInputError("invalid-data");
|
|
7819
|
-
validateRow("bundle_events", input.event);
|
|
7820
|
-
await model.recordEvent(input);
|
|
7743
|
+
const bundleEvents = (days) => defineTable({
|
|
7744
|
+
id: {
|
|
7745
|
+
type: "string",
|
|
7746
|
+
maxLength: 36
|
|
7821
7747
|
},
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
"sinceMs",
|
|
7826
|
-
"beforeReceivedAtMs",
|
|
7827
|
-
"after",
|
|
7828
|
-
"limit"
|
|
7829
|
-
]) || !isEventFilter(input.filter) || !isLimit(input.limit) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs !== void 0 && !isTimestamp(input.sinceMs)) invalidQuery();
|
|
7830
|
-
const sinceMs = input.sinceMs ?? 0;
|
|
7831
|
-
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();
|
|
7832
|
-
const rows = await model.listEvents(input);
|
|
7833
|
-
if (!Array.isArray(rows) || rows.length > input.limit) invalidResult();
|
|
7834
|
-
let previous = input.after;
|
|
7835
|
-
for (const row of rows) {
|
|
7836
|
-
validateRow("bundle_events", row, true);
|
|
7837
|
-
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();
|
|
7838
|
-
previous = {
|
|
7839
|
-
receivedAtMs: row.received_at_ms,
|
|
7840
|
-
id: row.id
|
|
7841
|
-
};
|
|
7842
|
-
}
|
|
7843
|
-
return rows;
|
|
7748
|
+
type: {
|
|
7749
|
+
type: "string",
|
|
7750
|
+
maxLength: 32
|
|
7844
7751
|
},
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
if (!hasOnlyKeys(input, ["installId"]) || !isIdentity(input.installId)) invalidQuery();
|
|
7849
|
-
} else if (!hasOnlyKeys(input, [
|
|
7850
|
-
"userId",
|
|
7851
|
-
"afterInstallId",
|
|
7852
|
-
"limit"
|
|
7853
|
-
]) || !isIdentity(input.userId) || !isLimit(input.limit) || input.afterInstallId !== void 0 && !isIdentity(input.afterInstallId)) invalidQuery();
|
|
7854
|
-
const rows = await model.findLatestEvents(input);
|
|
7855
|
-
if (!Array.isArray(rows) || rows.length > ("installId" in input ? 1 : input.limit)) invalidResult();
|
|
7856
|
-
let previous = "installId" in input ? void 0 : input.afterInstallId;
|
|
7857
|
-
for (const row of rows) {
|
|
7858
|
-
validateRow("bundle_events", row, true);
|
|
7859
|
-
if ("installId" in input ? row.install_id !== input.installId : row.user_id !== input.userId || previous !== void 0 && compareInsightsText(row.install_id, previous) <= 0) invalidResult();
|
|
7860
|
-
previous = row.install_id;
|
|
7861
|
-
}
|
|
7862
|
-
return rows;
|
|
7752
|
+
install_id: {
|
|
7753
|
+
type: "string",
|
|
7754
|
+
maxLength: 255
|
|
7863
7755
|
},
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
"sinceMs",
|
|
7869
|
-
"bundle"
|
|
7870
|
-
]) || !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, [
|
|
7871
|
-
"field",
|
|
7872
|
-
"value",
|
|
7873
|
-
"types"
|
|
7874
|
-
]) || 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) => ![
|
|
7875
|
-
"UNCHANGED",
|
|
7876
|
-
"UPDATE_DOWNLOADED",
|
|
7877
|
-
"UPDATE_APPLIED",
|
|
7878
|
-
"RECOVERED"
|
|
7879
|
-
].includes(type))))) invalidQuery();
|
|
7880
|
-
return validateCount(await model.countLatestEvents(input));
|
|
7756
|
+
user_id: {
|
|
7757
|
+
type: "string",
|
|
7758
|
+
maxLength: 255,
|
|
7759
|
+
required: false
|
|
7881
7760
|
},
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
"beforeReceivedAtMs"
|
|
7887
|
-
]) || !isBundleFilter(input.filter) || !isTimestamp(input.sinceMs) || !isTimestamp(input.beforeReceivedAtMs) || input.sinceMs > input.beforeReceivedAtMs) invalidQuery();
|
|
7888
|
-
return validateCount(await model.countEvents(input));
|
|
7761
|
+
from_release_id: {
|
|
7762
|
+
type: "string",
|
|
7763
|
+
maxLength: 36,
|
|
7764
|
+
required: false
|
|
7889
7765
|
},
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
"intervalMs"
|
|
7895
|
-
]) || !isBundleFilter(input.filter) || !isTimeRange(input.timeRange) || !isTimestamp(input.intervalMs)) invalidQuery();
|
|
7896
|
-
const { start, end } = input.timeRange;
|
|
7897
|
-
const { intervalMs } = input;
|
|
7898
|
-
if (intervalMs === 0 || intervalMs % 36e5 !== 0 || start % 36e5 !== 0 || (end - start) % intervalMs !== 0 || end - start > 7776e6) invalidQuery();
|
|
7899
|
-
const points = await model.countEventSeries(input);
|
|
7900
|
-
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();
|
|
7901
|
-
return points;
|
|
7766
|
+
from_bundle_id: {
|
|
7767
|
+
type: "string",
|
|
7768
|
+
maxLength: 36,
|
|
7769
|
+
required: false
|
|
7902
7770
|
},
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
return result;
|
|
7771
|
+
to_release_id: {
|
|
7772
|
+
type: "string",
|
|
7773
|
+
maxLength: 36,
|
|
7774
|
+
required: false
|
|
7908
7775
|
},
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
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
|
|
7925
7832
|
}
|
|
7926
7833
|
});
|
|
7927
|
-
//#endregion
|
|
7928
|
-
//#region ../insights/dist/server/model.mjs
|
|
7929
7834
|
/**
|
|
7930
|
-
*
|
|
7931
|
-
*
|
|
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.
|
|
7932
7838
|
*/
|
|
7933
|
-
const
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
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
|
+
}
|
|
7942
7894
|
});
|
|
7943
|
-
//#endregion
|
|
7944
|
-
//#region ../insights/dist/server/official.mjs
|
|
7945
|
-
/**
|
|
7946
|
-
* The brand of Hot Updater's own plugins, a registered symbol that
|
|
7947
|
-
* `@hot-updater/server` reads to reserve the id "insights" for this plugin.
|
|
7948
|
-
* Each official plugin package keeps this copy, so none imports another's
|
|
7949
|
-
* internals for it.
|
|
7950
|
-
*/
|
|
7951
|
-
const OFFICIAL = Symbol.for("@hot-updater/server/official-plugin");
|
|
7952
|
-
/** Marks a plugin one of Hot Updater's own factories made. */
|
|
7953
|
-
const markOfficial = (plugin) => Object.defineProperty(plugin, OFFICIAL, { value: true });
|
|
7954
|
-
//#endregion
|
|
7955
|
-
//#region ../insights/dist/server/errors.mjs
|
|
7956
7895
|
/**
|
|
7957
|
-
*
|
|
7958
|
-
*
|
|
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.
|
|
7959
7907
|
*/
|
|
7960
|
-
|
|
7961
|
-
|
|
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" }
|
|
7962
7918
|
};
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
constructor(maximumBytes) {
|
|
7967
|
-
super(`Event payload exceeds ${maximumBytes} bytes`);
|
|
7968
|
-
this.maximumBytes = maximumBytes;
|
|
7969
|
-
}
|
|
7919
|
+
const window = {
|
|
7920
|
+
eq: ["identity"],
|
|
7921
|
+
sort: ["bucket_start_ms"]
|
|
7970
7922
|
};
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
}
|
|
7979
|
-
function requireStringField(payload, key) {
|
|
7980
|
-
const value = payload[key];
|
|
7981
|
-
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}`);
|
|
7982
|
-
return value;
|
|
7983
|
-
}
|
|
7984
|
-
function requireNullableStringField(payload, key) {
|
|
7985
|
-
if (payload[key] === null) return null;
|
|
7986
|
-
return requireStringField(payload, key);
|
|
7987
|
-
}
|
|
7988
|
-
function requireIdentityField(payload, key) {
|
|
7989
|
-
const value = requireStringField(payload, key);
|
|
7990
|
-
if (value.length > MAX_IDENTITY_LENGTH$2) throw new InsightsBadRequestError(`Invalid event field: ${key}`);
|
|
7991
|
-
return value;
|
|
7992
|
-
}
|
|
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"
|
|
7929
|
+
];
|
|
7993
7930
|
/**
|
|
7994
|
-
*
|
|
7995
|
-
*
|
|
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.
|
|
7996
7934
|
*/
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
}
|
|
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
|
+
});
|
|
8003
7943
|
/**
|
|
8004
|
-
*
|
|
8005
|
-
*
|
|
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.
|
|
8006
7948
|
*/
|
|
8007
|
-
const
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
"invalid_response",
|
|
8019
|
-
"hash_mismatch",
|
|
8020
|
-
"signature",
|
|
8021
|
-
"patch",
|
|
8022
|
-
"extract",
|
|
8023
|
-
"storage"
|
|
8024
|
-
];
|
|
8025
|
-
const RESOURCES = [
|
|
8026
|
-
"catalog",
|
|
8027
|
-
"artifact",
|
|
8028
|
-
"manifest",
|
|
8029
|
-
"file",
|
|
8030
|
-
"patch",
|
|
8031
|
-
"archive"
|
|
8032
|
-
];
|
|
8033
|
-
const TRANSPORTS = [
|
|
8034
|
-
"timeout",
|
|
8035
|
-
"dns",
|
|
8036
|
-
"tls",
|
|
8037
|
-
"connection",
|
|
8038
|
-
"offline",
|
|
8039
|
-
"cancelled"
|
|
8040
|
-
];
|
|
8041
|
-
const DELIVERIES = [
|
|
8042
|
-
"patch",
|
|
8043
|
-
"manifest",
|
|
8044
|
-
"archive"
|
|
8045
|
-
];
|
|
8046
|
-
/** A storage error code: letters, digits, and `._-`. */
|
|
8047
|
-
const CODE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
8048
|
-
const readCode = (value) => typeof value === "string" && CODE.test(value) ? value : void 0;
|
|
8049
|
-
const readMetadata = (payload) => isRecord$1(payload.metadata) ? payload.metadata : {};
|
|
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 }
|
|
7959
|
+
});
|
|
8050
7960
|
/**
|
|
8051
|
-
*
|
|
8052
|
-
*
|
|
8053
|
-
*
|
|
8054
|
-
* as is a null one.
|
|
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`).
|
|
8055
7964
|
*/
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
}
|
|
8103
|
-
return text + decoder.decode();
|
|
8104
|
-
}
|
|
8105
|
-
async function parseJson(request) {
|
|
8106
|
-
const text = await readBoundedText(request);
|
|
8107
|
-
try {
|
|
8108
|
-
return JSON.parse(text);
|
|
8109
|
-
} catch (error) {
|
|
8110
|
-
if (error instanceof SyntaxError) throw new InsightsBadRequestError("Invalid event payload");
|
|
8111
|
-
throw error;
|
|
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
|
|
7972
|
+
});
|
|
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 }
|
|
7980
|
+
});
|
|
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
|
|
8112
8011
|
}
|
|
8113
|
-
}
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
platform,
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
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"]
|
|
8160
8067
|
}
|
|
8161
|
-
case "UPDATE_APPLIED":
|
|
8162
|
-
case "RECOVERED": return {
|
|
8163
|
-
...movement(),
|
|
8164
|
-
type
|
|
8165
|
-
};
|
|
8166
|
-
case "UPDATE_FAILED": return {
|
|
8167
|
-
...movement(),
|
|
8168
|
-
type,
|
|
8169
|
-
metadata: {
|
|
8170
|
-
...httpMetadata,
|
|
8171
|
-
failure: readFailure(metadata.failure)
|
|
8172
|
-
}
|
|
8173
|
-
};
|
|
8174
|
-
case "UNCHANGED":
|
|
8175
|
-
if (payload.fromBundleId !== null || payload.updateStrategy !== null) throw new InsightsBadRequestError("Invalid unchanged event shape");
|
|
8176
|
-
return {
|
|
8177
|
-
...base,
|
|
8178
|
-
type,
|
|
8179
|
-
fromBundleId: null,
|
|
8180
|
-
updateStrategy: null
|
|
8181
|
-
};
|
|
8182
|
-
default: throw new InsightsBadRequestError("Invalid event field: type");
|
|
8183
|
-
}
|
|
8184
|
-
}
|
|
8185
|
-
async function parseBundleEventRequest(request) {
|
|
8186
|
-
return requireEvent(await parseJson(request));
|
|
8187
|
-
}
|
|
8188
|
-
function createBundleEventRow(input) {
|
|
8189
|
-
input = requireEvent(input);
|
|
8190
|
-
const base = {
|
|
8191
|
-
app_version: input.appVersion,
|
|
8192
|
-
channel: input.channel,
|
|
8193
|
-
from_release_id: input.fromReleaseId,
|
|
8194
|
-
id: input.eventId ?? createUUIDv7(),
|
|
8195
|
-
install_id: input.installId,
|
|
8196
|
-
platform: input.platform,
|
|
8197
|
-
received_at_ms: Date.now(),
|
|
8198
|
-
to_bundle_id: input.toBundleId,
|
|
8199
|
-
to_release_id: input.toReleaseId,
|
|
8200
|
-
user_id: input.userId ?? null
|
|
8201
|
-
};
|
|
8202
|
-
const response = input.metadata?.httpResponse;
|
|
8203
|
-
const metadata = {
|
|
8204
|
-
...response ? { http_response: {
|
|
8205
|
-
resource: response.resource,
|
|
8206
|
-
path: response.path,
|
|
8207
|
-
status: response.status,
|
|
8208
|
-
body: response.body,
|
|
8209
|
-
body_truncated: response.bodyTruncated,
|
|
8210
|
-
received_at_ms: response.receivedAtMs
|
|
8211
|
-
} } : {},
|
|
8212
|
-
cohort: input.cohort,
|
|
8213
|
-
fingerprint_hash: input.fingerprintHash,
|
|
8214
|
-
sdk_version: input.sdkVersion ?? null,
|
|
8215
|
-
...input.minBundleId ? { min_bundle_id: input.minBundleId } : {},
|
|
8216
|
-
update_strategy: input.updateStrategy
|
|
8217
|
-
};
|
|
8218
|
-
switch (input.type) {
|
|
8219
|
-
case "UPDATE_DOWNLOADED": {
|
|
8220
|
-
const { delivery, patchFallback } = input.metadata ?? {};
|
|
8221
|
-
return {
|
|
8222
|
-
...base,
|
|
8223
|
-
from_bundle_id: input.fromBundleId,
|
|
8224
|
-
type: input.type,
|
|
8225
|
-
metadata: {
|
|
8226
|
-
...metadata,
|
|
8227
|
-
...delivery === void 0 ? {} : { delivery },
|
|
8228
|
-
...patchFallback ? { patch_fallback: true } : {}
|
|
8229
|
-
}
|
|
8230
|
-
};
|
|
8231
|
-
}
|
|
8232
|
-
case "UPDATE_APPLIED":
|
|
8233
|
-
case "RECOVERED": return {
|
|
8234
|
-
...base,
|
|
8235
|
-
from_bundle_id: input.fromBundleId,
|
|
8236
|
-
type: input.type,
|
|
8237
|
-
metadata
|
|
8238
|
-
};
|
|
8239
|
-
case "UPDATE_FAILED": {
|
|
8240
|
-
const { failure } = input.metadata;
|
|
8241
|
-
return {
|
|
8242
|
-
...base,
|
|
8243
|
-
from_bundle_id: input.fromBundleId,
|
|
8244
|
-
type: input.type,
|
|
8245
|
-
metadata: {
|
|
8246
|
-
...metadata,
|
|
8247
|
-
failure: {
|
|
8248
|
-
stage: failure.stage,
|
|
8249
|
-
reason: failure.reason,
|
|
8250
|
-
...failure.errorMessage === void 0 ? {} : { error_message: failure.errorMessage },
|
|
8251
|
-
...failure.errorStack === void 0 ? {} : { error_stack: failure.errorStack },
|
|
8252
|
-
...failure.resource === void 0 ? {} : { resource: failure.resource },
|
|
8253
|
-
...failure.httpStatus === void 0 ? {} : { http_status: failure.httpStatus },
|
|
8254
|
-
...failure.transport === void 0 ? {} : { transport: failure.transport },
|
|
8255
|
-
...failure.originCode === void 0 ? {} : { origin_code: failure.originCode }
|
|
8256
|
-
}
|
|
8257
|
-
}
|
|
8258
|
-
};
|
|
8259
|
-
}
|
|
8260
|
-
case "UNCHANGED": return {
|
|
8261
|
-
...base,
|
|
8262
|
-
from_bundle_id: null,
|
|
8263
|
-
type: input.type,
|
|
8264
|
-
metadata
|
|
8265
|
-
};
|
|
8266
|
-
}
|
|
8267
|
-
}
|
|
8268
|
-
//#endregion
|
|
8269
|
-
//#region ../insights/dist/server/provider.mjs
|
|
8270
|
-
const DEFAULT_PAGE_LIMIT = 50;
|
|
8271
|
-
const MAX_PAGE_LIMIT$1 = 100;
|
|
8272
|
-
const MAX_EVENT_ID_LENGTH = 1024;
|
|
8273
|
-
const MAX_IDENTITY_LENGTH$1 = 255;
|
|
8274
|
-
const MAX_CURSOR_LENGTH$1 = 8192;
|
|
8275
|
-
const HOUR_MS$1 = 36e5;
|
|
8276
|
-
const DAY_MS$1 = 24 * HOUR_MS$1;
|
|
8277
|
-
const WINDOW_MS = {
|
|
8278
|
-
"24h": 864e5,
|
|
8279
|
-
"7d": 6048e5,
|
|
8280
|
-
"30d": 2592e6
|
|
8281
|
-
};
|
|
8282
|
-
/**
|
|
8283
|
-
* The longest receipt range, `[sinceMs, beforeReceivedAtMs)`, of a global or
|
|
8284
|
-
* bundle event list, and the range one covers without `sinceMs`: raw events
|
|
8285
|
-
* are listed inside a time range, as analytics products list them. It is a
|
|
8286
|
-
* duration, so a range that ends mid-day touches 91 UTC days. Installation
|
|
8287
|
-
* history is one index range and reads back to `sinceMs`.
|
|
8288
|
-
*/
|
|
8289
|
-
const EVENT_LIST_RANGE_MS = 2160 * HOUR_MS$1;
|
|
8290
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8291
|
-
const requireString = (value, label, maximumLength) => {
|
|
8292
|
-
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}.`);
|
|
8293
|
-
return value;
|
|
8294
|
-
};
|
|
8295
|
-
const requireTimestamp = (value, label) => {
|
|
8296
|
-
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
8297
|
-
return Number(value);
|
|
8298
|
-
};
|
|
8299
|
-
const readLimit = (value) => {
|
|
8300
|
-
const limit = value ?? DEFAULT_PAGE_LIMIT;
|
|
8301
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT$1) throw new InsightsBadRequestError("Invalid page limit.");
|
|
8302
|
-
return limit;
|
|
8303
|
-
};
|
|
8304
|
-
const encodeBase64Url = (value) => {
|
|
8305
|
-
const bytes = new TextEncoder().encode(value);
|
|
8306
|
-
let binary = "";
|
|
8307
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
8308
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
8309
|
-
};
|
|
8310
|
-
const decodeBase64Url = (value) => {
|
|
8311
|
-
if (value.length === 0 || value.length > MAX_CURSOR_LENGTH$1) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
8312
|
-
try {
|
|
8313
|
-
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
8314
|
-
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
8315
|
-
const binary = atob(padded);
|
|
8316
|
-
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
8317
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
8318
|
-
} catch {
|
|
8319
|
-
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
8320
|
-
}
|
|
8321
|
-
};
|
|
8322
|
-
const encodeCursor = (value) => encodeBase64Url(JSON.stringify(value));
|
|
8323
|
-
const decodeCursor = (value) => {
|
|
8324
|
-
try {
|
|
8325
|
-
return JSON.parse(decodeBase64Url(value));
|
|
8326
|
-
} catch (error) {
|
|
8327
|
-
if (error instanceof InsightsBadRequestError) throw error;
|
|
8328
|
-
throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
8329
|
-
}
|
|
8330
|
-
};
|
|
8331
|
-
const readScope$1 = (input) => {
|
|
8332
|
-
if (input.platform !== "ios" && input.platform !== "android") throw new InsightsBadRequestError("Invalid Insights platform.");
|
|
8333
|
-
return {
|
|
8334
|
-
platform: input.platform,
|
|
8335
|
-
channel: requireString(input.channel, "channel", MAX_EVENT_ID_LENGTH)
|
|
8336
|
-
};
|
|
8337
|
-
};
|
|
8338
|
-
const bundleFilter = (input) => {
|
|
8339
|
-
const scope = readScope$1(input);
|
|
8340
|
-
const bundleId = requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
8341
|
-
switch (input.outcome) {
|
|
8342
|
-
case "downloaded": return {
|
|
8343
|
-
...scope,
|
|
8344
|
-
type: "UPDATE_DOWNLOADED",
|
|
8345
|
-
toBundleId: bundleId
|
|
8346
|
-
};
|
|
8347
|
-
case "applied": return {
|
|
8348
|
-
...scope,
|
|
8349
|
-
type: "UPDATE_APPLIED",
|
|
8350
|
-
toBundleId: bundleId
|
|
8351
|
-
};
|
|
8352
|
-
case "recovered": return {
|
|
8353
|
-
...scope,
|
|
8354
|
-
type: "RECOVERED",
|
|
8355
|
-
fromBundleId: bundleId
|
|
8356
|
-
};
|
|
8357
|
-
case "failed": return {
|
|
8358
|
-
...scope,
|
|
8359
|
-
type: "UPDATE_FAILED",
|
|
8360
|
-
toBundleId: bundleId
|
|
8361
|
-
};
|
|
8362
|
-
default: throw new InsightsBadRequestError("Invalid Insights outcome.");
|
|
8363
|
-
}
|
|
8364
|
-
};
|
|
8365
|
-
const sameFilter = (left, right) => {
|
|
8366
|
-
if (!isRecord(left) || left.kind !== right.kind) return false;
|
|
8367
|
-
switch (right.kind) {
|
|
8368
|
-
case "all": return true;
|
|
8369
|
-
case "installationMovement": return left.installId === right.installId;
|
|
8370
|
-
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);
|
|
8371
|
-
}
|
|
8372
|
-
};
|
|
8373
|
-
const readEventCursor = (value, filter) => {
|
|
8374
|
-
const cursor = decodeCursor(value);
|
|
8375
|
-
if (!isRecord(cursor) || cursor.version !== 2 || cursor.kind !== "events" || !isRecord(cursor.filter) || !isRecord(cursor.after)) throw new InsightsBadRequestError("Invalid Insights cursor.");
|
|
8376
|
-
if (!sameFilter(cursor.filter, filter)) throw new InsightsBadRequestError("Insights cursor does not match the requested events.");
|
|
8377
|
-
if (typeof cursor.after.id !== "string" || !isUUIDv7(cursor.after.id)) throw new InsightsBadRequestError("Invalid Insights event cursor ID.");
|
|
8378
|
-
return {
|
|
8379
|
-
after: {
|
|
8380
|
-
id: cursor.after.id,
|
|
8381
|
-
receivedAtMs: requireTimestamp(cursor.after.receivedAtMs, "event cursor")
|
|
8382
|
-
},
|
|
8383
|
-
beforeReceivedAtMs: requireTimestamp(cursor.beforeReceivedAtMs, "event cutoff"),
|
|
8384
|
-
kind: "events",
|
|
8385
|
-
sinceMs: requireTimestamp(cursor.sinceMs, "event start"),
|
|
8386
|
-
filter,
|
|
8387
|
-
version: 2
|
|
8388
|
-
};
|
|
8389
|
-
};
|
|
8390
|
-
const readUserInstallationCursor = (value, userId) => {
|
|
8391
|
-
const cursor = decodeCursor(value);
|
|
8392
|
-
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.");
|
|
8393
|
-
return {
|
|
8394
|
-
afterInstallId: requireString(cursor.afterInstallId, "installation cursor", MAX_IDENTITY_LENGTH$1),
|
|
8395
|
-
kind: "user-installations",
|
|
8396
|
-
userId,
|
|
8397
|
-
version: 1
|
|
8398
|
-
};
|
|
8399
|
-
};
|
|
8400
|
-
const compareEventNewest = (left, right) => right.received_at_ms - left.received_at_ms || compareInsightsText(right.id, left.id);
|
|
8401
|
-
const isAfterEventCursor = (row, after) => row.received_at_ms < after.receivedAtMs || row.received_at_ms === after.receivedAtMs && compareInsightsText(row.id, after.id) < 0;
|
|
8402
|
-
const assertEventRows = (rows, input) => {
|
|
8403
|
-
if (rows.length > input.limit) throw new Error("Insights database returned too many event rows.");
|
|
8404
|
-
for (let index = 0; index < rows.length; index += 1) {
|
|
8405
|
-
const row = rows[index];
|
|
8406
|
-
const previous = rows[index - 1];
|
|
8407
|
-
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.");
|
|
8408
|
-
}
|
|
8409
|
-
};
|
|
8410
|
-
/** An `UPDATE_FAILED` row's failure, as the event lists show it. */
|
|
8411
|
-
const toFailure = (failure) => ({
|
|
8412
|
-
stage: failure.stage,
|
|
8413
|
-
reason: failure.reason,
|
|
8414
|
-
...failure.error_message === void 0 ? {} : { errorMessage: failure.error_message },
|
|
8415
|
-
...failure.error_stack === void 0 ? {} : { errorStack: failure.error_stack },
|
|
8416
|
-
...failure.resource === void 0 ? {} : { resource: failure.resource },
|
|
8417
|
-
...failure.http_status === void 0 ? {} : { httpStatus: failure.http_status },
|
|
8418
|
-
...failure.transport === void 0 ? {} : { transport: failure.transport },
|
|
8419
|
-
...failure.origin_code === void 0 ? {} : { originCode: failure.origin_code }
|
|
8420
|
-
});
|
|
8421
|
-
/** What a row's metadata adds for its type: a failure or a delivery. */
|
|
8422
|
-
const eventDetails = ({ type, metadata }) => ({
|
|
8423
|
-
...metadata.http_response !== void 0 ? { httpResponse: {
|
|
8424
|
-
resource: metadata.http_response.resource,
|
|
8425
|
-
path: metadata.http_response.path,
|
|
8426
|
-
status: metadata.http_response.status,
|
|
8427
|
-
body: metadata.http_response.body,
|
|
8428
|
-
bodyTruncated: metadata.http_response.body_truncated,
|
|
8429
|
-
receivedAtMs: metadata.http_response.received_at_ms
|
|
8430
|
-
} } : {},
|
|
8431
|
-
...type === "UPDATE_FAILED" && metadata.failure !== void 0 ? { failure: toFailure(metadata.failure) } : {},
|
|
8432
|
-
...type === "UPDATE_DOWNLOADED" && metadata.delivery !== void 0 ? { delivery: metadata.delivery } : {},
|
|
8433
|
-
...type === "UPDATE_DOWNLOADED" && metadata.patch_fallback === true ? { patchFallback: true } : {}
|
|
8434
|
-
});
|
|
8435
|
-
const toEventHistoryRow = (row) => ({
|
|
8436
|
-
...row.to_release_id == null ? {} : { toReleaseId: row.to_release_id },
|
|
8437
|
-
...row.metadata.sdk_version == null ? {} : { sdkVersion: row.metadata.sdk_version },
|
|
8438
|
-
...row.metadata.min_bundle_id === void 0 ? {} : { minBundleId: row.metadata.min_bundle_id },
|
|
8439
|
-
appVersion: row.app_version,
|
|
8440
|
-
channel: row.channel,
|
|
8441
|
-
cohort: row.metadata.cohort,
|
|
8442
|
-
fromBundleId: row.from_bundle_id,
|
|
8443
|
-
id: row.id,
|
|
8444
|
-
installId: row.install_id,
|
|
8445
|
-
platform: row.platform,
|
|
8446
|
-
receivedAtMs: row.received_at_ms,
|
|
8447
|
-
toBundleId: row.to_bundle_id,
|
|
8448
|
-
type: row.type,
|
|
8449
|
-
userId: row.user_id,
|
|
8450
|
-
...eventDetails(row)
|
|
8451
|
-
});
|
|
8452
|
-
const toInstallationRow = (row) => ({
|
|
8453
|
-
...eventDetails(row).httpResponse ? { httpResponse: eventDetails(row).httpResponse } : {},
|
|
8454
|
-
...row.metadata.min_bundle_id === void 0 ? {} : { minBundleId: row.metadata.min_bundle_id },
|
|
8455
|
-
appVersion: row.app_version,
|
|
8456
|
-
channel: row.channel,
|
|
8457
|
-
cohort: row.metadata.cohort,
|
|
8458
|
-
installId: row.install_id,
|
|
8459
|
-
lastKnownBundleId: row.type === "UPDATE_DOWNLOADED" ? row.from_bundle_id : row.to_bundle_id,
|
|
8460
|
-
pendingBundleId: row.type === "UPDATE_DOWNLOADED" ? row.to_bundle_id : null,
|
|
8461
|
-
pendingReleaseId: row.type === "UPDATE_DOWNLOADED" ? row.to_release_id : null,
|
|
8462
|
-
latestStatus: row.type,
|
|
8463
|
-
platform: row.platform,
|
|
8464
|
-
receivedAtMs: row.received_at_ms,
|
|
8465
|
-
userId: row.user_id
|
|
8466
|
-
});
|
|
8467
|
-
/**
|
|
8468
|
-
* Where a list without `sinceMs` starts: 90 days before its cutoff for the
|
|
8469
|
-
* global and bundle lists, at the first event for installation history.
|
|
8470
|
-
*/
|
|
8471
|
-
const defaultEventStart = (filter, beforeReceivedAtMs) => filter.kind === "installationMovement" ? 0 : Math.max(0, beforeReceivedAtMs - EVENT_LIST_RANGE_MS);
|
|
8472
|
-
/** Whether a cursor's row lies inside the list's range. */
|
|
8473
|
-
const isInsideBounds = ({ after }, bounds) => after.receivedAtMs >= bounds.sinceMs && after.receivedAtMs < bounds.beforeReceivedAtMs;
|
|
8474
|
-
/** A page's range: its cursor's, which the request may repeat but not change. */
|
|
8475
|
-
const readEventBounds = (input, cursor, filter) => {
|
|
8476
|
-
const beforeReceivedAtMs = cursor?.beforeReceivedAtMs ?? (input.beforeReceivedAtMs === void 0 ? Date.now() : requireTimestamp(input.beforeReceivedAtMs, "event cutoff"));
|
|
8477
|
-
if (cursor !== void 0 && input.beforeReceivedAtMs !== void 0 && input.beforeReceivedAtMs !== beforeReceivedAtMs) throw new InsightsBadRequestError("Insights cursor does not match the requested event cutoff.");
|
|
8478
|
-
const sinceMs = cursor?.sinceMs ?? (input.sinceMs === void 0 ? defaultEventStart(filter, beforeReceivedAtMs) : requireTimestamp(input.sinceMs, "event start"));
|
|
8479
|
-
const bounds = {
|
|
8480
|
-
beforeReceivedAtMs,
|
|
8481
|
-
sinceMs
|
|
8482
|
-
};
|
|
8483
|
-
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.");
|
|
8484
|
-
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.");
|
|
8485
|
-
return bounds;
|
|
8486
|
-
};
|
|
8487
|
-
const pageEventRows = async (model, input, filter, map) => {
|
|
8488
|
-
const limit = readLimit(input.limit);
|
|
8489
|
-
const cursor = input.cursor === void 0 ? void 0 : readEventCursor(input.cursor, filter);
|
|
8490
|
-
const bounds = readEventBounds(input, cursor, filter);
|
|
8491
|
-
const query = {
|
|
8492
|
-
filter,
|
|
8493
|
-
...bounds,
|
|
8494
|
-
...cursor === void 0 ? {} : { after: cursor.after },
|
|
8495
|
-
limit
|
|
8496
|
-
};
|
|
8497
|
-
const rows = await model.listEvents(query);
|
|
8498
|
-
assertEventRows(rows, query);
|
|
8499
|
-
const last = rows.at(-1);
|
|
8500
|
-
return {
|
|
8501
|
-
beforeReceivedAtMs: bounds.beforeReceivedAtMs,
|
|
8502
|
-
data: rows.map(map),
|
|
8503
|
-
nextCursor: rows.length === limit && last ? encodeCursor({
|
|
8504
|
-
after: {
|
|
8505
|
-
id: last.id,
|
|
8506
|
-
receivedAtMs: last.received_at_ms
|
|
8507
|
-
},
|
|
8508
|
-
...bounds,
|
|
8509
|
-
kind: "events",
|
|
8510
|
-
filter,
|
|
8511
|
-
version: 2
|
|
8512
|
-
}) : null
|
|
8513
|
-
};
|
|
8514
|
-
};
|
|
8515
|
-
const assertInstallationRows = (rows, input) => {
|
|
8516
|
-
if (rows.length > input.limit) throw new Error("Insights database returned too many installation rows.");
|
|
8517
|
-
for (let index = 0; index < rows.length; index += 1) {
|
|
8518
|
-
const row = rows[index];
|
|
8519
|
-
const previous = rows[index - 1];
|
|
8520
|
-
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.");
|
|
8521
|
-
}
|
|
8522
|
-
};
|
|
8523
|
-
const createInsightsProvider = (model) => Object.freeze({
|
|
8524
|
-
async appendBundleEvent(input) {
|
|
8525
|
-
const event = createBundleEventRow(input);
|
|
8526
|
-
await model.recordEvent({ event });
|
|
8527
|
-
},
|
|
8528
|
-
listEvents(input) {
|
|
8529
|
-
const filter = input.bundle === void 0 ? { kind: "all" } : {
|
|
8530
|
-
kind: "bundle",
|
|
8531
|
-
...bundleFilter(input.bundle)
|
|
8532
|
-
};
|
|
8533
|
-
return pageEventRows(model, input, filter, toEventHistoryRow);
|
|
8534
|
-
},
|
|
8535
|
-
async listInstallationEvents(input) {
|
|
8536
|
-
if ("bundle" in input && input.bundle !== void 0) throw new InsightsBadRequestError("Installation movement queries cannot include a bundle filter.");
|
|
8537
|
-
const installId = requireString(input.installId, "install ID", MAX_IDENTITY_LENGTH$1);
|
|
8538
|
-
return pageEventRows(model, input, {
|
|
8539
|
-
kind: "installationMovement",
|
|
8540
|
-
installId
|
|
8541
|
-
}, (row) => toEventHistoryRow(row));
|
|
8542
|
-
},
|
|
8543
|
-
async getInstallation({ installId }) {
|
|
8544
|
-
const normalizedInstallId = requireString(installId, "install ID", MAX_IDENTITY_LENGTH$1);
|
|
8545
|
-
const rows = await model.findLatestEvents({ installId: normalizedInstallId });
|
|
8546
|
-
const row = rows[0] ?? null;
|
|
8547
|
-
if (rows.length > 1 || row !== null && row.install_id !== normalizedInstallId) throw new Error("Insights database returned an invalid installation.");
|
|
8548
|
-
return row === null ? null : toInstallationRow(row);
|
|
8549
|
-
},
|
|
8550
|
-
async pageInstallationsByCurrentUserId(input) {
|
|
8551
|
-
const userId = requireString(input.userId, "user ID", MAX_IDENTITY_LENGTH$1);
|
|
8552
|
-
const limit = readLimit(input.limit);
|
|
8553
|
-
const cursor = input.cursor === void 0 ? void 0 : readUserInstallationCursor(input.cursor, userId);
|
|
8554
|
-
const databaseInput = {
|
|
8555
|
-
userId,
|
|
8556
|
-
...cursor === void 0 ? {} : { afterInstallId: cursor.afterInstallId },
|
|
8557
|
-
limit
|
|
8558
|
-
};
|
|
8559
|
-
const rows = await model.findLatestEvents(databaseInput);
|
|
8560
|
-
assertInstallationRows(rows, databaseInput);
|
|
8561
|
-
const last = rows.at(-1);
|
|
8562
|
-
return {
|
|
8563
|
-
data: rows.map(toInstallationRow),
|
|
8564
|
-
nextCursor: rows.length === limit && last ? encodeCursor({
|
|
8565
|
-
afterInstallId: last.install_id,
|
|
8566
|
-
kind: "user-installations",
|
|
8567
|
-
userId,
|
|
8568
|
-
version: 1
|
|
8569
|
-
}) : null
|
|
8570
|
-
};
|
|
8571
8068
|
},
|
|
8572
|
-
|
|
8573
|
-
const scope = readScope$1(input);
|
|
8574
|
-
const { window } = input;
|
|
8575
|
-
if (!Object.hasOwn(WINDOW_MS, window)) throw new InsightsBadRequestError("Invalid reporting installation window.");
|
|
8576
|
-
const now = Date.now();
|
|
8577
|
-
const beforeReceivedAtMs = now - now % HOUR_MS$1 + (now % HOUR_MS$1 === 0 ? 0 : HOUR_MS$1);
|
|
8578
|
-
const reach = Math.max(0, beforeReceivedAtMs - WINDOW_MS[window]);
|
|
8579
|
-
const sinceMs = reach - reach % DAY_MS$1;
|
|
8580
|
-
const measure = async (count) => {
|
|
8581
|
-
const value = await count;
|
|
8582
|
-
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Insights database returned an invalid count.");
|
|
8583
|
-
return {
|
|
8584
|
-
count: value,
|
|
8585
|
-
measuredAtMs: Date.now()
|
|
8586
|
-
};
|
|
8587
|
-
};
|
|
8588
|
-
const bundleId = input.bundleId === void 0 ? void 0 : requireString(input.bundleId, "bundle ID", MAX_EVENT_ID_LENGTH);
|
|
8589
|
-
const reporting = measure(model.countLatestEvents({
|
|
8590
|
-
...scope,
|
|
8591
|
-
sinceMs
|
|
8592
|
-
}));
|
|
8593
|
-
if (bundleId === void 0) return {
|
|
8594
|
-
...scope,
|
|
8595
|
-
window,
|
|
8596
|
-
sinceMs,
|
|
8597
|
-
beforeReceivedAtMs,
|
|
8598
|
-
reportingInstallations: await reporting
|
|
8599
|
-
};
|
|
8600
|
-
const countOutcome = (outcome) => measure(model.countEvents({
|
|
8601
|
-
filter: bundleFilter({
|
|
8602
|
-
...scope,
|
|
8603
|
-
bundleId,
|
|
8604
|
-
outcome
|
|
8605
|
-
}),
|
|
8606
|
-
sinceMs,
|
|
8607
|
-
beforeReceivedAtMs
|
|
8608
|
-
}));
|
|
8609
|
-
const [reportingInstallations, bundleInstallations, downloadedReports, appliedReports, recoveredReports, failedReports] = await Promise.all([
|
|
8610
|
-
reporting,
|
|
8611
|
-
measure(model.countLatestEvents({
|
|
8612
|
-
...scope,
|
|
8613
|
-
sinceMs,
|
|
8614
|
-
bundle: [{
|
|
8615
|
-
field: "from_bundle_id",
|
|
8616
|
-
value: bundleId,
|
|
8617
|
-
types: ["UPDATE_DOWNLOADED"]
|
|
8618
|
-
}, {
|
|
8619
|
-
field: "to_bundle_id",
|
|
8620
|
-
value: bundleId,
|
|
8621
|
-
types: [
|
|
8622
|
-
"UNCHANGED",
|
|
8623
|
-
"UPDATE_APPLIED",
|
|
8624
|
-
"RECOVERED"
|
|
8625
|
-
]
|
|
8626
|
-
}]
|
|
8627
|
-
})),
|
|
8628
|
-
countOutcome("downloaded"),
|
|
8629
|
-
countOutcome("applied"),
|
|
8630
|
-
countOutcome("recovered"),
|
|
8631
|
-
countOutcome("failed")
|
|
8632
|
-
]);
|
|
8633
|
-
return {
|
|
8634
|
-
...scope,
|
|
8635
|
-
window,
|
|
8636
|
-
sinceMs,
|
|
8637
|
-
beforeReceivedAtMs,
|
|
8638
|
-
reportingInstallations,
|
|
8639
|
-
bundle: {
|
|
8640
|
-
bundleId,
|
|
8641
|
-
reportingInstallations: bundleInstallations,
|
|
8642
|
-
downloadedReports,
|
|
8643
|
-
appliedReports,
|
|
8644
|
-
recoveredReports,
|
|
8645
|
-
failedReports
|
|
8646
|
-
}
|
|
8647
|
-
};
|
|
8648
|
-
}
|
|
8649
|
-
});
|
|
8650
|
-
//#endregion
|
|
8651
|
-
//#region ../insights/dist/server/schema.mjs
|
|
8652
|
-
const HOUR_MS = 36e5;
|
|
8653
|
-
const DAY_MS = 864e5;
|
|
8654
|
-
const DEFAULT_INSIGHTS_RETENTION = {
|
|
8655
|
-
rawDays: 90,
|
|
8656
|
-
dailyDays: 400
|
|
8657
|
-
};
|
|
8658
|
-
const bucketRetention = (days) => ({
|
|
8659
|
-
field: "bucket_start_ms",
|
|
8660
|
-
days
|
|
8069
|
+
retention
|
|
8661
8070
|
});
|
|
8662
|
-
const MOVEMENTS = /* @__PURE__ */ new Set([
|
|
8663
|
-
"UPDATE_DOWNLOADED",
|
|
8664
|
-
"UPDATE_APPLIED",
|
|
8665
|
-
"RECOVERED",
|
|
8666
|
-
"UPDATE_FAILED"
|
|
8667
|
-
]);
|
|
8668
|
-
/** A failed update check: it names no target bundle, so no bundle list shows it. */
|
|
8669
|
-
const isFailedCheck = (row) => row.type === "UPDATE_FAILED" && row.metadata?.failure?.stage === "check";
|
|
8670
8071
|
/**
|
|
8671
|
-
*
|
|
8672
|
-
*
|
|
8673
|
-
*
|
|
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.
|
|
8674
8075
|
*/
|
|
8675
|
-
const
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
maxLength: 36
|
|
8679
|
-
},
|
|
8680
|
-
type: {
|
|
8681
|
-
type: "string",
|
|
8682
|
-
maxLength: 32
|
|
8683
|
-
},
|
|
8684
|
-
install_id: {
|
|
8076
|
+
const insightsBuiltinDistribution = (retention) => defineAggregate({
|
|
8077
|
+
channel: { type: "string" },
|
|
8078
|
+
platform: {
|
|
8685
8079
|
type: "string",
|
|
8686
|
-
maxLength:
|
|
8080
|
+
maxLength: 16
|
|
8687
8081
|
},
|
|
8688
|
-
|
|
8082
|
+
app_version: { type: "string" },
|
|
8083
|
+
release_id: {
|
|
8689
8084
|
type: "string",
|
|
8690
|
-
maxLength:
|
|
8691
|
-
required: false
|
|
8085
|
+
maxLength: 36
|
|
8692
8086
|
},
|
|
8693
|
-
|
|
8087
|
+
builtin_bundle_id: {
|
|
8694
8088
|
type: "string",
|
|
8695
|
-
maxLength: 36
|
|
8696
|
-
required: false
|
|
8089
|
+
maxLength: 36
|
|
8697
8090
|
},
|
|
8698
|
-
|
|
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
|
+
}
|
|
8117
|
+
},
|
|
8118
|
+
retention
|
|
8119
|
+
});
|
|
8120
|
+
/** Latest events by the bundle they came from or went to, bucketed like the distribution. */
|
|
8121
|
+
const insightsLatestByBundle = (retention) => defineAggregate({
|
|
8122
|
+
platform: {
|
|
8699
8123
|
type: "string",
|
|
8700
|
-
maxLength:
|
|
8701
|
-
required: false
|
|
8124
|
+
maxLength: 16
|
|
8702
8125
|
},
|
|
8703
|
-
|
|
8126
|
+
channel: { type: "string" },
|
|
8127
|
+
bundle_field: {
|
|
8704
8128
|
type: "string",
|
|
8705
|
-
maxLength:
|
|
8706
|
-
required: false
|
|
8129
|
+
maxLength: 16
|
|
8707
8130
|
},
|
|
8708
|
-
|
|
8131
|
+
bundle_id: {
|
|
8709
8132
|
type: "string",
|
|
8710
8133
|
maxLength: 36
|
|
8711
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
|
|
8163
|
+
});
|
|
8164
|
+
/**
|
|
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.
|
|
8169
|
+
*/
|
|
8170
|
+
const insightsOutcomes = (retention) => defineAggregate({
|
|
8712
8171
|
platform: {
|
|
8713
8172
|
type: "string",
|
|
8714
8173
|
maxLength: 16
|
|
8715
8174
|
},
|
|
8716
|
-
app_version: { type: "string" },
|
|
8717
8175
|
channel: { type: "string" },
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
key: ["id"],
|
|
8722
|
-
derived: {
|
|
8723
|
-
/** The UTC day the event was received in. */
|
|
8724
|
-
day: {
|
|
8725
|
-
type: "integer",
|
|
8726
|
-
compute: (row) => row.received_at_ms - row.received_at_ms % DAY_MS
|
|
8727
|
-
},
|
|
8728
|
-
/** Set only for events that belong in installation history. */
|
|
8729
|
-
movement_install_id: {
|
|
8730
|
-
type: "string",
|
|
8731
|
-
compute: (row) => MOVEMENTS.has(row.type) ? row.install_id : null
|
|
8732
|
-
},
|
|
8733
|
-
/**
|
|
8734
|
-
* The bundle a bundle filter matches: `from:<bundle>` for RECOVERED,
|
|
8735
|
-
* none for a failed check, else `to:<bundle>`.
|
|
8736
|
-
*/
|
|
8737
|
-
bundle_ref: {
|
|
8738
|
-
type: "string",
|
|
8739
|
-
multi: true,
|
|
8740
|
-
compute: (row) => isFailedCheck(row) ? [] : [row.type === "RECOVERED" && row.from_bundle_id !== null ? `from:${row.from_bundle_id}` : `to:${row.to_bundle_id}`]
|
|
8741
|
-
}
|
|
8176
|
+
type: {
|
|
8177
|
+
type: "string",
|
|
8178
|
+
maxLength: 32
|
|
8742
8179
|
},
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
sort: ["received_at_ms"]
|
|
8747
|
-
},
|
|
8748
|
-
byBundle: {
|
|
8749
|
-
eq: [
|
|
8750
|
-
"platform",
|
|
8751
|
-
"channel",
|
|
8752
|
-
"type",
|
|
8753
|
-
"bundle_ref",
|
|
8754
|
-
"day"
|
|
8755
|
-
],
|
|
8756
|
-
sort: ["received_at_ms"]
|
|
8757
|
-
},
|
|
8758
|
-
/** Events of every scope by day, for the console's unfiltered event list. */
|
|
8759
|
-
byDay: {
|
|
8760
|
-
eq: ["day"],
|
|
8761
|
-
sort: ["received_at_ms"]
|
|
8762
|
-
}
|
|
8180
|
+
bundle_ref: {
|
|
8181
|
+
type: "string",
|
|
8182
|
+
maxLength: 41
|
|
8763
8183
|
},
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
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;
|
|
8767
8326
|
}
|
|
8768
|
-
|
|
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);
|
|
8769
8362
|
/**
|
|
8770
|
-
*
|
|
8771
|
-
*
|
|
8772
|
-
* nothing, so no head holds one.
|
|
8363
|
+
* Downloads, applies, recoveries, failed updates, and the UNCHANGED rows
|
|
8364
|
+
* kept for a change belong in installation history.
|
|
8773
8365
|
*/
|
|
8774
|
-
const
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
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);
|
|
8791
8397
|
},
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
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;
|
|
8796
8420
|
},
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
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;
|
|
8801
8439
|
},
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
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));
|
|
8806
8457
|
},
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
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));
|
|
8810
8465
|
},
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
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;
|
|
8814
8478
|
},
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
key: ["install_id"],
|
|
8821
|
-
indexes: { byUser: {
|
|
8822
|
-
eq: ["user_id"],
|
|
8823
|
-
sort: ["install_id"]
|
|
8824
|
-
} },
|
|
8825
|
-
retention: {
|
|
8826
|
-
field: "received_at_ms",
|
|
8827
|
-
days
|
|
8828
|
-
}
|
|
8829
|
-
});
|
|
8830
|
-
/**
|
|
8831
|
-
* Shards are keyed by install id. Counters are blind increments and never
|
|
8832
|
-
* conflict, so they keep 8. Gauges and sketches are read, merged, and written
|
|
8833
|
-
* back. B3's rollout gate on PostgreSQL retried 2.4% of transactions at 8
|
|
8834
|
-
* shards and 1.3% at 16. DynamoDB's transactions take longer, so D8's gate
|
|
8835
|
-
* on DynamoDB Local retried 2–24% at 16, almost all on gauge rows, and 1–5%
|
|
8836
|
-
* with gauges at 32. Sketches stay at 16: each shard row carries 2 KB of
|
|
8837
|
-
* registers that every read merges.
|
|
8838
|
-
*
|
|
8839
|
-
* Every Insights aggregate is `batched`: on DynamoDB and Firestore, which
|
|
8840
|
-
* bill each write, an event's changes apply after it commits, merged with
|
|
8841
|
-
* other events' into one write per row, on one shard per writer.
|
|
8842
|
-
*/
|
|
8843
|
-
const COUNTER_SHARDS = 8;
|
|
8844
|
-
const GAUGE_SHARDS = 32;
|
|
8845
|
-
const SKETCH_SHARDS = 16;
|
|
8846
|
-
/** Rows keyed by a hashed identity (scope and period) and a bucket. */
|
|
8847
|
-
const identityFields = {
|
|
8848
|
-
identity: {
|
|
8849
|
-
type: "string",
|
|
8850
|
-
maxLength: 32
|
|
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;
|
|
8851
8484
|
},
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
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
|
|
8865
8505
|
/**
|
|
8866
|
-
*
|
|
8867
|
-
*
|
|
8868
|
-
* patch delivered and those that fell back from one.
|
|
8506
|
+
* The plugin's API as `InsightsModel`, validated at its boundary: what the
|
|
8507
|
+
* Insights routes, the console, and the e2e harness read through.
|
|
8869
8508
|
*/
|
|
8870
|
-
const
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
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
|
|
8877
8518
|
});
|
|
8519
|
+
//#endregion
|
|
8520
|
+
//#region ../insights/dist/server/official.mjs
|
|
8878
8521
|
/**
|
|
8879
|
-
*
|
|
8880
|
-
*
|
|
8881
|
-
*
|
|
8882
|
-
*
|
|
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.
|
|
8883
8526
|
*/
|
|
8884
|
-
const
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
"failed_updates"
|
|
8890
|
-
],
|
|
8891
|
-
shards: COUNTER_SHARDS,
|
|
8892
|
-
batched: true,
|
|
8893
|
-
indexes: { window }
|
|
8894
|
-
});
|
|
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
|
|
8895
8532
|
/**
|
|
8896
|
-
*
|
|
8897
|
-
*
|
|
8898
|
-
* installations whose update or update check failed (`failed_users`).
|
|
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.
|
|
8899
8535
|
*/
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
});
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
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
|
+
}
|
|
8916
8569
|
/**
|
|
8917
|
-
*
|
|
8918
|
-
*
|
|
8919
|
-
* `stage` and `reason` say where and why it failed, and `detail` what else
|
|
8920
|
-
* the client knew, as the JSON array `[resource, httpStatus, originCode,
|
|
8921
|
-
* transport]`. Every index is in key order, so its counters stay blind
|
|
8922
|
-
* increments.
|
|
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.
|
|
8923
8572
|
*/
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
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);
|
|
8676
|
+
}
|
|
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
|
+
};
|
|
8736
|
+
}
|
|
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");
|
|
8946
8759
|
}
|
|
8947
|
-
}
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
"
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
},
|
|
8995
|
-
byVersion: {
|
|
8996
|
-
eq: [
|
|
8997
|
-
"channel",
|
|
8998
|
-
"platform",
|
|
8999
|
-
"app_version"
|
|
9000
|
-
],
|
|
9001
|
-
sort: ["bucket_start_ms"]
|
|
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
|
+
};
|
|
9002
8807
|
}
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
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
|
+
};
|
|
8842
|
+
}
|
|
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
|
+
};
|
|
9006
8858
|
/**
|
|
9007
|
-
* The
|
|
9008
|
-
*
|
|
9009
|
-
*
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
}
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
}
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
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`.
|
|
8864
|
+
*/
|
|
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;
|
|
8870
|
+
};
|
|
8871
|
+
const requireTimestamp = (value, label) => {
|
|
8872
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0) throw new InsightsBadRequestError(`Invalid ${label}.`);
|
|
8873
|
+
return Number(value);
|
|
8874
|
+
};
|
|
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")
|
|
9043
8958
|
},
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
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 }
|
|
9054
8996
|
});
|
|
9055
|
-
/**
|
|
9056
|
-
const
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
},
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
}
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
"bundle_id",
|
|
9081
|
-
"type",
|
|
9082
|
-
"bucket_start_ms"
|
|
9083
|
-
],
|
|
9084
|
-
gauges: ["installations"],
|
|
9085
|
-
shards: GAUGE_SHARDS,
|
|
9086
|
-
batched: true,
|
|
9087
|
-
indexes: { byBundle: {
|
|
9088
|
-
eq: [
|
|
9089
|
-
"platform",
|
|
9090
|
-
"channel",
|
|
9091
|
-
"bundle_field",
|
|
9092
|
-
"bundle_id",
|
|
9093
|
-
"type"
|
|
9094
|
-
],
|
|
9095
|
-
sort: ["bucket_start_ms"]
|
|
9096
|
-
} },
|
|
9097
|
-
retention
|
|
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 } : {}
|
|
9098
9022
|
});
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
},
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
batched: true,
|
|
9131
|
-
indexes: { byRef: {
|
|
9132
|
-
eq: [
|
|
9133
|
-
"platform",
|
|
9134
|
-
"channel",
|
|
9135
|
-
"type",
|
|
9136
|
-
"bundle_ref"
|
|
9137
|
-
],
|
|
9138
|
-
sort: ["bucket_start_ms"]
|
|
9139
|
-
} },
|
|
9140
|
-
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)
|
|
9039
|
+
});
|
|
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
|
|
9141
9054
|
});
|
|
9142
9055
|
/**
|
|
9143
|
-
*
|
|
9144
|
-
* global
|
|
9145
|
-
* platform is ios or android, so no outcome row of a bundle filter shares it.
|
|
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.
|
|
9146
9058
|
*/
|
|
9147
|
-
const
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
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;
|
|
9152
9074
|
};
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
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);
|
|
9161
9088
|
return {
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
insights_builtin_distribution: insightsBuiltinDistribution(daily),
|
|
9175
|
-
insights_latest_by_bundle: insightsLatestByBundle(daily),
|
|
9176
|
-
insights_outcomes: insightsOutcomes(hourly),
|
|
9177
|
-
insights_failures: insightsFailures(hourly)
|
|
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
|
|
9178
9101
|
};
|
|
9179
9102
|
};
|
|
9180
|
-
|
|
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.");
|
|
9109
|
+
}
|
|
9110
|
+
};
|
|
9111
|
+
const createInsightsProvider = (model) => Object.freeze({
|
|
9112
|
+
async appendBundleEvent(input) {
|
|
9113
|
+
const event = createBundleEventRow(input);
|
|
9114
|
+
await model.recordEvent({ event });
|
|
9115
|
+
},
|
|
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);
|
|
9122
|
+
},
|
|
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));
|
|
9130
|
+
},
|
|
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);
|
|
9137
|
+
},
|
|
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
|
+
};
|
|
9159
|
+
},
|
|
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
|
+
}
|
|
9237
|
+
});
|
|
9181
9238
|
//#endregion
|
|
9182
9239
|
//#region ../insights/dist/server/overview.mjs
|
|
9183
9240
|
const encodeIdentity = (identity) => [
|
|
@@ -9245,18 +9302,19 @@ const insightsOverviewDeltas = (event) => {
|
|
|
9245
9302
|
bucketStartMs
|
|
9246
9303
|
}, windowed);
|
|
9247
9304
|
};
|
|
9248
|
-
|
|
9305
|
+
const implied = event.metadata.implied_download === true ? 1 : 0;
|
|
9306
|
+
if (!isLateEvent(event) && event.type === "UPDATE_DOWNLOADED") metric(event.to_release_id, {
|
|
9249
9307
|
downloads: 1,
|
|
9250
9308
|
applies: 0,
|
|
9251
9309
|
failedLaunches: 0
|
|
9252
9310
|
});
|
|
9253
|
-
else if (event
|
|
9254
|
-
downloads:
|
|
9311
|
+
else if (!isLateEvent(event) && isLaunch(event)) metric(event.to_release_id, {
|
|
9312
|
+
downloads: implied,
|
|
9255
9313
|
applies: 1,
|
|
9256
9314
|
failedLaunches: 0
|
|
9257
9315
|
});
|
|
9258
|
-
else if (event.type === "RECOVERED") metric(event.from_release_id, {
|
|
9259
|
-
downloads:
|
|
9316
|
+
else if (!isLateEvent(event) && event.type === "RECOVERED") metric(event.from_release_id, {
|
|
9317
|
+
downloads: implied,
|
|
9260
9318
|
applies: 0,
|
|
9261
9319
|
failedLaunches: 1
|
|
9262
9320
|
});
|
|
@@ -9280,8 +9338,8 @@ const insightsOverviewDeltas = (event) => {
|
|
|
9280
9338
|
};
|
|
9281
9339
|
//#endregion
|
|
9282
9340
|
//#region ../insights/dist/server/recordEvent.mjs
|
|
9283
|
-
const hourOf = (ms) => ms - ms % HOUR_MS;
|
|
9284
|
-
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;
|
|
9285
9343
|
/** The `identity` of an overview or sketch row: its scope and period, hashed. */
|
|
9286
9344
|
const insightsIdentity = (identity) => insightsOverviewId({
|
|
9287
9345
|
...identity,
|
|
@@ -9412,16 +9470,16 @@ const countEvent = (tx, event) => {
|
|
|
9412
9470
|
}
|
|
9413
9471
|
};
|
|
9414
9472
|
/**
|
|
9415
|
-
* A stored event's outcome rows: its bundle filter's hour,
|
|
9416
|
-
*
|
|
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.
|
|
9417
9475
|
*/
|
|
9418
9476
|
const countOutcome = (tx, event) => {
|
|
9419
9477
|
const shardBy = event.install_id;
|
|
9420
|
-
|
|
9478
|
+
for (const bundleRef of bundleRefsOf(event)) tx.aggregate("insights_outcomes", {
|
|
9421
9479
|
platform: event.platform,
|
|
9422
9480
|
channel: event.channel,
|
|
9423
9481
|
type: event.type,
|
|
9424
|
-
bundle_ref:
|
|
9482
|
+
bundle_ref: bundleRef,
|
|
9425
9483
|
bucket_start_ms: hourOf(event.received_at_ms)
|
|
9426
9484
|
}, { events: 1 }, { shardBy });
|
|
9427
9485
|
tx.aggregate("insights_outcomes", {
|
|
@@ -9525,6 +9583,126 @@ const countFailure = (tx, event) => {
|
|
|
9525
9583
|
}, { events: 1 }, { shardBy });
|
|
9526
9584
|
};
|
|
9527
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
|
+
};
|
|
9528
9706
|
/** What an UNCHANGED report must share with its installation's head to repeat it. */
|
|
9529
9707
|
const REPEATED_FIELDS = [
|
|
9530
9708
|
"channel",
|
|
@@ -9559,32 +9737,33 @@ const recordEvent = (db, event) => {
|
|
|
9559
9737
|
const [existing, previous] = await Promise.all([tx.findOne("bundle_events", { id: event.id }), tx.findOne("bundle_event_heads", { install_id: event.install_id })]);
|
|
9560
9738
|
if (existing !== null || previous?.id === event.id) return;
|
|
9561
9739
|
if (previous !== null && repeatsHead(event, previous)) return;
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
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);
|
|
9565
9744
|
}
|
|
9566
|
-
if (
|
|
9567
|
-
countFailure(tx,
|
|
9745
|
+
if (recorded.type === "UPDATE_FAILED") {
|
|
9746
|
+
countFailure(tx, recorded);
|
|
9568
9747
|
return;
|
|
9569
9748
|
}
|
|
9570
|
-
countEvent(tx,
|
|
9571
|
-
if (previous !== null && !isNewer(
|
|
9572
|
-
if (previous === null) tx.create("bundle_event_heads",
|
|
9749
|
+
countEvent(tx, recorded);
|
|
9750
|
+
if (previous !== null && (late || !isNewer(recorded, previous))) return;
|
|
9751
|
+
if (previous === null) tx.create("bundle_event_heads", recorded);
|
|
9573
9752
|
else {
|
|
9574
|
-
const { install_id: _, ...fields } =
|
|
9753
|
+
const { install_id: _, ...fields } = recorded;
|
|
9575
9754
|
countHead(tx, previous, -1);
|
|
9576
9755
|
tx.update("bundle_event_heads", previous, fields);
|
|
9577
9756
|
}
|
|
9578
|
-
countHead(tx,
|
|
9757
|
+
countHead(tx, recorded, 1);
|
|
9579
9758
|
});
|
|
9580
9759
|
};
|
|
9581
9760
|
//#endregion
|
|
9582
9761
|
//#region ../insights/dist/server/reads.mjs
|
|
9583
9762
|
const PAGE = 500;
|
|
9584
|
-
const hourFloor = (ms) => ms - ms % HOUR_MS;
|
|
9585
|
-
const hourCeil = (ms) => hourFloor(ms + HOUR_MS - 1);
|
|
9586
|
-
const dayFloor = (ms) => ms - ms % DAY_MS;
|
|
9587
|
-
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);
|
|
9588
9767
|
/** Every page of one read, in order. */
|
|
9589
9768
|
const drain = async (read) => {
|
|
9590
9769
|
const rows = [];
|
|
@@ -9613,12 +9792,11 @@ const EVENT_FIELDS = [
|
|
|
9613
9792
|
];
|
|
9614
9793
|
/** A stored event or head, as the event row it holds. */
|
|
9615
9794
|
const toEvent = (row) => Object.fromEntries(EVENT_FIELDS.map((field) => [field, row[field]]));
|
|
9616
|
-
const bundleRef = (filter) => filter.type === "RECOVERED" ? `from:${filter.fromBundleId}` : `to:${filter.toBundleId}`;
|
|
9617
9795
|
const scopeOf = (filter) => ({
|
|
9618
9796
|
platform: filter.platform,
|
|
9619
9797
|
channel: filter.channel,
|
|
9620
9798
|
type: filter.type,
|
|
9621
|
-
bundle_ref:
|
|
9799
|
+
bundle_ref: bundleRefOfFilter(filter)
|
|
9622
9800
|
});
|
|
9623
9801
|
/** One UTC day of a global or bundle list, newest first. */
|
|
9624
9802
|
const eventsOfDay = (db, filter, day, range, limit) => filter.kind === "all" ? db.findMany("bundle_events", {
|
|
@@ -9691,7 +9869,7 @@ const listEvents = async (db, input) => {
|
|
|
9691
9869
|
while (day !== void 0 && day >= bottom && rows.length < limit) {
|
|
9692
9870
|
const page = await eventsOfDay(db, filter, day, range, limit - rows.length);
|
|
9693
9871
|
rows.push(...page.rows.map(toEvent));
|
|
9694
|
-
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;
|
|
9695
9873
|
}
|
|
9696
9874
|
return rows;
|
|
9697
9875
|
};
|
|
@@ -9712,7 +9890,7 @@ const findLatestEvents = async (db, input) => {
|
|
|
9712
9890
|
/** Raw events of one bundle filter over [from, to), which spans at most two partial hours. */
|
|
9713
9891
|
const countRaw = async (db, filter, from, to) => {
|
|
9714
9892
|
let total = 0;
|
|
9715
|
-
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) {
|
|
9716
9894
|
const rows = await drain((page) => db.findMany("bundle_events", {
|
|
9717
9895
|
index: "byBundle",
|
|
9718
9896
|
where: {
|
|
@@ -9836,8 +10014,8 @@ const countLatestEvents = async (db, input) => {
|
|
|
9836
10014
|
};
|
|
9837
10015
|
/** The oldest whole hour and UTC day that hourly and daily rows still hold. */
|
|
9838
10016
|
const retained = (now, { rawDays, dailyDays }) => ({
|
|
9839
|
-
hour: hourCeil(now - rawDays * DAY_MS),
|
|
9840
|
-
day: dayCeil(now - dailyDays * DAY_MS)
|
|
10017
|
+
hour: hourCeil(now - rawDays * DAY_MS$1),
|
|
10018
|
+
day: dayCeil(now - dailyDays * DAY_MS$1)
|
|
9841
10019
|
});
|
|
9842
10020
|
/**
|
|
9843
10021
|
* Whether the rows a window reads still hold all of it; an older start is
|
|
@@ -10077,7 +10255,7 @@ const getAppUsage = async (db, input, now, retention) => {
|
|
|
10077
10255
|
};
|
|
10078
10256
|
};
|
|
10079
10257
|
/** The longest period the failures read covers: the console's longest, 30 days. */
|
|
10080
|
-
const UPDATE_FAILURES_RANGE_MS = 30 * DAY_MS;
|
|
10258
|
+
const UPDATE_FAILURES_RANGE_MS = 30 * DAY_MS$1;
|
|
10081
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;
|
|
10082
10260
|
const isText = (value, maxLength) => typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
10083
10261
|
const failed = (...rows) => countDistinct(mergeDistinct(rows.flat().map((row) => row.failed_users)));
|