Install ew & send events.
One async script, no cookies, no build step. This page covers the snippet,
the automatic events, the window.ew API, and the raw
/api/v1/collect HTTP contract if you want to send events yourself.
Drop one tag in <head>
Add the snippet to every page you want to measure — ideally in
<head> so early errors and Web Vitals are captured.
Replace YOUR_SITE_KEY with the key shown on your site's
Install tab.
<script async defer
src="https://ew.decipher-ip.com/static/tracker.js"
data-site="YOUR_SITE_KEY"></script>
That's it. The tracker is ~3 KB, sets no cookies, and starts recording the first pageview as soon as it loads. Your dashboard's install step flips to verified the moment the first event lands.
Configure via data-*
All configuration lives on the <script> tag — there is nothing to initialize in JS.
| Attribute | Required | Description |
|---|---|---|
data-site |
yes | Your site key. Without it the tracker logs a warning and does nothing. |
data-endpoint |
no | Override the collect URL. Defaults to https://ew.decipher-ip.com/api/v1/collect, derived from the script src. |
data-ew-release |
no | Build / release tag (e.g. 1.4.2), attached to every error. Lets the dashboard track which release introduced — or fixed — an issue, and detect regressions. |
<script async defer
src="https://ew.decipher-ip.com/static/tracker.js"
data-site="YOUR_SITE_KEY"
data-ew-release="1.4.2"></script>
What you get for free
Once the snippet loads, ew tracks the following with no extra code:
- Pageviews — on load, and on SPA navigation (it hooks
history.pushState/replaceStateandpopstate). Duplicate paths are de-duplicated. - Engagement — a once-per-second heartbeat separates active time (tab visible) from idle, so reported durations mean real attention.
- Web Vitals — TTFB, FCP, LCP, CLS and INP, each rated good / needs-improvement / poor.
- Errors — uncaught exceptions and unhandled promise rejections, with stack traces and breadcrumbs.
- Clicks — only on elements you opt in with
data-ew(see below).
sendBeacon — so almost
nothing is lost when a visitor leaves. Errors flush near-immediately.
Track clicks with data-ew
The tracker does not record every click. Add data-ew="<name>"
to any element you care about; the value becomes the event name. Any
extra data-ew-* attributes ride along as properties.
<button data-ew="signup_click"
data-ew-plan="pro"
data-ew-placement="hero">
Start free trial
</button>
A click on that button sends a click event named
signup_click with props { plan: "pro", placement: "hero" }.
Clicks bubble, so the attribute can sit on a wrapper too.
ew.track(name, props)
For anything not tied to a click — a form submit, a purchase, a feature
used — call ew.track. The name is capped at 120 chars;
props is any JSON-serialisable object.
window.ew.track("signup", { plan: "pro", trial: true });
window.ew.track("purchase", { value: 49, currency: "USD" });
These power goals and funnels: define a goal against a custom event name in the dashboard and conversions start counting.
async, so guard
early calls — window.ew && window.ew.track(…) — or call
from user interactions, by which point ew is defined.
Capture handled exceptions
Uncaught errors and rejections are captured automatically. For errors
you catch yourself, report them with ew.captureError so they
still reach your grouped issues.
try {
await checkout(cart);
} catch (err) {
window.ew.captureError(err, { step: "checkout", cartSize: cart.length });
showFallbackUI();
}
Errors carry the last 20 breadcrumbs (navigation, clicks,
console.error calls) and, if set, the
data-ew-release tag. Identical errors are throttled
client-side (max 5 per fingerprint per minute) so one broken loop can't
flood your dashboard. Server-side they're grouped by fingerprint into a
single issue.
window.ew
The global is available once the script has run.
| Method | Description |
|---|---|
ew.track(name, props) | Send a custom event. |
ew.pageview() | Manually record a pageview for the current path (de-duplicated against the last one). Rarely needed — SPA navigation is auto-tracked. |
ew.captureError(err, extra) | Report a caught error with optional extra props. |
ew.flush() | Force the queued batch to be sent now instead of waiting for the next timer / threshold. |
POST /api/v1/collect
The tracker is just a client for this endpoint — you can post events from a server, a native app, or anywhere else. It accepts a single JSON object with a batch of events.
Request
- POST
https://ew.decipher-ip.com/api/v1/collect - Body: JSON. May be gzipped with
Content-Encoding: gzip(or content typeapplication/x-ew-batch+gzip). - No auth header — the
k(site key) in the body identifies the site. Requests must come from an allowed origin for that site. - Limits: ≤ 64 KB compressed body, ≤ 100 events per batch.
{
"k": "YOUR_SITE_KEY",
"cid": "b1d4…", // stable client id (uuid) you generate & persist
"sent_at": 1733650000000,
"events": [
{
"kind": "pageview",
"sid": "9f2c…", // session id (uuid), stable per visit
"ts": 1733649999000, // event time, epoch ms
"url": "https://example.com/pricing",
"ref": "https://google.com/",
"lang": "en-US",
"sw": 1920, "sh": 1080
},
{
"kind": "custom",
"sid": "9f2c…",
"ts": 1733650000000,
"url": "https://example.com/pricing",
"name": "signup",
"props": { "plan": "pro" }
}
]
}
events array) is also accepted: put
k, cid and the event fields at the top level.
Response
| Status | Meaning |
|---|---|
| 204 | Accepted (also returned for OPTIONS preflight, DNT: 1, and even on internal ingest errors so the client never retries-storms). |
| 400 | Invalid JSON, or missing k / cid. |
| 403 | Origin not allowed for this site. |
| 404 | Unknown or inactive site key. |
| 413 | Body too large or bad encoding. |
| 429 | Rate limited (per client IP). |
The kind field
Every event has a kind. These are the values the ingest pipeline understands:
| kind | Notable props | Sent by |
|---|---|---|
pageview | — | auto (load + SPA nav) |
click | name, props | auto (data-ew elements) |
custom | name, props | ew.track() |
error | name, message, stack, breadcrumbs, release | auto + ew.captureError() |
vital | name (LCP…), value, rating, path | auto |
heartbeat | active_ms, idle_ms, path | auto (every 10s) |
engagement | duration_ms, path | auto (on page leave) |
Cookieless by design
- No cookies are set. The client id lives in
localStorage, the session id insessionStorage— both first-party, with in-memory fallbacks when storage is blocked. - No third-party scripts are loaded; events go only to your collect endpoint.
- Visitors sending
DNT: 1(Do Not Track) are dropped at the endpoint with a204— nothing is stored.