Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Quickstart

Wire one Strada project into a fullstack app so that browser and server telemetry land in the same place. The browser sends pageview spans and frontend errors; the server sends request spans, logs, and handled exceptions. Both flow to the same ClickHouse tables, correlated by session.id and user.id.
browser code server code initStrada({ projectId }) initStrada({ projectId, token }) (no token, anonymous) (token = trusted, not rate limited) ├─ pageview spans ── /v1/traces ├─ request spans ── /v1/traces ├─ frontend errors /v1/logs ├─ app logs ── /v1/logs └─ track() events ── /v1/logs └─ captureException /v1/logs baggage └──────── session.id + user.id ──────────── Strada collector ── otel_traces / otel_logs / otel_errors
The whole setup is the same import path (@strada.sh/sdk) in both runtimes. Export conditions resolve the browser build in bundlers and the Node build on the server. You never pick a runtime-specific package.

1. Create the project

strada projects create my-app
This prints a project ID and a server ingest token. Keep both.
  • The project ID is public. It identifies which project telemetry belongs to.
  • The token is a secret. It marks ingest as trusted so it is not rate limited. Server-side only. Never ship it to the browser.
You can create more server tokens later:
strada tokens create --scope ingest production-server

2. Install the SDK

npm install @strada.sh/sdk
One package, every runtime. The import path is always @strada.sh/sdk.

3. Server setup

