Client reference

The client is a single script with no dependencies and no build step. It captures a defined set of interactions automatically and exposes one function for everything else.

Current version: 2.0.0.

Installation

<script src="https://www.clientlog.bayis.co.uk/clientlog.js"></script>
<script>
  const logger = clientlog.createLogger({
    endpoint: "https://api.dev.clientlog.bayis.co.uk/v1/event",
    project: "myapp/home"
  });
</script>

The script attaches a global named clientlog.

Subresource integrity

A hash is published alongside each release so the browser can verify the file has not been altered:

<script
  src="https://www.clientlog.bayis.co.uk/clientlog.js"
  integrity="sha384-BASE64HASH"
  crossorigin="anonymous"></script>

The current value is at /clientlog.integrity.txt. Pin it, and update it when you update the client.

Options

const logger = clientlog.createLogger({
  endpoint: "https://api.dev.clientlog.bayis.co.uk/v1/event",
  project: "myapp/home",
  options: {
    session: true,
    scroll: true,
    clicks: true,
    navigation: true,
    dwell: true,

    scrollMilestones: [25, 50, 75, 90, 100],
    clickSelector: "a, button, [data-clientlog]",
    minDwellMs: 250,
    includeVersion: true,
    respectDoNotTrack: false,

    sessionIdFn: null,
    pageNameFn: null,

    debug: false,
    dryRun: false
  }
});

Capture

Option Default Description
session true Generate and attach a session identifier.
scroll true Emit scroll at reading-depth milestones.
clicks true Emit click for elements matching clickSelector.
navigation true Emit page views on history changes as well as load.
dwell true Emit page_exit with visible time and reading depth.

Behaviour

Option Default Description
scrollMilestones [25, 50, 75, 90, 100] Depth percentages that emit an event.
clickSelector a, button, [data-clientlog] Which elements produce click events.
minDwellMs 250 Below this, no page_exit is emitted.
includeVersion true Add _v with the client version to every payload.
respectDoNotTrack false Disable all logging when the browser sets DNT.

Overrides

Option Default Description
sessionIdFn null Function returning a session identifier.
pageNameFn null Function mapping a pathname before it becomes an action name.

Development

Option Default Description
debug false Log every outgoing event to the console.
dryRun false Capture and log without sending.

debug and dryRun together let you inspect the full event stream during development without writing anything.

Automatic events

page_view.<name>

On load, and on every history change when navigation is enabled.

{
  "action": "page_view.pricing",
  "payload": {
    "path": "/pricing.html",
    "query": "?ref=nav",
    "full_path": "/pricing.html?ref=nav",
    "referrer": "https://news.ycombinator.com/"
  }
}

The name derives from the path: extension stripped, slashes and punctuation replaced with underscores, lowercased. / becomes home.

/                    page_view.home
/pricing.html        page_view.pricing
/docs/rules.html     page_view.docs_rules
/blog/why-we-built   page_view.blog_why-we-built

Page identity in the action name is what makes the transition matrix, the landing page breakdown and the exit report meaningful. It also means rules can match a page without loading the session’s events, which is faster and cheaper.

page_exit.<name>

When the page is hidden or unloaded, if dwell is enabled and visible time exceeds minDwellMs.

{
  "action": "page_exit.pricing",
  "payload": {
    "path": "/pricing.html",
    "dwell_ms": 47200,
    "max_scroll": 82
  }
}

dwell_ms counts visible time only. A backgrounded tab does not accumulate. A page hidden and returned to emits more than one page_exit; sum dwell_ms across them for total attention.

max_scroll is the deepest position reached, as a percentage, whether or not it crossed a milestone.

These two fields are what makes “how long did they spend on the pricing page, and did they read it” answerable.

scroll

At each milestone, once per page.

{ "action": "scroll", "payload": { "percent": 75, "path": "/pricing.html" } }

Milestones fire at the first position at or past the threshold, so a single fast gesture from 20 to 90 emits 25, 50 and 75 as well as 90. Pages shorter than the viewport emit no scroll events at all.

