Skip to content
  1. Home
  2. Help center
  3. For developers
  4. Custom events from a regular website

Custom events from a regular website

Last updated:

By the end of this article you will be sending KO the things people do on a site where every page loads afresh — plain HTML, a landing page, WordPress, a site builder. No build step is needed: it all happens in one block in the page template. The counter has to be on your pages already: connecting a website.

If your site is produced by a generator — Astro, Hugo, Next, Eleventy — go to the next article instead: there the context values are substituted at build time. If your app lives in one tab and swaps screens without reloading, go to the SPA article.

What the counter already does

Page views, visits, sources, campaigns, device and visitor are recorded by the counter itself. On a multi-page site that comes for free: every navigation is a fresh load, and the view is sent without your involvement.

Your own code sends only what the counter cannot see: a button was clicked, a calculator was opened, a plan was chosen, a form was submitted. Such an event lands on the visit and brings the source with it — which turns “how many clicks” into “how many clicks from paid traffic”.

Three things follow from the reloads, and they will not be repeated below:

  • the wrapper is declared in the page template, which means on every page again;
  • shared event context (language, section) is also set again on every page;
  • an event fired while leaving the page ships unreliably from here — the fact is safer to catch on the next page.

Add the wrapper

The block below goes into the page template before the KO counter — usually in <head>. It works on its own: if the counter has not loaded yet, events wait in its queue.

<script>
(function () {
  var PROD_HOST = 'example.com';                     // your live domain
  var onProd = location.hostname === PROD_HOST || location.hostname === 'www.' + PROD_HOST;

  function call(method, args) {
    if (!onProd) return false;
    var visit = window.ko && window.ko.visit;
    if (visit && typeof visit[method] === 'function') {
      visit[method].apply(visit, args);
      return true;
    }
    window.koLayer = window.koLayer || [];
    window.koLayer.push(['visit', method, args]);
    return true;
  }

  window.siteTrack = function (event, data) {
    return call('track', [String(event || ''), { data: data || {} }]);
  };

  window.siteTrackData = function (data) {
    return call('setTrackData', [data || {}, true]);
  };
})();
</script>

Put your own domain in PROD_HOST — nothing else in this block needs changing. The call shape, how the koLayer queue works and the second argument of setTrackData are covered in counter calls.

The domain gate is mandatory. Without it the project fills up with “sites” like localhost and test.example.com, and half your numbers are your own work on the site.

No template? Site builders

Tilda, WordPress, Creatium and the like have a “code in <head>” field, or a site-wide code injection — the block goes there, once for the site. Make sure the setting applies to all pages: landing pages living outside the shared template are the most common reason events do not come from everywhere.

After pasting it, publish the site again: until you do, the builder keeps serving the previous HTML.

Send your first event

Take an action that actually happened and attach a call to it:

<button id="buy">Check out</button>
<script>
  document.getElementById('buy').addEventListener('click', function () {
    siteTrack('checkout_started', { plan: 'pro', place: 'pricing_table' });
  });
</script>

An event name is Latin letters, lowercase, past tense; what tells cases apart goes into the data, not into the name. The full rules, together with repeat protection and normalising values taken from the address, are in the event dictionary.

Fields that are the same for the whole page belong in the context, so you do not repeat them in every call:

<script>
  siteTrackData({ page_kind: 'pricing', locale: 'en' });
</script>

Write the values straight into this block on each page (in a builder, into the page’s code rather than the site’s). Computing them in the browser from the address is worth it only when there are many pages and the parsing is simple: your site already describes its address scheme in its own structure, and a second copy of it in JS drifts silently — in a report that looks like a page attributed to the wrong section.

Put “the person left” on the receiving page. Following a link here is a departure, and the event only ships if the counter is already loaded. The reliable anchor is not “clicked checkout” but “opened the checkout page”; the gap between those two numbers shows itself in the report.

Check that the event arrives

Open a live page and view its source (Ctrl+U): the wrapper block must be in the HTML before the counter. If it is not there, the site was not published again.

Then open the Network tab and do the action you attached the call to. The sign of success is a visit.php?type=event request; its body carries your event name and the fields inside data. If there is no request, add ?ko_debug to the address and repeat: the counter starts logging what happens to it.

Turn the event into a metric

  1. In the project’s Metrics section create a metric: your own key (say siteCheckoutStarted) and the formula {sites_events:visitor:checkout_started} — your event name in the third part.
  2. Rates and conversions are formulas over metric keys: {siteCheckouts}/{sitePricingViews}.
  3. People rather than clicks: {sites_events:visitor:checkout_started[:value=visitors_id][:aggregate=uniq]}.
  4. A breakdown by event data is the field:data.plan dimension in your report’s Breakdown section. You do not need a metric per value.
  5. If the event carries money, pass the amount as income next to data, not inside it — that is what makes it add up as money.

If it doesn’t add up

Events come from some pages only. The wrapper is in the template while some pages — standalone landings, entry pages — live outside it.

No events at all. The site was not published again after the code was pasted; or the page was opened somewhere other than the live domain, in which case the silence is correct; or a blocker in the browser is stopping the counter — check in a clean profile.

Fewer events than actions. Most likely events fired while leaving the page. Move the anchor to the receiving page.

More events than actions. The page reloads itself and there is no key-based protection — see the event dictionary.

One empty row in the breakdown. The data was passed around the wrapper, as a flat object. The working shape is { data: { … } }.