Skip to content

feat(observability-react-native): accept plain nested dictionaries in track#650

Open
abelonogov-ld wants to merge 20 commits into
mainfrom
feat/rn-track-plain-properties
Open

feat(observability-react-native): accept plain nested dictionaries in track#650
abelonogov-ld wants to merge 20 commits into
mainfrom
feat/rn-track-plain-properties

Conversation

@abelonogov-ld

Copy link
Copy Markdown
Contributor

Summary

Reshapes LDObserve.track (and the LDClient.track afterTrack hook) to accept a plain dictionary for properties, matching the iOS ([String: Any]) and Android (Map<String, Any?>) track surfaces — instead of requiring flat OpenTelemetry Attributes.

A shared converter (flattenTrackProperties) flattens the payload into attributes before recording the span, mirroring Android's AttributeConverter:

  • nested objects → dot-separated keys (e.g. user.id)
  • arrays of objects / mixed arrays → indexed dotted keys (e.g. products.0.price)
  • homogeneous scalar arrays → array attributes
  • null / undefined / unsupported values are skipped (never stringified)

Previously, passing a nested dictionary either failed to typecheck or had its nested members silently dropped by OpenTelemetry's attribute sanitizer. Now nested payloads behave consistently across platforms.

Changes

  • New TrackProperties / TrackPropertyValue types (exported from the API).
  • New flattenTrackProperties converter + unit tests.
  • track signature widened from AttributesTrackProperties across Observe, LDObserve, ObservabilityClient, InstrumentationManager.
  • afterTrack hook flattens data the same way; reserved key/value now take precedence over caller data.
  • Example app API screen updated to exercise the plain (nested) variant.

Notes

Test plan

  • yarn test (observability-react-native) — 69 passing, incl. new converter tests
  • Manually verify in the example app (API tab): "Track via LDObserve", "Track nested via LDObserve" produce dotted attributes (e.g. products.0.product_id)

Made with Cursor

abelonogov-ld and others added 20 commits June 24, 2026 13:48
Add a cookbook of distributed tracing patterns for the React Native
observability SDK, mirroring the existing .NET MAUI guide but adapted to
the JS/OpenTelemetry API (startActiveSpan/startSpan, explicit context
propagation across async boundaries, tracingOrigins-based mobile-to-backend
trace linking). Lives in guides/ since docs/ is reserved for generated
Typedoc output, and is linked from the package README.

Co-authored-by: Cursor <cursoragent@cursor.com>
… guide

Document setting, reading, and updating W3C baggage and how it propagates
to backends via tracingOrigins, plus a note that baggage is not copied
onto spans automatically.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add a Tracing tab to the session-replay example with a button per recipe
from the distributed tracing guide and a live output log, registering the
Observability plugin with tracingOrigins so the backend-propagation and
baggage recipes exercise real header injection.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop tracking the session-replay example's ios/Podfile.lock and Gemfile.lock,
which are regenerated per-machine and churn on every pod/bundle install. The
monorepo yarn.lock remains tracked for reproducible installs.

Co-authored-by: Cursor <cursoragent@cursor.com>
…s resource attributes

buildObservabilityResource strips the default service.name from the OTel
resource but never re-adds one from options, so exported spans, logs, and
metrics had an empty service.name (the value only flowed to Session Replay).

Populate service.name / service.version from ObservabilityOptions.serviceName
and serviceVersion in the shared resource builder so both init paths (LDClient
plugin and standalone LDObserve.init) emit a populated service identity.

Co-authored-by: Cursor <cursoragent@cursor.com>
…-tracing

Bring in the launchdarkly-android-client-sdk bump (0.46.1).
Introduce a LaunchDarkly-owned SessionManager (LDSessionManager) that can
be seeded with an external session id, so the native instance can adopt a
session id created elsewhere (e.g. the JS SDK in a React Native app) and
report a single shared session.id across spans, logs, metrics, and replay.

- LDSessionManager: seedable session manager replicating OTel Android's
  rotation semantics (foreground never expires, background inactivity and
  max-lifetime rotation) and observer notifications.
- LDRumSessionManagerAccessor: same-package shim to call the package-private
  OpenTelemetryRumBuilder.setSessionManager, making our manager back the RUM
  SDK's session.id span/log appenders (single source of session identity).
