@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
@@ -20,6 +20,7 @@ const ws_1 = require("ws");
20
20
  const logger_1 = __importDefault(require("../../logger"));
21
21
  const hubSessionToken_1 = require("../../gateway/hubSessionToken");
22
22
  const forwardToNode_1 = require("../../gateway/forwardToNode");
23
+ const outboundProxy_1 = require("../../helpers/outboundProxy");
23
24
  const controlDevice_1 = require("../../middleware/controlDevice");
24
25
  const nodePhoneControl_1 = require("../routers/nodePhoneControl");
25
26
  /**
@@ -85,21 +86,86 @@ function openNodeSocket(request_1) {
85
86
  const query = new URLSearchParams(request.query);
86
87
  query.set('ticket', ticket);
87
88
  const url = `${origin.replace(/^http/, 'ws')}${base}/${path}?${query}`;
88
- const socket = new ws_1.WebSocket(url, { handshakeTimeout: timeoutMs, perMessageDeflate: false });
89
- yield new Promise((resolve, reject) => {
90
- socket.once('open', () => {
91
- // Paused until something reads it: the node sends H.264's config
92
- // packet and logcat's replay as soon as it opens, and a message with
93
- // no listener yet is lost. relaySocket resumes it.
94
- socket.pause();
95
- resolve();
96
- });
97
- socket.once('error', reject);
98
- });
89
+ const socket = yield connectToNode(url, origin, timeoutMs);
99
90
  logger_1.default.info(`[${udid}] ${path} socket opened on node ${origin}`);
100
91
  return socket;
101
92
  });
102
93
  }
94
+ /** An HTTP answer where the socket's upgrade was expected. */
95
+ class AnsweredWithoutUpgrade extends Error {
96
+ }
97
+ /** The nodes whose proxy refused a tunnel, said once each. */
98
+ const refusedTunnels = new Set();
99
+ /**
100
+ * The node's socket, open and paused, through the proxy its ticket took (the
101
+ * environment's, helpers/outboundProxy.ts): through 2.15 it went straight to
102
+ * the node whatever the environment said. A proxy that answers the tunnel
103
+ * with anything but the upgrade, as a stock Squid answers a CONNECT to any
104
+ * port but 443, is tried around: the socket goes straight to the node, as it
105
+ * always did. The node never saw the ticket, so it is still good.
106
+ */
107
+ function connectToNode(url, origin, timeoutMs) {
108
+ return __awaiter(this, void 0, void 0, function* () {
109
+ const agent = (0, outboundProxy_1.socketProxyAgentFor)(url);
110
+ if (!agent)
111
+ return openSocket(url, timeoutMs);
112
+ try {
113
+ return yield openSocket(url, timeoutMs, agent);
114
+ }
115
+ catch (err) {
116
+ if (!(err instanceof AnsweredWithoutUpgrade))
117
+ throw err;
118
+ if (!refusedTunnels.has(origin)) {
119
+ refusedTunnels.add(origin);
120
+ logger_1.default.warn(`The proxy refused to carry the live-preview and log sockets to node ${origin} ` +
121
+ `(${err.message}); they go straight to the node. List it in NO_PROXY to skip the proxy.`);
122
+ }
123
+ return openSocket(url, timeoutMs);
124
+ }
125
+ });
126
+ }
127
+ function openSocket(url, timeoutMs, agent) {
128
+ const socket = new ws_1.WebSocket(url, {
129
+ handshakeTimeout: timeoutMs,
130
+ perMessageDeflate: false,
131
+ agent,
132
+ });
133
+ return new Promise((resolve, reject) => {
134
+ let settled = false;
135
+ const fail = (err) => {
136
+ if (settled)
137
+ return;
138
+ settled = true;
139
+ clearTimeout(timer);
140
+ reject(err);
141
+ };
142
+ // handshakeTimeout starts once the request has a socket, which a proxy
143
+ // agent hands it only after the proxy answers: a proxy that never
144
+ // answered held this for ever.
145
+ const timer = setTimeout(() => {
146
+ fail(new Error(`the node's socket did not open in ${timeoutMs} ms`));
147
+ socket.terminate();
148
+ }, timeoutMs);
149
+ socket.once('open', () => {
150
+ if (settled)
151
+ return;
152
+ settled = true;
153
+ clearTimeout(timer);
154
+ // Paused until something reads it: the node sends H.264's config
155
+ // packet and logcat's replay as soon as it opens, and a message with
156
+ // no listener yet is lost. relaySocket resumes it.
157
+ socket.pause();
158
+ resolve(socket);
159
+ });
160
+ socket.once('unexpected-response', (req, res) => {
161
+ fail(new AnsweredWithoutUpgrade(`HTTP ${res.statusCode}`));
162
+ req.destroy();
163
+ });
164
+ // Kept for good: whatever errors aborting the upgrade raises has a
165
+ // listener, as it had before.
166
+ socket.on('error', fail);
167
+ });
168
+ }
103
169
  /** A close code a server may send. 1005, 1006 and 1015 only describe a close. */
