Monitor  /  SDK

Monitor SDK

The SDK does three things: it reports each named feature run with its outcome and duration, it captures errors (with stack, breadcrumbs and active flags where the platform supports it), and it batches everything to Onelo in the background. You read the result in Feature Health. Pick your platform below.

Install & integrate

These are the exact snippets shown on your dashboard's SDK → Monitor tab — the same source, so they never drift. Every platform below is a complete, fully-tested integration.

① Clientfrontend

Install

// Xcode → File → Add Package Dependencies: // https://github.com/onelo-tools/onelo-swift

Initialize

/*
 * PLACEMENT: Initialize Onelo ONCE for the lifetime of the app.
 *   • SwiftUI: hold it as @StateObject on your @main App struct, then inject
 *     via .environmentObject(onelo) — see example below.
 *   • AppKit/UIKit: create in AppDelegate and expose as a singleton.
 * Never create inside a View — SwiftUI recreates views frequently.
 *
 * AUTOMATIC ERROR CAPTURE: NSException handlers + MetricKit (crashes,
 * hangs, CPU exceptions) are installed at init. Stack traces, breadcrumbs,
 * device context, and feature-flag state are auto-attached to every error.
 * No setup required. NOTE: MetricKit delivers reports daily, on real
 * devices only — crashes are NOT visible in the iOS Simulator and may
 * take up to 24h to arrive in production.
 *
 * LIFECYCLE: The SDK flushes every 15 seconds. Errors flush immediately.
 *   • macOS: call destroy() in applicationWillTerminate.
 *   • iOS:   applicationWillTerminate fires unreliably — also flush on
 *            UIApplication.willResignActiveNotification or ScenePhase
 *            .background so events aren't lost when the app is suspended.
 *   func applicationWillTerminate(_ notification: Notification) {
 *     onelo.monitor.destroy()   // invalidates flush timer + triggers a
 *                                // final flush; HTTP send is fire-and-forget
 *                                // (does NOT block termination). Force-quit
 *                                // / system shutdown may drop the last batch.
 *                                // Idempotent — safe to call from multiple
 *                                // lifecycle hooks; second call is a no-op.
 *   }
 *
 * MANUAL FLUSH: call onelo.monitor.flush() before a known risky moment
 * (e.g. just before app-initiated quit, or after a critical user action
 * you want to ship immediately). Safe to call from any thread; non-blocking.
 *
 * THREAD SAFETY: All public methods (event, track, capture, breadcrumb,
 * flush, setUserId) are safe to call from ANY thread. Internally the SDK
 * uses a serial queue — no need to wrap calls in MainActor.run { }.
 *
 * HOW IT WORKS: Events are buffered in memory (max 200, oldest dropped on
 * overflow) and sent to Onelo in a single HTTP batch.
 *
 * DELIVERY IS RE-QUEUED, ONCE PER FLUSH: exactly ONE request goes out per flush.
 * A batch the server did not accept — network failure, 5xx, or a 429 — goes back
 * in the buffer and rides the next 15s tick. The flush timer IS the retry, so a
 * backend outage never multiplies the requests your users' apps send (an
 * in-flight retry loop would triple them at the worst possible moment). A 429
 * additionally holds off the next flushes for its Retry-After. Only a permanent
 * rejection (4xx other than 429 — bad key, malformed payload) is dropped, and it
 * is logged with its status rather than discarded quietly.
 *
 * WHAT IS STILL LOST: retry is IN-MEMORY only — nothing is persisted to disk, so
 * events still buffered when the process dies are gone. The buffer is capped (200
 * events); during a long outage the OLDEST re-queued events are dropped first so
 * live telemetry keeps flowing, and every drop is counted in a log line. So
 * Feature Health is a strong signal, not an audit log: use it to spot what is
 * failing, never as the record of truth for billing, compliance or anything that
 * must be complete.
 *
 * SESSION: the session ID is install-bound — it persists across launches and
 * resets only on app uninstall.
 *
 * PRIVACY: Sensitive keys (password, token, authorization, cookie, etc.)
 * and patterns (Bearer tokens, JWT, Stripe keys, credit-card numbers) are
 * automatically redacted from `error` strings and `meta` payloads before
 * leaving the device.
 *
 * BASELINE EVENT: the SDK emits one 'session_opened' event automatically at
 * init, unconditionally — before any auth/paywall/consent gate you add. This
 * guarantees Feature Health is never truly empty, but it does NOT solve the
 * gating trap (see STEP 0 below) for the events YOU add: those still only
 * fire once a user gets past your own gates.
 */
import OneloSwift

// SwiftUI setup — @StateObject ensures the same instance survives view rebuilds.
@main
struct MyApp: App {
  // callbackScheme is your app's custom URL scheme — used by Onelo Auth to
  // return from the hosted sign-in page (e.g. "myapp://auth-callback").
  // The Monitor SDK itself does not use it; safe to pass any unique string
  // if you don't use Onelo Auth, but it must match your Info.plist scheme.
  @StateObject private var onelo = Onelo(
    publishableKey: "onelo_pk_live_YOUR_KEY",
    callbackScheme: "myapp",
    baseURL: URL(string: "https://api.onelo.tools")!
  )
  var body: some Scene {
    WindowGroup { ContentView().environmentObject(onelo) }
  }
}

