Architecture overview
Email Tracker uses a client-server architecture where the Chrome extension injects tracking pixels and the Node.js server records open events.Component responsibilities
Chrome extension
The extension has three main parts: Content script (extension/src/content/gmailCompose.js:161-315)
- Detects Gmail compose dialogs using MutationObserver
- Extracts recipient and sender email from compose UI
- Requests tracking data from background worker
- Injects 1x1 transparent pixel into message body
- Renders inbox badges showing open counts
- Scans thread images for sender suppression
extension/src/background/serviceWorker.js)
- Generates stable
user_id(stored in chrome.storage) - Creates unique
email_id(UUID) for each message - Encodes tracking token with email metadata
- Fetches dashboard data and caches results
- Sends
POST /mark-suppress-nextsignals
extension/src/popup/*)
- Configuration interface for tracker URL and dashboard token
- Debug view showing recent tracked emails
- Quick links to dashboard
Node.js server
The server is an Express application with the following structure: Main application (server/src/index.ts:1-24)
server/src/routes/track.ts)
GET /t/:token.gif- Serves transparent pixel and records openPOST /mark-suppress-next- Receives sender suppression signalsGET /metrics/gmail-proxy-latency- Returns latency statisticsGET /metrics/suppress-signals- Debug endpoint for suppression events
server/src/routes/dashboard.ts)
GET /dashboard- Serves dashboard HTML UIGET /dashboard/api/emails- Returns tracked emails with open countsGET /dashboard/api/open-events- Returns detailed open event history- All dashboard APIs require
X-Tracker-Tokenheader matchingDASHBOARD_TOKEN
SQLite database
The database uses two main tables: tracked_emails (server/src/db/schema.sql:3-11)
server/src/db/schema.sql:13-31)
Open events are always recorded, even if marked as duplicate or sender-suppressed. The
open_count only increments for non-duplicate, non-suppressed opens.End-to-end lifecycle
Here’s the complete flow from installation to analytics:1. Install and configure
- Operator installs dependencies and builds workspaces:
- Operator starts server with environment variables:
- Operator loads extension in Chrome and configures popup with tracker URL and token
2. Sender composes and sends
- Gmail content script detects compose dialogs using
document.querySelectorAll('div[role="dialog"]') - On send intent (click, keyboard shortcut), content script extracts recipient and sender
- Content script sends message to background worker:
- Background worker generates token and returns pixel URL
- Content script injects hidden pixel (
extension/src/content/gmailCompose.js:280-286):
3. Message arrives in recipient mailbox
- Recipient’s email client renders the HTML message
- When pixel URL is in viewport (or pre-fetched), client makes
GET /t/token.gifrequest - Some clients (Gmail, Outlook) use image proxy servers that pre-fetch images
4. Server processes pixel request
Token decoding (server/src/routes/track.ts:99)
server/src/routes/track.ts:106-120)
server/src/routes/track.ts:153-160)
src/services/openRecorder.ts)
- Queries
open_eventsfor recent opens with sameemail_id,ip_address, anduser_agent - If found within
DEDUP_WINDOW_MS(default 30 seconds), marks as duplicate - Duplicate events don’t increment
tracked_emails.open_count - All events are stored in
open_eventstable with flags
server/src/routes/track.ts:171-177)
5. Dashboard and extension read analytics
- Dashboard UI polls
GET /dashboard/api/emailswithX-Tracker-Tokenheader - Extension background worker fetches inbox badge data every 10 seconds
- Content script renders badges in Gmail inbox rows
- Badge click opens dashboard with pre-filtered view for that email
Unique suppression model
Email Tracker uses identity-based, event-driven suppression to prevent counting sender self-opens.Why identity-based?
Gmail is a single-page application where folder names and UI state are unreliable:- Sent folder vs Inbox detection is brittle with SPAs
- URL patterns change frequently
- Tab and thread navigation uses history API
How suppression works
Step 1: Content script scans images (extension/src/content/gmailCompose.js:519-570)
- Background worker receives message from content script
- Makes
POST /mark-suppress-nextrequest to server withemail_id - Server stores entry in in-memory map with timestamp
server/src/routes/track.ts:36-47)
server/src/routes/track.ts:106-120)
Key characteristics
Consume-once semantics- Each suppression signal is consumed by the first pixel hit
- Subsequent opens (e.g., recipient opens) are counted normally
- Prevents over-suppression from multiple Gmail tabs
SUPPRESSION_TTL_MS = 10_000)
- Entries expire after 10 seconds if not consumed
- This is cleanup logic, not core suppression behavior
- Normal flow: signal → pixel → consume (< 1 second)
- Gmail pre-fetches images through proxy servers
- Server detects proxy requests by User-Agent and IP prefix
- Measures time delta between signal and proxy hit
- Stores latency samples for metrics endpoint
The median latency between suppression signal and Gmail Image Proxy hit is typically 200-500ms.
Configuration options
Server environment variables| Variable | Default | Description |
|---|---|---|
PORT | 8080 | Server listen port |
DASHBOARD_TOKEN | (required) | Authentication token for dashboard API |
DB_PATH | server/data/tracker.db | SQLite database file path |
DEDUP_WINDOW_MS | 30000 | Deduplication time window (30 seconds) |
- Tracker Base URL - Your server URL (e.g.,
https://tracker.example.com) - Dashboard Token - Must match server’s
DASHBOARD_TOKEN
Data flow summary
Next steps
Quickstart guide
Set up the tracker locally and send your first tracked email
API reference
Explore all endpoints and authentication methods