JavaScript SDK

Drop-in event capture for Product Analyst AI — ~1.5KB gzipped, no dependencies

Overview

The Product Analyst AI SDK is a tiny JavaScript snippet that streams user behavior from your web app into the agent in real time. It autocaptures pageviews and clicks out of the box, supports custom events via pai.track(), and runs alongside any other analytics tool (Mixpanel, PostHog, Amplitude, Segment) with no conflicts.

Minimum viable setup

Three steps. Everything below this is deeper configuration — you don't need it to get started, and you don't need PostHog, CSV uploads, or any other data source. The SDK is self-sufficient for identity and analytics.

<!-- 1. Load the script -->
<script src="https://productanalyst.ai/sdk.js" data-api-key="ppk_YOUR_KEY"></script>

// 2. Identify the user — email + name populate your users list
pai.identify('user-42', { email: '[email protected]', name: 'Niko' })

// 3. Attach the user to their account — name populates your accounts list
pai.group('acme-corp-123', { name: 'Acme Corp', domain: 'acme.com' })

That's it. The SDK now captures every pageview and click automatically, and both your users and accounts lists are populated from the two identity calls above.

Contents

  1. Install the script
  2. Identify your users
  3. Associate users with companies (B2B)
  4. Send custom events
  5. Autocapture
  6. Opt elements out of capture
  7. Send events from your backend
  8. Privacy
  9. API reference

1. Install the script

Drop this in your <head>. Your public key lives in Settings → Integrations → Data Sources and starts with ppk_. It's a write-only key, safe to embed in client-side code.

<script src="https://productanalyst.ai/sdk.js" data-api-key="ppk_YOUR_PUBLIC_KEY"></script>

That's it — the SDK is now loaded. It stays inert (no events sent) until you identify a user.

2. Identify your users

Identifying users is required. The SDK does nothing until you call pai.identify(). There is no anonymous tracking — events are only captured for known, logged-in users. Call this as soon as the user logs in.
pai.identify('user-42', { email: '[email protected]', name: 'Niko', plan: 'pro' })
ParamTypeRequiredNotes
userIdstringYesStable user ID from your own database
traitsobjectNoUser metadata — email and name populate your users list; everything else lands in JSONB properties

Use the same user ID that your backend uses. Traits are stored two ways at once:

  • Your users list gets a row in tracked_users keyed on userId. email and name are stored as top-level columns so they're queryable without JSON digging; everything else is shallow-merged into a properties JSONB column. Subsequent calls enrich the row — existing values are never nullified when you omit a trait.
  • Every subsequent event carries the traits too, so the agent can answer questions like "how do pro users behave compared to free?"without a join.

Calling identify() also fires an initial $pageview for the current page. On logout, call pai.reset() to clear the user and return the SDK to its inert state.

3. Associate users with companies (B2B)

If your product is used by teams, workspaces, or organizations, tell the SDK which company the current user belongs to. This unlocks account-level analytics — questions like "which accounts show churn risk?" or "analyze behavior inside Acme Corp."

pai.identify('user-42', { email: '[email protected]', name: 'Niko' })
pai.group('acme-corp-123', { name: 'Acme Corp', domain: 'acme.com', plan: 'enterprise' })
ParamTypeRequiredNotes
accountIdstringYesStable account/org identifier from your app. Pass null to clear.
traitsobjectNoAccount metadata — name and domain populate your accounts list; everything else lands in JSONB properties

Which ID should I use? Whatever ID your own database uses for organizations, workspaces, or teams. The exact shape doesn't matter as long as it's stable across sessions.

Where traits land. Each pai.group() call upserts a row in tracked_accounts keyed on accountId.name and domain are stored as top-level columns (queryable directly); anything else — plan, mrr,signup_date, whatever — is shallow-merged into a JSONBproperties column. Subsequent calls enrich the row; existing values are never nullified when you omit a trait.

No separate data source needed. Company-level metadata goes through this one call. You don't need to stream it from PostHog, upload a CSV, or wire up a CRM sync to get name / domain / planinto your accounts list.

Consumer app? No companies? Skip this step entirely. pai.group() is optional — if you never call it, events are still linked to individual users and everything else works as expected.

Switching accounts. Call pai.group() again with the new ID whenever the user changes workspace. pai.reset() clears both the user and the account on logout.

4. Send custom events

Use pai.track() for anything autocapture can't see — backend signals, multi-step flows, business logic.

pai.track('upgrade_clicked', { plan: 'pro', annual: true })
ParamTypeRequiredNotes
eventNamestringYesStored as-is (no transformation)
propertiesobjectNoArbitrary key-value pairs passed to alert evaluators

Custom event names are kept exactly as you pass them. Autocaptured events ($pageview, $click) are renamed before storage — see below.

