@xenon-device-management/xenon 2.12.0 → 2.13.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 (140) hide show
  1. package/lib/package.json +2 -2
  2. package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
  6. package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
  9. package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
  10. package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
  15. package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
  16. package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
  17. package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
  18. package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
  19. package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
  21. package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
  24. package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
  25. package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
  26. package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
  27. package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
  29. package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
  30. package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
  31. package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
  32. package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
  33. package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
  34. package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
  35. package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
  36. package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
  37. package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
  38. package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
  39. package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
  40. package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
  41. package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
  42. package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
  43. package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
  44. package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
  45. package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
  46. package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
  47. package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
  48. package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
  49. package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
  50. package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
  51. package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
  52. package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
  53. package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
  54. package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
  55. package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
  56. package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
  57. package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
  58. package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
  59. package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
  60. package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
  61. package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
  62. package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
  63. package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
  64. package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
  65. package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
  66. package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
  67. package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
  68. package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
  69. package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
  70. package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
  71. package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
  72. package/lib/public/index.html +1 -1
  73. package/lib/src/app/apiErrors.js +118 -0
  74. package/lib/src/app/index.js +6 -1
  75. package/lib/src/app/openapi/control.yaml +3129 -0
  76. package/lib/src/app/openapi/grid.yaml +2295 -0
  77. package/lib/src/app/openapi/identity.yaml +2168 -0
  78. package/lib/src/app/openapi/platform.yaml +2885 -0
  79. package/lib/src/app/openapi/sessions.yaml +3784 -0
  80. package/lib/src/app/routers/bug-report.js +4 -1
  81. package/lib/src/app/routers/config.js +6 -107
  82. package/lib/src/app/routers/control.js +117 -59
  83. package/lib/src/app/routers/dashboard.js +13 -7
  84. package/lib/src/app/routers/grid.js +54 -14
  85. package/lib/src/app/routers/profile.js +27 -13
  86. package/lib/src/app/routers/recordings.js +11 -5
  87. package/lib/src/app/routers/reservation.js +63 -15
  88. package/lib/src/app/routers/users.js +4 -0
  89. package/lib/src/app/routers/webhook.js +17 -8
  90. package/lib/src/app/swagger.js +259 -177
  91. package/lib/src/data-service/device-service.js +4 -1
  92. package/lib/src/data-service/deviceFieldOwners.js +1 -0
  93. package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
  94. package/lib/src/device-managers/ios/WDAClient.js +32 -32
  95. package/lib/src/generated/client/edge.js +4 -3
  96. package/lib/src/generated/client/index-browser.js +1 -0
  97. package/lib/src/generated/client/index.d.ts +38 -0
  98. package/lib/src/generated/client/index.js +4 -3
  99. package/lib/src/generated/client/package.json +1 -1
  100. package/lib/src/generated/client/schema.prisma +2 -0
  101. package/lib/src/generated/client/wasm.js +1 -0
  102. package/lib/src/middleware/csrfMiddleware.js +13 -5
  103. package/lib/src/middleware/rateLimitMiddleware.js +19 -5
  104. package/lib/src/middleware/roleGuard.js +20 -0
  105. package/lib/src/services/AIService.js +13 -3
  106. package/lib/src/services/NotificationService.js +28 -28
  107. package/lib/src/services/bug-report/BugReportService.js +8 -2
  108. package/lib/src/services/lease/LeaseService.js +71 -20
  109. package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
  110. package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
  111. package/lib/test/helpers/expressRoutes.js +41 -0
  112. package/lib/test/integration/team-visibility-control.spec.js +2 -2
  113. package/lib/test/unit/access-scopes.spec.js +227 -0
  114. package/lib/test/unit/api-error-handling.spec.js +179 -0
  115. package/lib/test/unit/bug-report/route.spec.js +29 -0
  116. package/lib/test/unit/bug-report/service.spec.js +30 -0
  117. package/lib/test/unit/control-honest-answers.spec.js +134 -0
  118. package/lib/test/unit/device-allocation-routes.spec.js +232 -0
  119. package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
  120. package/lib/test/unit/install-repository-app-team.spec.js +4 -1
  121. package/lib/test/unit/lease/LeaseService.spec.js +5 -4
  122. package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
  123. package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
  124. package/lib/test/unit/omni-vision-failures.spec.js +81 -0
  125. package/lib/test/unit/openapi-coverage.spec.js +114 -0
  126. package/lib/test/unit/profile-router.test.js +42 -0
  127. package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
  128. package/lib/test/unit/recording-orchestrator.spec.js +80 -0
  129. package/lib/test/unit/recordings-library-routes.spec.js +37 -0
  130. package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
  131. package/lib/test/unit/reset-link.test.js +2 -1
  132. package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
  133. package/lib/test/unit/users-router.test.js +14 -1
  134. package/lib/test/unit/wda-client-failures.spec.js +90 -0
  135. package/lib/test/unit/webhook-delivery.spec.js +142 -0
  136. package/lib/tsconfig.tsbuildinfo +1 -1
  137. package/package.json +2 -2
  138. package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
  139. package/prisma/schema.prisma +2 -0
  140. package/lib/src/app/swagger-docs.js +0 -1701
