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

AttributeDescription
data-siteSite identifier. Defaults to "default" if omitted.
data-cookieEnable returning-visitor cookie. With ECHELON_COOKIE_CONSENT=true, shows consent banner first.
data-no-clicksDisable click tracking on elements with data-echelon-click attribute. (On by default.)
data-no-scrollDisable scroll depth milestones (25%, 50%, 75%, 90%, 100%). (On by default.)
data-no-hoverDisable 1-second hover tracking on elements with data-echelon-hover attribute. (On by default.)
data-no-outboundDisable outbound link click tracking. (On by default.)
data-no-downloadsDisable file download link tracking (pdf, zip, dmg, mp3, mp4, etc.). (On by default.)
data-no-formsDisable form tracking (field focus, field edits, submissions). (On by default.)
data-no-vitalsDisable 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.

EventTriggerData
form_focusUser focuses an input, select, or textareaTag name, input type, field name, form ID/name, page path
form_blurUser 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_submitForm is submittedForm 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:

EventTrigger
pageviewVia /b.gif beacon after first user interaction (pointer, scroll, click, keydown) or on visibility change after 4 seconds. Minimum 800ms interaction delay.
bounceNo interaction within 120 seconds, or tab hidden without prior interaction.
session_endTab becomes hidden.
session_resumeTab becomes visible again after session_end.
form_focusUser focuses a form field (input, select, textarea).
form_blurUser edits a form field (change event on blur).
form_submitForm 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

ParameterPurposeExample
utm_campaignCampaign name / IDspring-2026
utm_sourceTraffic sourcenewsletter, google
utm_mediumMarketing mediumemail, cpc, social
utm_contentAd creative / content varianthero-banner, sidebar-link
utm_termSearch keyword / termanalytics+tool

How It Works

  1. Visitor arrives with UTM parameters in the URL:
    https://example.com/pricing?utm_campaign=spring-2026&utm_source=newsletter&utm_medium=email
  2. Tracker extracts all five UTM params and stores them in sessionStorage (key: _eutm).
  3. 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.
  4. SPA navigation: On route changes (pushState/replaceState), the tracker re-reads URL params. If the new URL has a utm_campaign, it updates the session. Otherwise, the stored values persist.
  5. Server validates: The beacon endpoint checks utm_campaign against 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:

API

POST /api/campaigns
Authorization: Bearer <token>

{
  "id": "spring-2026",
  "name": "Spring Launch 2026",
  "utm_campaign": "spring-2026",
  "site_id": "default"
}

Campaign Lifecycle

StatusBehavior
activeUTM data is recorded. Appears in stats.
pausedNew UTM data is silently dropped. Existing data remains in stats.
archivedSame 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:

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

  1. Create an experiment with variants and a conversion metric (event type to measure)
  2. Your application assigns visitors to variants and sends events with experiment attribution
  3. Echelon counts unique sessions per variant (impressions) and sessions that triggered the metric event (conversions)
  4. 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:

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:

Experiment Lifecycle

StatusBehavior
draftCreated but not yet active. Events are still recorded if sent.
activeRunning. Sets started_at timestamp on first activation.
pausedTemporarily stopped. Events still recorded if sent.
completedFinished. Sets ended_at timestamp. Results frozen.
archivedSame 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:

MetricDescription
ImpressionsUnique sessions enrolled in this variant
ConversionsUnique sessions that triggered the metric event
Conversion Rateconversions / impressions
Relative Uplift(variant_rate - control_rate) / control_rate โ€” only shown for non-control variants
SignificanceStatistical significance level (see below)

Statistical Significance

Echelon uses a two-proportion z-test to determine if differences between variants are statistically significant:

  1. Calculate pooled proportion: p = (conversions_control + conversions_variant) / (impressions_control + impressions_variant)
  2. Calculate standard error: SE = √(p × (1-p) × (1/n1 + 1/n2))
  3. Calculate z-score: z = |rate_variant - rate_control| / SE
Z-ScoreSignificance
≥ 2.57699% significant (p < 0.01)
≥ 1.96095% significant (p < 0.05)
≥ 1.64590% significant (p < 0.10)
< 1.645Not 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.

  1. Create experiment button-color with metric purchase and two variants: control (blue, weight 50) and green (green, weight 50).
  2. In your frontend, assign visitors to variants based on a hash of their session ID. Show the appropriate button color.
  3. When the page loads, send: echelon.track("page_view", { experiment_id: "button-color", variant_id: "green" })
  4. When the user clicks Buy, send: echelon.track("purchase", { experiment_id: "button-color", variant_id: "green" })
  5. 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:

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.


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:


Installation API Reference Bot Defense Configuration Architecture Data Ownership & Open Access Portable Data MCP Server Telemetry Why ea.js?