Sumidatasumidata.io
Sign in
Docs/Guides/Product events
Guides · Events

Product events

Track any user action with a single push call. Events carry arbitrary metadata, link automatically to the current session and replay, and — when you tag them with a category, feature, and surface — feed the AAARRR funnel reports out of the box.

01Basic event tracking

Any user-visible action is a candidate for a product event.

Simple event

app.js
Sumidata.push('event', ['button_click'])

Event with properties

app.js
Sumidata.push('event', ['form_submitted', {
  formId: 'newsletter-signup',
  fields: 5
}])

02Automatic events

Zero-code events: pageviews, clicks, form submits, and scroll depth — collected automatically.

With autocapture on (the default), the SDK sends these events without any tracking code — the dashboard has data the moment the snippet is installed:

EventFiresProperties
$pageviewOn load and on every SPA navigation — History API pushState / replaceState / popstate with a changed path or query.url, referrer
$autocapture_clickClick on an interactive element — <a>, <button>, [role="button"], input[type="submit"]. Clicks elsewhere are ignored as noise; repeated clicks on the same element are throttled to one per second.url + element context
$autocapture_submitForm submission.url + element context
$autocapture_scrollScroll depth milestones — 25 / 50 / 75 / 100% of page height. Each milestone fires once per pageview and the set resets on SPA navigation. A page with nothing to scroll counts as 100.url, milestone

These scroll milestones power the Content Effectiveness board — for every page it shows the share of pageviews that reached 25 / 50 / 75 / 100% of the page, so you can see where readers drop off. "Reached 50%" counts every pageview whose furthest milestone was 50 or deeper; a page that fits on one screen reports 100% across the board.

Element context is a human-readable signature of what was clicked or submitted: tag, id, classes, tracking data-* attributes, inner text trimmed to 80 characters, and a CSS selector path — e.g. div#checkout > form.payment > button.cta. Only attributes from a tracking allowlist are collected (data-testid, data-test…, data-track…, data-analytics…, data-event…, data-action…, data-gtm…); any other data-* attribute never leaves the page.

Turning it off

One attribute on the snippet — data-autocapture, default on. off disables all automatic collection, including scroll depth; manual push('event', …) calls are unaffected:

index.html
<script
  src="https://sdk.sumidata.io/loader.js"
  data-project-id="YOUR_PROJECT_ID"
  data-autocapture="off"
  async></script>

Excluding elements

To keep a single element or a whole subtree out of autocapture, add data-sumi-no-capture to it — clicks and form submits on that element or any of its descendants are never captured:

index.html
<div data-sumi-no-capture>
  <button>Not tracked</button>
</div>

Masking interaction

Autocapture respects the same data-masking attribute as session replay (see SDK configuration):

  • Form field values (input, textarea, select) are never collected — in any masking mode, including dangerouslyNoMask.
  • With fieldsAndLabels, <label> text is also excluded from the element context.
  • The url on every automatic event is sanitized before it leaves the browser — sensitive query parameters (token, access_token, code, email, password, and similar) are removed.
i
Auto-events carry no funnel metadata (_category / _feature / _surface) — tag your key actions with manual events when you want them in the AAARRR reports.

03Event payload

Nested objects, arrays, numbers, booleans — anything JSON-serializable is fine.

app.js
Sumidata.push('event', ['feature_used', {
  featureId: 'export_csv',
  rowCount: 1200,
  format: 'xlsx',
  filters: ['last_30d', 'segment:enterprise']
}])
i
Properties are stored verbatim as a JSON column on the event row — there is no fixed schema. Query them with ClickHouse JSON functions (JSONExtractString, JSONExtract*) or ask the AI Analyst. Keep payloads small (a few KB) so ClickHouse JSON parsing stays cheap at report time.

04SDK commands

Four push commands cover the full lifecycle — event, identify, reset, identifyExperiment.

Every method is GTM-style: Sumidata.push('method', [args]). Calls made before the SDK finishes loading are queued and flushed on init — the stub <script> tag swallows the intent even if the main bundle hasn't arrived yet.

event

Fire a product event or conversion. See section 05 for conversion payloads.

app.js
Sumidata.push('event', ['page_viewed', { path: '/pricing' }])

identify

Stamp the session with your stable user ID. If a different user was previously identified on this device, the SDK automatically rotates the session — the old session stays attached to the old user, a new session is created for the new user. This prevents history merge on shared devices.

app.js
// On login
Sumidata.push('identify', [user.id])

reset

Clears the stored user ID, rotates the session, and starts a fresh replay. Always call this on logout — otherwise the next user on the same device inherits the prior identity.

app.js
// On logout
Sumidata.push('reset', [])

identifyExperiment

Register A/B variant assignments for the current session. Sticky: the first assignment per (sessionId, experimentId) wins. See Experiments for the full guide.

app.js
Sumidata.push('identifyExperiment', [[
  { id: 'pricing-v3', variant: 'control' }
]])
i
The SDK also exposes Sumidata.getSessionId() — useful when you need to carry the browser session ID back to your server for linked server-side conversions. Returns null until session creation finishes.