Usage

// ─── STEP 0 — INVENTORY BEFORE YOU INSTRUMENT (do this first) ────────────────
// Do NOT write a single track()/event() call until you have listed the app's
// features. Instrumenting the parts you happened to read produces a dashboard
// that looks healthy because the broken half is not being measured.
//
// THE GATING TRAP — read this before you enumerate anything. If sign-in,
// paywall or the consent gate stands in front of EVERY feature you instrument,
// a cold start can emit ZERO events: a user stuck at sign-in never reaches any
// of the operations you measured. You can satisfy the coverage rule below
// perfectly — every feature instrumented, every skip justified — and still ship
// an app that looks completely unintegrated on Feature Health for as long as a
// user hasn't gotten past the gate. The fix is not "instrument more" — it's
// making sure at least ONE event can fire BEFORE any gate: an app-open ping at
// startup, or the gate's own attempt/success/failure (sign-in attempted,
// paywall shown, consent accepted/declined). Keep that in mind through step 1.
//
// 1. ENUMERATE. Walk the codebase and list every feature a user can trigger
//    end-to-end. A feature = a user-initiated operation with an OUTCOME that
//    can succeed or fail. Cover, at minimum:
//      • every entry point in the UI (button, menu item, form submit, shortcut)
//      • every route / screen the user can reach
//      • everything that crosses a boundary: network, filesystem, storage,
//        clipboard, subprocess, native bridge, third-party SDK
//      • startup and teardown paths (restore session, load state, migrate data)
//      • flows the Onelo SDK itself presents: sign-in, store, customer portal,
//        consent gate, feedback. The SDK renders them, but they fail inside YOUR
//        app — a user who cannot get past them has no working product. "The SDK
//        handles it" is not a reason to leave them unmeasured. These are also
//        your best candidate for an event that fires BEFORE the gating trap
//        above bites: instrument the attempt, not just the eventual success.
//
// 2. FIND THE SILENT FAILURES. Grep for swallowed errors: an empty catch block,
//    a catch that returns null / [] / a default, a ?? fallback that hides a
//    failed read, and fire-and-forget calls (void somePromise(),
//    .catch(() => {})). These are the highest-value events in any codebase —
//    they are exactly the failures that reach the user and reach nobody else.
//    Instrument them FIRST.
//
// 3. WRITE THE TABLE. Before coding, output one row per feature:
//      feature | file | event name | track() or event() | covered / skipped + why
//    Show it to the developer. A skipped feature must say WHY it was skipped
//    (see WHAT NOT TO INSTRUMENT below) — an unmeasured feature and a
//    deliberately-excluded one must never look the same in your report.
//
// 4. THEN implement, and keep the table in the PR description or a comment.
//
// 5. VERIFY. After implementing, actually trigger one instrumented feature,
//    wait ~15s (the flush interval — see below), reload the Onelo dashboard's
//    Feature Health tab and confirm a row appears for it. "The code compiles
//    and calls event()" is not verification; a row on the dashboard is.
//
// COVERAGE RULE: every feature in the table is either instrumented, or listed
// with a stated reason. "I did not get to it" is not a reason.

// IDENTIFY YOUR USER
// If using Onelo Auth — user_id is set automatically, nothing to do.
// If using your own auth system:
// onelo.monitor.setUserId("user-123")  // after login
// onelo.monitor.setUserId(nil)          // after logout
//
// STARTUP IDENTITY RACE (Onelo Auth users — read this once):
// Onelo Auth restores the session from Keychain ASYNCHRONOUSLY at startup
// (~hundreds of ms). Events emitted BEFORE auth becomes ready will ship with
// user_id="anonymous" — even for users who are actually signed in. This is
// not a bug; it's the cost of not blocking startup on disk I/O.
//
// If you need accurate user attribution on startup events, await readiness:
//
//   try? await onelo.auth.awaitReady()  // default 5s timeout; no throw on time
//   onelo.monitor.event("app_launched", options: .init(ok: true))
//
// Or gate non-critical startup events on onelo.auth.currentSession != nil
// and accept that pre-restore events are anonymous.

// AUTO-TRACK (sync) — track() re-throws, wrap in do/catch.
// On error: stack trace, breadcrumbs, and active feature-flag state are
// auto-attached to the event.
do {
  let result = try onelo.monitor.track("checkout", meta: [
    "plan": user.plan,          // correlate errors with plan types
    "method": "stripe",         // which provider / path
  ]) {
    try processPayment()
  }
} catch {
  // The thrown error is already recorded in Onelo before being re-thrown:
  //   • error string  → localizedDescription
  //   • errorType     → String(describing: type(of: error)) (e.g. "URLError")
  //   • stack + breadcrumbs + active flags are auto-attached
  // Handle UI feedback here.
}

