@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
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
23
23
  const crypto_1 = __importDefault(require("crypto"));
24
24
  const prisma_1 = require("../prisma");
25
25
  const logger_1 = __importDefault(require("../logger"));
26
+ const identityChanges_1 = require("./identity/identityChanges");
26
27
  let ApiKeyService = class ApiKeyService {
27
28
  constructor() {
28
29
  this.log = logger_1.default.scope('ApiKey');
@@ -38,8 +39,13 @@ let ApiKeyService = class ApiKeyService {
38
39
  generateRaw() {
39
40
  return crypto_1.default.randomBytes(32).toString('hex');
40
41
  }
41
- verify(raw) {
42
- return __awaiter(this, void 0, void 0, function* () {
42
+ /**
43
+ * The live key for `raw`. A use of it is recorded (`lastUsedAt`);
44
+ * `touch: false` only looks, for a check that isn't a use (a socket's
45
+ * re-check). Likewise verifyPair.
46
+ */
47
+ verify(raw_1) {
48
+ return __awaiter(this, arguments, void 0, function* (raw, { touch = true } = {}) {
43
49
  if (!raw)
44
50
  return null;
45
51
  const row = yield prisma_1.prisma.apiKey.findUnique({ where: { keyHash: this.hash(raw) } });
@@ -47,6 +53,8 @@ let ApiKeyService = class ApiKeyService {
47
53
  return null;
48
54
  if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
49
55
  return null;
56
+ if (!touch)
57
+ return row;
50
58
  prisma_1.prisma.apiKey
51
59
  .update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
52
60
  .catch(() => undefined);
@@ -77,8 +85,8 @@ let ApiKeyService = class ApiKeyService {
77
85
  return { id: row.id, raw };
78
86
  });
79
87
  }
80
- verifyPair(accessKey, token) {
81
- return __awaiter(this, void 0, void 0, function* () {
88
+ verifyPair(accessKey_1, token_1) {
89
+ return __awaiter(this, arguments, void 0, function* (accessKey, token, { touch = true } = {}) {
82
90
  if (!accessKey || !token)
83
91
  return null;
84
92
  const user = yield prisma_1.prisma.user.findUnique({ where: { accessKey } });
@@ -91,6 +99,8 @@ let ApiKeyService = class ApiKeyService {
91
99
  return null;
92
100
  if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
93
101
  return null;
102
+ if (!touch)
103
+ return row;
94
104
  prisma_1.prisma.apiKey
95
105
  .update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
96
106
  .catch(() => undefined);
@@ -99,7 +109,8 @@ let ApiKeyService = class ApiKeyService {
99
109
  }
100
110
  revoke(id) {
101
111
  return __awaiter(this, void 0, void 0, function* () {
102
- yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
112
+ const row = yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
113
+ yield (0, identityChanges_1.identityChanged)(row.userId);
103
114
  });
104
115
  }
105
116
  list() {
@@ -240,7 +240,11 @@ let InterceptorService = class InterceptorService {
240
240
  throw new Error(`Interceptor not active for session ${sessionId}`);
241
241
  return state;
242
242
  }
243
- /** `udid` is the session's phone: the dashboard events reach only the teams that see it. */
243
+ /**
244
+ * `udid` is the session's phone. The dashboard events reach only admins who
245
+ * see it: a captured request's headers and bodies can carry the app's
246
+ * sign-in tokens, and REST's /interceptor routes are admin-only too.
247
+ */
244
248
  emit(evt, udid) {
245
249
  for (const l of this.listeners) {
246
250
  try {
@@ -255,7 +259,7 @@ let InterceptorService = class InterceptorService {
255
259
  broadcast(evt, udid) {
256
260
  try {
257
261
  const socket = typedi_1.Container.get(SocketServer_1.SocketServer);
258
- const scope = { udid };
262
+ const scope = { udid, adminOnly: true };
259
263
  if (evt.type === 'request') {
260
264
  void socket.emitToDashboardForDevices(SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, evt.payload, scope);
261
265
  }
@@ -26,6 +26,7 @@ const event_manager_1 = require("../dashboard/event-manager");
26
26
  const SessionStatus_1 = require("../types/SessionStatus");
27
27
  const device_service_1 = require("../data-service/device-service");
28
28
  const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
29
+ const TracingService_1 = require("./TracingService");
29
30
  const sessionMemory_1 = require("../sessions/sessionMemory");
30
31
  let OrphanSweeper = class OrphanSweeper {
31
32
  constructor() {
@@ -76,6 +77,11 @@ let OrphanSweeper = class OrphanSweeper {
76
77
  // the ledger's, at boot (PhoneNetworkRestore).
77
78
  yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
78
79
  (0, sessionMemory_1.forgetSessionMemory)(s.id);
80
+ // A previous process's session has no span here; this one's ends.
81
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(s.id, {
82
+ failed: true,
83
+ reason: 'Session heartbeat timeout',
84
+ });
79
85
  // This session's claim only, not every row with its udid: the phone
80
86
  // may be another session's by now (deviceClaims.ts).
81
87
  yield (0, device_service_1.releaseSessionDevices)(s.id);
@@ -51,6 +51,10 @@ function whereTuple(strategy, selector) {
51
51
  },
52
52
  };
53
53
  }
54
+ /** Just the selector a change is about, for the socket server's scoped emit. */
55
+ function selectorOf(tuple) {
56
+ return { strategy: tuple.strategy, selector: tuple.selector };
57
+ }
54
58
  /** The SelectorEvent row for one change. */
55
59
  function selectorEventData(tuple, action, userId, reason) {
56
60
  return {
@@ -140,7 +144,7 @@ let SelectorStateService = class SelectorStateService {
140
144
  return upserted;
141
145
  }));
142
146
  scopedLog.info(`markFixed: ${ctx.strategy}=${ctx.selector} → pending (api_key=${ctx.apiKeyId})`);
143
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row));
147
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row), selectorOf(ctx));
144
148
  return row;
145
149
  });
146
150
  }
@@ -174,7 +178,7 @@ let SelectorStateService = class SelectorStateService {
174
178
  return upserted;
175
179
  }));
176
180
  scopedLog.info(`mute: ${ctx.strategy}=${ctx.selector} → muted (api_key=${ctx.apiKeyId})`);
177
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row));
181
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row), selectorOf(ctx));
178
182
  return row;
179
183
  });
180
184
  }
@@ -231,13 +235,13 @@ let SelectorStateService = class SelectorStateService {
231
235
  scopedLog.info(`unmute: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
232
236
  // Emit the post-state. When the row was deleted, surface the prior tuple
233
237
  // so the dashboard can drop it from the muted list.
234
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, result
238
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, result
235
239
  ? serialize(result)
236
240
  : {
237
241
  original_strategy: ctx.strategy,
238
242
  original_selector: ctx.selector,
239
243
  status: 'deleted',
240
- });
244
+ }, selectorOf(ctx));
241
245
  return result;
242
246
  });
243
247
  }
@@ -279,13 +283,13 @@ let SelectorStateService = class SelectorStateService {
279
283
  return row;
280
284
  }));
281
285
  scopedLog.info(`cancelVerification: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
282
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
286
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
283
287
  ? serialize(result)
284
288
  : {
285
289
  original_strategy: ctx.strategy,
286
290
  original_selector: ctx.selector,
287
291
  status: 'deleted',
288
- });
292
+ }, selectorOf(ctx));
289
293
  return result;
290
294
  });
291
295
  }
@@ -329,7 +333,7 @@ let SelectorStateService = class SelectorStateService {
329
333
  return updated;
330
334
  }));
331
335
  scopedLog.warn(`onHealRecorded: ${ctx.strategy}=${ctx.selector} regressed from ${existing.status} (session=${ctx.sessionId})`);
332
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row));
336
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row), selectorOf(ctx));
333
337
  return row;