Initialize Strada before the rest of your app loads so the global OpenTelemetry providers are wired before any request. Pass the token here.
import { initStrada, captureException, getLogger } from "@strada.sh/sdk" initStrada({ projectId: process.env.STRADA_PROJECT_ID!, token: process.env.STRADA_TOKEN, // server only service: "my-app", environment: process.env.NODE_ENV ?? "development", }) const logger = getLogger("api") try { await chargeCard() } catch (error) { logger.error({ message: "payment failed", error: String(error) }) captureException(error, { tags: { route: "/checkout" } }) }
On Node, the SDK registers process handlers so buffered telemetry flushes on beforeExit, SIGTERM, SIGINT, and uncaughtException. You usually do not call flush() yourself. See the SDK reference for the exit-path details.

Cloudflare Workers

Workers expose bindings through cloudflare:workers. Initialize at module scope, not inside request middleware:
import { env } from "cloudflare:workers" import { initStrada } from "@strada.sh/sdk" initStrada({ projectId: env.STRADA_PROJECT_ID, token: env.STRADA_TOKEN, service: "my-worker", environment: env.ENVIRONMENT, })
Use one Strada project for related services. Give each Worker a distinct service and set environment to development, preview, or production so the same queries can compare deployments.
env.STRADA_PROJECT_ID is empty until the secret is uploaded, and it is always empty in wrangler dev. That is fine: a blank projectId disables export, the providers are still installed, and track() / captureException() become silent no-ops. Call initStrada() unconditionally and keep your call sites free of if (analyticsEnabled) checks. Use enabled: false when you want to turn telemetry off explicitly. See Turning telemetry off.

4. Browser setup

Initialize Strada in the browser with the same project ID but no token. Browser ingest is anonymous and rate limited by design, because any token shipped to the browser would be public.
import { initStrada } from "@strada.sh/sdk" initStrada({ projectId: process.env.PUBLIC_STRADA_PROJECT_ID!, // public, see step 5 service: "my-app-browser", environment: process.env.NODE_ENV ?? "development", enabled: !import.meta.hot, // keep OTel local during dev/HMR })
That single call gives you, with no extra code:
  • a pageview span that restarts on SPA navigation and ends on tab close
  • uncaught window.error and unhandledrejection captured as exception logs
  • session.id, visitor.id, and user.id injected into every span/log. Only session.id and user.id are propagated to the server
Add the Vite plugin so every browser trace, log, and error is tagged with the exact build (git commit, branch, version):
// vite.config.ts import { defineConfig } from "vite" import { stradaVitePlugin } from "@strada.sh/sdk/vite" export default defineConfig({ plugins: [stradaVitePlugin()], })

5. Env vars: public vs secret

This is the step that trips people up. The token is a server secret. The project ID must be public on the browser side, which means it needs a build-tool public prefix so the bundler inlines it into client code.
VariableWherePublic?Notes
STRADA_TOKENserver onlysecretNever expose to the browser. Marks ingest as trusted.
STRADA_PROJECT_IDserversafeRead directly from process.env on the server.
PUBLIC_STRADA_PROJECT_IDbrowsersafeSame value, but public-prefixed so the bundler inlines it.
The prefix depends on your bundler:
Bundler / frameworkPublic prefix
ViteVITE_ (or your envPrefix)
Next.jsNEXT_PUBLIC_
Custom define pluginwhatever you configure, e.g. PUBLIC_
Pick one public-prefixed name for the project ID and use it in the browser initStrada() call. Use the plain STRADA_PROJECT_ID on the server.

6. Fullstack frameworks (RSC / server-rendered)

In a React Server Components app there is often no explicit client entry file to drop browser initStrada() into. The clean pattern is a side-effect-only client module: a "use client" module whose top level runs the browser setup, exposed through a component that renders nothing.
// strada-browser.tsx "use client" import { initStrada, captureException } from "@strada.sh/sdk" import { setReactErrorHandlers } from "spiceflow/react" // or your framework's hook const projectId = process.env.PUBLIC_STRADA_PROJECT_ID if (projectId) { initStrada({ projectId, service: "my-app-browser", environment: process.env.NODE_ENV ?? "development", enabled: !import.meta.hot, }) // Optional: capture React render errors globally, even when an // ErrorBoundary swallows them. Hook name varies by framework. setReactErrorHandlers({ onCaughtError: (error) => captureException(error, { tags: { reactHandler: "onCaughtError" } }), onUncaughtError: (error) => captureException(error, { tags: { reactHandler: "onUncaughtError" } }), onRecoverableError: (error) => captureException(error, { tags: { reactHandler: "onRecoverableError" } }), }) } export function StradaBrowser() { return null }
Render it once in your root layout:
<body> <StradaBrowser /> {children} </body>
Why a component and not a bare import? In RSC, a "use client" module is only sent to the browser if something in the rendered tree references it. A plain import would run on the server and get tree-shaken from the client bundle. Rendering <StradaBrowser /> forces the bundler to ship and evaluate the chunk in the browser, which runs the top-level initStrada().
The chunk evaluates during hydration, not before first paint. Pageview spans and React error handlers work. A synchronous error thrown before hydration is not captured. If you need pre-hydration capture, use an inline <script> in <head> instead.
If a framework owns the root document and exposes no client component slot, do not force this component into an unrelated wildcard layout. Keep server-side instrumentation only until the framework provides a supported client hook.
If your framework exposes an OpenTelemetry tracer hook (for example Spiceflow's new Spiceflow({ tracer })), pass the SDK tracer so request spans flow to the same project:
import { trace } from "@strada.sh/sdk" const tracer = trace.getTracer("my-app") // pass `tracer` to your framework's constructor / config
Keep route and handler tags low-cardinality. Email and user id are fine on errors, logs, and product events. That is how you find who hit an issue. Do not attach API keys, prompts, or the raw text the user typed.

7. Verify

Trigger a request and a frontend pageview, then check the data with the CLI:
# handled and uncaught errors, grouped by fingerprint strada issues list -p my-app --since 1h # raw span count (browser pageviews + server requests) strada query "SELECT count() FROM otel_traces WHERE Timestamp >= now() - INTERVAL 1 HOUR LIMIT 1" -p my-app # errors with their service name strada query "SELECT ExceptionType, ExceptionMessage, ServiceName FROM otel_errors WHERE Timestamp >= now() - INTERVAL 1 HOUR ORDER BY Timestamp DESC LIMIT 10" -p my-app
Browser and server rows share the same project. Filter by ServiceName to tell them apart (my-app vs my-app-browser).

8. User identity (optional)

Once the browser knows who the user is, set the strada_uid cookie (JS-readable) on login, or call identifyUser({ id }). Unique visitors use a separate strada_vid cookie. Logout clears strada_uid and leaves strada_vid. The SDK propagates user.id to the server via W3C Baggage, so server spans and logs for that request also carry the user id. session.id, visitor.id, and user.id are injected into browser spans and logs. Only session.id and user.id are propagated to the server.
For server-first requests there is no browser header to read, so put the user into baggage yourself in auth middleware. That works with any auth library; the Better Auth plugin just does it for you. See Server-side user identification without Better Auth for the middleware, the withUser() wrapper, Workers, and the identifyUser() profile snapshot.