// AUTO-TRACK (async)
do {
  let data = try await onelo.monitor.track("ai_response", meta: [
    "model": "gpt-4",
  ]) {
    try await fetchAI()
  }
} catch { }

// INSTANTANEOUS EVENT: use event() only for things with no duration.
onelo.monitor.event("tab_viewed", options: MonitorEventOptions(
  ok: true,
  meta: ["tab": "export"]
))

// ERROR WITHOUT track()
onelo.monitor.event("ai_stream", options: MonitorEventOptions(
  ok: false,
  error: "timeout",
  meta: ["model": "gpt-4", "failedAt": "stream_start"]
))

// MANUAL ERROR CAPTURE: full stack trace + breadcrumbs + flag state attached.
// Use when you've already caught an error outside of track() and want full context.
do {
  try riskyOperation()
} catch {
  onelo.monitor.capture(error, featureName: "checkout")
}

// BREADCRUMBS: optional trail of context attached to the next captured error.
// Up to 100 most-recent breadcrumbs are kept. Devs can reconstruct what
// happened before failure.
onelo.monitor.breadcrumb(info: "user clicked Pay")
onelo.monitor.breadcrumb(navigationTo: "CheckoutScreen")

// RECIPE — drop a breadcrumb right BEFORE a risky operation so it lands
// in the error context if the operation fails:
onelo.monitor.breadcrumb(info: "starting ai_stream, model=gpt-4")
try await onelo.monitor.track("ai_stream") { try await streamAI() }

// AUTO-TRACE HTTP: every request on this session becomes a breadcrumb.
// Sensitive headers (Authorization, Cookie, X-API-Key) and query params
// (token, key, secret) are scrubbed before storage.
// LIFETIME: urlSession() returns a NEW URLSession on every call. Create it
// once at app startup (e.g. lazy property on AppDelegate or a service object)
// and reuse it for all requests — do NOT call urlSession() per-request.
let session = onelo.monitor.urlSession()
let (data, _) = try await session.data(from: url)

// FEATURE FLAGS: reading a flag (onelo.features.feature("x").isEnabled) does NOT
// emit a monitor event by itself. Instead, the active flag snapshot is attached
// to any error captured afterwards — so you can answer "is this error
// correlated with flag X being enabled?" without instrumenting reads.

// ─── NAMING CONVENTION ───────────────────────────────────────────────────────
// Use snake_case: {object}_{past_tense_verb}
// Object  → what the user acts on: checkout, pdf_export, ai_response, sync
// Verb    → past tense, result-oriented: completed, failed, started, retried
//
// Good:  checkout  pdf_export  ai_response  onboarding  sync  tab_viewed
// Bad:   checkout_completed  export_error  doSync  exportClicked
//
// Name the FEATURE, not the outcome — track() records ok/error/duration automatically.
// Use _verb suffix only for event() calls (instantaneous, no duration):
//   tab_viewed  button_clicked  plan_selected  modal_dismissed
//
// Avoid _started / _completed suffixes — use track() to wrap the operation instead.
//
// PITFALL — check-style events: don't name an event after its failure mode.
// The name shows up in Feature Health even on success, so failure-mode names
// read like permanent alerts.
//   ✗ permission_missing, binary_missing, network_unavailable
//   ✓ permission_check, embedded_binary_check, network_check
//     with ok: false, error: "denied" | "not_in_bundle" | "timeout"

// ─── GRANULARITY (depth is yours — coverage is not) ──────────────────────────
// You choose how DEEP to go on a given feature: one event for the whole
// operation, or one per diagnosable step. You do NOT choose which features get
// measured — that is settled by the STEP 0 inventory above.
//
// USE track() FOR OPERATIONS — it wraps async work, measures time automatically,
// and sends ONE event covering the whole operation:
//
//   try await onelo.monitor.track("pdf_export", meta: [
//     "pages": doc.pages, "format": "a4"
//   ]) {
//     try await generateAndUploadPdf(doc)
//   }
//
//   → ONE row in Feature Health: success rate, avg duration, error label.
//     track() re-throws errors so your app handles them normally.
//     Always wrap in do/catch.
//
// USE event() FOR INSTANTANEOUS THINGS — a click, a tab switch, a state change:
//
//   onelo.monitor.event("tab_viewed", options: MonitorEventOptions(
//     ok: true, meta: ["tab": "export"]
//   ))
//
// DO NOT split one operation into _started / _completed events — that creates
// two separate unrelated rows in Feature Health. Use track() instead:
//
//   ✗ event("export_started", ...)   then   event("export_completed", ...)
//   ✓ track("export") { /* entire export operation */ }
//
// FINE-GRAINED — if one operation has multiple steps you need to diagnose
// individually, use track() for each step with descriptive names:
//
//   try await onelo.monitor.track("pdf_render", meta: ["pages": pages]) {
//     try await render(doc)
//   }
//   try await onelo.monitor.track("pdf_upload", meta: ["size": file.size]) {
//     try await upload(file)
//   }
//
//   → Two rows, each with independent success rates and timing.
//     Add granularity only when the coarse single-event view is not enough.
//
// EVERYTHING IN META IS QUERYABLE in Feature Health sessions:
//   meta: ["plan": "pro", "pages": 42, "format": "pdf"]
//   Native types supported: Bool, Int, Double, String, Date (ISO-8601),
//   UUID, URL, plus nested dictionaries and arrays.
//   Unsupported types (custom enums, NSDate, class instances, etc.) fall
//   back to String(describing:) — they ship as strings, never throw.
//   Keep meta small: aim for under ~20 keys / 8 KB per event. Never put a
//   full API response or large blob in here.
//   → filter sessions by those values to debug "only fails for free-plan users".

