@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.
- package/README.md +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- 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 +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- 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 +57 -72
- 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/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -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 +83 -56
- 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/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- 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/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- 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/selector-state-flow.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/XenonCapabilityManager.spec.js +23 -0
- 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-adb-retry.spec.js +112 -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/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/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -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 +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -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/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -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/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- 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/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- 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-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -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 +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -45,60 +45,48 @@ 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 = 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
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'Session does not exist',
|
|
89
|
-
'the session is not in a running state',
|
|
90
|
-
],
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
category: 'XENON_COMMAND_FAILURE',
|
|
94
|
-
patterns: ['Command failed', 'telemetry failed', 'interceptor error'],
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
category: 'SYSTEM_OVERLOAD',
|
|
98
|
-
patterns: ['OutOfMemory', 'MemoryLimit', 'thermal throttling', 'too many open files'],
|
|
99
|
-
},
|
|
100
|
-
];
|
|
101
|
-
function analyzeSessionFailure(sessionId) {
|
|
53
|
+
const failureCategories_1 = require("./failureCategories");
|
|
54
|
+
/**
|
|
55
|
+
* How many failed sessions' AI analyses run at once (explainSessionFailure);
|
|
56
|
+
* the others wait their turn. They run after their sessions have ended, so
|
|
57
|
+
* nothing else limits them: a broken build on a big lab ends hundreds of
|
|
58
|
+
* failed sessions in minutes, and each analysis holds a screenshot while it
|
|
59
|
+
* runs. Few at once also spares the provider's rate limit.
|
|
60
|
+
*/
|
|
61
|
+
exports.MAX_CONCURRENT_FAILURE_ANALYSES = 4;
|
|
62
|
+
/** Each session's analysis, waiting or running, so one session gets one. */
|
|
63
|
+
const analyses = new Map();
|
|
64
|
+
let analysesRunning = 0;
|
|
65
|
+
const analysesWaiting = [];
|
|
66
|
+
function analysisTurn() {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
if (analysesRunning < exports.MAX_CONCURRENT_FAILURE_ANALYSES) {
|
|
69
|
+
analysesRunning++;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// The turn is handed over by endAnalysisTurn, still counted as running.
|
|
73
|
+
yield new Promise((resolve) => analysesWaiting.push(resolve));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function endAnalysisTurn() {
|
|
77
|
+
const next = analysesWaiting.shift();
|
|
78
|
+
if (next)
|
|
79
|
+
next();
|
|
80
|
+
else
|
|
81
|
+
analysesRunning--;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Files a failed session under a category (`failure_category`) by its failure
|
|
85
|
+
* reason and its last five failed commands' errors (categorizeFailure, in
|
|
86
|
+
* failureCategories.ts). Rules only, so it is quick: a session's end waits for
|
|
87
|
+
* it. Never throws.
|
|
88
|
+
*/
|
|
89
|
+
function categorizeSessionFailure(sessionId) {
|
|
102
90
|
return __awaiter(this, void 0, void 0, function* () {
|
|
103
91
|
try {
|
|
104
92
|
const session = yield prisma_1.prisma.session.findUnique({
|
|
@@ -109,61 +97,11 @@ function analyzeSessionFailure(sessionId) {
|
|
|
109
97
|
});
|
|
110
98
|
if (!session)
|
|
111
99
|
return;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const combined_text = (reason + ' ' + logs_text).toLowerCase();
|
|
115
|
-
let identifiedCategory = 'UNKNOWN';
|
|
116
|
-
for (const item of ERROR_PATTERNS) {
|
|
117
|
-
if (item.patterns.some((p) => new RegExp(p, 'i').test(combined_text))) {
|
|
118
|
-
identifiedCategory = item.category;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// Special case for App Crash - check Logcat/Syslog if available
|
|
123
|
-
// For now we rely on the error response text which usually mentions "process has died"
|
|
124
|
-
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
|
-
}
|
|
100
|
+
const category = (0, failureCategories_1.categorizeFailure)(session.failure_reason || '', session.SessionLog.map((l) => (0, failureCategories_1.commandErrorOf)(l.response)));
|
|
101
|
+
logger_1.default.info(`[FailureAnalysis] Session ${sessionId} identified as ${category}`);
|
|
161
102
|
yield prisma_1.prisma.session.update({
|
|
162
103
|
where: { id: sessionId },
|
|
163
|
-
data: {
|
|
164
|
-
failure_category: identifiedCategory,
|
|
165
|
-
ai_analysis: aiAnalysis,
|
|
166
|
-
},
|
|
104
|
+
data: { failure_category: category },
|
|
167
105
|
});
|
|
168
106
|
}
|
|
169
107
|
catch (err) {
|
|
@@ -171,3 +109,78 @@ function analyzeSessionFailure(sessionId) {
|
|
|
171
109
|
}
|
|
172
110
|
});
|
|
173
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Asks the AI provider why a failed session failed, and saves the answer
|
|
114
|
+
* (`ai_analysis`). Only an answer is saved: with no provider, a rate limit, a
|
|
115
|
+
* time-out (FAILURE_ANALYSIS_TIMEOUT_MS) or a failed call nothing is written,
|
|
116
|
+
* and an analysis saved earlier stays. Never rejects.
|
|
117
|
+
*
|
|
118
|
+
* A session's end doesn't wait for it (onSessionStopped): an AI call can take
|
|
119
|
+
* minutes, and the client's quit, or a hub's DELETE, waits for the end. At
|
|
120
|
+
* most MAX_CONCURRENT_FAILURE_ANALYSES run at once, and a session whose
|
|
121
|
+
* analysis is waiting or running gets that one (a session can end twice: a
|
|
122
|
+
* crash, then the client's delete).
|
|
123
|
+
*/
|
|
124
|
+
function explainSessionFailure(sessionId) {
|
|
125
|
+
const pending = analyses.get(sessionId);
|
|
126
|
+
if (pending)
|
|
127
|
+
return pending;
|
|
128
|
+
const analysis = (() => __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
yield analysisTurn();
|
|
130
|
+
try {
|
|
131
|
+
yield explain(sessionId);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
endAnalysisTurn();
|
|
135
|
+
}
|
|
136
|
+
}))().finally(() => analyses.delete(sessionId));
|
|
137
|
+
analyses.set(sessionId, analysis);
|
|
138
|
+
return analysis;
|
|
139
|
+
}
|
|
140
|
+
function explain(sessionId) {
|
|
141
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
142
|
+
try {
|
|
143
|
+
const { AI_SERVICE } = yield Promise.resolve().then(() => __importStar(require('../../services/AIService')));
|
|
144
|
+
if (!AI_SERVICE.isEnabled())
|
|
145
|
+
return;
|
|
146
|
+
const session = yield prisma_1.prisma.session.findUnique({ where: { id: sessionId } });
|
|
147
|
+
if (!session)
|
|
148
|
+
return;
|
|
149
|
+
const lastLogs = yield prisma_1.prisma.log.findMany({
|
|
150
|
+
where: { session_id: sessionId, log_type: 'DEVICE' },
|
|
151
|
+
take: 50,
|
|
152
|
+
orderBy: { timestamp: 'desc' },
|
|
153
|
+
});
|
|
154
|
+
const lastCommands = yield prisma_1.prisma.sessionLog.findMany({
|
|
155
|
+
where: { session_id: sessionId },
|
|
156
|
+
take: 10,
|
|
157
|
+
orderBy: { createdAt: 'desc' },
|
|
158
|
+
});
|
|
159
|
+
// Find the last screenshot in logs
|
|
160
|
+
const lastScreenshotLog = lastCommands.find((l) => l.screenshot !== null);
|
|
161
|
+
const aiAnalysis = yield AI_SERVICE.analyzeFailure({
|
|
162
|
+
sessionId,
|
|
163
|
+
failureReason: session.failure_reason || '',
|
|
164
|
+
commandLogs: lastCommands.map((c) => {
|
|
165
|
+
var _a;
|
|
166
|
+
return ({
|
|
167
|
+
command: c.command_name,
|
|
168
|
+
success: c.is_success,
|
|
169
|
+
response: (_a = c.response) === null || _a === void 0 ? void 0 : _a.slice(0, 500), // Truncate long responses
|
|
170
|
+
});
|
|
171
|
+
}),
|
|
172
|
+
deviceLogs: lastLogs.map((l) => l.message),
|
|
173
|
+
screenshotPath: (lastScreenshotLog === null || lastScreenshotLog === void 0 ? void 0 : lastScreenshotLog.screenshot) || undefined,
|
|
174
|
+
});
|
|
175
|
+
if (!aiAnalysis)
|
|
176
|
+
return;
|
|
177
|
+
yield prisma_1.prisma.session.update({
|
|
178
|
+
where: { id: sessionId },
|
|
179
|
+
data: { ai_analysis: aiAnalysis },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (aiErr) {
|
|
183
|
+
logger_1.default.warn(`[FailureAnalysis] AI Analysis failed for ${sessionId}: ${aiErr.message}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What files a failed session under a `failure_category`, and how.
|
|
4
|
+
*
|
|
5
|
+
* The texts here are those the installed drivers, Appium and Xenon send
|
|
6
|
+
* (UiAutomator2 6.4.1 with its server 9.3.1, XCUITest 10.9.0 with
|
|
7
|
+
* WebDriverAgent 10.2.3, base-driver 10.x), plus a few a test may report in
|
|
8
|
+
* its own words (`xenon: setSessionStatus`), marked as such.
|
|
9
|
+
*
|
|
10
|
+
* Through 2.15 the rules were guesses:
|
|
11
|
+
* - none of App crash's texts existed anywhere;
|
|
12
|
+
* - a raw body's stack trace was matched too. Every UiAutomator2 error a hub
|
|
13
|
+
* records carries netty's `IdleStateHandler`, so one that matched no
|
|
14
|
+
* earlier rule was filed TIMEOUT;
|
|
15
|
+
* - the five failed commands were read as one text, so a failure the test
|
|
16
|
+
* had recovered from could decide the category.
|
|
17
|
+
*
|
|
18
|
+
* A rule matches a W3C error code exactly (`codes`), or a phrase anywhere in
|
|
19
|
+
* the reason or a command's error or message (`phrases`), ignoring case.
|
|
20
|
+
* Codes are only on the rows a hub records for a node's sessions: this
|
|
21
|
+
* server's own rows hold the message alone, so every category needs phrases
|
|
22
|
+
* too. Xenon's own messages are read first, by how they start, since they
|
|
23
|
+
* quote the test's selector (xenonOwnCategory). Rules are tried in this order.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.RETIRED_CATEGORIES = exports.ANALYSIS_CATEGORIES = exports.FAILURE_RULES = exports.UNMATCHED_CATEGORY = exports.HUB_RESTART_CATEGORY = void 0;
|
|
27
|
+
exports.xenonOwnCategory = xenonOwnCategory;
|
|
28
|
+
exports.commandErrorOf = commandErrorOf;
|
|
29
|
+
exports.categorizeFailure = categorizeFailure;
|
|
30
|
+
/** A session the server's restart or shutdown ended. SessionManager also writes it at boot. */
|
|
31
|
+
exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
|
|
32
|
+
/** What a failure that matches no rule is filed as. */
|
|
33
|
+
exports.UNMATCHED_CATEGORY = 'UNKNOWN';
|
|
34
|
+
exports.FAILURE_RULES = [
|
|
35
|
+
{
|
|
36
|
+
// ShutdownCoordinator's drain: the whole reason, never a phrase in one.
|
|
37
|
+
category: exports.HUB_RESTART_CATEGORY,
|
|
38
|
+
codes: ['Hub shutdown'],
|
|
39
|
+
phrases: [],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
// Before SESSION_LOST: a proxied connect that fails with EMFILE ("Could
|
|
43
|
+
// not proxy command ... connect EMFILE") is the server out of files, not
|
|
44
|
+
// the helper gone. Not a bare "EMFILE": that is a substring of a locator
|
|
45
|
+
// such as "systemFileList".
|
|
46
|
+
category: 'SYSTEM_OVERLOAD',
|
|
47
|
+
codes: [],
|
|
48
|
+
phrases: ['OutOfMemory', 'too many open files', 'connect EMFILE'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
// The phone's automation helper (WebDriverAgent, the UiAutomator2
|
|
52
|
+
// server, Chromedriver) or the session itself went away during the run.
|
|
53
|
+
category: 'SESSION_LOST',
|
|
54
|
+
codes: ['invalid session id'],
|
|
55
|
+
phrases: [
|
|
56
|
+
// base-driver's proxy, when the helper doesn't answer at all (socket
|
|
57
|
+
// hang up, ECONNREFUSED, a read time-out).
|
|
58
|
+
'Could not proxy command to the remote server',
|
|
59
|
+
// UiAutomator2: its server's instrumentation exited ("probably crashed"
|
|
60
|
+
// is the helper, not the app), or the phone went away.
|
|
61
|
+
'instrumentation process is not running',
|
|
62
|
+
// WebDriverAgent restarted and lost the session.
|
|
63
|
+
'Session does not exist',
|
|
64
|
+
'A session is either terminated or not started',
|
|
65
|
+
// Appium's own, when a command races the session's end.
|
|
66
|
+
'The session with id',
|
|
67
|
+
// Appium ended the session itself (onUnexpectedShutdown).
|
|
68
|
+
'unexpectedly shut down',
|
|
69
|
+
'Driver shut down unexpectedly',
|
|
70
|
+
'Chromedriver quit unexpectedly',
|
|
71
|
+
// XCUITest's commandTimeouts, which then ends the session.
|
|
72
|
+
'Appium did not get any response from',
|
|
73
|
+
// Xenon lost the session: OrphanSweeper, SessionHeartbeatService, and
|
|
74
|
+
// the hub's answer for a node it can't reach (forwardToNode).
|
|
75
|
+
'Session heartbeat timeout',
|
|
76
|
+
'Session terminal failure',
|
|
77
|
+
'The node no longer has this session',
|
|
78
|
+
'Node session status failed',
|
|
79
|
+
'could not reach the node running this session',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
category: 'APP_CRASH',
|
|
84
|
+
codes: [],
|
|
85
|
+
phrases: [
|
|
86
|
+
// WebDriverAgent, when the app under test is gone.
|
|
87
|
+
'is not running, possibly crashed',
|
|
88
|
+
// A test's own words.
|
|
89
|
+
'The application has crashed',
|
|
90
|
+
'Application not responding',
|
|
91
|
+
'process has died',
|
|
92
|
+
'activity has died',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
category: 'STALE_ELEMENT',
|
|
97
|
+
codes: ['stale element reference'],
|
|
98
|
+
phrases: [
|
|
99
|
+
// base-driver's and WebDriverAgent's default, and the UiAutomator2
|
|
100
|
+
// server's.
|
|
101
|
+
'no longer attached to the DOM',
|
|
102
|
+
// The UiAutomator2 server's cache. "not present in the cache or has
|
|
103
|
+
// expired" is answered as "no such element", hence this rule first.
|
|
104
|
+
'does not exist in DOM anymore',
|
|
105
|
+
'do not exist in DOM anymore',
|
|
106
|
+
'is not linked to the same object in DOM anymore',
|
|
107
|
+
'is not present in the cache or has expired',
|
|
108
|
+
'StaleObjectException',
|
|
109
|
+
// WebDriverAgent's cache and snapshots.
|
|
110
|
+
'expired from the internal cache',
|
|
111
|
+
'is not present in the current view anymore',
|
|
112
|
+
// Safari and web views on an iPhone.
|
|
113
|
+
'Element does not exist in cache',
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
// Specific phrases only: a bare "timeout" also matched Xenon's own
|
|
118
|
+
// autowait misses, a hung helper, heartbeat reasons and command titles.
|
|
119
|
+
category: 'TIMEOUT',
|
|
120
|
+
codes: ['timeout', 'script timeout'],
|
|
121
|
+
phrases: [
|
|
122
|
+
'New Command Timeout of',
|
|
123
|
+
'timed out due to inactivity',
|
|
124
|
+
// base-driver's defaults for `timeout` and `script timeout`.
|
|
125
|
+
'did not complete before its timeout expired',
|
|
126
|
+
'Timed out waiting for asynchronous script result',
|
|
127
|
+
// XCUITest and WebDriverAgent.
|
|
128
|
+
'waiting for XCTest to complete',
|
|
129
|
+
'cannot be launched within',
|
|
130
|
+
'Timed out while waiting until the screen gets locked',
|
|
131
|
+
'Did not receive any expected',
|
|
132
|
+
// UiAutomator2, when the app never lets the screen go idle (an endless
|
|
133
|
+
// animation, a video, a busy main thread) and it can't be read.
|
|
134
|
+
'hogging the main UI thread',
|
|
135
|
+
// A test's own words (Selenium's exception).
|
|
136
|
+
'TimeoutException',
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
category: 'ELEMENT_NOT_FOUND',
|
|
141
|
+
codes: ['no such element'],
|
|
142
|
+
phrases: [
|
|
143
|
+
'An element could not be located',
|
|
144
|
+
// WebDriverAgent's identifier and predicate lookups.
|
|
145
|
+
"didn't match any elements",
|
|
146
|
+
// A test's own words (NoSuchElementException).
|
|
147
|
+
'NoSuchElement',
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
// No native driver reports a blocking prompt: it shows up as Element not
|
|
152
|
+
// found. Only web pages on an iPhone, and a test's own words.
|
|
153
|
+
category: 'PERMISSION_BLOCKED',
|
|
154
|
+
codes: ['unexpected alert open'],
|
|
155
|
+
phrases: [
|
|
156
|
+
'A modal dialog was open',
|
|
157
|
+
'Permission alert',
|
|
158
|
+
'Security alert',
|
|
159
|
+
'Always Allow',
|
|
160
|
+
'Allow while using app',
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
/** Every category the failure analysis writes, upper case as stored. */
|
|
165
|
+
exports.ANALYSIS_CATEGORIES = [
|
|
166
|
+
...exports.FAILURE_RULES.map((r) => r.category),
|
|
167
|
+
exports.UNMATCHED_CATEGORY,
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* Categories no longer written, which sessions filed through 2.15 still hold.
|
|
171
|
+
* Their runbooks stay. WDA_FAILURE's live cases are SESSION_LOST now, and
|
|
172
|
+
* XENON_COMMAND_FAILURE matched nothing real but base-driver's default
|
|
173
|
+
* stale-element text, which is STALE_ELEMENT.
|
|
174
|
+
*/
|
|
175
|
+
exports.RETIRED_CATEGORIES = ['WDA_FAILURE', 'XENON_COMMAND_FAILURE'];
|
|
176
|
+
/**
|
|
177
|
+
* The category of a message Xenon itself throws, by how it starts. These
|
|
178
|
+
* quote the test's selector, which may hold any rule's phrase ("Allow
|
|
179
|
+
* notifications to be enabled"), so they are read before the rules.
|
|
180
|
+
*/
|
|
181
|
+
function xenonOwnCategory(text) {
|
|
182
|
+
const t = text.trim();
|
|
183
|
+
// CommandInterceptor's autowait: an element that never became enabled, or
|
|
184
|
+
// one that was never found.
|
|
185
|
+
if (t.startsWith('Autowait timed out after ')) {
|
|
186
|
+
return t.endsWith(' to be enabled') ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND';
|
|
187
|
+
}
|
|
188
|
+
// OmniVision's miss, and an element id Xenon doesn't hold.
|
|
189
|
+
if (t.startsWith('Xenon found nothing on the screen matching '))
|
|
190
|
+
return 'ELEMENT_NOT_FOUND';
|
|
191
|
+
if (t.startsWith('Xenon has no element '))
|
|
192
|
+
return 'ELEMENT_NOT_FOUND';
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The error and message of a failed command's recorded answer, and nothing
|
|
197
|
+
* else: never its stack trace, which names netty's IdleStateHandler on every
|
|
198
|
+
* UiAutomator2 error and the driver on every error.
|
|
199
|
+
*/
|
|
200
|
+
function commandErrorOf(response) {
|
|
201
|
+
var _a;
|
|
202
|
+
if (!response)
|
|
203
|
+
return {};
|
|
204
|
+
try {
|
|
205
|
+
const value = (_a = JSON.parse(response)) === null || _a === void 0 ? void 0 : _a.value;
|
|
206
|
+
if (!value || typeof value !== 'object')
|
|
207
|
+
return {};
|
|
208
|
+
return {
|
|
209
|
+
error: typeof value.error === 'string' ? value.error : undefined,
|
|
210
|
+
message: typeof value.message === 'string' ? value.message : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (_b) {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
218
|
+
function ruleFor(texts, codes) {
|
|
219
|
+
for (const t of texts) {
|
|
220
|
+
const own = xenonOwnCategory(t);
|
|
221
|
+
if (own)
|
|
222
|
+
return own;
|
|
223
|
+
}
|
|
224
|
+
const haystack = texts.map(norm);
|
|
225
|
+
const codeSet = new Set(codes.map(norm));
|
|
226
|
+
for (const rule of exports.FAILURE_RULES) {
|
|
227
|
+
if (rule.codes.some((c) => codeSet.has(norm(c))))
|
|
228
|
+
return rule.category;
|
|
229
|
+
if (rule.phrases.some((p) => haystack.some((t) => t.includes(norm(p)))))
|
|
230
|
+
return rule.category;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A failed session's category, from its failure reason and its failed
|
|
236
|
+
* commands (newest first).
|
|
237
|
+
*
|
|
238
|
+
* The failure that ended the session decides alone: the reason, read with the
|
|
239
|
+
* newest failed command when the reason was taken from it (for a session on
|
|
240
|
+
* a node's phone the reason is that command's bare code, "no such element",
|
|
241
|
+
* and its message says more). A reason no rule matches is UNKNOWN: an earlier
|
|
242
|
+
* failure the test recovered from, or a find it expected to fail, doesn't
|
|
243
|
+
* decide. Only a session with no reason at all is read from its failed
|
|
244
|
+
* commands, newest first.
|
|
245
|
+
*/
|
|
246
|
+
function categorizeFailure(reason, failedCommands) {
|
|
247
|
+
const said = (c) => [c.error, c.message].filter((s) => !!s);
|
|
248
|
+
const codesOf = (c) => (c.error ? [c.error] : []);
|
|
249
|
+
const r = reason.trim();
|
|
250
|
+
if (r) {
|
|
251
|
+
const newest = failedCommands[0];
|
|
252
|
+
const fromNewest = !!newest && said(newest).some((s) => norm(s) === norm(r));
|
|
253
|
+
const category = fromNewest
|
|
254
|
+
? ruleFor([r, ...said(newest)], codesOf(newest))
|
|
255
|
+
: ruleFor([r], [r]);
|
|
256
|
+
return category !== null && category !== void 0 ? category : exports.UNMATCHED_CATEGORY;
|
|
257
|
+
}
|
|
258
|
+
for (const c of failedCommands) {
|
|
259
|
+
const category = ruleFor(said(c), codesOf(c));
|
|
260
|
+
if (category)
|
|
261
|
+
return category;
|
|
262
|
+
}
|
|
263
|
+
return exports.UNMATCHED_CATEGORY;
|
|
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
|
+
}
|
|
@@ -8,6 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
11
22
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
23
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
24
|
};
|
|
@@ -438,7 +449,9 @@ class LokiHealEtalonStore {
|
|
|
438
449
|
const coll = yield this.getCollection();
|
|
439
450
|
const existing = coll.findOne({ selector: etalon.selector });
|
|
440
451
|
if (existing) {
|
|
441
|
-
|
|
452
|
+
// A fingerprint saved without a path keeps the one stored, as in Prisma.
|
|
453
|
+
const { path } = etalon, rest = __rest(etalon, ["path"]);
|
|
454
|
+
Object.assign(existing, rest, path == null ? {} : { path });
|
|
442
455
|
coll.update(existing);
|
|
443
456
|
}
|
|
444
457
|
else {
|
|
@@ -625,21 +625,24 @@ class PrismaHealEtalonStore {
|
|
|
625
625
|
get prisma() {
|
|
626
626
|
return typedi_1.Container.get(prisma_service_1.PrismaService).client;
|
|
627
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* The fingerprint, and its path when it has one. A fingerprint saved without
|
|
630
|
+
* a path (a heal whose element's path couldn't be worked out) keeps the one
|
|
631
|
+
* stored. Through 2.14 the path was never stored, so the Resilio tier,
|
|
632
|
+
* which needs it, never found anything.
|
|
633
|
+
*/
|
|
628
634
|
saveSignature(etalon) {
|
|
629
635
|
return __awaiter(this, void 0, void 0, function* () {
|
|
636
|
+
const path = etalon.path == null ? undefined : JSON.stringify(etalon.path);
|
|
630
637
|
yield this.prisma.locatorEtalon.upsert({
|
|
631
638
|
where: { selector: etalon.selector },
|
|
632
|
-
update: {
|
|
633
|
-
strategy: etalon.strategy,
|
|
634
|
-
attributes: JSON.stringify(etalon.attributes),
|
|
635
|
-
nodeName: etalon.nodeName,
|
|
636
|
-
lastSeen: new Date(),
|
|
637
|
-
},
|
|
639
|
+
update: Object.assign(Object.assign({ strategy: etalon.strategy, attributes: JSON.stringify(etalon.attributes), nodeName: etalon.nodeName }, (path !== undefined ? { path } : {})), { lastSeen: new Date() }),
|
|
638
640
|
create: {
|
|
639
641
|
selector: etalon.selector,
|
|
640
642
|
strategy: etalon.strategy,
|
|
641
643
|
attributes: JSON.stringify(etalon.attributes),
|
|
642
644
|
nodeName: etalon.nodeName,
|
|
645
|
+
path,
|
|
643
646
|
lastSeen: new Date(),
|
|
644
647
|
},
|
|
645
648
|
});
|
|
@@ -652,7 +655,16 @@ class PrismaHealEtalonStore {
|
|
|
652
655
|
});
|
|
653
656
|
if (!etalon)
|
|
654
657
|
return null;
|
|
655
|
-
|
|
658
|
+
let path = null;
|
|
659
|
+
if (etalon.path) {
|
|
660
|
+
try {
|
|
661
|
+
path = JSON.parse(etalon.path);
|
|
662
|
+
}
|
|
663
|
+
catch (_a) {
|
|
664
|
+
path = null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return Object.assign(Object.assign({}, etalon), { attributes: JSON.parse(etalon.attributes), path, lastSeen: etalon.lastSeen.getTime() });
|
|
656
668
|
});
|
|
657
669
|
}
|
|
658
670
|
}
|
|
@@ -27,6 +27,13 @@ const SETTINGS = {
|
|
|
27
27
|
buildCleanupMaxCount: 'number',
|
|
28
28
|
buildCleanupSchedule: 'text',
|
|
29
29
|
deleteBuildAssets: 'boolean',
|
|
30
|
+
aiProvider: 'text',
|
|
31
|
+
aiModel: 'text',
|
|
32
|
+
aiBaseUrl: 'text',
|
|
33
|
+
geminiModel: 'text',
|
|
34
|
+
openaiModel: 'text',
|
|
35
|
+
anthropicModel: 'text',
|
|
36
|
+
ollamaModel: 'text',
|
|
30
37
|
};
|
|
31
38
|
/**
|
|
32
39
|
* The lab settings the dashboard saves, one `WebConfig` row each, keyed by the
|