@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
@@ -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
@@ -112,6 +112,14 @@ class XenonPlugin extends base_plugin_1.default {
112
112
  const sessionId = driver.sessionId;
113
113
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
114
114
  (0, sessionMemory_1.forgetSessionMemory)(sessionId);
115
+ // With the dashboard on or off: this was the only place a session span
116
+ // ended, and only with the dashboard on.
117
+ if (sessionId) {
118
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, {
119
+ failed: true,
120
+ reason: (0, shutdownReason_1.unexpectedShutdownReason)(cause),
121
+ });
122
+ }
115
123
  if (sessionId)
116
124
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
117
125
  // Appium's new-command timeout ends a session here, not in deleteSession:
@@ -160,9 +168,6 @@ class XenonPlugin extends base_plugin_1.default {
160
168
  }
161
169
  }
162
170
  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
171
  }
167
172
  });
168
173
  }
@@ -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 {};
@@ -14,42 +14,44 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const node_child_process_1 = require("node:child_process");
16
16
  const config_1 = require("../config");
17
- const prepare_prisma_1 = require("./prepare-prisma");
18
17
  const logger_1 = __importDefault(require("../logger"));
19
- const env = Object.assign(Object.assign({}, process.env), { DATABASE_URL: config_1.config.databaseUrl });
20
- function executeCmd(cmd) {
21
- var _a;
22
- try {
23
- (0, node_child_process_1.execSync)(cmd, {
24
- env,
25
- stdio: 'inherit',
26
- });
27
- }
28
- catch (error) {
29
- const msg = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : String(error);
30
- logger_1.default.error(`[DBInit] Failed to execute command: ${cmd} | Error: ${msg}`, error);
31
- throw error;
32
- }
33
- }
18
+ const database_check_1 = require("./database-check");
19
+ const run_migrations_1 = require("./run-migrations");
20
+ /**
21
+ * `npm run db:migrate`: bring the database up to date, then regenerate the
22
+ * client.
23
+ *
24
+ * It checks the database URL and uses the rule the server uses at startup
25
+ * (database-check.ts, then run-migrations.ts): the database decides between
26
+ * `migrate deploy` and `db push`, so a database whose migration history
27
+ * matches its tables stays on `migrate deploy`. It runs whatever
28
+ * XENON_AUTO_MIGRATE says, since it is how you update the schema when that is
29
+ * off. Through 2.15.0 it always ran `db push`, and first rewrote
30
+ * prisma/schema.prisma's provider to `databaseProvider`. With `postgresql`,
31
+ * that left a PostgreSQL schema in the checkout that the SQLite file couldn't
32
+ * use.
33
+ *
34
+ * It first warns of changes to prisma/schema.prisma that no migration makes.
35
+ * `npm run db:generate` gives the developer's database a migration history, and
36
+ * from then on it is updated with `migrate deploy`, which never applies them,
37
+ * while the log said the schema was in sync.
38
+ */
34
39
  function main() {
35
40
  return __awaiter(this, void 0, void 0, function* () {
36
- logger_1.default.info(`[DBInit] Preparing database for provider: ${config_1.config.databaseProvider}`);
37
- // 1. Ensure schema matches provider
38
- yield (0, prepare_prisma_1.preparePrismaSchema)();
39
- // 2. Handle Migrations
40
- if (config_1.config.databaseProvider === 'sqlite') {
41
- logger_1.default.info('[DBInit] Syncing SQLite schema via db push...');
42
- executeCmd('npx prisma db push --accept-data-loss --skip-generate');
43
- }
44
- else {
45
- // For PostgreSQL, we might not have migrations checked in yet.
46
- // In "Cellular Architecture", we use db push to ensure the schema is synced
47
- // without requiring migration history sync across cells.
48
- logger_1.default.info('[DBInit] Syncing PostgreSQL schema via db push...');
49
- executeCmd('npx prisma db push --accept-data-loss');
50
- }
41
+ // A PostgreSQL URL would otherwise reach the history read and fail there,
42
+ // under a message about the file.
43
+ (0, database_check_1.assertSupportedDatabase)();
44
+ (0, run_migrations_1.warnOfUnmigratedSchemaChanges)();
45
+ yield (0, run_migrations_1.syncDatabaseSchema)();
51
46
  logger_1.default.info('[DBInit] Generating Prisma Client...');
52
- executeCmd('npx prisma generate');
47
+ (0, node_child_process_1.execSync)('npx prisma generate', {
48
+ env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: config_1.config.databaseUrl }),
49
+ stdio: 'inherit',
50
+ });
53
51
  });
54
52
  }
55
- (() => __awaiter(void 0, void 0, void 0, function* () { return yield main(); }))();
53
+ main().catch((err) => {
54
+ var _a;
55
+ logger_1.default.error(`[DBInit] ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
56
+ process.exit(1);
57
+ });