// ─── WHEN "RESOLVED" IS NOT "SUCCEEDED" ──────────────────────────────────────
// track() sets ok:false only when the callback THROWS. An operation that
// returns empty-handed — null session, zero rows, cancelled picker — is still
// recorded as a success. If "no result" means the feature failed for your user,
// raise it inside the callback and handle it outside:
//
//   do {
//     let rows = try await onelo.monitor.track("library_load", meta: ["source": "disk"]) {
//       let found = try await loadLibrary()
//       if found.isEmpty { throw LibraryError.empty }   // denied? wrong path?
//       return found
//     }
//   } catch {
//     // already recorded as ok:false with its duration — show the empty state
//   }
//
// One truthful event per attempt, with timing on the failure path too. Do NOT
// emit a second event() for the failure — that splits one feature into two rows.

// ─── SPECIAL META KEYS (opt-in — set these to get dashboard columns) ─────────
// These keys, when present in meta, surface as dedicated columns + filters
// in Feature Health. Set them yourself in your event meta — use these EXACT
// names so the dashboard auto-categorizes:
//   meta.model        → "Model" column     (e.g., "gpt-4", "claude-opus-4-7")
//   meta.trigger      → "Trigger" column   (e.g., "ptt", "vad", "scheduled")
//   meta.environment  → "Environment"      ("production" | "staging" | "dev")
// Custom keys still work — they just don't get column / filter status.
// "Release" and "App build" are auto-filled from app.version / app.build below
// — you do NOT set them manually.
//
// ─── WHAT SDK AUTO-ATTACHES (do NOT add these to meta manually) ──────────────
//   session_id   → UUID per install (persists across launches, resets on uninstall)
//   user_id      → from setUserId() or Onelo Auth (if used)
//   platform     → "swift" (constant for this SDK)
//   timestamp    → UTC, set at event creation time
//   sdk          → { name: "onelo-swift", version }    → "SDK version" facet
//   app          → { version, build, bundleId } from Info.plist
//                  • app.build   → "App build" facet
//                  • app.version → "Release" facet
//
// ON ERROR EVENTS ONLY (auto-attached when ok=false):
//   stack        → full call stack with module + symbol + offset
//   breadcrumbs  → up to 100 most-recent breadcrumbs
//   flags        → active feature-flag evaluations at time of error
//   device       → model, OS, locale, timezone, memory, connection type

// ─── WHAT NOT TO INSTRUMENT ──────────────────────────────────────────────────
//   ✗ Mouse move, hover, scroll position (too frequent, no actionable signal)
//   ✗ Every keystroke in an input field (track _submitted instead)
//   ✗ Internal UI state toggles with no outcome (e.g. dropdown open/close)
//   ✗ Events faster than ~1 per second per user (aggregate on your side first)
//   ✗ PII in meta — sensitive keys/values are auto-redacted, but it's better
//     to never send them in the first place. No emails (unless intentional),
//     full names, passwords, tokens, or card numbers.
② Serverbackend

Install

pip install "onelo[fastapi] @ git+https://github.com/onelo-tools/onelo-python.git" # extras: [django] / [flask] / [litestar]

Initialize

