@xenon-device-management/xenon 2.14.0 → 2.16.0

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 (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -1,29 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Takes the README's dashboard screenshots: assets/dashboard-dark.png and
4
- * assets/dashboard-light.png.
3
+ * Takes the dashboard screenshots the README and the documentation site show.
4
+ *
5
+ * README: assets/dashboard-{dark,light}.png (the Devices page)
6
+ * Site: website/static/img/screens/{devices,device-control,session}-{dark,light}.webp
7
+ * (the Devices page, a phone's Logs tab in device control, and a
8
+ * finished session's page), 1600 px wide
5
9
  *
6
10
  * Serves the built dashboard (web/build) at /xenon/ and answers its API calls
7
- * with sample devices, so no server, database or phone is needed and the
8
- * pictures are the same on every machine.
11
+ * with sample data, so no server, database or phone is needed and the
12
+ * pictures are the same on every machine. The sample phone's screen is
13
+ * screenshot-fixtures/phone-screen.html: the live preview shows it, and a
14
+ * two-second recording made from it plays in the session's page. The sample
15
+ * session and log lines are in screenshot-fixtures/.
16
+ *
17
+ * The site's pictures are captured at 2x (2880 px wide) and shrunk to 1600 px
18
+ * WebP, which is what a 1200 px column needs and a sixth of the weight. The
19
+ * bundled ffmpeg has no WebP encoder, so Chromium does it: the capture is drawn
20
+ * to a canvas and read back as WebP (see toWebp).
9
21
  *
10
22
  * cd web && npm run build && cd ..
11
23
  * node scripts/dev/readme-screenshots.js
12
24
  *
13
- * Uses web/'s Playwright. Set CHROME_BIN to use a particular Chromium.
25
+ * Uses web/'s Playwright and the bundled ffmpeg (for the recording). Set
26
+ * CHROME_BIN to use a particular Chromium.
14
27
  */
15
28
  /* eslint-disable @typescript-eslint/no-var-requires -- a plain Node script, run without a build */
16
- /* global document, localStorage -- used inside the page, by Playwright */
29
+ /* global document, Image, localStorage -- used inside the page, by Playwright */
30
+ const { execFileSync } = require('child_process');
17
31
  const fs = require('fs');
18
32
  const http = require('http');
33
+ const os = require('os');
19
34
  const path = require('path');
20
35
 
21
36
  const ROOT = path.resolve(__dirname, '..', '..');
22
37
  const BUILD = path.join(ROOT, 'web', 'build');
23
38
  const ASSETS = path.join(ROOT, 'assets');
39
+ const SCREENS = path.join(ROOT, 'website', 'static', 'img', 'screens');
40
+ const FIXTURES = path.join(__dirname, 'screenshot-fixtures');
41
+ const SITE_IMAGE_WIDTH = 1600;
42
+ const SITE_IMAGE_QUALITY = 0.85;
24
43
  const PORT = 5179;
44
+ const BASE = `http://127.0.0.1:${PORT}`;
25
45
 
26
46
  const { chromium } = require(path.join(ROOT, 'web', 'node_modules', 'playwright'));
47
+ const { logcatRecords } = require('./screenshot-fixtures/logcat');
48
+ const sample = require('./screenshot-fixtures/session');
27
49
 
28
50
  if (!fs.existsSync(path.join(BUILD, 'index.html'))) {
29
51
  console.error('web/build is missing: run `npm run build` in web/ first.');
@@ -85,6 +107,9 @@ const DEVICES = [
85
107
  realDevice: true,
86
108
  batteryLevel: 92,
87
109
  tags: ['payments'],
110
+ // Device control draws the live preview at this size.
111
+ screenWidth: '1344',
112
+ screenHeight: '2992',
88
113
  },
89
114
  {
90
115
  ...base,
@@ -164,17 +189,101 @@ function answer(apiPath) {
164
189
  if (apiPath === '/devices' || apiPath.startsWith('/device')) return DEVICES;
165
190
  if (apiPath === '/queue/length') return 0;
166
191
  if (apiPath === '/queue/summary') return { total: 0, byPlatform: {}, otherCount: 0 };
167
- if (apiPath === '/queue' || apiPath === '/teams' || apiPath === '/build') return [];
192
+ if (apiPath === '/queue' || apiPath === '/teams') return [];
193
+ if (apiPath === '/build') return [sample.BUILD];
168
194
  if (apiPath.startsWith('/session')) return [];
169
195
  if (apiPath.startsWith('/healing/events')) return { events: [], todayCount: 0 };
170
196
  return {};
171
197
  }
172
198
 
173
- async function shoot(browser, theme, file) {
199
+ /** Calls a page made that nothing above answers: listed after each picture, to spot a missing sample. */
200
+ const unanswered = new Set();
201
+
202
+ /**
203
+ * Answers one /xenon/api call, or aborts it. `phone` holds the sample phone's
204
+ * screen as a JPEG and the session's recording as a WebM.
205
+ */
206
+ async function handleApi(route, phone) {
207
+ const request = route.request();
208
+ const apiPath = new URL(request.url()).pathname.replace('/xenon/api', '');
209
+ const method = request.method();
210
+
211
+ // Device control, for the sample Android phone. The live preview is a still
212
+ // JPEG, which an <img> shows as a stream that has just loaded.
213
+ const control = apiPath.match(/^\/control\/([^/]+)\/(.+)$/);
214
+ if (control && decodeURIComponent(control[1]) === sample.UDID) {
215
+ const action = control[2];
216
+ if (action === 'stream' && method === 'GET') {
217
+ return route.fulfill({ contentType: 'image/jpeg', body: phone.jpeg });
218
+ }
219
+ if (action === 'stream/start') return route.fulfill({ json: { success: true, type: 'mjpeg' } });
220
+ if (action === 'stream/ticket')
221
+ return route.fulfill({ json: { ticket: 'sample', expiresIn: 60 } });
222
+ if (action === 'stream/leave') return route.fulfill({ json: {} });
223
+ if (action === 'display') return route.fulfill({ json: { state: 'on' } });
224
+ if (action.startsWith('stream')) return route.abort();
225
+ } else if (apiPath.includes('/stream')) {
226
+ return route.abort();
227
+ }
228
+
229
+ // The sample session's page.
230
+ const session = apiPath.match(/^\/session\/([^/]+)(?:\/(.+))?$/);
231
+ if (session && session[1] === sample.SESSION_ID) {
232
+ const part = session[2] ?? '';
233
+ if (part === '') return route.fulfill({ json: sample.SESSION });
234
+ if (part === 'session_log') return route.fulfill({ json: sample.COMMANDS });
235
+ if (part === 'logs/device') return route.fulfill({ json: sample.DEVICE_LOGS });
236
+ if (part === 'logs/debug') return route.fulfill({ json: sample.DEBUG_LOGS });
237
+ if (part === 'profiling') return route.fulfill({ json: [] });
238
+ if (part === 'metrics') return route.fulfill({ json: sample.METRICS });
239
+ if (part === `asset/video/${sample.VIDEO_FILE}`) {
240
+ return route.fulfill({ contentType: 'video/webm', body: phone.webm });
241
+ }
242
+ }
243
+ // No network capture for it: the page says so.
244
+ if (apiPath === `/interceptor/sessions/${sample.SESSION_ID}/requests`) {
245
+ return route.fulfill({ status: 404, json: { error: 'No capture for this session' } });
246
+ }
247
+
248
+ const body = answer(apiPath);
249
+ if (JSON.stringify(body) === '{}') unanswered.add(`${method} ${apiPath}`);
250
+ return route.fulfill({ json: body });
251
+ }
252
+
253
+ /**
254
+ * The sample phone's screen, as the live preview's JPEG and as a 2-second
255
+ * WebM (VP9, which the bundled ffmpeg encodes and every Chromium plays).
256
+ */
257
+ async function preparePhone(browser, workDir) {
258
+ const page = await browser.newPage({ viewport: { width: 1344, height: 2992 } });
259
+ await page.setContent(fs.readFileSync(path.join(FIXTURES, 'phone-screen.html'), 'utf8'));
260
+ const jpeg = await page.screenshot({ type: 'jpeg', quality: 85 });
261
+ await page.close();
262
+
263
+ const still = path.join(workDir, 'phone.jpg');
264
+ const video = path.join(workDir, sample.VIDEO_FILE);
265
+ fs.writeFileSync(still, jpeg);
266
+ execFileSync(
267
+ require(path.join(ROOT, 'node_modules', '@ffmpeg-installer', 'ffmpeg')).path,
268
+ [
269
+ ...['-y', '-loglevel', 'error', '-loop', '1', '-framerate', '10', '-i', still, '-t', '2'],
270
+ ...['-vf', 'scale=448:996', '-c:v', 'libvpx-vp9', '-b:v', '0', '-crf', '40'],
271
+ ...['-pix_fmt', 'yuv420p', '-deadline', 'realtime', '-cpu-used', '8', video],
272
+ ],
273
+ { stdio: 'inherit' },
274
+ );
275
+ return { jpeg, webm: fs.readFileSync(video) };
276
+ }
277
+
278
+ /** A browser context in the theme, its page answering the dashboard's calls from the samples. */
279
+ async function openPage(browser, theme, viewport, phone) {
174
280
  const context = await browser.newContext({
175
- viewport: { width: 1440, height: 700 },
281
+ viewport,
176
282
  deviceScaleFactor: 2,
177
283
  colorScheme: theme,
284
+ // Log lines and sessions show local times: fix them.
285
+ timezoneId: 'UTC',
286
+ locale: 'en-US',
178
287
  });
179
288
  await context.addInitScript((t) => {
180
289
  try {
@@ -185,17 +294,15 @@ async function shoot(browser, theme, file) {
185
294
  }, theme);
186
295
  const page = await context.newPage();
187
296
  await page.route('**/socket.io/**', (route) => route.abort());
188
- await page.route('**/xenon/api/**', (route) => {
189
- const apiPath = new URL(route.request().url()).pathname.replace('/xenon/api', '');
190
- if (apiPath.includes('/stream')) return route.abort();
191
- return route.fulfill({ json: answer(apiPath) });
192
- });
297
+ await page.route('**/xenon/api/**', (route) => handleApi(route, phone));
298
+ return { context, page };
299
+ }
193
300
 
194
- await page.goto(`http://127.0.0.1:${PORT}/xenon/devices`, { waitUntil: 'load' });
195
- await page.waitForSelector('text=Pixel 8 Pro');
196
- await page.waitForTimeout(1000);
197
- // The live-updates badge says "Connecting" here only because the socket is
198
- // blocked; hide it rather than picture a connection problem.
301
+ /**
302
+ * The live-updates badge says "Connecting" here only because the socket is
303
+ * blocked; hide it rather than picture a connection problem.
304
+ */
305
+ async function hideConnectingBadge(page) {
199
306
  await page.evaluate(() => {
200
307
  for (const el of document.querySelectorAll('header *')) {
201
308
  if (el.children.length === 0 && /^Connecting/.test((el.textContent || '').trim())) {
@@ -210,20 +317,166 @@ async function shoot(browser, theme, file) {
210
317
  }
211
318
  }
212
319
  });
213
- await page.screenshot({ path: path.join(ASSETS, file) });
320
+ }
321
+
322
+ /** Throws if any of the texts is on the page: a picture of a spinner or an error is not a picture. */
323
+ async function assertAbsent(page, what, texts) {
324
+ for (const text of texts) {
325
+ if ((await page.getByText(text).count()) > 0) {
326
+ throw new Error(`${what}: the page still says "${text}"`);
327
+ }
328
+ }
329
+ }
330
+
331
+ /** Where a picture goes. */
332
+ function save(buffer, ...files) {
333
+ for (const file of files) {
334
+ fs.mkdirSync(path.dirname(file), { recursive: true });
335
+ fs.writeFileSync(file, buffer);
336
+ console.log(path.relative(ROOT, file));
337
+ }
338
+ }
339
+
340
+ /**
341
+ * A PNG as WebP, `width` px wide (its height in proportion). Chromium encodes
342
+ * it: the PNG is drawn to a canvas of that size and read back as WebP.
343
+ */
344
+ async function toWebp(browser, png, width = SITE_IMAGE_WIDTH, quality = SITE_IMAGE_QUALITY) {
345
+ const page = await browser.newPage();
346
+ try {
347
+ const dataUrl = await page.evaluate(
348
+ async ({ base64, size, q }) => {
349
+ const image = new Image();
350
+ image.src = `data:image/png;base64,${base64}`;
351
+ await image.decode();
352
+ const canvas = document.createElement('canvas');
353
+ canvas.width = size;
354
+ canvas.height = Math.round((image.naturalHeight * size) / image.naturalWidth);
355
+ const context = canvas.getContext('2d');
356
+ context.imageSmoothingQuality = 'high';
357
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
358
+ return canvas.toDataURL('image/webp', q);
359
+ },
360
+ { base64: png.toString('base64'), size: width, q: quality },
361
+ );
362
+ if (!dataUrl.startsWith('data:image/webp;base64,')) {
363
+ throw new Error('this Chromium cannot encode WebP');
364
+ }
365
+ return Buffer.from(dataUrl.slice(dataUrl.indexOf(',') + 1), 'base64');
366
+ } finally {
367
+ await page.close();
368
+ }
369
+ }
370
+
371
+ /** The site's copy of a capture: website/static/img/screens/<name>.webp. */
372
+ async function saveForSite(browser, png, name) {
373
+ save(await toWebp(browser, png), path.join(SCREENS, `${name}.webp`));
374
+ }
375
+
376
+ function reportUnanswered(what) {
377
+ if (unanswered.size > 0) {
378
+ console.log(` ${what}: no sample for ${Array.from(unanswered).join(', ')}`);
379
+ unanswered.clear();
380
+ }
381
+ }
382
+
383
+ /** The Devices page: the README's picture and the site's. */
384
+ async function shootDevices(browser, theme, phone) {
385
+ const { context, page } = await openPage(browser, theme, { width: 1440, height: 700 }, phone);
386
+ await page.goto(`${BASE}/xenon/devices`, { waitUntil: 'load' });
387
+ await page.waitForSelector('text=Pixel 8 Pro');
388
+ await page.waitForTimeout(1000);
389
+ await hideConnectingBadge(page);
390
+ const png = await page.screenshot();
391
+ save(png, path.join(ASSETS, `dashboard-${theme}.png`));
392
+ await saveForSite(browser, png, `devices-${theme}`);
393
+ reportUnanswered('devices');
394
+ await context.close();
395
+ }
396
+
397
+ /** Device control on the sample Android phone, the Logs tab streaming. */
398
+ async function shootDeviceControl(browser, theme, phone) {
399
+ const { context, page } = await openPage(browser, theme, { width: 1440, height: 900 }, phone);
400
+ // The logs arrive over a WebSocket: send them once it is open, as the
401
+ // phone's logcat does, since the tab clears its lines when the socket opens.
402
+ const live = Date.UTC(2026, 9, 4, 10, 42, 30);
403
+ await page.routeWebSocket(/\/xenon\/api\/control\/[^/]+\/logcat/, (ws) => {
404
+ setTimeout(() => {
405
+ for (const record of logcatRecords(live)) ws.send(JSON.stringify(record));
406
+ }, 300);
407
+ });
408
+
409
+ await page.goto(`${BASE}/xenon/devices/${sample.UDID}/control/logs`, { waitUntil: 'load' });
410
+ await page.waitForFunction(() => {
411
+ const img = document.querySelector('img[alt="Device Stream"]');
412
+ return !!img && img.complete && img.naturalWidth > 0;
413
+ });
414
+ await page.locator('.log-status', { hasText: 'Live' }).waitFor();
415
+ await page.waitForFunction(() => document.querySelectorAll('.log-row').length >= 30);
416
+ await page.waitForTimeout(800);
417
+ await hideConnectingBadge(page);
418
+ await assertAbsent(page, 'device control', [
419
+ 'Waiting for stream',
420
+ 'ESTABLISHING TRACE',
421
+ 'Stream unavailable',
422
+ 'Display is off',
423
+ ]);
424
+ await saveForSite(
425
+ browser,
426
+ await page.screenshot({ animations: 'disabled' }),
427
+ `device-control-${theme}`,
428
+ );
429
+ reportUnanswered('device control');
430
+ await context.close();
431
+ }
432
+
433
+ /** The page of the sample session: outcome, tiles, healing, CPU and memory, recording. */
434
+ async function shootSession(browser, theme, phone) {
435
+ const { context, page } = await openPage(browser, theme, { width: 1440, height: 900 }, phone);
436
+ await page.goto(`${BASE}/xenon/builds/${sample.BUILD_ID}/sessions/${sample.SESSION_ID}`, {
437
+ waitUntil: 'load',
438
+ });
439
+ await page.getByRole('heading', { name: sample.SESSION.name }).waitFor();
440
+ await page.waitForSelector('section[aria-label="CPU"] svg path');
441
+ await page.waitForFunction(() => {
442
+ const video = document.querySelector('video');
443
+ return !!video && video.readyState >= 2;
444
+ });
445
+ await page.waitForTimeout(800);
446
+ await hideConnectingBadge(page);
447
+ if (!page.url().includes(`/sessions/${sample.SESSION_ID}`)) {
448
+ throw new Error(`session: the page left the session (${page.url()})`);
449
+ }
450
+ await assertAbsent(page, 'session', [
451
+ 'Loading session',
452
+ 'Loading performance',
453
+ 'Session not found',
454
+ 'No video available',
455
+ ]);
456
+ await saveForSite(
457
+ browser,
458
+ await page.screenshot({ animations: 'disabled' }),
459
+ `session-${theme}`,
460
+ );
461
+ reportUnanswered('session');
214
462
  await context.close();
215
- console.log(`assets/${file}`);
216
463
  }
217
464
 
218
465
  (async () => {
466
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-screenshots-'));
219
467
  await new Promise((resolve) => server.listen(PORT, '127.0.0.1', resolve));
220
468
  const browser = await chromium.launch({ executablePath: process.env.CHROME_BIN || undefined });
221
469
  try {
222
- await shoot(browser, 'dark', 'dashboard-dark.png');
223
- await shoot(browser, 'light', 'dashboard-light.png');
470
+ const phone = await preparePhone(browser, workDir);
471
+ for (const theme of ['dark', 'light']) {
472
+ await shootDevices(browser, theme, phone);
473
+ await shootDeviceControl(browser, theme, phone);
474
+ await shootSession(browser, theme, phone);
475
+ }
224
476
  } finally {
225
477
  await browser.close();
226
478
  server.close();
479
+ fs.rmSync(workDir, { recursive: true, force: true });
227
480
  }
228
481
  })().catch((err) => {
229
482
  console.error(err);
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Sample log lines for the device control screenshot's Logs tab: two minutes
3
+ * of a made-up shop app's checkout on an Android phone. Every record has the
4
+ * shape the logcat WebSocket sends, `{ ts, pid, tid, level, tag, message, pkg }`.
5
+ *
6
+ * About forty lines, mostly info and debug, with the payment's two failed
7
+ * attempts (two errors) and what the app did about them (three warnings).
8
+ */
9
+
10
+ const APP = 'com.example.shop';
11
+ const SYSTEM = 'system_server';
12
+ const APP_PID = 14233;
13
+ const SYSTEM_PID = 1782;
14
+
15
+ /** [seconds after the first line, level, tag, message] per line, oldest first. */
16
+ const LINES = [
17
+ [
18
+ 0.0,
19
+ 'I',
20
+ 'ActivityTaskManager',
21
+ 'START u0 {cmp=com.example.shop/.cart.CartActivity} from uid 10312',
22
+ ],
23
+ [0.4, 'D', 'ShopCheckout', 'CartViewModel: 3 items, subtotal=145.50 USD'],
24
+ [1.9, 'D', 'OkHttp', '--> GET https://api.example.com/v2/cart/c_91f2/quote'],
25
+ [
26
+ 2.2,
27
+ 'D',
28
+ 'OkHttp',
29
+ '<-- 200 https://api.example.com/v2/cart/c_91f2/quote (287ms, 1421-byte body)',
30
+ ],
31
+ [
32
+ 4.8,
33
+ 'I',
34
+ 'ActivityTaskManager',
35
+ 'START u0 {cmp=com.example.shop/.checkout.CheckoutActivity} from uid 10312',
36
+ ],
37
+ [5.1, 'D', 'ShopCheckout', 'CheckoutViewModel init cartId=c_91f2 items=3'],
38
+ [5.3, 'D', 'ShopCheckout', 'loadSavedCards() -> 2 cards'],
39
+ [
40
+ 5.7,
41
+ 'I',
42
+ 'ActivityTaskManager',
43
+ 'Displayed com.example.shop/.checkout.CheckoutActivity for user 0: +412ms',
44
+ ],
45
+ [
46
+ 6.0,
47
+ 'D',
48
+ 'chromium',
49
+ '[INFO:CONSOLE(1)] "Payment sheet ready", source: https://pay.example.com/sheet.js (1)',
50
+ ],
51
+ [9.6, 'D', 'ShopCheckout', 'promo SPRING10 validated: discount=-14.55'],
52
+ [10.1, 'D', 'OkHttp', '--> POST https://api.example.com/v2/promo/validate (38-byte body)'],
53
+ [10.4, 'D', 'OkHttp', '<-- 200 https://api.example.com/v2/promo/validate (301ms, 96-byte body)'],
54
+ [18.5, 'I', 'ShopCheckout', 'payment method selected: card_visa_4242'],
55
+ [
56
+ 24.2,
57
+ 'D',
58
+ 'chromium',
59
+ '[INFO:CONSOLE(1)] "3DS challenge not required", source: https://pay.example.com/sheet.js (1)',
60
+ ],
61
+ [41.0, 'I', 'ShopCheckout', 'Pay now tapped total=130.95 USD'],
62
+ [41.1, 'D', 'OkHttp', '--> POST https://api.example.com/v2/payments (412-byte body)'],
63
+ [43.2, 'E', 'ShopCheckout', 'payment attempt 1 of 3 failed: SocketTimeoutException: timeout'],
64
+ [43.3, 'W', 'ShopCheckout', 'retrying payment in 1500ms idempotencyKey=pk_7c1e'],
65
+ [44.8, 'D', 'OkHttp', '--> POST https://api.example.com/v2/payments (412-byte body)'],
66
+ [
67
+ 46.3,
68
+ 'E',
69
+ 'ShopCheckout',
70
+ 'payment attempt 2 of 3 failed: HTTP 503 (payments-gateway unavailable)',
71
+ ],
72
+ [46.4, 'W', 'ShopCheckout', 'retrying payment in 3000ms idempotencyKey=pk_7c1e'],
73
+ [49.5, 'D', 'OkHttp', '--> POST https://api.example.com/v2/payments (412-byte body)'],
74
+ [50.3, 'D', 'OkHttp', '<-- 201 https://api.example.com/v2/payments (801ms, 214-byte body)'],
75
+ [50.4, 'I', 'ShopCheckout', 'payment captured paymentId=pay_3f9a2c attempt=3'],
76
+ [
77
+ 50.6,
78
+ 'W',
79
+ 'chromium',
80
+ '[WARNING:CONSOLE(1)] "Slow payment: 9.3s from tap to capture", source: https://pay.example.com/sheet.js (1)',
81
+ ],
82
+ [
83
+ 50.9,
84
+ 'I',
85
+ 'ActivityTaskManager',
86
+ 'START u0 {cmp=com.example.shop/.order.OrderConfirmationActivity} from uid 10312',
87
+ ],
88
+ [51.1, 'D', 'ShopCheckout', 'OrderConfirmationViewModel orderId=o_55820'],
89
+ [
90
+ 51.3,
91
+ 'I',
92
+ 'ActivityTaskManager',
93
+ 'Displayed com.example.shop/.order.OrderConfirmationActivity for user 0: +188ms',
94
+ ],
95
+ [51.8, 'D', 'OkHttp', '--> POST https://analytics.example.com/v1/events (233-byte body)'],
96
+ [52.1, 'D', 'OkHttp', '<-- 202 https://analytics.example.com/v1/events (264ms, 0-byte body)'],
97
+ [
98
+ 60.4,
99
+ 'D',
100
+ 'chromium',
101
+ '[INFO:CONSOLE(1)] "Receipt rendered", source: https://pay.example.com/receipt.js (1)',
102
+ ],
103
+ [63.0, 'I', 'ShopCheckout', 'receipt email queued orderId=o_55820'],
104
+ [63.2, 'D', 'OkHttp', '--> POST https://api.example.com/v2/orders/o_55820/receipt (0-byte body)'],
105
+ [
106
+ 63.6,
107
+ 'D',
108
+ 'OkHttp',
109
+ '<-- 202 https://api.example.com/v2/orders/o_55820/receipt (402ms, 0-byte body)',
110
+ ],
111
+ [78.9, 'D', 'ShopCheckout', 'CartRepository: cart c_91f2 cleared'],
112
+ [
113
+ 91.5,
114
+ 'I',
115
+ 'ActivityTaskManager',
116
+ 'Displayed com.example.shop/.home.HomeActivity for user 0: +96ms',
117
+ ],
118
+ [92.2, 'D', 'OkHttp', '--> GET https://api.example.com/v2/recommendations?limit=12'],
119
+ [
120
+ 92.7,
121
+ 'D',
122
+ 'OkHttp',
123
+ '<-- 200 https://api.example.com/v2/recommendations?limit=12 (512ms, 9.8-kB body)',
124
+ ],
125
+ [
126
+ 104.3,
127
+ 'D',
128
+ 'chromium',
129
+ '[INFO:CONSOLE(1)] "Session heartbeat", source: https://pay.example.com/sheet.js (1)',
130
+ ],
131
+ [116.8, 'D', 'ShopCheckout', 'HomeViewModel: refreshed 12 recommendations'],
132
+ ];
133
+
134
+ /**
135
+ * The records, the last one `endTs` (epoch ms) and the first about two
136
+ * minutes before it. A line's thread id is its pid plus a fixed offset, so
137
+ * the same input gives the same records every run.
138
+ */
139
+ function logcatRecords(endTs) {
140
+ const last = LINES[LINES.length - 1][0];
141
+ const start = endTs - Math.round((last + 1.2) * 1000);
142
+ return LINES.map(([offset, level, tag, message], i) => {
143
+ const fromSystem = tag === 'ActivityTaskManager';
144
+ const pid = fromSystem ? SYSTEM_PID : APP_PID;
145
+ return {
146
+ ts: start + Math.round(offset * 1000),
147
+ pid,
148
+ tid: fromSystem ? pid + 449 : pid + (i % 3 === 0 ? 0 : 27),
149
+ level,
150
+ tag,
151
+ message,
152
+ pkg: fromSystem ? SYSTEM : APP,
153
+ };
154
+ });
155
+ }
156
+
157
+ module.exports = { logcatRecords, LINES };