npm install @strada.sh/sdk
import { initStrada, captureException, track } from "@strada.sh/sdk" initStrada({ projectId: "01JTHG...", token: process.env.STRADA_TOKEN, service: "api" }) // errors, traces, logs, metrics, custom events // all flow through standard OpenTelemetry to your database
Sentry ─┐ errors, alerts, issue grouping │ │ Datadog │ traces, logs, metrics ├────► Strada │ Google Analytics │ one CLI pageviews, sessions, custom events │ one database │ one SQL dialect Grafana │ dashboards, visualizations, query & alerts ─┘
Bundle size, esbuild ESM, node18 target Strada SDK 400.5 kB ██████████ Sentry SDK 1908.6 kB ████████████████████████████████████████████████ Sentry is 4.8× larger.
strada query "SELECT ..." to answer any question about your system. Raw ClickHouse SQL, no proprietary APIstrada issues list to see error groups, read stacktraces, identify regressions, and open fix PRsotel_logs filtered by trace ID, session, or time range to reconstruct what happened before a failurecheckout_started and purchase_completed as custom events. Query success rates with SQL to catch drops earlyotel_errors, otel_logs, and otel_tracesBrowser SDK Node SDK Workers SDK pageviews, track, errors traces, logs, metrics captureException │ │ │ │ OTLP HTTP/JSON │ OTLP HTTP/JSON │ ▼ ▼ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ Strada OTLP Collector │ │ (Cloudflare Worker, open source) │ └────────┬─────────────────┬──────────────────┬──────────────────┬──────┘ │ │ │ │ ▼ ▼ ▼ ▼ otel_traces otel_logs otel_metrics otel_errors │ │ ▲ │ └─────────────▶ extract exceptions ────┘ ▼ otel_analytics_pages ◀────────── materialized views otel_analytics_sessions
exception.* attributes, extracted into otel_errorsotel_tracesotel_logsotel_metrics_*event.name attributestrada database create # Authenticates with Tinybird, deploys all tables and materialized views
strada projects create my-app # Returns a project ID, ingest endpoint, and a server-side token strada setup --project my-app # Saves this folder's default org/project in ~/.strada/config.json
strada setup binds the current folder to an organization and project. After setup, CLI commands run against that project implicitly, so you do not need to pass --project my-app to every command.cd ~/code/acme-api strada setup # pick Acme / api cd ~/code/personal-site strada setup # pick Personal / frontend
~/.strada/config.json, not committed to source control. Strada resolves config by walking up from the current working directory and using the closest matching folder scope:{ "scoped": { "/": { "sessionToken": "...", "baseUrl": "https://strada.sh" }, "/Users/me/code/acme-api": { "orgId": "01HORG...", "orgName": "Acme", "projectId": "01HPROJ...", "projectSlug": "api" } } }
strada projects create prints a token once. Use that token in trusted server runtimes like
Node.js, Vercel, and Cloudflare Workers. If you need another one later, run
strada tokens create --scope ingest production-server. Browser apps should omit token; browser ingest is
anonymous and rate limited because browser secrets are public.import { initStrada, captureException, track, startSpan } from "@strada.sh/sdk" initStrada({ projectId: "01JTHG5M7XPQR8KNCZ0W4D", token: process.env.STRADA_TOKEN, service: "api", environment: "production", version: "1.2.0", enabled: !import.meta.hot, }) // enabled: false keeps OTel APIs local but sends nothing to ingest. // In Vite/RSC dev servers, import.meta.hot is truthy during HMR. // capture errors try { await processPayment(order) } catch (err) { captureException(err) } // create traces (auto-ends span, auto-records errors) await startSpan({ name: "process-order" }, async (span) => { span.setAttribute("order.id", "ord_123") await processOrder(order) }) // track custom events track("purchase_completed", { plan: "pro", amount: 49 })
# list error groups from the last 24 hours strada issues list --since 24h # view a specific error with stacktrace strada issues view <fingerprint> # browse recent logs strada logs --since 1h strada logs --min-level error --since 24h strada logs --search "timeout" --service api # filter by any attribute with --where (-w) strada logs -w "mapContains(LogAttributes, 'event.name')" # custom events only strada logs -w "LogAttributes['user.id'] = 'user_123'" # specific user strada logs -w "LogAttributes['exception.type'] = 'TypeError'" # specific error type # log volume by service and severity strada logs stats --since 24h # run any SQL query strada query "SELECT count() FROM otel_errors WHERE ExceptionType = 'TypeError'" # browser analytics strada analytics pages --since 7d strada analytics overview --since 7d # custom events with attribute filters strada analytics events -w "LogAttributes['custom.plan'] = 'pro'" # events from pro users strada analytics events -w "LogAttributes['user.id'] = 'user_123'" # events from a user
strada with no arguments to launch an interactive terminal UI for browsing your observability data. The TUI uses termcast and requires Bun. If you run it under Node, it re-spawns with Bun automatically.strada
Ctrl+P):Ctrl+K). All selections persist across sessions.your code ──► initStrada() + captureException() + track() │ │ configures standard OTel providers ▼ TracerProvider LoggerProvider MeterProvider │ │ │ └────────────────────────┼───────────────────────┘ │ │ OTLP HTTP/JSON ▼ Strada Collector (Cloudflare Worker) │ ┌─────────────┬─────────────────┼─────────────────┬──────────────┐ ▼ ▼ ▼ ▼ ▼ otel_traces otel_logs otel_errors otel_metrics otel_analytics ┌──────────────────────────────────────────────────────────────────────────────────┐ │ ClickHouse (your database) │ └──────────────────────────────────────────────────────────────────────────────────┘
| Attribute | Purpose |
session.id | Per-tab browser session UUID for grouping pageviews |
visitor.id | Anonymous browser identity from strada_vid; powers unique visitor counts |
user.id | Signed-in user identity, propagated via W3C Baggage |
event.name | Distinguishes custom events from ordinary logs |
exception.mechanism.type | How an error was captured (onerror, unhandledrejection, etc.) |
exception.mechanism.handled | Whether user code caught the error |
exception.fingerprint | Custom grouping override for error deduplication |
@strada.sh/sdk needed.OTEL_EXPORTER_OTLP_ENDPOINT=https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer str_abc123..." OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS is a comma-separated list of key=value pairs. The OTel SDK adds them as HTTP headers on every export request. authorization=Bearer str_abc123... becomes the standard Authorization: Bearer str_abc123... header that the Strada collector expects.OTEL_EXPORTER_OTLP_PROTOCOL must be http/json. Strada does not support gRPC or protobuf. Most OTel SDKs default to protobuf, so this env var is required.import { NodeSDK } from "@opentelemetry/sdk-node" import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http" import { SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs" // env vars are read automatically by the exporters const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter(), logRecordProcessors: [new SimpleLogRecordProcessor(new OTLPLogExporter())], }) sdk.start()
OTEL_EXPORTER_OTLP_ENDPOINT=https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh \ OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer str_abc123..." \ OTEL_EXPORTER_OTLP_PROTOCOL=http/json \ OTEL_SERVICE_NAME=my-api \ node app.js
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
OTEL_EXPORTER_OTLP_ENDPOINT=https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh \ OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer str_abc123..." \ OTEL_EXPORTER_OTLP_PROTOCOL=http/json \ OTEL_SERVICE_NAME=my-api \ opentelemetry-instrument python app.py
WithEncoding in code:OTEL_EXPORTER_OTLP_ENDPOINT=https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh \ OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer str_abc123..." \ OTEL_EXPORTER_OTLP_PROTOCOL=http/json \ OTEL_SERVICE_NAME=my-api \ go run .
import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" exporter, _ := otlptracehttp.New(ctx, otlptracehttp.WithEndpointURL("https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh"), otlptracehttp.WithEncoding(otlptracehttp.EncodingJSON), otlptracehttp.WithHeaders(map[string]string{ "authorization": "Bearer str_abc123...", }), )
OTEL_EXPORTER_OTLP_TRACES_HEADERS="authorization=Bearer str_abc123..." OTEL_EXPORTER_OTLP_LOGS_HEADERS="authorization=Bearer str_abc123..." OTEL_EXPORTER_OTLP_METRICS_HEADERS="authorization=Bearer str_abc123..."
headers option:// Node.js new OTLPTraceExporter({ url: "https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh/v1/traces", headers: { authorization: "Bearer str_abc123..." }, })
# Python from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter exporter = OTLPSpanExporter( endpoint="https://01JTHG5M7XPQR8KNCZ0W4D-ingest.strada.sh/v1/traces", headers={"authorization": "Bearer str_abc123..."}, )
https://{projectId}-ingest.strada.sh. The project ID comes from strada projects create <slug>.strada query "SELECT ..." # any ClickHouse SQL strada issues list -p my-app # error groups, sorted by frequency strada issues view <fp> # stacktrace, recent events, metadata strada logs -p my-app # browse logs, colored one-line output strada logs stats -p my-app # log volume by service and severity strada analytics pages # top pages, browsers, countries strada analytics events # custom events with properties strada projects list # list all projects strada login # device flow auth
strada query with raw SQL to answer any question about your system. No rate limits, no API keys to manage, no pagination tokens. Just SQL.strada issues list, identify the top error, read the stacktrace, find the bug in your codebase, open a PRstrada logs -p my-app --min-level error --since 1h to see recent errors, then --trace-id to follow a specific request across servicesstrada query "SELECT ... FROM otel_errors WHERE ExceptionMessage LIKE '%stripe%'" to find all errors blocking Stripe subscriptions# example: find all unhandled errors in the checkout flow from the last hour strada query " SELECT ExceptionType, ExceptionMessage, count() as occurrences FROM otel_errors WHERE SpanName LIKE '%checkout%' AND MechanismHandled = 'false' AND Timestamp >= now() - INTERVAL 1 HOUR GROUP BY ExceptionType, ExceptionMessage ORDER BY occurrences DESC LIMIT 20 " -p my-app
strada query to find the heaviest HTTP routes by request count and latency. Filter to root spans with ParentSpanId = '' so each row is one HTTP request, not a nested DB query or downstream call. Duration is in nanoseconds; divide by 1e6 to get milliseconds.# top routes by request count, with RPS and latency percentiles strada query " SELECT SpanName AS path, count() AS total_requests, round(count() / 3600, 2) AS rps, round(avg(Duration) / 1e6, 1) AS avg_ms, round(quantile(0.95)(Duration) / 1e6, 1) AS p95_ms, round(max(Duration) / 1e6, 1) AS max_ms FROM otel_traces WHERE Timestamp >= now() - INTERVAL 1 HOUR AND ParentSpanId = '' GROUP BY path ORDER BY total_requests DESC LIMIT 30 " -p my-app
# per-minute breakdown for a specific route strada query " SELECT toStartOfMinute(Timestamp) AS minute, count() AS requests FROM otel_traces WHERE Timestamp >= now() - INTERVAL 1 HOUR AND ParentSpanId = '' AND SpanName = 'POST /api/my-route' GROUP BY minute ORDER BY minute DESC LIMIT 60 " -p my-app
quantile(0.95)(Duration) is ClickHouse's built-in percentile function. You can swap 0.95 for 0.5 (median) or 0.99 (p99).visitor.id (cookie strada_vid). Sessions use session.id in sessionStorage.import { initStrada, track, identifyUser } from "@strada.sh/sdk" initStrada({ projectId: "01JTHG5M7XPQR8KNCZ0W4D", service: "frontend", }) // pageview tracking starts automatically // track custom events track("signup_started", { plan: "pro", source: "pricing-page" }) track("purchase_completed", { amount: 49 })
strada_uid. Unique visitors use cookie strada_vid. Set the account cookie when the user logs in, or call identifyUser({ id }):identifyUser({ id: user.id }) identifyUser(null) // logout: clear strada_uid, keep strada_vid
user.id into every span, log, error, and custom event, and propagates it to the backend via W3C Baggage.-- find which pages have the most errors SELECT LogAttributes['url.path'] AS page, count() AS error_count FROM otel_errors WHERE Timestamp >= now() - INTERVAL 7 DAY GROUP BY page ORDER BY error_count DESC LIMIT 10
-- get the full session timeline for a user who hit an error SELECT Timestamp, ServiceName, SpanName, LogAttributes['event.name'] AS event FROM otel_logs WHERE LogAttributes['session.id'] = 'abc-123' ORDER BY Timestamp ASC
SELECT.strada database create deploys all tables, materialized views, and tokens to your Tinybird workspacewrangler deploy┌──────────────────────────────────────────────┐ strada.sh (managed) ─────────────────► │ Your Tinybird workspace │ auth, teams, ingestion, CLI │ │ │ otel_traces ─── otel_logs ─── otel_errors │ OR │ otel_metrics ─── otel_analytics_* │ │ │ self-hosted (fork + wrangler deploy) ──► │ same schema, same tables │ Cloudflare Workers, zero lock-in │ you own everything │ └──────────────────────────────────────────────┘
import { initStrada, captureException, track, trace, logs, metrics } from "@strada.sh/sdk"
| Runtime | What it sets up |
| Node.js / Bun | OTel providers, OTLP exporters, process error handlers, graceful shutdown |
| Browser | WebTracerProvider, pageview spans, session management, error/rejection handlers |
| Cloudflare Workers | BasicTracerProvider, auto-flush via waitUntil, zero overhead when unused |
initStrada(), all standard OTel APIs work: trace.getTracer(), logs.getLogger(), metrics.getMeter(). The SDK re-exports these so you don't need @opentelemetry/api as a dependency.startSpan({ name }, callback) creates a span, auto-ends it, and auto-records errors. No tracer instance neededcaptureException(error) normalizes errors, computes fingerprints, emits structured OTel log recordstrack(name, props) emits custom events as OTel log records with event.name and custom.* attributessetTags(tags) sets tags merged into subsequent error attributesflush() / shutdown() for manual lifecycle controlurl.path, session.id, visitor.id, and user.id from window.location, sessionStorage, cookie strada_vid (visitor), and cookie strada_uid (account).captureException() inside an HTTP handler automatically includes the handler's url.path, http.route, and http.method. This works even in nested spans (e.g., a DB query inside an Express handler) because the SDK propagates context from parent to child spans.GET /api/orders (url.path="/api/orders", http.method="GET") │ ├── db.query (inherits url.path="/api/orders" from parent) │ └── captureException(err) → error row has url.path="/api/orders" │ └── POST /v1/payment_intents (keeps its own url.path, not overwritten) └── captureException(err) → error row has url.path="/v1/payment_intents"
http.target, http.url) into url.path so errors show a clean path regardless of which instrumentation version you use.session.id and user.id propagate from browser to backend via W3C Baggage headers. Backend errors within a browser-initiated request carry the same session and user identity.// vite.config.ts import { defineConfig } from 'vite' export default defineConfig({ build: { rolldownOptions: { output: { keepNames: true }, }, }, })
npx -y skills add remorses/strada