Documentation

Complete guide to integrating Serla analytics into your application.

Getting Started

Serla is a privacy-focused analytics platform designed for developers. No cookies, no fingerprinting, no consent banners required.

Quick Start

  1. Create an account at serla.dev/signup
  2. Create a project and copy your API key from the dashboard
  3. Install the SDK or use the REST API directly
  4. Start tracking events with a single line of code

Your First Event

const serla = new Serla('sk_live_your_api_key');
serla.track('page_view', { page: '/home' });

Authentication

All API requests authenticate with a per-project API key. A single project can have many keys (e.g. one per environment or per app) so you can rotate without coordinating across services.

API Key Format

All keys are prefixed sk_live_ followed by a random suffix. The full key is shown once at creation. After that, the dashboard only displays the prefix.

Creating a Key

  1. Dashboard → Settings → API Keys (scoped to whichever project is selected in the sidebar).
  2. Click Create API key, give it a name (e.g. Production server, Local dev).
  3. Copy the full key from the dialog. It will not be shown again.
  4. Paste it into your SDK config or environment variable.

Using Your API Key

curl -X POST https://serla.dev/api/v1/events \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "signup_completed", "distinctId": "user_123"}'

Revoking a Key

Click Revoke next to any key on the API Keys page. Revocation is immediate — requests using that key start returning 401 Invalid API key the moment they reach the auth path. The row stays in the table (marked Revoked) for the audit trail.

Security Best Practices

  • Issue separate keys per environment. Revoking a leaked staging key shouldn't take down production.
  • Never commit keys to source control. Use environment variables and a secret manager.
  • The browser SDK uses a public-ish ingest key by design — events from the public web are expected. Don't expose admin-grade keys in browsers.
  • Rotate at a regular cadence: create a new key, deploy it, wait until last_used_at on the old key is stale, revoke.
  • A revoked key never re-activates. To restore service you must create a new one.

Installation

Serla ships five official SDKs distributed via the standard package registries. All speak the same HTTP API — mix and match (track from the browser and your server) and events show up in the same project.

Browser (serla-js)

npm
npm install serla-js
import { Serla } from 'serla-js';
Serla.init({
apiKey: 'sk_live_your_api_key',
autoPageviews: true,
errorTracking: true,
});
Serla.track('signup_completed', { plan: 'pro' });
Serla.identify('user_123', { email: 'a@example.com' });

React (serla-js-react)

Hooks and components on top of serla-js.

npm
npm install serla-js serla-js-react
App.tsx
import { SerlaProvider, useTrack, useIdentify } from 'serla-js-react';
export default function App({ user }) {
useIdentify(user?.id ?? null, user && { email: user.email });
const trackUpgrade = useTrack('plan_upgraded');
return <button onClick={() => trackUpgrade({ plan: 'pro' })}>Upgrade</button>;
}
// Mount at the root
<SerlaProvider config={{ apiKey: 'sk_live_your_api_key' }}>
<App />
</SerlaProvider>

Node.js (serla-node)

Server-side. Class-based instance, auto-flush on process.beforeExit, Edge-runtime compatible.

npm
npm install serla-node
server.js
import { Serla } from 'serla-node';
const serla = new Serla({ apiKey: process.env.SERLA_API_KEY });
// Next.js API route
export async function POST(req) {
const body = await req.json();
serla.track({ name: 'signup_completed', distinctId: body.userId });
await serla.flush(); // before serverless return
return Response.json({ ok: true });
}

Python (serla-py)

Zero runtime deps (stdlib only). Background daemon thread, atexit hook for graceful shutdown.

pip
pip install serla-py
app.py
from serla import Serla
serla = Serla(api_key="sk_live_your_api_key")
serla.track(
event="signup_completed",
distinct_id="user_123",
properties={"plan": "pro"},
)
# Context manager auto-flushes on exit (one-shot scripts):
with Serla(api_key="...") as s:
s.track(event="batch_job_done", distinct_id="cron")

Go (serla-go)

Zero runtime deps, channel-based queue, context-aware. Requires Go 1.21+.

go get
go get github.com/b9llach/serla-go
main.go
import serla "github.com/b9llach/serla-go"
client, _ := serla.New(serla.Config{
APIKey: "sk_live_your_api_key",
})
defer client.Shutdown(context.Background())
client.Track(serla.Event{
Name: "signup_completed",
DistinctID: "user_123",
Properties: map[string]any{"plan": "pro"},
})

SDK Configuration

Customize SDK behavior with configuration options.

