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
@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.strada projects create my-app
strada tokens create --scope ingest production-server
npm install @strada.sh/sdk
@strada.sh/sdk.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" } }) }
beforeExit, SIGTERM, SIGINT, and uncaughtException. You usually do not call flush() yourself. See the SDK reference for the exit-path details.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, })
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.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 })
window.error and unhandledrejection captured as exception logssession.id, visitor.id, and user.id injected into every span/log. Only session.id and user.id are propagated to the server// vite.config.ts import { defineConfig } from "vite" import { stradaVitePlugin } from "@strada.sh/sdk/vite" export default defineConfig({ plugins: [stradaVitePlugin()], })
| Variable | Where | Public? | Notes |
STRADA_TOKEN | server only | secret | Never expose to the browser. Marks ingest as trusted. |
STRADA_PROJECT_ID | server | safe | Read directly from process.env on the server. |
PUBLIC_STRADA_PROJECT_ID | browser | safe | Same value, but public-prefixed so the bundler inlines it. |
| Bundler / framework | Public prefix |
| Vite | VITE_ (or your envPrefix) |
| Next.js | NEXT_PUBLIC_ |
| Custom define plugin | whatever you configure, e.g. PUBLIC_ |
initStrada() call. Use the plain STRADA_PROJECT_ID on the server.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 }
<body> <StradaBrowser /> {children} </body>
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.
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
# 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
ServiceName to tell them apart (my-app vs my-app-browser).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.withUser() wrapper, Workers, and the identifyUser() profile snapshot.