Python

Python 3.9+, standard library only, safe across a fork.

Every call appends to a bounded in-memory queue and returns: nothing raises into your code, nothing blocks your thread, and if firstrun is unreachable your program is unaffected. No dependencies, the transport is urllib.request.

Install the package

Terminal
pip install firstrun

Create the client

Python
import os
import firstrun

firstrun.configure(
    source_key=os.environ["FIRSTRUN_SOURCE_KEY"],   # fr_server_...
    host="https://app.firstrun.app",
    service_name="etl",
    persist_distinct_id=False,      # on a server the id belongs to the request
)

Before configure() every module-level function is a silent no-op, so a library that writes events costs nothing in a program that never configures a client. Calling configure() before a fork is fine: Gunicorn, uWSGI and Celery are handled by an os.register_at_fork handler that replaces the locks, starts a fresh sender and drops the inherited queue so parent and child do not both send it. The anonymous id survives, because a fork is not a second installation.

Write events

Python
firstrun.event(
    "order_placed",
    {"currency": order.currency, "total": order.total},
    distinct_id=request.session.session_key or "anon",
)

firstrun.error(exc, {"http.route": "/orders"})

firstrun.log({
    "name": "queue_depth",
    "severity": 9,
    "attributes": {"firstrun.metric": "queue_depth", "firstrun.value": depth},
})

firstrun.identify("acct_8812")   # sets user.id from here on
firstrun.shutdown(timeout=5)     # atexit already does this with a 3s budget

event writes at INFO and error at ERROR, both filling in the conventional attributes; log takes any name, any severity and any attributes. Values go on the wire as JSON, so an int stays a number and a bool stays a boolean. Pass time= for something you are recording after the fact. Calling any of these from a coroutine is fine: there is nothing to await on the hot path.

On a server, distinct_id is yours to supply. A server process serves thousands of different people and has nothing correct to default to, so pass an id you already have: a session key, a cookie, an account id. Leave it out and every event in your fleet collapses onto a handful of ids, and your unique counts become a count of your workers.