# PLACEMENT: call monitor.init() ONCE during app startup, before any
# request handler runs. For FastAPI / Litestar do it in a lifespan or
# top-level module; for Django put it in settings.py.
#
# PER-REQUEST ISOLATION: each integration forks an isolation scope per
# request so user_id / breadcrumbs from concurrent requests never bleed
# into each other (ContextVars under the hood).
#
# CRASH CAPTURE: install_excepthook=True wires sys.excepthook,
# threading.excepthook, and the active asyncio loop exception handler
# so uncaught errors are reported even if no framework error handler ran.
#
# DELIVERY IS RETRIED: events buffer in memory and ship as one HTTP batch.
# A batch that fails on the network, or gets a 5xx, is RETRIED up to 3 times
# with exponential backoff (0.5s → 1s → 2s); a 429 is not retried but sets a
# Retry-After hold-off, and other 4xx are terminal. The buffer is emptied
# before the send, so a batch that exhausts its 3 attempts is dropped and
# logged — never re-queued. Feature Health is a strong signal, not an audit log.
#
# PRIVACY: sensitive headers (Authorization, Cookie, X-API-Key) and query
# params (token, key, secret, password, ...) are scrubbed before any data
# leaves your process — same regex set as the Swift / Electron SDK.
#
# BASELINE EVENT: monitor.init() emits one 'session_opened' event automatically,
# unconditionally — before any auth middleware or feature-flag gate in front of
# your routes. Fires once per init() call (not per request, not again after a
# fork — a forked worker inherits the parent's session), and is exempt from
# sample_rate/success_sample_rate so it can't be randomly dropped. This
# guarantees Feature Health is never truly empty, but it does NOT solve the
# gating trap (see STEP 0 below) for the events YOU add downstream of a gate.
import os
from onelo import Onelo, monitor

# ─── ENVIRONMENT VARIABLES — set these BEFORE starting your backend ──────────
# This is a BACKEND (server-side) integration, so it authenticates with your
# SERVER SECRET key — NOT a publishable key (publishable keys are for client
# apps). Create the secret key in the Onelo dashboard → API Keys → Secret keys,
# then expose it to the process via env (never hard-code a secret in source):
#
#   ONELO_SECRET_KEY    REQUIRED   your server secret key, "onelo_sk_live_..."
#                                  (create it: Onelo dashboard → API Keys)
#   ONELO_API_URL       REQUIRED   Onelo API base URL → "https://api.onelo.tools"
#
# (Monitor has no test/live split — that's a Features-only concept
# (ONELO_FEATURE_ENVIRONMENT). Monitor is a single event stream.)
#
onelo = Onelo(
    secret_key=os.environ["ONELO_SECRET_KEY"],              # onelo_sk_live_* — server-side only
    api_url=os.environ.get("ONELO_API_URL", "https://api.onelo.tools"),  # Onelo API base URL
)

monitor.init(
    onelo=onelo,                # reuse one client for auth + features + monitor
    release=os.environ.get("ONELO_RELEASE"),  # your build id — reads ONELO_RELEASE (else GIT_COMMIT_SHA)
    install_excepthook=True,
    # High-throughput backend? Sample client-side BEFORE the network so an error
    # storm can't thrash the buffer or trip the hourly quota. Default 1.0 = keep all.
    # sample_rate=0.25,          # keep-probability for ERROR events (ok=False)
    # success_sample_rate=0.05,  # keep-probability for success / track events (usually the high-volume ones)
)

# FastAPI:
from fastapi import FastAPI
from onelo.monitor.integrations import OneloMonitorASGIMiddleware
app = FastAPI()
app.add_middleware(OneloMonitorASGIMiddleware)

# Django:  add to MIDDLEWARE in settings.py
#   "onelo.monitor.integrations.django.OneloMonitorMiddleware",
#
# Flask:
#   from onelo.monitor.integrations.flask import install
#   install(app)
#
# Litestar / Starlette:  pass OneloMonitorASGIMiddleware in middleware=[...]

Usage

# ─── STEP 0 — INVENTORY BEFORE YOU INSTRUMENT (do this first) ────────────────
# Do NOT write a single monitor.track() / capture_message() call until you have listed what
# this service actually does. Instrumenting the handlers you happened to read
# produces a dashboard that looks healthy because the broken half is not being
# measured.
#
# 1. ENUMERATE. Walk the codebase and list every unit of work the service
#    performs end-to-end — anything with an OUTCOME that can succeed or fail:
#      • every HTTP route / endpoint (including webhooks and callbacks), grouped
#        by the business operation it performs, not by URL shape
#      • every background job, queue consumer, scheduled/cron task
#      • every outbound third-party integration: payment provider, mail, storage,
#        search, LLM provider, any other API you do not control
#      • every boundary crossing inside the process: database, cache, filesystem,
#        subprocess
#      • startup, migration and shutdown paths (config load, schema migration,
#        warm-up, graceful drain)
#
# 2. FIND THE SILENT FAILURES. Grep for swallowed errors: an empty except/catch,
#    a handler returning an empty list or a default on failure, a fallback value
#    that hides a failed read, a fire-and-forget task nobody awaits, and any
#    place that logs and continues. These are the highest-value events in any
#    backend — they are exactly the failures that reach the caller and reach
#    nobody else. Instrument them FIRST.
#
# 3. WRITE THE TABLE. Before coding, output one row per unit of work:
#      operation | file | event name | monitor.track() or capture_message() | covered / skipped + why
#    Show it to the developer. A skipped operation must say WHY it was skipped
#    (see WHAT NOT TO INSTRUMENT below) — an unmeasured operation and a
#    deliberately-excluded one must never look the same in your report.
#
# 4. THEN implement, and keep the table in the PR description or a comment.
#
# 5. VERIFY. After implementing, actually trigger one instrumented operation,
#    wait ~15s (the flush interval — see below), reload the Onelo dashboard's
#    Feature Health tab and confirm a row appears for it. "The code compiles
#    and calls monitor.track()" is not verification; a row on the dashboard is.
#
# THE GATING TRAP — if auth middleware or a feature flag stands in front of
# EVERY route you instrumented, a healthy-but-locked-out deployment (bad
# credentials, a misconfigured flag) still shows zero events, indistinguishable
# from "not integrated." Make sure at least one operation you instrument sits
# OUTSIDE any such gate — a health-check route, the auth middleware's own
# accept/reject outcome, or process startup itself.
#
# COVERAGE RULE: every operation in the table is either instrumented, or listed
# with a stated reason. "I did not get to it" is not a reason.

