Skip to content
Codeskop

Event model

Every event Codeskop records fits one journey: the app opens, a session starts, the user moves between screens, taps things, signs in, converts or fails, and errors land in the same session. This page is the map.

SDK versions

This model needs web SDK 1.5.0 or later, and Android, KMP and Flutter 0.4.0 or later. Older SDKs keep working: their events get a category on the server, but they don't send lifecycle events, clicks or session ids on errors.

Six categories

Each event belongs to exactly one category. The dashboard uses it for the icon and colour in the Events tab, the user journey and the funnel step picker, and you can filter any analytics view by it.

OptionTypeDescription
lifecycle$app_opened, $session_started…The app or tab opening, going to the background, and sessions starting and ending. Sent by the SDK only.
navigationscreen, $deep_link_openedScreens and pages, and links that opened the app.
interaction$click, filter_appliedTaps, clicks, submits and searches: things a user does on a screen.
identityidentify, login_failedSign-up, login, logout and identify, including the failures.
conversionorder_completedThe business steps you care about. The default for track.
technicalcrash, api_errorErrors, crashes, ANRs and API calls. Set by the event type, never by track.

When you don't pass a category

  • screen is navigation and identify is identity.
  • A track name starting with login_, signup_, sign_up_, logout, password_, mfa_ or otp_ is identity.
  • Any other track is conversion.
  • The list is fixed. The SDKs drop a category that isn't one of the five you can send (all but technical), log a warning and still send the event, so the server works one out. Sent straight to the ingest API, an unknown category rejects that event, and the response lists it under rejected.

Automatic events

The SDKs send these for you. Their names start with $, which your own events can't use, so they never clash with yours. The dashboard shows them with friendly labels, such as "App opened".

OptionTypeDescription
$app_openedlaunch_type, load_msMobile: cold on the first foreground of the process, with load_ms; warm on every return to the foreground, next to $app_foregrounded. Web: once per page load, cold for the first load in a tab and warm after.
$session_startedlaunch_typeA new session began.
$app_backgroundedscreenThe app or tab was hidden. screen is where the user was.
$app_foregroundedbackground_sThe user came back, and how long they were away.
$session_endedduration_s, screens, end_reasonThe session finished. inactive: 30 minutes idle, sent at the next event or foreground and dated at the last activity. closed (web): the tab was closed or reloaded. A same-site link click or back and forward don't end it.
$app_updatedprevious_version, versionFirst launch after an app update.
$deep_link_openedurl, sourceWeb: a page load from another site or with utm_ parameters, once per load. url keeps the path and utm_ parameters only; source is utm_source, else the referrer's host.
$clickelement, label, element_id, screenWeb only: a button, link, role=button element or submit button was clicked. label is the visible text (or aria-label), cut to 64 characters.

Screen views can also carry load_ms (how long the screen took to show), previous_screen and, on the web, referrer.

What counts toward your plan

  • Lifecycle events are free and recorded on every plan, including Free and Starter, so crash journeys show when a session started and ended. They never count toward the analytics meter.
  • $click and $deep_link_opened are analytics events: Growth and Scale, and they count toward the meter like your own events.

Clicks on the web

On Growth and Scale, the web SDK records clicks on buttons and links by default. It reads the element type, its id and up to 64 characters of visible text (or the aria-label). It never reads what users type into inputs.

Turn clicks off for a whole project in the dashboard under Settings → SDK (no release needed), or in code. The script tag also accepts data-autocapture-clicks="false".

Turn off click capture
import { init } from "@codeskop/tracker"; init({ apiKey: "cs_live_pk_…", autocapture: { clicks: false } }); // or, with the script tag:// <script src="…" data-codeskop-key="cs_live_pk_…" data-codeskop-autocapture-clicks="false"></script>

To skip one element and everything inside it, add data-codeskop-ignore:

HTML
<div data-codeskop-ignore>  <button>Not recorded</button></div>

On Android, iOS and Flutter, track the taps that matter with the interaction category:

Kotlin
// Mobile has no automatic tap capture. Track the taps that matter:Codeskop.track("delivery_method_selected", mapOf("method" to "pickup"), category = "interaction")

The shared envelope

Every event, from every platform, has the same shape. You don't build it yourself (the SDK does), but it helps to know what each event carries when you read exports or webhooks.

One event
{  "event_id": "4b1e…",          // unique per event, used to drop duplicates  "type": "track",               // track, screen, identify, or a technical type  "occurred_at": "2026-10-16T09:41:07.120Z",  "session_id": "s_8f2c…",       // the session it happened in, on every event  "seq": 12,                     // order within the session  "user": {    "id": "u_1042",              // after identify()    "anonymous_id": "a_93d1…"    // before and after identify(); new after reset()  },  "payload": {    "name": "login_failed",    "category": "identity",    "properties": { "method": "password", "reason": "invalid_password", "attempt": 2 }  }}

Sessions

  • One rule on every platform: a session ends after 30 minutes without activity.
  • On mobile the session is saved on the device, so reopening the app within 30 minutes continues it.
  • On the web a session continues across page loads in the same tab. Closing or reloading the tab ends it with $session_ended (closed). Following a link within your site, or going back and forward, keeps it going.
  • Every event carries the session_id, including errors, crashes and API calls. That is how a journey shows that a user dropped off right after a slow or failing request.

Anonymous users and login

Before identify, events carry an anonymous id kept on the device and no user id. When the user signs in, everything that anonymous id did before is linked to them: the sign-up screens, the failed logins, the first visit. Funnels, paths and the user's journey all show it as one person.

Call reset() when a user logs out. The device gets a new anonymous id, so the next person on a shared phone or computer isn't merged into the last one.

Naming your events

  • snake_case, object first, then a verb in the past tense: order_completed, video_played.
  • Name the outcome, not the button: payment_failed, not pay_button_tapped_error.
  • Send failures as their own events with a reason. A failure you track is a funnel step you can measure.
  • Put variable detail in properties, not the name: plan_selected with plan: "growth".

These are suggestions, not built in. Using them makes funnels and journeys read the same across your apps.

OptionTypeDescription
signup_started / signup_completedmethodidentity. method: phone, email, google.
login_succeededmethodidentity. Pair it with identify().
login_failedmethod, reason, attemptidentity. reason: invalid_password, otp_expired, account_locked, network.
otp_requested / otp_failedchannel, reasonidentity. channel: sms, whatsapp, email.
search_performedquery_length, resultsinteraction. Send the length, not the search text.
delivery_method_selectedmethodinteraction. method: delivery, pickup.
checkout_startedvalue, currency, itemsconversion.
payment_failedpayment_method, reason, valueconversion. reason: declined, insufficient_funds, timeout.
order_completedorder_id, value, currency, payment_methodconversion. Use value for revenue.
TypeScript
import { track } from "@codeskop/tracker"; // Category is optional. Without it, Codeskop works one out (see the table above).track("login_failed", { method: "password", reason: "invalid_password" }, { category: "identity" });track("filter_applied", { filter: "price", value: "under_1000" }, { category: "interaction" });track("order_completed", { order_id: "o_77", value: 4200, currency: "KES" }); // conversion

Never send

  • Names, emails, phone numbers or addresses. Values that look like an email or phone number are replaced with [REDACTED].
  • Passwords, OTP codes, card numbers, tokens or anything typed into a form.
  • Free text users wrote, such as search queries or messages. Send a length or a category instead.
  • Use your own user ids in identify, never an email address.

Next steps