334
338
  });
335
339
  }
@@ -39,6 +39,10 @@ const log = logger_1.default.scope('SelectorVerification');
39
39
  * selector that was never found again was verified as fixed.
40
40
  */
41
41
  const CLEAN_BUILDS_TO_RESOLVE = 3;
42
+ /** The selector a row is about, as the socket server's scoped emit names it. */
43
+ function keyOfRow(row) {
44
+ return { strategy: row.original_strategy, selector: row.original_selector };
45
+ }
42
46
  /**
43
47
  * Periodically scans Pending SelectorState rows and promotes them to Resolved
44
48
  * once enough clean CI builds have been observed (no heals on the selector
@@ -180,7 +184,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
180
184
  return promoted;
181
185
  }));
182
186
  log.info(`[${row.id}] promoted to resolved (${row.original_strategy}=${row.original_selector})`);
183
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_RESOLVED, this.serialize(updated));
187
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_RESOLVED, this.serialize(updated), keyOfRow(row));
184
188
  });
185
189
  }
186
190
  updateProgress(row, cleanBuildCount) {
@@ -189,7 +193,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
189
193
  where: { id: row.id },
190
194
  data: { clean_builds_count: cleanBuildCount, last_event_at: new Date() },
191
195
  });
192
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_PROGRESS, this.serialize(updated));
196
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_PROGRESS, this.serialize(updated), keyOfRow(row));
193
197
  });
194
198
  }
195
199
  serialize(row) {
@@ -56,8 +56,6 @@ const typedi_1 = require("typedi");
56
56
  const uuid_1 = require("uuid");
57
57
  const lodash_1 = __importDefault(require("lodash"));
58
58
  const axios_1 = __importDefault(require("axios"));
59
- const https_proxy_agent_1 = require("https-proxy-agent");
60
- const http_proxy_agent_1 = require("http-proxy-agent");
61
59
  const logger_1 = __importDefault(require("../logger"));
62
60
  const helpers_1 = require("../helpers");
63
61
  const InternalHttpClient_1 = require("../InternalHttpClient");
@@ -848,7 +846,7 @@ let SessionLifecycleService = class SessionLifecycleService {
848
846
  }
849
847
  finalizeSession(session_1, device_1, caps_1, driver_1, isRemote_1) {
850
848
  return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null, allocatedAt) {
851
- var _a;
849
+ var _a, _b;
852
850
  const sessionId = session.value[0];
853
851
  const sessionResponse = session.value[1];
854
852
  const requestedCaps = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
@@ -865,6 +863,23 @@ let SessionLifecycleService = class SessionLifecycleService {
865
863
  'xenon.udid': device.udid,
866
864
  });
867
865
  }
866
+ try {
867
+ yield this.registerSession(sessionId, sessionResponse, device, xenonCapabilities, driver, isRemote, apiKeyId, userId, allocatedAt);
868
+ }
869
+ catch (err) {
870
+ // The create fails, so no ending of the session will come to end it.
871
+ tracingService.endSessionSpan(sessionId, {
872
+ failed: true,
873
+ reason: `Xenon could not finish creating the session: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`,
874
+ });
875
+ throw err;
876
+ }
877
+ });
878
+ }
879
+ /** The rest of finalizeSession, once the session's span has started. */
880
+ registerSession(sessionId, sessionResponse, device, xenonCapabilities, driver, isRemote, apiKeyId, userId, allocatedAt) {
881
+ return __awaiter(this, void 0, void 0, function* () {
882
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
868
883
  // The session takes over the pending claim its phone was allocated with.
869
884
  yield (0, device_service_1.claimDeviceForSession)(device, sessionId, {
870
885
  session_id: sessionId,
@@ -1063,19 +1078,8 @@ let SessionLifecycleService = class SessionLifecycleService {
1063
1078
  timeout: exports.REMOTE_CREATE_TIMEOUT_MS,
1064
1079
  retry: false,
1065
1080
  };
1066
- if (context.pluginArgs.proxy) {
1067
- this.logger.info(`Added proxy to axios config: ${JSON.stringify(context.pluginArgs.proxy)}`);
1068
- const proxyUrl = typeof context.pluginArgs.proxy === 'string'
1069
- ? context.pluginArgs.proxy
1070
- : `http://${context.pluginArgs.proxy.host}:${context.pluginArgs.proxy.port}`;
1071
- config.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl, {
1072
- rejectUnauthorized: context.pluginArgs.tlsRejectUnauthorized,
1073
- });
1074
- config.httpAgent = new http_proxy_agent_1.HttpProxyAgent(proxyUrl, {
1075
- rejectUnauthorized: context.pluginArgs.tlsRejectUnauthorized,
1076
- });
1077
- config.proxy = false;
1078
- }
1081
+ // Its proxy is InternalHttpClient's, by Xenon's one rule: the `proxy`
1082
+ // option, else the environment's (helpers/outboundProxy.ts).
1079
1083
  const createdSession = yield this.invokeSessionRequest(config, context.pluginArgs.tlsRejectUnauthorized);
1080
1084
  if (createdSession instanceof Error) {
1081
1085
  return createdSession;
@@ -1207,6 +1211,12 @@ let SessionLifecycleService = class SessionLifecycleService {
1207
1211
  if (sessionId) {
1208
1212
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
1209
1213
  (0, sessionMemory_1.forgetSessionMemory)(sessionId);
1214
+ // Whether or not SESSION_MANAGER holds the session (a local one with
1215
+ // the dashboard off), and once: a second delete finds no span.
1216
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, {
1217
+ failed: status === SessionStatus_1.SessionStatus.FAILED,
1218
+ reason,
1219
+ });
1210
1220
  // Before the lock's "still in memory?" check, which a dashboard-off
1211
1221
  // node fails. On a node this ends the figures it holds for its hub;
1212
1222
  // on a hub it collects a node session's last ones. Idempotent.
@@ -1240,8 +1250,11 @@ let SessionLifecycleService = class SessionLifecycleService {
1240
1250
  logger_1.default.warn(`Port release failed: ${err.message}`);
1241
1251
  }
1242
1252
  }
1243
- // 1. iOS Profiling Archival
1244
- if (device && ((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'ios') {
1253
+ // 1. iOS Profiling Archival. Only on an iPhone, the one place
1254
+ // EventManager.onSessionStarted starts it. A simulator's driver refuses
1255
+ // the stop without relaxed security, and the refused stop counted as a
1256
+ // failed command of the session: every simulator session ended failed.
1257
+ if (device && ((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'ios' && device.realDevice === true) {
1245
1258
  this.logger.info(`[${sessionId}] Stopping iOS profiling for asset archival`);
1246
1259
  try {
1247
1260
  const traceBase64 = yield session.stopPerformanceRecording();
@@ -1323,6 +1336,7 @@ let SessionLifecycleService = class SessionLifecycleService {
1323
1336
  SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
1324
1337
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
1325
1338
  (0, sessionMemory_1.forgetSessionMemory)(sessionId);
1339
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, { failed: true, reason });
1326
1340
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
1327
1341
  }));
1328
1342
  });
@@ -15,6 +15,7 @@ const typedi_1 = require("typedi");
15
15
  const logger_1 = __importDefault(require("../logger"));
16
16
  const config_1 = require("../config");
17
17
  const SocketEvents_1 = require("../enums/SocketEvents");
18
+ const outboundProxy_1 = require("../helpers/outboundProxy");
18
19
  let SocketClient = class SocketClient {
19
20
  constructor() {
20
21
  this.socket = null;
@@ -46,6 +47,16 @@ let SocketClient = class SocketClient {
46
47
  reconnectionDelay: 1000,
47
48
  reconnectionDelayMax: 5000,
48
49
  auth: socketAuth,
50
+ // The proxy the node's other calls to its hub take (its phone reports,
51
+ // its JWKS fetch); it used to go straight to the hub whatever the
52
+ // environment said. Polling goes as plain requests, as axios sends
53
+ // them; only the WebSocket upgrade needs a CONNECT tunnel, and when the
54
+ // proxy refuses one (a stock Squid allows port 443 only) the
55
+ // connection stays on polling.
56
+ transportOptions: {
57
+ polling: { agent: (0, outboundProxy_1.proxyAgentFor)(normalizedHubUrl) },
58
+ websocket: { agent: (0, outboundProxy_1.socketProxyAgentFor)(normalizedHubUrl) },
59
+ },
49
60
  });
50
61
  this.socket.on('connect', () => {
51
62
  var _a, _b, _c;