- What is this?
- The Two Tools at a Glance
- Quick Start
- Recording Event Contract (shared)
- UIA Recorder — Desktop Recorder
- Chromium Recorder — Browser Recorder
- SignalR Client Examples
- G4.Recorders.ClientApp — Monitor Both Hubs
- G4 Recorders Path Finder — Quick Start
- License
G4 Recorders is a growing suite of recording tools that share one event contract. It currently ships two recorders, with Android and iOS recorders planned to extend the same contract to mobile platforms:
- UIA Recorder (
G4.Recorders.Uia) — a Windows UI Automation inspection and recording tool. It peeks at desktop UI elements (returning their ancestor chain, like an XPath for Windows UI) and records global keyboard and mouse events enriched with UI context. - Chromium Recorder (
G4.Recorders.Chromium) — a browser recording tool. A bundled Chromium recorder extension captures in-page interactions (with a DOM locator chain) and streams them to a hub, which relays them to consumers.
Every recorder in the suite broadcasts the same ReceiveRecordingEvent message over SignalR, so a consumer can watch interactions across desktop, browser, and (in the future) mobile platforms through a single, uniform contract.
Use cases:
- Debug and inspect desktop applications and web pages to troubleshoot UI behavior.
- Build automated UI tests or robotic process automation (RPA) workflows.
- Record user input with UI context for replay or analysis.
- Monitor UI and user actions in real time.
| Tool | Records | Hub port | How recording starts | REST surface |
|---|---|---|---|---|
| UIA Recorder | Windows desktop (global input) | 9955 |
Automatically (always-on capture) | peek + ping |
| Chromium Recorder | Chromium browser (via extension) | 9956 |
When a recorder browser is running | ping only |
Both hubs share the path /hub/v4/g4/peek and emit the same ReceiveRecordingEvent payload (see Recording Event Contract).
Requirements: .NET 10 Runtime/SDK. UIA Recorder requires Windows (and Administrator privileges are recommended for global input hooks). Chromium Recorder additionally requires a Chromium/Chrome browser with the recorder extension loaded.
- Go to Releases and download the latest artifact: 👉 Releases: https://github.com/g4-api/g4-recorders/releases
- Unzip the archive to a folder with write permissions (e.g.,
C:\Tools\G4.Recorders). - Run the executable(s) from that folder:
G4.Recorders.Uia.exe— desktop recorder + peek API (listens on9955). Run as administrator for global hooks.G4.Recorders.Chromium.exe— browser recorder hub (listens on9956).
Each service exposes a Swagger UI and a health-check endpoint:
# UIA Recorder
curl http://localhost:9955/api/v4/g4/ping
# Output: Pong
# Chromium Recorder
curl http://localhost:9956/api/v4/g4/ping
# Output: Pong- UIA Recorder Swagger:
http://localhost:9955/swagger - Chromium Recorder Swagger:
http://localhost:9956/swagger
CORS tip: If you call from a webview or browser, ensure your origin is allowed via the
ORIGINSenv var orAllowedOriginsconfig. VS Code webviews (vscode-webview://) and the Chromium recorder extension (chrome-extension://) are already handled.
Both tools broadcast recorded interactions to connected clients using the SignalR message ReceiveRecordingEvent. The payload is always wrapped in a { "value": { ... } } envelope with this shape:
chain— the element chain (locator + orderedpathof nodes) where the event occurred.event— the specific event name (e.g.,Down,Click).machineName— the machine that recorded the event.offset— the pointer distance from the target rectangle's top-left corner, defaulting to{ "x": 0, "y": 0 }.timestamp— Unix epoch milliseconds.type— the event category (e.g.,Keyboard,Mouse).value— the event payload (e.g., the pressed key, or coordinates).
Sample ReceiveRecordingEvent payload (UIA Recorder keyboard event):
{
"value": {
"chain": {
"locator": "/Desktop/Window[@Name='...']/Document[@Name='Text Area']",
"path": [
{
"automationId": "Console Window",
"className": "ConsoleWindowClass",
"controlTypeId": 50032,
"controlType": "Window",
"isTopWindow": true,
"isTriggerElement": false,
"name": "...exe",
"processId": 31036
},
{
"automationId": "Text Area",
"bounds": { "height": 519, "X": 104, "Y": 104, "width": 993 },
"controlTypeId": 50030,
"controlType": "Document",
"isTopWindow": false,
"isTriggerElement": true,
"name": "Text Area",
"processId": 31036
}
],
"trigger": "Focus"
},
"event": "Down",
"offset": { "x": 0, "y": 0 },
"timestamp": 1757618366611,
"type": "Keyboard",
"value": { "scanCode": 30, "virtualKey": 65, "key": "a" }
}
}Note: The
patharray is ordered top-down. The last element is always the target element (the trigger) and carries more metadata than its ancestors to keep the payload compact. Chromium Recorder events use the same top-level shape; only the chain node contents differ (DOM elements instead of UIA elements).
For UIA mouse events, offset.x is the captured pointer X coordinate minus the trigger bounds' X, and
offset.y is the pointer Y coordinate minus the trigger bounds' Y. Chromium events currently retain the shared
zero default until DOM-relative offset capture is enabled.
UIA Recorder listens on http://localhost:9955. Recording events flow automatically as soon as the service is running — there is no session to start; move the mouse or type and events are broadcast to every connected client.
# Peek at focused element
G4.Recorders.Uia peek -f
# Peek at specific coordinates
G4.Recorders.Uia peek -x 100 -y 200The RecorderController provides REST endpoints to peek at UI elements (UIA Recorder only).
# Peek at specific coordinates (x=250, y=300)
curl "http://localhost:9955/api/v4/g4/peek?x=250&y=300"
# Peek at the currently focused element
curl "http://localhost:9955/api/v4/g4/peek?focused=true"The response is the ancestor chain object (same shape as the chain field in a recording event).
Hub URL: http://localhost:9955/hub/v4/g4/peek
Client → Server Methods
SendHeartbeat()SendPeekAt({ xPos, yPos })SendPeekFocused()
Server → Client Events
ReceiveHeartbeat— heartbeat confirmationReceivePeek— ancestor chain resultReceiveRecordingEvent— keyboard/mouse events with UI context (broadcast automatically)
See SignalR Client Examples for Python, JavaScript, and C# clients.
Chromium Recorder listens on http://localhost:9956. Unlike UIA Recorder, it does not capture anything by itself — a Chromium recorder extension running in a browser produces the events.
- A client asks Chromium Recorder to launch a browser (
StartRecorder), or you start a browser that already has the recorder extension loaded. - The extension connects to the hub and pushes in-page interactions via
SendRecordingEvent. - The hub re-broadcasts each interaction to every connected consumer as
ReceiveRecordingEvent— the same message and envelope UIA Recorder uses. - On
StopRecorder, the hub asks the extension to close the browser gracefully (aCloseBrowsermessage), then force-stops the process if it does not exit in time.
The extension has its own setup and usage guide:
src/G4.Recorders.Chromium.Extension/README.md.
Hub URL: http://localhost:9956/hub/v4/g4/peek
Client → Server Methods
StartRecorder(driverParameters)→ returns the launched browser process idStopRecorder(processId)→ returnstrueif a tracked browser was stopped,falseif the id is unknown/already closedSendHeartbeat()SendRecordingEvent(event)— used by the extension (producer); consumers do not call this
Server → Client Events
ReceiveHeartbeat— heartbeat confirmationReceiveRecordingEvent— browser interactions with a DOM chainCloseBrowser— sent to the extension to close the browser during a graceful stop
No REST peek: Chromium Recorder is controlled entirely over SignalR. The only REST endpoint is the
pinghealth check.
StartRecorder takes a W3C-style driver-parameters object; only the Chromium binary and optional arguments are used:
{
"capabilities": {
"alwaysMatch": {
"goog:chromeOptions": {
"binary": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"args": ["--disable-gpu"]
}
}
}
}Each launch gets its own dedicated user-data directory, so it never collides with an already-running browser. Keep the returned process id to stop it later with StopRecorder(processId).
curl http://localhost:9956/api/v4/g4/ping
# Output: PongThe listening pattern is identical for both hubs — only the URL changes (9955 for UIA Recorder, 9956 for Chromium Recorder). The Chromium-only StartRecorder/StopRecorder calls are shown alongside.
Using the community client signalrcore.
pip install signalrcorefrom signalrcore.hub_connection_builder import HubConnectionBuilder
# Use 9955 for UIA Recorder or 9956 for Chromium Recorder.
hub = (
HubConnectionBuilder()
.with_url("http://localhost:9955/hub/v4/g4/peek")
.with_automatic_reconnect({"type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5})
.build()
)
def on_heartbeat(payload):
print("Heartbeat:", payload)
def on_peek(payload):
print("Peek:", payload)
def on_recording_event(payload):
event = (payload[0] if isinstance(payload, list) else payload).get("value", {})
print(f"RecordingEvent: type={event.get('type')} event={event.get('event')} value={event.get('value')}")
hub.on("ReceiveHeartbeat", on_heartbeat)
hub.on("ReceivePeek", on_peek)
hub.on("ReceiveRecordingEvent", on_recording_event)
hub.start()
# UIA Recorder: events flow automatically. Optional peek/heartbeat calls:
hub.send("SendHeartbeat", [])
hub.send("SendPeekFocused", [])
# Chromium Recorder only: launch a recorder browser (events start once it is recording).
# hub.send("StartRecorder", [{
# "capabilities": {"alwaysMatch": {"goog:chromeOptions": {
# "binary": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "args": []
# }}}
# }])
input("Press <Enter> to stop...\n")
hub.stop()Using @microsoft/signalr (browser or Node.js).
npm i @microsoft/signalrimport * as signalR from "@microsoft/signalr";
// Use 9955 for UIA Recorder or 9956 for Chromium Recorder.
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:9955/hub/v4/g4/peek")
.withAutomaticReconnect()
.build();
connection.on("ReceiveHeartbeat", (payload) => console.log("Heartbeat:", payload));
connection.on("ReceivePeek", (payload) => console.log("Peek chain:", payload?.value));
connection.on("ReceiveRecordingEvent", (payload) => {
const event = payload?.value || {};
console.log("RecordingEvent:", event.type, event.event, event.value);
});
async function main() {
await connection.start();
// UIA Recorder: events flow automatically. Optional peek/heartbeat:
await connection.invoke("SendHeartbeat");
await connection.invoke("SendPeekFocused");
// Chromium Recorder only: launch and later stop a recorder browser.
// const processId = await connection.invoke("StartRecorder", {
// capabilities: { alwaysMatch: { "goog:chromeOptions": {
// binary: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", args: []
// } } }
// });
// await connection.invoke("StopRecorder", processId);
}
main().catch(console.error);Using Microsoft.AspNetCore.SignalR.Client.
dotnet add package Microsoft.AspNetCore.SignalR.Clientusing Microsoft.AspNetCore.SignalR.Client;
using System.Text.Json;
// Use 9955 for UIA Recorder or 9956 for Chromium Recorder.
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:9955/hub/v4/g4/peek")
.WithAutomaticReconnect()
.Build();
connection.On<JsonElement>("ReceiveRecordingEvent", envelope =>
{
var recordingEvent = envelope.GetProperty("value");
var type = recordingEvent.GetProperty("type").GetString();
var name = recordingEvent.GetProperty("event").GetString();
Console.WriteLine($"RecordingEvent: {type} / {name}");
});
await connection.StartAsync();
// UIA Recorder: events flow automatically. Optional peek/heartbeat:
await connection.InvokeAsync("SendHeartbeat");
await connection.InvokeAsync("SendPeekFocused");
// Chromium Recorder only: launch and later stop a recorder browser.
// var driverParameters = new
// {
// capabilities = new
// {
// alwaysMatch = new Dictionary<string, object?>
// {
// ["goog:chromeOptions"] = new { binary = @"C:\Program Files\Google\Chrome\Application\chrome.exe", args = Array.Empty<string>() }
// }
// }
// };
// var processId = await connection.InvokeAsync<int>("StartRecorder", driverParameters);
// var isStopped = await connection.InvokeAsync<bool>("StopRecorder", processId);
Console.WriteLine("Press <Enter> to exit...");
Console.ReadLine();G4.Recorders.ClientApp (src/G4.Recorders.ClientApp) is a .NET 10 console test harness that connects to both hubs at once and prints every recorder event to the console. It is the quickest way to watch desktop and browser events side by side.
cd src/G4.Recorders.ClientApp
dotnet run # compact colored line per event
dotnet run -- --verbose # full JSON per event- Choose sources — toggle each hub in
appsettings.json(Hubs:UiaEnabled/Hubs:ChromiumEnabled) or at the command line (--no-uia,--no-chromium,--uia,--chromium). - Drive Chromium — set
Chromium:BinaryPathinappsettings.json, then presssto launch a recorder browser andxto stop it. - Keys —
sstart Chromium ·xstop Chromium ·vtoggle verbose ·hhelp ·qquit.
UIA events appear as soon as you move/click/type; Chromium events appear once a recorder browser is running.
G4 Recorders Path Finder shows the UI-Automation path (an XPath-like locator) for whatever is currently under your mouse pointer. Hover any app control; the locator appears in the text box for copy/paste into your automation or QA tools.
- Windows desktop.
- If you need to peek elevated apps, run Path Finder as Administrator.
-
Open G4 Recorders Path Finder v1.0 by running
G4.Recorders.Uia.PathFinder.exe. -
You’ll see a title, a locator text box, a Start/Stop button, and a Faster/Slower slider.
╭── G4 Recorders — Title ───────────────────────────────╮ │ │ │ Locator │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [ ▶ Start / ⬛ Stop] │ │ │ │ Faster ◄───────┈┈┈┈┈●┈┈┈┈────────► Slower │ │ │ ╰───────────────────────────────────────────────────────╯
- Click ▶ Start (or press Alt+S).
- Move your mouse over the target application.
- The locator appears and updates in the text box.
- Use the Faster ←→ Slower slider to change refresh rate.
- Faster ≈ ~500 ms; Slower ≈ ~3000 ms.
- Faster feels live; slower reduces CPU and makes copying easier.
- Select the locator in the text box and press Ctrl+C.
- Paste into your test or automation scripts.
- If the text changes while selecting, press ⬛ Stop first, then copy.
- Click ⬛ Stop (or Alt+S) to pause tracking.
- Open the target app.
- Start Path Finder and hover precisely over the desired control (button, textbox, menu item).
- Adjust the slider if updates are too fast or too slow.
- Stop, copy the locator, and paste where needed.
- Be precise: tiny mouse movements can change the target element.
- Hover the actionable sub-area (icon/text) if a widget is composite.
- Keep the Path Finder window out of the target area to avoid peeking it.
- Run as Administrator when inspecting admin-level windows.
- No updates: ensure you pressed Start; try a slower rate; run as Administrator for elevated apps.
- Hard to copy: press Stop, then copy; or slide toward Slower.
- Unexpected element: re-hover the exact clickable region; complex controls may have nested elements.
- Alt+S toggles Start/Stop.
- Ctrl+C copies the locator from the text box.
- Close the window (X or Alt+F4). Any active tracking stops automatically.
MIT License. See LICENSE for details.