const serla = new Serla('sk_live_your_api_key', {
// API endpoint (default: https://serla.dev/api/v1)
endpoint: 'https://serla.dev/api/v1',
// Enable debug logging (default: false)
debug: true,
// Batch size before auto-flush (default: 10)
batchSize: 10,
// Flush interval in ms (default: 5000)
flushInterval: 5000,
// Auto-track page views (default: false)
autoTrackPageViews: true,
// Respect Do Not Track header (default: true)
respectDoNotTrack: true,
// Session timeout in minutes (default: 30)
sessionTimeout: 30
});

SDK Methods

MethodDescription
track(name, options?)Queue event for batched sending
send(name, options?)Send event immediately (no batching)
identify(userId, props?)Identify a user
trackPageView(props?)Track a page view
flush()Send all queued events
reset()Clear user identity and session
getSessionId()Get current session ID
getUserId()Get current user ID
setDebug(enabled)Enable/disable debug mode

track() vs send()

track()

Queues events locally and sends them in batches. More efficient for high-volume tracking. Events are sent when the batch size is reached (default: 10) or flush interval elapses (default: 5 seconds).

send()

Sends a single event immediately without batching. Use for critical events that must be recorded instantly, like purchases or signups.

// Batched - efficient for high volume
serla.track('page_view', { page: '/home' });
serla.track('button_click', { button: 'cta' });
// Immediate - for critical events
await serla.send('purchase', {
distinctId: 'user_123',
properties: { revenue: 99.99 }
});

Tracking Events

Track any user action with custom properties. Events are the core of Serla analytics.

Basic Event

// Simple event
serla.track('button_click');
// Event with properties
serla.track('purchase', {
properties: {
product_id: 'prod_123',
product_name: 'Pro Plan',
price: 49.99,
currency: 'USD'
}
});
// Event with user ID
serla.track('signup', {
distinctId: 'user_456',
properties: {
plan: 'pro',
source: 'google_ads'
}
});

Revenue Tracking

Serla automatically recognizes and aggregates these revenue properties: revenue, amount, value, price

serla.track('purchase_completed', {
distinctId: 'user_123',
properties: {
revenue: 149.99,
quantity: 2,
product: 'Pro Plan',
currency: 'USD'
}
});

Tracking Best Practices

  • Use snake_case for event names: button_click, not buttonClick
  • Be consistent with naming across your app
  • Keep properties flat - avoid deeply nested objects
  • Use standard names for common events: signup, login, purchase

Identifying Users

Associate events with a user to track them across sessions and devices.

// Basic identification
serla.identify('user_123', {
email: 'john@example.com',
name: 'John Doe'
});
// Full user profile
serla.identify('user_123', {
// Contact info
email: 'john@example.com',
name: 'John Doe',
phone: '+1234567890',
// Account info
plan: 'pro',
created_at: '2024-01-15T10:30:00Z',
// Company info (B2B)
company: 'Acme Inc',
company_size: '50-100',
industry: 'Technology',
// Custom properties
lifetime_value: 499.99
});

Reset Identity

Clear user identity on logout:

serla.reset();
// Generates new anonymous session ID

Sessions & Page Views

Sessions are created automatically and track user activity over time.

How Sessions Work

  • Sessions are created automatically on first event
  • Sessions expire after 30 minutes of inactivity (configurable)
  • Session ID format: sess_abc123...
  • New session created after timeout, reset(), or new browser tab

Tracking Page Views

// Basic page view
serla.trackPageView();
// With custom properties
serla.trackPageView({
title: 'Pricing Page',
section: 'marketing'
});

SPA Integration

import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
serla.trackPageView();
}, [location.pathname]);
}

Properties & Schema

Understanding the data schema and auto-enriched properties.

Reserved Properties

These properties are auto-detected from the request:

PropertyDescription
$browserBrowser name
$osOperating system
$deviceDevice type (desktop, mobile, tablet)
$countryCountry code
$cityCity name
$referrerReferrer URL
$utm_sourceUTM source
$utm_mediumUTM medium
$utm_campaignUTM campaign

Property Limits

  • Max 100 properties per event
  • Property names: max 100 characters
  • String values: max 1000 characters
  • Arrays: max 100 items
  • Nested objects: max 3 levels deep

Batch Events

Events are automatically batched for efficiency. The queue flushes when batch size is reached, interval elapsed, or manually.

i
Automatic batching: Events queue locally and flush every 5 seconds or when 10 events are queued, whichever comes first.

Manual Flush

// Send all queued events immediately
await serla.flush();
// Flush before page unload (handled automatically)
window.addEventListener('beforeunload', () => {
serla.flush();
});

API Batch Endpoint

curl -X POST https://serla.dev/api/v1/events/batch \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"events": [
{ "name": "page_view", "properties": { "page": "/home" } },
{ "name": "button_click", "properties": { "button": "cta" } }
]
}'