- ObservabilityService: inject the custom manager, drop the old
  session-manager-bridge instrumentation and SessionConfig, drive the
  background-timeout from the app lifecycle, and read session.id from it.
- Thread an optional customSessionId through the Observability plugin and
  LDObserve.init for callers (RN integration to follow).

Co-authored-by: Cursor <cursoragent@cursor.com>
…on id

When a session id is supplied externally the caller owns the session
lifecycle, matching iOS's isCustomSession. Skip both background-inactivity
and max-lifetime rotation so the seeded id is used unchanged for the
lifetime of the manager.

Co-authored-by: Cursor <cursoragent@cursor.com>
…pendency

The OpenTelemetry Android `activity` instrumentation is always suppressed
(its app/activity lifecycle spans are superseded by our own), so the
dependency was dead weight. Remove it (also drops `common-api`) and keep
suppressInstrumentation("activity") as a defensive guard in case a host
reintroduces it transitively.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ound state

AppLifecycleTracker only reports genuine transitions (it suppresses the
initial replay and never emits a catch-up background), so an SDK init while
the app is already backgrounded left LDSessionManager stuck in FOREGROUND
and skipped background-inactivity rotation until the next stop/start cycle.
Query the process lifecycle at init and mark the manager backgrounded when
appropriate; a later genuine onStart settles it back to foreground.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ted-tracing

* feat/android-external-session-id:
  fix(observability-android): prime session manager with initial background state
  chore(observability-android): drop unused activity instrumentation dependency
  feat(observability-android): disable auto rotation for external session id
  feat(observability-android): support external session id
… track

Reshape LDObserve.track properties from flat OpenTelemetry `Attributes` to a
plain `TrackProperties` dictionary, matching the iOS (`[String: Any]`) and
Android (`Map<String, Any?>`) surfaces. A shared converter flattens the payload
into attributes before recording the span:
- nested objects -> dot-separated keys (e.g. `user.id`)
- arrays of objects / mixed arrays -> indexed dotted keys (e.g. `products.0.price`)
- homogeneous scalar arrays -> array attributes
- null / undefined / unsupported values are skipped (never stringified)

The LDClient.track afterTrack hook now flattens its data the same way (with
reserved key/value taking precedence), so both track paths behave identically.
Adds unit tests for the converter and updates the example app's API screen to
exercise the plain (nested) variant.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld abelonogov-ld requested a review from a team as a code owner June 25, 2026 18:48
abelonogov-ld added a commit that referenced this pull request Jun 25, 2026
## Summary

This PR adds React Native distributed-tracing documentation and the
supporting `LDObserve` API surface, automatic `track` tracing via the LD
plugin hook, an iOS/Android-parity example app, the RN external
session-id wiring, and a tracing-origin matching fix.

## Changes

### Docs
- **Distributed tracing guide**
(`observability-react-native/guides/tracing.md`): a cookbook of common
patterns — root/nested spans, manual HTTP spans, automatic `fetch`/`XHR`
instrumentation, exception recording, correlated logs, explicit context
propagation across async boundaries, **independent root spans via `{
root: true }`**, span events, end-to-end mobile-to-backend trace linking
via `tracingOrigins`, and OpenTelemetry **baggage** propagation. Placed
under `guides/` (not the gitignored, Typedoc-generated `docs/`) and
linked from the README.
- **README**: added a "Guides" link and a `withSpan` "Manual tracing"
example.

### SDK (`observability-react-native`)
- **`withSpan` API** (`src/sdk/withSpan.ts`, `src/api/SpanScope.ts`): an
ergonomic wrapper that runs a callback inside a span and ends it
automatically, with `scope.child` to parent nested spans off the
captured context — important in RN where the active context is only
tracked synchronously (lost across `await`).
- **`LDObserve.track`** (`Observe`, `LDObserve`, `ObservabilityClient`,
`InstrumentationManager`): records a custom `track` span carrying the
event `key`, optional numeric `value`, and properties — mirroring the
iOS/Android `track` APIs.
- **Automatic `track` tracing hook** (`plugin/observability.ts`): the
Observability plugin's `TracingHook` now implements `afterTrack`, so
every `LDClient.track` call automatically emits a span (event `key`,
optional metric `value`, evaluation context keys, and primitive track
data) — no manual instrumentation needed. Flag-evaluation spans are
additionally tagged with an internal attribute so SDK-emitted telemetry
can be filtered out universally (independent of instrumentation scope).
- **Root spans**: `{ root: true }` on `startSpan`/`startActiveSpan` is
forwarded to the underlying OpenTelemetry tracer, which drops the active
span from context so the new span begins a fresh trace regardless of
ambient context.
- **tracingOrigins matching fix**
(`observability-shared/src/instrumentation/utils.ts` + tests): match
against the URL's origin (anchored host/subdomain regex) instead of an
unanchored substring, so a configured host appearing as a query-param
value in a third-party URL no longer leaks trace/baggage headers. String
entries are escaped so `.` is literal.

