@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.
Files changed (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.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/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -59,6 +59,7 @@ const jose = __importStar(require("jose"));
59
59
  const logger_1 = __importDefault(require("../logger"));
60
60
  const JwtKeyService_1 = require("../services/token/JwtKeyService");
61
61
  const singleUseLedger_1 = require("../services/token/singleUseLedger");
62
+ const outboundProxy_1 = require("../helpers/outboundProxy");
62
63
  /**
63
64
  * The credential a hub's calls to a node carry.
64
65
  *
@@ -221,6 +222,22 @@ exports.HubSessionTokenIssuer = HubSessionTokenIssuer = __decorate([
221
222
  function hubJwksUrl(hub) {
222
223
  return new URL('/xenon/api/auth/jwks.json', hub);
223
224
  }
225
+ /**
226
+ * A key lookup that fails after `ms`. jose's own timeout starts once its
227
+ * request has a socket, which a proxy agent hands it only after the proxy
228
+ * answers, so a proxy that never answers held every token check.
229
+ */
230
+ function withinTime(getKey, ms) {
231
+ return (header, token) => {
232
+ let timer;
233
+ return Promise.race([
234
+ Promise.resolve(getKey(header, token)),
235
+ new Promise((_, reject) => {
236
+ timer = setTimeout(() => reject(new Error(`the hub's keys did not arrive in ${ms} ms`)), ms);
237
+ }),
238
+ ]).finally(() => clearTimeout(timer));
239
+ };
240
+ }
224
241
  /** The node could not check a hub token (the hub's keys could not be fetched). */
225
242
  class HubTokenUnavailableError extends Error {
226
243
  }
@@ -251,15 +268,21 @@ function isVerdict(err) {
251
268
  * a token names a key it does not have (a hub that rotated its key).
252
269
  */
253
270
  class HubSessionTokenVerifier {
254
- constructor(hubUrl, keys) {
271
+ constructor(hubUrl, keys, opts = {}) {
272
+ var _a;
255
273
  /** The create tokens already taken, by id, until each one expires. */
256
274
  this.usedCreates = new singleUseLedger_1.SingleUseLedger(exports.HUB_CREATE_TTL_SECONDS);
275
+ const jwksUrl = hubJwksUrl(hubUrl);
276
+ const timeoutMs = (_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : 5000;
257
277
  this.keys =
258
- keys !== null && keys !== void 0 ? keys : jose.createRemoteJWKSet(hubJwksUrl(hubUrl), {
259
- timeoutDuration: 5000,
278
+ keys !== null && keys !== void 0 ? keys : withinTime(jose.createRemoteJWKSet(jwksUrl, {
279
+ timeoutDuration: timeoutMs,
260
280
  cooldownDuration: 30000,
261
281
  cacheMaxAge: 10 * 60000,
262
- });
282
+ // The proxy the node's axios calls to its hub take; jose's own
283
+ // fetch takes none.
284
+ agent: (0, outboundProxy_1.proxyAgentFor)(jwksUrl),
285
+ }), timeoutMs);
263
286
  }
264
287
  verify(token, sessionId) {
265
288
  return __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OlderNodes = exports.OLDER_NODE_RECHECK_MS = void 0;
4
+ exports.readNodeReply = readNodeReply;
5
+ /**
6
+ * The node's answer at a route whose answers carry `header`. `answerOf` reads
7
+ * a 200's `value`, or returns null when it isn't one.
8
+ */
9
+ function readNodeReply(header, status, headers, data, answerOf) {
10
+ if (!(headers === null || headers === void 0 ? void 0 : headers[header])) {
11
+ // An older node answers its login's 401, an unknown route's 404, or a
12
+ // catch-all's 2xx. Anything else without the header (a proxy's 502 in
13
+ // front of a node) is an outage: ask again, never give the node up.
14
+ if (status === 401 || status === 404 || (status >= 200 && status < 300)) {
15
+ return { kind: 'unsupported', status };
16
+ }
17
+ return { kind: 'unavailable', reason: `answered ${status}` };
18
+ }
19
+ if (status === 404)
20
+ return { kind: 'refused' };
21
+ const answer = status === 200 ? answerOf(data === null || data === void 0 ? void 0 : data.value) : null;
22
+ if (answer !== null)
23
+ return { kind: 'answer', answer };
24
+ return { kind: 'unavailable', reason: `answered ${status}` };
25
+ }
26
+ /** How long a hub leaves a node without a route alone before asking it again. */
27
+ exports.OLDER_NODE_RECHECK_MS = 10 * 60000;
28
+ /**
29
+ * Hub side: which nodes lack a route (an older Xenon). Their sessions go
30
+ * without what the route serves; the hub says so once per node, and asks
31
+ * again after OLDER_NODE_RECHECK_MS, so a node upgraded in place is picked
32
+ * up without restarting the hub.
33
+ */
34
+ class OlderNodes {
35
+ constructor() {
36
+ this.now = () => Date.now();
37
+ this.unsupportedUntil = new Map();
38
+ this.warned = new Set();
39
+ }
40
+ shouldAsk(origin) {
41
+ const until = this.unsupportedUntil.get(origin);
42
+ if (until === undefined)
43
+ return true;
44
+ if (until > this.now())
45
+ return false;
46
+ this.unsupportedUntil.delete(origin);
47
+ return true;
48
+ }
49
+ unsupported(origin, status) {
50
+ this.unsupportedUntil.set(origin, this.now() + exports.OLDER_NODE_RECHECK_MS);
51
+ if (this.warned.has(origin))
52
+ return;
53
+ this.warned.add(origin);
54
+ this.logger.warn(this.warning(origin, status));
55
+ }
56
+ }
57
+ exports.OlderNodes = OlderNodes;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.nodeSessionDeviceLogsHandler = nodeSessionDeviceLogsHandler;
7
+ exports.registerNodeSessionDeviceLogs = registerNodeSessionDeviceLogs;
8
+ const typedi_1 = require("typedi");
9
+ const logger_1 = __importDefault(require("../logger"));
10
+ const config_1 = require("../config");
11
+ const commandAuth_1 = require("../middleware/commandAuth");
12
+ const nodeDeviceLogs_1 = require("../services/logcat/nodeDeviceLogs");
13
+ const NodeDeviceLogStore_1 = require("../services/logcat/NodeDeviceLogStore");
14
+ const hubSessionToken_1 = require("./hubSessionToken");
15
+ const nodeSessionMetrics_1 = require("./nodeSessionMetrics");
16
+ const nodeSessionStatus_1 = require("./nodeSessionStatus");
17
+ function nodeSessionDeviceLogsHandler(deps) {
18
+ return (req, res) => {
19
+ var _a, _b;
20
+ res.setHeader(nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER, '1');
21
+ const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
22
+ (0, nodeSessionStatus_1.answerForHub)(deps, req, res, sessionId, 'Session device logs', () => { var _a; return res.status(200).json({ value: deps.read(sessionId, (0, nodeSessionMetrics_1.parseAfter)((_a = req.query) === null || _a === void 0 ? void 0 : _a.after)) }); });
23
+ };
24
+ }
25
+ /** A node's route, ahead of its login, like the session-status route. A hub or standalone server has none. */
26
+ function registerNodeSessionDeviceLogs(router, pluginArgs) {
27
+ if (pluginArgs.hub === undefined)
28
+ return;
29
+ router.get(nodeDeviceLogs_1.NODE_DEVICE_LOGS_ROUTE, nodeSessionDeviceLogsHandler({
30
+ hubTokens: new hubSessionToken_1.HubSessionTokenVerifier(pluginArgs.hub),
31
+ enforced: () => (0, commandAuth_1.commandAuthEnabled)() && config_1.config.authDisabled !== true,
32
+ read: (sessionId, after) => typedi_1.Container.get(NodeDeviceLogStore_1.NodeDeviceLogStore).read(sessionId, after),
33
+ logger: logger_1.default.scope('NodeSessionDeviceLogs'),
34
+ }));
35
+ }
@@ -30,6 +30,7 @@ const logger_1 = __importDefault(require("../logger"));
30
30
  const helpers_1 = require("../helpers");
31
31
  const appiumBasePath_1 = require("../app/appiumBasePath");
32
32
  const PluginContext_1 = require("../PluginContext");
33
+ const outboundProxy_1 = require("../helpers/outboundProxy");
33
34
  /**
34
35
  * Where a node's WebDriver API lives.
35
36
  *
@@ -95,11 +96,8 @@ let NodeBasePathResolver = class NodeBasePathResolver {
95
96
  let advertised;
96
97
  try {
97
98
  const rejectUnauthorized = (_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _a === void 0 ? void 0 : _a.tlsRejectUnauthorized;
98
- const response = yield axios_1.default.get(`${origin}${exports.WEBDRIVER_INFO_PATH}`, {
99
- timeout: exports.LOOKUP_TIMEOUT_MS,
100
- validateStatus: (status) => status === 200,
101
- httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }),
102
- });
99
+ const url = `${origin}${exports.WEBDRIVER_INFO_PATH}`;
100
+ const response = yield axios_1.default.get(url, Object.assign({ timeout: exports.LOOKUP_TIMEOUT_MS, validateStatus: (status) => status === 200, httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }) }, (0, outboundProxy_1.axiosProxyConfig)(url, { rejectUnauthorized: rejectUnauthorized !== false })));
103
101
  const basePath = (_b = response.data) === null || _b === void 0 ? void 0 : _b.basePath;
104
102
  if (typeof basePath === 'string')
105
103
  advertised = (0, appiumBasePath_1.normalizeBasePath)(basePath);
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.outboundProxyFor = outboundProxyFor;
16
+ exports.envProxyFor = envProxyFor;
17
+ exports.proxyAgentFor = proxyAgentFor;
18
+ exports.socketProxyAgentFor = socketProxyAgentFor;
19
+ exports.axiosProxyConfig = axiosProxyConfig;
20
+ const typedi_1 = require("typedi");
21
+ const http_proxy_agent_1 = require("http-proxy-agent");
22
+ const https_proxy_agent_1 = require("https-proxy-agent");
23
+ const logger_1 = __importDefault(require("../logger"));
24
+ const PluginContext_1 = require("../PluginContext");
25
+ /**
26
+ * The proxy for one of Xenon's own calls to another server (a hub's to its
27
+ * nodes and to a cloud provider, a node's to its hub, and the rest of
28
+ * Xenon's internal calls), one rule whichever client makes the call:
29
+ *
30
+ * 1. None for a host `no_proxy` / `NO_PROXY` names (envProxyFor's list).
31
+ * 2. The `proxy` plugin option when it is set, for http and https alike,
32
+ * except for a loopback host (`localhost`, 127.x, ::1): a proxy elsewhere
33
+ * can't reach this machine's loopback.
34
+ * 3. Else the environment's proxy for the URL's scheme (envProxyFor).
35
+ *
36
+ * Through 2.15 the option reached one call, the create a hub sends to a node
37
+ * or a cloud provider; that session's commands, screenshots and heartbeats,
38
+ * device control, the sockets and a node's calls to its hub took the
39
+ * environment's proxy instead, or none.
40
+ */
41
+ function outboundProxyFor(target, opts = {}) {
42
+ var _a, _b;
43
+ const url = toUrl(target);
44
+ const scheme = schemeOf(url);
45
+ if (!scheme)
46
+ return undefined;
47
+ const env = (_a = opts.env) !== null && _a !== void 0 ? _a : process.env;
48
+ if (namedByNoProxy(url, env))
49
+ return undefined;
50
+ const option = 'option' in opts ? opts.option : pluginProxyOption();
51
+ const configured = proxyOptionUrl(option, (_b = opts.warn) !== null && _b !== void 0 ? _b : warnOnce);
52
+ if (configured && !isLoopback(url.hostname))
53
+ return configured;
54
+ return schemeProxy(scheme, env);
55
+ }
56
+ /**
57
+ * The environment's proxy for a URL, by the rule axios (0.27's http adapter)
58
+ * applies to Xenon's axios calls:
59
+ * - `http_proxy` or `HTTP_PROXY` for an http URL, `https_proxy` or
60
+ * `HTTPS_PROXY` for an https one, the lower-case name first (ws and wss
61
+ * count as http and https);
62
+ * - none when `no_proxy` or `NO_PROXY` (the lower-case name first; a
63
+ * comma-separated list) has `*`, the URL's host name, or a suffix of it
64
+ * starting with a dot (`.lab.example`). An entry is a host name only: one
65
+ * with a port matches nothing, as in axios.
66
+ *
67
+ * Through 2.15 a command forwarded to a node took HTTP_PROXY or HTTPS_PROXY
68
+ * whatever its scheme and ignored NO_PROXY, the live-preview and logcat
69
+ * sockets ignored every proxy, and so did a node fetching its hub's keys.
70
+ */
71
+ function envProxyFor(target, env = process.env) {
72
+ const url = toUrl(target);
73
+ const scheme = schemeOf(url);
74
+ if (!scheme || namedByNoProxy(url, env))
75
+ return undefined;
76
+ return schemeProxy(scheme, env);
77
+ }
78
+ /**
79
+ * An agent sending an HTTP request for `target` through its proxy, or
80
+ * undefined to go straight there: the request in absolute form to the proxy
81
+ * for an http URL, a CONNECT tunnel for an https one.
82
+ */
83
+ function proxyAgentFor(target, tls = {}) {
84
+ const proxy = outboundProxyFor(target);
85
+ if (!proxy)
86
+ return undefined;
87
+ const url = toUrl(target);
88
+ return url.protocol === 'https:' ? new TunnelAgent(proxy, tls) : new http_proxy_agent_1.HttpProxyAgent(proxy);
89
+ }
90
+ /**
91
+ * An agent opening a WebSocket for `target` (ws or wss) through its proxy, or
92
+ * undefined to go straight there. Always a CONNECT tunnel: an upgrade sent to
93
+ * a proxy in absolute form is not reliably passed on.
94
+ */
95
+ function socketProxyAgentFor(target, tls = {}) {
96
+ const proxy = outboundProxyFor(target);
97
+ return proxy ? new TunnelAgent(proxy, tls) : undefined;
98
+ }
99
+ /**
100
+ * An axios request's proxy settings for `target`: through its proxy's agent,
101
+ * or none. Either way `proxy: false`, since the decision is made here, by
102
+ * outboundProxyFor; axios's own lookup reads the environment only. With no
103
+ * proxy, the caller's own agents stay.
104
+ */
105
+ function axiosProxyConfig(target, tls = {}) {
106
+ const agent = proxyAgentFor(target, tls);
107
+ return agent ? { proxy: false, httpAgent: agent, httpsAgent: agent } : { proxy: false };
108
+ }
109
+ /**
110
+ * A CONNECT tunnel that checks the server behind it as the caller asked.
111
+ * HttpsProxyAgent's own options are for the connection to the proxy; the
112
+ * tunnelled TLS takes the request's, which axios doesn't fill in. So through
113
+ * 2.15 `tlsRejectUnauthorized` never reached a node or cloud provider that a
114
+ * proxy stood in front of.
115
+ */
116
+ class TunnelAgent extends https_proxy_agent_1.HttpsProxyAgent {
117
+ constructor(proxy, targetTls) {
118
+ super(proxy);
119
+ this.targetTls = targetTls;
120
+ }
121
+ connect(req, opts) {
122
+ const _super = Object.create(null, {
123
+ connect: { get: () => super.connect }
124
+ });
125
+ return __awaiter(this, void 0, void 0, function* () {
126
+ const tls = this.targetTls.rejectUnauthorized === undefined
127
+ ? {}
128
+ : { rejectUnauthorized: this.targetTls.rejectUnauthorized };
129
+ return _super.connect.call(this, req, Object.assign(Object.assign({}, opts), tls));
130
+ });
131
+ }
132
+ }
133
+ /**
134
+ * The `proxy` option as a proxy URL: a string as it is (http:// added when
135
+ * it names no scheme), or `{ protocol, host, port, auth: { username,
136
+ * password } }` (protocol http by default, credentials escaped). One that
137
+ * names no host is no proxy, said once.
138
+ */
139
+ function proxyOptionUrl(option, warn) {
140
+ var _a, _b;
141
+ if (option === undefined || option === null || option === '')
142
+ return undefined;
143
+ if (typeof option === 'string') {
144
+ const text = option.trim();
145
+ if (text === '')
146
+ return undefined;
147
+ return hasScheme(text) ? text : `http://${text}`;
148
+ }
149
+ const value = option;
150
+ if (typeof option !== 'object' || typeof value.host !== 'string' || value.host.trim() === '') {
151
+ warn(`The proxy option ${JSON.stringify(option)} names no host, so it is not used: ` +
152
+ "Xenon's calls to other servers take the environment's proxy, if any.");
153
+ return undefined;
154
+ }
155
+ const protocol = typeof value.protocol === 'string' && value.protocol !== ''
156
+ ? value.protocol.replace(/:$/, '')
157
+ : 'http';
158
+ const port = value.port === undefined || value.port === null ? '' : `:${value.port}`;
159
+ const username = (_a = value.auth) === null || _a === void 0 ? void 0 : _a.username;
160
+ const password = (_b = value.auth) === null || _b === void 0 ? void 0 : _b.password;
161
+ const credentials = typeof username === 'string' && username !== ''
162
+ ? `${encodeURIComponent(username)}${typeof password === 'string' ? `:${encodeURIComponent(password)}` : ''}@`
163
+ : '';
164
+ return `${protocol}://${credentials}${value.host.trim()}${port}`;
165
+ }
166
+ const warned = new Set();
167
+ function warnOnce(message) {
168
+ if (warned.has(message))
169
+ return;
170
+ warned.add(message);
171
+ logger_1.default.warn(message);
172
+ }
173
+ function pluginProxyOption() {
174
+ var _a;
175
+ try {
176
+ return (_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _a === void 0 ? void 0 : _a.proxy;
177
+ }
178
+ catch (_b) {
179
+ return undefined;
180
+ }
181
+ }
182
+ function toUrl(target) {
183
+ return typeof target === 'string' ? new URL(target) : target;
184
+ }
185
+ function schemeOf(url) {
186
+ const scheme = url.protocol.replace(/:$/, '').replace(/^ws(s?)$/, 'http$1');
187
+ return scheme === 'http' || scheme === 'https' ? scheme : undefined;
188
+ }
189
+ function hostOf(url) {
190
+ return url.hostname.replace(/^\[(.*)\]$/, '$1');
191
+ }
192
+ function namedByNoProxy(url, env) {
193
+ const noProxy = env.no_proxy || env.NO_PROXY;
194
+ if (!noProxy)
195
+ return false;
196
+ const host = hostOf(url);
197
+ return noProxy
198
+ .split(',')
199
+ .map((entry) => entry.trim())
200
+ .some((entry) => entry !== '' &&
201
+ (entry === '*' || entry === host || (entry.startsWith('.') && host.endsWith(entry))));
202
+ }
203
+ function schemeProxy(scheme, env) {
204
+ const proxy = env[`${scheme}_proxy`] || env[`${scheme.toUpperCase()}_PROXY`];
205
+ if (!proxy)
206
+ return undefined;
207
+ return hasScheme(proxy) ? proxy : `http://${proxy}`;
208
+ }
209
+ function hasScheme(text) {
210
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(text);
211
+ }
212
+ function isLoopback(hostname) {
213
+ const host = hostname.replace(/^\[(.*)\]$/, '$1').toLowerCase();
214
+ return (host === 'localhost' ||
215
+ host.endsWith('.localhost') ||
216
+ /^127(\.\d{1,3}){3}$/.test(host) ||
217
+ host === '::1' ||
218
+ host === '0:0:0:0:0:0:0:1');
219
+ }
package/lib/src/index.js CHANGED
@@ -68,6 +68,12 @@ const cleanup = () => __awaiter(void 0, void 0, void 0, function* () {
68
68
  // Bounded so a hung driver can't hold up systemd (default 90s timeout).
69
69
  const { ShutdownCoordinator } = yield Promise.resolve().then(() => __importStar(require('./services/ShutdownCoordinator')));
70
70
  yield typedi_1.Container.get(ShutdownCoordinator).drain(15000);
71
+ // Telemetry next, as early as it can be: on SIGTERM Appium exits before
72
+ // Phase 2 (see the 'exit' hook below), and placed last it never ran. What
73
+ // the OTel exporters still hold is sent, and the spans of sessions still
74
+ // running on a node or at a cloud provider end, saying so. Lines logged
75
+ // after this aren't exported.
76
+ yield shutDownTelemetry(3000);
71
77
  const { default: IOSStreamService } = yield Promise.resolve().then(() => __importStar(require('./device-managers/ios/IOSStreamService')));
72
78
  const { default: AndroidStreamService } = yield Promise.resolve().then(() => __importStar(require('./device-managers/android/AndroidStreamService')));
73
79
  const { LogcatStreamService } = yield Promise.resolve().then(() => __importStar(require('./device-managers/android/LogcatStreamService')));
@@ -94,11 +100,26 @@ const cleanup = () => __awaiter(void 0, void 0, void 0, function* () {
94
100
  logger_1.default.error(`❌ [Xenon] Cleanup failed: ${err.message}`);
95
101
  }
96
102
  finally {
103
+ // When the drain threw, the telemetry wasn't shut down above. A second
104
+ // call does nothing.
105
+ yield shutDownTelemetry(1000);
97
106
  // Principal Delay: Wait 2 seconds before hard-exiting to allow
98
107
  // other async handlers (like hub unregistration) to finish.
99
108
  setTimeout(() => process.exit(0), 2000);
100
109
  }
101
110
  });
111
+ function shutDownTelemetry(budgetMs) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ var _a;
114
+ try {
115
+ const { TracingService } = yield Promise.resolve().then(() => __importStar(require('./services/TracingService')));
116
+ yield typedi_1.Container.get(TracingService).shutdownWithin(budgetMs);
117
+ }
118
+ catch (err) {
119
+ logger_1.default.warn(`[Xenon] Telemetry shutdown failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
120
+ }
121
+ });
122
+ }
102
123
  process.on('SIGINT', cleanup);
103
124
  process.on('SIGTERM', cleanup);
104
125
  // Last-resort synchronous net for every long-lived sidecar.
@@ -118,6 +118,7 @@ let CommandInterceptor = class CommandInterceptor {
118
118
  return yield next();
119
119
  const sessionId = driver.sessionId || args[args.length - 1];
120
120
  const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
121
+ const spanId = `${sessionId}:${commandName}`;
121
122
  let span;
122
123
  if (isHub && sessionId) {
123
124
  span = tracingService.startCommandSpan(sessionId, commandName, {
@@ -130,6 +131,7 @@ let CommandInterceptor = class CommandInterceptor {
130
131
  // those services having to accept a context parameter.
131
132
  const spanCtx = span === null || span === void 0 ? void 0 : span.spanContext();
132
133
  const cmdStart = Date.now();
134
+ let failure;
133
135
  try {
134
136
  return yield sessionContext_1.sessionContext.run({
135
137
  sessionId: sessionId || undefined,
@@ -137,9 +139,16 @@ let CommandInterceptor = class CommandInterceptor {
137
139
  commandName,
138
140
  traceId: spanCtx === null || spanCtx === void 0 ? void 0 : spanCtx.traceId,
139
141
  spanId: spanCtx === null || spanCtx === void 0 ? void 0 : spanCtx.spanId,
140
- }, () => this.handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId, span, tracingService));
142
+ }, () => this.handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId));
143
+ }
144
+ catch (error) {
145
+ // What the client gets: a healed find returned an element instead.
146
+ failure = { error };
147
+ throw error;
141
148
  }
142
149
  finally {
150
+ if (span)
151
+ tracingService.endCommandSpan(spanId, span, failure);
143
152
  // Aggregate counter — no per-session label, just fleet-wide throughput
144
153
  // so Prom rate() gives commands/sec and the duration sum gives avg
145
154
  // latency. Errors still count: a 429-hit dashboard poll is still hub
@@ -148,7 +157,7 @@ let CommandInterceptor = class CommandInterceptor {
148
157
  }
149
158
  });
150
159
  }
151
- handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId, span, tracingService) {
160
+ handleInContext(next, driver, commandName, args, pluginArgs, isHub, sessionId) {
152
161
  return __awaiter(this, void 0, void 0, function* () {
153
162
  if (commandName === 'createSession' || commandName === 'deleteSession') {
154
163
  try {
@@ -158,8 +167,6 @@ let CommandInterceptor = class CommandInterceptor {
158
167
  if (commandName === 'deleteSession' && sessionId) {
159
168
  typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(sessionId);
160
169
  }
161
- if (span)
162
- tracingService.endSpan(`${sessionId}:${commandName}`);
163
170
  }
164
171
  }
165
172
  try {
@@ -181,7 +188,7 @@ let CommandInterceptor = class CommandInterceptor {
181
188
  scriptArgs = {};
182
189
  }
183
190
  }
184
- const aiCommand = script.replace(/^(xe|xenon)\s*:\s*/, '').trim();
191
+ const aiCommand = (0, xenonScripts_1.interceptorScriptName)(script);
185
192
  // appium-wait-plugin compatible alias — strip the `plugin:` prefix
186
193
  // and map the old camelCase names so existing tests Just Work.
187
194
  const legacyWaitCommand = script.replace(/^plugin\s*:\s*/, '').trim();
@@ -232,12 +239,7 @@ let CommandInterceptor = class CommandInterceptor {
232
239
  : '';
233
240
  return yield typedi_1.Container.get(AICommandService_1.AICommandService).assertVisualState(driver, instruction);
234
241
  }
235
- if (aiCommand === 'addMock' ||
236
- aiCommand === 'removeMock' ||
237
- aiCommand === 'clearMocks' ||
238
- aiCommand === 'getRequests' ||
239
- aiCommand === 'getMocks' ||
240
- aiCommand === 'exportHar') {
242
+ if ((0, xenonScripts_1.isNetworkCaptureScript)(script)) {
241
243
  const payload = typeof scriptArgs === 'object' && scriptArgs !== null ? scriptArgs : {};
242
244
  const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('../services/InterceptorService')));
243
245
  const interceptor = typedi_1.Container.get(InterceptorService);
@@ -385,10 +387,6 @@ let CommandInterceptor = class CommandInterceptor {
385
387
  }
386
388
  throw error;
387
389
  }
388
- finally {
389
- if (isHub && sessionId && span)
390
- tracingService.endSpan(`${sessionId}:${commandName}`);
391
- }
392
390
  });
393
391
  }
394
392
  warnUnreadableHealingTiers(driver, sessionId, unreadable) {
@@ -14,9 +14,26 @@
14
14
  * - A name Xenon doesn't have fails with `unknown command`.
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.XENON_SCRIPT_NAMES = void 0;
17
+ exports.XENON_SCRIPT_NAMES = exports.NETWORK_CAPTURE_SCRIPTS = void 0;
18
+ exports.interceptorScriptName = interceptorScriptName;
19
+ exports.isNetworkCaptureScript = isNetworkCaptureScript;
18
20
  exports.xenonScriptName = xenonScriptName;
19
21
  exports.unknownXenonScriptMessage = unknownXenonScriptMessage;
22
+ /**
23
+ * The scripts CommandInterceptor answers from the session's network capture.
24
+ * Their arguments and answers are the capture (requests, HAR, mocks), which
25
+ * REST shows to admins only, so the command log keeps neither (commandLogFields).
26
+ * A new script that answers from the capture goes in this list. A hub hides
27
+ * by its own copy, so it needs the release that adds the script.
28
+ */
29
+ exports.NETWORK_CAPTURE_SCRIPTS = [
30
+ 'addMock',
31
+ 'removeMock',
32
+ 'clearMocks',
33
+ 'getMocks',
34
+ 'getRequests',
35
+ 'exportHar',
36
+ ];
20
37
  /** The names CommandInterceptor and the session-details commands answer. Keep in step with both. */
21
38
  exports.XENON_SCRIPT_NAMES = [
22
39
  'setAutowaitProperties',
@@ -29,18 +46,22 @@ exports.XENON_SCRIPT_NAMES = [
29
46
  'analyzeScreen',
30
47
  'omniScan',
31
48
  'assertVisualState',
32
- 'addMock',
33
- 'removeMock',
34
- 'clearMocks',
35
- 'getMocks',
36
- 'getRequests',
37
- 'exportHar',
49
+ ...exports.NETWORK_CAPTURE_SCRIPTS,
38
50
  'setSessionName',
39
51
  'setSessionStatus',
40
52
  'debug',
41
53
  'addTag',
42
54
  'captureEvidence',
43
55
  ];
56
+ /** The name CommandInterceptor routes a script by: its `xe:` / `xenon:` prefix, if any, taken off. */
57
+ function interceptorScriptName(script) {
58
+ return script.replace(/^(xe|xenon)\s*:\s*/, '').trim();
59
+ }
60
+ /** Whether CommandInterceptor answers `script` from the session's network capture. */
61
+ function isNetworkCaptureScript(script) {
62
+ return (typeof script === 'string' &&
63
+ exports.NETWORK_CAPTURE_SCRIPTS.includes(interceptorScriptName(script)));
64
+ }
44
65
  /** The name in a `xenon: <name>` or `xe: <name>` script, or null for any other script. */
45
66
  function xenonScriptName(script) {
46
67
  if (typeof script !== 'string')
package/lib/src/logger.js CHANGED
@@ -185,7 +185,9 @@ class XenonLogger {
185
185
  }
186
186
  if (redactedArgs.length)
187
187
  logEntry.args = redactedArgs;
188
- this.baseLogger.info(JSON.stringify(logEntry));
188
+ // At its own level, so --log-level filters it and an error reaches
189
+ // stderr, as in text mode.
190
+ this.baseLogger[level](JSON.stringify(logEntry));
189
191
  }
190
192
  else {
191
193
  const ctxPrefix = this.buildTextPrefix(ctx);
package/lib/src/plugin.js CHANGED
@@ -68,6 +68,7 @@ const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
68
68
  const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
69
69
  const sessionMemory_1 = require("./sessions/sessionMemory");
70
70
  const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
71
+ const SessionDeviceLogs_1 = require("./services/logcat/SessionDeviceLogs");
71
72
  const PhoneNetworkRestore_1 = require("./services/network/PhoneNetworkRestore");
72
73
  const config_1 = require("./config");
73
74
  const SessionManager_1 = require("./sessions/SessionManager");
@@ -112,8 +113,18 @@ class XenonPlugin extends base_plugin_1.default {
112
113
  const sessionId = driver.sessionId;
113
114
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
114
115
  (0, sessionMemory_1.forgetSessionMemory)(sessionId);
115
- if (sessionId)
116
+ // With the dashboard on or off: this was the only place a session span
117
+ // ended, and only with the dashboard on.
118
+ if (sessionId) {
119
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, {
120
+ failed: true,
121
+ reason: (0, shutdownReason_1.unexpectedShutdownReason)(cause),
122
+ });
123
+ }
124
+ if (sessionId) {
116
125
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
126
+ yield typedi_1.Container.get(SessionDeviceLogs_1.SessionDeviceLogs).stop(sessionId);
127
+ }
117
128
  // Appium's new-command timeout ends a session here, not in deleteSession:
118
129
  // the phone's network (profile, interceptor proxy) and the capture are
119
130
  // put back before the phone is released.
@@ -160,9 +171,6 @@ class XenonPlugin extends base_plugin_1.default {
160
171
  }
161
172
  }
162
173
  yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, SessionStatus_1.SessionStatus.FAILED, reason);
163
- typedi_1.Container.get(TracingService_1.TracingService).endSpan(sessionId, 'ERROR', {
164
- 'xenon.session.stop_reason': 'Unexpected shutdown',
165
- });
166
174
  }
167
175
  });
168
176
  }
@@ -93,9 +93,9 @@ function checkDatabase(opts) {
93
93
  }
94
94
  if (configured && configured !== client) {
95
95
  return {
96
- warning: `[Database] databaseProvider is "${configured}", but this install's database client is ` +
97
- `built for ${clientLabel}, so the setting has no effect. Your data is in ${where}. ` +
98
- `Set databaseProvider to "${client}", or remove it.`,
96
+ warning: `[Database] databaseProvider is "${configured}", which has no effect: this install ` +
97
+ `stores its data in ${clientLabel}, in ${where}, and the database itself decides how ` +
98
+ `its schema is brought up to date. Remove the setting, or set it to "${client}".`,
99
99
  };
100
100
  }
101
101
  return {};