Skip to content

Commit 03f439f

Browse files
authored
Docker: Retain recordings for failed sessions only from session capabilities (#3111)
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
1 parent f775371 commit 03f439f

File tree

11 files changed

+252
-79
lines changed

11 files changed

+252
-79
lines changed

.github/workflows/docker-test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,34 +55,39 @@ jobs:
5555
os: ubuntu-24.04
5656
firefox-install-lang-package:
5757
enable-managed-downloads:
58+
retain-on-failure:
5859
- test-strategy: test_video_dynamic_name
5960
use-random-user: false
6061
test-video: true
6162
build-all: false
6263
os: ubuntu-24.04
6364
firefox-install-lang-package:
6465
enable-managed-downloads:
66+
retain-on-failure:
6567
- test-strategy: test_video_standalone
6668
use-random-user: false
6769
test-video: true
6870
build-all: false
6971
os: ubuntu-24.04
7072
firefox-install-lang-package:
7173
enable-managed-downloads:
74+
retain-on-failure:
7275
- test-strategy: test_node_docker
7376
use-random-user: false
7477
test-video: true
7578
build-all: false
7679
os: ubuntu-24.04
7780
firefox-install-lang-package:
7881
enable-managed-downloads:
82+
retain-on-failure:
7983
- test-strategy: test_standalone_docker
8084
use-random-user: false
8185
test-video: true
8286
build-all: false
8387
os: ubuntu-24.04
8488
firefox-install-lang-package:
8589
enable-managed-downloads:
90+
retain-on-failure:
8691
- test-strategy: test_parallel
8792
use-random-user: false
8893
test-video: false
@@ -119,34 +124,39 @@ jobs:
119124
os: ubuntu-24.04-arm
120125
firefox-install-lang-package: true
121126
enable-managed-downloads: true
127+
retain-on-failure: true
122128
- test-strategy: test_video_dynamic_name
123129
use-random-user: false
124130
test-video: true
125131
build-all: false
126132
os: ubuntu-24.04-arm
127133
firefox-install-lang-package: true
128134
enable-managed-downloads: true
135+
retain-on-failure: true
129136
- test-strategy: test_video_standalone
130137
use-random-user: false
131138
test-video: true
132139
build-all: false
133140
os: ubuntu-24.04-arm
134141
firefox-install-lang-package: true
135142
enable-managed-downloads: true
143+
retain-on-failure: true
136144
- test-strategy: test_node_docker_video_sidecar
137145
use-random-user: false
138146
test-video: true
139147
build-all: false
140148
os: ubuntu-24.04-arm
141149
firefox-install-lang-package: true
142150
enable-managed-downloads: false
151+
retain-on-failure: true
143152
- test-strategy: test_standalone_docker_video_sidecar
144153
use-random-user: false
145154
test-video: true
146155
build-all: false
147156
os: ubuntu-24.04-arm
148157
firefox-install-lang-package: true
149158
enable-managed-downloads: true
159+
retain-on-failure: true
150160
- test-strategy: test_parallel
151161
use-random-user: false
152162
test-video: false
@@ -246,9 +256,13 @@ jobs:
246256
if [ -n "${SELENIUM_ENABLE_MANAGED_DOWNLOADS}" ]; then
247257
echo "SELENIUM_ENABLE_MANAGED_DOWNLOADS=${SELENIUM_ENABLE_MANAGED_DOWNLOADS}" >> $GITHUB_ENV
248258
fi
259+
if [ -n "${TEST_RETAIN_ON_FAILURE}" ]; then
260+
echo "TEST_RETAIN_ON_FAILURE=${TEST_RETAIN_ON_FAILURE}" >> $GITHUB_ENV
261+
fi
249262
env:
250263
TEST_FIREFOX_INSTALL_LANG_PACKAGE: ${{ matrix.firefox-install-lang-package }}
251264
SELENIUM_ENABLE_MANAGED_DOWNLOADS: ${{ matrix.enable-managed-downloads }}
265+
TEST_RETAIN_ON_FAILURE: ${{ matrix.retain-on-failure }}
252266
- name: Run Docker Compose to ${{ matrix.test-strategy }} on AMD64
253267
if: contains(matrix.os, 'arm') == false
254268
uses: nick-invision/retry@master
@@ -268,6 +282,7 @@ jobs:
268282
command: |
269283
USE_RANDOM_USER_ID=${{ matrix.use-random-user }} VERSION=${BRANCH} BUILD_DATE=${BUILD_DATE} \
270284
TEST_FIREFOX_INSTALL_LANG_PACKAGE=${TEST_FIREFOX_INSTALL_LANG_PACKAGE} SELENIUM_ENABLE_MANAGED_DOWNLOADS=${SELENIUM_ENABLE_MANAGED_DOWNLOADS} \
285+
TEST_RETAIN_ON_FAILURE=${TEST_RETAIN_ON_FAILURE} \
271286
make ${{ matrix.test-strategy }}
272287
- name: Upload recorded video
273288
if: matrix.test-video == true

ENV_VARIABLES.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
|--------------|---------------|-------------|----------------------|
33
| SE_SCREEN_WIDTH | 1920 | Use in Node to set the screen width | |
44
| SE_SCREEN_HEIGHT | 1080 | Use in Node to set the screen height | |
5-
| SE_VIDEO_FILE_NAME | auto | Use in function video recording to set the output file name. Set `auto` for dynamic file name relying on test metadata | |
5+
| SE_VIDEO_FILE_NAME | video.mp4 | Use in function video recording to set the output file name. Set `auto` for dynamic file name relying on test metadata | |
66
| SE_FRAME_RATE | 15 | Set the frame rate for FFmpeg in video recording | |
77
| SE_CODEC | libx264 | Set the codec for FFmpeg in video recording | |
88
| SE_PRESET | -preset ultrafast | Set the preset for FFmpeg in video recording | |
9-
| SE_VIDEO_UPLOAD_ENABLED | false | Enable video upload | |
9+
| SE_VIDEO_UPLOAD_ENABLED | false | Deprecated in event-driven mode (`video_service.py`); upload enablement is now derived from non-empty `SE_UPLOAD_DESTINATION_PREFIX` | |
1010
| SE_VIDEO_INTERNAL_UPLOAD | true | Enable video upload using Rclone in the same recorder container | |
1111
| SE_UPLOAD_DESTINATION_PREFIX | | Remote name and destination path to upload | |
1212
| SE_UPLOAD_PIPE_FILE_NAME | | Set the pipe file name for video upload to consume | |
@@ -162,9 +162,9 @@
162162
| SE_BIND_BUS | true | When true, the Standalone will start the Event Bus and connect itself. Standalone also expose publishing and subscribing port for sidecar service can listen on session events. | --bind-bus |
163163
| SE_EVENT_BUS_IMPLEMENTATION | | Full class name of non-default event bus implementation. For example: org.openqa.selenium.events.zeromq.ZeroMqEventBus | --events-implementation |
164164
| SE_NODE_KUBERNETES_CONFIG_FILENAME | kubernetes.toml | A separate TOML config file name for Dynamic Grid in Kubernetes, which avoid conflict with browser config if shared mouted volume | |
165-
| SE_UPLOAD_FAILURE_SESSION_EVENTS | :failed,:failure | By default, a failure session event type contains ":failed" or ":failure" fires that will trigger the upload failure session only. User can define more event types which handled in your test framework, separated by comma. | |
166-
| SE_UPLOAD_FAILURE_SESSION_ONLY | false | When true, only recording of sessions that are not exited normally (session timed out, or custom events were fired by the client match with failure events defined) | |
167165
| SE_VIDEO_EVENT_DRIVEN | true | Backend of video recorder and uploader will subscribe to Grid Event Bus for session event lifecycle for processing instead of traditional polling Node session capabilities via /status endpoint. | |
168166
| SE_PLAIN_LOGS | true | Use plain log lines | --plain-logs |
169167
| SE_DYNAMIC_MAX_SESSIONS | | Set the number of maximum concurrent sessions of Dynamic Node (both Docker and Kubernetes) | |
170168
| SE_DYNAMIC_OVERRIDE_MAX_SESSIONS | | Enable this flag for setting max session take effect in Dynamic Node (both Docker and Kubernetes) | |
169+
| SE_FAILURE_SESSION_EVENTS | :failed,:failure,:error,:aborted | By default, a failure session event type contains ":failed", ":failure", ":error" or ":aborted" substrings that trigger the retain-on-failure sub-sequence. User can define more event types which handled in your test framework, separated by comma. | |
170+
| SE_RETAIN_ON_FAILURE | false | When true, recordings for sessions that pass are discarded immediately. Only sessions that fail (via failure events or abnormal close) retain their recordings and are queued for upload. | |

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,7 @@ test_video: video hub chrome firefox edge chromium
11111111
echo BASIC_AUTH_PASSWORD=$(or $(BASIC_AUTH_PASSWORD), "admin") >> .env ; \
11121112
echo SUB_PATH=$(or $(SUB_PATH), "/selenium") >> .env ; \
11131113
echo TEST_ADD_CAPS_RECORD_VIDEO=$(or $(TEST_ADD_CAPS_RECORD_VIDEO), "true") >> .env ; \
1114+
echo TEST_RETAIN_ON_FAILURE=$(or $(TEST_RETAIN_ON_FAILURE), "false") >> .env ; \
11141115
if [ $$node = "NodeChrome" ] ; then \
11151116
echo BROWSER=chrome >> .env ; \
11161117
echo VIDEO_FILE_NAME=$${VIDEO_FILE_NAME:-"chrome_video.mp4"} >> .env ; \
@@ -1230,6 +1231,7 @@ test_node_docker: hub standalone_docker standalone_chrome standalone_firefox sta
12301231
echo GRID_URL=$(or $(GRID_URL), "") >> .env ; \
12311232
echo HUB_CHECKS_INTERVAL=$(or $(HUB_CHECKS_INTERVAL), 20) >> .env ; \
12321233
echo TEST_CUSTOM_SPECIFIC_NAME=$(or $(TEST_CUSTOM_SPECIFIC_NAME), "true") >> .env ; \
1234+
echo TEST_RETAIN_ON_FAILURE=$(or $(TEST_RETAIN_ON_FAILURE), "false") >> .env ; \
12331235
echo NODE=$$node >> .env ; \
12341236
echo UID=$$(id -u) >> .env ; \
12351237
echo BINDING_VERSION=$(BINDING_VERSION) >> .env ; \

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,7 @@ services:
813813
`SE_VIDEO_FILE_NAME=auto` will use the session id as the video file name. This ensures that the video file name is unique to upload.
814814
Video file name construction automatically works based on Node endpoint `/status` (and optional GraphQL endpoint) to get session ID, capabilities.
815815
816-
`SE_VIDEO_UPLOAD_ENABLED=true` (`false` by default) will enable the video upload feature. In the background, it will create a pipefile with file and destination for uploader to consume and proceed.
816+
`SE_VIDEO_UPLOAD_ENABLED=true` enables upload in the legacy shell-based mode (`SE_VIDEO_EVENT_DRIVEN=false`). In event-driven mode (the default), this variable is **deprecated** — upload is enabled automatically when `SE_UPLOAD_DESTINATION_PREFIX` is set to a non-empty value.
817817
818818
`SE_VIDEO_INTERNAL_UPLOAD=true` (by default) will use RCLONE installed in the container for upload. If you want to use another sidecar container for upload, set it to `false`.
819819
@@ -846,6 +846,70 @@ When using in Dynamic Grid, those variables should be combined with the prefix `
846846
| `SE_UPLOAD_CONFIG_FILE_NAME` | `upload.conf` | Config file for remote host instead of set via env variable prefix SE_RCLONE_* |
847847
| `SE_UPLOAD_CONFIG_DIRECTORY` | `/opt/bin` | Directory of config file (change it when conf file in another directory is mounted) |
848848
849+
### Retain recordings for failed sessions only
850+
851+
In event-driven mode (`SE_VIDEO_EVENT_DRIVEN=true`, the default), the video service subscribes to the Grid's ZeroMQ event bus and reacts to session lifecycle events in real time. This enables a **retain-on-failure** strategy: record every session, but automatically discard the video when the session passes and only keep (and upload) recordings from sessions that fail.
852+
853+
Enable it globally with the environment variable:
854+
855+
```yaml
856+
SE_RETAIN_ON_FAILURE=true
857+
```
858+
859+
A session is treated as **failed** when either of the following is true:
860+
861+
1. The test code fires a session event whose `eventType` contains a substring from `SE_FAILURE_SESSION_EVENTS` (default: `:failed,:failure,:error,:aborted`).
862+
2. The session closes with an abnormal reason — `TIMEOUT`, `NODE_REMOVED`, or `NODE_RESTARTED` — instead of the normal `QUIT_COMMAND`.
863+
864+
| Environment variable | Default | Description |
865+
|---------------------------|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
866+
| `SE_RETAIN_ON_FAILURE` | `false` | Discard recordings of sessions that pass. Only recordings from failed sessions are retained on disk and queued for upload. |
867+
| `SE_FAILURE_SESSION_EVENTS` | `:failed,:failure,:error,:aborted` | Comma-separated substrings. Any session event whose `eventType` contains one of these (case-insensitive) marks the session as failed. |
868+
869+
The `se:retainOnFailure` session **capability** overrides the global container env var for a specific session. For example, to retain the recording of a single session regardless of the global setting:
870+
871+
```python
872+
options.set_capability('se:retainOnFailure', True)
873+
```
874+
875+
| `se:retainOnFailure` cap | `SE_RETAIN_ON_FAILURE` env | Effective behaviour |
876+
|--------------------------|----------------------------|----------------------------------|
877+
| `true` | `false` (default) | Retain on failure for this session |
878+
| `false` | `true` | Always retain for this session |
879+
| absent | `true` | Retain on failure (global default) |
880+
| absent | `false` (default) | Always retain (global default) |
881+
882+
#### Firing session events from test code
883+
884+
The [Session Event API](https://www.selenium.dev/blog/2026/selenium-grid-4-41-deep-dive/) lets test code push named events directly to the Grid. The video service listens for these events on the ZeroMQ bus and uses them to determine session failure.
885+
886+
Call `driver.fire_session_event(eventType, payload)` from your test. Any `eventType` that contains a configured failure substring (e.g. `"test:failed"` contains `":failed"`) marks the session as failed.
887+
888+
```python
889+
from selenium.webdriver.chrome.options import Options as ChromeOptions
890+
from selenium import webdriver
891+
892+
options = ChromeOptions()
893+
options.set_capability('se:name', 'checkout_flow')
894+
options.set_capability('se:retainOnFailure', True) # discard video if this session passes
895+
896+
driver = webdriver.Remote(options=options, command_executor="http://localhost:4444")
897+
898+
try:
899+
driver.get("https://selenium.dev")
900+
# ... test steps ...
901+
except Exception as exc:
902+
# "test:failed" contains ":failed" — matches the default SE_FAILURE_SESSION_EVENTS
903+
driver.fire_session_event("test:failed", {"error": str(exc)})
904+
raise
905+
finally:
906+
driver.quit()
907+
```
908+
909+
> **Note:** If the test catches an exception and still calls `driver.quit()` normally, the session close reason is `QUIT_COMMAND` (not abnormal). In that case, firing a failure event **before** `quit()` is the only way to mark the session as failed and prevent the recording from being discarded.
910+
911+
So, you can control the **retain-on-failure** strategy fully from test code via session capabilities and fire session event.
912+
849913
## Video recordings manager
850914
851915
We utilize [File Browser](https://filebrowser.org/) as a video manager. It is a web-based file manager that allows you to manage files and folders in the storage.

Video/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ ENV DISPLAY_NUM="99" \
5252
SE_VIDEO_UPLOAD_ENABLED="false" \
5353
SE_VIDEO_INTERNAL_UPLOAD="true" \
5454
SE_UPLOAD_DESTINATION_PREFIX="" \
55-
SE_UPLOAD_FAILURE_SESSION_ONLY="false" \
56-
SE_UPLOAD_FAILURE_SESSION_EVENTS=":failed,:failure"
55+
SE_RETAIN_ON_FAILURE="false" \
56+
SE_FAILURE_SESSION_EVENTS=":failed,:failure,:error,:aborted"
5757

5858
EXPOSE 9000

Video/video_ready.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,25 @@
1212
class Handler(BaseHTTPRequestHandler):
1313

1414
def do_GET(self):
15-
if (
16-
environ.get('SE_VIDEO_UPLOAD_ENABLED', 'false').lower() != 'true'
17-
and environ.get('SE_VIDEO_FILE_NAME', 'video.mp4').lower() != 'auto'
18-
):
19-
video_ready = "ffmpeg" in (p.name().lower() for p in psutil.process_iter())
20-
else:
15+
event_driven = environ.get('SE_VIDEO_EVENT_DRIVEN', 'false').lower() == 'true'
16+
record_video_enabled = environ.get('SE_RECORD_VIDEO', 'true').lower() == 'true'
17+
18+
# Legacy shell mode compatibility:
19+
# when global recording is disabled, report ready immediately.
20+
if not event_driven and not record_video_enabled:
2121
video_ready = True
22+
else:
23+
# Event-driven mode enables upload by non-empty destination.
24+
# Legacy shell mode keeps original SE_VIDEO_UPLOAD_ENABLED behaviour.
25+
if event_driven:
26+
upload_enabled = bool(environ.get('SE_UPLOAD_DESTINATION_PREFIX', '').strip())
27+
else:
28+
upload_enabled = environ.get('SE_VIDEO_UPLOAD_ENABLED', 'false').lower() == 'true'
29+
30+
if not upload_enabled and environ.get('SE_VIDEO_FILE_NAME', 'video.mp4').lower() != 'auto':
31+
video_ready = "ffmpeg" in (p.name().lower() for p in psutil.process_iter())
32+
else:
33+
video_ready = True
2234
response_code = 200 if video_ready else 404
2335
response_text = "ready" if video_ready else "not ready"
2436
self.send_response(response_code)

0 commit comments

Comments
 (0)