Skip to main content

Events & Data Model

:::caution Pre-release This is a pre-1.0 library (v0.1.1) — API may change without notice. :::

The SDK captures four event types that provide structured behavioral signals for agent control planes and analytics pipelines.

The Four Event Types

1. Page View

Emitted when the user navigates to a route.

{
event_type: "page_view",
route_pattern: "/products", // Normalized URL, no query/hash
ts: 1692547200000,
tab_id: "...",
client_seq: 1
}

Declarative Conversion Trigger: If conversionRoutePattern is set during initJourney(), a page view that matches also emits a conversion event with conversion_source: "route_pattern".

2. Rage Click

Emitted when the user clicks 3+ times in 1 second on the same target (mouse) or 4+ times (touch).

{
event_type: "rage_click",
route_pattern: "/checkout",
click_target_label: "Place Order", // Scrubbed button text or null
click_count: 5, // N of the collapsed burst
effect_latency_ms: 250, // Milliseconds until UI changed, or null
ts: 1692547200100,
tab_id: "...",
client_seq: 2
}

Interpretation: The user clicked multiple times in quick succession, suggesting frustration or confusion. If effect_latency_ms is null, the button had no observable effect.

3. Dead Click

Emitted when the user clicks but no observable effect occurs within 1 second.

{
event_type: "dead_click",
route_pattern: "/checkout",
click_target_label: "Next", // Scrubbed or null
effect_latency_ms: null, // Always null for dead clicks
ts: 1692547200200,
tab_id: "...",
client_seq: 3
}

Interpretation: The user clicked on something but nothing happened (disabled button, missing handler, network latency). Useful for identifying UX friction points.

4. Conversion

Emitted when the user completes a goal, via one of two mechanisms:

Declarative (Route Pattern)

const journey = initJourney({
conversionRoutePattern: "/thank-you",
});
// Any page_view to "/thank-you" automatically emits:
{
event_type: "conversion",
route_pattern: "/thank-you",
conversion_source: "route_pattern",
conversion_name: null,
ts: 1692547200300,
tab_id: "...",
client_seq: 4
}

Explicit (track() call)

journey.track("conversion", { name: "signup_complete" });
// Emits:
{
event_type: "conversion",
route_pattern: "/dashboard", // Current route
conversion_source: "explicit",
conversion_name: "signup_complete",
ts: 1692547200400,
tab_id: "...",
client_seq: 5
}

Interpretation: The user completed a defined goal. Use declarative conversions for route-based goals (e.g., landing on a thank-you page); use explicit conversions for event-based goals (e.g., form submission, button click).

Event Batches

Events are collected into batches and sent to the collector route:

{
sessionId: "abc123...",
events: [
{ event_type: "page_view", ... },
{ event_type: "rage_click", ... },
{ event_type: "conversion", ... },
],
batch_id: "...",
truncated: false, // true if session hit 200-event cap
}

Batches are sent whenever:

  • 60 seconds elapse (or on page unload, whichever comes first)
  • 10 events accumulate (or fewer if beaconed on unload)
  • The session ends (tab close, navigation to another domain)

Constraints

Per-Session Event Cap

A single session (same tab, same domain) is capped at 200 events. Events after the 200th are dropped client-side, and truncated: true is set on all subsequent batches. The server independently enforces the same cap and logs any violations.

export const MAX_EVENTS_PER_SESSION = 200; // From shared

Click Target Label Scrubbing

Click target labels (button text, input placeholder, etc.) are scrubbed of sensitive data:

  • Excluded inputs (password, email, credit card fields): label is always null.
  • Redacted keywords (email, password, "ssn", etc.): replaced with placeholder.
  • Truncated to 32 characters server-side as a final safeguard.

Example:

// Client sees:
{
click_target_label: "Sign In", // Safe
}

{
click_target_label: null, // password input is excluded
}

{
click_target_label: "Email [REDACTED]", // Keyword redacted
}

Route Pattern Normalization

URLs are always stored as normalized route_pattern, never with query strings or hash fragments. This is enforced by client-side normalization and re-verified server-side (see Privacy & Data Model).

Input: /checkout?email=test@example.com&utm_source=email#billing
Output: /checkout

Query strings and hashes are stripped before any other processing. This prevents accidental PII leakage (access tokens, email addresses, session IDs in query params; sensitive navigation in hash fragments).

Timestamps

  • ts (client-supplied, epoch ms): The client's local time when the event occurred. Bounded server-side before trust for anything temporal.
  • Server-side re-stamping: The collector adds its own timestamp for the partition key and rate limiting.

Session Identity

  • tab_id (UUID per browser tab): Allows grouping events within a session and distinguishing tab-open/close boundaries.
  • sessionId (batch-level): Groups events into batches for delivery.
  • __ns_aid cookie (first-party, anonymous ID): Optional. Persists a first-party identifier if identityResolver is not overridden.

When requireConsent: true is passed to initJourney(), no events are recorded until the user opts in:

// Default: consent not required, recording starts immediately
const journey = initJourney({
siteId: "my-site",
collectUrl: "/api/journey/collect",
});

// Strict: wait for explicit opt-in
const journey = initJourney({
siteId: "my-site",
collectUrl: "/api/journey/collect",
requireConsent: true, // Recording blocked until globalThis.__nsJourneyConsent = true
});

When requireConsent is true, set globalThis.__nsJourneyConsent from your consent banner:

// User opts in:
globalThis.__nsJourneyConsent = true;

// User opts out (or withdraws consent):
delete globalThis.__nsJourneyConsent; // or set to false

Listen for consent changes:

import { onConsentChange } from "@next-story/journey-recorder/client";

onConsentChange((consentGranted) => {
console.log("User consent changed to:", consentGranted);
});

Data Retention

Raw events are stored in a raw_event table partitioned by day. Retention is enforced by partition drop, not row-level DELETE:

  • After N days (configurable, default 30), the partition for that day is dropped entirely.
  • To comply with GDPR erasure requests, only raw_event and session_summary partitions need to be affected.
  • Aggregated tables (summary, rollup) carry no user identifiers, so they are unaffected.

This is enforced by a CI schema-audit test, not by application code.