click

For elements matching clickSelector.

{
  "action": "click",
  "payload": {
    "tag": "A",
    "text": "See pricing",
    "href": "https://example.com/pricing.html",
    "id": "nav-pricing",
    "label": "nav-pricing-cta"
  }
}

text is trimmed to 100 characters. href, id and label appear only when present. label comes from a data-clientlog attribute:

<a href="/pricing.html" data-clientlog="nav-pricing-cta">See pricing</a>

That attribute is a stable identifier you control. It survives copy changes that would break a rule matching on text, and it survives the class renames that break CSS selectors.

Custom events

logger.log(action, payload);
logger.log("user.payment.success", { plan: "standard", currency: "GBP" });
logger.log("story.viewer.load.complete", { duration_ms: 412 });
logger.log("filter.applied", { field: "category", value: "electronics" });

action is a string you choose. payload is any JSON-serialisable object, capped at 512 characters after serialisation. Every payload also carries path and, unless disabled, _v.

Naming conventions are in events.

Declarative binding

For click tracking on named elements without writing handlers:

clientlog.bindEvents({
  "#signup-button": "user.signup.click",
  ".cta-primary": { event: "cta.click", payload: { position: "hero" } },
  "#search-form": { type: "submit", event: "search.submit" }
}, logger);

Every element matching each selector is bound. A string value means a click event with that action name; an object accepts type, event and payload.

Page naming

Paths carrying identifiers produce one action name per identifier, which fills the transition graph with single-visit nodes and makes it unreadable.

options: {
  pageNameFn: (path) => path
    .replace(/^\/products\/[^/]+$/, "/products/:id")
    .replace(/^\/users\/[^/]+\/posts\/[^/]+$/, "/users/:id/posts/:id")
}

Which yields page_view.products_:id for every product page, with the specific path still available in the payload.

Apply this to any route with a numeric or slug segment. It is the single most useful piece of configuration on a content or commerce site.

Session identity

By default the identifier is opaque, generated in the browser, held in sessionStorage, and scoped to one tab on one site. It correlates with nothing: not with a previous visit, not with another site, not with a person. Closing the tab ends it.

1782693098872-7xzwuxbzd

sessionIdFn replaces the generator:

options: {
  sessionIdFn: () => "user-" + currentUser.id
}

This groups every session by the same authenticated user, which makes returning behaviour and multi-visit journeys measurable.

It also introduces identity linkage. Doing so makes you responsible for the lawful basis under UK GDPR, the consent notice your users see, and the retention and erasure policy that follows. Clientlog provides the mechanism; the obligation is the site owner’s. See privacy.

Storage is unavailable in some private browsing modes and sandboxed frames. In those cases the session identifier is null and events are recorded without one.

Single-page applications

With navigation enabled the client hooks pushState, replaceState and popstate, so route changes emit page_view and page_exit pairs without further work in React, Vue, Svelte or any router built on the History API.

Two things worth knowing:

  • Create the logger once, at application start. The history patch is applied once per document, so a second logger will not duplicate page views, but it will duplicate scroll and click listeners.
  • Only pathname changes emit a page view. A route change that alters only the query string does not. Call logger.log explicitly if that transition matters.

Do not track

options: { respectDoNotTrack: true }

When the browser sets DNT, createLogger returns a logger whose log does nothing and whose disabled property is true. No requests are made and no session identifier is created.

Off by default, because the default configuration collects no personal data and DNT signals in current browsers are inconsistent. Enable it if your privacy policy commits to honouring the header.

Content Security Policy

connect-src 'self' https://api.dev.clientlog.bayis.co.uk;
script-src  'self' https://www.clientlog.bayis.co.uk;

Ad blockers

The client and the API are served from the same infrastructure, and neither host appears on the filter lists that block advertising and analytics trackers. Blocking rates are consequently far lower than for Google Analytics or the Facebook pixel.

Serving the client from your own domain removes the question entirely. Host clientlog.js yourself and point endpoint at a CNAME under your domain. Contact us to have this configured.