@xenon-device-management/xenon 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
  37. package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
  39. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
  59. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  68. package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
  69. package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/openapi/sessions.yaml +7 -5
  78. package/lib/src/app/routers/auth.js +2 -1
  79. package/lib/src/app/routers/users.js +2 -1
  80. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  81. package/lib/src/dashboard/commandLogFields.js +42 -0
  82. package/lib/src/dashboard/event-manager.js +26 -154
  83. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  84. package/lib/src/dashboard/services/failureCategories.js +264 -0
  85. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  86. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  87. package/lib/src/device-utils.js +14 -6
  88. package/lib/src/gateway/forwardToNode.js +4 -11
  89. package/lib/src/gateway/hubSessionToken.js +27 -4
  90. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  91. package/lib/src/helpers/outboundProxy.js +219 -0
  92. package/lib/src/index.js +21 -0
  93. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  94. package/lib/src/interceptors/xenonScripts.js +28 -7
  95. package/lib/src/logger.js +3 -1
  96. package/lib/src/plugin.js +8 -3
  97. package/lib/src/scripts/database-check.js +3 -3
  98. package/lib/src/scripts/initialize-database.js +35 -33
  99. package/lib/src/scripts/run-migrations.js +1079 -27
  100. package/lib/src/services/ApiKeyService.js +16 -5
  101. package/lib/src/services/InterceptorService.js +6 -2
  102. package/lib/src/services/OrphanSweeper.js +6 -0
  103. package/lib/src/services/SelectorStateService.js +11 -7
  104. package/lib/src/services/SelectorVerificationJob.js +6 -2
  105. package/lib/src/services/SessionLifecycleService.js +32 -18
  106. package/lib/src/services/SocketClient.js +11 -0
  107. package/lib/src/services/SocketServer.js +361 -42
  108. package/lib/src/services/TeamService.js +5 -1
  109. package/lib/src/services/TracingService.js +171 -60
  110. package/lib/src/services/UserService.js +14 -3
  111. package/lib/src/services/UserSessionService.js +16 -4
  112. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  113. package/lib/src/services/identity/identityChanges.js +41 -0
  114. package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
  115. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  116. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  117. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  118. package/lib/src/services/token/JwtKeyService.js +8 -2
  119. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  120. package/lib/src/sessions/LocalSession.js +29 -1
  121. package/lib/src/sessions/RemoteSession.js +12 -2
  122. package/lib/src/sessions/SessionManager.js +5 -8
  123. package/lib/test/helpers/driver-log.js +38 -0
  124. package/lib/test/helpers/fake-http-proxy.js +106 -0
  125. package/lib/test/helpers/otlp-probe.js +107 -0
  126. package/lib/test/integration/androidDevices.spec.js +1 -1
  127. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  128. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  129. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  130. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  131. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  132. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  133. package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
  134. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  135. package/lib/test/unit/database-check.spec.js +1 -0
  136. package/lib/test/unit/device-utils.spec.js +13 -4
  137. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  138. package/lib/test/unit/failure-analysis.spec.js +22 -8
  139. package/lib/test/unit/failure-categories.spec.js +24 -22
  140. package/lib/test/unit/failure-categorization.spec.js +393 -0
  141. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  142. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  143. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  144. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  145. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  146. package/lib/test/unit/logcat-ws.spec.js +78 -37
  147. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  148. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  149. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  150. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  151. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  152. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  153. package/lib/test/unit/proxy-option.spec.js +358 -0
  154. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  155. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  156. package/lib/test/unit/run-migrations.test.js +864 -0
  157. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  158. package/lib/test/unit/selector-state-service.spec.js +72 -23
  159. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  160. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  161. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  162. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  163. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  164. package/lib/test/unit/session-device-logs.spec.js +11 -2
  165. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  166. package/lib/test/unit/session-log-indexes.spec.js +1 -1
  167. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  168. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  169. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  170. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  171. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  172. package/lib/tsconfig.tsbuildinfo +1 -1
  173. package/package.json +2 -1
  174. package/schema.json +11 -7
  175. package/scripts/dev/readme-screenshots.js +276 -23
  176. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  177. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  178. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  179. package/scripts/export-openapi.js +32 -0
  180. package/scripts/lib/openapi-for-site.js +79 -0
  181. package/lib/public/assets/index-BOxe43p1.js +0 -219
  182. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  183. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -20,6 +20,7 @@ const ws_1 = require("ws");
20
20
  const logger_1 = __importDefault(require("../../logger"));
