Your frontend emits OpenTelemetry spans like any other service. They join the same traces as your backend, land in the same store, and carry whatever business attributes your app knows — which tenant, which plan, which build, which experiment arm. This is not RUM. There is no session replay and no separate frontend product. It is the same telemetry you already collect, extended to the one place your server cannot see.
Everything on this page is published OpenTelemetry. There is no Rocketgraph browser SDK, and there will not be one — your instrumentation stays portable to any OTLP backend.

Install

Pin @opentelemetry/web-common. It is experimental, and the README on main documents classes that are not in the published release — 0.222.0 exports factory functions instead. Follow the type definitions, not the README.

Sessions

A session id turns a page load, four fetches, and the backend work they caused into one person trying to do one thing. Do not write your own — OpenTelemetry publishes generation, persistence, idle rotation, and span stamping.
createSessionSpanProcessor(sessionManager) writes session.id onto every span. Its interface is getSessionId(): string | null.

Your own attributes

This is the only code you write, and it is the only part specific to your business.
Use a span processor, not resource attributes. A resource is frozen when the provider is constructed — at page boot, before you know which tenant this is. onStart runs per span and sees what the app knows at that moment. Name keys in dotted namespaces (tenant.plan, checkout.version). Cohorts discovers dimensions by sampling attribute keys and filtering on cardinality, so a key that reads like a dimension becomes one, and a key that behaves like an identifier is filtered out.

Carry the session to your backend

traceparent links spans into one trace and carries no attributes at all. Your backend spans know they share a trace and nothing else. To get session.id onto them, use the other W3C header: baggage.
Baggage is a header the client controls. Treat it as a correlation hint, never an identity claim. Anything that decides tenancy — account, plan, entitlement — must be re-derived server side from the request body or session cookie before you stamp it on a span.

Wire it up

Processor order is execution order: session id, then your attributes, then the exporting BatchSpanProcessor last.
The OTLP exporter parses its url with new URL() and rejects a relative path. Writing '/v1/traces' throws at module scope, which kills the bundle and renders a blank page. Build it from window.location.origin.

Auto-instrumentation

Without propagateTraceHeaderCorsUrls, OpenTelemetry deliberately omits traceparent on cross-origin requests. Your browser trace silently never joins your backend trace, and nothing logs a complaint.

Real user clicks

instrumentation-user-interaction opens a span for each real click and runs your handler inside that span’s context — so the fetch it fires becomes a child of the click, not a sibling. One tree per user action:
Restrict eventNames. The default set includes mousedown and mouseup, which triples every interaction for no extra information.
Span names come from the DOM target. On an app with generated class names they are unreadable, so add a data-* convention on the elements you care about. Client-side route changes are not captured — emit a span on pathname change if you need them.

Send spans to your own origin

Point the browser at your own server and forward to Rocketgraph from there.
Three reasons, in the order they matter:
  1. No CORS. Same origin means no preflight to misconfigure, and no failure mode where spans are dropped with nothing logged anywhere.
  2. It is where a browser ingest key belongs. A key shipped to browsers is public by definition and needs origin checks, rate limits, and quotas.
  3. It is where PII scrubbing goes. URLs and attributes leak order ids and email addresses. Stripping them before storage is far cheaper than deleting them afterwards.
Forward with urllib, not requests — requests is auto-instrumented, so forwarding a span batch emits a span, which is forwarded, which emits a span. Exclude the path from your own server instrumentation for the same reason: OTEL_PYTHON_FASTAPI_EXCLUDED_URLS=v1/traces.

Receive the session on your backend

Baggage already flows — both the Python and Node SDKs ship tracecontext,baggage as default propagators, so it is extracted inbound and re-injected on outbound calls. What is missing is anything writing it onto a span.
Allow-list the keys. ALLOW_ALL_BAGGAGE_KEYS lets any visitor write arbitrary attributes onto your server spans — unbounded cardinality, and PII you never chose to collect.

Verify

If the browser rows are populated and the backend rows are zero, the propagator was constructed but never passed to provider.register(). That is the failure every time. Page navigations carry no baggage — fetch and XHR do, but document loads and static assets do not — so expect partial coverage on your web tier and full coverage on everything behind it. Session drill-down scopes by trace id rather than the attribute for exactly this reason, which also means it works for a frontend that propagates nothing at all.

Next

Cohorts

Rank any attribute by failure rate, then drill into the sessions and spans behind it.

OpenTelemetry web SDK

Upstream documentation for everything on this page.