@mentra/engine 3.4.0-dev.1207 → 3.4.0-dev.1209
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/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +5 -3
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -0
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +2 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +18 -1
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MentraJSCrashController.d.ts +2 -0
- package/build/services/MentraJSCrashController.d.ts.map +1 -1
- package/build/services/MentraJSCrashController.js +4 -0
- package/build/services/MentraJSCrashController.js.map +1 -1
- package/build/services/MentraJSRouter.d.ts.map +1 -1
- package/build/services/MentraJSRouter.js +14 -3
- package/build/services/MentraJSRouter.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts +6 -0
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +20 -3
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/fleet/FleetReporter.d.ts.map +1 -1
- package/build/services/fleet/FleetReporter.js +43 -0
- package/build/services/fleet/FleetReporter.js.map +1 -1
- package/build/services/fleet/fleetCollector.d.ts +1 -1
- package/build/services/fleet/fleetCollector.d.ts.map +1 -1
- package/build/services/fleet/fleetCollector.js +146 -5
- package/build/services/fleet/fleetCollector.js.map +1 -1
- package/build/services/fleet/fleetEvents.d.ts +39 -3
- package/build/services/fleet/fleetEvents.d.ts.map +1 -1
- package/build/services/fleet/fleetEvents.js +66 -22
- package/build/services/fleet/fleetEvents.js.map +1 -1
- package/build/services/fleet/fleetQueue.d.ts +26 -0
- package/build/services/fleet/fleetQueue.d.ts.map +1 -1
- package/build/services/fleet/fleetQueue.js +94 -8
- package/build/services/fleet/fleetQueue.js.map +1 -1
- package/build/services/fleet/telemetryLimits.d.ts +16 -0
- package/build/services/fleet/telemetryLimits.d.ts.map +1 -0
- package/build/services/fleet/telemetryLimits.js +69 -0
- package/build/services/fleet/telemetryLimits.js.map +1 -0
- package/build/services/installMiniapp.d.ts +13 -0
- package/build/services/installMiniapp.d.ts.map +1 -1
- package/build/services/installMiniapp.js +64 -1
- package/build/services/installMiniapp.js.map +1 -1
- package/build/services/miniappReleaseInstall.js +1 -1
- package/build/services/miniappReleaseInstall.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +16 -1
- package/build/stores/apps.js.map +1 -1
- package/package.json +8 -8
- package/src/facades/glasses.ts +5 -3
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/AppRegistry.ts +2 -0
- package/src/services/LocalMiniappRuntime.ts +20 -1
- package/src/services/MentraJSCrashController.ts +5 -0
- package/src/services/MentraJSRouter.ts +15 -3
- package/src/services/MiniappLauncher.ts +18 -2
- package/src/services/fleet/FleetReporter.ts +40 -0
- package/src/services/fleet/fleetCollector.ts +160 -6
- package/src/services/fleet/fleetEvents.ts +104 -20
- package/src/services/fleet/fleetQueue.ts +100 -7
- package/src/services/fleet/telemetryLimits.ts +78 -0
- package/src/services/installMiniapp.ts +78 -2
- package/src/services/miniappReleaseInstall.ts +1 -1
- package/src/stores/apps.ts +19 -2
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* the engine restores miniapps while the account it collects for may still be being resolved, and those launches are
|
|
16
16
|
* that account's. The reporter discards them when the account changes. Photo stages are not kept.
|
|
17
17
|
*
|
|
18
|
+
* Phase 12 adds a miniapp's own events (`telemetry`, from `session.telemetry.capture`, attributed by the runtime to the
|
|
19
|
+
* package whose bridge sent it), its crashes, install attempts, and the glasses' pairing and connection changes. None of
|
|
20
|
+
* these is kept while nothing listens: with no run collecting (Fleet not installed on the phone's Core, nobody signed
|
|
21
|
+
* in) they are dropped, and telemetry dropped this way is not counted.
|
|
22
|
+
*
|
|
18
23
|
* This module imports nothing at run time, so the hot paths that import it carry no Fleet weight.
|
|
19
24
|
*/
|
|
20
|
-
import type { FleetRecord } from "@mentra/cloud-protocol";
|
|
25
|
+
import type { FleetConnectionEventPayload, FleetLaunchFailureReason, FleetMiniappCrashPayload, FleetMiniappInstallPayload, FleetRecord } from "@mentra/cloud-protocol";
|
|
21
26
|
/** A miniapp start the engine attempted, and how it went. */
|
|
22
27
|
export type FleetLaunchEvent = Extract<FleetRecord, {
|
|
23
28
|
kind: "miniapp_launch";
|
|
@@ -36,9 +41,25 @@ export interface UndeliveredLaunch {
|
|
|
36
41
|
event: FleetLaunchEvent;
|
|
37
42
|
observedAt: number;
|
|
38
43
|
}
|
|
44
|
+
/** A miniapp's own event as the bridge delivered it: unchecked, the collector applies the limits. */
|
|
45
|
+
export interface FleetTelemetryEvent {
|
|
46
|
+
packageName: string;
|
|
47
|
+
versionName?: string;
|
|
48
|
+
event: unknown;
|
|
49
|
+
properties: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** A crash: the collector adds the miniapp's execution session. */
|
|
52
|
+
export type FleetCrashEvent = Omit<FleetMiniappCrashPayload, "executionSessionId">;
|
|
53
|
+
export type FleetInstallEvent = FleetMiniappInstallPayload;
|
|
54
|
+
/** A pairing or connection change: the collector adds the glasses, their model and the Mentra App's version. */
|
|
55
|
+
export type FleetConnectionEvent = Pick<FleetConnectionEventPayload, "event" | "category" | "reconnectMs">;
|
|
39
56
|
export interface FleetEventListener {
|
|
40
57
|
launch?: (event: FleetLaunchEvent) => void;
|
|
41
58
|
photo?: (event: FleetPhotoEvent) => void;
|
|
59
|
+
telemetry?: (event: FleetTelemetryEvent) => void;
|
|
60
|
+
crash?: (event: FleetCrashEvent) => void;
|
|
61
|
+
install?: (event: FleetInstallEvent) => void;
|
|
62
|
+
connection?: (event: FleetConnectionEvent) => void;
|
|
42
63
|
}
|
|
43
64
|
export declare const fleetEvents: {
|
|
44
65
|
/** Listens for events until the returned function is called. */
|
|
@@ -50,13 +71,28 @@ export declare const fleetEvents: {
|
|
|
50
71
|
* same miniapp is under way, which reports it. Never throws.
|
|
51
72
|
*/
|
|
52
73
|
launchUnlessClaimed(event: FleetLaunchEvent): void;
|
|
53
|
-
/**
|
|
54
|
-
|
|
74
|
+
/**
|
|
75
|
+
* A start of `packageName` that reports its own launch is under way, until the returned function is called. That
|
|
76
|
+
* function returns why a spawn the start held back failed, for the start's own report, or undefined.
|
|
77
|
+
*/
|
|
78
|
+
claimLaunch(packageName: string): () => FleetLaunchFailureReason | undefined;
|
|
55
79
|
/** The launches heard while nothing listened, oldest first; they are forgotten here. */
|
|
56
80
|
takeUndeliveredLaunches(): UndeliveredLaunch[];
|
|
57
81
|
/** Forgets the launches heard while nothing listened: they are no one's to report now. */
|
|
58
82
|
discardUndeliveredLaunches(): void;
|
|
59
83
|
/** A photo operation reached a stage. Never throws. */
|
|
60
84
|
photo(event: FleetPhotoEvent): void;
|
|
85
|
+
/** A miniapp captured one of its own events. Never throws. */
|
|
86
|
+
telemetry(event: FleetTelemetryEvent): void;
|
|
87
|
+
/** A miniapp crashed. Never throws. */
|
|
88
|
+
crash(event: FleetCrashEvent): void;
|
|
89
|
+
/** An install, update or removal of a miniapp was attempted. Never throws. */
|
|
90
|
+
install(event: FleetInstallEvent): void;
|
|
91
|
+
/** The person asked the glasses to disconnect (or forgot or unpaired them): the next disconnect is theirs. */
|
|
92
|
+
userDisconnect(): void;
|
|
93
|
+
/** When the person last asked the glasses to disconnect (epoch ms), or 0. */
|
|
94
|
+
lastUserDisconnect(): number;
|
|
95
|
+
/** The glasses' pairing or connection changed. Never throws. */
|
|
96
|
+
connection(event: FleetConnectionEvent): void;
|
|
61
97
|
};
|
|
62
98
|
//# sourceMappingURL=fleetEvents.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleetEvents.d.ts","sourceRoot":"","sources":["../../../src/services/fleet/fleetEvents.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fleetEvents.d.ts","sourceRoot":"","sources":["../../../src/services/fleet/fleetEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EACV,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,EACxB,0BAA0B,EAC1B,WAAW,EACZ,MAAM,wBAAwB,CAAA;AAE/B,6DAA6D;AAC7D,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,WAAW,EAAE;IAAC,IAAI,EAAE,gBAAgB,CAAA;CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAExF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,WAAW,EAAE;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAEhH,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,gBAAgB,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qGAAqG;AACrG,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,wBAAwB,EAAE,oBAAoB,CAAC,CAAA;AAClF,MAAM,MAAM,iBAAiB,GAAG,0BAA0B,CAAA;AAC1D,gHAAgH;AAChH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,2BAA2B,EAAE,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC,CAAA;AAE1G,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC1C,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAChD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;CACnD;AAoCD,eAAO,MAAM,WAAW;IACtB,gEAAgE;wBAC5C,kBAAkB,GAAG,MAAM,IAAI;IAOnD,mDAAmD;kBACrC,gBAAgB,GAAG,IAAI;IAIrC;;;OAGG;+BACwB,gBAAgB,GAAG,IAAI;IAYlD;;;OAGG;6BACsB,MAAM,GAAG,MAAM,wBAAwB,GAAG,SAAS;IAgB5E,wFAAwF;+BAC7D,iBAAiB,EAAE;IAI9C,0FAA0F;kCAC5D,IAAI;IAIlC,uDAAuD;iBAC1C,eAAe,GAAG,IAAI;IAInC,8DAA8D;qBAC7C,mBAAmB,GAAG,IAAI;IAI3C,uCAAuC;iBAC1B,eAAe,GAAG,IAAI;IAInC,8EAA8E;mBAC/D,iBAAiB,GAAG,IAAI;IAIvC,8GAA8G;sBAC5F,IAAI;IAItB,6EAA6E;0BACvD,MAAM;IAI5B,gEAAgE;sBAC9C,oBAAoB,GAAG,IAAI;CAG9C,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** More launches than this before the collector starts are not one boot's restores; the oldest go first. */
|
|
2
2
|
const MAX_UNDELIVERED_LAUNCHES = 32;
|
|
3
3
|
const listeners = new Set();
|
|
4
|
+
let lastUserDisconnectAt = 0;
|
|
4
5
|
const undelivered = [];
|
|
5
6
|
/** Starts under way that report their own launch, by package. */
|
|
6
7
|
const claims = new Map();
|
|
@@ -41,23 +42,37 @@ export const fleetEvents = {
|
|
|
41
42
|
* same miniapp is under way, which reports it. Never throws.
|
|
42
43
|
*/
|
|
43
44
|
launchUnlessClaimed(event) {
|
|
44
|
-
|
|
45
|
+
const held = claims.get(event.packageName);
|
|
46
|
+
if (!held) {
|
|
47
|
+
deliverLaunch(event);
|
|
45
48
|
return;
|
|
46
|
-
|
|
49
|
+
}
|
|
50
|
+
// The start reports this attempt as its own, so it keeps the reason the spawn failed for.
|
|
51
|
+
const reason = event.failureReason;
|
|
52
|
+
if (reason === undefined)
|
|
53
|
+
return;
|
|
54
|
+
for (const claim of held)
|
|
55
|
+
if (claim.failureReason === undefined || claim.failureReason === "unknown")
|
|
56
|
+
claim.failureReason = reason;
|
|
47
57
|
},
|
|
48
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* A start of `packageName` that reports its own launch is under way, until the returned function is called. That
|
|
60
|
+
* function returns why a spawn the start held back failed, for the start's own report, or undefined.
|
|
61
|
+
*/
|
|
49
62
|
claimLaunch(packageName) {
|
|
50
|
-
|
|
63
|
+
const claim = {};
|
|
64
|
+
const held = claims.get(packageName) ?? new Set();
|
|
65
|
+
held.add(claim);
|
|
66
|
+
claims.set(packageName, held);
|
|
51
67
|
let released = false;
|
|
52
68
|
return () => {
|
|
53
|
-
if (released)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
claims.delete(packageName);
|
|
69
|
+
if (!released) {
|
|
70
|
+
released = true;
|
|
71
|
+
held.delete(claim);
|
|
72
|
+
if (held.size === 0 && claims.get(packageName) === held)
|
|
73
|
+
claims.delete(packageName);
|
|
74
|
+
}
|
|
75
|
+
return claim.failureReason;
|
|
61
76
|
};
|
|
62
77
|
},
|
|
63
78
|
/** The launches heard while nothing listened, oldest first; they are forgotten here. */
|
|
@@ -70,16 +85,45 @@ export const fleetEvents = {
|
|
|
70
85
|
},
|
|
71
86
|
/** A photo operation reached a stage. Never throws. */
|
|
72
87
|
photo(event) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
deliver("photo", event);
|
|
89
|
+
},
|
|
90
|
+
/** A miniapp captured one of its own events. Never throws. */
|
|
91
|
+
telemetry(event) {
|
|
92
|
+
deliver("telemetry", event);
|
|
93
|
+
},
|
|
94
|
+
/** A miniapp crashed. Never throws. */
|
|
95
|
+
crash(event) {
|
|
96
|
+
deliver("crash", event);
|
|
97
|
+
},
|
|
98
|
+
/** An install, update or removal of a miniapp was attempted. Never throws. */
|
|
99
|
+
install(event) {
|
|
100
|
+
deliver("install", event);
|
|
101
|
+
},
|
|
102
|
+
/** The person asked the glasses to disconnect (or forgot or unpaired them): the next disconnect is theirs. */
|
|
103
|
+
userDisconnect() {
|
|
104
|
+
lastUserDisconnectAt = Date.now();
|
|
105
|
+
},
|
|
106
|
+
/** When the person last asked the glasses to disconnect (epoch ms), or 0. */
|
|
107
|
+
lastUserDisconnect() {
|
|
108
|
+
return lastUserDisconnectAt;
|
|
109
|
+
},
|
|
110
|
+
/** The glasses' pairing or connection changed. Never throws. */
|
|
111
|
+
connection(event) {
|
|
112
|
+
deliver("connection", event);
|
|
83
113
|
},
|
|
84
114
|
};
|
|
115
|
+
/** Hands `event` to every listener of `kind`; with none, it is dropped. */
|
|
116
|
+
function deliver(kind, event) {
|
|
117
|
+
if (listeners.size === 0)
|
|
118
|
+
return;
|
|
119
|
+
for (const listener of [...listeners]) {
|
|
120
|
+
try {
|
|
121
|
+
;
|
|
122
|
+
listener[kind]?.(event);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
warnListenerFailed(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
85
129
|
//# sourceMappingURL=fleetEvents.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleetEvents.js","sourceRoot":"","sources":["../../../src/services/fleet/fleetEvents.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The seam between the engine's lifecycle hooks and the Fleet reporter.\n *\n * Hooks in the miniapp, photo and pairing paths call `fleetEvents.launch` and `fleetEvents.photo`. With a reporter\n * running, the collector hears the event on the caller's own stack, so a listener must be cheap, and an emit never\n * throws into the caller: a listener that fails is logged by name and skipped. A hook can therefore never break or\n * slow the path it sits in.\n *\n * Every start of a miniapp is one launch. The apps store's `start` reports its own, and claims the miniapp while it\n * runs (`claimLaunch`); every other path that starts one (the launcher, for restores, miniapps started by other\n * miniapps and screens opened directly; built-ins restored running) reports through `launchUnlessClaimed`, so a spawn\n * a `start` causes elsewhere is not a second launch.\n *\n * A launch heard while nothing listens is kept, with the time it happened, for the collector to take when it starts:\n * the engine restores miniapps while the account it collects for may still be being resolved, and those launches are\n * that account's. The reporter discards them when the account changes. Photo stages are not kept.\n *\n * This module imports nothing at run time, so the hot paths that import it carry no Fleet weight.\n */\nimport type {FleetRecord} from \"@mentra/cloud-protocol\"\n\n/** A miniapp start the engine attempted, and how it went. */\nexport type FleetLaunchEvent = Extract<FleetRecord, {kind: \"miniapp_launch\"}>[\"payload\"]\n\n/**\n * One stage of a photo operation. `operationId` is the managed photo request ID, shared by every stage of the one\n * photo. `failedStage` is the last stage the operation reached before it failed.\n */\nexport type FleetPhotoEvent = {operationId: string} & Extract<FleetRecord, {kind: \"photo_operation\"}>[\"payload\"]\n\n/** A launch heard while nothing listened, and when it happened (epoch milliseconds). */\nexport interface UndeliveredLaunch {\n event: FleetLaunchEvent\n observedAt: number\n}\n\nexport interface FleetEventListener {\n launch?: (event: FleetLaunchEvent) => void\n photo?: (event: FleetPhotoEvent) => void\n}\n\n/** More launches than this before the collector starts are not one boot's restores; the oldest go first. */\nconst MAX_UNDELIVERED_LAUNCHES = 32\n\nconst listeners = new Set<FleetEventListener>()\nconst undelivered: UndeliveredLaunch[] = []\n/** Starts under way that report their own launch, by package. */\nconst claims = new Map<string, number>()\n\nfunction warnListenerFailed(error: unknown): void {\n // The event is left out of the warning: it names a package or an operation.\n console.warn(\"fleetEvents: a listener failed:\", error instanceof Error ? error.message : \"unknown error\")\n}\n\nfunction deliverLaunch(event: FleetLaunchEvent): void {\n if (listeners.size === 0) {\n undelivered.push({event, observedAt: Date.now()})\n if (undelivered.length > MAX_UNDELIVERED_LAUNCHES) undelivered.shift()\n return\n }\n for (const listener of [...listeners]) {\n try {\n listener.launch?.(event)\n } catch (error) {\n warnListenerFailed(error)\n }\n }\n}\n\nexport const fleetEvents = {\n /** Listens for events until the returned function is called. */\n subscribe(listener: FleetEventListener): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n\n /** A miniapp start was attempted. Never throws. */\n launch(event: FleetLaunchEvent): void {\n deliverLaunch(event)\n },\n\n /**\n * A miniapp start was attempted somewhere other than the apps store's `start`: reported unless a `start` of the\n * same miniapp is under way, which reports it. Never throws.\n */\n launchUnlessClaimed(event: FleetLaunchEvent): void {\n if ((claims.get(event.packageName) ?? 0) > 0) return\n deliverLaunch(event)\n },\n\n /** A start of `packageName` that reports its own launch is under way, until the returned function is called. */\n claimLaunch(packageName: string): () => void {\n claims.set(packageName, (claims.get(packageName) ?? 0) + 1)\n let released = false\n return () => {\n if (released) return\n released = true\n const left = (claims.get(packageName) ?? 1) - 1\n if (left > 0) claims.set(packageName, left)\n else claims.delete(packageName)\n }\n },\n\n /** The launches heard while nothing listened, oldest first; they are forgotten here. */\n takeUndeliveredLaunches(): UndeliveredLaunch[] {\n return undelivered.splice(0)\n },\n\n /** Forgets the launches heard while nothing listened: they are no one's to report now. */\n discardUndeliveredLaunches(): void {\n undelivered.length = 0\n },\n\n /** A photo operation reached a stage. Never throws. */\n photo(event: FleetPhotoEvent): void {\n if (listeners.size === 0) return\n for (const listener of [...listeners]) {\n try {\n listener.photo?.(event)\n } catch (error) {\n warnListenerFailed(error)\n }\n }\n },\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fleetEvents.js","sourceRoot":"","sources":["../../../src/services/fleet/fleetEvents.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The seam between the engine's lifecycle hooks and the Fleet reporter.\n *\n * Hooks in the miniapp, photo and pairing paths call `fleetEvents.launch` and `fleetEvents.photo`. With a reporter\n * running, the collector hears the event on the caller's own stack, so a listener must be cheap, and an emit never\n * throws into the caller: a listener that fails is logged by name and skipped. A hook can therefore never break or\n * slow the path it sits in.\n *\n * Every start of a miniapp is one launch. The apps store's `start` reports its own, and claims the miniapp while it\n * runs (`claimLaunch`); every other path that starts one (the launcher, for restores, miniapps started by other\n * miniapps and screens opened directly; built-ins restored running) reports through `launchUnlessClaimed`, so a spawn\n * a `start` causes elsewhere is not a second launch.\n *\n * A launch heard while nothing listens is kept, with the time it happened, for the collector to take when it starts:\n * the engine restores miniapps while the account it collects for may still be being resolved, and those launches are\n * that account's. The reporter discards them when the account changes. Photo stages are not kept.\n *\n * Phase 12 adds a miniapp's own events (`telemetry`, from `session.telemetry.capture`, attributed by the runtime to the\n * package whose bridge sent it), its crashes, install attempts, and the glasses' pairing and connection changes. None of\n * these is kept while nothing listens: with no run collecting (Fleet not installed on the phone's Core, nobody signed\n * in) they are dropped, and telemetry dropped this way is not counted.\n *\n * This module imports nothing at run time, so the hot paths that import it carry no Fleet weight.\n */\nimport type {\n FleetConnectionEventPayload,\n FleetLaunchFailureReason,\n FleetMiniappCrashPayload,\n FleetMiniappInstallPayload,\n FleetRecord,\n} from \"@mentra/cloud-protocol\"\n\n/** A miniapp start the engine attempted, and how it went. */\nexport type FleetLaunchEvent = Extract<FleetRecord, {kind: \"miniapp_launch\"}>[\"payload\"]\n\n/**\n * One stage of a photo operation. `operationId` is the managed photo request ID, shared by every stage of the one\n * photo. `failedStage` is the last stage the operation reached before it failed.\n */\nexport type FleetPhotoEvent = {operationId: string} & Extract<FleetRecord, {kind: \"photo_operation\"}>[\"payload\"]\n\n/** A launch heard while nothing listened, and when it happened (epoch milliseconds). */\nexport interface UndeliveredLaunch {\n event: FleetLaunchEvent\n observedAt: number\n}\n\n/** A miniapp's own event as the bridge delivered it: unchecked, the collector applies the limits. */\nexport interface FleetTelemetryEvent {\n packageName: string\n versionName?: string\n event: unknown\n properties: unknown\n}\n\n/** A crash: the collector adds the miniapp's execution session. */\nexport type FleetCrashEvent = Omit<FleetMiniappCrashPayload, \"executionSessionId\">\nexport type FleetInstallEvent = FleetMiniappInstallPayload\n/** A pairing or connection change: the collector adds the glasses, their model and the Mentra App's version. */\nexport type FleetConnectionEvent = Pick<FleetConnectionEventPayload, \"event\" | \"category\" | \"reconnectMs\">\n\nexport interface FleetEventListener {\n launch?: (event: FleetLaunchEvent) => void\n photo?: (event: FleetPhotoEvent) => void\n telemetry?: (event: FleetTelemetryEvent) => void\n crash?: (event: FleetCrashEvent) => void\n install?: (event: FleetInstallEvent) => void\n connection?: (event: FleetConnectionEvent) => void\n}\n\n/** More launches than this before the collector starts are not one boot's restores; the oldest go first. */\nconst MAX_UNDELIVERED_LAUNCHES = 32\n\nconst listeners = new Set<FleetEventListener>()\nlet lastUserDisconnectAt = 0\nconst undelivered: UndeliveredLaunch[] = []\n/** A start under way that reports its own launch: why a spawn it held back failed, if one did. */\ninterface LaunchClaim {\n failureReason?: FleetLaunchFailureReason\n}\n\n/** Starts under way that report their own launch, by package. */\nconst claims = new Map<string, Set<LaunchClaim>>()\n\nfunction warnListenerFailed(error: unknown): void {\n // The event is left out of the warning: it names a package or an operation.\n console.warn(\"fleetEvents: a listener failed:\", error instanceof Error ? error.message : \"unknown error\")\n}\n\nfunction deliverLaunch(event: FleetLaunchEvent): void {\n if (listeners.size === 0) {\n undelivered.push({event, observedAt: Date.now()})\n if (undelivered.length > MAX_UNDELIVERED_LAUNCHES) undelivered.shift()\n return\n }\n for (const listener of [...listeners]) {\n try {\n listener.launch?.(event)\n } catch (error) {\n warnListenerFailed(error)\n }\n }\n}\n\nexport const fleetEvents = {\n /** Listens for events until the returned function is called. */\n subscribe(listener: FleetEventListener): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n\n /** A miniapp start was attempted. Never throws. */\n launch(event: FleetLaunchEvent): void {\n deliverLaunch(event)\n },\n\n /**\n * A miniapp start was attempted somewhere other than the apps store's `start`: reported unless a `start` of the\n * same miniapp is under way, which reports it. Never throws.\n */\n launchUnlessClaimed(event: FleetLaunchEvent): void {\n const held = claims.get(event.packageName)\n if (!held) {\n deliverLaunch(event)\n return\n }\n // The start reports this attempt as its own, so it keeps the reason the spawn failed for.\n const reason = event.failureReason\n if (reason === undefined) return\n for (const claim of held) if (claim.failureReason === undefined || claim.failureReason === \"unknown\") claim.failureReason = reason\n },\n\n /**\n * A start of `packageName` that reports its own launch is under way, until the returned function is called. That\n * function returns why a spawn the start held back failed, for the start's own report, or undefined.\n */\n claimLaunch(packageName: string): () => FleetLaunchFailureReason | undefined {\n const claim: LaunchClaim = {}\n const held = claims.get(packageName) ?? new Set<LaunchClaim>()\n held.add(claim)\n claims.set(packageName, held)\n let released = false\n return () => {\n if (!released) {\n released = true\n held.delete(claim)\n if (held.size === 0 && claims.get(packageName) === held) claims.delete(packageName)\n }\n return claim.failureReason\n }\n },\n\n /** The launches heard while nothing listened, oldest first; they are forgotten here. */\n takeUndeliveredLaunches(): UndeliveredLaunch[] {\n return undelivered.splice(0)\n },\n\n /** Forgets the launches heard while nothing listened: they are no one's to report now. */\n discardUndeliveredLaunches(): void {\n undelivered.length = 0\n },\n\n /** A photo operation reached a stage. Never throws. */\n photo(event: FleetPhotoEvent): void {\n deliver(\"photo\", event)\n },\n\n /** A miniapp captured one of its own events. Never throws. */\n telemetry(event: FleetTelemetryEvent): void {\n deliver(\"telemetry\", event)\n },\n\n /** A miniapp crashed. Never throws. */\n crash(event: FleetCrashEvent): void {\n deliver(\"crash\", event)\n },\n\n /** An install, update or removal of a miniapp was attempted. Never throws. */\n install(event: FleetInstallEvent): void {\n deliver(\"install\", event)\n },\n\n /** The person asked the glasses to disconnect (or forgot or unpaired them): the next disconnect is theirs. */\n userDisconnect(): void {\n lastUserDisconnectAt = Date.now()\n },\n\n /** When the person last asked the glasses to disconnect (epoch ms), or 0. */\n lastUserDisconnect(): number {\n return lastUserDisconnectAt\n },\n\n /** The glasses' pairing or connection changed. Never throws. */\n connection(event: FleetConnectionEvent): void {\n deliver(\"connection\", event)\n },\n}\n\n/** Hands `event` to every listener of `kind`; with none, it is dropped. */\nfunction deliver<K extends \"photo\" | \"telemetry\" | \"crash\" | \"install\" | \"connection\">(kind: K, event: Parameters<NonNullable<FleetEventListener[K]>>[0]): void {\n if (listeners.size === 0) return\n for (const listener of [...listeners]) {\n try {\n ;(listener[kind] as ((value: typeof event) => void) | undefined)?.(event)\n } catch (error) {\n warnListenerFailed(error)\n }\n }\n}\n"]}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* sequence and the oldest and newest record keys.
|
|
14
14
|
* - `l:<partition>` holds the losses not yet reported: `{dropped, expired, from, to, updatedAt}`.
|
|
15
15
|
* - `n:<partition>` holds the collector-session sequence counters, as `[collectorSessionId, next][]`.
|
|
16
|
+
* - `t:<partition>` holds the miniapp telemetry lost and not yet reported, per package (`FleetTelemetryLosses`).
|
|
17
|
+
*
|
|
18
|
+
* Telemetry (Phase 12) has a share of the queue: a `telemetry_event`'s record ID starts with `tel.`, so its key says
|
|
19
|
+
* what it is. Telemetry takes at most 2,000 of the 10,000 places. When that share or the queue is full, the oldest
|
|
20
|
+
* telemetry record gives way, and a record of any other kind never does to telemetry: an arriving record that finds
|
|
21
|
+
* the queue full evicts the oldest telemetry before anything else. Telemetry that is evicted, expires, or that MFM
|
|
22
|
+
* rejects is counted per package under `t:`, never as a coverage gap, and the reporter sends it as `telemetry_loss`.
|
|
16
23
|
*
|
|
17
24
|
* MMKV can list keys only by listing all of them, so the first call for a partition in a process scans the store
|
|
18
25
|
* once, repairs the index if a crash left it behind, and keeps the partition's ordered record keys in memory.
|
|
@@ -48,6 +55,21 @@ export interface FleetLosses {
|
|
|
48
55
|
/** The latest `observedAt` among the lost records, or null if none had a readable one. */
|
|
49
56
|
to: string | null;
|
|
50
57
|
}
|
|
58
|
+
/** The telemetry the queue (or the Mentra App's limits) lost, per package, since the last `takeTelemetryLosses`. */
|
|
59
|
+
export interface FleetTelemetryLosses {
|
|
60
|
+
packages: Record<string, {
|
|
61
|
+
overLimit: number;
|
|
62
|
+
evicted: number;
|
|
63
|
+
expired: number;
|
|
64
|
+
rejected: number;
|
|
65
|
+
}>;
|
|
66
|
+
from: string | null;
|
|
67
|
+
to: string | null;
|
|
68
|
+
}
|
|
69
|
+
export type FleetTelemetryLossKind = "overLimit" | "evicted" | "expired" | "rejected";
|
|
70
|
+
/** A telemetry record's ID starts with this, so the queue tells telemetry from the rest by its key. */
|
|
71
|
+
export declare const TELEMETRY_RECORD_PREFIX = "tel.";
|
|
72
|
+
export declare const isTelemetryRecordId: (recordId: string) => boolean;
|
|
51
73
|
/** A partition is one account on one Core. */
|
|
52
74
|
export interface FleetQueue {
|
|
53
75
|
/** Adds a record. Drops the oldest beyond 10,000 and counts the drop. Never throws: a record that cannot be stored is counted as dropped. */
|
|
@@ -80,6 +102,10 @@ export interface FleetQueue {
|
|
|
80
102
|
* an idle reporter does not wake to sweep). `now` defaults to the queue's clock.
|
|
81
103
|
*/
|
|
82
104
|
sweepExpired(now?: number): void;
|
|
105
|
+
/** Counts `count` of `packageName`'s telemetry as lost, as of `at` (epoch ms). Never throws. */
|
|
106
|
+
noteTelemetryLoss(partition: string, packageName: string, kind: FleetTelemetryLossKind, count: number, at?: number): void;
|
|
107
|
+
/** The telemetry lost since the last call, per package, and resets the counters; `null` when nothing was lost. */
|
|
108
|
+
takeTelemetryLosses(partition: string, now: number): FleetTelemetryLosses | null;
|
|
83
109
|
/** Forgets everything held for the partition: records, losses and sequence counters. */
|
|
84
110
|
clearPartition(partition: string): void;
|
|
85
111
|
size(partition: string): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleetQueue.d.ts","sourceRoot":"","sources":["../../../src/services/fleet/fleetQueue.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fleetQueue.d.ts","sourceRoot":"","sources":["../../../src/services/fleet/fleetQueue.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* sequence and the oldest and newest record keys.
|
|
14
14
|
* - `l:<partition>` holds the losses not yet reported: `{dropped, expired, from, to, updatedAt}`.
|
|
15
15
|
* - `n:<partition>` holds the collector-session sequence counters, as `[collectorSessionId, next][]`.
|
|
16
|
+
* - `t:<partition>` holds the miniapp telemetry lost and not yet reported, per package (`FleetTelemetryLosses`).
|
|
17
|
+
*
|
|
18
|
+
* Telemetry (Phase 12) has a share of the queue: a `telemetry_event`'s record ID starts with `tel.`, so its key says
|
|
19
|
+
* what it is. Telemetry takes at most 2,000 of the 10,000 places. When that share or the queue is full, the oldest
|
|
20
|
+
* telemetry record gives way, and a record of any other kind never does to telemetry: an arriving record that finds
|
|
21
|
+
* the queue full evicts the oldest telemetry before anything else. Telemetry that is evicted, expires, or that MFM
|
|
22
|
+
* rejects is counted per package under `t:`, never as a coverage gap, and the reporter sends it as `telemetry_loss`.
|
|
16
23
|
*
|
|
17
24
|
* MMKV can list keys only by listing all of them, so the first call for a partition in a process scans the store
|
|
18
25
|
* once, repairs the index if a crash left it behind, and keeps the partition's ordered record keys in memory.
|
|
@@ -29,10 +36,13 @@
|
|
|
29
36
|
* `sweepExpired` is how the 7-day bound still holds for it: one scan finds every partition, expires its old
|
|
30
37
|
* records and removes what an empty partition no longer needs.
|
|
31
38
|
*/
|
|
32
|
-
import { FLEET_LIMITS } from "@mentra/cloud-protocol";
|
|
39
|
+
import { FLEET_LIMITS, FLEET_TELEMETRY_LIMITS } from "@mentra/cloud-protocol";
|
|
33
40
|
import { normalizeCoreUrl } from "./coreUrl";
|
|
34
41
|
import { getFleetStore } from "./fleetStorage";
|
|
35
42
|
import { sha256HexOfText, utf8ByteLength } from "./utf8";
|
|
43
|
+
/** A telemetry record's ID starts with this, so the queue tells telemetry from the rest by its key. */
|
|
44
|
+
export const TELEMETRY_RECORD_PREFIX = "tel.";
|
|
45
|
+
export const isTelemetryRecordId = (recordId) => recordId.startsWith(TELEMETRY_RECORD_PREFIX);
|
|
36
46
|
const SEQUENCE_WIDTH = 16;
|
|
37
47
|
const MAX_TRACKED_COLLECTOR_SESSIONS = 16;
|
|
38
48
|
const EMPTY_LOSSES = { dropped: 0, expired: 0, from: null, to: null, updatedAt: 0 };
|
|
@@ -47,6 +57,7 @@ const recordPrefix = (partition) => `q:${partition}:`;
|
|
|
47
57
|
const indexKey = (partition) => `i:${partition}`;
|
|
48
58
|
const lossKey = (partition) => `l:${partition}`;
|
|
49
59
|
const sequenceKey = (partition) => `n:${partition}`;
|
|
60
|
+
const telemetryLossKey = (partition) => `t:${partition}`;
|
|
50
61
|
function recordKey(partition, sequence, recordId) {
|
|
51
62
|
return `${recordPrefix(partition)}${String(sequence).padStart(SEQUENCE_WIDTH, "0")}:${recordId}`;
|
|
52
63
|
}
|
|
@@ -120,7 +131,7 @@ function groupByPartition(keys) {
|
|
|
120
131
|
const entry = (partition) => {
|
|
121
132
|
let existing = found.get(partition);
|
|
122
133
|
if (!existing) {
|
|
123
|
-
existing = { records: [], hasIndex: false, hasLosses: false, hasSequences: false };
|
|
134
|
+
existing = { records: [], hasIndex: false, hasLosses: false, hasTelemetryLosses: false, hasSequences: false };
|
|
124
135
|
found.set(partition, existing);
|
|
125
136
|
}
|
|
126
137
|
return existing;
|
|
@@ -137,6 +148,8 @@ function groupByPartition(keys) {
|
|
|
137
148
|
entry(key.slice(2)).hasLosses = true;
|
|
138
149
|
else if (key.startsWith("n:"))
|
|
139
150
|
entry(key.slice(2)).hasSequences = true;
|
|
151
|
+
else if (key.startsWith("t:"))
|
|
152
|
+
entry(key.slice(2)).hasTelemetryLosses = true;
|
|
140
153
|
}
|
|
141
154
|
return found;
|
|
142
155
|
}
|
|
@@ -256,14 +269,35 @@ function buildQueue(store, options) {
|
|
|
256
269
|
}
|
|
257
270
|
/** Discards records that cannot be read, counting them as dropped. */
|
|
258
271
|
function discardUnreadable(partition, state, unreadable) {
|
|
259
|
-
|
|
272
|
+
// An unreadable telemetry record names no miniapp to count it for, and is never a coverage gap.
|
|
273
|
+
noteLosses(partition, "dropped", unreadable.filter((key) => !isTelemetryKey(partition, key)).map(() => undefined));
|
|
260
274
|
const gone = new Set(unreadable);
|
|
261
275
|
for (const key of unreadable)
|
|
262
276
|
removeKey(key);
|
|
263
277
|
state.keys = state.keys.filter((key) => !gone.has(key));
|
|
264
278
|
persistIndex(partition, state);
|
|
265
279
|
}
|
|
280
|
+
const idOffsetOf = (partition) => recordPrefix(partition).length + SEQUENCE_WIDTH + 1;
|
|
281
|
+
const isTelemetryKey = (partition, key) => isTelemetryRecordId(key.slice(idOffsetOf(partition)));
|
|
282
|
+
/** Removes the telemetry record at `key`, counting it as `kind` for its package. */
|
|
283
|
+
function evictTelemetry(partition, state, key, kind, at = now()) {
|
|
284
|
+
const record = readEntry(key)?.record;
|
|
285
|
+
const packageName = record?.kind === "telemetry_event" ? record.payload.packageName : null;
|
|
286
|
+
if (packageName !== null)
|
|
287
|
+
noteTelemetryLoss(partition, packageName, kind, 1, at, record.observedAt);
|
|
288
|
+
state.keys.splice(state.keys.indexOf(key), 1);
|
|
289
|
+
removeKey(key);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Keeps the partition inside its places: telemetry inside its share, and the whole inside 10,000, the oldest
|
|
293
|
+
* telemetry going first. Only a queue full of other records drops the oldest record of all, as a coverage gap.
|
|
294
|
+
*/
|
|
266
295
|
function trimToCapacity(partition, state) {
|
|
296
|
+
const telemetry = state.keys.filter((key) => isTelemetryKey(partition, key));
|
|
297
|
+
while (telemetry.length > FLEET_TELEMETRY_LIMITS.queueShare)
|
|
298
|
+
evictTelemetry(partition, state, telemetry.shift(), "evicted");
|
|
299
|
+
while (state.keys.length > FLEET_LIMITS.phoneQueueMaxRecords && telemetry.length > 0)
|
|
300
|
+
evictTelemetry(partition, state, telemetry.shift(), "evicted");
|
|
267
301
|
const excess = state.keys.length - FLEET_LIMITS.phoneQueueMaxRecords;
|
|
268
302
|
if (excess <= 0)
|
|
269
303
|
return;
|
|
@@ -271,6 +305,29 @@ function buildQueue(store, options) {
|
|
|
271
305
|
noteLosses(partition, "dropped", observedAts);
|
|
272
306
|
removeOldest(state, excess);
|
|
273
307
|
}
|
|
308
|
+
function readTelemetryLosses(partition) {
|
|
309
|
+
const value = readJson(telemetryLossKey(partition));
|
|
310
|
+
return value && typeof value === "object" && value.packages && typeof value.packages === "object" ? value : null;
|
|
311
|
+
}
|
|
312
|
+
function noteTelemetryLoss(partition, packageName, kind, count, at = now(), observedAt) {
|
|
313
|
+
if (!(count > 0))
|
|
314
|
+
return;
|
|
315
|
+
try {
|
|
316
|
+
const stored = readTelemetryLosses(partition) ?? { packages: {}, from: null, to: null, updatedAt: at };
|
|
317
|
+
const entry = stored.packages[packageName] ?? { overLimit: 0, evicted: 0, expired: 0, rejected: 0 };
|
|
318
|
+
entry[kind] += count;
|
|
319
|
+
stored.packages[packageName] = entry;
|
|
320
|
+
const when = observedAt ?? new Date(at).toISOString();
|
|
321
|
+
stored.from = earlier(stored.from, when);
|
|
322
|
+
stored.to = later(stored.to, when);
|
|
323
|
+
stored.updatedAt = at;
|
|
324
|
+
store.set(telemetryLossKey(partition), JSON.stringify(stored));
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// Telemetry losses are best effort: a count that cannot be stored is lost with the events it counted.
|
|
328
|
+
warn("could not persist a telemetry loss count", error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
274
331
|
function expire(partition, state, at) {
|
|
275
332
|
const cutoff = at - FLEET_LIMITS.phoneQueueMaxAgeMs;
|
|
276
333
|
const expired = [];
|
|
@@ -280,10 +337,18 @@ function buildQueue(store, options) {
|
|
|
280
337
|
// a young record can sit ahead of older ones; those wait until it ages out, which only delays their expiry.
|
|
281
338
|
for (; head < state.keys.length; head += 1) {
|
|
282
339
|
const entry = readEntry(state.keys[head]);
|
|
283
|
-
if (entry === null)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
340
|
+
if (entry === null) {
|
|
341
|
+
if (!isTelemetryKey(partition, state.keys[head]))
|
|
342
|
+
unreadable.push(undefined);
|
|
343
|
+
}
|
|
344
|
+
else if (entry.enqueuedAt < cutoff) {
|
|
345
|
+
// Expired telemetry is counted per package, never as a coverage gap.
|
|
346
|
+
if (entry.record.kind === "telemetry_event") {
|
|
347
|
+
noteTelemetryLoss(partition, entry.record.payload.packageName, "expired", 1, at, entry.record.observedAt);
|
|
348
|
+
}
|
|
349
|
+
else
|
|
350
|
+
expired.push(entry.record.observedAt);
|
|
351
|
+
}
|
|
287
352
|
else
|
|
288
353
|
break;
|
|
289
354
|
}
|
|
@@ -309,6 +374,11 @@ function buildQueue(store, options) {
|
|
|
309
374
|
if (losses === null || at - losses.updatedAt > FLEET_LIMITS.phoneQueueMaxAgeMs)
|
|
310
375
|
removeKey(lossKey(partition));
|
|
311
376
|
}
|
|
377
|
+
if (found.hasTelemetryLosses) {
|
|
378
|
+
const losses = readTelemetryLosses(partition);
|
|
379
|
+
if (losses === null || at - losses.updatedAt > FLEET_LIMITS.phoneQueueMaxAgeMs)
|
|
380
|
+
removeKey(telemetryLossKey(partition));
|
|
381
|
+
}
|
|
312
382
|
}
|
|
313
383
|
function loadSequences(partition) {
|
|
314
384
|
const cached = sequences.get(partition);
|
|
@@ -331,7 +401,11 @@ function buildQueue(store, options) {
|
|
|
331
401
|
}
|
|
332
402
|
catch (error) {
|
|
333
403
|
warn("could not store a record", error);
|
|
334
|
-
|
|
404
|
+
// Telemetry that cannot be stored is its miniapp's loss, never a coverage gap.
|
|
405
|
+
if (record?.kind === "telemetry_event")
|
|
406
|
+
noteTelemetryLoss(partition, record.payload.packageName, "evicted", 1, now(), record.observedAt);
|
|
407
|
+
else
|
|
408
|
+
noteLosses(partition, "dropped", [record?.observedAt]);
|
|
335
409
|
return;
|
|
336
410
|
}
|
|
337
411
|
trimToCapacity(partition, state);
|
|
@@ -410,12 +484,24 @@ function buildQueue(store, options) {
|
|
|
410
484
|
warn("could not sweep expired records", error);
|
|
411
485
|
}
|
|
412
486
|
},
|
|
487
|
+
noteTelemetryLoss(partition, packageName, kind, count, at) {
|
|
488
|
+
noteTelemetryLoss(partition, packageName, kind, count, at);
|
|
489
|
+
},
|
|
490
|
+
takeTelemetryLosses(partition, at) {
|
|
491
|
+
expire(partition, load(partition), at);
|
|
492
|
+
const losses = readTelemetryLosses(partition);
|
|
493
|
+
if (losses === null || Object.keys(losses.packages).length === 0)
|
|
494
|
+
return null;
|
|
495
|
+
removeKey(telemetryLossKey(partition));
|
|
496
|
+
return { packages: losses.packages, from: losses.from, to: losses.to };
|
|
497
|
+
},
|
|
413
498
|
clearPartition(partition) {
|
|
414
499
|
const state = load(partition);
|
|
415
500
|
for (const key of state.keys)
|
|
416
501
|
removeKey(key);
|
|
417
502
|
removeKey(indexKey(partition));
|
|
418
503
|
removeKey(lossKey(partition));
|
|
504
|
+
removeKey(telemetryLossKey(partition));
|
|
419
505
|
removeKey(sequenceKey(partition));
|
|
420
506
|
state.keys = [];
|
|
421
507
|
state.nextSeq = 0;
|