104
170
  function isSendable(code) {
105
171
  return ((code >= 1000 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ||
package/lib/src/config.js CHANGED
@@ -87,6 +87,7 @@ exports.config = {
87
87
  userSessionTtlMs: Number(process.env.XENON_USER_SESSION_TTL_MS) || 24 * 60 * 60 * 1000,
88
88
  smtpUrl: process.env.XENON_SMTP_URL,
89
89
  smtpFrom: process.env.XENON_SMTP_FROM,
90
+ publicUrl: process.env.XENON_PUBLIC_URL,
90
91
  resetTokenTtlMs: Number(process.env.XENON_RESET_TOKEN_TTL_MS) || 60 * 60 * 1000,
91
92
  // Opt-in since 1.20.7: it writes the raw reset link (a credential) to the
92
93
  // server log. Without SMTP, admins issue links from the Users page instead.
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NETWORK_CAPTURE_NOT_KEPT = void 0;
4
+ exports.commandLogFields = commandLogFields;
5
+ const xenonScripts_1 = require("../interceptors/xenonScripts");
6
+ /** What the command log keeps in place of a network-capture script's arguments or answer. */
7
+ exports.NETWORK_CAPTURE_NOT_KEPT = 'Not kept: network capture is shown to admins only';
8
+ /**
9
+ * The `body` and `response` the command log (SessionLog) keeps for a command.
10
+ * The log is read by everyone who can see the session (`session_log`, bug
11
+ * reports), so a network-capture script (NETWORK_CAPTURE_SCRIPTS) keeps
12
+ * neither its arguments nor its answer: they are the capture itself (a HAR,
13
+ * the requests' headers and bodies, the mocks), which REST shows to admins
14
+ * only.
15
+ * The script's name stays, and a failed call keeps its error, which says why
16
+ * and carries no capture.
17
+ *
18
+ * CommandInterceptor answers these scripts before its post-command hooks, so
19
+ * a session on this server's own phone records one only when it fails. A hub
20
+ * with its dashboard on records every command it forwards to a node, and
21
+ * through 2.15 kept a test's `xenon: exportHar` whole. Failure analysis sends
22
+ * a session's last commands' answers to the AI provider, so they went there
23
+ * too.
24
+ *
25
+ * `requestBody` is the client's W3C body (`{ script, args }`) or, from
26
+ * CommandInterceptor, the command's arguments (`[script, args]`).
27
+ */
28
+ function commandLogFields(commandName, requestBody, responseBody, isSuccess) {
29
+ const body = JSON.stringify(requestBody);
30
+ if (commandName !== 'execute')
31
+ return { body, response: responseBody };
32
+ const fromCommand = Array.isArray(requestBody);
33
+ const script = fromCommand
34
+ ? requestBody[0]
35
+ : requestBody === null || requestBody === void 0 ? void 0 : requestBody.script;
36
+ if (!(0, xenonScripts_1.isNetworkCaptureScript)(script))
37
+ return { body, response: responseBody };
38
+ return {
39
+ body: JSON.stringify(fromCommand ? [script, exports.NETWORK_CAPTURE_NOT_KEPT] : { script, args: exports.NETWORK_CAPTURE_NOT_KEPT }),
40
+ response: isSuccess ? JSON.stringify({ value: exports.NETWORK_CAPTURE_NOT_KEPT }) : responseBody,
41
+ };
42
+ }
@@ -59,10 +59,10 @@ const xenonScripts_1 = require("../interceptors/xenonScripts");
59
59
  *
60
60
  * Each answers `{ recorded: true }` when it wrote, or `{ recorded: false,
61
61
  * message }` saying why nothing was written: no record of the session on this
62
- * server (its dashboard is off, or it is a node, which keeps none for the
63
- * hub's sessions), an argument it can't use, or a failed write. None of them
64
- * fails the test's command. They are bookkeeping, and a test shouldn't die
65
- * because the server it ran on keeps no dashboard. Through 2.13.2
62
+ * server (it runs without `enableDashboard`, or it is a node, which keeps
63
+ * none for the hub's sessions), an argument it can't use, or a failed write.
64
+ * None of them fails the test's command. They are bookkeeping, and a test
65
+ * shouldn't die because the server it ran on records no sessions. Through 2.13.2
66
66
  * setSessionName, setSessionStatus and debug failed the command there
67
67
  * (Prisma's P2025, a foreign-key error), and addTag and captureEvidence
68
68
  * answered `null` as if they had worked.
@@ -132,7 +132,7 @@ class DashboardCommands {
132
132
  });
133
133
  if (!row) {
134
134
  answer = notRecorded(`This server keeps no record of session ${sessionId}, so nothing was saved. ` +
135
- 'Sessions are recorded on a hub with its dashboard turned on.');
135
+ 'Sessions are recorded by a server started with enableDashboard, never by a node.');
136
136
  }
137
137
  else {
138
138
  switch (command) {
@@ -63,12 +63,9 @@ const helpers_1 = require("../helpers");
63
63
  const asset_manager_1 = require("./asset-manager");
64
64
  const commands_1 = require("./commands");
65
65
  const SessionStatus_1 = require("../types/SessionStatus");
66
- const appium_ios_device_1 = require("appium-ios-device");
67
66
  const config_1 = require("../config");
68
67
  const helpers_2 = require("../helpers");
69
68
  const typedi_1 = require("typedi");
70
- const device_managers_1 = require("../device-managers");
71
- const AndroidDeviceManager_1 = __importDefault(require("../device-managers/AndroidDeviceManager"));
72
69
  const IOSStreamService_1 = __importDefault(require("../device-managers/ios/IOSStreamService"));
73
70
  const AndroidStreamService_1 = __importDefault(require("../device-managers/android/AndroidStreamService"));
74
71
  const SocketServer_1 = require("../services/SocketServer");
@@ -80,7 +77,10 @@ const SelectorStateService_1 = require("../services/SelectorStateService");
80
77
  const recording_store_1 = require("../services/recording/recording-store");
81
78
  const NotificationService_1 = require("../services/NotificationService");
82
79
  const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
80
+ const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
83
81
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
82
+ const sessionCommandSummary_1 = require("./sessionCommandSummary");
83
+ const commandLogFields_1 = require("./commandLogFields");
84
84
  const typedi_2 = require("typedi");
85
85
  /**
86
86
  * What a Session row keeps of the capabilities the driver returned. The row
@@ -100,14 +100,8 @@ function storedSessionCapabilities(sessionResponse) {
100
100
  let DashboardEventManager = class DashboardEventManager {
101
101
  constructor() {
102
102
  // private SCREENSHOT_FOR_COMMANDS = ['click', 'setUrl', 'setValue', 'performActions'];
103
- // Store syslog services for real iOS devices
104
- this.syslogServices = new Map();
105
- // Map session ID to UDID for cleanup
106
- this.sessionToUdid = new Map();
107
103
  // Map session ID to device info
108
104
  this.sessionToDevice = new Map();
109
- // Track last log line for each session (for device logs)
110
- this.lastLogLine = new Map();
111
105
  // Track start time for each command to calculate duration
112
106
  this.commandStartTime = new Map();
113
107
  // Idempotency Guard: Prevents double-invocation of onSessionStopped by racing actors
@@ -115,7 +109,7 @@ let DashboardEventManager = class DashboardEventManager {
115
109
  }
116
110
  onSessionStarted(capabilities, session, device) {
117
111
  return __awaiter(this, void 0, void 0, function* () {
118
- var _a, _b;
112
+ var _a, _b, _c, _d, _e, _f;
119
113
  // Store device info for this session
120
114
  this.sessionToDevice.set(session.getId(), device);
121
115
  const createOptions = {
@@ -158,6 +152,24 @@ let DashboardEventManager = class DashboardEventManager {
158
152
  if (sampled) {
159
153
  metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
160
154
  }
155
+ // The device's log, from about when the phone was given to the session,
156
+ // once its row exists (the lines point at it), unless the session turned
157
+ // it off: an Android phone's stream, or the log an iPhone's or
158
+ // simulator's driver captures. Not waited for: an Android phone's clock
159
+ // and log stream are read in the background.
160
+ const deviceLogs = typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs);
161
+ if (capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] === false) {
162
+ void deviceLogs.noteOff(session.getId());
163
+ }
164
+ else {
165
+ void deviceLogs.start({
166
+ sessionId: session.getId(),
167
+ device,
168
+ since: session.allocatedAt,
169
+ driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c),
170
+ appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e),
171
+ });
172
+ }
161
173
  // Emit session started event
162
174
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
163
175
  // Increment Metrics
@@ -177,9 +189,7 @@ let DashboardEventManager = class DashboardEventManager {
177
189
  return __awaiter(this, void 0, void 0, function* () {
178
190
  try {
179
191
  const isApple = device.platform === 'ios' || device.platform === 'tvos';
180
- const svc = isApple
181
- ? typedi_1.Container.get(IOSStreamService_1.default)
182
- : typedi_1.Container.get(AndroidStreamService_1.default);
192
+ const svc = isApple ? typedi_1.Container.get(IOSStreamService_1.default) : typedi_1.Container.get(AndroidStreamService_1.default);
183
193
  const status = svc.getStreamStatus(device.udid);
184
194
  if (status && status.viewerCount === 0) {
185
195
  yield svc.stopStream(device.udid);
@@ -211,8 +221,10 @@ let DashboardEventManager = class DashboardEventManager {
211
221
  this.stoppingSessionIds.add(sessionId);
212
222
  try {
213
223
  logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
214
- // However the session ended, in memory or not, its sampler stops here.
224
+ // However the session ended, in memory or not, its sampler stops here,
225
+ // and its device log is written to the end.
215
226
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
227
+ yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
216
228
  // Video recording is now handled in plugin.ts deleteSession() before the session is deleted
217
229
  // This ensures we can call stop_recording_screen while the session is still active
218
230
  // Here we just handle the session status update
@@ -221,20 +233,6 @@ let DashboardEventManager = class DashboardEventManager {
221
233
  logger_1.default.info(`Session ${sessionId} found in SESSION_MANAGER`);
222
234
  // iOS profiling is now handled in plugin.ts deleteSession() before the session is deleted
223
235
  // This ensures we can call mobile: stopPerfRecord while the driver is still alive
224
- // Clean up syslog service for real iOS devices
225
- const udid = this.sessionToUdid.get(sessionId);
226
- if (udid) {
227
- try {
228
- this.syslogServices.delete(udid);
229
- this.sessionToUdid.delete(sessionId);
230
- logger_1.default.info(`Cleaned up syslog service for device ${udid}`);
231
- }
232
- catch (err) {
233
- logger_1.default.debug(`Error cleaning up syslog service info: ${err}`);
234
- }
235
- }
236
- // Clean up last log line tracking
237
- this.lastLogLine.delete(sessionId);
238
236
  // Principal Resource Management: Unblock device immediately
239
237
  const device = this.sessionToDevice.get(sessionId);
240
238
  if (device) {
@@ -265,12 +263,6 @@ let DashboardEventManager = class DashboardEventManager {
265
263
  }
266
264
  // Final local cleanup to prevent state leaks
267
265
  this.sessionToDevice.delete(sessionId);
268
- this.lastLogLine.delete(sessionId);
269
- const orphanUdid = this.sessionToUdid.get(sessionId);
270
- if (orphanUdid) {
271
- this.sessionToUdid.delete(sessionId);
272
- this.syslogServices.delete(orphanUdid);
273
- }
274
266
  }
275
267
  else {
276
268
  logger_1.default.warn(`⚠️ Session ${sessionId} not found in SESSION_MANAGER`);
@@ -286,12 +278,6 @@ let DashboardEventManager = class DashboardEventManager {
286
278
  }
287
279
  finally {
288
280
  this.sessionToDevice.delete(sessionId);
289
- this.lastLogLine.delete(sessionId);
290
- const orphanUdid = this.sessionToUdid.get(sessionId);
291
- if (orphanUdid) {
292
- this.sessionToUdid.delete(sessionId);
293
- this.syslogServices.delete(orphanUdid);
294
- }
295
281
  }
296
282
  }
297
283
  const sessionEntry = yield (0, session_service_1.getSessionById)(sessionId);
@@ -356,11 +342,15 @@ let DashboardEventManager = class DashboardEventManager {
356
342
  if (updateData.status === SessionStatus_1.SessionStatus.FAILED && options.notify !== false) {
357
343
  this.announceFailure(Object.assign(Object.assign({}, sessionEntry), updateData));
358
344
  }
359
- // Principal Triage: If session failed, perform intelligent failure analysis
345
+ // Principal Triage: If session failed, perform intelligent failure analysis.
346
+ // The category now; the AI's analysis is not awaited, since every way
347
+ // a session ends waits for this method (the client's quit, a hub's
348
+ // DELETE) and an AI call can take minutes. It is saved when it comes.
360
349
  if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
361
350
  try {
362
- const { analyzeSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
363
- yield analyzeSessionFailure(sessionId);
351
+ const { categorizeSessionFailure, explainSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
352
+ yield categorizeSessionFailure(sessionId);
353
+ void explainSessionFailure(sessionId);
364
354
  }
365
355
  catch (analysisErr) {
366
356
  logger_1.default.warn(`⚠️ Failure analysis skipped for ${sessionId}: ${analysisErr.message}`);
@@ -429,14 +419,10 @@ let DashboardEventManager = class DashboardEventManager {
429
419
  }
430
420
  afterSessionCommand(sessionId, commandName, driver, request, response, responseBody, healingInfo) {
431
421
  return __awaiter(this, void 0, void 0, function* () {
432
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
422
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
433
423
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
434
424
  if (session) {
435
425
  try {
436
- // Save device logs (only if driver is available)
437
- if (driver) {
438
- yield this.saveDeviceLogs(sessionId, driver);
439
- }
440
426
  // Save command log
441
427
  const parsedResponse = (0, helpers_1.safeParseJson)(responseBody);
442
428
  const isSuccessResponse = !((_a = parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse.value) === null || _a === void 0 ? void 0 : _a.error);
@@ -445,11 +431,13 @@ let DashboardEventManager = class DashboardEventManager {
445
431
  const traceId = tracingService.getTraceId(session.getId());
446
432
  const startTime = this.commandStartTime.get(`${sessionId}:${commandName}`);
447
433
  const duration = startTime ? Date.now() - startTime : null;
434
+ // A network-capture script keeps neither its arguments nor its answer.
435
+ const recorded = (0, commandLogFields_1.commandLogFields)(commandName, request.body, responseBody, isSuccessResponse);
448
436
  const logEntry = {
449
437
  session_id: session.getId(),
450
438
  command_name: commandName || null,
451
- body: JSON.stringify(request.body),
452
- response: responseBody,
439
+ body: recorded.body,
440
+ response: recorded.response,
453
441
  is_success: isSuccessResponse,
454
442
  is_error: !isSuccessResponse,
455
443
  method: request.method,
@@ -470,15 +458,20 @@ let DashboardEventManager = class DashboardEventManager {
470
458
  };
471
459
  // Smart Passive: capture strategy + selector on every findElement,
472
460
  // not just heals. CommandInterceptor synthesizes request.body = args
473
- // (the array [strategy, value]). This gives the verification job
474
- // exact evidence of "selector ran and didn't heal" per build.
475
- if ((commandName === 'findElement' || commandName === 'findElements') &&
476
- Array.isArray(request.body)) {
477
- if (logEntry.original_strategy === null) {
478
- logEntry.original_strategy = (_g = request.body[0]) !== null && _g !== void 0 ? _g : null;
461
+ // (the array [strategy, value]); a command a hub forwards to a node
462
+ // has the client's own W3C body ({ using, value }). This gives the
463
+ // verification job exact evidence of "selector ran and didn't heal"
464
+ // per build.
465
+ if (commandName === 'findElement' || commandName === 'findElements') {
466
+ const body = request.body;
467
+ const [strategy, selector] = Array.isArray(body)
468
+ ? [body[0], body[1]]
469
+ : [body === null || body === void 0 ? void 0 : body.using, body === null || body === void 0 ? void 0 : body.value];
470
+ if (logEntry.original_strategy === null && typeof strategy === 'string') {
471
+ logEntry.original_strategy = strategy;
479
472
  }
480
- if (logEntry.original_selector === null) {
481
- logEntry.original_selector = (_h = request.body[1]) !== null && _h !== void 0 ? _h : null;
473
+ if (logEntry.original_selector === null && typeof selector === 'string') {
474
+ logEntry.original_selector = selector;
482
475
  }
483
476
  }
484
477
  // Increment Healing Metrics
@@ -530,8 +523,10 @@ let DashboardEventManager = class DashboardEventManager {
530
523
  });
531
524
  // Emit command log event to dashboard. One per command: the phone's
532
525
  // team comes from DeviceTeamResolver's cache, not a query each time.
526
+ // Its summary only: what the command typed and answered stays in the
527
+ // SessionLog row above, which goes with the session.
533
528
  const device = session.getDevice();
534
- void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_COMMAND, Object.assign({ session_id: session.getId() }, logEntry), { udid: device === null || device === void 0 ? void 0 : device.udid });
529
+ void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_COMMAND, (0, sessionCommandSummary_1.sessionCommandSummary)(Object.assign(Object.assign({}, logEntry), { session_id: session.getId() })), { udid: device === null || device === void 0 ? void 0 : device.udid });
535
530
  // Heal event broadcast — match the shape returned by GET /healing/events
536
531
  // so the Overview activity feed and Settings list can consume both
537
532
  // sources interchangeably without per-source field translation.
@@ -539,15 +534,15 @@ let DashboardEventManager = class DashboardEventManager {
539
534
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.HEALING_EVENT, {
540
535
  id: persistedLog.id,
541
536
  sessionId: session.getId(),
542
- deviceUdid: (_j = device === null || device === void 0 ? void 0 : device.udid) !== null && _j !== void 0 ? _j : null,
543
- deviceName: (_k = device === null || device === void 0 ? void 0 : device.name) !== null && _k !== void 0 ? _k : null,
544
- devicePlatform: (_l = device === null || device === void 0 ? void 0 : device.platform) !== null && _l !== void 0 ? _l : null,
545
- commandName: (_m = logEntry.command_name) !== null && _m !== void 0 ? _m : null,
546
- originalSelector: (_o = logEntry.original_selector) !== null && _o !== void 0 ? _o : null,
547
- healedSelector: (_p = logEntry.healed_selector) !== null && _p !== void 0 ? _p : null,
548
- confidence: (_q = logEntry.healing_confidence) !== null && _q !== void 0 ? _q : null,
549
- tier: (_r = logEntry.healing_tier) !== null && _r !== void 0 ? _r : null,
550
- isSuccess: (_s = logEntry.is_success) !== null && _s !== void 0 ? _s : null,
537
+ deviceUdid: (_g = device === null || device === void 0 ? void 0 : device.udid) !== null && _g !== void 0 ? _g : null,
538
+ deviceName: (_h = device === null || device === void 0 ? void 0 : device.name) !== null && _h !== void 0 ? _h : null,
539
+ devicePlatform: (_j = device === null || device === void 0 ? void 0 : device.platform) !== null && _j !== void 0 ? _j : null,
540
+ commandName: (_k = logEntry.command_name) !== null && _k !== void 0 ? _k : null,
541
+ originalSelector: (_l = logEntry.original_selector) !== null && _l !== void 0 ? _l : null,
542
+ healedSelector: (_m = logEntry.healed_selector) !== null && _m !== void 0 ? _m : null,
543
+ confidence: (_o = logEntry.healing_confidence) !== null && _o !== void 0 ? _o : null,
544
+ tier: (_p = logEntry.healing_tier) !== null && _p !== void 0 ? _p : null,
545
+ isSuccess: (_q = logEntry.is_success) !== null && _q !== void 0 ? _q : null,
551
546
  createdAt: persistedLog.createdAt.toISOString(),
552
547
  }, { udid: device === null || device === void 0 ? void 0 : device.udid });
553
548
  }
@@ -555,9 +550,7 @@ let DashboardEventManager = class DashboardEventManager {
555
550
  // If the (original_strategy, original_selector) row is in pending/resolved,
556
551
  // SelectorStateService.onHealRecorded will flip it back to active with
557
552
  // regression_count++ and emit SELECTOR_REGRESSED.
558
- if (logEntry.is_healed &&
559
- logEntry.original_strategy &&
560
- logEntry.original_selector) {
553
+ if (logEntry.is_healed && logEntry.original_strategy && logEntry.original_selector) {
561
554
  typedi_1.Container.get(SelectorStateService_1.SelectorStateService)
562
555
  .onHealRecorded({
563
556
  strategy: logEntry.original_strategy,
@@ -593,139 +586,6 @@ let DashboardEventManager = class DashboardEventManager {
593
586
  }
594
587
  return undefined;
595
588
  }
596
- getDeviceLogs(driver, sessionId) {
597
- return __awaiter(this, void 0, void 0, function* () {
598
- try {
599
- if (!driver || !driver.caps || !driver.caps.automationName) {
600
- return [];
601
- }
602
- const automationName = driver.caps.automationName.toLowerCase();
603
- // Get device info for this session
604
- const device = this.sessionToDevice.get(sessionId);
605
- // Use Appium driver's extractLogs method for proper log extraction
606
- if (automationName === 'xcuitest' && typeof driver.extractLogs === 'function') {
607
- try {
608
- // Check if this is a real iOS device using device.realDevice property
609
- const isRealDevice = (device === null || device === void 0 ? void 0 : device.realDevice) === true;
610
- logger_1.default.debug(`Device info for session ${sessionId} - isRealDevice: ${isRealDevice}, deviceType: ${device === null || device === void 0 ? void 0 : device.deviceType}, UDID: ${device === null || device === void 0 ? void 0 : device.udid}`);
611
- if (isRealDevice) {
612
- // For real iOS devices, use appium-ios-device syslog service
613
- const udid = driver.caps.udid;
614
- // Track session to UDID mapping for cleanup
615
- this.sessionToUdid.set(sessionId, udid);
616
- // If we don't have a syslog service for this device yet, start one
617
- if (!this.syslogServices.has(udid)) {
618
- try {
619
- const syslogService = yield appium_ios_device_1.services.startSyslogService(udid);
620
- const logs = [];
621
- // Start listening to logs and buffer them
622
- syslogService.start((logLine) => {
623
- logs.push(logLine);
624
- });
625
- // Store the service and logs
626
- this.syslogServices.set(udid, { service: syslogService, logs });
627
- logger_1.default.info(`Started syslog service for real iOS device ${udid}`);
628
- }
629
- catch (err) {
630
- logger_1.default.debug(`Could not start syslog service for real device: ${err}`);
631
- return [];
632
- }
633
- }
634
- // Return the buffered logs
635
- const deviceData = this.syslogServices.get(udid);
636
- if (deviceData && deviceData.logs) {
637
- const currentLogs = [...deviceData.logs];
638
- // Clear the buffer after retrieving
639
- deviceData.logs.length = 0;
640
- return currentLogs.map((logLine) => ({ message: logLine, timestamp: Date.now() }));
641
- }
642
- return [];
643
- }
644
- // For iOS simulators, extract syslog
645
- const logs = yield driver.extractLogs('syslog');
646
- return Array.isArray(logs) ? logs : [];
647
- }
648
- catch (err) {
649
- logger_1.default.debug(`Could not extract syslog: ${err}`);
650
- return [];
651
- }
652
- }
653
- else if (automationName === 'uiautomator2' || (device === null || device === void 0 ? void 0 : device.platform) === 'android') {
654
- try {
655
- if (typeof driver.extractLogs === 'function') {
656
- const logs = yield driver.extractLogs('logcat');
657
- if (Array.isArray(logs) && logs.length > 0) {
658
- return logs;
659
- }
660
- }
661
- }
662
- catch (err) {
663
- logger_1.default.debug(`Could not extract logcat via driver: ${err}. Trying direct ADB...`);
664
- }
665
- // Principal Intelligence: Fallback to direct ADB logs if driver fails or returns nothing
666
- if (device === null || device === void 0 ? void 0 : device.udid) {
667
- try {
668
- const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
669
- const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
670
- if (androidManager) {
671
- const rawLogs = yield androidManager.getLogs(device.udid);
672
- if (rawLogs && typeof rawLogs === 'string') {
673
- const logLines = rawLogs.split('\n').filter((l) => l.trim().length > 0);
674
- // Convert string lines to expected format (last 100 lines to avoid DB bloat)
675
- return logLines.slice(-100).map((line) => ({
676
- message: line,
677
- timestamp: Date.now(),
678
- level: 'INFO',
679
- }));
680
- }
681
- }
682
- }
683
- catch (adbErr) {
684
- logger_1.default.debug(`Direct ADB log fetch failed for ${device.udid}: ${adbErr.message}`);
685
- }
686
- }
687
- }
688
- return [];
689
- }
690
- catch (error) {
691
- logger_1.default.error(`Error getting device logs: ${error}`);
692
- return [];
693
- }
694
- });
695
- }
696
- saveDeviceLogs(sessionId, driver) {
697
- return __awaiter(this, void 0, void 0, function* () {
698
- try {
699
- const logs = yield this.getDeviceLogs(driver, sessionId);
700
- if (!logs || logs.length === 0) {
701
- return;
702
- }
703
- const lastLine = this.lastLogLine.get(sessionId) || 0;
704
- const newLogs = logs.slice(lastLine);
705
- if (newLogs.length === 0) {
706
- return;
707
- }
708
- this.lastLogLine.set(sessionId, logs.length);
709
- // Save device logs to database
710
- const logEntries = newLogs.map((logItem) => ({
711
- session_id: sessionId,
712
- log_type: 'DEVICE',
713
- message: typeof logItem === 'string' ? logItem : logItem.message || JSON.stringify(logItem),
714
- timestamp: logItem.timestamp ? new Date(logItem.timestamp) : new Date(),
715
- }));
716
- if (logEntries.length > 0) {
717
- for (const logEntry of logEntries) {
718
- yield prisma_1.prisma.log.create({
719
- data: logEntry,
720
- });
721
- }
722
- }
723
- }
724
- catch (error) {
725
- logger_1.default.error(`Error saving device logs: ${error.message}`);
726
- }
727
- });
728
- }
729
589
  // ─── Recording (free-form mosaic) events ─────────────────────────────────
730
590
  // Emitted by RecordingOrchestrator. Distinct namespace from SESSION_*; the
731
591
  // dashboard listens to both independently. Team-scoped: a group's started