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_startedsignup_completedcheckout_step_viewedbackend_job_completedinvite_sentreport_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:
| Option | Purpose |
|---|---|
apiHost | Ingest service URL. |
capturePageview | true, false, or "history_change" for browser apps with client-side routing. |
capturePageleave | true, false, or "if_capture_pageview". |
autocapture | Disabled by default. Pass an object to capture safe click/change/submit events. |
persistence | localStorage+cookie, localStorage, or memory. |
requestBatching | Enables batched SDK sends. |
flushAt | Queue size that triggers a flush. |
flushIntervalMs | Timed flush interval. |
beforeSend | Mutate or drop events before they enter the queue. |
propertyDenylist | Remove 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
}