API Reference

Base URL: https://serla.dev/api/v1

POST/events

Track a single event.

Request

{
"name": "purchase",
"distinctId": "user_123",
"sessionId": "sess_abc",
"timestamp": "2024-01-15T10:30:00Z",
"properties": {
"product_id": "prod_123",
"price": 49.99
}
}

Response

{
"success": true,
"eventId": "evt_abc123",
"sessionId": "sess_abc"
}
POST/events/batch

Track multiple events (max 100 per request).

Request

{
"events": [
{ "name": "page_view", "properties": { "page": "/home" } },
{ "name": "button_click", "properties": { "button": "cta" } }
]
}

Response

{
"success": true,
"count": 2,
"eventIds": ["evt_abc", "evt_def"]
}
POST/identify

Identify a user and set properties.

Request

{
"distinctId": "user_123",
"properties": {
"email": "john@example.com",
"name": "John Doe",
"plan": "pro"
}
}
GET/export

Export events as JSON or CSV.

Query Parameters

formatjson or csv (default: json)
startDateISO 8601 date
endDateISO 8601 date
eventNameFilter by event name
limitMax results (default: 1000, max: 10000)
DELETE/users/:distinctId

Delete all data for a user (GDPR right to deletion).

Example

curl -X DELETE https://serla.dev/api/v1/users/user_123 \
-H "Authorization: Bearer sk_live_..."

Rate Limits

Limits by Plan

PlanEvents/secEvents/month
Free1025,000
Hobby50500,000
Pro2002,500,000
Max1000Unlimited

Rate Limit Headers

X-RateLimit-Limit: 50
X-RateLimit-Remaining: 49
X-RateLimit-Reset: 1705312800

Handling 429 Errors

async function trackWithRetry(event, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await serla.track(event);
} catch (error) {
if (error.status === 429) {
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
} else {
throw error;
}
}
}
}

Webhooks

Receive real-time notifications when events occur.

Setup

  1. Go to Dashboard > Settings > Webhooks
  2. Click "Add Webhook"
  3. Enter your endpoint URL
  4. Select events to receive
  5. Copy the signing secret

Webhook Payload

{
"id": "wh_abc123",
"type": "event.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"event": {
"id": "evt_xyz",
"name": "purchase",
"distinctId": "user_123",
"properties": { "price": 49.99 }
}
}
}

Webhook Events

EventDescription
event.createdNew event tracked
user.identifiedUser identified
goal.completedGoal conversion
threshold.exceededCustom alert triggered

Signature Verification

const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

Session Replay

Capture every interaction on your app (clicks, scrolls, typing, navigation) and replay it as a video in the dashboard. Built on rrweb — masks input values by default so PII never leaves the browser. Hobby plan and above.

Enable Recording

Serla.init
import { Serla } from 'serla-js';
Serla.init({
apiKey: 'sk_live_your_api_key',
recordSessions: true,
recordingOptions: {
maskAllInputs: true, // default: true
blockClass: 'serla-no-record', // fully blocked from recording
ignoreClass: 'serla-no-record-events', // layout kept, interactions dropped
},
});

rrweb is dynamic-imported, so users who don't opt in don't pay the ~80 KB bundle cost.

What's Captured

  • DOM mutations, mouse moves, clicks, scroll position
  • Typing (input values are masked by default)
  • Console errors and unhandled rejections — these flip the has_errors flag on the recording so you can find error sessions fast

View Recordings

Dashboard → Replays (under PRODUCT in the sidebar) lists every session with the start URL, duration, distinct_id, browser/OS, and an errors badge if applicable. Click a row to open the player with timeline scrubbing and playback speed control.

Feature Flags

Toggle features on or off, run gradual rollouts, or A/B test variants. Evaluation is deterministic — the same user always gets the same value as long as the rollout doesn't shrink past their bucket. Hobby plan and above.

Read a Flag from the SDK

flags.js
import { Serla } from 'serla-js';
// Boolean flag
const enabled = await Serla.isFeatureEnabled('new-checkout-flow');
if (enabled) showNewCheckout();
// Multivariate flag - returns the variant key as a string
const variant = await Serla.getFeatureFlag('button-color', 'control');
if (variant === 'variant_a') renderRedButton();
// All flags at once
const flags = await Serla.getAllFeatureFlags();

The SDK caches flag values for 30 seconds. Calling identify() or reset() invalidates the cache so a logged-in user immediately sees flags evaluated against their new distinct_id.

Evaluation Order

  1. If the flag is disabled, returns false.
  2. If any condition matches the user, returns true (or the variant).
  3. If the user's deterministic bucket is inside the rollout %, returns true (or the variant).
  4. Otherwise, returns false.