@@ -0,0 +1,3784 @@
1
+ paths:
2
+ /api/session:
3
+ get:
4
+ operationId: listSessions
5
+ summary: List sessions
6
+ description: >-
7
+ Lists Appium sessions, newest first (by `createdAt`, then by `id`), each with who ran it
8
+ (`owner`) and where (`ranOn`). Only sessions the caller may see are listed: a member sees
9
+ the sessions on their teams' devices and the shared pool, plus their own sessions whose
10
+ device is no longer known; an admin sees all. The team rule is part of the query, so
11
+ `limit` counts the caller's sessions.
12
+
13
+
14
+ Page through the list with `before` and `beforeId`: pass the `createdAt` and `id` of the
15
+ last session of the previous page. A query parameter that can't be read is refused with
16
+ `400` rather than ignored.
17
+
18
+
19
+ Any signed-in user (role `MEMBER` or above) may call it.
20
+ tags:
21
+ - Sessions
22
+ parameters:
23
+ - in: query
24
+ name: buildId
25
+ schema:
26
+ type: string
27
+ description: Only this build's sessions.
28
+ example: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
29
+ - in: query
30
+ name: status
31
+ schema:
32
+ type: string
33
+ description: >-
34
+ Only sessions with exactly this stored status, for example `running`, `success`,
35
+ `passed`, `failed`, `error`, `timeout` or `ended`.
36
+ example: failed
37
+ - in: query
38
+ name: platform
39
+ schema:
40
+ type: string
41
+ description: Only sessions on this platform, as stored (`android` or `ios`).
42
+ example: android
43
+ - in: query
44
+ name: query
45
+ schema:
46
+ type: string
47
+ description: >-
48
+ Text contained in the session's id, name, device udid, device name, failure category
49
+ or tags.
50
+ example: Pixel 7
51
+ - in: query
52
+ name: ids
53
+ schema:
54
+ type: string
55
+ description: >-
56
+ Only these sessions: 1 to 200 session ids separated by commas. An empty list or more
57
+ than 200 ids is refused (`400 invalid_ids`).
58
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,0b7e4d21-55c6-4c0b-8f0e-6a1d9c3b2e10
59
+ - in: query
60
+ name: since
61
+ schema:
62
+ type: string
63
+ format: date-time
64
+ description: >-
65
+ Only sessions created at or after this time (ISO 8601). A value that isn't a date is
66
+ refused (`400 invalid_since`).
67
+ example: '2026-10-01T00:00:00.000Z'
68
+ - in: query
69
+ name: limit
70
+ schema:
71
+ type: integer
72
+ minimum: 1
73
+ default: 500
74
+ description: >-
75
+ Sessions per answer. A limit above 2000 is answered with 2000. A value that isn't a
76
+ whole number from 1 is refused (`400 invalid_limit`).
77
+ example: 100
78
+ - in: query
79
+ name: before
80
+ schema:
81
+ type: string
82
+ format: date-time
83
+ description: >-
84
+ Start of the next page: the `createdAt` of the last session of the previous page. A
85
+ value that isn't a date is refused (`400 invalid_before`).
86
+ example: '2026-10-03T14:22:05.120Z'
87
+ - in: query
88
+ name: beforeId
89
+ schema:
90
+ type: string
91
+ description: >-
92
+ The `id` of the last session of the previous page, used with `before` so sessions
93
+ created in the same millisecond are neither skipped nor repeated. Without it, the page
94
+ holds everything older than `before`.
95
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
96
+ responses:
97
+ '200':
98
+ description: Up to `limit` sessions, newest first.
99
+ content:
100
+ application/json:
101
+ schema:
102
+ type: array
103
+ items:
104
+ $ref: '#/components/schemas/SessionsSessionRecord'
105
+ example:
106
+ - id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
107
+ build_id: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
108
+ name: Checkout with saved card
109
+ status: failed
110
+ node_id: 3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d
111
+ has_live_video: true
112
+ video_recording_enabled: true
113
+ video_recording: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
114
+ startTime: '2026-10-03T14:20:11.004Z'
115
+ endTime: '2026-10-03T14:22:05.120Z'
116
+ failure_reason: 'NoSuchElementError: An element could not be located'
117
+ failure_category: element_not_found
118
+ device_udid: emulator-5554
119
+ device_platform: android
120
+ device_version: '14'
121
+ device_name: Pixel 7
122
+ createdAt: '2026-10-03T14:20:11.004Z'
123
+ updatedAt: '2026-10-03T14:22:05.311Z'
124
+ api_key_id: null
125
+ user_id: 5b8e2c14-0f3a-4d6b-9e7c-1a2b3c4d5e6f
126
+ owner:
127
+ name: Priya Raman
128
+ email: priya@example.com
129
+ ranOn: here
130
+ '400':
131
+ description: A query parameter can't be read.
132
+ content:
133
+ application/json:
134
+ schema:
135
+ $ref: '#/components/schemas/Error'
136
+ examples:
137
+ since:
138
+ value:
139
+ error: invalid_since
140
+ message: since must be an ISO 8601 date
141
+ limit:
142
+ value:
143
+ error: invalid_limit
144
+ message: limit must be a whole number from 1
145
+ before:
146
+ value:
147
+ error: invalid_before
148
+ message: before must be an ISO 8601 date
149
+ ids:
150
+ value:
151
+ error: invalid_ids
152
+ message: ids must list 1 to 200 session ids, separated by commas
153
+ '401':
154
+ $ref: '#/components/responses/Unauthorized'
155
+ '429':
156
+ $ref: '#/components/responses/RateLimited'
157
+ /api/session-summary:
158
+ get:
159
+ operationId: summarizeSessions
160
+ summary: Summarize sessions over a period
161
+ description: >-
162
+ The Sessions page's summary strip, over the sessions the caller may see (and of one build
163
+ when `buildId` is given):
164
+
165
+ - `current`: the period from `since` to now, with counts by outcome and the median and
166
+ 90th-percentile duration of the sessions in it that ended. Durations come from at most the
167
+ 10,000 newest ended sessions of the period.
168
+
169
+ - `previous`: the period of the same length just before it, or null without `since`.
170
+
171
+ - `runningNow`: sessions running now and the distinct devices they run on.
172
+
173
+
174
+ Outcomes: `success`, `passed` and `ended` count as passed; `failed`, `error` and `timeout`
175
+ as failed; `running` as running. Any other status counts only in `total`. Any signed-in
176
+ user (role `MEMBER` or above) may call it.
177
+ tags:
178
+ - Sessions
179
+ parameters:
180
+ - in: query
181
+ name: since
182
+ schema:
183
+ type: string
184
+ format: date-time
185
+ description: >-
186
+ Start of the period. Without it the period is all time and `previous` is null. A value
187
+ that isn't a date is refused (`400 invalid_since`).
188
+ example: '2026-09-27T00:00:00.000Z'
189
+ - in: query
190
+ name: buildId
191
+ schema:
192
+ type: string
193
+ description: Only this build's sessions.
194
+ example: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
195
+ responses:
196
+ '200':
197
+ description: The summary.
198
+ content:
199
+ application/json:
200
+ schema:
201
+ $ref: '#/components/schemas/SessionsSummary'
202
+ example:
203
+ since: '2026-09-27T00:00:00.000Z'
204
+ current:
205
+ total: 412
206
+ passed: 371
207
+ failed: 33
208
+ running: 4
209
+ medianMs: 94210
210
+ p90Ms: 241880
211
+ previous:
212
+ total: 388
213
+ passed: 340
214
+ failed: 45
215
+ running: 0
216
+ runningNow:
217
+ sessions: 4
218
+ devices: 4
219
+ '400':
220
+ description: '`since` is not a date.'
221
+ content:
222
+ application/json:
223
+ schema:
224
+ $ref: '#/components/schemas/Error'
225
+ example:
226
+ error: invalid_since
227
+ message: since must be an ISO 8601 date
228
+ '401':
229
+ $ref: '#/components/responses/Unauthorized'
230
+ '429':
231
+ $ref: '#/components/responses/RateLimited'
232
+ /api/session/{sessionId}:
233
+ get:
234
+ operationId: getSession
235
+ summary: Get one session
236
+ description: >-
237
+ Returns one session's stored record, with who ran it (`owner`) and where (`ranOn`), as the
238
+ list gives it. A session the caller may not see answers exactly like an unknown id: `404`.
239
+ Any signed-in user (role `MEMBER` or above) may call it.
240
+ tags:
241
+ - Sessions
242
+ parameters:
243
+ - $ref: '#/components/parameters/SessionsSessionId'
244
+ responses:
245
+ '200':
246
+ description: The session.
247
+ content:
248
+ application/json:
249
+ schema:
250
+ $ref: '#/components/schemas/SessionsSessionRecord'
251
+ example:
252
+ id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
253
+ build_id: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
254
+ name: Checkout with saved card
255
+ status: success
256
+ desired_capabilities: '{"platformName":"Android","appium:automationName":"UiAutomator2"}'
257
+ session_capabilities: '{"platformName":"Android","appium:udid":"emulator-5554"}'
258
+ node_id: 3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d
259
+ has_live_video: true
260
+ video_recording_enabled: true
261
+ video_recording: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
262
+ startTime: '2026-10-03T14:20:11.004Z'
263
+ endTime: '2026-10-03T14:21:45.214Z'
264
+ failure_reason: null
265
+ is_profiling_available: false
266
+ device_udid: emulator-5554
267
+ device_platform: android
268
+ device_version: '14'
269
+ device_name: Pixel 7
270
+ createdAt: '2026-10-03T14:20:11.004Z'
271
+ updatedAt: '2026-10-03T14:21:45.400Z'
272
+ user_id: 5b8e2c14-0f3a-4d6b-9e7c-1a2b3c4d5e6f
273
+ owner:
274
+ name: Priya Raman
275
+ email: priya@example.com
276
+ ranOn: here
277
+ '401':
278
+ $ref: '#/components/responses/Unauthorized'
279
+ '404':
280
+ $ref: '#/components/responses/SessionsSessionNotFound'
281
+ '429':
282
+ $ref: '#/components/responses/RateLimited'
283
+ /api/session/{sessionId}/session_log:
284
+ get:
285
+ operationId: listSessionCommands
286
+ summary: List a session's commands
287
+ description: >-
288
+ Every Appium command the session ran, newest first, as stored: the WebDriver request and
289
+ response, its duration, whether it failed, and, for a `findElement` that needed
290
+ self-healing, the original and healed selectors, the healing method and its confidence.
291
+ The whole list in one answer. A session the caller may not see answers `404`, like an
292
+ unknown id. Any signed-in user (role `MEMBER` or above) may call it.
293
+ tags:
294
+ - Sessions
295
+ parameters:
296
+ - $ref: '#/components/parameters/SessionsSessionId'
297
+ responses:
298
+ '200':
299
+ description: The session's commands, newest first.
300
+ content:
301
+ application/json:
302
+ schema:
303
+ type: array
304
+ items:
305
+ $ref: '#/components/schemas/SessionsCommand'
306
+ example:
307
+ - id: a3c5e7f9-1b2d-4f6a-8c0e-2d4f6a8c0e1b
308
+ session_id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
309
+ command_name: findElement
310
+ url: /session/6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b/element
311
+ method: POST
312
+ title: Find element
313
+ subtitle: 'accessibility id: login-btn'
314
+ body: '{"using":"accessibility id","value":"login-btn"}'
315
+ response: '{"element-6066-11e4-a52e-4f735466cecf":"00000000-0000-0011-ffff-ffff0000001c"}'
316
+ screenshot: null
317
+ is_success: true
318
+ is_error: false
319
+ is_healed: true
320
+ original_strategy: accessibility id
321
+ original_selector: login-btn
322
+ healed_strategy: xpath
323
+ healed_selector: //android.widget.Button[@text="Log in"]
324
+ healing_confidence: 0.92
325
+ healing_tier: Fuzzy XML
326
+ duration: 1840
327
+ createdAt: '2026-10-03T14:20:31.512Z'
328
+ updatedAt: '2026-10-03T14:20:31.512Z'
329
+ span_id: null
330
+ trace_id: null
331
+ '401':
332
+ $ref: '#/components/responses/Unauthorized'
333
+ '404':
334
+ $ref: '#/components/responses/SessionsSessionNotFound'
335
+ '429':
336
+ $ref: '#/components/responses/RateLimited'
337
+ /api/session/{sessionId}/logs/device:
338
+ get:
339
+ operationId: listSessionDeviceLogs
340
+ summary: List a session's device log lines
341
+ description: >-
342
+ The device log lines (logcat or syslog) stored for the session, oldest first. A session
343
+ the caller may not see answers `404`, like an unknown id. Any signed-in user (role
344
+ `MEMBER` or above) may call it.
345
+ tags:
346
+ - Sessions
347
+ parameters:
348
+ - $ref: '#/components/parameters/SessionsSessionId'
349
+ responses:
350
+ '200':
351
+ description: The device log lines, oldest first.
352
+ content:
353
+ application/json:
354
+ schema:
355
+ type: array
356
+ items:
357
+ $ref: '#/components/schemas/SessionsLogLine'
358
+ example:
359
+ - id: 1e2d3c4b-5a69-4788-97a6-b5c4d3e2f1a0
360
+ session_id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
361
+ log_type: DEVICE
362
+ message: '10-03 14:20:12.345 1234 1250 I ActivityManager: Start proc com.example.shop'
363
+ timestamp: '2026-10-03T14:20:12.345Z'
364
+ createdAt: '2026-10-03T14:20:12.401Z'
365
+ updatedAt: '2026-10-03T14:20:12.401Z'
366
+ '401':
367
+ $ref: '#/components/responses/Unauthorized'
368
+ '404':
369
+ $ref: '#/components/responses/SessionsSessionNotFound'
370
+ '429':
371
+ $ref: '#/components/responses/RateLimited'
372
+ /api/session/{sessionId}/logs/debug:
373
+ get:
374
+ operationId: listSessionDebugLogs
375
+ summary: List a session's debug log lines
376
+ description: >-
377
+ Xenon's and the driver's debug log lines stored for the session, oldest first. A session
378
+ the caller may not see answers `404`, like an unknown id. Any signed-in user (role
379
+ `MEMBER` or above) may call it.
380
+ tags:
381
+ - Sessions
382
+ parameters:
383
+ - $ref: '#/components/parameters/SessionsSessionId'
384
+ responses:
385
+ '200':
386
+ description: The debug log lines, oldest first.
387
+ content:
388
+ application/json:
389
+ schema:
390
+ type: array
391
+ items:
392
+ $ref: '#/components/schemas/SessionsLogLine'
393
+ example:
394
+ - id: 7c6b5a49-3827-4615-8f4e-3d2c1b0a9f8e
395
+ session_id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
396
+ log_type: DEBUG
397
+ message: '[HealingOrchestrator] findElement healed by Fuzzy XML (0.92)'
398
+ timestamp: '2026-10-03T14:20:31.510Z'
399
+ createdAt: '2026-10-03T14:20:31.512Z'
400
+ updatedAt: '2026-10-03T14:20:31.512Z'
401
+ '401':
402
+ $ref: '#/components/responses/Unauthorized'
403
+ '404':
404
+ $ref: '#/components/responses/SessionsSessionNotFound'
405
+ '429':
406
+ $ref: '#/components/responses/RateLimited'
407
+ /api/session/{sessionId}/profiling:
408
+ get:
409
+ operationId: listSessionProfiling
410
+ summary: List a session's legacy profiling samples
411
+ description: >-
412
+ The older profiling samples (CPU and memory as text) stored for the session, oldest first.
413
+ Only sessions recorded before version 2.10 have any; newer sessions answer an empty list
414
+ and keep their figures in `GET /api/session/{sessionId}/metrics`. A session the caller may
415
+ not see answers `404`, like an unknown id. Any signed-in user (role `MEMBER` or above) may
416
+ call it.
417
+ tags:
418
+ - Sessions
419
+ parameters:
420
+ - $ref: '#/components/parameters/SessionsSessionId'
421
+ responses:
422
+ '200':
423
+ description: The profiling samples, oldest first; often empty.
424
+ content:
425
+ application/json:
426
+ schema:
427
+ type: array
428
+ items:
429
+ $ref: '#/components/schemas/SessionsProfilingSample'
430
+ example:
431
+ - id: 1841
432
+ session_id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
433
+ cpu: '12.5'
434
+ memory: '184320'
435
+ total_cpu_used: '38.0'
436
+ total_memory_used: '5120000'
437
+ raw_cpu_log: null
438
+ raw_memory_log: null
439
+ timestamp: '2026-10-03T14:20:15.000Z'
440
+ createdAt: '2026-10-03T14:20:15.020Z'
441
+ updatedAt: '2026-10-03T14:20:15.020Z'
442
+ '401':
443
+ $ref: '#/components/responses/Unauthorized'
444
+ '404':
445
+ $ref: '#/components/responses/SessionsSessionNotFound'
446
+ '429':
447
+ $ref: '#/components/responses/RateLimited'
448
+ /api/session/{sessionId}/metrics:
449
+ get:
450
+ operationId: getSessionMetrics
451
+ summary: Get a session's CPU and memory samples
452
+ description: >-
453
+ The CPU and memory samples recorded for the session every 2 seconds, oldest first, for the
454
+ session page's Performance panel.
455
+
456
+
457
+ - `series` says what the platform can record: Android has device CPU and memory and the
458
+ app's CPU and memory; an iPhone has device CPU only; any other platform nothing.
459
+
460
+ - `recording` is set only while the session runs: `sampling` (being sampled), `stopped`
461
+ (sampling gave up after repeated failures) or `off` (nothing samples it, for example with
462
+ session metrics turned off). It is null once the session has ended.
463
+
464
+ - `appId` is the app the newest app figures are for; each sample also names its own `app`,
465
+ since the foreground app can change.
466
+
467
+
468
+ Samples are written in batches every 10 seconds, so a running session's newest samples can
469
+ be up to 10 seconds behind. A session on a node's device is collected from the node by the
470
+ hub in the same way. A session the caller may not see answers `404`, like an unknown id.
471
+ Any signed-in user (role `MEMBER` or above) may call it.
472
+ tags:
473
+ - Sessions
474
+ parameters:
475
+ - $ref: '#/components/parameters/SessionsSessionId'
476
+ responses:
477
+ '200':
478
+ description: The samples, and what the platform records.
479
+ content:
480
+ application/json:
481
+ schema:
482
+ $ref: '#/components/schemas/SessionsMetrics'
483
+ example:
484
+ platform: android
485
+ intervalMs: 2000
486
+ appId: com.example.shop
487
+ series:
488
+ deviceCpu: true
489
+ deviceMem: true
490
+ appCpu: true
491
+ appMem: true
492
+ recording: sampling
493
+ samples:
494
+ - t: 1759501213000
495
+ deviceCpu: 23.4
496
+ deviceMemMb: 3120.5
497
+ deviceMemTotalMb: 7680
498
+ appCpu: 8.1
499
+ appMemMb: 212.7
500
+ app: com.example.shop
501
+ - t: 1759501215000
502
+ deviceCpu: 31.9
503
+ deviceMemMb: 3134.2
504
+ deviceMemTotalMb: 7680
505
+ appCpu: 14.6
506
+ appMemMb: 219.3
507
+ app: com.example.shop
508
+ '401':
509
+ $ref: '#/components/responses/Unauthorized'
510
+ '404':
511
+ $ref: '#/components/responses/SessionsSessionNotFound'
512
+ '429':
513
+ $ref: '#/components/responses/RateLimited'
514
+ /api/session/{sessionId}/asset/{kind}/{file}:
515
+ get:
516
+ operationId: getSessionAsset
517
+ summary: Download a session's screenshot, video or trace
518
+ description: >-
519
+ Serves one stored file of the session: a command screenshot (`screenshots`), the session
520
+ video (`video`, an mp4 named after the session id) or a performance trace (`performance`).
521
+ The content type follows the file's extension. `Range` requests are honoured, so a video
522
+ can be seeked; an unsatisfiable range answers `416` with no body. Answers carry
523
+ `Cache-Control: private, max-age=300`.
524
+
525
+
526
+ Session files are served only through this route. A session the caller may not see answers
527
+ `404`, like an unknown id. An unknown `kind`, a `file` that isn't a plain file name (no
528
+ path, no leading dot), or a missing file answers `404` too. Any signed-in user (role
529
+ `MEMBER` or above) may call it.
530
+ tags:
531
+ - Sessions
532
+ parameters:
533
+ - $ref: '#/components/parameters/SessionsSessionId'
534
+ - in: path
535
+ name: kind
536
+ required: true
537
+ schema:
538
+ type: string
539
+ enum:
540
+ - screenshots
541
+ - video
542
+ - performance
543
+ description: Which of the session's folders.
544
+ example: video
545
+ - in: path
546
+ name: file
547
+ required: true
548
+ schema:
549
+ type: string
550
+ pattern: '^[A-Za-z0-9_-][A-Za-z0-9._-]*$'
551
+ description: The file's name within that folder.
552
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
553
+ - in: header
554
+ name: Range
555
+ required: false
556
+ schema:
557
+ type: string
558
+ description: A byte range, for seeking in a video.
559
+ example: bytes=0-1048575
560
+ responses:
561
+ '200':
562
+ description: The whole file.
563
+ headers:
564
+ Accept-Ranges:
565
+ schema:
566
+ type: string
567
+ example: bytes
568
+ Cache-Control:
569
+ schema:
570
+ type: string
571
+ example: private, max-age=300
572
+ content:
573
+ video/mp4:
574
+ schema:
575
+ type: string
576
+ format: binary
577
+ image/png:
578
+ schema:
579
+ type: string
580
+ format: binary
581
+ application/zip:
582
+ schema:
583
+ type: string
584
+ format: binary
585
+ application/octet-stream:
586
+ schema:
587
+ type: string
588
+ format: binary
589
+ '206':
590
+ description: The requested byte range.
591
+ headers:
592
+ Content-Range:
593
+ schema:
594
+ type: string
595
+ example: bytes 0-1048575/48211967
596
+ content:
597
+ video/mp4:
598
+ schema:
599
+ type: string
600
+ format: binary
601
+ '401':
602
+ $ref: '#/components/responses/Unauthorized'
603
+ '404':
604
+ description: >-
605
+ The session is unknown or hidden (the session's not-found answer), or the file isn't
606
+ there.
607
+ content:
608
+ application/json:
609
+ schema:
610
+ $ref: '#/components/schemas/Error'
611
+ examples:
612
+ file:
613
+ value:
614
+ error: true
615
+ message: Session asset not found
616
+ session:
617
+ value:
618
+ error: true
619
+ message: Session with id 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b not found
620
+ '416':
621
+ description: The requested range is outside the file. No body.
622
+ '429':
623
+ $ref: '#/components/responses/RateLimited'
624
+ /api/session/{sessionId}/live_video:
625
+ get:
626
+ operationId: streamSessionLiveVideo
627
+ summary: Stream a running session's live video
628
+ description: >-
629
+ An MJPEG stream (`multipart/x-mixed-replace`) of a running session's screen, relayed from
630
+ the driver's MJPEG server. One upstream connection is shared by every viewer of the
631
+ session. It works only for a session this server is driving right now and whose driver
632
+ offers an MJPEG port; otherwise it answers `500`. `503` (plain text) means the upstream
633
+ didn't start sending in time.
634
+
635
+
636
+ A session the caller may not see answers `404`, like an unknown id. Any signed-in user
637
+ (role `MEMBER` or above) may call it.
638
+ tags:
639
+ - Sessions
640
+ parameters:
641
+ - $ref: '#/components/parameters/SessionsSessionId'
642
+ responses:
643
+ '200':
644
+ description: The MJPEG stream; it lasts as long as the viewer stays connected.
645
+ content:
646
+ multipart/x-mixed-replace:
647
+ schema:
648
+ type: string
649
+ format: binary
650
+ '401':
651
+ $ref: '#/components/responses/Unauthorized'
652
+ '404':
653
+ $ref: '#/components/responses/SessionsSessionNotFound'
654
+ '429':
655
+ $ref: '#/components/responses/RateLimited'
656
+ '500':
657
+ description: The session isn't running here, or has no live video.
658
+ content:
659
+ application/json:
660
+ schema:
661
+ $ref: '#/components/schemas/Error'
662
+ example:
663
+ error: true
664
+ message: Live video not available for session with id 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
665
+ '503':
666
+ description: The upstream video source wasn't available in time.
667
+ content:
668
+ text/plain:
669
+ schema:
670
+ type: string
671
+ example: '[MjpegProxy] Source not available after timeout'
672
+ /api/sessions/active:
673
+ get:
674
+ operationId: listActiveSessions
675
+ summary: List the sessions this server is running
676
+ description: >-
677
+ The sessions this server holds in memory right now: its own local sessions, and on a hub
678
+ the sessions it routes to nodes (`remote`) or cloud providers (`cloud`). `sessions` is
679
+ filtered to the devices the caller may see; a session whose device has no row is left out
680
+ for a member. `stats` counts the sessions listed, so a member's counts are only theirs
681
+ to see; through 2.12 they counted every team's.
682
+ Any signed-in user (role `MEMBER` or above) may call it.
683
+ tags:
684
+ - Sessions
685
+ responses:
686
+ '200':
687
+ description: The server's live sessions.
688
+ content:
689
+ application/json:
690
+ schema:
691
+ $ref: '#/components/schemas/SessionsActiveList'
692
+ example:
693
+ stats:
694
+ total: 3
695
+ byType:
696
+ local: 2
697
+ remote: 1
698
+ cloud: 0
699
+ sessions:
700
+ - id: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
701
+ type: local
702
+ deviceUdid: emulator-5554
703
+ deviceName: Pixel 7
704
+ platform: android
705
+ - id: 0b7e4d21-55c6-4c0b-8f0e-6a1d9c3b2e10
706
+ type: remote
707
+ deviceUdid: 00008110-00084CE80E51401E
708
+ deviceName: iPhone 14 Pro
709
+ platform: ios
710
+ '401':
711
+ $ref: '#/components/responses/Unauthorized'
712
+ '429':
713
+ $ref: '#/components/responses/RateLimited'
714
+ /api/sessions/{sessionId}/bug-report:
715
+ post:
716
+ operationId: createSessionBugReport
717
+ summary: Download a bug report bundle for a session
718
+ description: >-
719
+ Builds a zip for a session and streams it as a download. The zip holds `manifest.json`
720
+ (session, device, capabilities, last command, the time window and any warnings),
721
+ `README.md` and `logs.txt`, plus `video.mp4`, `network.har` and `ai-summary.txt` when the
722
+ session has them. `network.har`, every request the app made with its headers and bodies,
723
+ is added only for an `ADMIN` or `SUPER_ADMIN`, who may read it on `/api/interceptor`; for
724
+ anyone else the manifest's warnings say it was left out. Through 2.12 every caller who
725
+ could see the session got it.
726
+
727
+
728
+ - `mode=slice` covers only the last `windowSec` seconds of the session (5 to 600, default
729
+ 60), video and logs included.
730
+
731
+ - `mode=full` covers the whole session; `windowSec` is ignored.
732
+
733
+
734
+ When the download finishes, dashboards that can see the session's device get a
735
+ `bug report generated` live event. A session the caller may not see answers `404`, like
736
+ an unknown one. Any signed-in user (role `MEMBER` or above) may call it. With the dashboard
737
+ cookie, the request needs a same-host `Origin` or `Referer`.
738
+ tags:
739
+ - Sessions
740
+ parameters:
741
+ - in: path
742
+ name: sessionId
743
+ required: true
744
+ schema:
745
+ type: string
746
+ description: The Appium session id.
747
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
748
+ - in: query
749
+ name: mode
750
+ required: true
751
+ schema:
752
+ type: string
753
+ enum:
754
+ - slice
755
+ - full
756
+ description: The last few seconds, or the whole session.
757
+ example: slice
758
+ - in: query
759
+ name: windowSec
760
+ required: false
761
+ schema:
762
+ type: number
763
+ minimum: 5
764
+ maximum: 600
765
+ default: 60
766
+ description: With `mode=slice`, how many seconds before the session's end to cover.
767
+ example: 120
768
+ responses:
769
+ '200':
770
+ description: >-
771
+ The zip, as an attachment named `bugreport-<sessionId>-<generatedAt>.zip`.
772
+ headers:
773
+ Content-Disposition:
774
+ schema:
775
+ type: string
776
+ example: attachment; filename="bugreport-6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b-2026-10-03T14-30-02-114Z.zip"
777
+ content:
778
+ application/zip:
779
+ schema:
780
+ type: string
781
+ format: binary
782
+ '400':
783
+ description: '`mode` is missing or unknown, or `windowSec` is out of range.'
784
+ content:
785
+ application/json:
786
+ schema:
787
+ $ref: '#/components/schemas/Error'
788
+ examples:
789
+ mode:
790
+ value:
791
+ error: mode must be "slice" or "full"
792
+ windowSec:
793
+ value:
794
+ error: windowSec must be between 5 and 600
795
+ '401':
796
+ $ref: '#/components/responses/Unauthorized'
797
+ '403':
798
+ $ref: '#/components/responses/Forbidden'
799
+ '404':
800
+ description: The session is unknown, or one the caller may not see.
801
+ content:
802
+ application/json:
803
+ schema:
804
+ $ref: '#/components/schemas/Error'
805
+ example:
806
+ error: Session 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b not found
807
+ '429':
808
+ $ref: '#/components/responses/RateLimited'
809
+ '500':
810
+ description: The bundle couldn't be assembled.
811
+ content:
812
+ application/json:
813
+ schema:
814
+ $ref: '#/components/schemas/Error'
815
+ example:
816
+ error: 'ENOENT: no such file or directory'
817
+ /api/build:
818
+ get:
819
+ operationId: listBuilds
820
+ summary: List builds
821
+ description: >-
822
+ Lists builds, newest first, with their session counts by outcome. Builds are shared by
823
+ name, so one build can hold several teams' sessions: a member sees a build only if at
824
+ least one of its sessions is visible to them, and its counts cover those sessions only.
825
+ An admin sees every build and every session. `success`, `passed` and `ended` count as
826
+ passed; `failed`, `error` and `timeout` as failed. Any signed-in user (role `MEMBER` or
827
+ above) may call it.
828
+ tags:
829
+ - Builds
830
+ responses:
831
+ '200':
832
+ description: The builds, newest first.
833
+ content:
834
+ application/json:
835
+ schema:
836
+ type: array
837
+ items:
838
+ $ref: '#/components/schemas/SessionsBuildRecord'
839
+ example:
840
+ - id: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
841
+ name: nightly-2026-10-03
842
+ createdAt: '2026-10-03T02:00:04.881Z'
843
+ updatedAt: '2026-10-03T02:00:04.881Z'
844
+ _count:
845
+ sessions: 48
846
+ sessionCount: 48
847
+ passedCount: 44
848
+ failedCount: 3
849
+ runningCount: 1
850
+ '401':
851
+ $ref: '#/components/responses/Unauthorized'
852
+ '429':
853
+ $ref: '#/components/responses/RateLimited'
854
+ /api/build/{buildId}/export:
855
+ post:
856
+ operationId: exportBuildSessions
857
+ summary: Export a build's sessions as JSON or CSV
858
+ description: >-
859
+ Downloads up to 5000 of the build's sessions, newest first, as a JSON array of stored
860
+ session records or as CSV with the columns `id`, `build_id`, `status`,
861
+ `failure_category`, `failure_reason`, `device_platform`, `device_version`, `device_name`,
862
+ `node_id`, `startTime`, `endTime` and `name` (dates in ISO 8601). Give `sessionIds` to
863
+ export only those sessions of the build.
864
+
865
+
866
+ A member gets only the sessions on devices of their teams or the shared pool; a session
867
+ whose device no longer has a row is left out. An unknown build, or one with no visible
868
+ sessions, gives an empty export rather than `404`. Any signed-in user (role `MEMBER` or
869
+ above) may call it. With the dashboard cookie, the request needs a same-host `Origin` or
870
+ `Referer`.
871
+ tags:
872
+ - Builds
873
+ parameters:
874
+ - in: path
875
+ name: buildId
876
+ required: true
877
+ schema:
878
+ type: string
879
+ description: The build's id.
880
+ example: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
881
+ requestBody:
882
+ required: false
883
+ content:
884
+ application/json:
885
+ schema:
886
+ type: object
887
+ properties:
888
+ format:
889
+ type: string
890
+ enum:
891
+ - json
892
+ - csv
893
+ default: json
894
+ sessionIds:
895
+ type: array
896
+ maxItems: 5000
897
+ items:
898
+ type: string
899
+ description: Only these sessions of the build. An empty list means all of them.
900
+ example:
901
+ format: csv
902
+ sessionIds:
903
+ - 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
904
+ - 0b7e4d21-55c6-4c0b-8f0e-6a1d9c3b2e10
905
+ responses:
906
+ '200':
907
+ description: >-
908
+ The export, as an attachment named `build-<buildId>-sessions.json` or
909
+ `build-<buildId>-sessions.csv`.
910
+ headers:
911
+ Content-Disposition:
912
+ schema:
913
+ type: string
914
+ example: attachment; filename="build-9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c-sessions.csv"
915
+ content:
916
+ application/json:
917
+ schema:
918
+ type: array
919
+ items:
920
+ $ref: '#/components/schemas/Session'
921
+ text/csv:
922
+ schema:
923
+ type: string
924
+ example: |
925
+ id,build_id,status,failure_category,failure_reason,device_platform,device_version,device_name,node_id,startTime,endTime,name
926
+ 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,element_not_found,"NoSuchElementError: An element could not be located",android,14,Pixel 7,3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d,2026-10-03T14:20:11.004Z,2026-10-03T14:22:05.120Z,Checkout with saved card
927
+ '400':
928
+ description: '`format` is neither `json` nor `csv`.'
929
+ content:
930
+ application/json:
931
+ schema:
932
+ $ref: '#/components/schemas/Error'
933
+ example:
934
+ error: format must be 'json' or 'csv'
935
+ '401':
936
+ $ref: '#/components/responses/Unauthorized'
937
+ '403':
938
+ $ref: '#/components/responses/Forbidden'
939
+ '413':
940
+ description: More than 5000 `sessionIds`.
941
+ content:
942
+ application/json:
943
+ schema:
944
+ $ref: '#/components/schemas/Error'
945
+ example:
946
+ error: sessionIds too large (max 5000)
947
+ '429':
948
+ $ref: '#/components/responses/RateLimited'
949
+ /api/logs/requests:
950
+ get:
951
+ operationId: listInternalRequestLogs
952
+ summary: List Xenon's recent outgoing HTTP calls
953
+ description: >-
954
+ The newest of the last 500 HTTP calls this server made itself (to nodes, drivers and other
955
+ services), newest first, with statistics over all 500. Bodies have secrets redacted, long
956
+ strings and arrays truncated. The log holds every internal call, including device control
957
+ forwarded to nodes (with what was typed) and forwarded new sessions, so it needs role
958
+ `ADMIN` and the `admin` scope. It lives in memory and is lost on restart.
959
+ tags:
960
+ - Admin
961
+ parameters:
962
+ - in: query
963
+ name: limit
964
+ schema:
965
+ type: integer
966
+ minimum: 1
967
+ default: 50
968
+ description: How many calls to return; the buffer holds at most 500.
969
+ example: 100
970
+ - in: query
971
+ name: method
972
+ schema:
973
+ type: string
974
+ description: Only calls with exactly this method (case-sensitive), for example `POST`.
975
+ example: POST
976
+ - in: query
977
+ name: url
978
+ schema:
979
+ type: string
980
+ description: Only calls whose URL contains this text.
981
+ example: /wd/hub/session
982
+ - in: query
983
+ name: hasError
984
+ schema:
985
+ type: string
986
+ enum:
987
+ - 'true'
988
+ - 'false'
989
+ description: '`true` for only failed calls, `false` for only calls without an error.'
990
+ example: 'true'
991
+ responses:
992
+ '200':
993
+ description: The calls and the buffer's statistics.
994
+ content:
995
+ application/json:
996
+ schema:
997
+ $ref: '#/components/schemas/SessionsRequestLogList'
998
+ example:
999
+ stats:
1000
+ totalLogged: 500
1001
+ errorCount: 7
1002
+ avgDurationMs: 84
1003
+ byMethod:
1004
+ GET: 311
1005
+ POST: 182
1006
+ DELETE: 7
1007
+ byStatusCode:
1008
+ '200': 486
1009
+ '404': 7
1010
+ logs:
1011
+ - timestamp: '2026-10-03T14:30:02.114Z'
1012
+ direction: outgoing
1013
+ method: POST
1014
+ url: http://192.168.1.21:4723/wd/hub/session/0b7e4d21-55c6-4c0b-8f0e-6a1d9c3b2e10/element
1015
+ requestBody: '{"using":"accessibility id","value":"login-btn"}'
1016
+ responseBody: '{"value":{"error":"no such element"}}'
1017
+ statusCode: 404
1018
+ durationMs: 212
1019
+ error: Request failed with status code 404
1020
+ '401':
1021
+ $ref: '#/components/responses/Unauthorized'
1022
+ '403':
1023
+ $ref: '#/components/responses/Forbidden'
1024
+ '429':
1025
+ $ref: '#/components/responses/RateLimited'
1026
+ /api/healing/events:
1027
+ get:
1028
+ operationId: listRecentHealingEvents
1029
+ summary: List recent self-healing events
1030
+ description: >-
1031
+ The newest heals (commands whose selector was found only through self-healing), newest
1032
+ first, and `todayCount`, the number of heals since the start of the server's local day.
1033
+ Only heals in sessions the caller may see count: another team's session, even named by
1034
+ `sessionId`, reads as one with no heals. Any signed-in user (role `MEMBER` or above) may
1035
+ call it.
1036
+ tags:
1037
+ - Selector Health
1038
+ parameters:
1039
+ - in: query
1040
+ name: limit
1041
+ schema:
1042
+ type: integer
1043
+ minimum: 1
1044
+ maximum: 200
1045
+ default: 50
1046
+ description: How many heals to return; clamped to 1–200.
1047
+ example: 20
1048
+ - in: query
1049
+ name: sessionId
1050
+ schema:
1051
+ type: string
1052
+ description: Only this session's heals. `todayCount` still counts all the caller's heals.
1053
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
1054
+ responses:
1055
+ '200':
1056
+ description: The heals, newest first.
1057
+ content:
1058
+ application/json:
1059
+ schema:
1060
+ type: object
1061
+ properties:
1062
+ events:
1063
+ type: array
1064
+ items:
1065
+ $ref: '#/components/schemas/SelectorHealthEvent'
1066
+ todayCount:
1067
+ type: integer
1068
+ example:
1069
+ events:
1070
+ - id: a3c5e7f9-1b2d-4f6a-8c0e-2d4f6a8c0e1b
1071
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
1072
+ deviceUdid: emulator-5554
1073
+ deviceName: Pixel 7
1074
+ devicePlatform: android
1075
+ commandName: findElement
1076
+ originalSelector: login-btn
1077
+ healedSelector: //android.widget.Button[@text="Log in"]
1078
+ confidence: 0.92
1079
+ tier: Fuzzy XML
1080
+ isSuccess: true
1081
+ createdAt: '2026-10-03T14:20:31.512Z'
1082
+ todayCount: 17
1083
+ '401':
1084
+ $ref: '#/components/responses/Unauthorized'
1085
+ '429':
1086
+ $ref: '#/components/responses/RateLimited'
1087
+ /api/healing/summary:
1088
+ get:
1089
+ operationId: summarizeSelectorHealth
1090
+ summary: Summarize heals over a period
1091
+ description: >-
1092
+ Selector Health's summary: heals in the last `windowDays` days (`current`) and in the
1093
+ same number of days before that (`prior`), with distinct selectors, sessions touched,
1094
+ heals per healing method and `timeSpentMs` (the recorded duration of the commands that
1095
+ needed healing). `trend` gives heals and AI heals (Visual AI or LLM) per day of the
1096
+ period in the caller's time zone, days with none included.
1097
+
1098
+
1099
+ Heal counts cover only sessions the caller may see. `resolvedCount` (selectors verified
1100
+ fixed in the period) and `pendingCount` (selectors being verified now) are lab-wide. Any
1101
+ signed-in user (role `MEMBER` or above) may call it.
1102
+ tags:
1103
+ - Selector Health
1104
+ parameters:
1105
+ - $ref: '#/components/parameters/SelectorHealthWindowDays'
1106
+ - $ref: '#/components/parameters/SelectorHealthTz'
1107
+ responses:
1108
+ '200':
1109
+ description: The summary.
1110
+ content:
1111
+ application/json:
1112
+ schema:
1113
+ $ref: '#/components/schemas/SelectorHealthSummary'
1114
+ example:
1115
+ windowDays: 7
1116
+ current:
1117
+ totalHeals: 126
1118
+ distinctSelectors: 14
1119
+ sessionsTouched: 58
1120
+ byTier:
1121
+ Resilio: 71
1122
+ Fuzzy XML: 38
1123
+ OCR: 9
1124
+ LLM: 8
1125
+ timeSpentMs: 412870
1126
+ prior:
1127
+ totalHeals: 151
1128
+ distinctSelectors: 17
1129
+ sessionsTouched: 63
1130
+ byTier:
1131
+ Resilio: 80
1132
+ Fuzzy XML: 52
1133
+ LLM: 19
1134
+ timeSpentMs: 538114
1135
+ resolvedCount: 3
1136
+ pendingCount: 2
1137
+ trend:
1138
+ - t: 1758931200000
1139
+ heals: 21
1140
+ aiHeals: 2
1141
+ - t: 1759017600000
1142
+ heals: 17
1143
+ aiHeals: 1
1144
+ '401':
1145
+ $ref: '#/components/responses/Unauthorized'
1146
+ '429':
1147
+ $ref: '#/components/responses/RateLimited'
1148
+ /api/healing/hotspots:
1149
+ get:
1150
+ operationId: listHealingHotspots
1151
+ summary: List the most-healed selectors
1152
+ description: >-
1153
+ The selectors healed most often in the last `windowDays` days, grouped by strategy and
1154
+ selector, most heals first (then most recent), with the most common healed selector as a
1155
+ suggested rewrite and the selector's lifecycle state. It reads at most the newest 5000
1156
+ heals of the period (`totalScanned`), so on a busy lab the counts are of those. The
1157
+ Selector Health page itself uses `GET /api/healing/selectors`, which counts every heal.
1158
+
1159
+
1160
+ `status` filters by lifecycle state, after `limit` is applied: `active` (the default:
1161
+ never marked, or active again) hides selectors being verified, fixed or muted. Any other
1162
+ value is treated as `active`. Only heals in sessions the caller may see count; the `state`
1163
+ row is lab-wide. Any signed-in user (role `MEMBER` or above) may call it.
1164
+ tags:
1165
+ - Selector Health
1166
+ parameters:
1167
+ - $ref: '#/components/parameters/SelectorHealthWindowDays'
1168
+ - in: query
1169
+ name: limit
1170
+ schema:
1171
+ type: integer
1172
+ minimum: 1
1173
+ maximum: 100
1174
+ default: 20
1175
+ description: How many selectors; clamped to 1–100.
1176
+ example: 10
1177
+ - in: query
1178
+ name: status
1179
+ schema:
1180
+ type: string
1181
+ enum:
1182
+ - active
1183
+ - pending
1184
+ - resolved
1185
+ - muted
1186
+ - all
1187
+ default: active
1188
+ description: >-
1189
+ Lifecycle state: `pending` is being verified, `resolved` is verified fixed.
1190
+ example: active
1191
+ - in: query
1192
+ name: tier
1193
+ schema:
1194
+ type: string
1195
+ description: Only heals by this healing method, for example `LLM` or `Fuzzy XML`.
1196
+ example: LLM
1197
+ - in: query
1198
+ name: platform
1199
+ schema:
1200
+ type: string
1201
+ description: Only heals in sessions on this platform.
1202
+ example: android
1203
+ responses:
1204
+ '200':
1205
+ description: The hotspots.
1206
+ content:
1207
+ application/json:
1208
+ schema:
1209
+ type: object
1210
+ properties:
1211
+ windowDays:
1212
+ type: integer
1213
+ totalScanned:
1214
+ type: integer
1215
+ description: Heals read, at most 5000.
1216
+ filters:
1217
+ type: object
1218
+ properties:
1219
+ tier:
1220
+ type: string
1221
+ nullable: true
1222
+ platform:
1223
+ type: string
1224
+ nullable: true
1225
+ status:
1226
+ type: string
1227
+ hotspots:
1228
+ type: array
1229
+ items:
1230
+ $ref: '#/components/schemas/SelectorHealthHotspot'
1231
+ example:
1232
+ windowDays: 30
1233
+ totalScanned: 412
1234
+ filters:
1235
+ tier: null
1236
+ platform: android
1237
+ status: active
1238
+ hotspots:
1239
+ - originalStrategy: accessibility id
1240
+ originalSelector: login-btn
1241
+ healCount: 41
1242
+ sessionCount: 29
1243
+ topTier: Fuzzy XML
1244
+ suggestedRewrite: //android.widget.Button[@text="Log in"]
1245
+ suggestedStrategy: xpath
1246
+ suggestedRewriteShare: 0.88
1247
+ averageConfidence: 0.91
1248
+ firstHealedAt: '2026-09-05T08:12:44.019Z'
1249
+ lastHealedAt: '2026-10-03T14:20:31.512Z'
1250
+ state: null
1251
+ '401':
1252
+ $ref: '#/components/responses/Unauthorized'
1253
+ '429':
1254
+ $ref: '#/components/responses/RateLimited'
1255
+ /api/healing/hotspots/violations:
1256
+ get:
1257
+ operationId: listHealingViolations
1258
+ summary: List selectors over a heal threshold, for CI
1259
+ description: >-
1260
+ For a CI gate: the active selectors (not being verified, fixed or muted) healed at least
1261
+ `minHealCount` times in the last `windowDays` days, optionally in one build, most heals
1262
+ first, at most 100. It always answers `200`; CI decides what `violationCount` means for
1263
+ the build. It reads at most the newest 5000 heals of the period. Only heals in sessions the
1264
+ caller may see count. Any signed-in user (role `MEMBER` or above) may call it.
1265
+ tags:
1266
+ - Selector Health
1267
+ parameters:
1268
+ - in: query
1269
+ name: windowDays
1270
+ schema:
1271
+ type: integer
1272
+ minimum: 1
1273
+ maximum: 365
1274
+ default: 7
1275
+ description: The period, in days back from now; clamped to 1–365.
1276
+ example: 7
1277
+ - in: query
1278
+ name: minHealCount
1279
+ schema:
1280
+ type: integer
1281
+ minimum: 1
1282
+ maximum: 1000
1283
+ default: 5
1284
+ description: The fewest heals that make a violation; clamped to 1–1000.
1285
+ example: 3
1286
+ - in: query
1287
+ name: build
1288
+ schema:
1289
+ type: string
1290
+ description: Only heals in this build's sessions (a build id).
1291
+ example: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
1292
+ - in: query
1293
+ name: tier
1294
+ schema:
1295
+ type: string
1296
+ description: Only heals by this healing method.
1297
+ example: LLM
1298
+ - in: query
1299
+ name: platform
1300
+ schema:
1301
+ type: string
1302
+ description: Only heals in sessions on this platform.
1303
+ example: ios
1304
+ responses:
1305
+ '200':
1306
+ description: The violations.
1307
+ content:
1308
+ application/json:
1309
+ schema:
1310
+ type: object
1311
+ properties:
1312
+ windowDays:
1313
+ type: integer
1314
+ minHealCount:
1315
+ type: integer
1316
+ filters:
1317
+ type: object
1318
+ properties:
1319
+ tier:
1320
+ type: string
1321
+ nullable: true
1322
+ platform:
1323
+ type: string
1324
+ nullable: true
1325
+ buildId:
1326
+ type: string
1327
+ nullable: true
1328
+ violationCount:
1329
+ type: integer
1330
+ totalHeals:
1331
+ type: integer
1332
+ description: Heals read in the period, at most 5000.
1333
+ distinctSelectors:
1334
+ type: integer
1335
+ violations:
1336
+ type: array
1337
+ items:
1338
+ $ref: '#/components/schemas/SelectorHealthHotspot'
1339
+ example:
1340
+ windowDays: 7
1341
+ minHealCount: 3
1342
+ filters:
1343
+ tier: null
1344
+ platform: null
1345
+ buildId: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
1346
+ violationCount: 1
1347
+ totalHeals: 19
1348
+ distinctSelectors: 4
1349
+ violations:
1350
+ - originalStrategy: accessibility id
1351
+ originalSelector: login-btn
1352
+ healCount: 12
1353
+ sessionCount: 12
1354
+ topTier: Fuzzy XML
1355
+ suggestedRewrite: //android.widget.Button[@text="Log in"]
1356
+ suggestedStrategy: xpath
1357
+ suggestedRewriteShare: 1
1358
+ averageConfidence: 0.9
1359
+ firstHealedAt: '2026-10-03T02:04:10.338Z'
1360
+ lastHealedAt: '2026-10-03T02:41:55.002Z'
1361
+ state: null
1362
+ '401':
1363
+ $ref: '#/components/responses/Unauthorized'
1364
+ '429':
1365
+ $ref: '#/components/responses/RateLimited'
1366
+ /api/healing/selector:
1367
+ get:
1368
+ operationId: getHealedSelectorBreakdown
1369
+ summary: Break down one selector's heals
1370
+ description: >-
1371
+ For one selector value (any strategy): its newest heals in the last `windowDays` days (at
1372
+ most 1000 read), the healed selectors it was replaced with and how often, and heal counts
1373
+ by healing method, platform and build, with a timeline of the newest 100 heals. A selector
1374
+ healed only in sessions the caller can't see reads as one with no heals (`healCount: 0`),
1375
+ not `404`. The Selector Health side panel uses `GET /api/healing/selectors/detail`. Any
1376
+ signed-in user (role `MEMBER` or above) may call it.
1377
+ tags:
1378
+ - Selector Health
1379
+ parameters:
1380
+ - in: query
1381
+ name: value
1382
+ required: true
1383
+ schema:
1384
+ type: string
1385
+ description: The original selector, exactly.
1386
+ example: login-btn
1387
+ - $ref: '#/components/parameters/SelectorHealthWindowDays'
1388
+ responses:
1389
+ '200':
1390
+ description: The breakdown.
1391
+ content:
1392
+ application/json:
1393
+ schema:
1394
+ $ref: '#/components/schemas/SelectorHealthBreakdown'
1395
+ example:
1396
+ originalSelector: login-btn
1397
+ windowDays: 30
1398
+ healCount: 41
1399
+ sessionCount: 29
1400
+ byTier:
1401
+ Fuzzy XML: 36
1402
+ LLM: 5
1403
+ byPlatform:
1404
+ android: 41
1405
+ byBuild:
1406
+ - buildId: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
1407
+ count: 12
1408
+ - buildId: unattached
1409
+ count: 3
1410
+ alternates:
1411
+ - healedSelector: //android.widget.Button[@text="Log in"]
1412
+ count: 36
1413
+ share: 0.878
1414
+ averageConfidence: 0.92
1415
+ tiers:
1416
+ - Fuzzy XML
1417
+ timeline:
1418
+ - id: a3c5e7f9-1b2d-4f6a-8c0e-2d4f6a8c0e1b
1419
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
1420
+ buildId: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
1421
+ deviceUdid: emulator-5554
1422
+ deviceName: Pixel 7
1423
+ devicePlatform: android
1424
+ commandName: findElement
1425
+ healedSelector: //android.widget.Button[@text="Log in"]
1426
+ confidence: 0.92
1427
+ tier: Fuzzy XML
1428
+ isSuccess: true
1429
+ createdAt: '2026-10-03T14:20:31.512Z'
1430
+ '400':
1431
+ description: '`value` is missing.'
1432
+ content:
1433
+ application/json:
1434
+ schema:
1435
+ $ref: '#/components/schemas/Error'
1436
+ example:
1437
+ error: true
1438
+ message: value query param is required
1439
+ '401':
1440
+ $ref: '#/components/responses/Unauthorized'
1441
+ '429':
1442
+ $ref: '#/components/responses/RateLimited'
1443
+ /api/healing/selector-health:
1444
+ get:
1445
+ operationId: listSelectorHealthFeed
1446
+ summary: List selectors' heal counts and state, for tools
1447
+ description: >-
1448
+ A compact feed for external tools (such as an MCP plugin): selectors healed in the last 365
1449
+ days, most heals first, whatever their lifecycle state, each with its state and, when the
1450
+ selector's stored element fingerprint has the same strategy, `etalonAge` (milliseconds
1451
+ since that fingerprint was last seen). Built from the 1000 most-healed selectors of the
1452
+ newest 5000 heals, so a rarely healed selector may be missing; that is not an error. Only
1453
+ heals in sessions the caller may see count. Any signed-in user (role `MEMBER` or above)
1454
+ may call it.
1455
+ tags:
1456
+ - Selector Health
1457
+ parameters:
1458
+ - in: query
1459
+ name: selector
1460
+ schema:
1461
+ type: string
1462
+ description: Only this selector value, exactly.
1463
+ example: login-btn
1464
+ - in: query
1465
+ name: limit
1466
+ schema:
1467
+ type: integer
1468
+ minimum: 1
1469
+ maximum: 200
1470
+ default: 50
1471
+ description: How many selectors; clamped to 1–200.
1472
+ example: 25
1473
+ - in: query
1474
+ name: appId
1475
+ schema:
1476
+ type: string
1477
+ description: Accepted for compatibility and ignored.
1478
+ example: com.example.shop
1479
+ responses:
1480
+ '200':
1481
+ description: The selectors.
1482
+ content:
1483
+ application/json:
1484
+ schema:
1485
+ type: array
1486
+ items:
1487
+ $ref: '#/components/schemas/SelectorHealthFeedItem'
1488
+ example:
1489
+ - selector: login-btn
1490
+ strategy: accessibility id
1491
+ healCount: 41
1492
+ topTier: Fuzzy XML
1493
+ suggestedRewrite: //android.widget.Button[@text="Log in"]
1494
+ state:
1495
+ id: 2a4c6e80-1b3d-4f5a-9c7e-0d2f4a6c8e1b
1496
+ original_strategy: accessibility id
1497
+ original_selector: login-btn
1498
+ status: pending
1499
+ fixed_at: '2026-10-02T09:15:00.000Z'
1500
+ fixed_by_api_key: ''
1501
+ resolved_at: null
1502
+ muted_at: null
1503
+ muted_by_api_key: null
1504
+ regression_count: 0
1505
+ clean_builds_count: 1
1506
+ last_event_at: '2026-10-02T09:15:00.000Z'
1507
+ createdAt: '2026-10-02T09:15:00.000Z'
1508
+ updatedAt: '2026-10-03T02:45:12.000Z'
1509
+ lastHealedAt: '2026-10-03T14:20:31.512Z'
1510
+ etalonAge: 3600000
1511
+ '401':
1512
+ $ref: '#/components/responses/Unauthorized'
1513
+ '429':
1514
+ $ref: '#/components/responses/RateLimited'
1515
+ /api/healing/selectors:
1516
+ get:
1517
+ operationId: listSelectorHealth
1518
+ summary: List one tab of Selector Health
1519
+ description: >-
1520
+ One page of the Selector Health list, the four tab counts, and whether the caller may act
1521
+ (`canAct`: the `sessions` or `admin` scope).
1522
+
1523
+
1524
+ - `fix` (To fix): the period's heals grouped by strategy and selector, every heal counted,
1525
+ leaving out selectors being verified, fixed or muted. `platform`, `method` and `sort`
1526
+ apply here only.
1527
+
1528
+ - `verifying`, `fixed` and `muted`: the selectors with that status, newest change first,
1529
+ with their heals in the period. `fixed` lists those verified within the period.
1530
+
1531
+
1532
+ Only selectors healed, at any time, in a session the caller may see are listed. `counts`
1533
+ follow the period, not the search or filters. `q` is compared as plain text (`%` and `_`
1534
+ have no special meaning) against the selector and, on `fix`, its suggested fix. A page
1535
+ past the end answers the last page. A value that can't be read falls back to its default.
1536
+ Any signed-in user (role `MEMBER` or above) may call it.
1537
+ tags:
1538
+ - Selector Health
1539
+ parameters:
1540
+ - in: query
1541
+ name: tab
1542
+ schema:
1543
+ type: string
1544
+ enum:
1545
+ - fix
1546
+ - verifying
1547
+ - fixed
1548
+ - muted
1549
+ default: fix
1550
+ example: fix
1551
+ - in: query
1552
+ name: days
1553
+ schema:
1554
+ type: integer
1555
+ minimum: 1
1556
+ maximum: 365
1557
+ default: 30
1558
+ description: The period, in days back from now; clamped to 1–365.
1559
+ example: 30
1560
+ - in: query
1561
+ name: q
1562
+ schema:
1563
+ type: string
1564
+ maxLength: 200
1565
+ description: Text in the selector or, on `fix`, in its suggested fix; case-insensitive.
1566
+ example: login
1567
+ - in: query
1568
+ name: platform
1569
+ schema:
1570
+ type: string
1571
+ description: On `fix`, only heals in sessions on this platform.
1572
+ example: android
1573
+ - in: query
1574
+ name: method
1575
+ schema:
1576
+ type: string
1577
+ description: On `fix`, only heals by this healing method.
1578
+ example: LLM
1579
+ - in: query
1580
+ name: sort
1581
+ schema:
1582
+ type: string
1583
+ enum:
1584
+ - heals
1585
+ - recent
1586
+ - time
1587
+ default: heals
1588
+ description: On `fix`, most heals, most recent heal, or most time spent first.
1589
+ example: heals
1590
+ - in: query
1591
+ name: page
1592
+ schema:
1593
+ type: integer
1594
+ minimum: 1
1595
+ maximum: 100000
1596
+ default: 1
1597
+ example: 1
1598
+ - in: query
1599
+ name: pageSize
1600
+ schema:
1601
+ type: integer
1602
+ minimum: 1
1603
+ maximum: 100
1604
+ default: 50
1605
+ example: 50
1606
+ responses:
1607
+ '200':
1608
+ description: The page.
1609
+ content:
1610
+ application/json:
1611
+ schema:
1612
+ $ref: '#/components/schemas/SelectorHealthList'
1613
+ example:
1614
+ tab: fix
1615
+ days: 30
1616
+ page: 1
1617
+ pageSize: 50
1618
+ total: 1
1619
+ counts:
1620
+ fix: 1
1621
+ verifying: 2
1622
+ fixed: 3
1623
+ muted: 1
1624
+ items:
1625
+ - strategy: accessibility id
1626
+ selector: login-btn
1627
+ heals: 41
1628
+ sessions: 29
1629
+ lastHealedAt: '2026-10-03T14:20:31.512Z'
1630
+ timeSpentMs: 75440
1631
+ topMethod: Fuzzy XML
1632
+ suggestion:
1633
+ selector: //android.widget.Button[@text="Log in"]
1634
+ strategy: xpath
1635
+ share: 0.88
1636
+ state: null
1637
+ canAct: true
1638
+ '401':
1639
+ $ref: '#/components/responses/Unauthorized'
1640
+ '429':
1641
+ $ref: '#/components/responses/RateLimited'
1642
+ '500':
1643
+ description: The list couldn't be read.
1644
+ content:
1645
+ application/json:
1646
+ schema:
1647
+ $ref: '#/components/schemas/Error'
1648
+ example:
1649
+ error: internal
1650
+ /api/healing/selectors/detail:
1651
+ get:
1652
+ operationId: getSelectorHealthDetail
1653
+ summary: Get one selector for the Selector Health panel
1654
+ description: >-
1655
+ Everything the side panel shows for one selector, by strategy and value, over the last
1656
+ `days` days: heal and session counts, time spent, first and last heal, heals per day in
1657
+ the caller's time zone, every suggested fix with its share, methods and average
1658
+ confidence, the top 5 platforms, builds and devices, the 20 newest heals, the selector's
1659
+ status, its 50 newest status changes with who made them, and `canAct`.
1660
+
1661
+
1662
+ A heal recorded with no strategy has strategy `''`. Status and activity are lab-wide. A
1663
+ selector that no session the caller may see has healed, at any time, answers `404`
1664
+ exactly as an unknown one. Any signed-in user (role `MEMBER` or above) may call it.
1665
+ tags:
1666
+ - Selector Health
1667
+ parameters:
1668
+ - in: query
1669
+ name: selector
1670
+ required: true
1671
+ schema:
1672
+ type: string
1673
+ maxLength: 4000
1674
+ description: The original selector, exactly.
1675
+ example: login-btn
1676
+ - in: query
1677
+ name: strategy
1678
+ schema:
1679
+ type: string
1680
+ maxLength: 200
1681
+ default: ''
1682
+ description: The original strategy; empty for heals recorded with no strategy.
1683
+ example: accessibility id
1684
+ - in: query
1685
+ name: days
1686
+ schema:
1687
+ type: integer
1688
+ minimum: 1
1689
+ maximum: 365
1690
+ default: 30
1691
+ description: The period, in days back from now; clamped to 1–365.
1692
+ example: 30
1693
+ - $ref: '#/components/parameters/SelectorHealthTz'
1694
+ responses:
1695
+ '200':
1696
+ description: The panel's data.
1697
+ content:
1698
+ application/json:
1699
+ schema:
1700
+ $ref: '#/components/schemas/SelectorHealthDetail'
1701
+ example:
1702
+ strategy: accessibility id
1703
+ selector: login-btn
1704
+ days: 30
1705
+ heals: 41
1706
+ sessions: 29
1707
+ timeSpentMs: 75440
1708
+ firstHealedAt: '2026-09-05T08:12:44.019Z'
1709
+ lastHealedAt: '2026-10-03T14:20:31.512Z'
1710
+ daily:
1711
+ - t: 1759449600000
1712
+ heals: 3
1713
+ suggestions:
1714
+ - selector: //android.widget.Button[@text="Log in"]
1715
+ strategy: xpath
1716
+ count: 36
1717
+ share: 0.878
1718
+ methods:
1719
+ - Fuzzy XML
1720
+ averageConfidence: 0.92
1721
+ platforms:
1722
+ - name: android
1723
+ count: 41
1724
+ builds:
1725
+ - id: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
1726
+ name: nightly-2026-10-03
1727
+ count: 12
1728
+ devices:
1729
+ - udid: emulator-5554
1730
+ name: Pixel 7
1731
+ count: 30
1732
+ recent:
1733
+ - id: a3c5e7f9-1b2d-4f6a-8c0e-2d4f6a8c0e1b
1734
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
1735
+ buildId: 9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c
1736
+ at: '2026-10-03T14:20:31.512Z'
1737
+ device: Pixel 7
1738
+ platform: android
1739
+ method: Fuzzy XML
1740
+ confidence: 0.92
1741
+ healedSelector: //android.widget.Button[@text="Log in"]
1742
+ state:
1743
+ status: pending
1744
+ cleanBuilds: 1
1745
+ fixedAt: '2026-10-02T09:15:00.000Z'
1746
+ fixedBy:
1747
+ id: 5b8e2c14-0f3a-4d6b-9e7c-1a2b3c4d5e6f
1748
+ name: Priya Raman
1749
+ resolvedAt: null
1750
+ mutedAt: null
1751
+ mutedBy: null
1752
+ muteReason: null
1753
+ brokeAgain: 0
1754
+ activity:
1755
+ - action: marked_fixed
1756
+ at: '2026-10-02T09:15:00.000Z'
1757
+ by:
1758
+ id: 5b8e2c14-0f3a-4d6b-9e7c-1a2b3c4d5e6f
1759
+ name: Priya Raman
1760
+ reason: null
1761
+ canAct: true
1762
+ '400':
1763
+ description: '`selector` is missing or longer than 4000 characters.'
1764
+ content:
1765
+ application/json:
1766
+ schema:
1767
+ $ref: '#/components/schemas/Error'
1768
+ example:
1769
+ error: bad_request
1770
+ message: selector is required
1771
+ '401':
1772
+ $ref: '#/components/responses/Unauthorized'
1773
+ '404':
1774
+ $ref: '#/components/responses/SelectorHealthNotFound'
1775
+ '429':
1776
+ $ref: '#/components/responses/RateLimited'
1777
+ '500':
1778
+ description: The selector couldn't be read.
1779
+ content:
1780
+ application/json:
1781
+ schema:
1782
+ $ref: '#/components/schemas/Error'
1783
+ example:
1784
+ error: internal
1785
+ /api/healing/selector/state:
1786
+ post:
1787
+ operationId: changeSelectorState
1788
+ summary: Mark a selector fixed, mute it or undo either
1789
+ description: >-
1790
+ Changes a selector's lab-wide status and records who did it (and, for a mute, why):
1791
+
1792
+ - `mark_fixed`: starts verification (status `pending`), resetting its clean-build count.
1793
+ Refused with `409` on a muted selector.
1794
+
1795
+ - `mute`: status `muted`. Muting again refreshes the time and records the new reason.
1796
+
1797
+ - `unmute`: back to `active`, or the status row is removed when the selector has no other
1798
+ history (`state: null`). On a selector that isn't muted it changes nothing and answers its
1799
+ current row.
1800
+
1801
+ - `cancel_verification`: undoes `mark_fixed` while it is `pending`; refused with `409`
1802
+ otherwise. The row is removed when there is no other history.
1803
+
1804
+
1805
+ Dashboards are told of every change at once. Needs role `MEMBER` and the `sessions` scope
1806
+ (`admin` has it). The selector must have healed in a session the caller may see;
1807
+ otherwise `404`, as for an unknown one. With the dashboard cookie, the request needs a
1808
+ same-host `Origin` or `Referer`.
1809
+ tags:
1810
+ - Selector Health
1811
+ requestBody:
1812
+ required: true
1813
+ content:
1814
+ application/json:
1815
+ schema:
1816
+ type: object
1817
+ required:
1818
+ - original_strategy
1819
+ - original_selector
1820
+ - action
1821
+ properties:
1822
+ original_strategy:
1823
+ type: string
1824
+ description: The strategy; may be empty for heals recorded with no strategy.
1825
+ original_selector:
1826
+ type: string
1827
+ minLength: 1
1828
+ action:
1829
+ type: string
1830
+ enum:
1831
+ - mark_fixed
1832
+ - mute
1833
+ - unmute
1834
+ - cancel_verification
1835
+ reason:
1836
+ type: string
1837
+ nullable: true
1838
+ maxLength: 500
1839
+ description: Why it is muted. Kept for `mute` only, trimmed.
1840
+ example:
1841
+ original_strategy: accessibility id
1842
+ original_selector: login-btn
1843
+ action: mute
1844
+ reason: Login screen is being redesigned; selector changes next sprint
1845
+ responses:
1846
+ '200':
1847
+ description: >-
1848
+ The selector's stored status row after the change, or null when it was removed.
1849
+ content:
1850
+ application/json:
1851
+ schema:
1852
+ type: object
1853
+ properties:
1854
+ state:
1855
+ allOf:
1856
+ - $ref: '#/components/schemas/SelectorHealthStateRow'
1857
+ nullable: true
1858
+ example:
1859
+ state:
1860
+ id: 2a4c6e80-1b3d-4f5a-9c7e-0d2f4a6c8e1b
1861
+ original_strategy: accessibility id
1862
+ original_selector: login-btn
1863
+ status: muted
1864
+ fixed_at: null
1865
+ fixed_by_api_key: null
1866
+ resolved_at: null
1867
+ muted_at: '2026-10-04T09:02:41.330Z'
1868
+ muted_by_api_key: ''
1869
+ regression_count: 0
1870
+ clean_builds_count: 0
1871
+ last_event_at: '2026-10-04T09:02:41.330Z'
1872
+ createdAt: '2026-10-04T09:02:41.330Z'
1873
+ updatedAt: '2026-10-04T09:02:41.330Z'
1874
+ '400':
1875
+ description: A field is missing, the action is unknown, or the reason is too long.
1876
+ content:
1877
+ application/json:
1878
+ schema:
1879
+ $ref: '#/components/schemas/Error'
1880
+ examples:
1881
+ missing:
1882
+ value:
1883
+ error: original_strategy, original_selector, and action are required
1884
+ action:
1885
+ value:
1886
+ error: action must be one of mark_fixed, mute, unmute, cancel_verification
1887
+ reason:
1888
+ value:
1889
+ error: reason must be text of at most 500 characters
1890
+ '401':
1891
+ $ref: '#/components/responses/Unauthorized'
1892
+ '403':
1893
+ $ref: '#/components/responses/Forbidden'
1894
+ '404':
1895
+ $ref: '#/components/responses/SelectorHealthNotFound'
1896
+ '409':
1897
+ description: The action doesn't fit the selector's current status.
1898
+ content:
1899
+ application/json:
1900
+ schema:
1901
+ $ref: '#/components/schemas/Error'
1902
+ example:
1903
+ error: Cannot markFixed on a muted selector (accessibility id=login-btn)
1904
+ currentStatus: muted
1905
+ '429':
1906
+ $ref: '#/components/responses/RateLimited'
1907
+ '500':
1908
+ description: The change couldn't be saved.
1909
+ content:
1910
+ application/json:
1911
+ schema:
1912
+ $ref: '#/components/schemas/Error'
1913
+ example:
1914
+ error: internal
1915
+ /api/healing/state/muted:
1916
+ get:
1917
+ operationId: listMutedSelectors
1918
+ summary: List muted selectors
1919
+ description: >-
1920
+ Every muted selector, most recently muted first, including those that haven't healed
1921
+ lately. The mute is lab-wide, so every caller gets the same rows. `last_healed_at` is the
1922
+ latest heal among the sessions the caller may see, null if none. Any signed-in user (role
1923
+ `MEMBER` or above) may call it.
1924
+ tags:
1925
+ - Selector Health
1926
+ parameters:
1927
+ - in: query
1928
+ name: limit
1929
+ schema:
1930
+ type: integer
1931
+ minimum: 1
1932
+ maximum: 200
1933
+ default: 50
1934
+ description: How many rows; clamped to 1–200.
1935
+ example: 50
1936
+ - in: query
1937
+ name: offset
1938
+ schema:
1939
+ type: integer
1940
+ minimum: 0
1941
+ default: 0
1942
+ description: How many rows to skip.
1943
+ example: 0
1944
+ responses:
1945
+ '200':
1946
+ description: One page of muted selectors.
1947
+ content:
1948
+ application/json:
1949
+ schema:
1950
+ type: object
1951
+ properties:
1952
+ muted:
1953
+ type: array
1954
+ items:
1955
+ type: object
1956
+ properties:
1957
+ original_strategy:
1958
+ type: string
1959
+ original_selector:
1960
+ type: string
1961
+ muted_at:
1962
+ type: string
1963
+ format: date-time
1964
+ nullable: true
1965
+ muted_by_api_key:
1966
+ type: string
1967
+ nullable: true
1968
+ description: >-
1969
+ The API key that muted it; empty when it was muted from the
1970
+ dashboard.
1971
+ last_healed_at:
1972
+ type: string
1973
+ format: date-time
1974
+ nullable: true
1975
+ regression_count:
1976
+ type: integer
1977
+ description: How often it healed again after being fixed.
1978
+ total:
1979
+ type: integer
1980
+ limit:
1981
+ type: integer
1982
+ offset:
1983
+ type: integer
1984
+ example:
1985
+ muted:
1986
+ - original_strategy: id
1987
+ original_selector: com.example.shop:id/promo_banner
1988
+ muted_at: '2026-10-01T11:40:12.004Z'
1989
+ muted_by_api_key: ''
1990
+ last_healed_at: '2026-10-03T13:58:20.771Z'
1991
+ regression_count: 0
1992
+ total: 1
1993
+ limit: 50
1994
+ offset: 0
1995
+ '401':
1996
+ $ref: '#/components/responses/Unauthorized'
1997
+ '429':
1998
+ $ref: '#/components/responses/RateLimited'
1999
+ /api/healing/state/{strategy}/{value}:
2000
+ get:
2001
+ operationId: getSelectorStateRow
2002
+ summary: Get one selector's stored status
2003
+ description: >-
2004
+ The selector's stored status row, or `{ "state": null }` when it has none (the selector is
2005
+ active). The row is lab-wide and answers the same for every caller; there is no `404`.
2006
+
2007
+
2008
+ Encode each segment once with `encodeURIComponent` (a `%` in a selector is `%25`). A
2009
+ malformed `%` escape answers `400`. A selector recorded with an empty strategy can't be
2010
+ looked up here. Any signed-in user (role `MEMBER`
2011
+ or above) may call it.
2012
+ tags:
2013
+ - Selector Health
2014
+ parameters:
2015
+ - in: path
2016
+ name: strategy
2017
+ required: true
2018
+ schema:
2019
+ type: string
2020
+ description: The original strategy, URL-encoded.
2021
+ example: accessibility%20id
2022
+ - in: path
2023
+ name: value
2024
+ required: true
2025
+ schema:
2026
+ type: string
2027
+ description: The original selector, URL-encoded.
2028
+ example: login-btn
2029
+ responses:
2030
+ '200':
2031
+ description: The row, or null.
2032
+ content:
2033
+ application/json:
2034
+ schema:
2035
+ type: object
2036
+ properties:
2037
+ state:
2038
+ allOf:
2039
+ - $ref: '#/components/schemas/SelectorHealthStateRow'
2040
+ nullable: true
2041
+ example:
2042
+ state:
2043
+ id: 2a4c6e80-1b3d-4f5a-9c7e-0d2f4a6c8e1b
2044
+ original_strategy: accessibility id
2045
+ original_selector: login-btn
2046
+ status: resolved
2047
+ fixed_at: '2026-09-28T10:00:00.000Z'
2048
+ fixed_by_api_key: ''
2049
+ resolved_at: '2026-10-01T02:50:00.000Z'
2050
+ muted_at: null
2051
+ muted_by_api_key: null
2052
+ regression_count: 0
2053
+ clean_builds_count: 3
2054
+ last_event_at: '2026-10-01T02:50:00.000Z'
2055
+ createdAt: '2026-09-28T10:00:00.000Z'
2056
+ updatedAt: '2026-10-01T02:50:00.000Z'
2057
+ '400':
2058
+ description: A path segment has a malformed `%` escape.
2059
+ content:
2060
+ application/json:
2061
+ schema:
2062
+ $ref: '#/components/schemas/Error'
2063
+ example:
2064
+ error: bad_request
2065
+ message: The URL has a malformed % escape
2066
+ '401':
2067
+ $ref: '#/components/responses/Unauthorized'
2068
+ '429':
2069
+ $ref: '#/components/responses/RateLimited'
2070
+ /api/healing/digest/send:
2071
+ post:
2072
+ operationId: sendSelectorHealthDigest
2073
+ summary: Send the selector health digest now
2074
+ description: >-
2075
+ Sends the selector health digest to every active webhook subscribed to the
2076
+ `selector_health_digest` event: the most-healed active selectors of the last `windowDays`
2077
+ days (healed at least `minHealCount` times), with total heals and distinct selectors. It
2078
+ counts every team's heals, so it needs role `ADMIN` and the `admin` scope. Each setting
2079
+ may be given in the JSON body or as a query parameter; the body wins. With the dashboard
2080
+ cookie, the request needs a same-host `Origin` or `Referer`.
2081
+ tags:
2082
+ - Selector Health
2083
+ parameters:
2084
+ - in: query
2085
+ name: windowDays
2086
+ schema:
2087
+ type: integer
2088
+ minimum: 1
2089
+ maximum: 365
2090
+ default: 7
2091
+ example: 7
2092
+ - in: query
2093
+ name: limit
2094
+ schema:
2095
+ type: integer
2096
+ minimum: 1
2097
+ maximum: 20
2098
+ default: 5
2099
+ example: 5
2100
+ - in: query
2101
+ name: minHealCount
2102
+ schema:
2103
+ type: integer
2104
+ minimum: 1
2105
+ maximum: 1000
2106
+ default: 2
2107
+ example: 2
2108
+ requestBody:
2109
+ required: false
2110
+ content:
2111
+ application/json:
2112
+ schema:
2113
+ type: object
2114
+ properties:
2115
+ windowDays:
2116
+ type: integer
2117
+ minimum: 1
2118
+ maximum: 365
2119
+ default: 7
2120
+ description: The period, in days back from now; clamped to 1–365.
2121
+ limit:
2122
+ type: integer
2123
+ minimum: 1
2124
+ maximum: 20
2125
+ default: 5
2126
+ description: How many selectors the digest lists; clamped to 1–20.
2127
+ minHealCount:
2128
+ type: integer
2129
+ minimum: 1
2130
+ maximum: 1000
2131
+ default: 2
2132
+ description: The fewest heals a listed selector has; clamped to 1–1000.
2133
+ example:
2134
+ windowDays: 7
2135
+ limit: 5
2136
+ minHealCount: 2
2137
+ responses:
2138
+ '200':
2139
+ description: The digest was handed to the webhooks.
2140
+ content:
2141
+ application/json:
2142
+ schema:
2143
+ type: object
2144
+ properties:
2145
+ sent:
2146
+ type: integer
2147
+ description: How many active webhooks subscribe to the digest.
2148
+ windowDays:
2149
+ type: integer
2150
+ hotspotsIncluded:
2151
+ type: integer
2152
+ example:
2153
+ sent: 2
2154
+ windowDays: 7
2155
+ hotspotsIncluded: 4
2156
+ '401':
2157
+ $ref: '#/components/responses/Unauthorized'
2158
+ '403':
2159
+ $ref: '#/components/responses/Forbidden'
2160
+ '429':
2161
+ $ref: '#/components/responses/RateLimited'
2162
+ /api/interceptor/sessions/{sessionId}/requests:
2163
+ get:
2164
+ operationId: listInterceptedRequests
2165
+ summary: List a session's captured HTTP requests
2166
+ description: >-
2167
+ The HTTP requests the network interceptor captured for a session (Android, with the
2168
+ interceptor enabled for the session). While the interceptor runs they come from memory;
2169
+ after it stops, from the session's saved capture. `404` when there is neither. Needs role
2170
+ `ADMIN`.
2171
+ tags:
2172
+ - Network Interceptor
2173
+ parameters:
2174
+ - $ref: '#/components/parameters/InterceptorSessionId'
2175
+ responses:
2176
+ '200':
2177
+ description: The captured requests.
2178
+ content:
2179
+ application/json:
2180
+ schema:
2181
+ type: object
2182
+ properties:
2183
+ requests:
2184
+ type: array
2185
+ items:
2186
+ $ref: '#/components/schemas/InterceptorCapturedRequest'
2187
+ example:
2188
+ requests:
2189
+ - id: req-01J9Z4K7Q2
2190
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2191
+ ts: 1759501232118
2192
+ method: GET
2193
+ url: https://api.example.com/v2/cart
2194
+ host: api.example.com
2195
+ path: /v2/cart
2196
+ reqHeaders:
2197
+ accept: application/json
2198
+ reqBody: null
2199
+ resStatus: 200
2200
+ resHeaders:
2201
+ content-type: application/json
2202
+ resBody: '{"items":[],"total":0}'
2203
+ durationMs: 143
2204
+ mocked: false
2205
+ modified: false
2206
+ commandHint:
2207
+ commandName: click
2208
+ commandTs: 1759501231990
2209
+ '401':
2210
+ $ref: '#/components/responses/Unauthorized'
2211
+ '403':
2212
+ $ref: '#/components/responses/Forbidden'
2213
+ '404':
2214
+ $ref: '#/components/responses/InterceptorInactive'
2215
+ '429':
2216
+ $ref: '#/components/responses/RateLimited'
2217
+ '500':
2218
+ $ref: '#/components/responses/InterceptorFailed'
2219
+ /api/interceptor/sessions/{sessionId}/requests/{requestId}:
2220
+ get:
2221
+ operationId: getInterceptedRequest
2222
+ summary: Get one captured HTTP request
2223
+ description: >-
2224
+ One captured request with its bodies. While the interceptor runs, a response body that was
2225
+ kept on disk is read back into `resBody`. A finished session's saved capture has no large
2226
+ bodies (`resBody` null). Needs role `ADMIN`.
2227
+ tags:
2228
+ - Network Interceptor
2229
+ parameters:
2230
+ - $ref: '#/components/parameters/InterceptorSessionId'
2231
+ - in: path
2232
+ name: requestId
2233
+ required: true
2234
+ schema:
2235
+ type: string
2236
+ description: The captured request's `id`.
2237
+ example: req-01J9Z4K7Q2
2238
+ responses:
2239
+ '200':
2240
+ description: The request.
2241
+ content:
2242
+ application/json:
2243
+ schema:
2244
+ $ref: '#/components/schemas/InterceptorCapturedRequest'
2245
+ example:
2246
+ id: req-01J9Z4K7Q2
2247
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2248
+ ts: 1759501232118
2249
+ method: POST
2250
+ url: https://api.example.com/v2/cart/items
2251
+ host: api.example.com
2252
+ path: /v2/cart/items
2253
+ reqHeaders:
2254
+ content-type: application/json
2255
+ reqBody: '{"sku":"SKU-1182","qty":1}'
2256
+ resStatus: 503
2257
+ resHeaders:
2258
+ content-type: application/json
2259
+ resBody: '{"error":"maintenance"}'
2260
+ durationMs: 1500
2261
+ mocked: true
2262
+ modified: false
2263
+ mockId: mock-mg9x2k1a-1
2264
+ '401':
2265
+ $ref: '#/components/responses/Unauthorized'
2266
+ '403':
2267
+ $ref: '#/components/responses/Forbidden'
2268
+ '404':
2269
+ description: No capture for the session, or no request with that id.
2270
+ content:
2271
+ application/json:
2272
+ schema:
2273
+ $ref: '#/components/schemas/Error'
2274
+ examples:
2275
+ inactive:
2276
+ value:
2277
+ error: interceptor inactive
2278
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2279
+ request:
2280
+ value:
2281
+ error: not found
2282
+ '429':
2283
+ $ref: '#/components/responses/RateLimited'
2284
+ '500':
2285
+ $ref: '#/components/responses/InterceptorFailed'
2286
+ /api/interceptor/sessions/{sessionId}/har:
2287
+ get:
2288
+ operationId: exportInterceptedHar
2289
+ summary: Download a session's traffic as HAR
2290
+ description: >-
2291
+ The session's captured traffic as a HAR 1.2 document, downloaded as
2292
+ `<sessionId>.har` (`application/json`). Built from memory while the interceptor runs,
2293
+ otherwise from the HAR saved when it stopped. Mocked and modified entries carry `_mocked`,
2294
+ `_modified` and `_mockId`. Needs role `ADMIN`.
2295
+ tags:
2296
+ - Network Interceptor
2297
+ parameters:
2298
+ - $ref: '#/components/parameters/InterceptorSessionId'
2299
+ responses:
2300
+ '200':
2301
+ description: The HAR document.
2302
+ headers:
2303
+ Content-Disposition:
2304
+ schema:
2305
+ type: string
2306
+ example: attachment; filename="6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.har"
2307
+ content:
2308
+ application/json:
2309
+ schema:
2310
+ type: object
2311
+ properties:
2312
+ log:
2313
+ type: object
2314
+ properties:
2315
+ version:
2316
+ type: string
2317
+ example: '1.2'
2318
+ creator:
2319
+ type: object
2320
+ properties:
2321
+ name:
2322
+ type: string
2323
+ version:
2324
+ type: string
2325
+ comment:
2326
+ type: string
2327
+ entries:
2328
+ type: array
2329
+ items:
2330
+ type: object
2331
+ additionalProperties: true
2332
+ example:
2333
+ log:
2334
+ version: '1.2'
2335
+ creator:
2336
+ name: Xenon Interceptor
2337
+ version: '1.0'
2338
+ comment: session=6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2339
+ entries:
2340
+ - startedDateTime: '2026-10-03T14:20:32.118Z'
2341
+ time: 143
2342
+ request:
2343
+ method: GET
2344
+ url: https://api.example.com/v2/cart
2345
+ httpVersion: HTTP/1.1
2346
+ headers:
2347
+ - name: accept
2348
+ value: application/json
2349
+ queryString: []
2350
+ cookies: []
2351
+ headersSize: -1
2352
+ bodySize: 0
2353
+ response:
2354
+ status: 200
2355
+ statusText: OK
2356
+ httpVersion: HTTP/1.1
2357
+ headers:
2358
+ - name: content-type
2359
+ value: application/json
2360
+ cookies: []
2361
+ content:
2362
+ size: 22
2363
+ mimeType: application/json
2364
+ text: '{"items":[],"total":0}'
2365
+ redirectURL: ''
2366
+ headersSize: -1
2367
+ bodySize: 22
2368
+ cache: {}
2369
+ timings:
2370
+ send: 0
2371
+ wait: 143
2372
+ receive: 0
2373
+ '401':
2374
+ $ref: '#/components/responses/Unauthorized'
2375
+ '403':
2376
+ $ref: '#/components/responses/Forbidden'
2377
+ '404':
2378
+ $ref: '#/components/responses/InterceptorInactive'
2379
+ '429':
2380
+ $ref: '#/components/responses/RateLimited'
2381
+ '500':
2382
+ $ref: '#/components/responses/InterceptorFailed'
2383
+ /api/interceptor/sessions/{sessionId}/mocks:
2384
+ get:
2385
+ operationId: listInterceptorMocks
2386
+ summary: List a session's mock rules
2387
+ description: >-
2388
+ The mock rules of a session whose interceptor is running, in the order they were added (the
2389
+ newest matching rule wins). A finished session has none: `404`. Needs role `ADMIN`.
2390
+ tags:
2391
+ - Network Interceptor
2392
+ parameters:
2393
+ - $ref: '#/components/parameters/InterceptorSessionId'
2394
+ responses:
2395
+ '200':
2396
+ description: The mock rules.
2397
+ content:
2398
+ application/json:
2399
+ schema:
2400
+ type: object
2401
+ properties:
2402
+ mocks:
2403
+ type: array
2404
+ items:
2405
+ $ref: '#/components/schemas/InterceptorMock'
2406
+ example:
2407
+ mocks:
2408
+ - id: mock-mg9x2k1a-1
2409
+ addedAt: 1759501100412
2410
+ match:
2411
+ url: https://api.example.com/v2/cart/**
2412
+ method: POST
2413
+ respondWith:
2414
+ status: 503
2415
+ headers:
2416
+ content-type: application/json
2417
+ body:
2418
+ error: maintenance
2419
+ delayMs: 1500
2420
+ '401':
2421
+ $ref: '#/components/responses/Unauthorized'
2422
+ '403':
2423
+ $ref: '#/components/responses/Forbidden'
2424
+ '404':
2425
+ $ref: '#/components/responses/InterceptorInactive'
2426
+ '429':
2427
+ $ref: '#/components/responses/RateLimited'
2428
+ '500':
2429
+ $ref: '#/components/responses/InterceptorFailed'
2430
+ post:
2431
+ operationId: addInterceptorMock
2432
+ summary: Add a mock rule to a running session
2433
+ description: >-
2434
+ Adds a mock rule to a session whose interceptor is running. A rule matches a request by
2435
+ URL (exact, or a glob where `*` is any text without `/` and `**` any text) and optionally
2436
+ by method, then rewrites the request, answers with `respondWith` without reaching the
2437
+ server, or changes the real response (`rewriteResponse`). When several rules match, the
2438
+ newest wins. Without an `id` one is generated. The body is stored as given and isn't
2439
+ validated, so send a `match.url`. Needs role `ADMIN`. With the dashboard cookie, the
2440
+ request needs a same-host `Origin` or `Referer`.
2441
+ tags:
2442
+ - Network Interceptor
2443
+ parameters:
2444
+ - $ref: '#/components/parameters/InterceptorSessionId'
2445
+ requestBody:
2446
+ required: true
2447
+ content:
2448
+ application/json:
2449
+ schema:
2450
+ $ref: '#/components/schemas/InterceptorMockInput'
2451
+ example:
2452
+ match:
2453
+ url: https://api.example.com/v2/cart/**
2454
+ method: POST
2455
+ respondWith:
2456
+ status: 503
2457
+ headers:
2458
+ content-type: application/json
2459
+ body:
2460
+ error: maintenance
2461
+ delayMs: 1500
2462
+ responses:
2463
+ '201':
2464
+ description: The rule was added.
2465
+ content:
2466
+ application/json:
2467
+ schema:
2468
+ type: object
2469
+ properties:
2470
+ id:
2471
+ type: string
2472
+ example:
2473
+ id: mock-mg9x2k1a-1
2474
+ '400':
2475
+ description: The rule couldn't be added.
2476
+ content:
2477
+ application/json:
2478
+ schema:
2479
+ $ref: '#/components/schemas/Error'
2480
+ example:
2481
+ error: Cannot read properties of undefined (reading 'url')
2482
+ '401':
2483
+ $ref: '#/components/responses/Unauthorized'
2484
+ '403':
2485
+ $ref: '#/components/responses/Forbidden'
2486
+ '404':
2487
+ $ref: '#/components/responses/InterceptorInactive'
2488
+ '429':
2489
+ $ref: '#/components/responses/RateLimited'
2490
+ delete:
2491
+ operationId: clearInterceptorMocks
2492
+ summary: Remove all of a session's mock rules
2493
+ description: >-
2494
+ Removes every mock rule of a session whose interceptor is running. Needs role `ADMIN`.
2495
+ With the dashboard cookie, the request needs a same-host `Origin` or `Referer`.
2496
+ tags:
2497
+ - Network Interceptor
2498
+ parameters:
2499
+ - $ref: '#/components/parameters/InterceptorSessionId'
2500
+ responses:
2501
+ '200':
2502
+ description: The rules were removed.
2503
+ content:
2504
+ application/json:
2505
+ schema:
2506
+ type: object
2507
+ properties:
2508
+ ok:
2509
+ type: boolean
2510
+ example:
2511
+ ok: true
2512
+ '400':
2513
+ description: The interceptor stopped while the request was handled.
2514
+ content:
2515
+ application/json:
2516
+ schema:
2517
+ $ref: '#/components/schemas/Error'
2518
+ example:
2519
+ error: Interceptor not active for session 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2520
+ '401':
2521
+ $ref: '#/components/responses/Unauthorized'
2522
+ '403':
2523
+ $ref: '#/components/responses/Forbidden'
2524
+ '404':
2525
+ $ref: '#/components/responses/InterceptorInactive'
2526
+ '429':
2527
+ $ref: '#/components/responses/RateLimited'
2528
+ /api/interceptor/sessions/{sessionId}/mocks/{mockId}:
2529
+ delete:
2530
+ operationId: removeInterceptorMock
2531
+ summary: Remove one mock rule
2532
+ description: >-
2533
+ Removes one mock rule of a session whose interceptor is running. An unknown `mockId`
2534
+ answers `200` with `removed: false`. Needs role `ADMIN`. With the dashboard cookie, the
2535
+ request needs a same-host `Origin` or `Referer`.
2536
+ tags:
2537
+ - Network Interceptor
2538
+ parameters:
2539
+ - $ref: '#/components/parameters/InterceptorSessionId'
2540
+ - in: path
2541
+ name: mockId
2542
+ required: true
2543
+ schema:
2544
+ type: string
2545
+ description: The rule's `id`.
2546
+ example: mock-mg9x2k1a-1
2547
+ responses:
2548
+ '200':
2549
+ description: Whether a rule was removed.
2550
+ content:
2551
+ application/json:
2552
+ schema:
2553
+ type: object
2554
+ properties:
2555
+ removed:
2556
+ type: boolean
2557
+ example:
2558
+ removed: true
2559
+ '400':
2560
+ description: The interceptor stopped while the request was handled.
2561
+ content:
2562
+ application/json:
2563
+ schema:
2564
+ $ref: '#/components/schemas/Error'
2565
+ example:
2566
+ error: Interceptor not active for session 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2567
+ '401':
2568
+ $ref: '#/components/responses/Unauthorized'
2569
+ '403':
2570
+ $ref: '#/components/responses/Forbidden'
2571
+ '404':
2572
+ $ref: '#/components/responses/InterceptorInactive'
2573
+ '429':
2574
+ $ref: '#/components/responses/RateLimited'
2575
+ components:
2576
+ parameters:
2577
+ SessionsSessionId:
2578
+ in: path
2579
+ name: sessionId
2580
+ required: true
2581
+ schema:
2582
+ type: string
2583
+ description: The Appium session id.
2584
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2585
+ InterceptorSessionId:
2586
+ in: path
2587
+ name: sessionId
2588
+ required: true
2589
+ schema:
2590
+ type: string
2591
+ description: The Appium session id.
2592
+ example: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2593
+ SelectorHealthWindowDays:
2594
+ in: query
2595
+ name: windowDays
2596
+ schema:
2597
+ type: integer
2598
+ minimum: 1
2599
+ maximum: 365
2600
+ default: 30
2601
+ description: The period, in days back from now; clamped to 1–365.
2602
+ example: 7
2603
+ SelectorHealthTz:
2604
+ in: query
2605
+ name: tz
2606
+ schema:
2607
+ type: integer
2608
+ minimum: -840
2609
+ maximum: 840
2610
+ default: 0
2611
+ description: >-
2612
+ The caller's offset from UTC in minutes, east positive (the browser's
2613
+ `-getTimezoneOffset()`), so days are the caller's. Anything else counts as 0.
2614
+ example: 330
2615
+ responses:
2616
+ SessionsSessionNotFound:
2617
+ description: The session is unknown, or one the caller may not see (the two answer the same).
2618
+ content:
2619
+ application/json:
2620
+ schema:
2621
+ $ref: '#/components/schemas/Error'
2622
+ example:
2623
+ error: true
2624
+ message: Session with id 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b not found
2625
+ SelectorHealthNotFound:
2626
+ description: >-
2627
+ No session the caller may see has healed this selector, or it is unknown (the two answer
2628
+ the same).
2629
+ content:
2630
+ application/json:
2631
+ schema:
2632
+ $ref: '#/components/schemas/Error'
2633
+ example:
2634
+ error: not_found
2635
+ message: Selector not found
2636
+ InterceptorInactive:
2637
+ description: The session's interceptor isn't running, and there is no saved capture to read.
2638
+ content:
2639
+ application/json:
2640
+ schema:
2641
+ $ref: '#/components/schemas/Error'
2642
+ example:
2643
+ error: interceptor inactive
2644
+ sessionId: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b
2645
+ InterceptorFailed:
2646
+ description: The capture couldn't be read.
2647
+ content:
2648
+ application/json:
2649
+ schema:
2650
+ $ref: '#/components/schemas/Error'
2651
+ example:
2652
+ error: 'EACCES: permission denied'
2653
+ schemas:
2654
+ SessionsSessionRecord:
2655
+ description: >-
2656
+ A session's stored record, with who ran it and where. Capabilities are stored as JSON
2657
+ text, with credentials already removed.
2658
+ allOf:
2659
+ - $ref: '#/components/schemas/Session'
2660
+ - type: object
2661
+ properties:
2662
+ desired_capabilities:
2663
+ type: string
2664
+ description: The capabilities the client asked for, as JSON text.
2665
+ session_capabilities:
2666
+ type: string
2667
+ description: The capabilities the driver answered, as JSON text.
2668
+ node_id:
2669
+ type: string
2670
+ description: The server that ran it, as of that server's boot.
2671
+ has_live_video:
2672
+ type: boolean
2673
+ video_recording_enabled:
2674
+ type: boolean
2675
+ video_recording:
2676
+ type: string
2677
+ nullable: true
2678
+ startTime:
2679
+ type: string
2680
+ format: date-time
2681
+ endTime:
2682
+ type: string
2683
+ format: date-time
2684
+ nullable: true
2685
+ failure_reason:
2686
+ type: string
2687
+ nullable: true
2688
+ failure_category:
2689
+ type: string
2690
+ nullable: true
2691
+ is_profiling_available:
2692
+ type: boolean
2693
+ device_info:
2694
+ type: string
2695
+ nullable: true
2696
+ device_version:
2697
+ type: string
2698
+ ai_analysis:
2699
+ type: string
2700
+ nullable: true
2701
+ tags:
2702
+ type: string
2703
+ nullable: true
2704
+ trace_id:
2705
+ type: string
2706
+ nullable: true
2707
+ api_key_id:
2708
+ type: string
2709
+ nullable: true
2710
+ description: The API key that created it, if any.
2711
+ user_id:
2712
+ type: string
2713
+ nullable: true
2714
+ description: The person who ran it, if known.
2715
+ owner:
2716
+ type: object
2717
+ nullable: true
2718
+ description: Who ran it, or null when that isn't known.
2719
+ properties:
2720
+ name:
2721
+ type: string
2722
+ email:
2723
+ type: string
2724
+ ranOn:
2725
+ type: string
2726
+ nullable: true
2727
+ description: >-
2728
+ `here` for this server, a node's host (`192.168.1.21:4723`) for another server's
2729
+ device, or null when that isn't known any more.
2730
+ SessionsSummary:
2731
+ type: object
2732
+ properties:
2733
+ since:
2734
+ type: string
2735
+ format: date-time
2736
+ nullable: true
2737
+ current:
2738
+ allOf:
2739
+ - $ref: '#/components/schemas/SessionsPeriodCounts'
2740
+ - type: object
2741
+ properties:
2742
+ medianMs:
2743
+ type: integer
2744
+ nullable: true
2745
+ description: Median duration of the period's ended sessions; null if none.
2746
+ p90Ms:
2747
+ type: integer
2748
+ nullable: true
2749
+ description: 90th-percentile duration of the period's ended sessions.
2750
+ previous:
2751
+ allOf:
2752
+ - $ref: '#/components/schemas/SessionsPeriodCounts'
2753
+ nullable: true
2754
+ description: The period of the same length before; null without `since`.
2755
+ runningNow:
2756
+ type: object
2757
+ properties:
2758
+ sessions:
2759
+ type: integer
2760
+ devices:
2761
+ type: integer
2762
+ SessionsPeriodCounts:
2763
+ type: object
2764
+ properties:
2765
+ total:
2766
+ type: integer
2767
+ passed:
2768
+ type: integer
2769
+ failed:
2770
+ type: integer
2771
+ running:
2772
+ type: integer
2773
+ SessionsBuildRecord:
2774
+ type: object
2775
+ properties:
2776
+ id:
2777
+ type: string
2778
+ name:
2779
+ type: string
2780
+ nullable: true
2781
+ createdAt:
2782
+ type: string
2783
+ format: date-time
2784
+ updatedAt:
2785
+ type: string
2786
+ format: date-time
2787
+ _count:
2788
+ type: object
2789
+ properties:
2790
+ sessions:
2791
+ type: integer
2792
+ sessionCount:
2793
+ type: integer
2794
+ passedCount:
2795
+ type: integer
2796
+ failedCount:
2797
+ type: integer
2798
+ runningCount:
2799
+ type: integer
2800
+ SessionsCommand:
2801
+ type: object
2802
+ description: One Appium command of a session, as stored.
2803
+ properties:
2804
+ id:
2805
+ type: string
2806
+ session_id:
2807
+ type: string
2808
+ command_name:
2809
+ type: string
2810
+ nullable: true
2811
+ url:
2812
+ type: string
2813
+ method:
2814
+ type: string
2815
+ title:
2816
+ type: string
2817
+ subtitle:
2818
+ type: string
2819
+ nullable: true
2820
+ body:
2821
+ type: string
2822
+ nullable: true
2823
+ description: The request body, as JSON text.
2824
+ response:
2825
+ type: string
2826
+ description: The response, as JSON text.
2827
+ screenshot:
2828
+ type: string
2829
+ nullable: true
2830
+ description: The screenshot's file name, served by the session asset route.
2831
+ is_success:
2832
+ type: boolean
2833
+ nullable: true
2834
+ is_error:
2835
+ type: boolean
2836
+ is_healed:
2837
+ type: boolean
2838
+ original_strategy:
2839
+ type: string
2840
+ nullable: true
2841
+ original_selector:
2842
+ type: string
2843
+ nullable: true
2844
+ healed_strategy:
2845
+ type: string
2846
+ nullable: true
2847
+ healed_selector:
2848
+ type: string
2849
+ nullable: true
2850
+ healing_confidence:
2851
+ type: number
2852
+ nullable: true
2853
+ healing_tier:
2854
+ type: string
2855
+ nullable: true
2856
+ description: The healing method that found it.
2857
+ duration:
2858
+ type: integer
2859
+ nullable: true
2860
+ description: Milliseconds.
2861
+ span_id:
2862
+ type: string
2863
+ nullable: true
2864
+ trace_id:
2865
+ type: string
2866
+ nullable: true
2867
+ createdAt:
2868
+ type: string
2869
+ format: date-time
2870
+ updatedAt:
2871
+ type: string
2872
+ format: date-time
2873
+ SessionsLogLine:
2874
+ type: object
2875
+ properties:
2876
+ id:
2877
+ type: string
2878
+ session_id:
2879
+ type: string
2880
+ log_type:
2881
+ type: string
2882
+ enum:
2883
+ - DEVICE
2884
+ - DEBUG
2885
+ message:
2886
+ type: string
2887
+ timestamp:
2888
+ type: string
2889
+ format: date-time
2890
+ createdAt:
2891
+ type: string
2892
+ format: date-time
2893
+ updatedAt:
2894
+ type: string
2895
+ format: date-time
2896
+ SessionsProfilingSample:
2897
+ type: object
2898
+ properties:
2899
+ id:
2900
+ type: integer
2901
+ session_id:
2902
+ type: string
2903
+ cpu:
2904
+ type: string
2905
+ nullable: true
2906
+ memory:
2907
+ type: string
2908
+ nullable: true
2909
+ total_cpu_used:
2910
+ type: string
2911
+ nullable: true
2912
+ total_memory_used:
2913
+ type: string
2914
+ nullable: true
2915
+ raw_cpu_log:
2916
+ type: string
2917
+ nullable: true
2918
+ raw_memory_log:
2919
+ type: string
2920
+ nullable: true
2921
+ timestamp:
2922
+ type: string
2923
+ format: date-time
2924
+ createdAt:
2925
+ type: string
2926
+ format: date-time
2927
+ updatedAt:
2928
+ type: string
2929
+ format: date-time
2930
+ SessionsMetrics:
2931
+ type: object
2932
+ properties:
2933
+ platform:
2934
+ type: string
2935
+ description: The session's platform, lower case.
2936
+ intervalMs:
2937
+ type: integer
2938
+ description: Time between samples.
2939
+ example: 2000
2940
+ appId:
2941
+ type: string
2942
+ nullable: true
2943
+ description: The app of the newest sample that names one.
2944
+ series:
2945
+ type: object
2946
+ description: What this platform can record.
2947
+ properties:
2948
+ deviceCpu:
2949
+ type: boolean
2950
+ deviceMem:
2951
+ type: boolean
2952
+ appCpu:
2953
+ type: boolean
2954
+ appMem:
2955
+ type: boolean
2956
+ recording:
2957
+ type: string
2958
+ nullable: true
2959
+ enum:
2960
+ - sampling
2961
+ - stopped
2962
+ - 'off'
2963
+ - null
2964
+ description: For a running session, whether it is being sampled; null once it ended.
2965
+ samples:
2966
+ type: array
2967
+ items:
2968
+ type: object
2969
+ properties:
2970
+ t:
2971
+ type: number
2972
+ description: Epoch milliseconds.
2973
+ deviceCpu:
2974
+ type: number
2975
+ nullable: true
2976
+ description: Percent of the whole device.
2977
+ deviceMemMb:
2978
+ type: number
2979
+ nullable: true
2980
+ deviceMemTotalMb:
2981
+ type: number
2982
+ nullable: true
2983
+ appCpu:
2984
+ type: number
2985
+ nullable: true
2986
+ description: Percent of the whole device.
2987
+ appMemMb:
2988
+ type: number
2989
+ nullable: true
2990
+ app:
2991
+ type: string
2992
+ nullable: true
2993
+ description: The app the app figures are for.
2994
+ SessionsActiveList:
2995
+ type: object
2996
+ properties:
2997
+ stats:
2998
+ type: object
2999
+ description: Every session on this server, whatever the caller may see.
3000
+ properties:
3001
+ total:
3002
+ type: integer
3003
+ byType:
3004
+ type: object
3005
+ properties:
3006
+ local:
3007
+ type: integer
3008
+ remote:
3009
+ type: integer
3010
+ cloud:
3011
+ type: integer
3012
+ sessions:
3013
+ type: array
3014
+ items:
3015
+ type: object
3016
+ properties:
3017
+ id:
3018
+ type: string
3019
+ type:
3020
+ type: string
3021
+ enum:
3022
+ - local
3023
+ - remote
3024
+ - cloud
3025
+ deviceUdid:
3026
+ type: string
3027
+ deviceName:
3028
+ type: string
3029
+ platform:
3030
+ type: string
3031
+ SessionsRequestLogList:
3032
+ type: object
3033
+ properties:
3034
+ stats:
3035
+ type: object
3036
+ description: Over the whole buffer (up to 500 calls), not just those returned.
3037
+ properties:
3038
+ totalLogged:
3039
+ type: integer
3040
+ errorCount:
3041
+ type: integer
3042
+ avgDurationMs:
3043
+ type: integer
3044
+ byMethod:
3045
+ type: object
3046
+ additionalProperties:
3047
+ type: integer
3048
+ byStatusCode:
3049
+ type: object
3050
+ additionalProperties:
3051
+ type: integer
3052
+ logs:
3053
+ type: array
3054
+ items:
3055
+ type: object
3056
+ properties:
3057
+ timestamp:
3058
+ type: string
3059
+ format: date-time
3060
+ direction:
3061
+ type: string
3062
+ enum:
3063
+ - outgoing
3064
+ - incoming
3065
+ method:
3066
+ type: string
3067
+ url:
3068
+ type: string
3069
+ requestBody:
3070
+ type: string
3071
+ description: JSON text, secrets redacted, long values truncated.
3072
+ responseBody:
3073
+ type: string
3074
+ description: JSON text, secrets redacted, long values truncated.
3075
+ statusCode:
3076
+ type: integer
3077
+ durationMs:
3078
+ type: integer
3079
+ error:
3080
+ type: string
3081
+ source:
3082
+ type: string
3083
+ correlationId:
3084
+ type: string
3085
+ SelectorHealthEvent:
3086
+ type: object
3087
+ properties:
3088
+ id:
3089
+ type: string
3090
+ sessionId:
3091
+ type: string
3092
+ deviceUdid:
3093
+ type: string
3094
+ nullable: true
3095
+ deviceName:
3096
+ type: string
3097
+ nullable: true
3098
+ devicePlatform:
3099
+ type: string
3100
+ nullable: true
3101
+ commandName:
3102
+ type: string
3103
+ nullable: true
3104
+ originalSelector:
3105
+ type: string
3106
+ nullable: true
3107
+ healedSelector:
3108
+ type: string
3109
+ nullable: true
3110
+ confidence:
3111
+ type: number
3112
+ nullable: true
3113
+ tier:
3114
+ type: string
3115
+ nullable: true
3116
+ description: The healing method.
3117
+ isSuccess:
3118
+ type: boolean
3119
+ nullable: true
3120
+ createdAt:
3121
+ type: string
3122
+ format: date-time
3123
+ SelectorHealthPeriod:
3124
+ type: object
3125
+ properties:
3126
+ totalHeals:
3127
+ type: integer
3128
+ distinctSelectors:
3129
+ type: integer
3130
+ sessionsTouched:
3131
+ type: integer
3132
+ byTier:
3133
+ type: object
3134
+ description: Heals per healing method (`Unknown` when none was recorded).
3135
+ additionalProperties:
3136
+ type: integer
3137
+ timeSpentMs:
3138
+ type: integer
3139
+ description: The recorded duration of the commands that needed healing.
3140
+ SelectorHealthTrendDay:
3141
+ type: object
3142
+ properties:
3143
+ t:
3144
+ type: integer
3145
+ description: When the caller's local day began, epoch milliseconds.
3146
+ heals:
3147
+ type: integer
3148
+ aiHeals:
3149
+ type: integer
3150
+ description: Heals by Visual AI or an LLM.
3151
+ SelectorHealthSummary:
3152
+ type: object
3153
+ properties:
3154
+ windowDays:
3155
+ type: integer
3156
+ current:
3157
+ $ref: '#/components/schemas/SelectorHealthPeriod'
3158
+ prior:
3159
+ $ref: '#/components/schemas/SelectorHealthPeriod'
3160
+ resolvedCount:
3161
+ type: integer
3162
+ description: Selectors verified fixed in the period, lab-wide.
3163
+ pendingCount:
3164
+ type: integer
3165
+ description: Selectors being verified now, lab-wide.
3166
+ trend:
3167
+ type: array
3168
+ items:
3169
+ $ref: '#/components/schemas/SelectorHealthTrendDay'
3170
+ SelectorHealthStateRow:
3171
+ type: object
3172
+ description: >-
3173
+ A selector's stored, lab-wide status row. `status` is `active`, `pending` (being
3174
+ verified), `resolved` (verified fixed) or `muted`. The `*_by_api_key` fields name the API
3175
+ key that acted, and are empty for dashboard actions; who acted is in the selector's
3176
+ activity.
3177
+ properties:
3178
+ id:
3179
+ type: string
3180
+ original_strategy:
3181
+ type: string
3182
+ original_selector:
3183
+ type: string
3184
+ status:
3185
+ type: string
3186
+ enum:
3187
+ - active
3188
+ - pending
3189
+ - resolved
3190
+ - muted
3191
+ fixed_at:
3192
+ type: string
3193
+ format: date-time
3194
+ nullable: true
3195
+ fixed_by_api_key:
3196
+ type: string
3197
+ nullable: true
3198
+ resolved_at:
3199
+ type: string
3200
+ format: date-time
3201
+ nullable: true
3202
+ muted_at:
3203
+ type: string
3204
+ format: date-time
3205
+ nullable: true
3206
+ muted_by_api_key:
3207
+ type: string
3208
+ nullable: true
3209
+ regression_count:
3210
+ type: integer
3211
+ description: How often it healed again after being fixed.
3212
+ clean_builds_count:
3213
+ type: integer
3214
+ last_event_at:
3215
+ type: string
3216
+ format: date-time
3217
+ createdAt:
3218
+ type: string
3219
+ format: date-time
3220
+ updatedAt:
3221
+ type: string
3222
+ format: date-time
3223
+ SelectorHealthHotspot:
3224
+ type: object
3225
+ properties:
3226
+ originalStrategy:
3227
+ type: string
3228
+ nullable: true
3229
+ originalSelector:
3230
+ type: string
3231
+ healCount:
3232
+ type: integer
3233
+ sessionCount:
3234
+ type: integer
3235
+ topTier:
3236
+ type: string
3237
+ nullable: true
3238
+ description: The healing method used most.
3239
+ suggestedRewrite:
3240
+ type: string
3241
+ nullable: true
3242
+ description: The healed selector found most often.
3243
+ suggestedStrategy:
3244
+ type: string
3245
+ nullable: true
3246
+ suggestedRewriteShare:
3247
+ type: number
3248
+ nullable: true
3249
+ description: The share of heals that found `suggestedRewrite`, 0 to 1.
3250
+ averageConfidence:
3251
+ type: number
3252
+ nullable: true
3253
+ firstHealedAt:
3254
+ type: string
3255
+ format: date-time
3256
+ lastHealedAt:
3257
+ type: string
3258
+ format: date-time
3259
+ state:
3260
+ allOf:
3261
+ - $ref: '#/components/schemas/SelectorHealthStateRow'
3262
+ nullable: true
3263
+ SelectorHealthBreakdown:
3264
+ type: object
3265
+ properties:
3266
+ originalSelector:
3267
+ type: string
3268
+ windowDays:
3269
+ type: integer
3270
+ healCount:
3271
+ type: integer
3272
+ sessionCount:
3273
+ type: integer
3274
+ byTier:
3275
+ type: object
3276
+ additionalProperties:
3277
+ type: integer
3278
+ byPlatform:
3279
+ type: object
3280
+ additionalProperties:
3281
+ type: integer
3282
+ byBuild:
3283
+ type: array
3284
+ description: Heals per build id; `unattached` for sessions with no build.
3285
+ items:
3286
+ type: object
3287
+ properties:
3288
+ buildId:
3289
+ type: string
3290
+ count:
3291
+ type: integer
3292
+ alternates:
3293
+ type: array
3294
+ items:
3295
+ type: object
3296
+ properties:
3297
+ healedSelector:
3298
+ type: string
3299
+ count:
3300
+ type: integer
3301
+ share:
3302
+ type: number
3303
+ averageConfidence:
3304
+ type: number
3305
+ nullable: true
3306
+ tiers:
3307
+ type: array
3308
+ items:
3309
+ type: string
3310
+ timeline:
3311
+ type: array
3312
+ items:
3313
+ type: object
3314
+ properties:
3315
+ id:
3316
+ type: string
3317
+ sessionId:
3318
+ type: string
3319
+ buildId:
3320
+ type: string
3321
+ nullable: true
3322
+ deviceUdid:
3323
+ type: string
3324
+ nullable: true
3325
+ deviceName:
3326
+ type: string
3327
+ nullable: true
3328
+ devicePlatform:
3329
+ type: string
3330
+ nullable: true
3331
+ commandName:
3332
+ type: string
3333
+ nullable: true
3334
+ healedSelector:
3335
+ type: string
3336
+ nullable: true
3337
+ confidence:
3338
+ type: number
3339
+ nullable: true
3340
+ tier:
3341
+ type: string
3342
+ nullable: true
3343
+ isSuccess:
3344
+ type: boolean
3345
+ nullable: true
3346
+ createdAt:
3347
+ type: string
3348
+ format: date-time
3349
+ SelectorHealthFeedItem:
3350
+ type: object
3351
+ properties:
3352
+ selector:
3353
+ type: string
3354
+ strategy:
3355
+ type: string
3356
+ nullable: true
3357
+ healCount:
3358
+ type: integer
3359
+ topTier:
3360
+ type: string
3361
+ nullable: true
3362
+ suggestedRewrite:
3363
+ type: string
3364
+ nullable: true
3365
+ state:
3366
+ allOf:
3367
+ - $ref: '#/components/schemas/SelectorHealthStateRow'
3368
+ nullable: true
3369
+ lastHealedAt:
3370
+ type: string
3371
+ format: date-time
3372
+ etalonAge:
3373
+ type: integer
3374
+ description: >-
3375
+ Milliseconds since the selector's stored fingerprint was last seen; absent when there
3376
+ is none for this strategy.
3377
+ SelectorHealthPerson:
3378
+ type: object
3379
+ description: Who did something. `name` is null for a deleted user, or with auth disabled.
3380
+ properties:
3381
+ id:
3382
+ type: string
3383
+ name:
3384
+ type: string
3385
+ nullable: true
3386
+ SelectorHealthStateView:
3387
+ type: object
3388
+ description: A selector's status as the dashboard shows it.
3389
+ properties:
3390
+ status:
3391
+ type: string
3392
+ enum:
3393
+ - active
3394
+ - pending
3395
+ - resolved
3396
+ - muted
3397
+ cleanBuilds:
3398
+ type: integer
3399
+ fixedAt:
3400
+ type: string
3401
+ format: date-time
3402
+ nullable: true
3403
+ fixedBy:
3404
+ allOf:
3405
+ - $ref: '#/components/schemas/SelectorHealthPerson'
3406
+ nullable: true
3407
+ resolvedAt:
3408
+ type: string
3409
+ format: date-time
3410
+ nullable: true
3411
+ mutedAt:
3412
+ type: string
3413
+ format: date-time
3414
+ nullable: true
3415
+ mutedBy:
3416
+ allOf:
3417
+ - $ref: '#/components/schemas/SelectorHealthPerson'
3418
+ nullable: true
3419
+ muteReason:
3420
+ type: string
3421
+ nullable: true
3422
+ brokeAgain:
3423
+ type: integer
3424
+ description: How often it healed again after being fixed.
3425
+ SelectorHealthListItem:
3426
+ type: object
3427
+ properties:
3428
+ strategy:
3429
+ type: string
3430
+ description: Empty for heals recorded with no strategy.
3431
+ selector:
3432
+ type: string
3433
+ heals:
3434
+ type: integer
3435
+ description: Heals in the period.
3436
+ sessions:
3437
+ type: integer
3438
+ lastHealedAt:
3439
+ type: string
3440
+ format: date-time
3441
+ nullable: true
3442
+ timeSpentMs:
3443
+ type: integer
3444
+ topMethod:
3445
+ type: string
3446
+ nullable: true
3447
+ suggestion:
3448
+ type: object
3449
+ nullable: true
3450
+ properties:
3451
+ selector:
3452
+ type: string
3453
+ strategy:
3454
+ type: string
3455
+ nullable: true
3456
+ share:
3457
+ type: number
3458
+ description: The share of the period's heals that found it, 0 to 1.
3459
+ state:
3460
+ allOf:
3461
+ - $ref: '#/components/schemas/SelectorHealthStateView'
3462
+ nullable: true
3463
+ SelectorHealthList:
3464
+ type: object
3465
+ properties:
3466
+ tab:
3467
+ type: string
3468
+ enum:
3469
+ - fix
3470
+ - verifying
3471
+ - fixed
3472
+ - muted
3473
+ days:
3474
+ type: integer
3475
+ page:
3476
+ type: integer
3477
+ description: The page answered; the last page when the one asked for is past the end.
3478
+ pageSize:
3479
+ type: integer
3480
+ total:
3481
+ type: integer
3482
+ description: Selectors in this tab after the search and filters.
3483
+ counts:
3484
+ type: object
3485
+ description: Selectors in each tab for the period, before the search and filters.
3486
+ properties:
3487
+ fix:
3488
+ type: integer
3489
+ verifying:
3490
+ type: integer
3491
+ fixed:
3492
+ type: integer
3493
+ muted:
3494
+ type: integer
3495
+ items:
3496
+ type: array
3497
+ items:
3498
+ $ref: '#/components/schemas/SelectorHealthListItem'
3499
+ canAct:
3500
+ type: boolean
3501
+ description: Whether the caller may mark fixed, mute, unmute and cancel.
3502
+ SelectorHealthDetail:
3503
+ type: object
3504
+ properties:
3505
+ strategy:
3506
+ type: string
3507
+ selector:
3508
+ type: string
3509
+ days:
3510
+ type: integer
3511
+ heals:
3512
+ type: integer
3513
+ sessions:
3514
+ type: integer
3515
+ timeSpentMs:
3516
+ type: integer
3517
+ firstHealedAt:
3518
+ type: string
3519
+ format: date-time
3520
+ nullable: true
3521
+ lastHealedAt:
3522
+ type: string
3523
+ format: date-time
3524
+ nullable: true
3525
+ daily:
3526
+ type: array
3527
+ items:
3528
+ type: object
3529
+ properties:
3530
+ t:
3531
+ type: integer
3532
+ description: When the caller's local day began, epoch milliseconds.
3533
+ heals:
3534
+ type: integer
3535
+ suggestions:
3536
+ type: array
3537
+ items:
3538
+ type: object
3539
+ properties:
3540
+ selector:
3541
+ type: string
3542
+ strategy:
3543
+ type: string
3544
+ nullable: true
3545
+ count:
3546
+ type: integer
3547
+ share:
3548
+ type: number
3549
+ methods:
3550
+ type: array
3551
+ items:
3552
+ type: string
3553
+ averageConfidence:
3554
+ type: number
3555
+ nullable: true
3556
+ platforms:
3557
+ type: array
3558
+ items:
3559
+ type: object
3560
+ properties:
3561
+ name:
3562
+ type: string
3563
+ count:
3564
+ type: integer
3565
+ builds:
3566
+ type: array
3567
+ items:
3568
+ type: object
3569
+ properties:
3570
+ id:
3571
+ type: string
3572
+ nullable: true
3573
+ name:
3574
+ type: string
3575
+ count:
3576
+ type: integer
3577
+ devices:
3578
+ type: array
3579
+ items:
3580
+ type: object
3581
+ properties:
3582
+ udid:
3583
+ type: string
3584
+ name:
3585
+ type: string
3586
+ count:
3587
+ type: integer
3588
+ recent:
3589
+ type: array
3590
+ items:
3591
+ type: object
3592
+ properties:
3593
+ id:
3594
+ type: string
3595
+ sessionId:
3596
+ type: string
3597
+ buildId:
3598
+ type: string
3599
+ nullable: true
3600
+ at:
3601
+ type: string
3602
+ format: date-time
3603
+ device:
3604
+ type: string
3605
+ nullable: true
3606
+ platform:
3607
+ type: string
3608
+ nullable: true
3609
+ method:
3610
+ type: string
3611
+ nullable: true
3612
+ confidence:
3613
+ type: number
3614
+ nullable: true
3615
+ healedSelector:
3616
+ type: string
3617
+ nullable: true
3618
+ state:
3619
+ allOf:
3620
+ - $ref: '#/components/schemas/SelectorHealthStateView'
3621
+ nullable: true
3622
+ activity:
3623
+ type: array
3624
+ items:
3625
+ type: object
3626
+ properties:
3627
+ action:
3628
+ type: string
3629
+ example: marked_fixed
3630
+ at:
3631
+ type: string
3632
+ format: date-time
3633
+ by:
3634
+ allOf:
3635
+ - $ref: '#/components/schemas/SelectorHealthPerson'
3636
+ nullable: true
3637
+ reason:
3638
+ type: string
3639
+ nullable: true
3640
+ canAct:
3641
+ type: boolean
3642
+ InterceptorCapturedRequest:
3643
+ type: object
3644
+ properties:
3645
+ id:
3646
+ type: string
3647
+ sessionId:
3648
+ type: string
3649
+ ts:
3650
+ type: number
3651
+ description: Epoch milliseconds.
3652
+ method:
3653
+ type: string
3654
+ url:
3655
+ type: string
3656
+ host:
3657
+ type: string
3658
+ path:
3659
+ type: string
3660
+ reqHeaders:
3661
+ type: object
3662
+ additionalProperties:
3663
+ type: string
3664
+ reqBody:
3665
+ type: string
3666
+ nullable: true
3667
+ resStatus:
3668
+ type: integer
3669
+ description: -1 when the request never completed.
3670
+ resHeaders:
3671
+ type: object
3672
+ additionalProperties:
3673
+ type: string
3674
+ resBody:
3675
+ type: string
3676
+ nullable: true
3677
+ bodyPath:
3678
+ type: string
3679
+ durationMs:
3680
+ type: number
3681
+ mocked:
3682
+ type: boolean
3683
+ modified:
3684
+ type: boolean
3685
+ mockId:
3686
+ type: string
3687
+ commandHint:
3688
+ type: object
3689
+ description: The Appium command that ran just before it.
3690
+ properties:
3691
+ commandName:
3692
+ type: string
3693
+ commandTs:
3694
+ type: number
3695
+ failed:
3696
+ type: boolean
3697
+ failureReason:
3698
+ type: string
3699
+ failureKind:
3700
+ type: string
3701
+ enum:
3702
+ - HTTPS_CLIENT_ERROR
3703
+ - HTTPS_SERVER_ERROR
3704
+ - OPEN_HTTPS_SERVER_ERROR
3705
+ - ON_CONNECT_ERROR
3706
+ - PROXY_TO_SERVER_REQUEST_ERROR
3707
+ InterceptorMockInput:
3708
+ type: object
3709
+ required:
3710
+ - match
3711
+ properties:
3712
+ id:
3713
+ type: string
3714
+ description: Generated when absent.
3715
+ match:
3716
+ type: object
3717
+ required:
3718
+ - url
3719
+ properties:
3720
+ url:
3721
+ type: string
3722
+ description: The exact URL, or a glob (`*` within a path segment, `**` across them).
3723
+ method:
3724
+ type: string
3725
+ description: Matched case-insensitively; any method when absent.
3726
+ rewriteRequest:
3727
+ type: object
3728
+ properties:
3729
+ url:
3730
+ type: string
3731
+ headers:
3732
+ type: object
3733
+ additionalProperties:
3734
+ type: string
3735
+ body:
3736
+ oneOf:
3737
+ - type: string
3738
+ - type: object
3739
+ additionalProperties: true
3740
+ respondWith:
3741
+ type: object
3742
+ required:
3743
+ - status
3744
+ properties:
3745
+ status:
3746
+ type: integer
3747
+ headers:
3748
+ type: object
3749
+ additionalProperties:
3750
+ type: string
3751
+ body:
3752
+ oneOf:
3753
+ - type: string
3754
+ - type: object
3755
+ additionalProperties: true
3756
+ delayMs:
3757
+ type: integer
3758
+ rewriteResponse:
3759
+ type: object
3760
+ properties:
3761
+ status:
3762
+ type: integer
3763
+ headers:
3764
+ type: object
3765
+ additionalProperties:
3766
+ type: string
3767
+ bodyTransform:
3768
+ type: string
3769
+ enum:
3770
+ - jsonMerge
3771
+ - replace
3772
+ body:
3773
+ oneOf:
3774
+ - type: string
3775
+ - type: object
3776
+ additionalProperties: true
3777
+ InterceptorMock:
3778
+ allOf:
3779
+ - $ref: '#/components/schemas/InterceptorMockInput'
3780
+ - type: object
3781
+ properties:
3782
+ addedAt:
3783
+ type: number
3784
+ description: Epoch milliseconds.