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