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
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.
'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.
// 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:
- Sensitive keys redacted. Keys in the blocked set (
password,passwd,pwd,email,credit_card,cvv,ssn,phone,dob,address,postcode,zip,auth,cookie, and others) or matching the regex/(password|secret|token|api[_-]?key|jwt|session)/ihave their string values replaced with[REDACTED]. Non-string values matching the regex are passed through untouched. - URL-shaped values scrubbed. Keys called
url,href,uri,referrer, orrefererhave their values routed throughredactUrl— sensitive query params become[REDACTED], UUID path segments become[id]. - Depth bounded at 4. Anything deeper than four levels of nesting is passed through unredacted. Prevents unbounded recursion on large payloads.
// 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
- You don't have analytics yet. Skip the source plugin. Call
track()directly at the points that matter. You get a clean event log with no third-party SDK on the page. - You already run Segment / GA4 / Mixpanel. Set
sourceininit()and your existing events flow through automatically. Addtrack()calls only for the events your analytics provider doesn't already capture. - You want full control. Omit
sourceand emit only the events you care about withtrack(). You get a clean log with no framework noise and no third-party SDK to vet.