@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.
- package/lib/package.json +2 -2
- package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
- package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
- package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
- package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
- package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
- package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
- package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
- package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
- package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
- package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
- package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
- package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
- package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
- package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
- package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
- package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
- package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
- package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
- package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
- package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
- package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
- package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
- package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
- package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
- package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
- package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
- package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
- package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
- package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
- package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
- package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
- package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
- package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
- package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
- package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
- package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
- package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
- package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
- package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
- package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
- package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
- package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
- package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
- package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
- package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
- package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
- package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
- package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
- package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
- package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
- package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
- package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
- package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
- package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
- package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
- package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
- package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
- package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
- package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
- package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
- package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/apiErrors.js +118 -0
- package/lib/src/app/index.js +6 -1
- package/lib/src/app/openapi/control.yaml +3129 -0
- package/lib/src/app/openapi/grid.yaml +2295 -0
- package/lib/src/app/openapi/identity.yaml +2168 -0
- package/lib/src/app/openapi/platform.yaml +2885 -0
- package/lib/src/app/openapi/sessions.yaml +3784 -0
- package/lib/src/app/routers/bug-report.js +4 -1
- package/lib/src/app/routers/config.js +6 -107
- package/lib/src/app/routers/control.js +117 -59
- package/lib/src/app/routers/dashboard.js +13 -7
- package/lib/src/app/routers/grid.js +54 -14
- package/lib/src/app/routers/profile.js +27 -13
- package/lib/src/app/routers/recordings.js +11 -5
- package/lib/src/app/routers/reservation.js +63 -15
- package/lib/src/app/routers/users.js +4 -0
- package/lib/src/app/routers/webhook.js +17 -8
- package/lib/src/app/swagger.js +259 -177
- package/lib/src/data-service/device-service.js +4 -1
- package/lib/src/data-service/deviceFieldOwners.js +1 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
- package/lib/src/device-managers/ios/WDAClient.js +32 -32
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +38 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +2 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/middleware/csrfMiddleware.js +13 -5
- package/lib/src/middleware/rateLimitMiddleware.js +19 -5
- package/lib/src/middleware/roleGuard.js +20 -0
- package/lib/src/services/AIService.js +13 -3
- package/lib/src/services/NotificationService.js +28 -28
- package/lib/src/services/bug-report/BugReportService.js +8 -2
- package/lib/src/services/lease/LeaseService.js +71 -20
- package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
- package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
- package/lib/test/helpers/expressRoutes.js +41 -0
- package/lib/test/integration/team-visibility-control.spec.js +2 -2
- package/lib/test/unit/access-scopes.spec.js +227 -0
- package/lib/test/unit/api-error-handling.spec.js +179 -0
- package/lib/test/unit/bug-report/route.spec.js +29 -0
- package/lib/test/unit/bug-report/service.spec.js +30 -0
- package/lib/test/unit/control-honest-answers.spec.js +134 -0
- package/lib/test/unit/device-allocation-routes.spec.js +232 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
- package/lib/test/unit/install-repository-app-team.spec.js +4 -1
- package/lib/test/unit/lease/LeaseService.spec.js +5 -4
- package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
- package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
- package/lib/test/unit/omni-vision-failures.spec.js +81 -0
- package/lib/test/unit/openapi-coverage.spec.js +114 -0
- package/lib/test/unit/profile-router.test.js +42 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
- package/lib/test/unit/recording-orchestrator.spec.js +80 -0
- package/lib/test/unit/recordings-library-routes.spec.js +37 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
- package/lib/test/unit/reset-link.test.js +2 -1
- package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
- package/lib/test/unit/users-router.test.js +14 -1
- package/lib/test/unit/wda-client-failures.spec.js +90 -0
- package/lib/test/unit/webhook-delivery.spec.js +142 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
- 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.
|