21
21
  const hubSessionToken_1 = require("../../gateway/hubSessionToken");
22
22
  const forwardToNode_1 = require("../../gateway/forwardToNode");
23
+ const outboundProxy_1 = require("../../helpers/outboundProxy");
23
24
  const controlDevice_1 = require("../../middleware/controlDevice");
24
25
  const nodePhoneControl_1 = require("../routers/nodePhoneControl");
25
26
  /**
@@ -85,21 +86,86 @@ function openNodeSocket(request_1) {
85
86
  const query = new URLSearchParams(request.query);
86
87
  query.set('ticket', ticket);
87
88
  const url = `${origin.replace(/^http/, 'ws')}${base}/${path}?${query}`;
88
- const socket = new ws_1.WebSocket(url, { handshakeTimeout: timeoutMs, perMessageDeflate: false });
89
- yield new Promise((resolve, reject) => {
90
- socket.once('open', () => {
91
- // Paused until something reads it: the node sends H.264's config
92
- // packet and logcat's replay as soon as it opens, and a message with
93
- // no listener yet is lost. relaySocket resumes it.
94
- socket.pause();
95
- resolve();
96
- });
97
- socket.once('error', reject);
98
- });
89
+ const socket = yield connectToNode(url, origin, timeoutMs);
99
90
  logger_1.default.info(`[${udid}] ${path} socket opened on node ${origin}`);
100
91
  return socket;
101
92
  });
102
93
  }
94
+ /** An HTTP answer where the socket's upgrade was expected. */
95
+ class AnsweredWithoutUpgrade extends Error {
96
+ }
97
+ /** The nodes whose proxy refused a tunnel, said once each. */
98
+ const refusedTunnels = new Set();
99
+ /**
100
+ * The node's socket, open and paused, through the proxy its ticket took (the
101
+ * environment's, helpers/outboundProxy.ts): through 2.15 it went straight to
102
+ * the node whatever the environment said. A proxy that answers the tunnel
103
+ * with anything but the upgrade, as a stock Squid answers a CONNECT to any
104
+ * port but 443, is tried around: the socket goes straight to the node, as it
105
+ * always did. The node never saw the ticket, so it is still good.
106
+ */
107
+ function connectToNode(url, origin, timeoutMs) {
108
+ return __awaiter(this, void 0, void 0, function* () {
109
+ const agent = (0, outboundProxy_1.socketProxyAgentFor)(url);
110
+ if (!agent)
111
+ return openSocket(url, timeoutMs);
112
+ try {
113
+ return yield openSocket(url, timeoutMs, agent);
114
+ }
115
+ catch (err) {
116
+ if (!(err instanceof AnsweredWithoutUpgrade))
117
+ throw err;
118
+ if (!refusedTunnels.has(origin)) {
119
+ refusedTunnels.add(origin);
120
+ logger_1.default.warn(`The proxy refused to carry the live-preview and log sockets to node ${origin} ` +
121
+ `(${err.message}); they go straight to the node. List it in NO_PROXY to skip the proxy.`);
122
+ }
123
+ return openSocket(url, timeoutMs);
124
+ }
125
+ });
126
+ }
127
+ function openSocket(url, timeoutMs, agent) {
128
+ const socket = new ws_1.WebSocket(url, {
129
+ handshakeTimeout: timeoutMs,
130
+ perMessageDeflate: false,
131
+ agent,
132
+ });
133
+ return new Promise((resolve, reject) => {
134
+ let settled = false;
135
+ const fail = (err) => {
136
+ if (settled)
137
+ return;
138
+ settled = true;
139
+ clearTimeout(timer);
140
+ reject(err);
141
+ };
142
+ // handshakeTimeout starts once the request has a socket, which a proxy
143
+ // agent hands it only after the proxy answers: a proxy that never
144
+ // answered held this for ever.
145
+ const timer = setTimeout(() => {
146
+ fail(new Error(`the node's socket did not open in ${timeoutMs} ms`));
147
+ socket.terminate();
148
+ }, timeoutMs);
149
+ socket.once('open', () => {
150
+ if (settled)
151
+ return;
152
+ settled = true;
153
+ clearTimeout(timer);
154
+ // Paused until something reads it: the node sends H.264's config
155
+ // packet and logcat's replay as soon as it opens, and a message with
156
+ // no listener yet is lost. relaySocket resumes it.
157
+ socket.pause();
158
+ resolve(socket);
159
+ });
160
+ socket.once('unexpected-response', (req, res) => {
161
+ fail(new AnsweredWithoutUpgrade(`HTTP ${res.statusCode}`));
162
+ req.destroy();
163
+ });
164
+ // Kept for good: whatever errors aborting the upgrade raises has a
165
+ // listener, as it had before.
166
+ socket.on('error', fail);
167
+ });
168
+ }
103
169
  /** A close code a server may send. 1005, 1006 and 1015 only describe a close. */
