Documentation
Coming from another tool
Reading the reports
Count what matters
Machine access
Count what a pageview cannot.
One attribute where there is an element, one function call where there is not. Properties, their values read back, and the checks that keep people out of your data.
Already counted for you
Two events need no declaring. A click on a link that leaves your site arrives as outbound, carrying the destination host and never its path or query, the same restraint the referrer already gets. A click on a file arrives as download with the file's path. Middle-clicks count too, since opening a new tab is how many people leave.
| Event | Property | Carries |
|---|---|---|
| outbound | host | The destination hostname, without www. |
| download | path | The file's path on your own site |
Everything else, a signup, a plan chosen, a quote requested, is a custom event: you pick the name, and the name becomes a row in the Events report, a dimension the API serves, and something a goal can watch.
Declare it on the element
Where the thing worth counting is a click or a submit, put data-kehai-event on the element. No JavaScript, no handler to find a place for.
<button data-kehai-event="newsletter">Subscribe</button>
<form data-kehai-event="contact" data-kehai-event-form="footer">
...
</form>Every click or submit on the element, or on anything inside it, counts under that name. Any data-kehai-event-* attribute travels along as a property: the form above sends contact with form: footer. A form counts on submit, never on click.
The listener is delegated and runs in the capture phase, which buys two things. An element rendered after the page loaded still counts, so a single page application needs nothing extra. And a handler that calls stopPropagation, which form libraries do constantly, cannot silently swallow the count.
Send it from code
Where the event has no element, an order confirmed, a step reached, a threshold crossed, call the function the snippet installs on window:
kehai('signup', { plan: 'pro' })
kehai('preorder', { sku: 'TSHIRT-M' }, { amount: 49.9, currency: 'EUR' })| Argument | What it is | |
|---|---|---|
| name | required | The event's name. Use a stable, short convention: signup, not signup-v2-footer-test. Put the detail in properties instead of minting a new name per placement. |
| properties | optional | An object of short strings, up to 24 entries, 255 characters per key or value. A property holding anything but a string drops the whole event rather than sending part of it. |
| value | optional | An amount plus an ISO 4217 currency, for an event whose worth you already know in the browser. With no currency it stays a bare number, and unlike currencies are never summed. |
The tag carries defer, so window.kehai exists only after the script has run. A call made earlier throws and sends nothing. Guard any code that can run first:
// Anywhere your code might run before the deferred script has:
window.kehai?.('signup', { plan: 'pro' })The value argument is how the simple purchase path works: a purchase event with an amount, summed per currency by an Event goal. It never enters the Revenue reports, because a browser can be made to say any number. When revenue starts steering decisions, orders travel from your server instead. Both paths, and when to switch, are in the ecommerce guide.
From Google Tag Manager
If your events already flow through a dataLayer, one Custom HTML tag bridges them: fire it on the dataLayer event you care about and pass the trigger's variables along.
<script>
window.kehai?.('signup', { plan: '{{DL - plan}}' })
</script>Quote the variable. Tag Manager substitutes its text into the tag as written, so an unquoted {{DL - plan}} becomes a bare word that throws, or a number that drops the event, and a property has to be a string. The guard matters twice in GTM: the snippet may not have loaded yet, and a blocked container should fail to nothing rather than to an error. Properties still have to be short strings, whatever the dataLayer held.
What never leaves the browser
The snippet checks every event before sending it. The name and every value are refused when they contain an email address, a phone-shaped number, or a network address: the event is dropped in the browser, nothing travels, and nothing has to be scrubbed later. A property's key is judged by what it names rather than by what it contains. A key like email, phone, user_id, or order_id names a column that holds a person and drops the event, while an event named email_signup, click_phone, or session_start names a thing that happened, identifies nobody, and arrives. The collector applies the same rules again on arrival, so an old cached script cannot sneak one past.
The check catches the shapes it can recognize. It cannot know that plan: anna-enterprise names a customer. Send categories, never people: plans, variants, sources, positions.
Where events land
| Surface | What you get |
|---|---|
| Events report | Every name, counted and trended, with a drill into its properties |
| Goals | A goal of kind Event watches one name and reports daily completions, converters, and a conversion rate |
| REST and MCP | The same numbers over the API, from the same functions the panel calls |
| Export | Raw rows leave completely, as newline-delimited JSON |
The Install screen watches for arriving events and lists recent recorded validation refusals with their reasons. Requests rejected before that diagnostic stage are not listed.
Reading the values of a property
Select an event on the Events report and the Properties card lists the fields it carried, ranked by how many occurrences carried each, with how many different values each took. Select a property and a second card ranks those values: plan answers pro 120 and free 80, each with its visitor days and its share of the event. Every value is reachable. The card pages through the whole tail and searches it, so a product property with four hundred values does not stop at a top fifty.
Share divides a value's occurrences by every occurrence of the event, so the values of a property the event does not always carry add up to that property's coverage rather than to 100%. A property marked constant took one value everywhere, which is a tag somebody fixed rather than a measurement, and it stays listed because a missing field and a fixed one are different facts.
The same answer is the property-values report over the API and the MCP tool of the same name, taking event and key beside the usual range, filters, paging, and search. A value that reads as a person is withheld there even though the collector already refuses one on arrival, and its count is kept so you can find out what was sent. A value longer than 255 characters is cut where the collector cuts it.