serla
Official Python SDK for Serla - privacy-focused product analytics for developers.
- Zero runtime dependencies (stdlib
urllibfor HTTP) - Class-based: instantiate as many clients as you need
- Background daemon-thread flusher with exponential-backoff retries
- Batched delivery with per-batch idempotency keys
- Graceful shutdown via
flush()/shutdown()/atexithook - Context-manager support for one-shot scripts
- Type hints throughout, Python 3.9+
Install
pip install serla
Quick start
from serla import Serla
serla = Serla(
api_key="sk_live_...",
# Optional - defaults shown
host="https://serla.dev",
flush_interval_seconds=5,
batch_size=50,
debug=False,
)
# Track an event
serla.track(
event="signup_completed",
distinct_id="user_123",
properties={"plan": "pro", "source": "organic"},
)
# Identify a user (synchronous - hits /api/v1/identify directly)
serla.identify(
"user_123",
properties={"email": "a@example.com", "plan": "pro"},
)
# Force a flush before shutdown
serla.flush()
serla.shutdown()
One-shot scripts: use the context manager
For cron jobs and one-off scripts, the with form auto-flushes on exit:
from serla import Serla
with Serla(api_key="sk_live_...") as serla:
serla.track(event="cron_finished", distinct_id="cron")
# shutdown() + flush() happen automatically when the block exits
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
api_key |
str |
(required) | Your project API key (sk_live_...). |
host |
str |
https://serla.dev |
Base URL of your Serla deployment. |
batch_size |
int |
50 |
Max events per flushed batch. Larger batches reduce network overhead. |
flush_interval_seconds |
float |
5.0 |
How often the background thread flushes the queue. |
debug |
bool |
False |
Sets the serla logger to DEBUG and attaches a default stderr handler. |
flush_on_exit |
bool |
True |
Register an atexit hook for best-effort flush on interpreter exit. |
API reference
Serla(api_key, *, host=..., flush_interval_seconds=..., batch_size=..., debug=..., flush_on_exit=...)
Construct a client. Raises ValueError if api_key is empty. Reuse the instance for the lifetime of the process.
serla.track(event, distinct_id, properties=None, timestamp=None)
Enqueue an event for asynchronous delivery. Non-blocking - returns immediately. The event is sent on the next flush tick or when the buffer fills up.
import datetime
serla.track(
event="order_placed",
distinct_id="user_123",
properties={"total_cents": 4900, "currency": "USD"},
timestamp=datetime.datetime.now(datetime.timezone.utc), # optional
)
distinct_id is required - there's no anonymous-ID fallback on the server. If you don't know the user yet, pass a stable system identifier (org ID, hashed IP, etc).
timestamp accepts a datetime (naive datetimes are assumed to be UTC) or a pre-formatted ISO-8601 string. If omitted, the current UTC time is used.
serla.identify(distinct_id, properties=None)
Set user properties for a distinct ID. POSTs synchronously to /api/v1/identify and returns when the response arrives. Failures are logged but never raised.
serla.identify(
"user_123",
properties={
"email": "a@example.com",
"plan": "pro",
"signed_up_at": "2025-01-01T00:00:00Z",
},
)
serla.flush(timeout=30.0)
Block until all currently-queued events have been delivered (or definitively failed and re-queued for retry). Pass timeout=None for an unbounded wait.
Call this before a serverless function returns so events aren't lost when the runtime freezes the process.
serla.shutdown(timeout=30.0)
Flush the queue, stop the worker thread, and unregister the atexit hook. Safe to call multiple times. After shutdown, track() and identify() calls are dropped (with a warning logged in debug mode).
serla.pending_count() -> int
Returns the number of events currently buffered. Useful for tests or "are we caught up?" health checks.
Context-manager protocol
with Serla(api_key="...") as serla:
serla.track(event="x", distinct_id="u")
# shutdown() is called automatically on block exit
Framework examples
Django
# settings.py
import os
SERLA_API_KEY = os.environ["SERLA_API_KEY"]
# analytics.py - one shared client for the whole process
from django.conf import settings
from serla import Serla
serla = Serla(api_key=settings.SERLA_API_KEY)
# views.py
from django.http import JsonResponse
from .analytics import serla
def signup(request):
user = create_user(request.POST)
serla.track(
event="signup_completed",
distinct_id=str(user.id),
properties={"plan": user.plan},
)
return JsonResponse({"ok": True})
Optional middleware idea - track every request:
# middleware.py
from .analytics import serla
class AnalyticsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.user.is_authenticated:
serla.track(
event="request_handled",
distinct_id=str(request.user.id),
properties={"path": request.path, "status": response.status_code},
)
return response
FastAPI
Use a lifespan handler to ensure events flush on shutdown:
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI
from serla import Serla
serla = Serla(api_key="sk_live_...")
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
serla.shutdown()
app = FastAPI(lifespan=lifespan)
def get_serla() -> Serla:
return serla
@app.post("/signup")
async def signup(user_id: str, serla: Serla = Depends(get_serla)):
serla.track(
event="signup_completed",
distinct_id=user_id,
properties={"source": "api"},
)
return {"ok": True}
Flask
from flask import Flask, request
from serla import Serla
app = Flask(__name__)
serla = Serla(api_key="sk_live_...")
@app.post("/signup")
def signup():
serla.track(
event="signup_completed",
distinct_id=request.json["user_id"],
properties={"plan": request.json.get("plan")},
)
return {"ok": True}
@app.teardown_appcontext
def _teardown(_exc):
# Optional: ensure pending events are flushed at the end of each request.
# In a long-running gunicorn worker, the periodic flusher will handle
# this on its own - you only need this for short-lived processes.
pass
import atexit
atexit.register(serla.shutdown)
AWS Lambda
from serla import Serla
# Module-level: reused across warm invocations.
serla = Serla(api_key="...")
def handler(event, context):
serla.track(
event="lambda_invoked",
distinct_id=event["user_id"],
properties={"region": "us-east-1"},
)
# CRITICAL: Lambda freezes the execution context when the handler returns.
# If you don't flush, events queued during this invocation sit in the
# buffer until the next invocation (or are lost if the container is
# recycled).
serla.flush()
return {"statusCode": 200}
One-shot script
from serla import Serla
with Serla(api_key="sk_live_...") as serla:
for record in batch_records:
serla.track(
event="record_processed",
distinct_id=record["user_id"],
properties={"size": record["size"]},
)
# All events flush automatically on context exit.
Plain script with atexit
If you can't use the context manager, the default flush_on_exit=True already registers an atexit hook so events flush on normal interpreter exit:
from serla import Serla
serla = Serla(api_key="sk_live_...")
serla.track(event="started", distinct_id="cron")
# Script exits, atexit hook flushes the queue.
Reliability
- Events are queued in memory and flushed every
flush_interval_seconds. - Forced flush when the buffer reaches
batch_size. - On flush failure, events are re-queued at the front of the buffer.
- Buffer is capped at 1000 events to prevent unbounded memory growth when the endpoint is down.
- Exponential backoff on failure: 1s, 2s, 4s, 8s, 16s, max 30s. The worker thread will not retry until the backoff window elapses, so a broken endpoint isn't hammered.
- Every batch carries an
X-Idempotency-Key(uuid4) header so the server can dedupe retried-and-eventually-succeeded batches. - All network I/O happens on a daemon thread. The thread is started in
__init__and stopped inshutdown(). Because it'sdaemon=True, it won't prevent the interpreter from exiting even ifshutdown()is never called.
Comparison
serla-python |
serla-node |
serla-js (browser) |
|
|---|---|---|---|
| Runtime | Python 3.9+ | Node 18+ | Browsers |
| Singleton | No (Serla(api_key=...)) |
No (new Serla({...})) |
Yes (Serla.init()) |
| Distinct ID | Required on every track | Required on every track | Auto-generated, localStorage |
| Session tracking | None | None | Auto, 30min inactivity |
| Page context | None | None | window.location, document |
| Async API | Blocking-on-flush | await flush() |
Fire-and-forget |
| Unload flush | atexit + daemon thread |
beforeExit hook |
navigator.sendBeacon |
| Worker delivery | threading.Thread(daemon) |
setInterval(...).unref() |
setInterval + sendBeacon |
| HTTP transport | urllib.request (stdlib) |
fetch (Node 18+) |
fetch / sendBeacon |
| Runtime deps | Zero | Zero | Zero |
License
MIT
Metadata
Release files for serla-py 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| serla_py-0.1.0.tar.gz | 12.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| serla_py-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.5 kB
Release files / serla_py-0.1.0.tar.gz
| Download URL | serla_py-0.1.0.tar.gz |
|---|---|
| Size | 12.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
46ed44acd5bb694d103daec1aa7e6ef3c4509794863472fed6d3f05645e6dd47
|
|
BLAKE2b-256 checksum How to use checksums |
407cfe9bc25dcafa94342e7654e748e0d2e5fce47a438f960bb0ee285c8409ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.5
|
Release files / serla_py-0.1.0-py3-none-any.whl
| Download URL | serla_py-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
afe62352fc745a058bbbd2d9d6eed6e3a906c154580057745fd3eba12318cf8f
|
|
BLAKE2b-256 checksum How to use checksums |
23837af3dbcf0c591c2b1716a116f09d58f8c5ad999e7dfabc66c2136d912894
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.5
|