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.