Documentation
Coming from another tool
Reading the reports
Count what matters
Machine access
What a visitor, a visit, and a day mean here.
The definitions behind every number in the panel, and the privacy model that shapes them. Nothing in the reports makes sense without this page, and all of it fits on one.
A visitor is a day-long code
Kehai sets no analytics cookies and stores no visitor identifier in the browser. A visitor code is a one-way hash of the site, network address, and browser, salted with a random value that changes every midnight UTC. Kehai stops using the previous day's salt at that boundary.
The consequences are the point:
- Visitor and visit codes start fresh each UTC day. Kehai does not carry a visit or its source past midnight UTC, and it does not report returning visitors across days. Earlier event rows remain; daily codes do not make their times, paths, retained click values, or customer-supplied properties impossible to correlate.
- Visitor codes are scoped to one site. The site's identifier is part of the hash, so Kehai does not use a shared visitor code across sites.
- No analytics identifier is stored in the browser. Kehai sets no cookie and keeps no visitor identifier in browser storage. Its server-side visitor and visit codes are scoped to one site and one UTC day.
A visit
A visit ends after 30 minutes without activity, or at midnight UTC, whichever comes first. The next pageview or custom event supplies the new visit's source. Pageviews at 23:55 and 00:05 UTC belong to two separate visits. The new visit's source comes from its own events; yesterday's source is not carried forward. A visit appears in the visit count only if it has a pageview in the selected range and filters. A visibility heartbeat can only extend a visit that is still active. It cannot start one. A pageleave after the visit expires does not start another visit or carry its source forward.
Visit trends place each visit on its first matching pageview in the selected range and filters. Pageviews and visitor days stay on the dates they occurred.
Older measurements can contain visits that continued across midnight UTC. Kehai keeps those historical rows unchanged for their retention window. The current rule applies to new measurements; earlier exports and backups can still contain the old visit codes and links between days.
Pageviews, including the single-page kind
A pageview is counted on load and on every route change through pushState, replaceState, or the back button. An application routed on the URL fragment can opt in with data-hash. See the attributes table. A prerendered page counts when it is actually shown, not when the browser warmed it up. Without hash routing enabled, fragments do not distinguish pages. A trailing slash is normalized away.
Engaged time and bounces
Engaged time accumulates only while the page is actually visible. A tab in the background counts nothing. The tracker sends the elapsed interval when the reader leaves or hides the page. During uninterrupted visible reading, it also sends a heartbeat every five minutes. These incremental intervals keep the existing visit active without adding pageviews, cookies, or persistent browser identifiers. Hidden pages send no heartbeat.
A gap of 30 minutes or a change of UTC day stops the heartbeat until a real new pageview. It cannot reconnect an expired visit after a sleeping browser or a long-hidden tab returns. A page that comes back into view starts a new visible interval and reports it on its next leave, so one view can send several intervals. Each reports only time not already sent.
If the browser delays an unflushed interval for at least 30 minutes, Kehai marks that interval unknown. It does not turn a possible sleeping browser into thirty minutes of reading.
A late interval can enter time per visit only when it belongs to a matching pageview and visit in the selected range and filters. Time received after expiry or midnight UTC may have no such match. Kehai keeps that received time separately and shows time per visit as unavailable when measurement is incomplete. It never assigns the time to an unrelated later pageview. Older measurements keep their original calculation rules. A bounce remains a visit with a single pageview.
A day is your site's day
Every report cuts time at midnight in the site's own time zone, set on the Site screen. The visitor and visit codes still reset at midnight UTC. A local report day can therefore include codes from two UTC days: visitor days count those daily codes, not unique people in that local day.
Three origins that never mix
Every stored day names where it came from: measured by Kehai's own snippet, imported from GA4, Plausible, or Umami, or modeled by the forecast. A forecast row is never summed with a counted one, and an imported day is labeled with the system that measured it rather than being restated as Kehai's own. When a number is missing it stays missing: an absent measure is never reconstructed from another denominator, because absent is not zero.
Bots are removed before you see anything
Traffic declaring itself as a crawler is classified at intake and kept out of every human metric. Each removal ships with the rule that caught it, on the Bots screen. A name is a declaration, never a verification: confirming a crawler would take a reverse lookup of an address Kehai does not keep, so the panel says "declared itself Googlebot" and no more. AI assistants get their own channel, classified ahead of search.
What stays out of ordinary analytics rows
- Raw network addresses and human User-Agent strings. Intake uses them to derive geography, browser details, and the daily code without storing them in ordinary human-traffic rows.
- Page titles, canvas output, font and plugin lists, audio fingerprints, and timezone lists.
- The current site list of blocked query parameters. The script carries no list of its own: intake applies the site's configured list to every arriving event, without additional fixed exclusions, so a Settings change takes effect on the next event that arrives.
Unblocked campaign click and sharing parameters, including gclid and fbclid, keep their supplied values within the existing 255-character query key/value and 2,048-character URL input limits. These values can link records beyond a daily code. Their presence helps classify a source; a sharing identifier alone does not prove paid traffic. Older rows containing only a 1 marker remain unchanged, and the discarded values cannot be recovered. New original values require an updated tracker and collector. This collection does not send conversions to Google Ads.
IP rate-limit counters last up to three minutes. Declared bot User-Agent strings can remain in bot reports for the site's retention period. Infrastructure providers process request data separately. These limits are described in the privacy policy. The cookie and storage statement lists the fields.