# IDENTIFY THE USER on each request (Onelo Auth populates this for you;
# call manually only if you have your own auth):
monitor.set_user({"id": user.id, "email": user.email})

# TRACK AN OPERATION — measures wall-clock time and emits ONE event:
# ok=True on success, or an error event (full stack + breadcrumbs + active
# feature flags) if the block raises. The exception still propagates, so your
# control flow is unchanged. Same semantics as the Swift SDK's track().
# Python has no trailing-closure idiom, so track() works three ways — pick one:

# 1) context manager (sync)
with monitor.track("checkout", meta={"plan": user.plan}):
    process_payment()

# 2) context manager (async)
async with monitor.track("ai_response", meta={"model": "gpt-4"}):
    await call_model()

# 3) decorator (works on sync AND async functions)
@monitor.track("pdf_export")
async def export(doc):
    await render_and_upload(doc)

# Name the FEATURE, not the outcome — track() records ok/error/duration for you.
# Prefer track() for operations; use capture_exception() (below) only for errors
# you catch OUTSIDE a track() block.

# CAPTURE AN EXCEPTION manually. Always call from inside an except block:
try:
    process_payment()
except Exception:
    monitor.capture_exception()  # uses the active sys.exc_info()

# Or pass an explicit exception:
try:
    risky()
except RuntimeError as exc:
    monitor.capture_exception(exc, feature_name="payment", meta={"plan": user.plan})

# INSTANTANEOUS EVENT: no duration, just a marker.
monitor.capture_message("rate-limit triggered", level="warning",
                        feature_name="rate_limit")

# BREADCRUMBS: trail of context attached to the next captured event.
# Up to 100 most-recent are kept per request.
monitor.add_breadcrumb("loaded user", category="db")
monitor.add_breadcrumb("called Stripe", category="http")

# AUTO-TRACE OUTGOING HTTP: wrap your shared httpx / requests client once.
import httpx
from onelo.monitor.integrations.httpx import wrap_async_transport
client = httpx.AsyncClient(transport=wrap_async_transport())

import requests
from onelo.monitor.integrations.requests import install_session
session = install_session(requests.Session())

# AUTO-CAPTURE FROM logging: errors -> events, warnings/info -> breadcrumbs.
import logging
from onelo.monitor.integrations.logging import OneloLoggingHandler
logging.getLogger().addHandler(OneloLoggingHandler())

# BACKGROUND JOBS (Celery / RQ): producer attaches the carrier, worker
# decorates the task and inherits user_id / tags / trace_id automatically.
@celery_app.task(bind=True)
@monitor.continue_trace_task
def my_task(self, *args, **kwargs):
    monitor.add_breadcrumb("task body running")
    do_work()

# Producer:
my_task.apply_async(args=(...,), headers={"onelo": monitor.carrier()})

# FEATURE FLAG CORRELATION (auto): pass onelo=client to monitor.init and
# every captured event ships with active flag values, so the dashboard can
# show "this error spiked when flag X went from off to on" without extra
# wiring on your side.

# ─── NAMING CONVENTION ───────────────────────────────────────────────────────
# Use snake_case: {object}_{past_tense_verb}
# Object  → what the user acts on: checkout, pdf_export, ai_response, sync
# Verb    → past tense, result-oriented: completed, failed, started, retried
#
# Good:  checkout  pdf_export  ai_response  onboarding  sync  tab_viewed
# Bad:   checkout_completed  export_error  doSync  exportClicked
#
# Name the FEATURE, not the outcome — track() records ok/error/duration automatically.
# Use _verb suffix only for capture_message() calls (instantaneous, no duration):
#   tab_viewed  button_clicked  plan_selected  modal_dismissed
#
# Avoid _started / _completed suffixes — use track() to wrap the operation instead.
#
# PITFALL — check-style events: don't name an event after its failure mode.
# The name shows up in Feature Health even on success, so failure-mode names
# read like permanent alerts.
#   ✗ permission_missing, binary_missing, network_unavailable
#   ✓ permission_check, embedded_binary_check, network_check
#     with ok=False, error="denied" | "not_found" | "timeout"

