@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.
Files changed (67) hide show
  1. package/build/facades/glasses.d.ts.map +1 -1
  2. package/build/facades/glasses.js +5 -3
  3. package/build/facades/glasses.js.map +1 -1
  4. package/build/generated/releaseMetadata.js +5 -5
  5. package/build/generated/releaseMetadata.js.map +1 -1
  6. package/build/services/AppRegistry.d.ts +2 -0
  7. package/build/services/AppRegistry.d.ts.map +1 -1
  8. package/build/services/AppRegistry.js.map +1 -1
  9. package/build/services/LocalMiniappRuntime.d.ts +2 -0
  10. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  11. package/build/services/LocalMiniappRuntime.js +18 -1
  12. package/build/services/LocalMiniappRuntime.js.map +1 -1
  13. package/build/services/MentraJSCrashController.d.ts +2 -0
  14. package/build/services/MentraJSCrashController.d.ts.map +1 -1
  15. package/build/services/MentraJSCrashController.js +4 -0
  16. package/build/services/MentraJSCrashController.js.map +1 -1
  17. package/build/services/MentraJSRouter.d.ts.map +1 -1
  18. package/build/services/MentraJSRouter.js +14 -3
  19. package/build/services/MentraJSRouter.js.map +1 -1
  20. package/build/services/MiniappLauncher.d.ts +6 -0
  21. package/build/services/MiniappLauncher.d.ts.map +1 -1
  22. package/build/services/MiniappLauncher.js +20 -3
  23. package/build/services/MiniappLauncher.js.map +1 -1
  24. package/build/services/fleet/FleetReporter.d.ts.map +1 -1
  25. package/build/services/fleet/FleetReporter.js +43 -0
  26. package/build/services/fleet/FleetReporter.js.map +1 -1
  27. package/build/services/fleet/fleetCollector.d.ts +1 -1
  28. package/build/services/fleet/fleetCollector.d.ts.map +1 -1
  29. package/build/services/fleet/fleetCollector.js +146 -5
  30. package/build/services/fleet/fleetCollector.js.map +1 -1
  31. package/build/services/fleet/fleetEvents.d.ts +39 -3
  32. package/build/services/fleet/fleetEvents.d.ts.map +1 -1
  33. package/build/services/fleet/fleetEvents.js +66 -22
  34. package/build/services/fleet/fleetEvents.js.map +1 -1
  35. package/build/services/fleet/fleetQueue.d.ts +26 -0
  36. package/build/services/fleet/fleetQueue.d.ts.map +1 -1
  37. package/build/services/fleet/fleetQueue.js +94 -8
  38. package/build/services/fleet/fleetQueue.js.map +1 -1
  39. package/build/services/fleet/telemetryLimits.d.ts +16 -0
  40. package/build/services/fleet/telemetryLimits.d.ts.map +1 -0
  41. package/build/services/fleet/telemetryLimits.js +69 -0
  42. package/build/services/fleet/telemetryLimits.js.map +1 -0
  43. package/build/services/installMiniapp.d.ts +13 -0
  44. package/build/services/installMiniapp.d.ts.map +1 -1
  45. package/build/services/installMiniapp.js +64 -1
  46. package/build/services/installMiniapp.js.map +1 -1
  47. package/build/services/miniappReleaseInstall.js +1 -1
  48. package/build/services/miniappReleaseInstall.js.map +1 -1
  49. package/build/stores/apps.d.ts.map +1 -1
  50. package/build/stores/apps.js +16 -1
  51. package/build/stores/apps.js.map +1 -1
  52. package/package.json +8 -8
  53. package/src/facades/glasses.ts +5 -3
  54. package/src/generated/releaseMetadata.ts +5 -5
  55. package/src/services/AppRegistry.ts +2 -0
  56. package/src/services/LocalMiniappRuntime.ts +20 -1
  57. package/src/services/MentraJSCrashController.ts +5 -0
  58. package/src/services/MentraJSRouter.ts +15 -3
  59. package/src/services/MiniappLauncher.ts +18 -2
  60. package/src/services/fleet/FleetReporter.ts +40 -0
  61. package/src/services/fleet/fleetCollector.ts +160 -6
  62. package/src/services/fleet/fleetEvents.ts +104 -20
  63. package/src/services/fleet/fleetQueue.ts +100 -7
  64. package/src/services/fleet/telemetryLimits.ts +78 -0
  65. package/src/services/installMiniapp.ts +78 -2
  66. package/src/services/miniappReleaseInstall.ts +1 -1
  67. 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
- /** A start of `packageName` that reports its own launch is under way, until the returned function is called. */
54
- claimLaunch(packageName: string): () => void;
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;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAA;AAEvD,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,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;CACzC;AA8BD,eAAO,MAAM,WAAW;IACtB,gEAAgE;wBAC5C,kBAAkB,GAAG,MAAM,IAAI;IAOnD,mDAAmD;kBACrC,gBAAgB,GAAG,IAAI;IAIrC;;;OAGG;+BACwB,gBAAgB,GAAG,IAAI;IAKlD,gHAAgH;6BACvF,MAAM,GAAG,MAAM,IAAI;IAY5C,wFAAwF;+BAC7D,iBAAiB,EAAE;IAI9C,0FAA0F;kCAC5D,IAAI;IAIlC,uDAAuD;iBAC1C,eAAe,GAAG,IAAI;CAUpC,CAAA"}
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
- if ((claims.get(event.packageName) ?? 0) > 0)
45
+ const held = claims.get(event.packageName);
46
+ if (!held) {
47
+ deliverLaunch(event);
45
48
  return;
46
- deliverLaunch(event);
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
- /** A start of `packageName` that reports its own launch is under way, until the returned function is called. */
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
- claims.set(packageName, (claims.get(packageName) ?? 0) + 1);
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
- return;
55
- released = true;
56
- const left = (claims.get(packageName) ?? 1) - 1;
57
- if (left > 0)
58
- claims.set(packageName, left);
59
- else
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
- if (listeners.size === 0)
74
- return;
75
- for (const listener of [...listeners]) {
76
- try {
77
- listener.photo?.(event);
78
- }
79
- catch (error) {
80
- warnListenerFailed(error);
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAe,KAAK,WAAW,EAAC,MAAM,wBAAwB,CAAA;AAKrE,wGAAwG;AACxG,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC1C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,IAAI,MAAM,EAAE,CAAA;CACvB;AAED,mGAAmG;AACnG,MAAM,WAAW,WAAW;IAC1B,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAA;IACf,4FAA4F;IAC5F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,0FAA0F;IAC1F,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAClB;AAED,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,6IAA6I;IAC7I,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IACrD;;;;;;OAMG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE,CAAA;IACjF,+EAA+E;IAC/E,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACpD;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;IACvD;;;;;;;;;;OAUG;IACH,YAAY,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,wFAAwF;IACxF,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;IAC/B,0FAA0F;IAC1F,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAA;CACpE;AAED,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAWD,oGAAoG;AACpG,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAI1E;AAwbD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,CAAC,EAAE,kBAAkB,EAAE,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAGxG"}
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
- noteLosses(partition, "dropped", unreadable.map(() => undefined));
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
- unreadable.push(undefined);
285
- else if (entry.enqueuedAt < cutoff)
286
- expired.push(entry.record.observedAt);
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
- noteLosses(partition, "dropped", [record?.observedAt]);
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;