A trail can now drive several devices in one session, several runs of a trail render as one side-by-side report, and the report viewer ships inside the CLI so it can never drift from the reports it opens.
⚠️ Upgrade notes
- Session video is now opt-in. Recordings are large and sprite extraction is expensive, so pass
--capture-video, setTRAILBLAZE_CAPTURE_VIDEO, or runtrailblaze config capture-video true. The sprite timeline comes back with it. - The legacy WASM report is gone. The interactive report is the only HTML report now, and the
--no-wasm-report/--export-fromflags are removed. trailblaze appopens the legacy desktop app;trailblaze app --v2opens Trail Runner. Trail Runner is still being validated, so the default reverted while that finishes.- Daemon ports must stay below
52529. The52530-59529range is reserved for per-device port allocation, and the daemon now refuses to start there rather than being silently disconnected later.
📱 One trail, several devices
A trail can declare a cast of named devices and hand the session between them with switchDevice. Bind them per run — no daemon restart, and two multi-device trails can run concurrently against different device sets:
trailblaze run checkout.trail.yaml --bind buyer=emulator-5556Each device runs its own app target, and network capture and artifacts are per device, so both displays' evidence lands in one session. A phone and a browser can share a session. Recorded handovers replay mechanically, and AI-driven steps work too — the model is told the roster and can hand over itself. Record one interactively with trailblaze session start --bind, and the save writes the cast into the trail for you.
--bind / --configuration replace the daemon-wide TRAILBLAZE_DEVICE_BINDINGS / TRAILBLAZE_DEVICE_CONFIGURATION, which remain as a fallback for callers that can't pass flags.
🤖 In-process Android test driver (in development)
A third Android driver runs Trailblaze tools inside the app's own AndroidJUnitRunner test — Espresso for Views, your existing Compose rule for composables. No cross-process IPC, no settle floor, no tree marshalling:
trailblaze config android-driver in-processYour existing trails replay on it unchanged. The androidAccessibility selector shape is the canonical one and the resolver bridges it onto the in-process tree, so a trail recorded against the accessibility driver runs here without being re-recorded. Keep authoring in that shape — the native androidView: and compose: dialects name which toolkit drew a widget, so a screen refactored from Views to Compose breaks a trail that still reads identically to a user.
What you give up is the accessibility tree as the source of truth, so an in-process pass is not evidence about what assistive technology can perceive. The accessibility driver remains the default, and stays the one to run when that's the property you care about.
You don't need a Gradle module of your own: trailblaze inprocess retargets a prebuilt shell test APK at one app and signs it with that app's key, and probe-apk tells you first whether the driver can attach at all. Note that no release publishes the shell APK yet — build it from a framework checkout with scripts/build-inprocess-shell.sh.
📊 Reports
- Compare runs side by side. Any report with more than one run can stage several as lanes — the same trail across devices, a retry beside the run it followed, or any set you tick. Map, Grid and Replay all read across the lanes on a shared wall clock.
- Several archives, one report. The viewer holds a list rather than a single field, so a URL from CI and a file off your own disk can sit in the same report. A list of URLs is still a shareable link.
- The viewer ships in the CLI.
trailblaze viewer --output out/index.htmlis a file copy out of the binary — nobun, no Gradle, no checkout — so a hosted viewer always matches the renderer that generated your reports. - Inspect UI works in the standalone viewer. Ranked selector suggestions were silently unavailable outside an exported report. The hosted viewer and docs Report Gallery were also serving a placeholder rather than the real viewer; they now serve it, and every gallery run opens through it.
- Trails that never ran are visible. A trail held back by
config.skip:used to leave no trace, so coverage quietly shrank while the report read green. Reports now carry a Skipped section with each row's reason, andtrailblaze_test_report.jsongains a"SKIPPED"row. A skip never moves a verdict. - Snapshot baselines against a previous run.
--snapshot-baseline <ref>diffstakeSnapshotcaptures against a real earlier run — a CI artifact URL, a local zip, or a session directory — instead of checked-in golden files. - A build's worth of runs. The
generateRunIndextask emits the index half of a report with no evidence in it, for CI builds far too large to embed, linking each cell out to that run's own archive.
Also: attachments render across every report surface, and reports name their device classifier.
⏱️ Profiling and tracing
Four overlapping profiling systems collapsed into one span producer, one durable record, one consumer (trailblaze profile), and two export formats. A run's host half and device half now merge into a single trace.json on one trace id, so the device lane sits alongside the host's tools on one timeline.
Pick the detail with TRAILBLAZE_TRACE_LEVEL (off, normal, verbose — verbose is for a specific investigation, not a default), and export with trailblaze otel, which writes OTLP/JSON and can post to a collector. The standard OTEL_EXPORTER_OTLP_* variables work, so a collector you're already running needs no Trailblaze-specific setup.
✍️ Authoring
trailblaze usages— find usages, for tools. Every trail that invokes a tool, with the device classifiers per usage.--changed-since <ref>derives the tool set from your diff instead, and--jsonis a documented, versioned report contract for gating CI on which trails a tool change affects.- Run the framework's selector resolver inside your own tool via
@trailblaze/scripting/matcher— the resolver, node-detail accessors, tree/bounds helpers and selector types, kept in parity with the Kotlin implementation. - Two authoring traps now documented: don't commit
tools/tsconfig.json(it's machine-generated and workspace-relative), and a subprocess tool needs a// @ts-nocheckline comment — a block comment suppresses nothing.
🔧 Fixes
- Playwright web recordings replay as recorded, and the module's tests now run in CI.
- Device-clock skew no longer corrupts a run's recording.
- A trail declaring a driver outside the classifier it pins now fails instead of running on the wrong one.
- A recorded tap records when it landed on coordinates because its selector missed, so a silently degraded tap is visible in the trail.
- Text input can name the field it types into, and accessibility asserts get the same wait window as instrumentation.
- The shared iOS driver closes when its last owner lets go.
configuration.mdnow documents where session logs land, the daemon ports, and which surface wins when two disagree.- Trail Runner opens in 70 ms instead of 6 seconds.
What's Changed
- Build the standalone report viewer in the Pages deploy by @handstandsam in #241
- Deep-link the report gallery at the hosted report viewer by @handstandsam in #242
- Embed the selector engine in the standalone report viewer by @handstandsam in #243
- Multi-device trails,
trailblaze usages, and a report viewer bundled into the CLI by @handstandsam in #244 - In-process Android test driver, pluggable drivers, unified tracing, and multi-run report views by @handstandsam in #245
Full Changelog: v2026.08.19...v2026.09.02