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
- Create an account at serla.dev/signup
- Create a project and copy your API key from the dashboard
- Install the SDK or use the REST API directly
- 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
- Dashboard → Settings → API Keys (scoped to whichever project is selected in the sidebar).
- Click Create API key, give it a name (e.g.
Production server,Local dev). - Copy the full key from the dialog. It will not be shown again.
- 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_aton 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 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 install serla-js serla-js-react
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 install serla-node
import { Serla } from 'serla-node';const serla = new Serla({ apiKey: process.env.SERLA_API_KEY });// Next.js API routeexport async function POST(req) {const body = await req.json();serla.track({ name: 'signup_completed', distinctId: body.userId });await serla.flush(); // before serverless returnreturn Response.json({ ok: true });}
Python (serla-py)
Zero runtime deps (stdlib only). Background daemon thread, atexit hook for graceful shutdown.
pip install serla-py
from serla import Serlaserla = 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 github.com/b9llach/serla-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
| Method | Description |
|---|---|
| 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 volumeserla.track('page_view', { page: '/home' });serla.track('button_click', { button: 'cta' });// Immediate - for critical eventsawait 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 eventserla.track('button_click');// Event with propertiesserla.track('purchase', {properties: {product_id: 'prod_123',product_name: 'Pro Plan',price: 49.99,currency: 'USD'}});// Event with user IDserla.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, notbuttonClick - 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 identificationserla.identify('user_123', {email: 'john@example.com',name: 'John Doe'});// Full user profileserla.identify('user_123', {// Contact infoemail: 'john@example.com',name: 'John Doe',phone: '+1234567890',// Account infoplan: 'pro',created_at: '2024-01-15T10:30:00Z',// Company info (B2B)company: 'Acme Inc',company_size: '50-100',industry: 'Technology',// Custom propertieslifetime_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 viewserla.trackPageView();// With custom propertiesserla.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:
| Property | Description |
|---|---|
| $browser | Browser name |
| $os | Operating system |
| $device | Device type (desktop, mobile, tablet) |
| $country | Country code |
| $city | City name |
| $referrer | Referrer URL |
| $utm_source | UTM source |
| $utm_medium | UTM medium |
| $utm_campaign | UTM 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.
Manual Flush
// Send all queued events immediatelyawait 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
/eventsTrack 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"}
/events/batchTrack 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"]}
/identifyIdentify a user and set properties.
Request
{"distinctId": "user_123","properties": {"email": "john@example.com","name": "John Doe","plan": "pro"}}
/exportExport events as JSON or CSV.
Query Parameters
formatjson or csv (default: json)startDateISO 8601 dateendDateISO 8601 dateeventNameFilter by event namelimitMax results (default: 1000, max: 10000)/users/:distinctIdDelete 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
| Plan | Events/sec | Events/month |
|---|---|---|
| Free | 10 | 25,000 |
| Hobby | 50 | 500,000 |
| Pro | 200 | 2,500,000 |
| Max | 1000 | Unlimited |
Rate Limit Headers
X-RateLimit-Limit: 50X-RateLimit-Remaining: 49X-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
- Go to Dashboard > Settings > Webhooks
- Click "Add Webhook"
- Enter your endpoint URL
- Select events to receive
- 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
| Event | Description |
|---|---|
| event.created | New event tracked |
| user.identified | User identified |
| goal.completed | Goal conversion |
| threshold.exceeded | Custom 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
import { Serla } from 'serla-js';Serla.init({apiKey: 'sk_live_your_api_key',recordSessions: true,recordingOptions: {maskAllInputs: true, // default: trueblockClass: 'serla-no-record', // fully blocked from recordingignoreClass: '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_errorsflag 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
import { Serla } from 'serla-js';// Boolean flagconst enabled = await Serla.isFeatureEnabled('new-checkout-flow');if (enabled) showNewCheckout();// Multivariate flag - returns the variant key as a stringconst variant = await Serla.getFeatureFlag('button-color', 'control');if (variant === 'variant_a') renderRedButton();// All flags at onceconst 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
- If the flag is disabled, returns
false. - If any condition matches the user, returns
true(or the variant). - If the user's deterministic bucket is inside the rollout %, returns
true(or the variant). - 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:
- page_view (page=/pricing)
- signup_started
- signup_completed
- 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.
Country = United StatesAND Browser = ChromeAND 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
| Plan | Retention |
|---|---|
| Free | 7 days |
| Hobby | 60 days |
| Pro | 180 days |
| Max | 3 years |
User Opt-Out
// Check opt-out before initializingif (!localStorage.getItem('serla_optout')) {const serla = new Serla('sk_live_...');}// Opt-out function for privacy settingsfunction optOut() {localStorage.setItem('serla_optout', 'true');serla.reset();}
Error Handling
Error Codes
| Code | Status | Description |
|---|---|---|
| invalid_request | 400 | Malformed request body |
| unauthorized | 401 | Invalid or missing API key |
| forbidden | 403 | Key does not have permission |
| not_found | 404 | Resource not found |
| rate_limit_exceeded | 429 | Too many requests |
| internal_error | 500 | Server 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 runtimeserla.setDebug(true);