05Conversions

A conversion is an event with orderId + totalAmount at the top level — sent to the ingest API, not via push().

A conversion is an event that carries orderId (identifier, used for dedup) and totalAmount (net revenue) as top-level fields on the ingest request. Everything else is optional but the richer the payload, the deeper the revenue, campaign, and cohort reports go.

!
Conversions are not sent with Sumidata.push('event', ['purchase', {}]). The SDK extracts only _category / _feature / _surface and moves every other property into payload — so orderId and totalAmount never reach the conversion columns. Send them to POST /sdk/ingest with the fields at the top level.
conversion.sh
curl "https://api.sumidata.io/sdk/ingest" -X POST \
  -H "Content-Type: application/json" \
  -d '{"projectId":"…","deviceId":"…","source":"realtime","events":[{"name":"purchase","orderId":"ord_01HW","totalAmount":79.99,"currency":"USD","_category":"revenue"}]}'

For the full field reference, multi-line-item handling, and backend ingest, see Conversions & attribution and Server-side ingest.

06Funnel metadata (AAARRR)

Three reserved keys (_category, _feature, _surface) bucket the event into AAARRR funnels at ingest.

Sumidata does not auto-classify events by name. Category is something you declare — by passing three reserved keys alongside the event. The server validates them, strips them out of the stored payload, and writes them into a per-project dictionary (event_metadata_dict) so every future event with the same name joins the same funnel.

FieldAllowed values
_categoryawareness · acquisition · activation · revenue · retention · referral
_surfacemarketing_site · app · pdp · checkout · dashboard · … (98 total — full list below)
_featureLowercase alphanumeric + underscores only, ^[a-z0-9_]+$

Allowed surfaces

_surface accepts one of these 98 values. Anything else is skipped (see the validation note below) — pick the entry that best describes where in the product the event fired.

AreaSurfaces
Marketing & public sitemarketing_site · landing · home · pricing · features · about · contact · blog · careers · faq · partners · resources · changelog
App shells & platformsapp · mobile_app · mobile_web · admin · admin_panel
Dashboard & analyticsdashboard · overview · analytics · reports · insights · metrics · activity
Navigation & chromenavbar · sidebar · header · footer · menu · search · search_results · notifications · user_menu
Auth & onboardinglogin · signup · logout · forgot_password · reset_password · onboarding · email_verification · sso · invite
Commerce — browseproducts · product · pdp · plp · catalog · collection · category
Commerce — buycart · checkout · payment · order_confirmation · order_history · wishlist · reviews · shipping · returns
Account & settingsaccount · profile · settings · billing · subscription · plan · usage · security · preferences · team · members · integrations · api_keys
Content & supportdocs · documentation · help_center · support · knowledge_base · tutorials · guides · community · forum
Communicationfeed · messages · chat · inbox · comments
UI surfacesmodal · dialog · banner · toast · widget · embed · popup
Channelsemail · push_notification · sms
Systemerror_page · not_found
events.js
// A signup — acquisition stage, marketing site, auth feature
Sumidata.push('event', ['signup', {
  _category: 'acquisition',
  _feature: 'auth',
  _surface: 'marketing_site'
}])

// A purchase — revenue stage, checkout feature, in the app
Sumidata.push('event', ['purchase', {
  orderId: order.id, totalAmount: order.total, currency: 'USD',
  _category: 'revenue', _feature: 'checkout', _surface: 'app'
}])
!
Invalid values no longer fail the request. The offending event is skipped and listed in the response's skipped array — { "index": i, "name": "…", "errors": { "_surface": ["Invalid surface. Allowed: …"] } } — while every valid event in the same batch is still stored and the response stays 200 with { "status": "ok", "skipped": [...] }. Keep your taxonomy stable — the first event you send with a given name seeds the dictionary entry that every subsequent event resolves against at query time.

07Payload budget

Practical budgets — they aren't enforced server-side today, but staying under them keeps reports fast.

There is no hard 400 on payload size or key count at ingest today. Keep each event under these targets so ClickHouse JSON extraction at query time stays cheap and the UI renders payloads cleanly:

  • ≤ 32 top-level keys per payload.
  • ≤ 8 KB of JSON per payload.
  • ≤ 64 characters per event name, snake_case by convention.

If you need to stash large blobs (full API responses, rendered documents), store them somewhere else and put the URL or hash on the event.

08Server-side ingest

For events that don't originate in a browser, post to the same SDK ingest endpoint with source: 'backend'.

Not every event fires from the browser. Stripe webhooks, background-job results, emailed receipts — all of these originate on your server. Post them to the same POST /sdk/ingest endpoint the SDK uses, with source: 'backend':

curl
curl "https://api.sumidata.io/sdk/ingest" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"projectId":"proj_…","deviceId":"00000000-0000-0000-0000-000000000000","externalUserId":"user_7e2a9c","source":"backend","events":[{"name":"subscription_renewed","_category":"retention","_feature":"billing","_surface":"app"}]}'

See Server-side ingest for the full API reference — request body, error codes, attribution fallback, and AI-agent spec.