### React Native external session id (`react-native-ld-session-replay`)
- Wire the JS `sessionId` through to the native Android SDK so RN,
native observability, and session replay share one `session.id`
(`SessionReplayClientAdapter.kt`/`.swift`,
`NativeSessionReplayReactNative.ts`, `src/index.tsx`).
- Bump `com.launchdarkly:launchdarkly-android-client-sdk` and the iOS
`LaunchDarklySessionReplay` pod to `0.46.1`.

### Example app (`react-native-ld-session-replay/example`)
- New **Tracing** tab (`TracingScreen.tsx`) exercising every recipe in
the guide, and an **API** tab (`ApiScreen.tsx`) mirroring the iOS
TestApp (`MainMenuViewModel`): identify, spans, metrics, logs, errors,
track (via LD client), network, crash.
- The independent-root-spans recipe creates root spans **inside an
active parent** and asserts each child trace is detached from the parent
and mutually unique (logs `PASS`/`FAIL`), so it actually exercises `{
root: true }`.
- Register the `Observability` plugin with demo `tracingOrigins` in
`App.tsx`; add `@launchdarkly/observability-react-native` +
`@opentelemetry/api` deps; monorepo `metro.config.js` resolution
(workspace watch folders + forced react/react-native singletons).
- Repo hygiene: gitignore the example's iOS/Ruby lock files and stop
tracking `Podfile.lock` / `Gemfile.lock`.

## Notes
- The original prompt referenced `react-native-ld-session-replay`, but
that package is purely session replay/masking; the tracing API lives in
`observability-react-native`, so the guide and APIs were added there.
- The **`LDObserve.track` plain-dictionary (nested) reshaping** is
intentionally split into a separate stacked PR (#650) on top of this
branch.

## Test plan
- [x] `observability-react-native` unit tests pass (incl. tracingOrigins
matching).
- [ ] Verify the guide renders on GitHub (Markdown + mermaid).
- [ ] Run the example app: Tracing tab recipes produce expected spans;
root-span recipe logs `PASS`; `LDClient.track` auto-emits a span via the
hook; API tab mirrors the iOS demo; shared `session.id` across JS +
native (Android).
- [ ] Confirm trace/baggage headers are only sent to configured
`tracingOrigins`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes public tracing APIs and distributed trace header matching
(security-sensitive); native session ID wiring and dependency bumps
affect session-replay integration.
> 
> **Overview**
> Adds **manual tracing for React Native** via `LDObserve.withSpan` and
`SpanScope` (`child`, `active`, `ctx`) so span hierarchies survive
`await`s where OTel’s stack context does not, plus **`LDObserve.track`**
and an **`afterTrack`** plugin hook so `LDClient.track` emits `track`
spans like other mobile SDKs. Flag-evaluation spans get
**`launchdarkly.internal`** for filtering.
> 
> Ships a **tracing cookbook** (`guides/tracing.md`) and README updates;
the session-replay **example** gains Observability, **Tracing** and
**API** tabs, and Metro monorepo fixes.
> 
> **Security / propagation:** `tracingOrigins` string hosts are
**origin-anchored** in `observability-shared` so trace/baggage headers
are not matched inside third-party URLs.
> 
> **Session replay:** Android forwards JS **`sessionId`** /
**`serviceVersion`** into native observability; bumps Android/iOS LD
dependencies; example lockfiles are gitignored.
> 
> **E2E:** Android sample sets `sessionBackgroundTimeout` to 3 minutes.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9d06f16. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Base automatically changed from docs/rn-distributed-tracing to main June 25, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant