@mentra/cloud-protocol 3.4.0-dev.1208 → 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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/fleet.ts +198 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/cloud-protocol",
3
- "version": "3.4.0-dev.1208",
3
+ "version": "3.4.0-dev.1209",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
package/src/fleet.ts CHANGED
@@ -23,8 +23,26 @@ export const FLEET_LIMITS = {
23
23
  phoneQueueMaxAgeMs: 7 * 24 * 60 * 60 * 1000,
24
24
  replayWindowMs: 14 * 24 * 60 * 60 * 1000,
25
25
  maxFutureSkewMs: 5 * 60 * 1000,
26
+ maxTelemetryLossPackages: 500,
27
+ maxCrashesInWindow: 1_000,
28
+ maxReconnectMs: 7 * 24 * 60 * 60 * 1000,
26
29
  } as const
27
30
 
31
+ /**
32
+ * Phase 12: a miniapp's own events. The Mentra App applies these before it queues an event (dropping the event, a
33
+ * property, or cutting a string) and MFM checks them again, rejecting a record that still breaks one.
34
+ */
35
+ export const FLEET_TELEMETRY_LIMITS = {
36
+ maxProperties: 25,
37
+ maxStringValueLength: 64,
38
+ eventsPerMinute: 60,
39
+ eventsPerDay: 2_000,
40
+ /** Places of the phone's queue telemetry may hold; when full, the oldest telemetry record gives way. */
41
+ queueShare: 2_000,
42
+ } as const
43
+ /** An event or property name: 1-64 lowercase letters, digits and underscores, starting with a letter. */
44
+ export const FLEET_TELEMETRY_NAME = /^[a-z][a-z0-9_]{0,63}$/
45
+
28
46
  const FleetId = z
29
47
  .string()
30
48
  .min(1)
@@ -63,6 +81,10 @@ export const FLEET_CAPABILITIES = [
63
81
  "engine_active",
64
82
  "photo_operations",
65
83
  "phone_battery",
84
+ // Phase 12. A record kind the phone can produce is declared, so a view tells a measured 0 from an unknown.
85
+ "miniapp_failures",
86
+ "glasses_connectivity",
87
+ "miniapp_telemetry",
66
88
  ] as const
67
89
 
68
90
  const Battery = z.discriminatedUnion("state", [
@@ -115,6 +137,18 @@ export const FleetSnapshotPayloadSchema = z
115
137
  })
116
138
  .strict()
117
139
 
140
+ /** Phase 12: why a launch failed. Codes only, never error text. */
141
+ export const FLEET_LAUNCH_FAILURE_REASONS = [
142
+ "bundle_missing",
143
+ "incompatible_host",
144
+ "policy_blocked",
145
+ "spawn_failed",
146
+ "connect_timeout",
147
+ "permission_denied",
148
+ "unknown",
149
+ ] as const
150
+ export type FleetLaunchFailureReason = (typeof FLEET_LAUNCH_FAILURE_REASONS)[number]
151
+
118
152
  export const FleetLaunchPayloadSchema = z
119
153
  .object({
120
154
  packageName: FleetText,
@@ -122,8 +156,14 @@ export const FleetLaunchPayloadSchema = z
122
156
  versionName: FleetText.optional(),
123
157
  start: z.enum(["user", "automatic"]),
124
158
  outcome: z.enum(["started", "failed"]),
159
+ /** Phase 12. Only a failed launch carries it; a Mentra App from before Phase 12 sends none. */
160
+ failureReason: z.enum(FLEET_LAUNCH_FAILURE_REASONS).optional(),
125
161
  })
126
162
  .strict()
163
+ .refine((v) => v.failureReason === undefined || v.outcome === "failed", {
164
+ path: ["failureReason"],
165
+ message: "only a failed launch carries a failureReason",
166
+ })
127
167
 
128
168
  export const FLEET_ACTIVITIES = ["glasses_connection", "miniapp_execution", "app_foreground", "engine_active"] as const
129
169
  export const FleetActivityCheckpointSchema = z
@@ -258,6 +298,146 @@ export const FleetCoverageGapPayloadSchema = z
258
298
  })
259
299
  .strict()
260
300
 