# ─── GRANULARITY (depth is yours — coverage is not) ──────────────────────────
# You choose how DEEP to go on a given route or job: one event for the whole
# request, or one per diagnosable step. You do NOT choose which routes and jobs
# get measured — that is settled by the STEP 0 inventory above.
#
# USE track() FOR OPERATIONS — it wraps the work, measures wall-clock time, and
# emits ONE event covering the whole operation:
#
#   with monitor.track("invoice_issue", meta={"provider": "fakturownia"}):
#       issue_invoice(order)
#
#   → ONE row in Feature Health: success rate, avg duration, error label.
#     The exception still propagates, so your control flow is unchanged.
#
# USE capture_message() FOR INSTANTANEOUS THINGS — a marker with no duration:
# a rate-limit trip, a config reload, a feature toggled off by a kill switch:
#
#   monitor.capture_message("rate-limit triggered", level="warning",
#                           feature_name="rate_limit")
#
# DO NOT split one operation into _started / _completed events — that creates
# two separate unrelated rows in Feature Health. Use track() instead.
#
# FINE-GRAINED — if one request has several steps you need to diagnose
# individually (DB query, third-party call, PDF render), wrap each in its own
# track() with a descriptive name:
#
#   with monitor.track("invoice_render", meta={"lines": len(order.items)}):
#       pdf = render_invoice(order)
#   with monitor.track("invoice_upload", meta={"size": len(pdf)}):
#       upload(pdf)
#
#   → One row per step, each with independent success rates and timing.
#     Add granularity only when the coarse single-event view is not enough.
#
# EVERYTHING IN META IS QUERYABLE in Feature Health sessions:
#   meta={"plan": "pro", "region": "eu", "provider": "stripe"}
#   → filter sessions by those values to debug "only fails for free-plan users".

# ─── WHEN "RESOLVED" IS NOT "SUCCEEDED" ──────────────────────────────────────
# track() sets ok:false only when the block RAISES. An operation that
# returns empty-handed — null session, zero rows, cancelled picker — is still
# recorded as a success. If "no result" means the feature failed for your user,
# raise it inside the block and handle it outside:
#
#   try:
#       with monitor.track("library_load", meta={"source": "db"}):
#           rows = load_library(tenant_id)
#           if not rows:
#               raise EmptyLibrary()      # wrong tenant? RLS blocking the read?
#   except EmptyLibrary:
#       # already recorded as an error event with its duration — return 404, not 200 []
#       rows = []
#
# One truthful event per attempt, with timing on the failure path too. Do NOT
# emit a second capture_message() for the failure — that splits one feature into two rows.

# ─── WHAT SDK AUTO-ATTACHES ─────────────────────────────────────────────────
#   trace_id           → per request (or per background task via continue_trace)
#   user_id            → from monitor.set_user(...)
#   request context    → method, scrubbed URL, scrubbed headers
#   sdk / app / flags  → versions + active feature-flag snapshot
#   stack / frames     → on every captured exception (in_app classifier)
#   breadcrumbs        → up to 100 most recent (HTTP, log, custom)

# ─── WHAT NOT TO INSTRUMENT ─────────────────────────────────────────────────
#   ✗ DEBUG-level logs (would flood the breadcrumb buffer in busy services)
#   ✗ Health checks (/healthz) — not failures, no signal
#   ✗ Validation errors the framework already returns as 4xx — usually expected
#   ✗ PII in meta — secrets are scrubbed by the SDK, but better not to send them

The snippet carries its own naming, granularity and meta guidance in comments. Copy it from your dashboard SDK tab to get the placeholders below filled in automatically. Kotlin (server) is not yet part of the snippet set.

Verify it's working

Within 15 seconds of your first track() or event() call, the feature appears in Monitor → Feature Health. Click into it for the runs subpage — per-execution duration, status, platform and metadata. No run showing up? Check that your key and apiUrl match your dashboard (client SDKs use the publishable key; a Python/Node/PHP backend uses the secret key — see Placeholders below). Also check your plan's monitored-features limit: at the cap, events for new feature names are silently not registered (existing features keep ingesting).

Platform maturity

PlatformPackageStatus
Swift (iOS/macOS)OneloSwiftStable
PythononeloStable
Electron@onelo/electronStable
JS / Web@onelo/jsStable
React Native@onelo/react-nativeStable
Androidtools.onelo:onelo-androidStable
FlutteroneloStable
Node.js@onelo/nodeStable
PHPonelo/onelo-phpStable

SDKs install directly from GitHub (onelo-tools org) — the install step in each snippet has the exact command. Android ships on Maven Central as tools.onelo:onelo-android.

The methods

