Features
Tracking features, script tag options, JS API, live dashboard, and SPA support.
Script Tag
Basic embed โ add to any HTML page:
<script async src="https://your-host/ea.js" data-site="my-site"></script>
Script Attributes
| Attribute | Description |
|---|---|
data-site | Site identifier. Defaults to "default" if omitted. |
data-cookie | Enable returning-visitor cookie. With ECHELON_COOKIE_CONSENT=true, shows consent banner first. |
data-no-clicks | Disable click tracking on elements with data-echelon-click attribute. (On by default.) |
data-no-scroll | Disable scroll depth milestones (25%, 50%, 75%, 90%, 100%). (On by default.) |
data-no-hover | Disable 1-second hover tracking on elements with data-echelon-hover attribute. (On by default.) |
data-no-outbound | Disable outbound link click tracking. (On by default.) |
data-no-downloads | Disable file download link tracking (pdf, zip, dmg, mp3, mp4, etc.). (On by default.) |
data-no-forms | Disable form tracking (field focus, field edits, submissions). (On by default.) |
data-no-vitals | Disable Core Web Vitals: LCP, CLS, INP. (On by default.) |
All behavioral tracking is enabled by default. Use data-no-* attributes to selectively disable features.
Selective Opt-Out
<script async src="https://your-host/ea.js" data-site="my-site" data-no-hover data-no-vitals></script>
HTML Element Markup
Annotate elements to track specific interactions:
Click Tracking
Enabled by default. Disable with data-no-clicks on the script tag.
<button data-echelon-click="signup-cta">Sign Up</button>
Hover Tracking
Enabled by default. Fires after 1 second of hover. Disable with data-no-hover on the script tag.
<div data-echelon-hover="pricing-card">...</div>
Form Tracking
Enabled by default. Tracks three levels of form interaction. Disable with data-no-forms on the script tag.
| Event | Trigger | Data |
|---|---|---|
form_focus | User focuses an input, select, or textarea | Tag name, input type, field name, form ID/name, page path |
form_blur | User edits a field (fires on change/blur) | Same as focus + value length. Value included for non-sensitive fields (passwords and hidden fields excluded). On anonymized sites, values are scrambled client-side. |
form_submit | Form is submitted | Form method, ID, name, optional data-echelon-form label, page path |
Optionally label forms for easier identification in the dashboard:
<form data-echelon-form="contact-form">...</form>
Custom Data Attributes
Any data-echelon-* attribute on annotated elements is included in event data (max 8 keys, max 256 chars per value).
<button data-echelon-click="buy" data-echelon-product="widget" data-echelon-price="9.99">Buy Now</button>
JavaScript API
Send custom events programmatically:
window.echelon.track("event_name", {
key: "value",
another: "prop"
});
Limits: max 16 properties, key max 64 chars, value max 512 chars.
Auto-Tracked Events
These fire automatically โ no configuration needed:
| Event | Trigger |
|---|---|
pageview | Via /b.gif beacon after first user interaction (pointer, scroll, click, keydown) or on visibility change after 4 seconds. Minimum 800ms interaction delay. |
bounce | No interaction within 120 seconds, or tab hidden without prior interaction. |
session_end | Tab becomes hidden. |
session_resume | Tab becomes visible again after session_end. |
form_focus | User focuses a form field (input, select, textarea). |
form_blur | User edits a form field (change event on blur). |
form_submit | Form submission. |
SPA Support
The tracker automatically patches pushState, replaceState, and listens for popstate events. When the route changes, a new pageview beacon fires automatically. No configuration needed for React Router, Vue Router, SvelteKit, Fresh, or any other SPA framework.
๐งญ "Did you know the tracker automatically detects route changes in React, Vue, Svelte, and Fresh without any configuration? Just add the script tag and pageviews fire on every navigation." -๐ฆญ
UTM Campaign Tracking
Echelon tracks all five standard UTM parameters. Attribution is session-scoped and persisted across page loads.
Supported Parameters
| Parameter | Purpose | Example |
|---|---|---|
utm_campaign | Campaign name / ID | spring-2026 |
utm_source | Traffic source | newsletter, google |
utm_medium | Marketing medium | email, cpc, social |
utm_content | Ad creative / content variant | hero-banner, sidebar-link |
utm_term | Search keyword / term | analytics+tool |
How It Works
- Visitor arrives with UTM parameters in the URL:
https://example.com/pricing?utm_campaign=spring-2026&utm_source=newsletter&utm_medium=email - Tracker extracts all five UTM params and stores them in
sessionStorage(key:_eutm). - Every pageview beacon (
/b.gif) includes the UTM parameters for the rest of the session โ even when the visitor navigates to pages without UTM in the URL. - SPA navigation: On route changes (
pushState/replaceState), the tracker re-reads URL params. If the new URL has autm_campaign, it updates the session. Otherwise, the stored values persist. - Server validates: The beacon endpoint checks
utm_campaignagainst a whitelist of active campaigns (refreshed every 60 seconds from the database). Unknown campaigns are silently dropped โ only registered, active campaigns record UTM data.
Setting Up Campaigns
Campaigns must be registered before they appear in stats. Create them via the admin dashboard or the API:
Admin Dashboard
Navigate to /admin/campaigns and fill in the form:
- ID โ URL-safe slug (alphanumeric,
.,_,-), e.g.spring-2026 - Name โ Human-readable label, e.g.
Spring Launch 2026 - UTM Campaign โ Exact value that will appear in URLs, e.g.
spring-2026 - Site ID โ Scope to a specific site (defaults to
default)
API
POST /api/campaigns
Authorization: Bearer <token>
{
"id": "spring-2026",
"name": "Spring Launch 2026",
"utm_campaign": "spring-2026",
"site_id": "default"
}
Campaign Lifecycle
| Status | Behavior |
|---|---|
active | UTM data is recorded. Appears in stats. |
paused | New UTM data is silently dropped. Existing data remains in stats. |
archived | Same as paused. Hidden from the default campaign list in the admin UI. |
Change status via PATCH /api/campaigns/:id with { "status": "paused" }, or use the buttons on the campaign detail page.
Campaign Stats
The campaign detail page (and GET /api/stats/campaigns?id=X) shows:
- Summary โ Total views and unique visitors for the campaign
- By Source โ Breakdown by
utm_source(views + visitors) - By Medium โ Breakdown by
utm_medium - By Content โ Breakdown by
utm_content - By Term โ Breakdown by
utm_term - Daily Trend โ Views per day over the selected period
- Top Landing Pages โ Pages visitors entered through
All stats exclude bot traffic (bot_score ≥ 50). Default lookback is 30 days (max 90).
UTM URL Builder
Build tagged URLs by appending parameters to your landing page. For example, a newsletter link promoting a spring sale:
https://example.com/sale?utm_campaign=spring-2026&utm_source=newsletter&utm_medium=email&utm_content=hero-cta
All five parameters are optional except utm_campaign, which must match an active campaign.
๐ "Tip: The campaign whitelist prevents spam traffic from polluting your stats. If a crawler hits your URL with a random utm_campaign, it gets quietly dropped because there's no matching active campaign." -๐ฆญ
A/B Experiments
Run split tests to measure the impact of changes. Echelon tracks impressions, conversions, and calculates statistical significance automatically.
How It Works
- Create an experiment with variants and a conversion metric (event type to measure)
- Your application assigns visitors to variants and sends events with experiment attribution
- Echelon counts unique sessions per variant (impressions) and sessions that triggered the metric event (conversions)
- Statistical significance is calculated automatically using a two-proportion z-test
Creating an Experiment
Admin Dashboard
Navigate to /admin/experiments and fill in the form:
- Experiment ID โ URL-safe slug, e.g.
checkout-v2 - Name โ Human-readable label, e.g.
Checkout Redesign - Description โ Optional context
- Metric Event Type โ The event type that counts as a conversion, e.g.
purchase,signup,click - Allocation % โ Percentage of traffic enrolled in the experiment (1โ100, default 100)
- Variants โ At least 2, up to 20. Each has an ID, name, weight, and a control flag
API
POST /api/experiments
Authorization: Bearer <token>
{
"experiment_id": "checkout-v2",
"name": "Checkout Redesign",
"description": "Test new checkout flow vs original",
"metric_event_type": "purchase",
"allocation_percent": 100,
"variants": [
{ "variant_id": "control", "name": "Original", "weight": 50, "is_control": true },
{ "variant_id": "redesign", "name": "New Flow", "weight": 50 }
]
}
Variant Weights
Weights are relative, not percentages. Two variants with weights 50/50 each get 50% of traffic. Weights of 1/1 or 100/100 produce the same split. Three variants with weights 2/1/1 get 50%/25%/25%.
Your application is responsible for reading the weights and assigning visitors to variants (e.g. using a hash of the visitor ID modulo total weight). Echelon stores the weights but does not perform assignment โ this keeps the system flexible for any frontend framework.
Sending Experiment Events
Include experiment_id and variant_id in your event data. Use the JavaScript API:
// Track an impression (user saw the variant)
window.echelon.track("page_view", {
experiment_id: "checkout-v2",
variant_id: "redesign"
});
// Track a conversion (user completed the goal)
window.echelon.track("purchase", {
experiment_id: "checkout-v2",
variant_id: "redesign"
});
Both fields are stored in the semantic_events table. Echelon counts:
- Impressions โ Unique sessions that sent any event for the experiment+variant
- Conversions โ Unique sessions that sent the
metric_event_typeevent for the experiment+variant
Experiment Lifecycle
| Status | Behavior |
|---|---|
draft | Created but not yet active. Events are still recorded if sent. |
active | Running. Sets started_at timestamp on first activation. |
paused | Temporarily stopped. Events still recorded if sent. |
completed | Finished. Sets ended_at timestamp. Results frozen. |
archived | Same as completed. Hidden from default views. |
Change status via PATCH /api/experiments/:id with { "status": "active" }.
Reading Results
The experiment detail page (and GET /api/stats/experiments?experiment_id=X) shows per-variant:
| Metric | Description |
|---|---|
| Impressions | Unique sessions enrolled in this variant |
| Conversions | Unique sessions that triggered the metric event |
| Conversion Rate | conversions / impressions |
| Relative Uplift | (variant_rate - control_rate) / control_rate โ only shown for non-control variants |
| Significance | Statistical significance level (see below) |
Statistical Significance
Echelon uses a two-proportion z-test to determine if differences between variants are statistically significant:
- Calculate pooled proportion:
p = (conversions_control + conversions_variant) / (impressions_control + impressions_variant) - Calculate standard error:
SE = √(p × (1-p) × (1/n1 + 1/n2)) - Calculate z-score:
z = |rate_variant - rate_control| / SE
| Z-Score | Significance |
|---|---|
| ≥ 2.576 | 99% significant (p < 0.01) |
| ≥ 1.960 | 95% significant (p < 0.05) |
| ≥ 1.645 | 90% significant (p < 0.10) |
| < 1.645 | Not statistically significant |
Minimum data: At least 100 impressions per variant are required before significance is calculated. Below this threshold, results show "Insufficient data".
Example: Button Color Test
You want to test whether a green "Buy" button converts better than the original blue one.
- Create experiment
button-colorwith metricpurchaseand two variants:control(blue, weight 50) andgreen(green, weight 50). - In your frontend, assign visitors to variants based on a hash of their session ID. Show the appropriate button color.
- When the page loads, send:
echelon.track("page_view", { experiment_id: "button-color", variant_id: "green" }) - When the user clicks Buy, send:
echelon.track("purchase", { experiment_id: "button-color", variant_id: "green" }) - Check /admin/experiments to see conversion rates and significance as data accumulates.
๐งช "Wait for at least 95% significance before declaring a winner. Early results often flip as more data comes in." -๐ฆญ
Cookieless by Default
Without data-cookie, visitors are identified by a daily-rotating HMAC-SHA256 hash of IP + User-Agent + site ID + date. This produces a 16-character hex identifier that:
- Resets every day โ no cross-day tracking
- Cannot be reversed to recover the original IP
- Requires no cookie consent (GDPR/ePrivacy compliant)
Add data-cookie to enable a persistent visitor cookie for returning visitor tracking.
Admin Dashboard
The admin UI at /admin/ provides a full-featured analytics dashboard:
Live Dashboard
Now gauge with active visitors, 60-minute and 24-hour trend charts, recent visitors and events tables. Auto-refreshes every 10 seconds.
Visitor Detail
Full visitor history with page views, events, bot scores, devices, countries, interaction times. Exclude/include controls.
Events Listing
Browse all semantic events with type badges, filtering by event type, and search. Paginated with visitor links.
Professional Design
Government-inspired admin interface with navy and burgundy palette โ designed for visual integrity. Clean, professional look with live stats bar and persistent navigation controls.
- Persistent site selector and date range picker in navigation
- Overview with KPIs, daily trends, top pages, devices, countries, referrers
- Realtime visitor monitoring
- Visitor views listing โ filterable, sortable, paginated
- Bot management โ suspicious visitors, exclude/include controls
- A/B experiments โ create, manage, view conversion uplift
- UTM campaigns โ track and break down by source, medium, content
- Performance metrics โ CI/CD benchmark tracking with trends
- Configurable timezone display for all admin timestamps
Future Plans
No roadmap exists. This project is built for personal use. It might get worked on, it might not. If you find it interesting you're welcome to fork it, open pull requests, or contribute โ just work within the AGPL-3.0 license.
Known ideas on the horizon:
- MariaDB/PostgreSQL support if SQLite ever becomes a bottleneck. It probably won't.
- Ad system integration โ Google AdSense, Meta Ads, and other ad platform tracking may be integrated at a later date to correlate ad spend with analytics data.
- MCP write tools โ The MCP server is currently read-only by design. Write tools (campaign/experiment management) may be added in a future version.