301
+ const TelemetryName = z.string().regex(FLEET_TELEMETRY_NAME)
302
+ const TelemetryValue = z.union([
303
+ z.string().max(FLEET_TELEMETRY_LIMITS.maxStringValueLength),
304
+ z.number().finite(),
305
+ z.boolean(),
306
+ ])
307
+
308
+ /** Phase 12: one event a miniapp captured, attributed by the Mentra App to the miniapp whose bridge sent it. */
309
+ export const FleetTelemetryEventPayloadSchema = z
310
+ .object({
311
+ packageName: FleetText,
312
+ versionName: FleetText.optional(),
313
+ /** The miniapp's execution session at capture (its `miniapp_execution` activitySessionId), when one was open. */
314
+ executionSessionId: FleetId.optional(),
315
+ event: TelemetryName,
316
+ properties: z
317
+ .record(TelemetryName, TelemetryValue)
318
+ .refine((p) => Object.keys(p).length <= FLEET_TELEMETRY_LIMITS.maxProperties, "too many properties"),
319
+ /** Properties the Mentra App dropped from this event (bad name or value, or past the 25th). */
320
+ droppedProperties: z.number().int().min(0).max(1_000_000),
321
+ /** String values the Mentra App cut to 64 characters. */
322
+ cutValues: z.number().int().min(0).max(FLEET_TELEMETRY_LIMITS.maxProperties),
323
+ })
324
+ .strict()
325
+
326
+ /** What the Mentra App lost of one miniapp's telemetry. */
327
+ export const FleetTelemetryLossSchema = z
328
+ .object({
329
+ packageName: FleetText,
330
+ /** Over the name, rate or daily limit. */
331
+ overLimit: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
332
+ /** Given way in the queue to newer telemetry. */
333
+ evicted: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
334
+ /** Too old in the queue to send. */
335
+ expired: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
336
+ /** Sent, and rejected by MFM. */
337
+ rejected: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
338
+ })
339
+ .strict()
340
+ .refine((v) => v.overLimit + v.evicted + v.expired + v.rejected > 0, "a loss counts something")
341
+
342
+ /** Phase 12: telemetry the Mentra App lost over a period, per miniapp. Never a coverage gap. */
343
+ export const FleetTelemetryLossPayloadSchema = z
344
+ .object({
345
+ from: FleetUtcTime,
346
+ to: FleetUtcTime,
347
+ losses: z.array(FleetTelemetryLossSchema).min(1).max(FLEET_LIMITS.maxTelemetryLossPackages),
348
+ })
349
+ .strict()
350
+ .refine((v) => Date.parse(v.from) <= Date.parse(v.to), {path: ["to"], message: "to is not before from"})
351
+
352
+ export const FLEET_CRASH_SURFACES = ["context", "webview"] as const
353
+ export const FLEET_CRASH_CATEGORIES = [
354
+ "uncaught_exception",
355
+ "bridge_error",
356
+ "unresponsive",
357
+ "watchdog_kill",
358
+ "content_process_terminated",
359
+ "unknown",
360
+ ] as const
361
+ export type FleetCrashCategory = (typeof FLEET_CRASH_CATEGORIES)[number]
362
+ /**
363
+ * What followed a crash: the engine restarts the miniapp, or stops restarting it after repeated crashes, or (a screen
364
+ * whose content process died) the screen closes and nothing restarts it until the person opens it again.
365
+ */
366
+ export const FLEET_CRASH_OUTCOMES = ["respawn_scheduled", "crash_loop_stopped", "closed"] as const
367
+
368
+ /** Phase 12: one miniapp crash. The engine maps its reason to a category; the reason text never leaves the phone. */
369
+ export const FleetMiniappCrashPayloadSchema = z
370
+ .object({
371
+ packageName: FleetText,
372
+ versionName: FleetText.optional(),
373
+ executionSessionId: FleetId.optional(),
374
+ surface: z.enum(FLEET_CRASH_SURFACES),
375
+ category: z.enum(FLEET_CRASH_CATEGORIES),
376
+ outcome: z.enum(FLEET_CRASH_OUTCOMES),
377
+ /** Crashes of this miniapp in the crash window, this one included. */
378
+ crashesInWindow: z.number().int().min(1).max(FLEET_LIMITS.maxCrashesInWindow),
379
+ })
380
+ .strict()
381
+
382
+ export const FLEET_INSTALL_OPERATIONS = ["install", "update", "remove"] as const
383
+ /** What started an install: `url` covers a link, the QR scanner, the paste box, the system list and the update check. */
384
+ export const FLEET_INSTALL_SOURCES = ["bundled", "url", "manifest", "local", "assignment"] as const
385
+ export const FLEET_INSTALL_OUTCOMES = ["succeeded", "failed", "deferred"] as const
386
+ /** `assignment_outcome`'s reasons where they apply, and three of its own. */
387
+ export const FLEET_INSTALL_FAILURE_REASONS = [...FLEET_ASSIGNMENT_REASONS, "network", "user_cancelled", "unknown"] as const
388
+ export type FleetInstallFailureReason = (typeof FLEET_INSTALL_FAILURE_REASONS)[number]
389
+
390
+ /** Phase 12: one install, update or removal attempt, managed or not. */
391
+ export const FleetMiniappInstallPayloadSchema = z
392
+ .object({
393
+ packageName: FleetText,
394
+ operation: z.enum(FLEET_INSTALL_OPERATIONS),
395
+ source: z.enum(FLEET_INSTALL_SOURCES),
396
+ fromVersion: FleetText.optional(),
397
+ toVersion: FleetText.optional(),
398
+ outcome: z.enum(FLEET_INSTALL_OUTCOMES),
399
+ reason: z.enum(FLEET_INSTALL_FAILURE_REASONS).optional(),
400
+ })
401
+ .strict()
402
+ .refine((v) => (v.outcome === "succeeded") === (v.reason === undefined), {
403
+ path: ["reason"],
404
+ message: "a failed or deferred attempt carries its reason, and only it",
405
+ })
406
+
407
+ export const FLEET_CONNECTION_EVENTS = ["pair_failed", "connect_failed", "disconnected", "reconnected"] as const
408
+ export type FleetConnectionEventKind = (typeof FLEET_CONNECTION_EVENTS)[number]
409
+ export const FLEET_CONNECTION_CATEGORIES = [
410
+ "bluetooth_off",
411
+ "permission_denied",
412
+ "bond_failed",
413
+ "not_found",
414
+ "timeout",
415
+ "firmware_incompatible",
416
+ "link_lost",
417
+ "user_action",
418
+ "unknown",
419
+ ] as const
420
+ export type FleetConnectionCategory = (typeof FLEET_CONNECTION_CATEGORIES)[number]
421
+
422
+ /**
423
+ * Phase 12: a pairing or connection change. A disconnect the person asked for is `user_action`, and is not shown as a
424
+ * problem. The glasses are the record's `deviceBinding` when the phone knows them.
425
+ */
426
+ export const FleetConnectionEventPayloadSchema = z
427
+ .object({
428
+ event: z.enum(FLEET_CONNECTION_EVENTS),
429
+ category: z.enum(FLEET_CONNECTION_CATEGORIES),
430
+ glassesModel: FleetText.optional(),
431
+ appVersion: FleetText,
432
+ /** How long the reconnect took, for `reconnected` only. */
433
+ reconnectMs: z.number().int().min(0).max(FLEET_LIMITS.maxReconnectMs).optional(),
434
+ })
435
+ .strict()
436
+ .refine((v) => (v.event === "reconnected") === (v.reconnectMs !== undefined), {
437
+ path: ["reconnectMs"],
438
+ message: "a reconnect carries the time it took, and only it",
439
+ })
440
+
261
441
  const base = {
262
442
  schemaVersion: z.literal(FLEET_SCHEMA_VERSION),
263
443
  recordId: FleetId,
@@ -290,6 +470,18 @@ export const FleetRecordSchema = z.discriminatedUnion("kind", [
290
470
  z.object({...base, kind: z.literal("coverage_gap"), payload: FleetCoverageGapPayloadSchema}).strict(),
291
471
  z.object({...base, kind: z.literal("miniapp_inventory"), payload: FleetMiniappInventoryPayloadSchema}).strict(),
292
472
  z.object({...base, kind: z.literal("assignment_outcome"), payload: FleetAssignmentOutcomePayloadSchema}).strict(),
473
+ z.object({...base, kind: z.literal("telemetry_event"), payload: FleetTelemetryEventPayloadSchema}).strict(),
474
+ z.object({...base, kind: z.literal("telemetry_loss"), payload: FleetTelemetryLossPayloadSchema}).strict(),
475
+ z.object({...base, kind: z.literal("miniapp_crash"), payload: FleetMiniappCrashPayloadSchema}).strict(),
476
+ z.object({...base, kind: z.literal("miniapp_install"), payload: FleetMiniappInstallPayloadSchema}).strict(),
477
+ z
478
+ .object({
479
+ ...base,
480
+ kind: z.literal("connection_event"),
481
+ deviceBinding: FleetDeviceBindingSchema.optional(),
482
+ payload: FleetConnectionEventPayloadSchema,
483
+ })
484
+ .strict(),
293
485
  ]).superRefine((value, ctx) => {
294
486
  // A snapshot's glasses belong to the pair its binding names, so the two come together.
295
487
  if (value.kind === "snapshot" && (value.payload.glasses !== undefined) !== (value.deviceBinding !== undefined))
@@ -386,6 +578,12 @@ export type FleetDesiredStateEntry = z.infer<typeof FleetDesiredStateEntrySchema
386
578
  export type FleetDesiredState = z.infer<typeof FleetDesiredStateSchema>
387
579
  export type FleetMiniappInventoryPayload = z.infer<typeof FleetMiniappInventoryPayloadSchema>
388
580
  export type FleetAssignmentOutcomePayload = z.infer<typeof FleetAssignmentOutcomePayloadSchema>
581
+ export type FleetLaunchPayload = z.infer<typeof FleetLaunchPayloadSchema>
582
+ export type FleetTelemetryEventPayload = z.infer<typeof FleetTelemetryEventPayloadSchema>
583
+ export type FleetTelemetryLossPayload = z.infer<typeof FleetTelemetryLossPayloadSchema>
584
+ export type FleetMiniappCrashPayload = z.infer<typeof FleetMiniappCrashPayloadSchema>
585
+ export type FleetMiniappInstallPayload = z.infer<typeof FleetMiniappInstallPayloadSchema>
586
+ export type FleetConnectionEventPayload = z.infer<typeof FleetConnectionEventPayloadSchema>
389
587
 
390
588
  /** Whole-response error codes Core and MFM answer with. */
391
589
  export const FLEET_NOT_INSTALLED = "fleet_not_installed"