track(), event(), capture() and breadcrumb() exist on every SDK (exact spelling follows each language's idiom — see the snippet):

MethodUse it for
track(name, fn, { meta })Wrap an operation — measures duration, records ok/error, re-throws so your control flow is unchanged. Sync and async forms. Your default.
event(name, { ok, error, meta })An instantaneous marker with no duration — a tab switch, a state change, or an error you caught outside track().
capture(error, …)Record an already-caught error with full context (stack, breadcrumbs, flags). JS/Electron/RN take an options object ({ featureName, meta }); Python: capture_exception() / capture_message().
breadcrumb(...)Leave a trail of up to 100 context entries attached to the next captured error. Python: add_breadcrumb().
setUserId(id)Identify the current user. Automatic when you use Onelo Auth. Python: monitor.set_user({...}).
flush() / destroy()Force a send / tear down before exit. The SDK already flushes every 15s and on error (PHP: in one batch at request end — no timer).

Platform notes

A few platform-specific things that are easy to miss:

PlatformWatch out for
SwiftCrash reports arrive via MetricKit — real devices only, up to ~24h delay, not in the Simulator. Session id is install-bound. Failed batches are dropped (no disk retry).
PythonPer-request isolation scopes keep concurrent requests from bleeding into each other. install_excepthook wires uncaught capture. Integrations for FastAPI/Django/Flask/Litestar/httpx/requests/logging/Celery — see the snippet comments.
Node.jsPer-request isolation scopes (AsyncLocalStorage) via the Express / Next / Fastify / Hono adapters keep concurrent requests from bleeding. Opt-in auto-capture: captureConsole (console.error → event, console.warn → breadcrumb) and httpBreadcrumbs (each outgoing fetch → a breadcrumb, URL scrubbed). uncaughtException / unhandledRejection are observed at init; source lines are attached to in-app exception frames. For cross-process jobs (BullMQ / SQS / a separate worker), carrier() serialises the request scope (user id + tags, optionally HMAC-signed with your publishable key) onto the queued job and continueTrace() restores it in the worker, so errors there carry the dispatching user. Full parity with the Python backend SDK.
PHPUnder PHP-FPM every request is its own process, so the monitor scope is naturally request-isolated — no middleware required. In a persistent runtime (Laravel Octane / Swoole / RoadRunner) mount the OneloMonitor (Laravel) or OneloMonitorMiddleware (PSR-15) adapter, which resetScope()s per request and adds request context — this is also what keeps auto user-attribution from leaking across requests. Opt-in auto-capture: captureErrors (PHP warnings/notices → breadcrumbs, honours error_reporting). Uncaught exceptions AND fatal errors are always reported via a global exception handler + a shutdown drain, independent of captureErrors; source lines are attached to in-app exception frames. For queued jobs (Laravel queue / a separate worker), carrier() serialises the request scope (user id + tags, optionally HMAC-signed with your publishable key) onto the dispatched job and continueTrace() restores it in the worker, so errors there carry the dispatching user. No background thread — events buffer and send in one batch at request end (register_shutdown_function); call flush() in long-running workers. Full parity with the Python backend SDK.
ElectronInstantiate in the MAIN process only. Renderer exceptions are not auto-captured — bridge them over IPC. A before-quit hook flushes the last batch automatically (bounded at 2 s); destroy() is only needed when you tear the SDK down manually.
FlutterGlobal error capture is installed automatically at init (idempotent, chains any existing handler) — no registerGlobalHandlers() call needed. Uncaught Flutter/Dart errors are reported out of the box.
Android / React NativeUncaught Kotlin/JVM exceptions (Android) and uncaught JS errors (React Native, via ErrorUtils) are auto-captured — handlers install at init. Signal-level native crashes (NDK, iOS native) have no dedicated handler — catch and capture()/event(ok:false) where you can.
JS / WebBrowser errors auto-report via window error + unhandledrejection handlers registered at init; on the server (Next.js) wire onRequestError in instrumentation.ts. Needs your origin in the app's allowed-origins list; session id resets on every page reload; initialize once at module level (SSR-safe).

Placeholders in the snippet

The snippet is a template. On your dashboard's SDK → Monitor tab these are filled in for you; if you copy from here, replace them by hand:

PlaceholderWhat it isWhere to get it
{{publishableKey}}Client SDKs (Swift, JS, Electron, RN, Android, Flutter): your publishable key (onelo_pk_live_*), the same app-level key as Auth and Features. Backend SDKs (Python, Node.js, PHP) use your server secret key (onelo_sk_live_*) via ONELO_SECRET_KEY instead — a secret key must never ship in client code.Dashboard → Credentials → API Keys
{{apiUrl}}Your Onelo backend base URL — the SDK posts events here.Pre-filled by the dashboard for your environment
{{swiftRef}} / {{pythonRef}} / …The install ref (branch / version) for that platform.Pre-filled by the dashboard (staging vs production)
Client SDKs use the publishable key, exactly like Features and Auth; the backend SDKs (Python, Node.js, PHP) authenticate with your server secret key (onelo_sk_live_*) — a secret key must never ship in client code. A pk_test key behaves the same as a live key for Monitor — there is no separate test tenant or fake data; only Features treats test keys specially. See Security for how Onelo binds keys to your app.

Next

Monitor SDK — Onelo Docs