Skip to main content

serla

Official Python SDK for Serla - privacy-focused product analytics for developers.

  • Zero runtime dependencies (stdlib urllib for 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() / atexit hook
  • 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 in shutdown(). Because it's daemon=True, it won't prevent the interpreter from exiting even if shutdown() 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)

Source distribution for serla-py 0.1.0
File Size Uploaded
serla_py-0.1.0.tar.gz 12.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for serla-py 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page