104
170
  function isSendable(code) {
105
171
  return ((code >= 1000 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ||
@@ -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");
@@ -81,6 +80,7 @@ const SessionMetricsService_1 = require("../services/metrics/SessionMetricsServi
81
80
  const SessionDeviceLogs_1 = require("../services/logcat/SessionDeviceLogs");
82
81
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
83
82
  const sessionCommandSummary_1 = require("./sessionCommandSummary");
83
+ const commandLogFields_1 = require("./commandLogFields");
84
84
  const typedi_2 = require("typedi");
85
85
  /**
86
86
  * What a Session row keeps of the capabilities the driver returned. The row
@@ -100,14 +100,8 @@ function storedSessionCapabilities(sessionResponse) {
100
100
  let DashboardEventManager = class DashboardEventManager {
101
101
  constructor() {
102
102
  // private SCREENSHOT_FOR_COMMANDS = ['click', 'setUrl', 'setValue', 'performActions'];
103
- // Store syslog services for real iOS devices
104
- this.syslogServices = new Map();
105
- // Map session ID to UDID for cleanup
106
- this.sessionToUdid = new Map();
107
103
  // Map session ID to device info
108
104
  this.sessionToDevice = new Map();
109
- // Track last log line for each session (an iPhone's device logs)
110
- this.lastLogLine = new Map();
111
105
  // Track start time for each command to calculate duration
112
106
  this.commandStartTime = new Map();
113
107
  // Idempotency Guard: Prevents double-invocation of onSessionStopped by racing actors
@@ -115,7 +109,7 @@ let DashboardEventManager = class DashboardEventManager {
115
109
  }
116
110
  onSessionStarted(capabilities, session, device) {
117
111
  return __awaiter(this, void 0, void 0, function* () {
118
- var _a, _b;
112
+ var _a, _b, _c, _d, _e, _f;
119
113
  // Store device info for this session
120
114
  this.sessionToDevice.set(session.getId(), device);
121
115
  const createOptions = {
@@ -158,14 +152,24 @@ let DashboardEventManager = class DashboardEventManager {
158
152
  if (sampled) {
159
153
  metrics.start(Object.assign({ sessionId: session.getId(), device, capabilities: session.getCapabilities() }, (source ? { source } : {})));
160
154
  }
161
- // An Android phone's device log, from about when the phone was given to
162
- // the session, once its row exists (the lines point at it). Not waited
163
- // for: the phone's clock and log stream are read in the background.
164
- void typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).start({
165
- sessionId: session.getId(),
166
- device,
167
- since: session.allocatedAt,
168
- });
155
+ // The device's log, from about when the phone was given to the session,
156
+ // once its row exists (the lines point at it), unless the session turned
157
+ // it off: an Android phone's stream, or the log an iPhone's or
158
+ // simulator's driver captures. Not waited for: an Android phone's clock
159
+ // and log stream are read in the background.
160
+ const deviceLogs = typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs);
161
+ if (capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS] === false) {
162
+ void deviceLogs.noteOff(session.getId());
163
+ }
164
+ else {
165
+ void deviceLogs.start({
166
+ sessionId: session.getId(),
167
+ device,
168
+ since: session.allocatedAt,
169
+ driverLog: (_d = (_c = session).deviceLog) === null || _d === void 0 ? void 0 : _d.call(_c),
170
+ appUnderTest: (_f = (_e = session).appUnderTest) === null || _f === void 0 ? void 0 : _f.call(_e),
171
+ });
172
+ }
169
173
  // Emit session started event
170
174
  void typedi_1.Container.get(SocketServer_1.SocketServer).emitToDashboardForDevices(SocketEvents_1.SocketEvents.SESSION_STARTED, Object.assign(Object.assign({}, createData), { status: 'running', build_name: buildName }), { udid: device.udid });
171
175
  // Increment Metrics
@@ -185,9 +189,7 @@ let DashboardEventManager = class DashboardEventManager {
185
189
  return __awaiter(this, void 0, void 0, function* () {
186
190
  try {
187
191
  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);
192
+ const svc = isApple ? typedi_1.Container.get(IOSStreamService_1.default) : typedi_1.Container.get(AndroidStreamService_1.default);
191
193
  const status = svc.getStreamStatus(device.udid);
192
194
  if (status && status.viewerCount === 0) {
193
195
  yield svc.stopStream(device.udid);
@@ -231,20 +233,6 @@ let DashboardEventManager = class DashboardEventManager {
231
233
  logger_1.default.info(`Session ${sessionId} found in SESSION_MANAGER`);
232
234
  // iOS profiling is now handled in plugin.ts deleteSession() before the session is deleted
233
235
  // 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
236
  // Principal Resource Management: Unblock device immediately
249
237
  const device = this.sessionToDevice.get(sessionId);
250
238
  if (device) {
@@ -275,12 +263,6 @@ let DashboardEventManager = class DashboardEventManager {
275
263
  }
276
264
  // Final local cleanup to prevent state leaks
277
265
  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
266
  }
285
267
  else {
286
268
  logger_1.default.warn(`⚠️ Session ${sessionId} not found in SESSION_MANAGER`);
@@ -296,12 +278,6 @@ let DashboardEventManager = class DashboardEventManager {
296
278
  }
297
279
  finally {
298
280
  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
281
  }
306
282
  }
307
283
  const sessionEntry = yield (0, session_service_1.getSessionById)(sessionId);
@@ -447,10 +423,6 @@ let DashboardEventManager = class DashboardEventManager {
447
423
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
448
424
  if (session) {
449
425
  try {
450
- // Save device logs (only if driver is available)
451
- if (driver) {
452
- yield this.saveDeviceLogs(sessionId, driver);
453
- }
454
426
  // Save command log
455
427
  const parsedResponse = (0, helpers_1.safeParseJson)(responseBody);
456
428
  const isSuccessResponse = !((_a = parsedResponse === null || parsedResponse === void 0 ? void 0 : parsedResponse.value) === null || _a === void 0 ? void 0 : _a.error);
@@ -459,11 +431,13 @@ let DashboardEventManager = class DashboardEventManager {
459
431
  const traceId = tracingService.getTraceId(session.getId());
460
432
  const startTime = this.commandStartTime.get(`${sessionId}:${commandName}`);
461
433
  const duration = startTime ? Date.now() - startTime : null;
434
+ // A network-capture script keeps neither its arguments nor its answer.
435
+ const recorded = (0, commandLogFields_1.commandLogFields)(commandName, request.body, responseBody, isSuccessResponse);
462
436
  const logEntry = {
463
437
  session_id: session.getId(),
464
438
  command_name: commandName || null,
465
- body: JSON.stringify(request.body),
466
- response: responseBody,
439
+ body: recorded.body,
440
+ response: recorded.response,
467
441
  is_success: isSuccessResponse,
468
442
  is_error: !isSuccessResponse,
469
443
  method: request.method,
@@ -576,9 +550,7 @@ let DashboardEventManager = class DashboardEventManager {
576
550
  // If the (original_strategy, original_selector) row is in pending/resolved,
577
551
  // SelectorStateService.onHealRecorded will flip it back to active with
578
552
  // regression_count++ and emit SELECTOR_REGRESSED.
579
- if (logEntry.is_healed &&
580
- logEntry.original_strategy &&
581
- logEntry.original_selector) {
553
+ if (logEntry.is_healed && logEntry.original_strategy && logEntry.original_selector) {
582
554
  typedi_1.Container.get(SelectorStateService_1.SelectorStateService)
583
555
  .onHealRecorded({
584
556
  strategy: logEntry.original_strategy,
@@ -614,106 +586,6 @@ let DashboardEventManager = class DashboardEventManager {
614
586
  }
615
587
  return undefined;
616
588
  }
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
589
  // ─── Recording (free-form mosaic) events ─────────────────────────────────
718
590
  // Emitted by RecordingOrchestrator. Distinct namespace from SESSION_*; the
719
591
  // dashboard listens to both independently. Team-scoped: a group's started
@@ -45,80 +45,12 @@ 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.MAX_CONCURRENT_FAILURE_ANALYSES = exports.ANALYSIS_CATEGORIES = exports.ERROR_PATTERNS = void 0;
48
+ exports.MAX_CONCURRENT_FAILURE_ANALYSES = void 0;
49
49
  exports.categorizeSessionFailure = categorizeSessionFailure;
50
50
  exports.explainSessionFailure = explainSessionFailure;
51
51
  const prisma_1 = require("../../prisma");
52
52
  const logger_1 = __importDefault(require("../../logger"));
53
- /**
54
- * Each category and the texts that file a failure under it, tried in this
55
- * order against the failure reason and the last five failed commands. The
56
- * dashboard's runbooks describe a category by these, not by its name
57
- * (failure-categories.spec.ts).
58
- */
59
- exports.ERROR_PATTERNS = [
60
- {
61
- category: 'ELEMENT_NOT_FOUND',
62
- patterns: [
63
- 'NoSuchElementError',
64
- 'unable to find an element',
65
- 'An element could not be located',
66
- 'no such element',
67
- ],
68
- },
69
- {
70
- category: 'APP_CRASH',
71
- patterns: [
72
- 'Appium crashed',
73
- 'process has died',
74
- 'activity has died',
75
- 'The application has crashed',
76
- 'Application not responding',
77
- "org.openqa.selenium.WebDriverException: An unknown server-side error occurred while processing the command. Original error: The application under test with bundle id '.*' is not running or cannot be found",
78
- ],
79
- },
80
- {
81
- category: 'TIMEOUT',
82
- patterns: ['timeout', 'timed out', 'TimeoutException', 'New Command Timeout', 'socket hang up'],
83
- },
84
- {
85
- category: 'PERMISSION_BLOCKED',
86
- patterns: ['Permission alert', 'Security alert', 'Always Allow', 'Allow while using app'],
87
- },
88
- {
89
- category: 'WDA_FAILURE',
90
- patterns: [
91
- 'WebDriverAgent',
92
- 'WDA',
93
- 'xcodebuild failed',
94
- 'crashed with code',
95
- 'Unable to connect to WDA',
96
- 'Session does not exist',
97
- 'the session is not in a running state',
98
- ],
99
- },
100
- {
101
- category: 'XENON_COMMAND_FAILURE',
102
- patterns: ['Command failed', 'telemetry failed', 'interceptor error'],
103
- },
104
- {
105
- category: 'SYSTEM_OVERLOAD',
106
- patterns: ['OutOfMemory', 'MemoryLimit', 'thermal throttling', 'too many open files'],
107
- },
108
- ];
109
- /** What the analysis writes when no pattern matches. */
110
- const UNMATCHED = 'UNKNOWN';
111
- /**
112
- * Every category the analysis writes to a failed session's
113
- * `failure_category`, upper case as stored. The only other value the column
114
- * holds is `HUB_RESTART_CATEGORY` (SessionManager). The API reference's
115
- * examples and the dashboard's runbooks are held to these
116
- * (failure-categories.spec.ts).
117
- */
118
- exports.ANALYSIS_CATEGORIES = [
119
- ...exports.ERROR_PATTERNS.map((p) => p.category),
120
- UNMATCHED,
121
- ];
53
+ const failureCategories_1 = require("./failureCategories");
122
54
  /**
123
55
  * How many failed sessions' AI analyses run at once (explainSessionFailure);
124
56
  * the others wait their turn. They run after their sessions have ended, so
@@ -150,8 +82,9 @@ function endAnalysisTurn() {
150
82
  }
151
83
  /**
152
84
  * Files a failed session under a category (`failure_category`) by its failure
153
- * reason and its failed commands. Rules only, so it is quick: a session's end
154
- * waits for it. Never throws.
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.
155
88
  */
156
89
  function categorizeSessionFailure(sessionId) {
157
90
  return __awaiter(this, void 0, void 0, function* () {
@@ -164,22 +97,11 @@ function categorizeSessionFailure(sessionId) {
164
97
  });
165
98
  if (!session)
166
99
  return;
167
- const reason = session.failure_reason || '';
168
- const logs_text = session.SessionLog.map((l) => `${l.title} ${l.response}`).join(' ');
169
- const combined_text = (reason + ' ' + logs_text).toLowerCase();
170
- let identifiedCategory = UNMATCHED;
171
- for (const item of exports.ERROR_PATTERNS) {
172
- if (item.patterns.some((p) => new RegExp(p, 'i').test(combined_text))) {
173
- identifiedCategory = item.category;
174
- break;
175
- }
176
- }
177
- // Special case for App Crash - check Logcat/Syslog if available
178
- // For now we rely on the error response text which usually mentions "process has died"
179
- logger_1.default.info(`[FailureAnalysis] Session ${sessionId} identified as ${identifiedCategory}`);
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}`);
180
102
  yield prisma_1.prisma.session.update({
181
103
  where: { id: sessionId },
182
- data: { failure_category: identifiedCategory },
104
+ data: { failure_category: category },
183
105
  });
184
106
  }
185
107
  catch (err) {