@xenon-device-management/xenon 2.15.0 → 2.17.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 +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- 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/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -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/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/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- 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/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- 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/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- 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/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 +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- 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/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -1736,6 +1736,82 @@ paths:
|
|
|
1736
1736
|
$ref: '#/components/responses/GridNodeUnknownSession'
|
|
1737
1737
|
'503':
|
|
1738
1738
|
$ref: '#/components/responses/GridNodeTokenUnavailable'
|
|
1739
|
+
/api/node/sessions/{sessionId}/device-logs:
|
|
1740
|
+
get:
|
|
1741
|
+
operationId: getGridNodeSessionDeviceLogs
|
|
1742
|
+
summary: Collect a session's device log lines from this node
|
|
1743
|
+
description: |
|
|
1744
|
+
Node only: a hub collects the device log lines a node has recorded
|
|
1745
|
+
for a session on one of its phones, as soon as the session is
|
|
1746
|
+
created, every 10 s after that, and once more when the session
|
|
1747
|
+
ends. The hub writes them as the session's Device logs. Only a
|
|
1748
|
+
server started with `hub` set has this route.
|
|
1749
|
+
|
|
1750
|
+
The node records each session its hub created, with the same limit
|
|
1751
|
+
as a session on the hub's own phone (10,000 lines, then up to 2,000
|
|
1752
|
+
more errors), and keeps the lines in memory until the hub collects
|
|
1753
|
+
them, an ended session's for 10 minutes. If no hub asks within 2
|
|
1754
|
+
minutes of the start, the node stops recording that session.
|
|
1755
|
+
|
|
1756
|
+
Each line has a number, `seq`, counting up from 1. `after` asks for
|
|
1757
|
+
the lines after that number, and the node drops the others, so a
|
|
1758
|
+
later call never returns them again. An answer holds at most 2,000
|
|
1759
|
+
lines; `more` says the node has others, to ask for at once. `state`
|
|
1760
|
+
is `recording`, `ended`, or `off` (the node doesn't record this
|
|
1761
|
+
session). `timestamp` is the line's moment by the node's clock.
|
|
1762
|
+
|
|
1763
|
+
It is answered before the login, with the same rule as
|
|
1764
|
+
`GET /api/node/sessions/{sessionId}`: no credential with
|
|
1765
|
+
per-command auth off or auth disabled, otherwise the hub's session
|
|
1766
|
+
token for that session. Every answer from a node carries
|
|
1767
|
+
`x-xenon-node-device-logs: 1`. It isn't rate-limited.
|
|
1768
|
+
tags:
|
|
1769
|
+
- Hub-Node
|
|
1770
|
+
security:
|
|
1771
|
+
- HubToken: []
|
|
1772
|
+
- {}
|
|
1773
|
+
parameters:
|
|
1774
|
+
- $ref: '#/components/parameters/GridNodeSessionId'
|
|
1775
|
+
- in: query
|
|
1776
|
+
name: after
|
|
1777
|
+
required: false
|
|
1778
|
+
description: A line number; only later lines are returned, and the others are dropped. Anything that isn't a plain number is ignored.
|
|
1779
|
+
schema:
|
|
1780
|
+
type: integer
|
|
1781
|
+
example: 2000
|
|
1782
|
+
responses:
|
|
1783
|
+
'200':
|
|
1784
|
+
description: The session's lines.
|
|
1785
|
+
headers:
|
|
1786
|
+
x-xenon-node-device-logs:
|
|
1787
|
+
description: Always `1` on a node.
|
|
1788
|
+
schema:
|
|
1789
|
+
type: string
|
|
1790
|
+
example: '1'
|
|
1791
|
+
content:
|
|
1792
|
+
application/json:
|
|
1793
|
+
schema:
|
|
1794
|
+
type: object
|
|
1795
|
+
properties:
|
|
1796
|
+
value:
|
|
1797
|
+
$ref: '#/components/schemas/GridNodeDeviceLogs'
|
|
1798
|
+
example:
|
|
1799
|
+
value:
|
|
1800
|
+
state: recording
|
|
1801
|
+
more: false
|
|
1802
|
+
lines:
|
|
1803
|
+
- seq: 2001
|
|
1804
|
+
message: '10-05 14:20:02.114 4127 4127 I ActivityManager: Start proc 4127:com.example.shop/u0a215'
|
|
1805
|
+
timestamp: 1791111602114
|
|
1806
|
+
- seq: 2002
|
|
1807
|
+
message: '10-05 14:20:02.391 4127 4127 E AndroidRuntime: FATAL EXCEPTION: main'
|
|
1808
|
+
timestamp: 1791111602391
|
|
1809
|
+
'401':
|
|
1810
|
+
$ref: '#/components/responses/GridNotANodeUnauthorized'
|
|
1811
|
+
'404':
|
|
1812
|
+
$ref: '#/components/responses/GridNodeUnknownSession'
|
|
1813
|
+
'503':
|
|
1814
|
+
$ref: '#/components/responses/GridNodeTokenUnavailable'
|
|
1739
1815
|
/api/status:
|
|
1740
1816
|
get:
|
|
1741
1817
|
operationId: getGridServerStatus
|
|
@@ -2304,6 +2380,32 @@ components:
|
|
|
2304
2380
|
type: string
|
|
2305
2381
|
nullable: true
|
|
2306
2382
|
description: The app the app figures are for (Android), else null.
|
|
2383
|
+
GridNodeDeviceLogs:
|
|
2384
|
+
type: object
|
|
2385
|
+
properties:
|
|
2386
|
+
state:
|
|
2387
|
+
type: string
|
|
2388
|
+
enum:
|
|
2389
|
+
- recording
|
|
2390
|
+
- ended
|
|
2391
|
+
- 'off'
|
|
2392
|
+
more:
|
|
2393
|
+
type: boolean
|
|
2394
|
+
description: The node holds more lines than this answer; ask again with the last `seq`.
|
|
2395
|
+
lines:
|
|
2396
|
+
type: array
|
|
2397
|
+
items:
|
|
2398
|
+
type: object
|
|
2399
|
+
properties:
|
|
2400
|
+
seq:
|
|
2401
|
+
type: integer
|
|
2402
|
+
description: The line's number in the session, from 1.
|
|
2403
|
+
message:
|
|
2404
|
+
type: string
|
|
2405
|
+
description: The line as the device printed it.
|
|
2406
|
+
timestamp:
|
|
2407
|
+
type: integer
|
|
2408
|
+
description: Epoch milliseconds, by the node's clock.
|
|
2307
2409
|
GridWebDriverError:
|
|
2308
2410
|
type: object
|
|
2309
2411
|
description: A WebDriver-style error, as Appium answers one.
|
|
@@ -2689,11 +2689,13 @@ components:
|
|
|
2689
2689
|
type: string
|
|
2690
2690
|
nullable: true
|
|
2691
2691
|
description: >-
|
|
2692
|
-
What kind of failure it was
|
|
2693
|
-
|
|
2694
|
-
`
|
|
2695
|
-
|
|
2696
|
-
|
|
2692
|
+
What kind of failure it was, read from the failure reason and the last failed
|
|
2693
|
+
commands' errors when the session fails: `SESSION_LOST`, `APP_CRASH`,
|
|
2694
|
+
`SYSTEM_OVERLOAD`, `STALE_ELEMENT`, `TIMEOUT`, `ELEMENT_NOT_FOUND`,
|
|
2695
|
+
`PERMISSION_BLOCKED` or `UNKNOWN`; or `HUB_RESTART` for a session the server's
|
|
2696
|
+
restart or shutdown ended. Sessions filed through 2.15 may also hold `WDA_FAILURE` or
|
|
2697
|
+
`XENON_COMMAND_FAILURE`, no longer written. Null for a session that hasn't failed,
|
|
2698
|
+
and for a failure recorded without that analysis.
|
|
2697
2699
|
example: ELEMENT_NOT_FOUND
|
|
2698
2700
|
is_profiling_available:
|
|
2699
2701
|
type: boolean
|
|
@@ -29,9 +29,10 @@ const mcpScopes_1 = require("../../services/token/mcpScopes");
|
|
|
29
29
|
const passwordResetMode_1 = require("../../services/passwordResetMode");
|
|
30
30
|
const passwordResetLink_1 = require("../../services/passwordResetLink");
|
|
31
31
|
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
32
|
+
const mcpTokenTtl_1 = require("../../services/token/mcpTokenTtl");
|
|
32
33
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
33
34
|
const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
|
|
34
|
-
const MCP_TTL_SEC =
|
|
35
|
+
const MCP_TTL_SEC = (0, mcpTokenTtl_1.mcpTokenTtlSec)();
|
|
35
36
|
const REST_TTL_SEC = 3600;
|
|
36
37
|
const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
|
|
37
38
|
function grantsScope(scopesCsv, scope) {
|
|
@@ -18,6 +18,7 @@ const express_1 = require("express");
|
|
|
18
18
|
const typedi_1 = require("typedi");
|
|
19
19
|
const crypto_1 = __importDefault(require("crypto"));
|
|
20
20
|
const UserService_1 = require("../../services/UserService");
|
|
21
|
+
const UserSessionService_1 = require("../../services/UserSessionService");
|
|
21
22
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
22
23
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
23
24
|
const userAuthorization_1 = require("../../services/identity/userAuthorization");
|
|
@@ -130,7 +131,7 @@ function usersRouter() {
|
|
|
130
131
|
});
|
|
131
132
|
if (status === 'INACTIVE') {
|
|
132
133
|
// updateUser sets status; we additionally revoke sessions.
|
|
133
|
-
yield
|
|
134
|
+
yield typedi_1.Container.get(UserSessionService_1.UserSessionService).revokeAllForUser(targetId);
|
|
134
135
|
}
|
|
135
136
|
res.json({
|
|
136
137
|
id: updated.id,
|
|
@@ -20,6 +20,7 @@ const ws_1 = require("ws");
|
|
|
20
20
|
const logger_1 = __importDefault(require("../../logger"));
|
|
21
21
|
const hubSessionToken_1 = require("../../gateway/hubSessionToken");
|
|
22
22
|
const forwardToNode_1 = require("../../gateway/forwardToNode");
|
|
23
|
+
const outboundProxy_1 = require("../../helpers/outboundProxy");
|
|
23
24
|
const controlDevice_1 = require("../../middleware/controlDevice");
|
|
24
25
|
const nodePhoneControl_1 = require("../routers/nodePhoneControl");
|
|
25
26
|
/**
|
|
@@ -85,21 +86,86 @@ function openNodeSocket(request_1) {
|
|
|
85
86
|
const query = new URLSearchParams(request.query);
|
|
86
87
|
query.set('ticket', ticket);
|
|
87
88
|
const url = `${origin.replace(/^http/, 'ws')}${base}/${path}?${query}`;
|
|
88
|
-
const socket =
|
|
89
|
-
yield new Promise((resolve, reject) => {
|
|
90
|
-
socket.once('open', () => {
|
|
91
|
-
// Paused until something reads it: the node sends H.264's config
|
|
92
|
-
// packet and logcat's replay as soon as it opens, and a message with
|
|
93
|
-
// no listener yet is lost. relaySocket resumes it.
|
|
94
|
-
socket.pause();
|
|
95
|
-
resolve();
|
|
96
|
-
});
|
|
97
|
-
socket.once('error', reject);
|
|
98
|
-
});
|
|
89
|
+
const socket = yield connectToNode(url, origin, timeoutMs);
|
|
99
90
|
logger_1.default.info(`[${udid}] ${path} socket opened on node ${origin}`);
|
|
100
91
|
return socket;
|
|
101
92
|
});
|
|
102
93
|
}
|
|
94
|
+
/** An HTTP answer where the socket's upgrade was expected. */
|
|
95
|
+
class AnsweredWithoutUpgrade extends Error {
|
|
96
|
+
}
|
|
97
|
+
/** The nodes whose proxy refused a tunnel, said once each. */
|
|
98
|
+
const refusedTunnels = new Set();
|
|
99
|
+
/**
|
|
100
|
+
* The node's socket, open and paused, through the proxy its ticket took (the
|
|
101
|
+
* environment's, helpers/outboundProxy.ts): through 2.15 it went straight to
|
|
102
|
+
* the node whatever the environment said. A proxy that answers the tunnel
|
|
103
|
+
* with anything but the upgrade, as a stock Squid answers a CONNECT to any
|
|
104
|
+
* port but 443, is tried around: the socket goes straight to the node, as it
|
|
105
|
+
* always did. The node never saw the ticket, so it is still good.
|
|
106
|
+
*/
|
|
107
|
+
function connectToNode(url, origin, timeoutMs) {
|
|
108
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
const agent = (0, outboundProxy_1.socketProxyAgentFor)(url);
|
|
110
|
+
if (!agent)
|
|
111
|
+
return openSocket(url, timeoutMs);
|
|
112
|
+
try {
|
|
113
|
+
return yield openSocket(url, timeoutMs, agent);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (!(err instanceof AnsweredWithoutUpgrade))
|
|
117
|
+
throw err;
|
|
118
|
+
if (!refusedTunnels.has(origin)) {
|
|
119
|
+
refusedTunnels.add(origin);
|
|
120
|
+
logger_1.default.warn(`The proxy refused to carry the live-preview and log sockets to node ${origin} ` +
|
|
121
|
+
`(${err.message}); they go straight to the node. List it in NO_PROXY to skip the proxy.`);
|
|
122
|
+
}
|
|
123
|
+
return openSocket(url, timeoutMs);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function openSocket(url, timeoutMs, agent) {
|
|
128
|
+
const socket = new ws_1.WebSocket(url, {
|
|
129
|
+
handshakeTimeout: timeoutMs,
|
|
130
|
+
perMessageDeflate: false,
|
|
131
|
+
agent,
|
|
132
|
+
});
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
let settled = false;
|
|
135
|
+
const fail = (err) => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
reject(err);
|
|
141
|
+
};
|
|
142
|
+
// handshakeTimeout starts once the request has a socket, which a proxy
|
|
143
|
+
// agent hands it only after the proxy answers: a proxy that never
|
|
144
|
+
// answered held this for ever.
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
fail(new Error(`the node's socket did not open in ${timeoutMs} ms`));
|
|
147
|
+
socket.terminate();
|
|
148
|
+
}, timeoutMs);
|
|
149
|
+
socket.once('open', () => {
|
|
150
|
+
if (settled)
|
|
151
|
+
return;
|
|
152
|
+
settled = true;
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
// Paused until something reads it: the node sends H.264's config
|
|
155
|
+
// packet and logcat's replay as soon as it opens, and a message with
|
|
156
|
+
// no listener yet is lost. relaySocket resumes it.
|
|
157
|
+
socket.pause();
|
|
158
|
+
resolve(socket);
|
|
159
|
+
});
|
|
160
|
+
socket.once('unexpected-response', (req, res) => {
|
|
161
|
+
fail(new AnsweredWithoutUpgrade(`HTTP ${res.statusCode}`));
|
|
162
|
+
req.destroy();
|
|
163
|
+
});
|
|
164
|
+
// Kept for good: whatever errors aborting the upgrade raises has a
|
|
165
|
+
// listener, as it had before.
|
|
166
|
+
socket.on('error', fail);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
103
169
|
/** A close code a server may send. 1005, 1006 and 1015 only describe a close. */
|
|
104
170
|
function isSendable(code) {
|
|
105
171
|
return ((code >= 1000 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ||
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NETWORK_CAPTURE_NOT_KEPT = void 0;
|
|
4
|
+
exports.commandLogFields = commandLogFields;
|
|
5
|
+
const xenonScripts_1 = require("../interceptors/xenonScripts");
|
|
6
|
+
/** What the command log keeps in place of a network-capture script's arguments or answer. */
|
|
7
|
+
exports.NETWORK_CAPTURE_NOT_KEPT = 'Not kept: network capture is shown to admins only';
|
|
8
|
+
/**
|
|
9
|
+
* The `body` and `response` the command log (SessionLog) keeps for a command.
|
|
10
|
+
* The log is read by everyone who can see the session (`session_log`, bug
|
|
11
|
+
* reports), so a network-capture script (NETWORK_CAPTURE_SCRIPTS) keeps
|
|
12
|
+
* neither its arguments nor its answer: they are the capture itself (a HAR,
|
|
13
|
+
* the requests' headers and bodies, the mocks), which REST shows to admins
|
|
14
|
+
* only.
|
|
15
|
+
* The script's name stays, and a failed call keeps its error, which says why
|
|
16
|
+
* and carries no capture.
|
|
17
|
+
*
|
|
18
|
+
* CommandInterceptor answers these scripts before its post-command hooks, so
|
|
19
|
+
* a session on this server's own phone records one only when it fails. A hub
|
|
20
|
+
* with its dashboard on records every command it forwards to a node, and
|
|
21
|
+
* through 2.15 kept a test's `xenon: exportHar` whole. Failure analysis sends
|
|
22
|
+
* a session's last commands' answers to the AI provider, so they went there
|
|
23
|
+
* too.
|
|
24
|
+
*
|
|
25
|
+
* `requestBody` is the client's W3C body (`{ script, args }`) or, from
|
|
26
|
+
* CommandInterceptor, the command's arguments (`[script, args]`).
|
|
27
|
+
*/
|
|
28
|
+
function commandLogFields(commandName, requestBody, responseBody, isSuccess) {
|
|
29
|
+
const body = JSON.stringify(requestBody);
|
|
30
|
+
if (commandName !== 'execute')
|
|
31
|
+
return { body, response: responseBody };
|
|
32
|
+
const fromCommand = Array.isArray(requestBody);
|
|
33
|
+
const script = fromCommand
|
|
34
|
+
? requestBody[0]
|
|
35
|
+
: requestBody === null || requestBody === void 0 ? void 0 : requestBody.script;
|
|
36
|
+
if (!(0, xenonScripts_1.isNetworkCaptureScript)(script))
|
|
37
|
+
return { body, response: responseBody };
|
|
38
|
+
return {
|
|
39
|
+
body: JSON.stringify(fromCommand ? [script, exports.NETWORK_CAPTURE_NOT_KEPT] : { script, args: exports.NETWORK_CAPTURE_NOT_KEPT }),
|
|
40
|
+
response: isSuccess ? JSON.stringify({ value: exports.NETWORK_CAPTURE_NOT_KEPT }) : responseBody,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -63,7 +63,6 @@ const helpers_1 = require("../helpers");
|
|
|
63
63
|
const asset_manager_1 = require("./asset-manager");
|
|
64
64
|
const commands_1 = require("./commands");
|
|
65
65
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
66
|
-
const appium_ios_device_1 = require("appium-ios-device");
|
|
67
66
|
const config_1 = require("../config");
|
|
68
67
|
const helpers_2 = require("../helpers");
|
|
69
68
|
const typedi_1 = require("typedi");
|
|
@@ -80,7 +79,9 @@ const NotificationService_1 = require("../services/NotificationService");
|
|
|
80
79
|
const SessionMetricsService_1 = require("../services/metrics/SessionMetricsService");
|
|
81
80
|
const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
|
|
82
81
|
const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
|
|
82
|
+
const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
|
|
83
83
|
const sessionCommandSummary_1 = require("./sessionCommandSummary");
|
|
84
|
+
const commandLogFields_1 = require("./commandLogFields");
|
|
84
85
|
const typedi_2 = require("typedi");
|
|
85
86
|
/**
|
|
86
87
|
* What a Session row keeps of the capabilities the driver returned. The row
|
|
@@ -100,14 +101,8 @@ function storedSessionCapabilities(sessionResponse) {
|
|
|
100
101
|
let DashboardEventManager = class DashboardEventManager {
|
|
101
102
|
constructor() {
|
|
102
103
|
// private SCREENSHOT_FOR_COMMANDS = ['click', 'setUrl', 'setValue', 'performActions'];
|
|
103
|
-
// Store syslog services for real iOS devices
|
|
104
|
-
this.syslogServices = new Map();
|
|
105
|
-
// Map session ID to UDID for cleanup
|
|
106
|
-
this.sessionToUdid = new Map();
|
|
107
104
|
// Map session ID to device info
|
|
108
105
|
this.sessionToDevice = new Map();
|
|
109
|
-
// Track last log line for each session (an iPhone's device logs)
|
|
110
|
-
this.lastLogLine = new Map();
|
|
111
106
|
// Track start time for each command to calculate duration
|
|
112
107
|
this.commandStartTime = new Map();
|
|
113
108
|
// Idempotency Guard: Prevents double-invocation of onSessionStopped by racing actors
|
|
@@ -115,7 +110,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
115
110
|
}
|
|
116
111
|
onSessionStarted(capabilities, session, device) {
|
|
117
112
|
return __awaiter(this, void 0, void 0, function* () {
|
|
118
|
-
var _a, _b;
|
|
113
|
+
var _a, _b, _c, _d, _e, _f;
|
|
119
114
|
// Store device info for this session
|
|
120
115
|
this.sessionToDevice.set(session.getId(), device);
|
|
121
116
|
const createOptions = {
|
|
@@ -158,14 +153,20 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
158
153
|
if (sampled) {
|
|
159
154
|
metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
|
|
160
155
|
}
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
// The device's log, from about when the phone was given to the session,
|
|
157
|
+
// once its row exists (the lines point at it), unless the session turned
|
|
158
|
+
// it off: an Android phone's stream, or the log an iPhone's or
|
|
159
|
+
// simulator's driver captures. Not waited for: an Android phone's clock
|
|
160
|
+
// and log stream are read in the background. A node's phone's lines are
|
|
161
|
+
// collected from the node, through the session.
|
|
162
|
+
const deviceLogs = typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs);
|
|
163
|
+
if (capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] === false) {
|
|
164
|
+
void deviceLogs.noteOff(session.getId());
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const deviceLogSource = (0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(session);
|
|
168
|
+
void deviceLogs.start(Object.assign({ sessionId: session.getId(), device, since: session.allocatedAt, driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c), appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e) }, (deviceLogSource ? { source: deviceLogSource } : {})));
|
|
169
|
+
}
|
|
169
170
|
// Emit session started event
|
|
170
171
|
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 });
|
|
171
172
|
// Increment Metrics
|
|
@@ -185,9 +186,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
185
186
|
return __awaiter(this, void 0, void 0, function* () {
|
|
186
187
|
try {
|
|
187
188
|
const isApple = device.platform === 'ios' || device.platform === 'tvos';
|
|
188
|
-
const svc = isApple
|
|
189
|
-
? typedi_1.Container.get(IOSStreamService_1.default)
|
|
190
|
-
: typedi_1.Container.get(AndroidStreamService_1.default);
|
|
189
|
+
const svc = isApple ? typedi_1.Container.get(IOSStreamService_1.default) : typedi_1.Container.get(AndroidStreamService_1.default);
|
|
191
190
|
const status = svc.getStreamStatus(device.udid);
|
|
192
191
|
if (status && status.viewerCount === 0) {
|
|
193
192
|
yield svc.stopStream(device.udid);
|
|
@@ -231,20 +230,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
231
230
|
logger_1.default.info(`Session ${sessionId} found in SESSION_MANAGER`);
|
|
232
231
|
// iOS profiling is now handled in plugin.ts deleteSession() before the session is deleted
|
|
233
232
|
// This ensures we can call mobile: stopPerfRecord while the driver is still alive
|
|
234
|
-
// Clean up syslog service for real iOS devices
|
|
235
|
-
const udid = this.sessionToUdid.get(sessionId);
|
|
236
|
-
if (udid) {
|
|
237
|
-
try {
|
|
238
|
-
this.syslogServices.delete(udid);
|
|
239
|
-
this.sessionToUdid.delete(sessionId);
|
|
240
|
-
logger_1.default.info(`Cleaned up syslog service for device ${udid}`);
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
logger_1.default.debug(`Error cleaning up syslog service info: ${err}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
// Clean up last log line tracking
|
|
247
|
-
this.lastLogLine.delete(sessionId);
|
|
248
233
|
// Principal Resource Management: Unblock device immediately
|
|
249
234
|
const device = this.sessionToDevice.get(sessionId);
|
|
250
235
|
if (device) {
|
|
@@ -275,12 +260,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
275
260
|
}
|
|
276
261
|
// Final local cleanup to prevent state leaks
|
|
277
262
|
this.sessionToDevice.delete(sessionId);
|
|
278
|
-
this.lastLogLine.delete(sessionId);
|
|
279
|
-
const orphanUdid = this.sessionToUdid.get(sessionId);
|
|
280
|
-
if (orphanUdid) {
|
|
281
|
-
this.sessionToUdid.delete(sessionId);
|
|
282
|
-
this.syslogServices.delete(orphanUdid);
|
|
283
|
-
}
|
|
284
263
|
}
|
|
285
264
|
else {
|
|
286
265
|
logger_1.default.warn(`⚠️ Session ${sessionId} not found in SESSION_MANAGER`);
|
|
@@ -296,12 +275,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
296
275
|
}
|
|
297
276
|
finally {
|
|
298
277
|
this.sessionToDevice.delete(sessionId);
|
|
299
|
-
this.lastLogLine.delete(sessionId);
|
|
300
|
-
const orphanUdid = this.sessionToUdid.get(sessionId);
|
|
301
|
-
if (orphanUdid) {
|
|
302
|
-
this.sessionToUdid.delete(sessionId);
|
|
303
|
-
this.syslogServices.delete(orphanUdid);
|
|
304
|
-
}
|
|
305
278
|
}
|
|
306
279
|
}
|
|
307
280
|
const sessionEntry = yield (0, session_service_1.getSessionById)(sessionId);
|
|
@@ -447,10 +420,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
447
420
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
448
421
|
if (session) {
|
|
449
422
|
try {
|
|
450
|
-
// Save device logs (only if driver is available)
|
|
451
|
-
if (driver) {
|
|
452
|
-
yield this.saveDeviceLogs(sessionId, driver);
|
|
453
|
-
}
|
|
454
423
|
// Save command log
|
|
455
424
|
const parsedResponse = (0, helpers_1.safeParseJson)(responseBody);
|
|
456
425
|
const isSuccessResponse = !((_a = parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse.value) === null || _a === void 0 ? void 0 : _a.error);
|
|
@@ -459,11 +428,13 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
459
428
|
const traceId = tracingService.getTraceId(session.getId());
|
|
460
429
|
const startTime = this.commandStartTime.get(`${sessionId}:${commandName}`);
|
|
461
430
|
const duration = startTime ? Date.now() - startTime : null;
|
|
431
|
+
// A network-capture script keeps neither its arguments nor its answer.
|
|
432
|
+
const recorded = (0, commandLogFields_1.commandLogFields)(commandName, request.body, responseBody, isSuccessResponse);
|
|
462
433
|
const logEntry = {
|
|
463
434
|
session_id: session.getId(),
|
|
464
435
|
command_name: commandName || null,
|
|
465
|
-
body:
|
|
466
|
-
response:
|
|
436
|
+
body: recorded.body,
|
|
437
|
+
response: recorded.response,
|
|
467
438
|
is_success: isSuccessResponse,
|
|
468
439
|
is_error: !isSuccessResponse,
|
|
469
440
|
method: request.method,
|
|
@@ -576,9 +547,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
576
547
|
// If the (original_strategy, original_selector) row is in pending/resolved,
|
|
577
548
|
// SelectorStateService.onHealRecorded will flip it back to active with
|
|
578
549
|
// regression_count++ and emit SELECTOR_REGRESSED.
|
|
579
|
-
if (logEntry.is_healed &&
|
|
580
|
-
logEntry.original_strategy &&
|
|
581
|
-
logEntry.original_selector) {
|
|
550
|
+
if (logEntry.is_healed && logEntry.original_strategy && logEntry.original_selector) {
|
|
582
551
|
typedi_1.Container.get(SelectorStateService_1.SelectorStateService)
|
|
583
552
|
.onHealRecorded({
|
|
584
553
|
strategy: logEntry.original_strategy,
|
|
@@ -614,106 +583,6 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
614
583
|
}
|
|
615
584
|
return undefined;
|
|
616
585
|
}
|
|
617
|
-
getDeviceLogs(driver, sessionId) {
|
|
618
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
619
|
-
try {
|
|
620
|
-
if (!driver || !driver.caps || !driver.caps.automationName) {
|
|
621
|
-
return [];
|
|
622
|
-
}
|
|
623
|
-
const automationName = driver.caps.automationName.toLowerCase();
|
|
624
|
-
// Get device info for this session
|
|
625
|
-
const device = this.sessionToDevice.get(sessionId);
|
|
626
|
-
// Use Appium driver's extractLogs method for proper log extraction
|
|
627
|
-
if (automationName === 'xcuitest' && typeof driver.extractLogs === 'function') {
|
|
628
|
-
try {
|
|
629
|
-
// Check if this is a real iOS device using device.realDevice property
|
|
630
|
-
const isRealDevice = (device === null || device === void 0 ? void 0 : device.realDevice) === true;
|
|
631
|
-
logger_1.default.debug(`Device info for session ${sessionId} - isRealDevice: ${isRealDevice}, deviceType: ${device === null || device === void 0 ? void 0 : device.deviceType}, UDID: ${device === null || device === void 0 ? void 0 : device.udid}`);
|
|
632
|
-
if (isRealDevice) {
|
|
633
|
-
// For real iOS devices, use appium-ios-device syslog service
|
|
634
|
-
const udid = driver.caps.udid;
|
|
635
|
-
// Track session to UDID mapping for cleanup
|
|
636
|
-
this.sessionToUdid.set(sessionId, udid);
|
|
637
|
-
// If we don't have a syslog service for this device yet, start one
|
|
638
|
-
if (!this.syslogServices.has(udid)) {
|
|
639
|
-
try {
|
|
640
|
-
const syslogService = yield appium_ios_device_1.services.startSyslogService(udid);
|
|
641
|
-
const logs = [];
|
|
642
|
-
// Start listening to logs and buffer them
|
|
643
|
-
syslogService.start((logLine) => {
|
|
644
|
-
logs.push(logLine);
|
|
645
|
-
});
|
|
646
|
-
// Store the service and logs
|
|
647
|
-
this.syslogServices.set(udid, { service: syslogService, logs });
|
|
648
|
-
logger_1.default.info(`Started syslog service for real iOS device ${udid}`);
|
|
649
|
-
}
|
|
650
|
-
catch (err) {
|
|
651
|
-
logger_1.default.debug(`Could not start syslog service for real device: ${err}`);
|
|
652
|
-
return [];
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
// Return the buffered logs
|
|
656
|
-
const deviceData = this.syslogServices.get(udid);
|
|
657
|
-
if (deviceData && deviceData.logs) {
|
|
658
|
-
const currentLogs = [...deviceData.logs];
|
|
659
|
-
// Clear the buffer after retrieving
|
|
660
|
-
deviceData.logs.length = 0;
|
|
661
|
-
return currentLogs.map((logLine) => ({ message: logLine, timestamp: Date.now() }));
|
|
662
|
-
}
|
|
663
|
-
return [];
|
|
664
|
-
}
|
|
665
|
-
// For iOS simulators, extract syslog
|
|
666
|
-
const logs = yield driver.extractLogs('syslog');
|
|
667
|
-
return Array.isArray(logs) ? logs : [];
|
|
668
|
-
}
|
|
669
|
-
catch (err) {
|
|
670
|
-
logger_1.default.debug(`Could not extract syslog: ${err}`);
|
|
671
|
-
return [];
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
// An Android phone's lines are recorded for the whole session by
|
|
675
|
-
// SessionDeviceLogs, not per command.
|
|
676
|
-
return [];
|
|
677
|
-
}
|
|
678
|
-
catch (error) {
|
|
679
|
-
logger_1.default.error(`Error getting device logs: ${error}`);
|
|
680
|
-
return [];
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
saveDeviceLogs(sessionId, driver) {
|
|
685
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
686
|
-
try {
|
|
687
|
-
const logs = yield this.getDeviceLogs(driver, sessionId);
|
|
688
|
-
if (!logs || logs.length === 0) {
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const lastLine = this.lastLogLine.get(sessionId) || 0;
|
|
692
|
-
const newLogs = logs.slice(lastLine);
|
|
693
|
-
if (newLogs.length === 0) {
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
this.lastLogLine.set(sessionId, logs.length);
|
|
697
|
-
// Save device logs to database
|
|
698
|
-
const logEntries = newLogs.map((logItem) => ({
|
|
699
|
-
session_id: sessionId,
|
|
700
|
-
log_type: 'DEVICE',
|
|
701
|
-
message: typeof logItem === 'string' ? logItem : logItem.message || JSON.stringify(logItem),
|
|
702
|
-
timestamp: logItem.timestamp ? new Date(logItem.timestamp) : new Date(),
|
|
703
|
-
}));
|
|
704
|
-
if (logEntries.length > 0) {
|
|
705
|
-
for (const logEntry of logEntries) {
|
|
706
|
-
yield prisma_1.prisma.log.create({
|
|
707
|
-
data: logEntry,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch (error) {
|
|
713
|
-
logger_1.default.error(`Error saving device logs: ${error.message}`);
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
586
|
// ─── Recording (free-form mosaic) events ─────────────────────────────────
|
|
718
587
|
// Emitted by RecordingOrchestrator. Distinct namespace from SESSION_*; the
|
|
719
588
|
// dashboard listens to both independently. Team-scoped: a group's started
|