ClickHouse Product Analytics
Start

Sending Events

Browser SDK, direct API, batch API, and event naming examples.

Events are JSON objects with an event name, a distinct ID, optional timestamp, and properties. Browser apps usually use the SDK or React wrapper. Backend services can send the same event shape directly to the HTTP API.

Event Naming

Use stable, human-readable event names that describe behavior. Good names are specific enough to query and stable enough to become part of your analytics contract:

  • signup_started
  • signup_completed
  • checkout_step_viewed
  • backend_job_completed
  • invite_sent
  • report_exported

Use properties for context, not for the primary action:

{
  "event": "backend_job_completed",
  "properties": {
    "job_id": "job_456",
    "duration_ms": 481,
    "status": "success"
  }
}

Browser SDK

For exact method and option signatures, see the SDK reference.

import analytics from '@clickhouse-product-analytics/sdk'

analytics.init({
  apiHost: 'http://127.0.0.1:8080',
  capturePageview: 'history_change',
  autocapture: {
    captureText: true,
    elementAllowlist: ['button', 'a']
  },
  propertyDenylist: ['secret']
})

analytics.capture('signup_started', {
  plan: 'pro',
  source: 'pricing_page'
})

await analytics.flush()

Important options:

OptionPurpose
apiHostIngest service URL.
capturePageviewtrue, false, or "history_change" for browser apps with client-side routing.
capturePageleavetrue, false, or "if_capture_pageview".
autocaptureDisabled by default. Pass an object to capture safe click/change/submit events.
persistencelocalStorage+cookie, localStorage, or memory.
requestBatchingEnables batched SDK sends.
flushAtQueue size that triggers a flush.
flushIntervalMsTimed flush interval.
beforeSendMutate or drop events before they enter the queue.
propertyDenylistRemove sensitive properties before sending.

Pageviews

For single-page applications, use capturePageview: "history_change" so route changes emit $pageview:

analytics.init({
  apiHost: 'https://analytics.example.com',
  capturePageview: 'history_change'
})

The SDK promotes URL fields into ClickHouse columns:

  • $current_url -> events.current_url
  • $host -> events.host
  • $session_id -> events.session_id
  • $window_id -> events.window_id

Autocapture

Autocapture is opt-in. Keep it narrow and explicit:

analytics.init({
  apiHost: 'https://analytics.example.com',
  autocapture: {
    captureText: true,
    elementAllowlist: ['button', 'a'],
    domEventAllowlist: ['click', 'submit']
  }
})

The SDK avoids hidden/password inputs, common payment and secret field names, credit-card-like values, SSN-like values, and elements marked with cpa-sensitive or cpa-no-capture.

Event Hooks and Privacy

Use beforeSend to enforce a local event contract:

analytics.init({
  apiHost: 'https://analytics.example.com',
  beforeSend: (event) => {
    if (event.event === 'debug_event') {
      return undefined
    }

    event.properties.app_version = '1.2.3'
    return event
  },
  propertyDenylist: ['token', 'secret', 'password']
})

Use opt-out helpers for consent flows:

analytics.opt_out_capturing()
analytics.has_opted_out_capturing()
analytics.opt_in_capturing()

Direct API

For exact endpoint, payload, response, and error details, see the API reference.

Backend services send JSON batch payloads to POST /batch/. A single event is sent as a one-item batch:

curl -X POST http://127.0.0.1:8080/batch/ \
  -H 'content-type: application/json' \
  -d '{
    "api_key": "local_dev_key",
    "batch": [
      {
        "event": "backend_job_completed",
        "distinct_id": "user_123",
        "properties": {
          "job_id": "job_456",
          "duration_ms": 481,
          "status": "success"
        }
      }
    ]
  }'

Batch API

Use the same envelope for multiple events:

curl -X POST http://127.0.0.1:8080/batch/ \
  -H 'content-type: application/json' \
  -d '{
    "api_key": "local_dev_key",
    "batch": [
      {
        "event": "$pageview",
        "distinct_id": "anon_123",
        "properties": {
          "$current_url": "https://example.com/",
          "$host": "example.com",
          "$session_id": "session_123"
        }
      },
      {
        "event": "checkout_step_viewed",
        "distinct_id": "anon_123",
        "properties": {
          "step": "payment",
          "$session_id": "session_123"
        }
      }
    ]
  }'

Compressed Requests

The ingest service accepts gzip-compressed JSON request bodies with Content-Encoding: gzip.

Example:

import { gzipSync } from 'node:zlib'

await fetch('http://127.0.0.1:8080/batch/', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'content-encoding': 'gzip'
  },
  body: gzipSync(JSON.stringify({
    api_key: 'local_dev_key',
    batch: [
      {
        event: 'backend_job_completed',
        distinct_id: 'user_123',
        properties: { job_id: 'job_456' }
      }
    ]
  }))
})

Invalid Events

The service rejects malformed requests such as invalid timestamps, mixed API keys in one request, disallowed origins, invalid keys, missing keys on no-origin backend requests, oversized bodies, and oversized batches.

Inside an otherwise valid batch, events missing an event name or distinct ID are dropped and counted in the response:

{
  "status": "ok",
  "ingested": 2,
  "dropped": 1
}

On this page