Create a Flag

Dashboard → Flags → New flag. Set a key (stable identifier used in code), name, rollout %, and optional variants. Variants' weights must sum to 100.

LLM Observability

Track every prompt, completion, token count, cost, and latency from your LLM calls. Works with any provider (OpenAI, Anthropic, Gemini, Mistral, your own model) — the data shape is generic. Hobby plan and above.

Track a Generation

import { Serla } from 'serla-node';
const serla = new Serla({ apiKey: process.env.SERLA_API_KEY });
const startedAt = Date.now();
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello' }],
});
await serla.trackLLM({
model: 'gpt-4o',
provider: 'openai',
distinctId: 'user_123',
input: messages,
output: response.choices[0].message,
inputTokens: response.usage.prompt_tokens,
outputTokens: response.usage.completion_tokens,
latencyMs: Date.now() - startedAt,
});

Cost Backfill

If you don't supply costUsd, the server computes it from inputTokens + outputTokens against a built-in pricing table for known models (Claude, GPT, Gemini, Mistral). For unknown or finetuned models, set costUsd explicitly.

Tracing Chains

For multi-step calls (agent loops, RAG retrieval + answer), set traceId and parentId on each generation so the dashboard links them together.

Error Tracking

Capture server-side and client-side exceptions, group them by deterministic stack-trace fingerprint, and triage in the dashboard. Hobby plan and above.

Capture an Exception

import { Serla } from 'serla-js';
try {
await checkout();
} catch (err) {
Serla.captureException(err, { context: 'checkout', orderId });
}

Fingerprinting & Grouping

Errors are grouped by SHA-256 of (error type, top-of-stack file, top-of-stack function). Cosmetic refactors don't fragment groups. Minified chunk hashes (page-abc123.js) are stripped before hashing. If a resolved error fingerprint reoccurs, the dashboard auto-reopens it.

Triage in Dashboard

Dashboard → Errors lists groups with tabs for Unresolved / Resolved / Ignored / All. Click a group to see the stack trace, browser/URL breakdown, and recent occurrences. Resolve, ignore, or reopen from the detail page.

Dashboard Features

Funnels

Create conversion funnels to see where users drop off.

Example funnel:

  1. page_view (page=/pricing)
  2. signup_started
  3. signup_completed
  4. purchase

Goals

Track conversion events and assign monetary values.

  • Event-based: Track any event as a conversion
  • Pageview-based: Track specific page visits
  • Revenue-based: Track events with revenue properties

Retention

Analyze user retention with cohort analysis.

  • Group users by signup date
  • View retention by day, week, or month
  • Define custom retention events

Segments

Create saved segments for filtering data.

Example Segment
Country = United States
AND Browser = Chrome
AND Plan = pro

Attribution

Understand how users find you with attribution models.

  • First-touch: Credit first interaction
  • Last-touch: Credit last interaction before conversion
  • Linear: Equal credit to all touchpoints
  • Time-decay: More credit to recent touchpoints

Journeys

Analyze user navigation patterns through your site.

  • User paths: See top navigation patterns
  • Drop-off points: Identify where users leave
  • Session analysis: Understand user flow

Built automatically from page views. Enable autoTrackPageViews: true or call trackPageView().

Privacy & Compliance

No Cookies

Serla does not use cookies. Session tracking uses in-memory storage or optional localStorage.

No Fingerprinting

We never fingerprint users. Identification is explicit via identify() or session-based for anonymous users.

IP Addresses

IPs are used for geolocation only and are never stored. Geolocation is resolved at ingestion time.

Data Retention

PlanRetention
Free7 days
Hobby60 days
Pro180 days
Max3 years

User Opt-Out

// Check opt-out before initializing
if (!localStorage.getItem('serla_optout')) {
const serla = new Serla('sk_live_...');
}
// Opt-out function for privacy settings
function optOut() {
localStorage.setItem('serla_optout', 'true');
serla.reset();
}

Error Handling

Error Codes

CodeStatusDescription
invalid_request400Malformed request body
unauthorized401Invalid or missing API key
forbidden403Key does not have permission
not_found404Resource not found
rate_limit_exceeded429Too many requests
internal_error500Server error

SDK Error Handling

try {
await serla.track('event');
} catch (error) {
if (error.code === 'rate_limit_exceeded') {
// Wait and retry
} else if (error.code === 'unauthorized') {
// Check API key
} else {
console.error('Serla error:', error.message);
}
}

Debug Mode

const serla = new Serla('sk_live_...', { debug: true });
// Or toggle at runtime
serla.setDebug(true);