Skip to content
  1. Home
  2. Help center
  3. For developers
  4. Counter calls — track, setTrackData, koLayer, ko_options

Counter calls — track, setTrackData, koLayer, ko_options

Last updated:

A reference for what the KO counter puts on window. How to add the wrapper and send your first event is in Custom events from a single-page app and Custom events from a regular website; how to name an event is in The event dictionary.

This describes the counter as it works today. It is not a contract promising compatibility.

Two calls

Everything custom events need is two methods on the ko.visit module:

ko.visit.track(event_name, envelope)     // send an event
ko.visit.setTrackData(data, merge)       // set the context for following events

ko.visit exists only once the counter has loaded. Before that — the koLayer queue.

track(event_name, envelope)

event_name is a string. envelope is the envelope, not the event’s data:

ko.visit.track('report_exported', {
  data:   { rows: 42, format: 'xlsx' },   // event fields go here and nowhere else
  income: 0,                              // money, optional
  cost:   0,                              // spend, optional
});

Envelope fields:

FieldTypeWhat it does
dataobjectThe event’s fields. A report breakdown uses the field:data.<key> dimension
incomenumberAdds up as money. Sits next to data, not inside it
costnumberThe same, for spend
entitystringvisitor by default — the second part of a metric formula. No reason to change it

The common mistake is a flat object:

ko.visit.track('report_exported', { rows: 42 });   // ← rows lands in the service part

The event is recorded, but rows is not among its data: a breakdown by that field gives one empty row.

An amount inside data does not become money — it adds up as an ordinary number. Money is income and cost on the envelope, nothing else.

setTrackData(data, merge)

Mixes fields into every following event on this page.

ko.visit.setTrackData({ user_id: 42, plan: 'pro' }, true);   // extend
ko.visit.setTrackData({ user_id: 42 });                      // replace the whole context

merge defaults to false. A call without the second argument replaces the context entirely rather than adding to it — a second setTrackData wipes what the first one set. If you set context in layers as facts become known, the second argument is not optional.

On a key collision the context wins, not the event. Context fields are applied on top of the event’s data, so a plan in the context overwrites the plan you passed to track. A given key lives either in the context or in the event, never in both.

The context lives in page memory. A reload clears it.

The koLayer queue

The counter loads asynchronously and the first event often happens before it. Until it loads, calls queue up in window.koLayer.

window.koLayer = window.koLayer || [];
window.koLayer.push(['visit', 'track', ['report_exported', { data: { rows: 42 } }]]);

A queue item is one array of three parts: module name, method name, array of arguments. Four separate arguments to push will not be executed.

A function is also accepted, and runs in the counter’s context:

window.koLayer.push(function () { /* the counter is here by now */ });

On initialisation the counter replaces push with its own implementation, so a write after loading executes immediately instead of waiting to be drained. The “counter already loaded → call it directly” branch in the wrapper is still worth having: it gives you a return value.

There is a second queue inside the counter that needs nothing from you: an event fired before the visit has an identifier waits there and ships once the visit opens.

Counter options — ko_options

The ko_options object is declared before the counter loads; afterwards it is not read.

var ko_options = {
  modules: {
    visit: {
      url_tracking_hash_router: false,
      url_tracking_query_patterns: ['*', '!utm_*', '!_*'],
      url_tracking_debounce_ms: 100,
    },
  },
};
OptionDefaultWhat it does
url_tracking_hash_routerfalsetrue if the app routes through #/
url_tracking_query_patterns['*', '!utm_*', '!_*']Which query parameters are part of the page address. * allows everything, ! excludes by mask
url_tracking_debounce_ms100Delay before an address change is processed

The parameter filter is what stops one page multiplying into a dozen report rows because of ?tab=2&_r=17. By default utm_* and anything starting with _ are already excluded.

Debugging

WhatHow
Turn debugging on?ko_debug in the page address
Silence the counter?ko_off in the page address
Confirm an event shippedNetwork tab, the visit.php?type=event request; its body is the whole envelope

The counter also stays silent without a key: an empty hash in the install snippet switches it off by itself.

Limits

  • There is no deduplication. One call, one record. A screen that re-renders itself fires on every pass; the “already counted” key is yours to keep.
  • An event lives on a visit. An action that did not happen in a browser — a payment confirmed server-side, a nightly recalculation — has no visit and cannot be a site event.
  • Context does not survive a reload: it lives in page memory.
  • An event fired while leaving the page may not make it if the counter has not loaded. The reliable anchor is a fact on the receiving screen.
  • The counter records page views itself, SPA routes included. Your own router subscription doubles them.