5. Autocapture

Out of the box, the SDK captures:

  • Pageviews — initial load and SPA navigation (pushState, replaceState, popstate)
  • Clicks<a>, <button>, [role="button"], and any element with cursor: pointer

When a nested element is clicked (e.g. a <span> inside a <button>), the SDK walks up to the nearest interactive ancestor and captures that.

Human-readable event names

Raw autocapture events are automatically transformed into readable names before storage, so you don't have to grep through thousands of $click rows to find what you need:

Raw eventStored as
$pageview on /settings/integrationsviewed_settings_integrations
$pageview on /viewed_home
$click "Manage" under "PostHog" heading on /settingsclicked_posthog_manage_on_settings
$click "Save" on /settings/profileclicked_save_on_settings_profile

Click names pull context from the nearest heading or aria-label, so generic "Save" or "Manage" buttons are disambiguated automatically.

Properties captured

Pageviews:

PropertyExample
urlhttps://app.acme.com/dashboard
path/dashboard
titleDashboard — Acme
referrerhttps://app.acme.com/settings

Query parameters are stripped by default to avoid leaking tokens or PII.

Clicks:

PropertyExample
tagbutton
textUpgrade Plan (truncated to 255 chars)
href/pricing (links only)
idupgrade-btn (if present)
classesbtn btn-primary (if present)
path/settings/integrations
contextPostHog (nearest heading or aria-label)

Text from <input>, <textarea>, and password fields is never captured.

6. Opt elements out of capture

Add the class pai-no-capture to any element to exclude it and all its descendants from autocapture:

<div class="pai-no-capture">
  <!-- Nothing inside here will be captured -->
  <button>This click is not tracked</button>
</div>

For a full kill-switch, just don't call pai.identify() — the SDK stays inert and sends nothing.

7. Send events from your backend

The SDK POSTs batched events to the ingest endpoint. You can also hit it directly from your own server if you prefer not to use the SDK:

POST https://productanalyst.ai/api/events/ingest
Authorization: Bearer ppk_YOUR_PUBLIC_KEY

Payload format

{
  "events": [
    {
      "event": "upgrade_clicked",
      "userId": "user-42",
      "accountId": "acme-corp-123",
      "timestamp": "2026-01-15T10:00:00Z",
      "properties": { "plan": "pro", "annual": true },
      "traits": { "plan": "pro" },
      "id": "a1b2c3d4-e5f6-..."
    }
  ]
}
FieldTypeRequiredNotes
eventstringYesEvent name
userIdstringYesUser identifier
accountIdstringNoB2B account/org ID (set via pai.group() from the SDK)
timestampISO 8601NoDefaults to server receive time
propertiesobjectNoEvent-specific data
traitsobjectNoUser properties merged from identify()
idstringNoClient-generated UUID for deduplication — duplicates are silently ignored, so retries are safe

Maximum 100 events per request. From the SDK, events are flushed every 5 seconds or when the queue hits 10 events, whichever comes first. On page unload, remaining events are flushed via fetch with keepalive: true.

Internal metadata events

Two reserved event names, $identify and $group, travel through the same batch endpoint but are not stored in your event stream. The server diverts them into upserts against your users and accounts lists:

EventSourceRouted toNotes
$identifyEmitted by pai.identify()tracked_users upsertNever appears in company_events. Payload = the trait object.
$groupEmitted by pai.group()tracked_accounts upsertNever appears in company_events. Payload = the trait object.

If you're hitting the ingest endpoint directly from your own backend, you can post $identify / $group events with the same shape as regular events — the properties field carries the trait payload, and for $group, accountId is required.

Privacy

  • No cookies. The SDK stores nothing in the browser.
  • No anonymous tracking. Events are only sent after identify().
  • Query parameters stripped from captured URLs.
  • Input text never captured — inputs, textareas, and password fields are always ignored.
  • pai-no-capture to exclude sensitive UI regions.

Compatibility

Works in all modern browsers (Chrome, Firefox, Safari, Edge). Requires the fetch API. No dependencies. Runs alongside Mixpanel, PostHog, Amplitude, Segment, and any other analytics tool with no conflicts — it uses the pai global namespace.

API reference

MethodPurpose
pai.identify(userId, traits?)Activates the SDK, links events to a user, upserts the row in your users list (email + name as top-level columns, everything else into JSONB properties), and fires an initial $pageview. Required.
pai.group(accountId, traits?)Associates the current user with a B2B account and upserts the row in your accounts list (name + domain as top-level columns, everything else into JSONB properties). Pass null as the first argument to clear. Optional.
pai.track(eventName, properties?)Send a custom event with arbitrary properties.
pai.reset()Clear the user and account, flush pending events, return SDK to its inert state. Call on logout.
JavaScript SDK - Product Analyst AI