@kixo.io/web
v0.1.33
Published
Kixo Web Analytics SDK — lightweight, privacy-first analytics for web applications
Maintainers
Readme
Kixo Web SDK
Lightweight, privacy-first analytics SDK for web applications. Part of the Kixo platform.
The SDK auto-captures page views, clicks, scroll depth, errors, sessions, performance vitals (LCP / INP / FCP / CLS / TTFB), Web Push permission state, third-party SDK usage, heatmaps, and project-controlled session replay — with rrweb lazy-loaded only when pixel replay is enabled.
Same wire format as the iOS / Android SDKs, identical StandardProperty
keys (37 cases across 8 packs), identical lifecycle states. Cross-platform
funnels Just Work.
Install
npm
npm install @kixo.io/webimport Kixo from '@kixo.io/web';
Kixo.init({ projectId: 'kx_proj_...', apiKey: 'kx_key_...' });Script tag (CDN)
<script type="module">
import Kixo from 'https://cdn.kixo.io/kixo.min.js';
Kixo.init({ projectId: 'kx_proj_…', apiKey: 'kx_key_…' });
</script>The script auto-detects ?id=…&key=… query params from its own
<script type="module" src=…> URL, so an explicit Kixo.init({…}) call is optional
when you set the params on the URL itself.
The npm package and CDN embed are two supported distributions of the same
release. Pin the immutable CDN URL when you need a fixed browser version, for
example https://cdn.kixo.io/kixo-VERSION.min.js.
Configuration
Kixo.init({
projectId: 'kx_proj_…',
apiKey: 'kx_key_…',
environment: 'production', // or 'development'; staging is not provisioned
// ── Auto-trackers ─────────────────────────────────────────
autoTrack: {
pageViews: true,
clicks: true,
scrollDepth: true,
sessions: true,
forms: true,
network: true, // opt-in; default false because URL paths may contain PII
errors: true,
performance: true, // LCP / INP / FCP / CLS / TTFB / DOM-load
rageClicks: true,
deadClicks: true,
engagementTime: true,
context: true,
refreshLoop: true,
lifecycle: true,
resourceErrors: true,
perfBudget: true,
attribution: true,
push: true, // Notification permission monitoring (privacy-neutral)
},
heatmap: {
enabled: true,
clicks: true,
moves: true,
scroll: true,
},
});Session replay is configured per project in Kixo Dashboard, not in the embed
code. The SDK applies enabled, sampling, masking, and duration from remote
config on init and on periodic refresh.
Data ownership
Kixo does not maintain end-user consent or opt-out state. Your application decides which data to send and which automatic trackers to enable. Kixo applies the project policy returned by the control plane and the collection settings configured in your project.
API
Track events
Kixo.track('purchase_completed', {
product_id: 'SKU-123',
amount: 49.99,
currency: 'USD',
});Page views
Kixo.page('Product Detail', { category: 'shoes' });Identify
import Kixo, { StandardProperty } from 'https://cdn.kixo.io/kixo.min.js';
// Reserved standard properties carry `$`-prefix (Mixpanel convention)
// so they namespace away from your custom traits.
Kixo.identify('user_123', {
$email: '[email protected]', // identity
$name: 'Jane Doe', // identity
$plan: 'pro', // saas pack
$lifetime_orders: 12, // ecommerce pack
signup_source: 'twitter_ad', // custom trait
});
// Typed standard properties (37 cases mirror iOS / Android)
Kixo.setUserProperty(StandardProperty.Email, '[email protected]');
Kixo.setUserProperty(StandardProperty.Country, 'US');
Kixo.setUserProperty(StandardProperty.Plan, 'pro');
Kixo.setUserProperty(StandardProperty.LifetimeOrders, 12);
// Or string-keyed for custom traits (autocomplete still surfaces the 37):
Kixo.setUserProperty('signup_source', 'twitter_ad');
// Boolean flags — tag a user for segmentation & campaigns
Kixo.setUserProperty('subscribe', true); // queryable as "subscribe = true"
Kixo.setUserProperty('vip', true);
Kixo.setUserProperty('beta_tester', false);
// Bulk
Kixo.setUserProperties({
$email: '[email protected]',
$plan: 'pro',
subscribe: true,
custom_trait: 'value',
});Boolean / string / numeric properties all power segments + campaigns + chat
("send a welcome email to users where subscribe is true"). Persisted in
localStorage; cleared on Kixo.reset().
37 typed StandardProperty cases across 8 packs (identity / geo /
lifecycle + 5 verticals). The dashboard auto-detects which packs your
project actively uses and adapts the audience-explorer columns to match.
Byte-for-byte mirrored across iOS / Android.
| Pack | Cases |
|---|---|
| identity (universal) | Email, Phone, Name, FirstName, LastName, AvatarUrl |
| geo (universal) | Country, City, Region, Timezone, Language, Locale |
| lifecycle (universal) | Created, LastSeen |
| saas (vertical) | Plan, SubscriptionStatus, TrialEnds, Mrr, SubscriptionStarted |
| ecommerce (vertical) | LifetimeOrders, LifetimeRevenue, Aov, LastPurchase, FirstPurchase, CartAbandonedCount |
| media (vertical) | ContentTier, SubscribedCategories, WatchTimeTotal, LastPlayed |
| marketplace (vertical) | SellerTier, BuyerTier, ListingsCount, ReviewsCount, Verified |
| loyalty (vertical) | LoyaltyPoints, VipLevel, ReferralCount |
Don't see your pattern? Use bare keys (no
$-prefix) for custom traits — they surface in the Custom Traits panel of the audience explorer without polluting the first-class profile columns.
Group / B2B
Kixo.group('team_456', { company: 'Acme Inc', plan: 'enterprise' });Super-properties (decorate every event)
Kixo.setSuperProperty('app_variant', 'B');
Kixo.setSuperProperties({ app_variant: 'B', build_flavor: 'beta' });
Kixo.unsetSuperProperty('app_variant');
Kixo.clearSuperProperties();Web Push (FCM)
// 1. Prompt for permission (must be inside a user-gesture handler)
const result = await Kixo.requestPushPermission();
// → 'granted' | 'denied' | 'default'
// 2. Obtain the current FCM Web registration token with Firebase's
// getToken(...) API, then register it with Kixo on every app start.
Kixo.setPushToken(fcmRegistrationToken); // provider defaults to 'firebase'
// 3. When manually integrating delivery callbacks, log funnel events:
Kixo.logPushReceived({ campaign_id: '…' });
Kixo.logPushOpened({ campaign_id: '…', action_id: '…' });
Kixo.logPushDismissed({ campaign_id: '…' });Kixo's web sender uses FCM HTTP v1. Raw PushSubscription endpoint URLs
are not supported: setPushToken rejects them (and non-firebase
providers) with a console.warn and returns false instead of creating
an undeliverable token — it never throws into your registration handler.
Use Firebase Web SDK deleteToken(...) when removing an FCM token.
push_permission events fire automatically as the user grants /
denies / resets — no manual wiring.
Manual goal markers (May 2026)
Two surfaces declare conversion goals — both fire the same
event_type='goal' payload, both validated against the strict
[a-z0-9_-]{3,64} whitelist (server-template-injection-safe).
HTML attribute — best for inline declarations on existing buttons / forms:
<button data-kixo-goal="checkout_complete"
data-kixo-goal-value="49.99"
data-kixo-goal-currency="USD">Pay</button>
<form data-kixo-goal="signup" data-kixo-goal-trigger="submit">…</form>
<section data-kixo-goal="onboarding"
data-kixo-goal-step="2"
data-kixo-goal-trigger="view">
<!-- IntersectionObserver fires after ≥50% visible for ≥500ms -->
</section>Sub-attributes: -trigger (click / submit / view —
default click), -step (1-32 funnel step), -value,
-currency (ISO 4217), -once (session / page / never —
default session), -props (JSON object string ≤ 2 KB).
JS API — best for server-confirmed events without a clickable element:
Kixo.markGoal('checkout_complete', {
value: 49.99,
currency: 'USD',
step: 3,
properties: { plan: 'pro' }, // PII-filtered automatically
});Manual marks are ground truth — they beat AI auto-detected goal candidates on the same scope. The auto-goals analyzer (in the dashboard) skips proposing goals the host already declared manually.
Auto-tracked behavior signals (May 2026)
Beyond clicks + page views, the SDK now emits 4 new event types that drive auto-goals AI + behavior-trail replay:
| event_type | event_name(s) | When |
|---|---|---|
| modal | modal_opened / modal_closed | DOM mutation creates/removes a dialog (native <dialog>, role=dialog + aria-modal, fixed-position overlays) |
| page_semantic | page_semantic | Once per page-view — captures path, H1/H2/H3, top-3 CTAs by visual prominence, form field signature, OpenGraph type, JSON-LD @type |
| interaction | long_press / swipe_* / double_tap / pinch_* / drag / scroll_end | Pointer/touch/scroll thresholds — cross-platform parity with iOS |
| goal | <your_goal_name> | data-kixo-goal HTML attribute or Kixo.markGoal() JS call |
All event payloads pass through PII filter (regex + Luhn for cards, Unicode-aware for emails) before reaching the wire.
Feature flags & experiments
const variant = Kixo.getVariant('checkout_experiment', ['control', 'variant_a']);
const showNewUI = Kixo.getFeatureFlag('new_checkout_ui');
await Kixo.refreshFlags();Operational
await Kixo.flush(); // force-drain the buffer
const diag = Kixo.diagnostics(); // lifecycle + release + queue + replay
Kixo.reset(); // logout — new anonymousId, full clearAuto-tracking
| Event type | Description |
|---|---|
| page_view | SPA navigation + initial page load |
| click | All clicks with selector / text / coords (text PII-filtered) |
| scroll_depth | 25 / 50 / 75 / 100 % milestones |
| session_start | New session with idle-timeout detection |
| session_end | Session closed after idle rotation or explicit SDK teardown; browsers may omit it on tab close |
| form_submit | Submission (values excluded for privacy) |
| network_request | XHR/fetch with timing + auto-classified _kixo_classification |
| error | Unhandled exceptions + promise rejections |
| performance | LCP, FCP, FID, INP, CLS, TTFB, DOM-load |
| rage_click | 3+ clicks on the same element within 1s |
| dead_click | Click that produces no DOM change |
| engagement_tick | Visible-active-time per 15s |
| frustration_refresh_loop | 3+ reloads in 2 minutes |
| bfcache_restore, pwa_installed, online, offline | Lifecycle |
| resource_error | Failed image / script / CSS / font loads |
| push_permission | Notification permission state changes |
| push_received / push_open / push_dismissed | Delivery events (from SW) |
Web Vitals (CrUX-standard payload)
Every performance event ships the standard { id, value, rating, delta, navigation_type }
shape that downstream RUM dashboards expect:
{
"name": "lcp",
"value": 1843,
"id": "v3-lcp-9f8b3c4d2e1a",
"rating": "good",
"navigation_type": "navigate",
"url": "https://shop.example.com/cart",
"path": "/cart"
}Rating thresholds match Google's official Core Web Vitals (web.dev/vitals).
INP gets its own visibility-change report capturing the worst interaction
across the page lifetime; CLS reports incrementally with delta since the
last sample.
Privacy filtering (PII)
Every string in event properties (and breadcrumb data) passes through
the central PII filter before reaching the wire format. Detected and
redacted:
- Email addresses
- Phone numbers (E.164, parens, dotted)
- Credit cards — Luhn-validated (only real card numbers, not random 16-digit IDs)
- US SSN (3-2-4 digit format)
- IBAN, JWT tokens, API keys (32+ char alphanumeric runs),
social handles (
@username)
Long copy (>256 chars) head-scans only — editorial article text isn't treated as a PII surface to keep false-positives down.
This filter is identical to the iOS SDK's (PIIFilter.swift), so
cross-platform replay surfaces redact the same things on both sides.
The kixo-private HTML attribute (<input data-kixo-mask>) excludes
specific elements from snapshot capture entirely.
SDK / vendor classification
Network events get auto-stamped with _kixo_classification for the
top 50 web vendors (Google Analytics, Stripe, Auth0, Sentry, Mixpanel,
Hotjar, Intercom, etc.). The full 1100-entry catalog applies server-side
for the long tail.
Stamp shape:
"_kixo_classification": {
"sdk_name": "Stripe",
"sdk_vendor": "Stripe",
"sdk_category": "payments"
}Categories: analytics · payments · auth · crash-reporting ·
session-replay · ab-testing · support · cdn · email · maps ·
storage · realtime · cms · ads.
Heatmaps
When heatmap.enabled is true, the SDK records click positions, mouse
movements, and scroll depth, then uploads DOM snapshots for
server-side heatmap rendering. Snapshots dedupe via a fingerprint
endpoint so re-visits to the same page don't re-upload.
Session replay
When Session replay is enabled for the project in Kixo Dashboard, the SDK
records DOM mutations and user interactions for full session playback without
an application-code opt-in. The dashboard player renders the rrweb stream
in a static iframe. Interaction taxonomy (long_press, swipe,
double_tap, pinch, drag, scroll_end) is captured separately as
analytics events.
Diagnostics
const d = Kixo.diagnostics();
// d.lifecycle — initialising / running / recovering / pausedByServer
// d.releaseId — server-bound release, or null before init resolves
// d.pauseReason — stable server reason when paused
// d.queue.bufferedEventCount, …, retryTier
// d.replay.serverEnabled
// d.replay.sampleRatePercent
// d.replay.captureOnCellularApplied // always false on WebInspect from devtools: copy(Kixo.__diag).
Testing
npm test # full Vitest suite
npm run test:watch # interactive
npm run test:coverageThe current 0.1.8 gate is 39 files / 430 tests and also runs TypeScript, clean Rollup build, Service Worker syntax, package dry-run checks, and the leased/monotonic CDN publication contract. Use the command output as the source of truth for test counts.
Performance
- Code-split ESM entry; exact compressed size is measured from each clean build
- rrweb is the only runtime dependency and stays in the lazy replay chunk
sideEffects: false— fully tree-shakeablerequestIdleCallback-deferred flush, never blocks main threadsendBeacononpagehidefor reliable last-mile delivery- Replay recorder is lazy-loaded via dynamic
import()— projects whose remote policy keeps replay disabled never download it
Build outputs
| Format | File | Use case |
|---|---|---|
| CDN ESM | dist/kixo.min.js | Public script-module entry |
| ESM | dist/esm/index.js | npm bundlers and modern runtimes |
| CJS | dist/kixo.cjs.js | npm CommonJS consumers |
| Types | dist/esm/index.d.ts | npm TypeScript consumers |
Compatibility
| | Modern | Legacy | |---|---|---| | Browsers | Chrome 90+, Firefox 90+, Safari 15+, Edge 90+ | Chrome 80, Safari 14 (degraded — INP unavailable) | | Frameworks | React, Vue, Svelte, Angular, Solid, raw HTML | All | | Build tools | Vite, esbuild, webpack 5+, rollup, Parcel 2 | webpack 4 (use ESM entry) |
Development
npm install
npm run build # clean Rollup build → CDN ESM + ESM/CJS QA artifacts
npm run dev # rollup -w
npm run typecheck # tsc --noEmit
npm test # vitestLicense
MIT
