This repository contains a Chrome extension that captures user events and a backend API (FastAPI + PyMongo) that writes those events to MongoDB Atlas.
- ENV (connection and access):
server/.env- Set your Mongo connection (
ATLAS_URI) and access targets (ALLOWED_DB,EVENT_COLLECTION). - Optional:
API_KEYto require an API key (if you set this, also set it inextension/config.js).
- Set your Mongo connection (
- Config (what to record):
extension/event-config.json- Define which browser events are captured (
name), whether they are enabled (enabled), and which handler to use (handler).
- Define which browser events are captured (
These two files are the only drivers; everything else reads from them.
- Prerequisites
- Setup
- Quick Start
- Environment Variables
- Config (Central Driver)
- API
- How it works
- Output
- Storage
- Notes
- MongoDB Atlas cluster and connection string
- Chrome (for loading the extension)
- Python 3.10+
- Environment variables are already provided in this repo via
server/.env.example.- Just copy it to
server/.envand you will have the current access configuration (my access). - To view results in Mongo with the current defaults, see the "Where to find the data" link in the Output section.
- If you want your own access: the current setup does not use an API key and points to my Atlas admin URI. Replace the following in
server/.envwith your values:ATLAS_URI(your MongoDB connection string)- optionally
ALLOWED_DBandEVENT_COLLECTION - if you set
API_KEY, also set the same value inextension/config.js(API_KEYconstant)
- You can always view the output locally under
<project-root>/intermediate/<ISO-timestamp>/.
- Just copy it to
Run everything from the repository root (capstone_git):
# 0) Enter the project directory
cd event-capture
# 1) Create a virtual environment and install server dependencies
python3 -m venv server/.venv
source server/.venv/bin/activate
pip install -r server/requirements.txt
# 2) Copy env template and edit credentials (env is already in repo)
cp server/.env.example server/.env
# Open server/.env and set ATLAS_URI, API_KEY (optional), etc.
# 3) Start the API server on port 3000
server/.venv/bin/python -m uvicorn server.server:app --host 0.0.0.0 --port 3000 --reloadThen load the Chrome extension:
- Open Chrome > Extensions > Enable Developer mode > Load unpacked
- Select the
extension/directory (the folder containingmanifest.json) - The extension will POST to
http://localhost:3000/api/eventsby default
For subsequent shells, re-activate with:
source event-capture/server/.venv/bin/activateCredentials: See Setup for using the defaults or replacing with your own.
A template is already provided. Just copy it and edit your values:
cp server/.env.example server/.envExample values inside .env:
ATLAS_URI="mongodb+srv://<username>:<password>@<cluster-host>/?retryWrites=true&w=majority"
ALLOWED_DB="capstone"
ALLOWED_COLLECTIONS='["events"]'
EVENT_COLLECTION="events"
API_KEY="replace-with-strong-secret"- ATLAS_URI: Your MongoDB connection string (Atlas or self-hosted)
- ALLOWED_DB: Database name the API is allowed to access
- ALLOWED_COLLECTIONS: JSON array of allowed collection names
- EVENT_COLLECTION: Collection used for
/api/eventsinserts (should be in ALLOWED_COLLECTIONS) - API_KEY: Optional. If set, calls must include the header
x-api-key: <API_KEY>
The central configuration for what gets recorded is extension/event-config.json (already present). This file drives which events are captured by the content script.
- It is the single source of truth for enabling/disabling categories of events.
- Each entry defines the browser event name, whether it is enabled, and which handler to use.
- Historical
screenshottoggles have been removed now that screen recording is always on.
Fields:
name: The DOM or navigation event name (e.g.,click,input,popstate).enabled:trueto attach a listener and record the event;falseto skip.handler: Which function the recorder will attach for this event (recordEvent,debouncedRecordInput,debouncedRecordScroll).
Details (click to expand):
Example: domEvents and navigationEvents
{
"domEvents": [
{ "name": "click", "enabled": true, "handler": "recordEvent" },
{ "name": "input", "enabled": false, "handler": "debouncedRecordInput" },
{ "name": "scroll", "enabled": false, "handler": "debouncedRecordScroll" }
],
"navigationEvents": [
{ "name": "popstate", "enabled": false },
{ "name": "pushState", "enabled": false },
{ "name": "replaceState", "enabled": false },
{ "name": "beforeunload", "enabled": false }
],
"observers": { "dynamicDom": false }
}Event meanings and examples:
| Event name | What it captures | Example |
|---|---|---|
| click | User clicks (button, link, interactive element) | Click on #submit button |
| input | Input text changes (debounced) | Typing in #search field |
| change | Committed value changes | Selecting from a <select> |
| scroll | Significant scrolls (debounced) | Scrolling page 200px |
| keydown/up | Keyboard presses/releases | Pressing Enter |
| mouseover/out | Pointer enter/leave (interactive/tooltip) | Hover over menu item |
| submit | Form submissions | Submitting login form |
| popstate/pushState/replaceState | Page navigations (recorded with the same event name) | /home → /profile |
| focus/blur | Focus gained/lost | Focusing an input |
| touch* | Mobile touch interactions | touchstart on element |
Navigation events emitted by the recorder use the browser event name for type (e.g., popstate) and include category: "navigation" for downstream grouping.
Adding a new event type:
- Add an entry in
extension/event-config.json(setenabled: trueand choose ahandler). - If it needs special handling, add logic in
extension/recorder.js(e.g., extendrecordEventor add a new handler and map it ingetHandlerByKey). - Optionally add a summary row to the table above in this README.
-
Keep enabled (core flow integrity)
click: Primary interaction signal; required for almost every task.navigationEvents(popstate,pushState,replaceState,beforeunload): Strongly recommended to reconstruct flows and correlate with screen video.submit: Recommended for forms and e‑commerce (e.g., Add to Cart triggers form submits on many sites).change: Recommended if you need actual selection/value changes (e.g., Quantity dropdowns, checkboxes, radios).input: Recommended if you need typed text; disable if minimizing PII.
-
Optional / noisy (enable only when needed)
scroll: High‑volume; enable if scroll position matters for analysis.mouseover/mouseout(hover): High‑volume; enable for hover‑driven menus/tooltips.pointerdown/pointerup/mousedown/mouseup: Usually redundant withclick; enable for low‑level pointer diagnostics or exotic widgets.focus/blur: Useful for detailed form flows; optional otherwise.keydown/keyup/keypress: Enable for keyboard‑centric tasks; preferkeydown/keyupover legacykeypress.touchstart/touchend/touchmove: Only for mobile/touch testing.observers.dynamicDom: Enable to re‑mark dynamic pages (BrowserGym marks); may add CPU overhead.
Note: The recorder attaches early, capture‑phase listeners for robustness on sites that stop propagation; the enabled flags in event-config.json still govern which events are actually recorded.
The repo now defaults to a lean capture preset focused on core flow signals:
- Enabled:
click,input,change,submit, and allnavigationEvents. - Disabled: everything else (scroll, hover, pointer low‑level, focus/blur, key*, touch*, etc.).
Example minimal config
{
"domEvents": [
{ "name": "click", "enabled": true, "handler": "recordEvent" },
{ "name": "input", "enabled": true, "handler": "debouncedRecordInput" },
{ "name": "change", "enabled": true, "handler": "debouncedRecordInput" },
{ "name": "submit", "enabled": true, "handler": "recordEvent" },
{ "name": "scroll", "enabled": false, "handler": "debouncedRecordScroll" },
{ "name": "mouseover", "enabled": false, "handler": "recordEvent" },
{ "name": "mouseout", "enabled": false, "handler": "recordEvent" },
{ "name": "keydown", "enabled": false, "handler": "recordEvent" },
{ "name": "keyup", "enabled": false, "handler": "recordEvent" },
{ "name": "keypress", "enabled": false, "handler": "recordEvent" },
{ "name": "pointerdown", "enabled": false, "handler": "recordEvent" },
{ "name": "pointerup", "enabled": false, "handler": "recordEvent" }
],
"navigationEvents": [
{ "name": "popstate", "enabled": true },
{ "name": "pushState", "enabled": true },
{ "name": "replaceState", "enabled": true },
{ "name": "beforeunload", "enabled": true }
],
"observers": { "dynamicDom": false }
}This keeps event volume low while preserving everything needed to reconstruct task flows (including SELECT dropdown changes and form submits like “Add to Cart”).
The snippets below show how an event is enabled in event-config.json, a minimal HTML that triggers it, and an excerpt of the recorded payload.
Config
{ "name": "click", "enabled": true, "handler": "recordEvent" }HTML
<button id="buy">Add to Cart</button>
<!-- Amazon also uses <input type="submit" id="add-to-cart-button" /> -->Recorded (excerpt)
{ "type": "click", "target": { "tag": "BUTTON", "id": "buy", "text": "Add to Cart" } }Config
{ "name": "input", "enabled": true, "handler": "debouncedRecordInput" }HTML
<label>Email <input id="email" type="email" /></label>Recorded (excerpt)
{ "type": "input", "target": { "id": "email" }, "inputType": "insertText", "data": "a" }Config
{ "name": "change", "enabled": true, "handler": "debouncedRecordInput" }HTML (Quantity dropdown)
<label for="qty">Quantity:</label>
<select id="qty">
<option>1</option>
<option>2</option>
<option>3</option>
<!-- Amazon often uses a stylized span that forwards to a hidden <select>; enabling change captures the value update. -->
</select>Recorded (excerpt)
{ "type": "change", "target": { "tag": "SELECT", "id": "qty" }, "targetValue": "2" }Config
{ "name": "submit", "enabled": true, "handler": "recordEvent" }HTML
<form id="f">
<input name="q" />
<button type="submit" id="go">Search</button>
</form>Recorded (excerpt)
{ "type": "submit", "target": { "tag": "FORM", "id": "f" } }Config
{ "name": "keydown", "enabled": true, "handler": "recordEvent" }
{ "name": "keyup", "enabled": true, "handler": "recordEvent" }
{ "name": "keypress", "enabled": true, "handler": "recordEvent" }HTML
<input id="search" placeholder="Type and press Enter" />Recorded (excerpt)
{ "type": "keydown", "key": "Enter", "code": "Enter", "target": { "id": "search" } }Config
{ "name": "scroll", "enabled": true, "handler": "debouncedRecordScroll" }HTML
<div id="pane" style="height:120px; overflow:auto">
<div style="height:800px"></div>
</div>Recorded (excerpt)
{ "type": "scroll", "target": { "id": "pane" }, "scroll": { "scrollTop": 120 } }Config
{ "name": "mouseover", "enabled": true, "handler": "recordEvent" }
{ "name": "mouseout", "enabled": true, "handler": "recordEvent" }HTML
<a id="help" title="Opens help">Help</a>Recorded (excerpt)
{ "type": "mouseover", "target": { "id": "help" } }Config
{ "name": "pointerdown", "enabled": true, "handler": "recordEvent" }
{ "name": "pointerup", "enabled": true, "handler": "recordEvent" }
{ "name": "touchstart", "enabled": true, "handler": "recordEvent" }
{ "name": "touchend", "enabled": true, "handler": "recordEvent" }
{ "name": "touchmove", "enabled": true, "handler": "recordEvent" }HTML
<button id="tap">Tap me</button>Recorded (excerpt)
{ "type": "pointerdown", "pointerType": "touch", "target": { "id": "tap" } }Config
{ "name": "popstate", "enabled": true }
{ "name": "pushState", "enabled": true }
{ "name": "replaceState", "enabled": true }
{ "name": "beforeunload", "enabled": true }HTML/JS
<button id="route">Go to /settings</button>
<script>
document.getElementById('route').onclick = () => {
history.pushState({}, '', '/settings');
};
</script>Recorded (excerpt)
{ "type": "pushState", "fromUrl": "https://example.com/", "toUrl": "https://example.com/settings" }- POST
/api/events- Headers:
Content-Type: application/json,x-api-key: <API_KEY>(if configured) - Body (matches the Chrome extension payload shape):
{ "task": "My Task Title", "duration": 123, "events_recorded": 2, "start_url": "https://example.com", "end_url": "https://example.com/page", "data": [ { "type": "click", "timestamp": 1728213140000, "url": "https://example.com", "target": { "tag": "BUTTON", "id": "submit", "class": "btn primary", "text": "Submit", "value": "", "isInteractive": true, "xpath": "//*[@id=\"submit\"]", "cssPath": "button#submit.btn.primary", "bid": "button-primary-abc123", "a11y": { "role": "button", "name": "Submit" }, "attributes": { "id": "submit", "class": "btn primary" }, "boundingBox": { "x": 10, "y": 20, "width": 100, "height": 30 } } } ] } - Response:
{ "success": true, "documentId": "<mongo-id>" }
- Headers:
Chrome Extension → FastAPI → MongoDB
- Chrome extension (popup + content script) captures user actions based on
extension/event-config.jsonand builds a payload. - FastAPI (
server/server.py) accepts POST/api/events, inserts into MongoDB, and writes a local snapshot underintermediate/<timestamp>/. - You can view results immediately on disk, or in Mongo using the connection from your
server/.env.
Where to find the data:
- MongoDB (current defaults in this repo):
- Database:
capstone - Collection:
events - Copy/paste Connection (MongoDB Compass/Driver):
mongodb+srv://sid:cYbyLu9DhiZNNvIj@capstone.xydgfjo.mongodb.net/capstone?retryWrites=true&w=majority&appName=capstone - If you change
ALLOWED_DBorEVENT_COLLECTIONinserver/.env, open that DB/collection accordingly.
- Database:
- Local disk:
<project-root>/intermediate/<ISO-timestamp>/payload.jsonandmetadata.json.
Sample MongoDB document (click to expand):
Show sample document
{
"task": "Search and submit",
"duration": 42,
"events_recorded": 2,
"start_url": "https://example.com",
"end_url": "https://example.com/results",
"data": [
{
"type": "click",
"timestamp": 1728213140000,
"url": "https://example.com",
"target": {
"tag": "BUTTON",
"id": "submit",
"class": "btn primary",
"text": "Submit",
"value": "",
"isInteractive": true,
"xpath": "//*[@id=\"submit\"]",
"cssPath": "button#submit.btn.primary",
"bid": "button-primary-abc123",
"a11y": { "role": "button", "name": "Submit" },
"attributes": { "id": "submit", "class": "btn primary" },
"boundingBox": { "x": 10, "y": 20, "width": 100, "height": 30 }
}
},
{
"type": "popstate",
"category": "navigation",
"timestamp": "2025-10-06T16:28:20.456Z",
"fromUrl": "https://example.com",
"toUrl": "https://example.com/results",
"title": "Results",
"referrer": "https://example.com",
"fromUserInput": true
}
],
"timestamp": "2025-10-06T16:29:14.694Z"
}- MongoDB: Documents are inserted into database
ALLOWED_DBand collectionEVENT_COLLECTION(both fromserver/.env). - Local intermediate archive:
- The server writes files under
<project-root>/intermediate/<ISO-timestamp>/:payload.json: the request payload datametadata.json: save time, inserted id, counts, and file paths
- The server writes files under
Find these under the project root after a successful POST.
- The Chrome extension code lives in
extension/and posts to the API viaextension/config.js. - If you set
API_KEYinserver/.env, also set it in the extension to send thex-api-keyheader. - To customize captured events, edit
extension/event-config.json.