custom events

Log your own structured events without setting up Snowplow, Segment, GA, or GTM. AgentContext.track(name, properties?) lands the event in event_log with the same sanitisation as every other source.

The event lives in the snapshot in the user's browser, and you decide if and when it goes to your server. There's no collector behind it and nothing for us to host.

1 / basic usage

three calls, no setupts
import { AgentContext } from '@contextune/sdk';

AgentContext.init();

// Anywhere in your app:
AgentContext.track('filter-dropdown:applied', { facet: 'size', value: 'M' });
AgentContext.track('checkout-form:submitted', { cart_value: 129 });

// No props is fine.
AgentContext.track('searched');

track() is on both the static AgentContext façade and the instance returned by init(). Use whichever shape is convenient.

2 / react / framework integration

Wire track()into the same hooks you use for any other client-side event handler. There's no wrapper or provider to add.

track from a react componenttsx
'use client';

import { useEffect } from 'react';
import { AgentContext } from '@contextune/sdk';

export function FilterDropdown({ facet, value }: { facet: string; value: string }) {
  useEffect(() => {
    AgentContext.track('filter-dropdown:applied', { facet, value });
  }, [facet, value]);

  return null;
}

3 / coexistence with source plugins

track() and an external event source (Segment / GA4 / GTM) can run side by side. The source plugin owns its own ingress path — Segment events come in through analytics.on('track'), GA4 / GTM through dataLayer.push. Custom track() calls land directly in the engine and skip whatever source you chose.

Both ingresses end up in the same event_log, in chronological order.

segment + custom events togetherts
// Custom events coexist with source plugins. If you tap Segment,
// Segment events flow through analytics.on('track') as normal;
// your AgentContext.track() calls go straight to the engine.
// Same event_log, different ingress.

import { AgentContext } from '@contextune/sdk';

AgentContext.init({ eventSource: 'segment' });

// segment fires this through analytics.on('track')
analytics.track('Product Viewed', { product_id: '12345' });

// you fire this directly into the SDK engine
AgentContext.track('hero-cta:clicked', { variant: 'A' });

// both land in snapshot.event_log
const snapshot = AgentContext.getSnapshot();

DOM-derived behavioural primitives (scroll, idle, rage clicks, current focus) run regardless — they're wired off native browser events, not the source plugin.

4 / naming events

Use noun-context:verb — concrete enough that you can search for it later, and easy for an agent reading the snapshot to interpret. The model will see the event names verbatim; clarity beats brevity.

// Use a verb plus a context. "Applied a filter" beats "filter."
AgentContext.track('filter-dropdown:applied', { facet: 'size' });
AgentContext.track('checkout-form:submitted', { cart_value: 129 });
AgentContext.track('hero-cta:clicked', { variant: 'A' });

// avoid generic verbs by themselves
// AgentContext.track('clicked');     <- which click?
// AgentContext.track('input');       <- which input?
// AgentContext.track('viewed');      <- viewed what?

Event names are sanitised at the boundary (sanitiseForLLMstrips chat-template markers, control characters, and length-caps to 256 chars). You can't accidentally smuggle </system> into your event log.

5 / what the sanitiser does to your properties

Properties are recursively sanitised before they land in the snapshot. Three things happen:

what gets dropped, what gets keptts
// track() is safe to call at any time. Pre-init and post-destroy
// calls are silent no-ops — you can wire it up without worrying
// about lifecycle. Properties are recursively sanitised; sensitive
// keys are dropped, URL-shaped values redacted, depth bounded at 4.

AgentContext.track('login-form:submitted', {
  email: 'leaked@example.com',     // dropped: matches sensitive key
  remember_me: true,               // kept
  next_url: 'https://app/?token=secret', // redacted: query stripped
});

The full sanitisation rules are documented in Security.

6 / when to use track() vs a source plugin