Python SDK · Behaviors worth knowing

Empty action is a no-op

Events with no action set are dropped at send time. Useful when you want the middleware to attach an event to every request and let handlers conditionally opt out. Set action only when something audit-worthy happens.

Dropping an unnamed event is a deliberate opt-out for an outcome that is not audit-worthy, not a way to skip failures. The difference is whether an auditor would want to see the exit path.

A read that found nothing, or a malformed request that reached no resource, is not audit-worthy: leave action unset. That is what the handler below does on its 404.

A failed attempt at an audit-worthy action is the opposite. A denied authorization, a rejected payment, an invite that errored: all of those must be recorded, which means naming the event before the early return that handles them.

@app.get("/api/secrets/{secret_id}")
async def get_secret(secret_id: str):
    s = vault.get(secret_id)
    if s is None:
        return PlainTextResponse("not found", status_code=404)  # action never set -> dropped
    e = current_event()
    e.action = "secret.read"
    e.target = Target(type="secret", id=s.id)
    return JSONResponse(to_wire(s))

Naming an event whose target does not exist yet

When the target's ID only exists after the operation succeeds, name the event anyway and backfill the ID. Moving the naming below the error return is what silently drops the failure.

from everscribe.event import Target, current_event

@app.post("/api/members")
async def invite_member(body: InviteBody):
    e = current_event()
    e.action = "member.invited"
    e.target = Target(type="user")  # no ID yet, and that is fine

    try:
        u = await members.invite(body)
    except InviteError:
        # recorded, result filled by the middleware
        return PlainTextResponse("invite failed", status_code=500)
    e.target.id = u.id  # backfilled on the success path
    return JSONResponse(to_wire(u))

An event whose target has a type but no id is still a useful audit record: it says who tried to do what, and that it failed. An event that was never named says nothing at all.

The rule generalizes past target.id. Any field you cannot populate until the operation succeeds gets assigned after the error return, never used as a reason to delay naming the event.

record() is non-blocking

record() enqueues the event on the in-memory buffer and returns immediately; a background thread handles the HTTP call on the next flush. This is what makes it safe to call from async def handlers — the enqueue never touches the event loop. Don't await audit acks in critical paths; there's nothing to await, because the network call hasn't started yet.

import time

start = time.perf_counter()
rec.record(e)
print(time.perf_counter() - start)  # sub-millisecond — no network call

Caller-set fields take precedence

The middleware auto-populates result from the response status. If you set result yourself, that wins. Useful for anti-enumeration handlers that return the same HTTP status for different audit outcomes.

from everscribe.event import Result, Target

@app.get("/api/secrets/{secret_id}")
async def read(secret_id: str, request: Request):
    me = actor(request)
    e = current_event()
    e.action = "secret.reveal"
    e.target = Target(type="secret", id=secret_id)

    s = vault.get(secret_id)
    if s is None:
        return PlainTextResponse("not found", status_code=404)  # records result: error / 404
    if s.tenant_id != me.tenant_id:
        # Return 404 instead of 403 so attackers can't probe for secret IDs
        # in other tenants. The user-facing response can't tell the two cases
        # apart — but the audit log should.
        e.result = Result(status="denied", code=403, message="cross-tenant access")
        return PlainTextResponse("not found", status_code=404)
    return JSONResponse({"value": s.value})