@xenon-device-management/xenon 2.13.2 → 2.15.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.
- package/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +85 -71
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -67,8 +67,6 @@ const appium_ios_device_1 = require("appium-ios-device");
|
|
|
67
67
|
const config_1 = require("../config");
|
|
68
68
|
const helpers_2 = require("../helpers");
|
|
69
69
|
const typedi_1 = require("typedi");
|
|
70
|
-
const device_managers_1 = require("../device-managers");
|
|
71
|
-
const AndroidDeviceManager_1 = __importDefault(require("../device-managers/AndroidDeviceManager"));
|
|
72
70
|
const IOSStreamService_1 = __importDefault(require("../device-managers/ios/IOSStreamService"));
|
|
73
71
|
const AndroidStreamService_1 = __importDefault(require("../device-managers/android/AndroidStreamService"));
|
|
74
72
|
const SocketServer_1 = require("../services/SocketServer");
|
|
@@ -78,8 +76,11 @@ const SocketEvents_1 = require("../enums/SocketEvents");
|
|
|
78
76
|
const types_1 = require("../services/healing/types");
|
|
79
77
|
const SelectorStateService_1 = require("../services/SelectorStateService");
|
|
80
78
|
const recording_store_1 = require("../services/recording/recording-store");
|
|
79
|
+
const NotificationService_1 = require("../services/NotificationService");
|
|
81
80
|
const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
|
|
81
|
+
const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
|
|
82
82
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
83
|
+
const sessionCommandSummary_1 = require("./sessionCommandSummary");
|
|
83
84
|
const typedi_2 = require("typedi");
|
|
84
85
|
/**
|
|
85
86
|
* What a Session row keeps of the capabilities the driver returned. The row
|
|
@@ -105,7 +106,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
105
106
|
this.sessionToUdid = new Map();
|
|
106
107
|
// Map session ID to device info
|
|
107
108
|
this.sessionToDevice = new Map();
|
|
108
|
-
// Track last log line for each session (
|
|
109
|
+
// Track last log line for each session (an iPhone's device logs)
|
|
109
110
|
this.lastLogLine = new Map();
|
|
110
111
|
// Track start time for each command to calculate duration
|
|
111
112
|
this.commandStartTime = new Map();
|
|
@@ -157,6 +158,14 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
157
158
|
if (sampled) {
|
|
158
159
|
metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
|
|
159
160
|
}
|
|
161
|
+
// An Android phone's device log, from about when the phone was given to
|
|
162
|
+
// the session, once its row exists (the lines point at it). Not waited
|
|
163
|
+
// for: the phone's clock and log stream are read in the background.
|
|
164
|
+
void typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).start({
|
|
165
|
+
sessionId: session.getId(),
|
|
166
|
+
device,
|
|
167
|
+
since: session.allocatedAt,
|
|
168
|
+
});
|
|
160
169
|
// Emit session started event
|
|
161
170
|
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 });
|
|
162
171
|
// Increment Metrics
|
|
@@ -190,8 +199,16 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
190
199
|
}
|
|
191
200
|
});
|
|
192
201
|
}
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Closes a session's record: its end time, its final status, the device's
|
|
204
|
+
* release. Every way a session ends comes here (the client's delete, an
|
|
205
|
+
* inactivity timeout, a driver crash, a heartbeat timeout, a shutdown), so
|
|
206
|
+
* this is where a failed one is announced to the `session_failed` webhooks.
|
|
207
|
+
* `notify: false` records the failure without announcing it: a server
|
|
208
|
+
* shutting down fails the sessions it drains, which no test caused.
|
|
209
|
+
*/
|
|
210
|
+
onSessionStopped(sessionId_1, status_1, failureReason_1) {
|
|
211
|
+
return __awaiter(this, arguments, void 0, function* (sessionId, status, failureReason, options = {}) {
|
|
195
212
|
var _a, _b, _c, _d;
|
|
196
213
|
// Idempotency Guard: If this session is already being stopped by another actor
|
|
197
214
|
// (heartbeat, stream watchdog, plugin.deleteSession), skip to avoid double-cleanup.
|
|
@@ -202,8 +219,10 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
202
219
|
this.stoppingSessionIds.add(sessionId);
|
|
203
220
|
try {
|
|
204
221
|
logger_1.default.info(`🟢 onSessionStopped called for session ${sessionId}`);
|
|
205
|
-
// However the session ended, in memory or not, its sampler stops here
|
|
222
|
+
// However the session ended, in memory or not, its sampler stops here,
|
|
223
|
+
// and its device log is written to the end.
|
|
206
224
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
225
|
+
yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
|
|
207
226
|
// Video recording is now handled in plugin.ts deleteSession() before the session is deleted
|
|
208
227
|
// This ensures we can call stop_recording_screen while the session is still active
|
|
209
228
|
// Here we just handle the session status update
|
|
@@ -341,11 +360,21 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
341
360
|
else if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
|
|
342
361
|
typedi_1.Container.get(MetricsService_1.MetricsService).incrementSessionFailure();
|
|
343
362
|
}
|
|
344
|
-
//
|
|
363
|
+
// Webhooks: not awaited, since a slow or dead webhook must not hold up
|
|
364
|
+
// the end of the session. The service sends once per session even
|
|
365
|
+
// though a session can end twice (a crash, then the client's delete).
|
|
366
|
+
if (updateData.status === SessionStatus_1.SessionStatus.FAILED && options.notify !== false) {
|
|
367
|
+
this.announceFailure(Object.assign(Object.assign({}, sessionEntry), updateData));
|
|
368
|
+
}
|
|
369
|
+
// Principal Triage: If session failed, perform intelligent failure analysis.
|
|
370
|
+
// The category now; the AI's analysis is not awaited, since every way
|
|
371
|
+
// a session ends waits for this method (the client's quit, a hub's
|
|
372
|
+
// DELETE) and an AI call can take minutes. It is saved when it comes.
|
|
345
373
|
if (updateData.status === SessionStatus_1.SessionStatus.FAILED) {
|
|
346
374
|
try {
|
|
347
|
-
const {
|
|
348
|
-
yield
|
|
375
|
+
const { categorizeSessionFailure, explainSessionFailure } = yield Promise.resolve().then(() => __importStar(require('./services/failure-analysis-service')));
|
|
376
|
+
yield categorizeSessionFailure(sessionId);
|
|
377
|
+
void explainSessionFailure(sessionId);
|
|
349
378
|
}
|
|
350
379
|
catch (analysisErr) {
|
|
351
380
|
logger_1.default.warn(`⚠️ Failure analysis skipped for ${sessionId}: ${analysisErr.message}`);
|
|
@@ -363,6 +392,18 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
363
392
|
}
|
|
364
393
|
});
|
|
365
394
|
}
|
|
395
|
+
/** Starts the `session_failed` webhooks; never throws and never waits for them. */
|
|
396
|
+
announceFailure(session) {
|
|
397
|
+
var _a;
|
|
398
|
+
try {
|
|
399
|
+
void typedi_1.Container.get(NotificationService_1.NotificationService)
|
|
400
|
+
.notifySessionFailed(session)
|
|
401
|
+
.catch((err) => { var _a; return logger_1.default.warn(`session_failed webhook for ${session.id} not sent: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
logger_1.default.warn(`session_failed webhook for ${session.id} not sent: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
366
407
|
beforeSessionCommand(sessionId, commandName, request, response) {
|
|
367
408
|
return __awaiter(this, void 0, void 0, function* () {
|
|
368
409
|
var _a;
|
|
@@ -370,16 +411,22 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
370
411
|
this.commandStartTime.set(`${sessionId}:${commandName}`, Date.now());
|
|
371
412
|
logger_1.default.debug(`[EventManager] beforeSessionCommand: sessionId=${sessionId}, commandName=${commandName}`);
|
|
372
413
|
}
|
|
373
|
-
//
|
|
414
|
+
// The session-details commands (`xenon: setSessionName`, ...) are answered
|
|
415
|
+
// here, from this server's record of the session, whether or not the
|
|
416
|
+
// session is in memory. Every other `xenon:` script goes on: to the
|
|
417
|
+
// plugin's CommandInterceptor on the server that drives the phone, which
|
|
418
|
+
// a hub reaches by forwarding it. Taking them all here answered a node
|
|
419
|
+
// phone's autowait, Omni-Vision and network-capture scripts with null on
|
|
420
|
+
// the hub, and they never reached the node.
|
|
374
421
|
if (commandName === 'execute') {
|
|
375
422
|
const script = ((_a = request.body) === null || _a === void 0 ? void 0 : _a.script) || (Array.isArray(request.body) ? request.body[0] : undefined);
|
|
376
|
-
if (
|
|
423
|
+
if ((0, commands_1.sessionDetailsCommandOf)(script)) {
|
|
377
424
|
logger_1.default.info(`[EventManager] Intercepting Xenon command: ${script} for session ${sessionId}`);
|
|
378
425
|
yield commands_1.dashboardCommands.process(sessionId, request, response);
|
|
379
426
|
return false;
|
|
380
427
|
}
|
|
381
|
-
else if (script && script.includes(':')) {
|
|
382
|
-
logger_1.default.debug(`[EventManager]
|
|
428
|
+
else if (typeof script === 'string' && script.includes(':')) {
|
|
429
|
+
logger_1.default.debug(`[EventManager] Script ${script} is not a session-details command; passing it on.`);
|
|
383
430
|
}
|
|
384
431
|
}
|
|
385
432
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
@@ -396,7 +443,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
396
443
|
}
|
|
397
444
|
afterSessionCommand(sessionId, commandName, driver, request, response, responseBody, healingInfo) {
|
|
398
445
|
return __awaiter(this, void 0, void 0, function* () {
|
|
399
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q
|
|
446
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
400
447
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
401
448
|
if (session) {
|
|
402
449
|
try {
|
|
@@ -437,15 +484,20 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
437
484
|
};
|
|
438
485
|
// Smart Passive: capture strategy + selector on every findElement,
|
|
439
486
|
// not just heals. CommandInterceptor synthesizes request.body = args
|
|
440
|
-
// (the array [strategy, value])
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
487
|
+
// (the array [strategy, value]); a command a hub forwards to a node
|
|
488
|
+
// has the client's own W3C body ({ using, value }). This gives the
|
|
489
|
+
// verification job exact evidence of "selector ran and didn't heal"
|
|
490
|
+
// per build.
|
|
491
|
+
if (commandName === 'findElement' || commandName === 'findElements') {
|
|
492
|
+
const body = request.body;
|
|
493
|
+
const [strategy, selector] = Array.isArray(body)
|
|
494
|
+
? [body[0], body[1]]
|
|
495
|
+
: [body === null || body === void 0 ? void 0 : body.using, body === null || body === void 0 ? void 0 : body.value];
|
|
496
|
+
if (logEntry.original_strategy === null && typeof strategy === 'string') {
|
|
497
|
+
logEntry.original_strategy = strategy;
|
|
446
498
|
}
|
|
447
|
-
if (logEntry.original_selector === null) {
|
|
448
|
-
logEntry.original_selector =
|
|
499
|
+
if (logEntry.original_selector === null && typeof selector === 'string') {
|
|
500
|
+
logEntry.original_selector = selector;
|
|
449
501
|
}
|
|
450
502
|
}
|
|
451
503
|
// Increment Healing Metrics
|
|
@@ -497,8 +549,10 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
497
549
|
});
|
|
498
550
|
// Emit command log event to dashboard. One per command: the phone's
|
|
499
551
|
// team comes from DeviceTeamResolver's cache, not a query each time.
|
|
552
|
+
// Its summary only: what the command typed and answered stays in the
|
|
553
|
+
// SessionLog row above, which goes with the session.
|
|
500
554
|
const device = session.getDevice();
|
|
501
|
-
void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_COMMAND, Object.assign({ session_id: session.getId() }
|
|
555
|
+
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 });
|
|
502
556
|
// Heal event broadcast — match the shape returned by GET /healing/events
|
|
503
557
|
// so the Overview activity feed and Settings list can consume both
|
|
504
558
|
// sources interchangeably without per-source field translation.
|
|
@@ -506,15 +560,15 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
506
560
|
void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.HEALING_EVENT, {
|
|
507
561
|
id: persistedLog.id,
|
|
508
562
|
sessionId: session.getId(),
|
|
509
|
-
deviceUdid: (
|
|
510
|
-
deviceName: (
|
|
511
|
-
devicePlatform: (
|
|
512
|
-
commandName: (
|
|
513
|
-
originalSelector: (
|
|
514
|
-
healedSelector: (
|
|
515
|
-
confidence: (
|
|
516
|
-
tier: (
|
|
517
|
-
isSuccess: (
|
|
563
|
+
deviceUdid: (_g = device === null || device === void 0 ? void 0 : device.udid) !== null && _g !== void 0 ? _g : null,
|
|
564
|
+
deviceName: (_h = device === null || device === void 0 ? void 0 : device.name) !== null && _h !== void 0 ? _h : null,
|
|
565
|
+
devicePlatform: (_j = device === null || device === void 0 ? void 0 : device.platform) !== null && _j !== void 0 ? _j : null,
|
|
566
|
+
commandName: (_k = logEntry.command_name) !== null && _k !== void 0 ? _k : null,
|
|
567
|
+
originalSelector: (_l = logEntry.original_selector) !== null && _l !== void 0 ? _l : null,
|
|
568
|
+
healedSelector: (_m = logEntry.healed_selector) !== null && _m !== void 0 ? _m : null,
|
|
569
|
+
confidence: (_o = logEntry.healing_confidence) !== null && _o !== void 0 ? _o : null,
|
|
570
|
+
tier: (_p = logEntry.healing_tier) !== null && _p !== void 0 ? _p : null,
|
|
571
|
+
isSuccess: (_q = logEntry.is_success) !== null && _q !== void 0 ? _q : null,
|
|
518
572
|
createdAt: persistedLog.createdAt.toISOString(),
|
|
519
573
|
}, { udid: device === null || device === void 0 ? void 0 : device.udid });
|
|
520
574
|
}
|
|
@@ -617,41 +671,8 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
617
671
|
return [];
|
|
618
672
|
}
|
|
619
673
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (typeof driver.extractLogs === 'function') {
|
|
623
|
-
const logs = yield driver.extractLogs('logcat');
|
|
624
|
-
if (Array.isArray(logs) && logs.length > 0) {
|
|
625
|
-
return logs;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
catch (err) {
|
|
630
|
-
logger_1.default.debug(`Could not extract logcat via driver: ${err}. Trying direct ADB...`);
|
|
631
|
-
}
|
|
632
|
-
// Principal Intelligence: Fallback to direct ADB logs if driver fails or returns nothing
|
|
633
|
-
if (device === null || device === void 0 ? void 0 : device.udid) {
|
|
634
|
-
try {
|
|
635
|
-
const deviceManager = typedi_1.Container.get(device_managers_1.XenonManager);
|
|
636
|
-
const androidManager = (yield deviceManager.deviceInstances()).find((m) => m instanceof AndroidDeviceManager_1.default);
|
|
637
|
-
if (androidManager) {
|
|
638
|
-
const rawLogs = yield androidManager.getLogs(device.udid);
|
|
639
|
-
if (rawLogs && typeof rawLogs === 'string') {
|
|
640
|
-
const logLines = rawLogs.split('\n').filter((l) => l.trim().length > 0);
|
|
641
|
-
// Convert string lines to expected format (last 100 lines to avoid DB bloat)
|
|
642
|
-
return logLines.slice(-100).map((line) => ({
|
|
643
|
-
message: line,
|
|
644
|
-
timestamp: Date.now(),
|
|
645
|
-
level: 'INFO',
|
|
646
|
-
}));
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
catch (adbErr) {
|
|
651
|
-
logger_1.default.debug(`Direct ADB log fetch failed for ${device.udid}: ${adbErr.message}`);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
674
|
+
// An Android phone's lines are recorded for the whole session by
|
|
675
|
+
// SessionDeviceLogs, not per command.
|
|
655
676
|
return [];
|
|
656
677
|
}
|
|
657
678
|
catch (error) {
|
|
@@ -45,10 +45,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.
|
|
48
|
+
exports.MAX_CONCURRENT_FAILURE_ANALYSES = exports.ANALYSIS_CATEGORIES = exports.ERROR_PATTERNS = void 0;
|
|
49
|
+
exports.categorizeSessionFailure = categorizeSessionFailure;
|
|
50
|
+
exports.explainSessionFailure = explainSessionFailure;
|
|
49
51
|
const prisma_1 = require("../../prisma");
|
|
50
52
|
const logger_1 = __importDefault(require("../../logger"));
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Each category and the texts that file a failure under it, tried in this
|
|
55
|
+
* order against the failure reason and the last five failed commands. The
|
|
56
|
+
* dashboard's runbooks describe a category by these, not by its name
|
|
57
|
+
* (failure-categories.spec.ts).
|
|
58
|
+
*/
|
|
59
|
+
exports.ERROR_PATTERNS = [
|
|
52
60
|
{
|
|
53
61
|
category: 'ELEMENT_NOT_FOUND',
|
|
54
62
|
patterns: [
|
|
@@ -98,7 +106,54 @@ const ERROR_PATTERNS = [
|
|
|
98
106
|
patterns: ['OutOfMemory', 'MemoryLimit', 'thermal throttling', 'too many open files'],
|
|
99
107
|
},
|
|
100
108
|
];
|
|
101
|
-
|
|
109
|
+
/** What the analysis writes when no pattern matches. */
|
|
110
|
+
const UNMATCHED = 'UNKNOWN';
|
|
111
|
+
/**
|
|
112
|
+
* Every category the analysis writes to a failed session's
|
|
113
|
+
* `failure_category`, upper case as stored. The only other value the column
|
|
114
|
+
* holds is `HUB_RESTART_CATEGORY` (SessionManager). The API reference's
|
|
115
|
+
* examples and the dashboard's runbooks are held to these
|
|
116
|
+
* (failure-categories.spec.ts).
|
|
117
|
+
*/
|
|
118
|
+
exports.ANALYSIS_CATEGORIES = [
|
|
119
|
+
...exports.ERROR_PATTERNS.map((p) => p.category),
|
|
120
|
+
UNMATCHED,
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* How many failed sessions' AI analyses run at once (explainSessionFailure);
|
|
124
|
+
* the others wait their turn. They run after their sessions have ended, so
|
|
125
|
+
* nothing else limits them: a broken build on a big lab ends hundreds of
|
|
126
|
+
* failed sessions in minutes, and each analysis holds a screenshot while it
|
|
127
|
+
* runs. Few at once also spares the provider's rate limit.
|
|
128
|
+
*/
|
|
129
|
+
exports.MAX_CONCURRENT_FAILURE_ANALYSES = 4;
|
|
130
|
+
/** Each session's analysis, waiting or running, so one session gets one. */
|
|
131
|
+
const analyses = new Map();
|
|
132
|
+
let analysesRunning = 0;
|
|
133
|
+
const analysesWaiting = [];
|
|
134
|
+
function analysisTurn() {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
+
if (analysesRunning < exports.MAX_CONCURRENT_FAILURE_ANALYSES) {
|
|
137
|
+
analysesRunning++;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// The turn is handed over by endAnalysisTurn, still counted as running.
|
|
141
|
+
yield new Promise((resolve) => analysesWaiting.push(resolve));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function endAnalysisTurn() {
|
|
145
|
+
const next = analysesWaiting.shift();
|
|
146
|
+
if (next)
|
|
147
|
+
next();
|
|
148
|
+
else
|
|
149
|
+
analysesRunning--;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Files a failed session under a category (`failure_category`) by its failure
|
|
153
|
+
* reason and its failed commands. Rules only, so it is quick: a session's end
|
|
154
|
+
* waits for it. Never throws.
|
|
155
|
+
*/
|
|
156
|
+
function categorizeSessionFailure(sessionId) {
|
|
102
157
|
return __awaiter(this, void 0, void 0, function* () {
|
|
103
158
|
try {
|
|
104
159
|
const session = yield prisma_1.prisma.session.findUnique({
|
|
@@ -112,8 +167,8 @@ function analyzeSessionFailure(sessionId) {
|
|
|
112
167
|
const reason = session.failure_reason || '';
|
|
113
168
|
const logs_text = session.SessionLog.map((l) => `${l.title} ${l.response}`).join(' ');
|
|
114
169
|
const combined_text = (reason + ' ' + logs_text).toLowerCase();
|
|
115
|
-
let identifiedCategory =
|
|
116
|
-
for (const item of ERROR_PATTERNS) {
|
|
170
|
+
let identifiedCategory = UNMATCHED;
|
|
171
|
+
for (const item of exports.ERROR_PATTERNS) {
|
|
117
172
|
if (item.patterns.some((p) => new RegExp(p, 'i').test(combined_text))) {
|
|
118
173
|
identifiedCategory = item.category;
|
|
119
174
|
break;
|
|
@@ -122,48 +177,9 @@ function analyzeSessionFailure(sessionId) {
|
|
|
122
177
|
// Special case for App Crash - check Logcat/Syslog if available
|
|
123
178
|
// For now we rely on the error response text which usually mentions "process has died"
|
|
124
179
|
logger_1.default.info(`[FailureAnalysis] Session ${sessionId} identified as ${identifiedCategory}`);
|
|
125
|
-
// AI Root-Cause Analysis
|
|
126
|
-
let aiAnalysis = null;
|
|
127
|
-
try {
|
|
128
|
-
const { AI_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../services/AIService')));
|
|
129
|
-
if (AI_SERVICE.isEnabled()) {
|
|
130
|
-
const lastLogs = yield prisma_1.prisma.log.findMany({
|
|
131
|
-
where: { session_id: sessionId, log_type: 'DEVICE' },
|
|
132
|
-
take: 50,
|
|
133
|
-
orderBy: { timestamp: 'desc' },
|
|
134
|
-
});
|
|
135
|
-
const lastCommands = yield prisma_1.prisma.sessionLog.findMany({
|
|
136
|
-
where: { session_id: sessionId },
|
|
137
|
-
take: 10,
|
|
138
|
-
orderBy: { createdAt: 'desc' },
|
|
139
|
-
});
|
|
140
|
-
// Find the last screenshot in logs
|
|
141
|
-
const lastScreenshotLog = lastCommands.find((l) => l.screenshot !== null);
|
|
142
|
-
aiAnalysis = yield AI_SERVICE.analyzeFailure({
|
|
143
|
-
sessionId,
|
|
144
|
-
failureReason: reason,
|
|
145
|
-
commandLogs: lastCommands.map((c) => {
|
|
146
|
-
var _a;
|
|
147
|
-
return ({
|
|
148
|
-
command: c.command_name,
|
|
149
|
-
success: c.is_success,
|
|
150
|
-
response: (_a = c.response) === null || _a === void 0 ? void 0 : _a.slice(0, 500), // Truncate long responses
|
|
151
|
-
});
|
|
152
|
-
}),
|
|
153
|
-
deviceLogs: lastLogs.map((l) => l.message),
|
|
154
|
-
screenshotPath: (lastScreenshotLog === null || lastScreenshotLog === void 0 ? void 0 : lastScreenshotLog.screenshot) || undefined,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch (aiErr) {
|
|
159
|
-
logger_1.default.warn(`[FailureAnalysis] AI Analysis failed for ${sessionId}: ${aiErr.message}`);
|
|
160
|
-
}
|
|
161
180
|
yield prisma_1.prisma.session.update({
|
|
162
181
|
where: { id: sessionId },
|
|
163
|
-
data: {
|
|
164
|
-
failure_category: identifiedCategory,
|
|
165
|
-
ai_analysis: aiAnalysis,
|
|
166
|
-
},
|
|
182
|
+
data: { failure_category: identifiedCategory },
|
|
167
183
|
});
|
|
168
184
|
}
|
|
169
185
|
catch (err) {
|
|
@@ -171,3 +187,78 @@ function analyzeSessionFailure(sessionId) {
|
|
|
171
187
|
}
|
|
172
188
|
});
|
|
173
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Asks the AI provider why a failed session failed, and saves the answer
|
|
192
|
+
* (`ai_analysis`). Only an answer is saved: with no provider, a rate limit, a
|
|
193
|
+
* time-out (FAILURE_ANALYSIS_TIMEOUT_MS) or a failed call nothing is written,
|
|
194
|
+
* and an analysis saved earlier stays. Never rejects.
|
|
195
|
+
*
|
|
196
|
+
* A session's end doesn't wait for it (onSessionStopped): an AI call can take
|
|
197
|
+
* minutes, and the client's quit, or a hub's DELETE, waits for the end. At
|
|
198
|
+
* most MAX_CONCURRENT_FAILURE_ANALYSES run at once, and a session whose
|
|
199
|
+
* analysis is waiting or running gets that one (a session can end twice: a
|
|
200
|
+
* crash, then the client's delete).
|
|
201
|
+
*/
|
|
202
|
+
function explainSessionFailure(sessionId) {
|
|
203
|
+
const pending = analyses.get(sessionId);
|
|
204
|
+
if (pending)
|
|
205
|
+
return pending;
|
|
206
|
+
const analysis = (() => __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
yield analysisTurn();
|
|
208
|
+
try {
|
|
209
|
+
yield explain(sessionId);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
endAnalysisTurn();
|
|
213
|
+
}
|
|
214
|
+
}))().finally(() => analyses.delete(sessionId));
|
|
215
|
+
analyses.set(sessionId, analysis);
|
|
216
|
+
return analysis;
|
|
217
|
+
}
|
|
218
|
+
function explain(sessionId) {
|
|
219
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
220
|
+
try {
|
|
221
|
+
const { AI_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../services/AIService')));
|
|
222
|
+
if (!AI_SERVICE.isEnabled())
|
|
223
|
+
return;
|
|
224
|
+
const session = yield prisma_1.prisma.session.findUnique({ where: { id: sessionId } });
|
|
225
|
+
if (!session)
|
|
226
|
+
return;
|
|
227
|
+
const lastLogs = yield prisma_1.prisma.log.findMany({
|
|
228
|
+
where: { session_id: sessionId, log_type: 'DEVICE' },
|
|
229
|
+
take: 50,
|
|
230
|
+
orderBy: { timestamp: 'desc' },
|
|
231
|
+
});
|
|
232
|
+
const lastCommands = yield prisma_1.prisma.sessionLog.findMany({
|
|
233
|
+
where: { session_id: sessionId },
|
|
234
|
+
take: 10,
|
|
235
|
+
orderBy: { createdAt: 'desc' },
|
|
236
|
+
});
|
|
237
|
+
// Find the last screenshot in logs
|
|
238
|
+
const lastScreenshotLog = lastCommands.find((l) => l.screenshot !== null);
|
|
239
|
+
const aiAnalysis = yield AI_SERVICE.analyzeFailure({
|
|
240
|
+
sessionId,
|
|
241
|
+
failureReason: session.failure_reason || '',
|
|
242
|
+
commandLogs: lastCommands.map((c) => {
|
|
243
|
+
var _a;
|
|
244
|
+
return ({
|
|
245
|
+
command: c.command_name,
|
|
246
|
+
success: c.is_success,
|
|
247
|
+
response: (_a = c.response) === null || _a === void 0 ? void 0 : _a.slice(0, 500), // Truncate long responses
|
|
248
|
+
});
|
|
249
|
+
}),
|
|
250
|
+
deviceLogs: lastLogs.map((l) => l.message),
|
|
251
|
+
screenshotPath: (lastScreenshotLog === null || lastScreenshotLog === void 0 ? void 0 : lastScreenshotLog.screenshot) || undefined,
|
|
252
|
+
});
|
|
253
|
+
if (!aiAnalysis)
|
|
254
|
+
return;
|
|
255
|
+
yield prisma_1.prisma.session.update({
|
|
256
|
+
where: { id: sessionId },
|
|
257
|
+
data: { ai_analysis: aiAnalysis },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (aiErr) {
|
|
261
|
+
logger_1.default.warn(`[FailureAnalysis] AI Analysis failed for ${sessionId}: ${aiErr.message}`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SESSION_COMMAND_SUMMARY_FIELDS = void 0;
|
|
4
|
+
exports.sessionCommandSummary = sessionCommandSummary;
|
|
5
|
+
/**
|
|
6
|
+
* The fields of a command's record that leave its session: the live
|
|
7
|
+
* `session_command` event and the event log carry these and nothing else.
|
|
8
|
+
* Which command ran in which session, how it went and how it healed. The rest
|
|
9
|
+
* stays in the session's SessionLog, which goes with the session: the
|
|
10
|
+
* record's `body` is the command's arguments (the text `setValue` typed) and
|
|
11
|
+
* its `response` the command's answer (a page source, a screenshot's base64).
|
|
12
|
+
* Named one by one, so a field added to the record later goes nowhere until
|
|
13
|
+
* it is listed here.
|
|
14
|
+
*/
|
|
15
|
+
exports.SESSION_COMMAND_SUMMARY_FIELDS = [
|
|
16
|
+
'session_id',
|
|
17
|
+
'command_name',
|
|
18
|
+
'method',
|
|
19
|
+
'is_success',
|
|
20
|
+
'is_error',
|
|
21
|
+
'is_healed',
|
|
22
|
+
'original_strategy',
|
|
23
|
+
'original_selector',
|
|
24
|
+
'healed_strategy',
|
|
25
|
+
'healed_selector',
|
|
26
|
+
'healing_confidence',
|
|
27
|
+
'healing_tier',
|
|
28
|
+
'duration',
|
|
29
|
+
'span_id',
|
|
30
|
+
'trace_id',
|
|
31
|
+
];
|
|
32
|
+
function sessionCommandSummary(record) {
|
|
33
|
+
const summary = {};
|
|
34
|
+
for (const field of exports.SESSION_COMMAND_SUMMARY_FIELDS) {
|
|
35
|
+
if ((record === null || record === void 0 ? void 0 : record[field]) !== undefined)
|
|
36
|
+
summary[field] = record[field];
|
|
37
|
+
}
|
|
38
|
+
return summary;
|
|
39
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.appleFamilyOf = appleFamilyOf;
|
|
4
|
+
/** Whichever of the two words a name carries; iPad first, as "iPad" holds no "iPhone". */
|
|
5
|
+
function familyInName(name) {
|
|
6
|
+
const lower = (name !== null && name !== void 0 ? name : '').toLowerCase();
|
|
7
|
+
if (lower.includes('ipad'))
|
|
8
|
+
return 'ipad';
|
|
9
|
+
if (lower.includes('iphone'))
|
|
10
|
+
return 'iphone';
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether a phone is an iPhone or an iPad, or neither (an Android device, an
|
|
15
|
+
* Apple TV). Read from what the phone reports, in order of how far it can be
|
|
16
|
+
* trusted:
|
|
17
|
+
*
|
|
18
|
+
* 1. its model, lockdown's ProductType ("iPhone17,3", "iPad13,4");
|
|
19
|
+
* 2. its form factor, which a simulator gets from its own name and a real
|
|
20
|
+
* phone from its DeviceClass;
|
|
21
|
+
* 3. its marketing name, then its name, for a row written before the
|
|
22
|
+
* identity columns existed.
|
|
23
|
+
*
|
|
24
|
+
* A real phone's own name is whatever its owner typed ("QA-Phone-3", or
|
|
25
|
+
* "iPhone spare" for an iPad), so it is the last resort, not the first.
|
|
26
|
+
*/
|
|
27
|
+
function appleFamilyOf(device) {
|
|
28
|
+
var _a, _b, _c;
|
|
29
|
+
if (((_a = device.platform) !== null && _a !== void 0 ? _a : '').toLowerCase() !== 'ios')
|
|
30
|
+
return undefined;
|
|
31
|
+
const model = (_b = device.model) !== null && _b !== void 0 ? _b : '';
|
|
32
|
+
if (/^iPhone/i.test(model))
|
|
33
|
+
return 'iphone';
|
|
34
|
+
if (/^iPad/i.test(model))
|
|
35
|
+
return 'ipad';
|
|
36
|
+
if (device.formFactor === 'phone')
|
|
37
|
+
return 'iphone';
|
|
38
|
+
if (device.formFactor === 'tablet')
|
|
39
|
+
return 'ipad';
|
|
40
|
+
if (device.formFactor === 'tv')
|
|
41
|
+
return undefined;
|
|
42
|
+
return (_c = familyInName(device.marketingName)) !== null && _c !== void 0 ? _c : familyInName(device.name);
|
|
43
|
+
}
|
|
@@ -20,6 +20,10 @@ class XenonDatabase {
|
|
|
20
20
|
static get DeviceModel() {
|
|
21
21
|
return XenonDatabase.getDeviceModel();
|
|
22
22
|
}
|
|
23
|
+
/** Each phone's saved settings (deviceSettings.ts), by udid and host. */
|
|
24
|
+
static get DeviceSettingsModel() {
|
|
25
|
+
return XenonDatabase.getDeviceSettingsModel();
|
|
26
|
+
}
|
|
23
27
|
static get PendingSessionsModel() {
|
|
24
28
|
return XenonDatabase.getPendingSessionsModel();
|
|
25
29
|
}
|
|
@@ -49,6 +53,12 @@ class XenonDatabase {
|
|
|
49
53
|
return db.getCollection('devices') || db.addCollection('devices');
|
|
50
54
|
});
|
|
51
55
|
}
|
|
56
|
+
static getDeviceSettingsModel() {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
const db = yield XenonDatabase.getDB();
|
|
59
|
+
return db.getCollection('device-settings') || db.addCollection('device-settings');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
52
62
|
static getPendingSessionsModel() {
|
|
53
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
54
64
|
const db = yield XenonDatabase.getDB();
|
|
@@ -63,6 +73,7 @@ class XenonDatabase {
|
|
|
63
73
|
}
|
|
64
74
|
static initCollections(db) {
|
|
65
75
|
db.addCollection('devices');
|
|
76
|
+
db.addCollection('device-settings');
|
|
66
77
|
db.addCollection('pending-sessions');
|
|
67
78
|
db.addCollection('cliArgs');
|
|
68
79
|
}
|
|
@@ -81,8 +81,13 @@ function removeDevice(devices_1) {
|
|
|
81
81
|
const team = socket.hasScopedDashboard()
|
|
82
82
|
? yield typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).resolve(device.udid)
|
|
83
83
|
: undefined;
|
|
84
|
+
// The callers hold a udid and a host. The device_offline webhook promises
|
|
85
|
+
// its name and platform too, so read them while the row still exists.
|
|
86
|
+
const known = yield store
|
|
87
|
+
.findDevice({ udid: device.udid, host: device.host })
|
|
88
|
+
.catch(() => null);
|
|
84
89
|
yield store.removeDevices({ udid: device.udid, host: device.host }, options);
|
|
85
|
-
typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('device_offline', device);
|
|
90
|
+
typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('device_offline', Object.assign(Object.assign({}, known), device));
|
|
86
91
|
void socket.emitToDashboardForDevices('device_removed', device, deviceScope(device.udid, (team === null || team === void 0 ? void 0 : team.known) ? team.teamId : undefined));
|
|
87
92
|
}
|
|
88
93
|
});
|