custom events

Log your own structured events with Contextune.track(name, properties?). The event lands in event_log with the same sanitisation as every other source — no setup beyond the initial init() call.

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 { Contextune } from '@contextune/sdk';

Contextune.init();

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

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

track() is on both the static Contextune 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 { Contextune } from '@contextune/sdk';

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

  return null;
}

3 / coexistence with source plugins

track() and a source plugin (Segment / GA4 / Mixpanel) can run side by side. The source plugin owns its own ingress path — it captures events from the intercepted network traffic. Custom track() calls land directly in the event log and skip the network intercept path entirely.

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 the network intercept path as normal;
// your Contextune.track() calls go straight into the event log.
// Same event_log, different ingress.

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

Contextune.init({ source: 'segment' });

// segment fires this — the SDK intercepts it from the network
analytics.track('Product Viewed', { product_id: '12345' });

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

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

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."
Contextune.track('filter-dropdown:applied', { facet: 'size' });
Contextune.track('checkout-form:submitted', { cart_value: 129 });
Contextune.track('hero-cta:clicked', { variant: 'A' });

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

Event names are sanitised at the boundary (sanitiseForLLMstrips chat-template markers, control characters, and length-caps to 64 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 redacted, 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 redacted, URL-shaped values scrubbed, depth bounded at 4.

Contextune.track('login-form:submitted', {
  password: 'secret',               // redacted: matches sensitive key set
  remember_me: true,                // kept: non-sensitive boolean
  next_url: 'https://app/?token=x', // scrubbed: token query param → [REDACTED]
});

The full sanitisation rules are documented in Security.

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