diff --git a/.gitignore b/.gitignore index fb5f955dba..3209752e12 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,9 @@ sdk/@launchdarkly/mobile-dotnet/**/*.csproj.user sdk/@launchdarkly/mobile-dotnet/**/*.sln sdk/@launchdarkly/mobile-dotnet/**/nupkgs/ sdk/@launchdarkly/mobile-dotnet/**/xcuserdata/ + +# React Native session-replay example: local iOS/Ruby tooling locks +# (regenerated per-machine by `pod install` / `bundle install`). +# NOTE: the monorepo yarn.lock is intentionally NOT ignored. +sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock +sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile.lock diff --git a/e2e/android/app/src/compose/java/com/example/androidobservability/BaseApplication.kt b/e2e/android/app/src/compose/java/com/example/androidobservability/BaseApplication.kt index 46d4ffa4fe..7aef09840d 100644 --- a/e2e/android/app/src/compose/java/com/example/androidobservability/BaseApplication.kt +++ b/e2e/android/app/src/compose/java/com/example/androidobservability/BaseApplication.kt @@ -20,6 +20,7 @@ import com.launchdarkly.sdk.android.LDClient import com.launchdarkly.sdk.android.LDConfig import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes +import kotlin.time.Duration.Companion.minutes open class BaseApplication : Application() { @@ -43,6 +44,7 @@ open class BaseApplication : Application() { debug = true, otlpEndpoint = BuildConfig.OTLP_ENDPOINT, backendUrl = BuildConfig.BACKEND_URL, + sessionBackgroundTimeout = 3.minutes, tracesApi = ObservabilityOptions.TracesApi.enabled(), metricsApi = ObservabilityOptions.MetricsApi.enabled(), instrumentations = ObservabilityOptions.Instrumentations( diff --git a/sdk/@launchdarkly/observability-react-native/README.md b/sdk/@launchdarkly/observability-react-native/README.md index d1126a6fb3..458a4a4426 100644 --- a/sdk/@launchdarkly/observability-react-native/README.md +++ b/sdk/@launchdarkly/observability-react-native/README.md @@ -39,6 +39,34 @@ const client = new LDClient( ); ``` +### Manual tracing + +Use the `LDObserve` singleton to create spans by hand. `withSpan` runs your +callback inside a span and ends it automatically — even across `await`s, where +React Native only tracks the active context synchronously. Use `scope.child` to +parent nested spans off the captured context: + +```typescript +import { LDObserve } from '@launchdarkly/observability-react-native'; + +await LDObserve.withSpan('LoadProducts', async (scope) => { + scope.span.setAttribute('source', 'api'); + + // `scope.child` parents off LoadProducts even after the await above. + const products = await scope.child('FetchFromApi', async (fetchScope) => { + const response = await fetch('https://api.example.com/products'); + fetchScope.span.setAttribute('http.status_code', response.status); + return response.json(); + }); + + scope.span.setAttribute('product_count', products.length); +}); +``` + +## Guides + +- [Tracing Guide](guides/tracing.md) — a cookbook of common tracing patterns (spans, nested operations, error handling, correlated logs, and end-to-end mobile-to-backend traces). + ## About LaunchDarkly - LaunchDarkly Observability provides a way to collect and send errors, logs, traces to LaunchDarkly. Correlate latency or exceptions with your releases to safely ship code. diff --git a/sdk/@launchdarkly/observability-react-native/guides/tracing.md b/sdk/@launchdarkly/observability-react-native/guides/tracing.md new file mode 100644 index 0000000000..6e4d4e6e0e --- /dev/null +++ b/sdk/@launchdarkly/observability-react-native/guides/tracing.md @@ -0,0 +1,733 @@ +# Tracing Guide + +This cookbook covers common tracing patterns with the LaunchDarkly Observability SDK for React Native — from simple spans to nested operations, error handling, correlated logs, and end-to-end mobile-to-backend distributed traces. Each recipe is self-contained and demonstrates a single concept with realistic examples. + +Each runnable recipe below matches the example screen [`TracingScreen.tsx`](../../react-native-ld-session-replay/example/src/TracingScreen.tsx) verbatim (only the on-screen `log(...)` lines are omitted here); a few recipes additionally show the lower-level explicit form for reference. The endpoints are inlined so each recipe stands on its own; requests to the `jsonplaceholder` host carry trace/baggage headers because it is configured as a tracing origin (see [recipe 11](#11-connecting-mobile-traces-to-your-backend-end-to-end-distributed-tracing)). They assume the SDK has already been initialized (see [Usage](../README.md#usage)) and the following imports are present: + +```typescript +import { Platform } from 'react-native' +import { LDObserve } from '@launchdarkly/observability-react-native' +import { context, propagation, SpanStatusCode, trace } from '@opentelemetry/api' +``` + +Spans returned by the SDK are standard OpenTelemetry [`Span`](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.Span.html) objects, so every span operation in this guide is the regular OpenTelemetry API. + +> **A note on context propagation in React Native.** React Native uses OpenTelemetry's `StackContextManager` (there is no `AsyncLocalStorage`/`AsyncLocal` equivalent), so the active span is tracked **only synchronously**. It is **not** restored after an `await`, `setTimeout`, `Promise` callback, or event handler — including across `await`s *within the same* `startActiveSpan` callback. In practice this means anything created in the synchronous part of a callback (before the first `await`) is parented automatically, but any span or log created **after an `await`** would become a new root unless you parent it explicitly. +> +> The SDK provides [`LDObserve.withSpan`](#0-recommended-withspan-for-nested-async-work) to handle this for you — it ends spans automatically and parents children off a captured context, so the hierarchy survives `await`s without manual plumbing. **Prefer `withSpan` for nested async work.** The lower-level recipes below also show the explicit `startActiveSpan`/`startSpan` + `ctx` form so you understand what `withSpan` does under the hood, and for cases where you need full manual control. + +--- + +## 0. Recommended: `withSpan` for nested async work + +`LDObserve.withSpan(name, fn, options?)` starts a span, runs `fn` within it, and **ends it automatically** (status `OK` on success, `ERROR` with the error recorded if `fn` throws or rejects). The callback receives a `SpanScope` that solves the context-propagation problem above: + +- `scope.span` — the underlying OpenTelemetry span (set attributes, add events, etc.). +- `scope.child(name, fn, options?)` — start a child span parented to **this** scope. Because the parent comes from the captured scope rather than the (lost) active context, children nest correctly **even after an `await`**, and even across concurrent (`Promise.all`) work. +- `scope.active(fn)` — run `fn` with this span active. Use it to parent **auto-instrumented** `fetch`/`XMLHttpRequest` spans that start *after* an `await` (see [recipe 4](#4-automatic-fetch--xmlhttprequest-instrumentation-under-a-custom-parent)). +- `scope.ctx` — this span's `Context`, for the rare case you need to pass an explicit parent elsewhere (e.g. a `setTimeout` callback — see [recipe 8](#8-creating-a-child-span-where-automatic-propagation-wont-work)). + +The nested workflow from [recipe 2](#2-nested-spans-for-a-typical-react-native-workflow), written with `withSpan`: + +```typescript +const nestedSpans = async () => { + // `withSpan` ends each span automatically and `scope.child` parents off the + // captured context, so the LoadProducts > FetchFromApi > DeserializeJson / + // RenderUI hierarchy survives the `await`s without threading context by hand + // (React Native's StackContextManager only tracks the active span + // synchronously). + const count = await LDObserve.withSpan('LoadProducts', async (load) => { + const items = await load.child('FetchFromApi', async (fetchScope) => { + const response = await fetch('https://jsonplaceholder.typicode.com/posts') + fetchScope.span.setAttribute('http.status_code', response.status) + const json = await response.text() + // Parents to FetchFromApi even though we are past two awaits. + return fetchScope.child('DeserializeJson', (parseScope) => { + const result = JSON.parse(json) as unknown[] + parseScope.span.setAttribute('product_count', result.length) + return result + }) + }) + // Parents to LoadProducts (not FetchFromApi) — uses the captured context. + load.child('RenderUI', (renderScope) => { + renderScope.span.setAttribute('product_count', items.length) + }) + return items.length + }) +} +``` + +No `getContextFromSpan`, no `ctx` threading, no manual `span.end()`. The trace tree is identical to recipe 2: + +```mermaid +graph TD + A[LoadProducts] --> B[FetchFromApi] + B --> C[DeserializeJson] + A --> D[RenderUI] +``` + +**Concurrency bonus.** `startActiveSpan` shares a single active-context stack, so interleaved `Promise.all` work corrupts parentage. `withSpan` does not — each `child` parents off its own captured context: + +```typescript +await LDObserve.withSpan('LoadDashboard', async (root) => { + const [profile, feed] = await Promise.all([ + root.child('fetch.profile', async ({ span }) => { + const res = await fetch('https://api.example.com/me') + span.setAttribute('http.status_code', res.status) + return res.json() + }), + root.child('fetch.feed', async ({ span }) => { + const res = await fetch('https://api.example.com/feed') + span.setAttribute('http.status_code', res.status) + return res.json() + }), + ]) + // both spans correctly parent to LoadDashboard +}) +``` + +> `withSpan` is built entirely on the explicit `startSpan` + `parent` context mechanism shown in the recipes below — it is sugar, not magic. The one thing it cannot change is **auto-instrumented** `fetch`/XHR parenting after an `await`; use `scope.active(...)` there. + +--- + +## 1. Start a Root Span + +Create an independent span that begins a brand-new trace by passing `{ root: true }`. `startActiveSpan` makes the span active for the duration of the callback; as in standard OpenTelemetry, it does **not** end the span for you, so call `span.end()` when the work is done (otherwise the span is never exported). + +```typescript +const rootSpan = () => { + LDObserve.startActiveSpan( + 'app-cold-start', + (span) => { + span.setAttribute('launch_type', 'cold') + span.setAttribute('device_os', Platform.OS) + span.addEvent('splash_rendered') + span.addEvent('home_screen_ready') + span.end() + }, + { root: true }, + ) +} +``` + +If you need to control the span's lifetime manually (for example, it ends in a different function), use `startSpan` and call `span.end()` yourself: + +```typescript +const span = LDObserve.startSpan('app-cold-start', { root: true }) +span.setAttribute('launch_type', 'cold') +// ... later ... +span.end() +``` + +Use `{ root: true }` when you want a span that is guaranteed to start a new trace, regardless of any ambient context. + +--- + +## 2. Nested Spans for a Typical React Native Workflow + +> For most code you should use [`LDObserve.withSpan`](#0-recommended-withspan-for-nested-async-work), which handles everything below automatically. This recipe shows the explicit form so you understand the underlying mechanism and can use it when you need full manual control. + +`startActiveSpan` parents each new span under the currently active one — but only **synchronously**. In React Native the active context is lost after every `await` (see the note above), so a span created after an `await` would become a new root instead of nesting. To keep the hierarchy, capture each parent's context with `LDObserve.getContextFromSpan` and pass it as the final argument when you start a child after an `await`. + +```typescript +async function loadProducts() { + return LDObserve.startActiveSpan('LoadProducts', async (loadSpan) => { + // The active context is lost across each `await`, so capture parents and + // pass them explicitly to keep the LoadProducts > FetchFromApi > + // DeserializeJson / RenderUI hierarchy. + const loadCtx = LDObserve.getContextFromSpan(loadSpan) + + const products = await LDObserve.startActiveSpan( + 'FetchFromApi', + async (fetchSpan) => { + const fetchCtx = LDObserve.getContextFromSpan(fetchSpan) + const response = await fetch('https://api.example.com/products') + fetchSpan.setAttribute('http.status_code', response.status) + const json = await response.text() + + // DeserializeJson runs after `await`, so parent it on fetchCtx. + const parsed = LDObserve.startActiveSpan( + 'DeserializeJson', + (parseSpan) => { + const result = JSON.parse(json) as Product[] + parseSpan.setAttribute('product_count', result.length) + parseSpan.end() + return result + }, + undefined, // no extra span options + fetchCtx, // explicit parent context + ) + fetchSpan.end() + return parsed + }, + undefined, + loadCtx, // FetchFromApi is started synchronously, but passing loadCtx is + // harmless and keeps the intent explicit. + ) + + // RenderUI runs after the `await` above, so parent it on loadCtx. + LDObserve.startActiveSpan( + 'RenderUI', + (renderSpan) => { + renderSpan.setAttribute('product_count', products.length) + setProducts(products) + renderSpan.end() + }, + undefined, + loadCtx, + ) + + loadSpan.end() + return products + }) +} +``` + +> Without the explicit `ctx` arguments, `DeserializeJson` and `RenderUI` — both created after an `await` — would have no `parent_span_id` and show up as separate root traces instead of children of `LoadProducts`. + +The resulting trace tree: + +```mermaid +graph TD + A[LoadProducts] --> B[FetchFromApi] + B --> C[DeserializeJson] + A --> D[RenderUI] +``` + +--- + +## 3. HTTP Call Span with Manual Error Handling + +Wrap a network call in a span to capture timing, status codes, and errors in a single trace unit. + +```typescript +const httpWithErrorHandling = async () => { + await LDObserve.startActiveSpan('FetchUserProfile', async (span) => { + span.setAttribute('user.id', '1') + span.setAttribute('http.method', 'GET') + try { + const url = 'https://jsonplaceholder.typicode.com/users/1' + span.setAttribute('http.url', url) + const response = await fetch(url) + span.setAttribute('http.status_code', response.status) + if (!response.ok) { + span.setStatus({ code: SpanStatusCode.ERROR }) + span.setAttribute('error.type', `HTTP ${response.status}`) + return + } + await response.json() + span.setStatus({ code: SpanStatusCode.OK }) + } catch (err) { + span.recordException(err as Error) + span.setStatus({ code: SpanStatusCode.ERROR }) + } finally { + span.end() + } + }) +} +``` + +> `startActiveSpan` does not end the span for you. Always call `span.end()` yourself — a `finally` block is a good place so the span is closed even if the callback throws or returns early. + +--- + +## 4. Automatic `fetch` / `XMLHttpRequest` Instrumentation Under a Custom Parent + +The SDK auto-instruments `fetch` and `XMLHttpRequest` (unless `disableTraces` is set). Every network request generates its own span automatically. If a custom span is active at call time, the auto-generated HTTP span becomes its child. + +```typescript +async function syncOrders() { + await LDObserve.startActiveSpan('SyncOrders', async (span) => { + span.setAttribute('sync.direction', 'pull') + + // The HTTP span for this fetch is auto-created as a child of "SyncOrders" + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts?_limit=5', + ) + span.setAttribute('http.status_code', response.status) + + const orders = (await response.json()) as unknown[] + span.setAttribute('order_count', orders.length) + span.end() + }) +} +``` + +The resulting trace tree: + +```mermaid +graph TD + A[SyncOrders] --> B["HTTP GET /orders (auto-instrumented)"] +``` + +You do not need to create a span for the HTTP call itself; the SDK handles it. Your business-logic span provides the parent context. The key requirement is that the request happens while your span is **active** — i.e. inside a `startActiveSpan` (or `withSpan`) callback. + +The same recipe with `withSpan` (note the request is in the synchronous window before the first `await`, so it auto-parents): + +```typescript +const autoInstrumentedChild = async () => { + await LDObserve.withSpan('SyncOrders', async ({ span }) => { + span.setAttribute('sync.direction', 'pull') + // `withSpan` makes SyncOrders active for the synchronous window, so this + // fetch (started before the first await) auto-parents to it. + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts?_limit=5', + ) + span.setAttribute('http.status_code', response.status) + const orders = (await response.json()) as unknown[] + span.setAttribute('order_count', orders.length) + }) +} +``` + +> **After an `await`, the active context is gone**, so a `fetch` started later would create a *root* HTTP span. Auto-instrumentation reads the active context at call time and can't see your captured scope — so wrap such calls in `scope.active(...)`: +> +> ```typescript +> await LDObserve.withSpan('Checkout', async (scope) => { +> const cart = await loadCart() // <- await drops the active context +> // Re-establish it so the auto HTTP span parents to "Checkout": +> const res = await scope.active(() => +> fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST' }), +> ) +> scope.span.setAttribute('http.status_code', res.status) +> }) +> ``` + +--- + +## 5. Record Exception and Mark Span as Failed + +Use `span.recordException` to attach structured error data to a span, then `span.setStatus` to mark it as failed. Pair it with `LDObserve.recordError` to surface the error in your error backend. + +```typescript +const recordException = async () => { + await LDObserve.startActiveSpan('ProcessPayment', async (span) => { + span.setAttribute('order.id', 'order-42') + span.setAttribute('payment.amount', 19.99) + try { + // Simulate a gateway failure. + throw new Error('Payment gateway timeout') + } catch (err) { + const error = err as Error + span.recordException(error) + span.setStatus({ code: SpanStatusCode.ERROR }) + span.setAttribute('error.category', error.name) + // Pass the active span so the error attaches to it instead of a new one. + LDObserve.recordError(error, { 'order.id': 'order-42' }, { span }) + } finally { + span.end() + } + }) +} +``` + +`span.recordException` adds an `exception` event with `exception.type`, `exception.message`, and `exception.stacktrace` attributes following the OpenTelemetry semantic conventions. Passing `{ span }` to `recordError` records the error against the span you already created; omit it and the SDK attaches the error to the currently active span (or creates a short-lived one). + +--- + +## 6. Correlated Logs Inside the Active Span + +When a span is active, `LDObserve.recordLog` automatically picks up the ambient trace and span IDs from the active context. That works for logs emitted in the **synchronous** part of the callback. After an `await` the active context is gone (see the note above), so re-establish it with `scope.active(...)` (or `context.with(capturedContext, ...)`) for any log you emit later. + +```typescript +const correlatedLogs = async () => { + await LDObserve.withSpan('ImportCatalog', async ({ span, active }) => { + // Logs correlate to a span via the active context. The first log is in the + // synchronous window so it correlates automatically. + LDObserve.recordLog('Import started', 'info', { source: 'demo' }) + let imported = 0 + for (const _row of [1, 2, 3, 4, 5]) { + await new Promise((r) => setTimeout(r, 5)) + imported++ + } + span.setAttribute('imported_count', imported) + // After the awaits the active context is gone — re-establish it with + // `active()` so this log still correlates to ImportCatalog. + active(() => + LDObserve.recordLog('Import completed', 'info', { + imported_count: imported, + }), + ) + }) +} +``` + +Both log records carry the same `traceId` and `spanId` as the `ImportCatalog` span, linking them together in your observability backend — the first because the span is active synchronously, the second because `scope.active(...)` re-establishes that span's context. + +--- + +## 7. Re-establishing Context to Correlate Logs Across Async Boundaries + +The active context is not automatically restored inside a detached callback such as `setTimeout`, a timer, or an event handler that runs after the original `startActiveSpan` callback has returned. Capture the span's context with `LDObserve.getContextFromSpan` and re-activate it with `context.with` so the log correlates with the right span. + +```typescript +const correlateAcrossAsync = () => { + const span = LDObserve.startSpan('UploadReport') + span.setAttribute('report.type', 'daily') + const capturedContext = LDObserve.getContextFromSpan(span) + span.end() + + setTimeout(() => { + // Active context is empty here -- re-establish it explicitly. + context.with(capturedContext, () => { + LDObserve.recordLog('Upload processing on background tick', 'info', { + phase: 'start', + }) + LDObserve.recordLog('Upload complete', 'info', { phase: 'end' }) + }) + }, 0) +} +``` + +Every `recordLog` call made inside `context.with(capturedContext, ...)` is stamped with the `UploadReport` span's `traceId` and `spanId`. + +--- + +## 8. Creating a Child Span Where Automatic Propagation Won't Work + +Sometimes you need a full child *span* (not just a correlated log) in a callback where the active context has been lost. `withSpan` accepts an explicit `parent` context via its options (and `startSpan`/`startActiveSpan` accept one as their final argument). Capture the parent scope's `ctx` (or use `getContextFromSpan`) and pass it through once the deferred work runs. + +```typescript +const detachedChildSpan = () => { + LDObserve.withSpan('ScheduleSync', ({ span, ctx }) => { + span.setAttribute('sync.mode', 'background') + + // setTimeout drops the active context -> capture ScheduleSync's `ctx` and + // pass it as the explicit `parent` once the timer fires. + setTimeout(async () => { + await LDObserve.withSpan( + 'BackgroundSync', + async ({ span: childSpan }) => { + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts/1', + ) + childSpan.setAttribute('http.status_code', response.status) + childSpan.addEvent('sync.complete') + }, + { parent: ctx }, + ) + }, 0) + }) +} +``` + +The same technique applies to recurring timer callbacks (here bounded to three ticks; `intervalRef` is a `useRef` held by the screen): + +```typescript +const startBoundedPolling = () => { + if (intervalRef.current) { + return + } + const span = LDObserve.startSpan('StartPolling') + const parentContext = LDObserve.getContextFromSpan(span) + span.end() + + let ticks = 0 + intervalRef.current = setInterval(() => { + ticks++ + LDObserve.withSpan( + 'PollTick', + ({ span: pollSpan }) => { + pollSpan.setAttribute('tick.time', new Date().toISOString()) + pollSpan.setAttribute('tick.number', ticks) + }, + { parent: parentContext }, + ) + if (ticks >= 3 && intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, 1000) +} +``` + +The resulting trace has `StartPolling` as the short-lived parent, with each `PollTick` appearing as a child span fired at one-second intervals. + +If you need a span that is *not* active but still parented explicitly, use `startSpan`: + +```typescript +const childSpan = LDObserve.startSpan('DetachedWork', undefined, parentContext) +// ... do work ... +childSpan.end() +``` + +--- + +## 9. Sequential Independent Root Spans + +Use `{ root: true }` to create spans that each begin a separate trace, **even when there is an active span on the current context**. This is useful for batch operations or analytics events where each item should be its own trace rather than nesting under the surrounding work. + +```typescript +const independentRootSpans = () => { + const events = [ + { type: 'view', userId: 'u1' }, + { type: 'click', userId: 'u2' }, + { type: 'purchase', userId: 'u3' }, + ] + // Created inside an active parent span on purpose: with `{ root: true }` each + // analytics span must start a brand-new trace instead of nesting under the + // parent. We then assert every child trace differs from the parent's and from + // each other, so this actually exercises `root` rather than relying on there + // being no ambient context. + LDObserve.startActiveSpan('AnalyticsBatch', (parent) => { + const parentTrace = parent.spanContext().traceId + const childTraces: string[] = [] + for (const evt of events) { + LDObserve.startActiveSpan( + `Analytics:${evt.type}`, + (span) => { + span.setAttribute('event.type', evt.type) + span.setAttribute('event.timestamp', new Date().toISOString()) + span.setAttribute('event.user_id', evt.userId) + span.setStatus({ code: SpanStatusCode.OK }) + childTraces.push(span.spanContext().traceId) + span.end() + }, + { root: true }, + ) + } + parent.end() + + // Each child started its own trace, distinct from the batch and each other. + const detachedFromParent = childTraces.every((t) => t !== parentTrace) + const allUnique = new Set(childTraces).size === childTraces.length + console.log('independent roots:', { detachedFromParent, allUnique }) + }) +} +``` + +Each iteration creates an independent trace: every child `traceId` differs from the enclosing `AnalyticsBatch` trace and from the other children. Drop `{ root: true }` and the children would instead share `AnalyticsBatch`'s `traceId`. + +> The `{ root: true }` option is honored by the underlying OpenTelemetry tracer — it drops the active span from context so the new span starts a fresh trace. It only matters when a span is currently active; with no ambient context a span is already a root. + +--- + +## 10. Span Events as Lightweight Checkpoints + +Use `span.addEvent` to mark milestones within a long-running span without creating child spans. Events are cheaper than spans and ideal for logging progress through a linear pipeline. + +```typescript +const spanEvents = async () => { + await LDObserve.withSpan('DownloadAndCacheImage', async ({ span }) => { + const imageUrl = 'https://reactnative.dev/img/tiny_logo.png' + span.setAttribute('image.url', imageUrl) + span.addEvent('download.started') + const response = await fetch(imageUrl) + const blob = await response.blob() + span.addEvent('download.completed') + span.setAttribute('image.size_bytes', blob.size) + span.addEvent('cache.write.started') + // (no real filesystem write in the demo) + span.addEvent('cache.write.completed', { bytes: blob.size }) + }) +} +``` + +You can also attach attributes to an event: `span.addEvent('cache.write.completed', { bytes: blob.size })`. + +--- + +## 11. Connecting Mobile Traces to Your Backend (End-to-End Distributed Tracing) + +The real power of distributed tracing is linking a span on the device to the spans your backend produces for the same request. The SDK does this automatically by injecting a W3C `traceparent` header into outgoing requests — but **only** for URLs you opt in via the `tracingOrigins` option. This prevents leaking trace headers to third-party domains. + +```typescript +new Observability({ + serviceName: 'my-react-native-app', + // Attach trace headers to requests whose URL matches any of these entries. + tracingOrigins: ['api.example.com', /\.internal\.example\.com$/], +}) +``` + +With `tracingOrigins` configured, any `fetch`/`XHR` request to a matching host carries a `traceparent` header, so the backend continues the same trace: + +```typescript +const backendDistributedTrace = async () => { + await LDObserve.withSpan('Checkout', async ({ span }) => { + span.setAttribute('cart.id', 'cart-7') + // traceparent is injected automatically because the host is a tracing + // origin -> a backend span would join this trace. + const response = await fetch('https://jsonplaceholder.typicode.com/posts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cartId: 'cart-7' }), + }) + span.setAttribute('http.status_code', response.status) + }) +} +``` + +```mermaid +graph TD + A["Checkout (mobile)"] --> B["POST /checkout (auto-instrumented, mobile)"] + B --> C["/checkout handler (backend)"] + C --> D["charge-payment (backend)"] +``` + +### Continuing a trace from incoming headers + +If your app receives work along with trace headers (for example, a push payload or a webhook-style message that carries `x-request-id` / `x-session-id`), use the header helpers to start a span annotated with that context: + +```typescript +const incomingHeaders = () => { + const headers = { + 'x-session-id': 'sess-abc', + 'x-request-id': 'req-123', + } + // Run a callback inside a span that records the incoming headers as attributes. + LDObserve.runWithHeaders('HandlePushPayload', headers, (span) => { + span.setAttribute('payload.kind', 'promo') + }) + // Extract just the correlation IDs (sessionId, requestId) without a span. + const ctx = LDObserve.parseHeaders(headers) +} +``` + +> Prefer a span you end yourself? `LDObserve.startWithHeaders('HandlePushPayload', headers)` returns a started span (records the same `http.header.*` attributes); call `span.end()` when the work completes. + +### Suppressing propagation for specific URLs + +Use `urlBlocklist` to ensure trace headers are never attached (and request bodies/headers are never recorded) for sensitive endpoints, even if they would otherwise match `tracingOrigins`: + +```typescript +new Observability({ + tracingOrigins: ['api.example.com'], + urlBlocklist: ['api.example.com/auth', 'api.example.com/payment'], +}) +``` + +--- + +## 12. Propagating Baggage Across Spans and Services + +Baggage is a set of key-value pairs that travels with the active context — across child spans and, when a request targets one of your `tracingOrigins`, across the network to your backend via the W3C `baggage` header. The SDK registers a `W3CBaggagePropagator` by default, so you only need the standard OpenTelemetry baggage API. + +Add the import: + +```typescript +import { propagation } from '@opentelemetry/api' +``` + +Set baggage and run work within it. Spans started inside the callback — and outgoing `fetch`/`XHR` requests to tracing origins — carry these entries: + +```typescript +const baggage = async () => { + const bag = propagation.createBaggage({ + 'app.tenant_id': { value: 'acme' }, + 'app.user_tier': { value: 'gold' }, + }) + await context.with( + propagation.setBaggage(context.active(), bag), + async () => { + // Read it back. + const tenant = propagation + .getActiveBaggage() + ?.getEntry('app.tenant_id')?.value + + await LDObserve.startActiveSpan('LoadDashboard', async (span) => { + // Baggage is not copied onto spans automatically -- do it explicitly. + propagation + .getActiveBaggage() + ?.getAllEntries() + .forEach(([key, entry]) => span.setAttribute(key, entry.value)) + + // Outgoing request to a tracing origin also carries the baggage header. + await fetch('https://jsonplaceholder.typicode.com/posts/1') + span.end() + }) + }, + ) +} +``` + +Read baggage anywhere it is active: + +```typescript +const current = propagation.getActiveBaggage() +const tenantId = current?.getEntry('app.tenant_id')?.value +``` + +Add to (or update) existing baggage without dropping what is already there — `Baggage` is immutable, so each change returns a new instance: + +```typescript +const existing = propagation.getActiveBaggage() ?? propagation.createBaggage() +const updated = existing.setEntry('app.experiment', { value: 'new-checkout' }) + +context.with(propagation.setBaggage(context.active(), updated), () => { + // ... work that should see the added entry ... +}) +``` + +> **Baggage is not automatically copied onto spans.** It rides along the context and is sent to the backend, but it does not become span attributes unless you put it there. Copy the entries you want to query on explicitly: +> +> ```typescript +> const bag = propagation.getActiveBaggage() +> bag?.getAllEntries().forEach(([key, entry]) => span.setAttribute(key, entry.value)) +> ``` + +Like trace headers, the `baggage` header is only attached to requests whose URL matches `tracingOrigins`, and `urlBlocklist` suppresses it. Avoid putting sensitive data in baggage, and keep entries small — they are sent on every matching request. + +--- + +## Quick Reference + +### Span Creation (`LDObserve`) + +| Method | Parent | Returns | +|---|---|---| +| `withSpan(name, fn, options?)` | Captured scope context (or `options.parent`); **auto-ends** the span | result of `fn` | +| `startActiveSpan(name, fn, options?, ctx?)` | Current active span (or `ctx` if provided) | result of `fn` | +| `startActiveSpan(name, fn, { root: true })` | None (new trace) | result of `fn` | +| `startSpan(name, options?, ctx?)` | Current active span (or `ctx`); span is **not** made active | `Span` | +| `startSpan(name, { root: true })` | None (new trace) | `Span` | +| `runWithHeaders(name, headers, cb, options?)` | Current active span; records `http.header.*` attributes | result of `cb` | +| `startWithHeaders(name, headers, options?)` | Current active span; records `http.header.*` attributes | `Span` | +| `getContextFromSpan(span)` | -- | `Context` (for explicit re-parenting) | +| `parseHeaders(headers)` | -- | `RequestContext` (`sessionId`, `requestId`) | + +> Both `startActiveSpan` and `startSpan` require a manual `span.end()`. The difference is that `startActiveSpan` makes the span active (the current parent) for the duration of its callback, while `startSpan` does not change the active context. `withSpan` is the recommended wrapper: it ends the span automatically and its `scope.child(...)` keeps nesting correct across `await`s. + +> **`withSpan` scope** — the callback receives a `SpanScope`: `scope.span` (the OTel `Span`), `scope.child(name, fn, options?)` (start a correctly-parented child), `scope.active(fn)` (run with this span active, e.g. for auto-instrumented `fetch`/XHR after an `await`), and `scope.ctx` (the span's `Context`). + +### Span Operations (OpenTelemetry `Span`) + +| Method | Description | +|---|---| +| `span.setAttribute(key, value)` | Set a single key-value attribute on the span | +| `span.setAttributes({ ... })` | Set multiple attributes at once | +| `span.addEvent(name, attributes?)` | Record a named event (lightweight checkpoint) | +| `span.recordException(error)` | Attach exception details as a span event | +| `span.setStatus({ code: SpanStatusCode.OK })` | Mark the span as successful | +| `span.setStatus({ code: SpanStatusCode.ERROR })` | Mark the span as failed | +| `span.spanContext()` | Get the raw `SpanContext` (`traceId`, `spanId`, `traceFlags`) | +| `span.updateName(name)` | Rename the span | +| `span.end()` | Manually end the span | + +### Logs, Errors, and Metrics (`LDObserve`) + +| Method | Description | +|---|---| +| `recordLog(message, level, attributes?)` | Emit a structured log; auto-correlates with the active span | +| `recordError(error, attributes?, { span }?)` | Record an error; attaches to `span`, else the active span | +| `recordMetric(metric)` | Record a gauge value | +| `recordCount(metric)` | Add to a counter | +| `recordIncr(metric)` | Increment a counter by 1 | +| `recordHistogram(metric)` | Record a histogram value | +| `recordUpDownCounter(metric)` | Add to an up/down counter | +| `flush()` | Flush all pending telemetry (`Promise`) | +| `getSessionInfo()` | Get the current session info | +| `isInitialized()` | Whether the observability client is ready | + +> A `Metric` is `{ name: string; value: number; attributes?: Attributes; timestamp?: number }`. + +### Context Helpers (`@opentelemetry/api`) + +| Helper | Description | +|---|---| +| `trace.getActiveSpan()` | Get the currently active span, if any | +| `context.active()` | Get the currently active context | +| `context.with(ctx, fn)` | Run `fn` with `ctx` as the active context (re-establish across async boundaries) | +| `propagation.getActiveBaggage()` | Get the baggage from the active context, if any | +| `propagation.createBaggage(entries?)` | Create a `Baggage` (e.g. `{ key: { value } }`) | +| `propagation.setBaggage(ctx, baggage)` | Return a new context with the baggage attached | +| `baggage.setEntry` / `getEntry` / `getAllEntries` | Read or modify baggage entries (immutable; returns a new `Baggage`) | diff --git a/sdk/@launchdarkly/observability-react-native/src/api/Observe.ts b/sdk/@launchdarkly/observability-react-native/src/api/Observe.ts index 5cae3a490b..f3cd7925a6 100644 --- a/sdk/@launchdarkly/observability-react-native/src/api/Observe.ts +++ b/sdk/@launchdarkly/observability-react-native/src/api/Observe.ts @@ -7,6 +7,7 @@ import { import { Metric } from './Metric' import { RequestContext } from './RequestContext' import { SessionInfo } from '../client/SessionManager' +import { SpanScope, WithSpanOptions } from './SpanScope' export interface Observe { /** @@ -64,6 +65,20 @@ export interface Observe { */ recordLog(message: any, level: string, attributes?: Attributes): void + /** + * Record a custom track event as a `track` span. + * + * Mirrors the iOS and Android `LDObserve.track(...)` API (and `LDClient.track`): + * emits a span named `track` carrying the event `key`, an optional numeric + * `value` for LaunchDarkly numeric custom metrics, and any `properties` as + * additional span attributes. + * + * @param key The key for the event. + * @param properties Optional data associated with the event; attached as span attributes. + * @param metricValue Optional numeric value used by LaunchDarkly experimentation for numeric custom metrics. + */ + track(key: string, properties?: Attributes, metricValue?: number): void + /** * Parse headers to extract request context. * @param headers The headers to parse @@ -116,6 +131,30 @@ export interface Observe { ctx?: Context, ): T + /** + * Start a span, run `fn` within it, and end the span automatically. + * + * This is an ergonomic wrapper over {@link startSpan} designed for React + * Native, where the active context is tracked only synchronously and is lost + * across each `await`. The {@link SpanScope} passed to `fn` exposes a + * {@link SpanScope.child} method that parents child spans off the captured + * context, so the hierarchy is preserved across `await`s and even under + * concurrent (`Promise.all`) work — without manually threading context. + * + * The span's status is set to `OK` on success, or `ERROR` (with the error + * recorded) if `fn` throws or returns a rejecting promise. If `fn` returns a + * promise, the span ends when it settles and the promise is returned. + * + * @param spanName The span name + * @param fn The callback to run within the span's scope + * @param options Optional span options, including an explicit `parent` + */ + withSpan( + spanName: string, + fn: (scope: SpanScope) => T, + options?: WithSpanOptions, + ): T + /** * Get the context from a span. * @param span The span to get the context from diff --git a/sdk/@launchdarkly/observability-react-native/src/api/SpanScope.ts b/sdk/@launchdarkly/observability-react-native/src/api/SpanScope.ts new file mode 100644 index 0000000000..b5894cade0 --- /dev/null +++ b/sdk/@launchdarkly/observability-react-native/src/api/SpanScope.ts @@ -0,0 +1,67 @@ +import { Context, Span as OtelSpan, SpanOptions } from '@opentelemetry/api' + +/** + * Options accepted by {@link Observe.withSpan} and {@link SpanScope.child}. + * + * In addition to the standard OpenTelemetry {@link SpanOptions}, an explicit + * `parent` context can be supplied. This is the mechanism that makes spans + * nest correctly across `await`s in React Native, where the active context is + * tracked only synchronously (see the distributed tracing guide). + */ +export type WithSpanOptions = SpanOptions & { + /** + * The context to parent the new span under. When omitted the currently + * active context is used. {@link SpanScope.child} sets this automatically to + * the parent scope's context, so children nest correctly even after an + * `await`. + */ + parent?: Context +} + +/** + * A handle to a span started with {@link Observe.withSpan}. + * + * A scope captures its own span context, so child spans created via + * {@link SpanScope.child} are parented correctly even across `await` + * boundaries — without manually threading context through your code. + */ +export interface SpanScope { + /** The underlying OpenTelemetry span. */ + readonly span: OtelSpan + + /** + * This span's context. Pass it anywhere an explicit parent {@link Context} + * is required. + */ + readonly ctx: Context + + /** + * Start a child span parented to *this* scope, run `fn`, and end the span + * automatically. The span's status is set to `OK` on success, or `ERROR` + * (with the thrown error recorded) if `fn` throws or rejects. + * + * Because the parent is captured from this scope rather than read from the + * active context, the child nests correctly even when created after an + * `await`. + * + * @param name The child span name + * @param fn The callback to run within the child scope + * @param options Optional span options + */ + child( + name: string, + fn: (scope: SpanScope) => T, + options?: WithSpanOptions, + ): T + + /** + * Run `fn` with *this* span active. Use this to parent auto-instrumented + * `fetch`/`XMLHttpRequest` spans that are started after an `await` (the + * point at which React Native loses the active context). Calls started in + * the synchronous portion of a {@link Observe.withSpan} callback are already + * parented automatically. + * + * @param fn The callback to run with this span active + */ + active(fn: () => T): T +} diff --git a/sdk/@launchdarkly/observability-react-native/src/api/index.ts b/sdk/@launchdarkly/observability-react-native/src/api/index.ts index e6b4b542da..f47a26d9c3 100644 --- a/sdk/@launchdarkly/observability-react-native/src/api/index.ts +++ b/sdk/@launchdarkly/observability-react-native/src/api/index.ts @@ -1,3 +1,4 @@ export * from './Options' export * from './Metric' export * from './RequestContext' +export type { SpanScope, WithSpanOptions } from './SpanScope' diff --git a/sdk/@launchdarkly/observability-react-native/src/client/InstrumentationManager.ts b/sdk/@launchdarkly/observability-react-native/src/client/InstrumentationManager.ts index 6eb9d62e7e..948a14886e 100644 --- a/sdk/@launchdarkly/observability-react-native/src/client/InstrumentationManager.ts +++ b/sdk/@launchdarkly/observability-react-native/src/client/InstrumentationManager.ts @@ -6,6 +6,7 @@ import { Gauge, Histogram, Span as OtelSpan, + SpanKind, SpanOptions, trace, propagation, @@ -59,6 +60,9 @@ export type InstrumentationManagerOptions = Required & { projectId: string } +// Span name for custom track events, matching the iOS/Android SDKs (`track`). +const TRACK_SPAN_NAME = 'track' + export class InstrumentationManager { private traceProvider?: WebTracerProvider private loggerProvider?: LoggerProvider @@ -393,6 +397,36 @@ export class InstrumentationManager { } } + /** + * Single emitter for `track` spans, mirroring the iOS/Android `track` API. + * Emits a span named `track` carrying the event `key`, an optional numeric + * `value`, and any caller `properties` as attributes. + */ + public track( + key: string, + properties?: Attributes, + metricValue?: number, + ): void { + try { + const sessionId = this.sessionManager?.getSessionInfo().sessionId + const attributes: Attributes = { + ...(properties ?? {}), + key, + ...(metricValue !== undefined ? { value: metricValue } : {}), + ...(sessionId ? { ['highlight.session_id']: sessionId } : {}), + } + + this.getTracer() + .startSpan(TRACK_SPAN_NAME, { + kind: SpanKind.CLIENT, + attributes, + }) + .end() + } catch (e) { + console.error('Failed to record track event:', e) + } + } + public runWithHeaders( name: string, headers: Record, diff --git a/sdk/@launchdarkly/observability-react-native/src/client/ObservabilityClient.ts b/sdk/@launchdarkly/observability-react-native/src/client/ObservabilityClient.ts index 1c8f1d2b4e..1d34c54dc7 100644 --- a/sdk/@launchdarkly/observability-react-native/src/client/ObservabilityClient.ts +++ b/sdk/@launchdarkly/observability-react-native/src/client/ObservabilityClient.ts @@ -167,6 +167,15 @@ export class ObservabilityClient { this.instrumentationManager.recordLog(message, level, attributes) } + public track( + key: string, + properties?: Attributes, + metricValue?: number, + ): void { + if (this.options.disableTraces) return + this.instrumentationManager.track(key, properties, metricValue) + } + public parseHeaders(headers: Record): RequestContext { const sessionInfo = this.sessionManager.getSessionInfo() return { diff --git a/sdk/@launchdarkly/observability-react-native/src/constants/featureFlags.ts b/sdk/@launchdarkly/observability-react-native/src/constants/featureFlags.ts index 532d239d69..ec95ae9d96 100644 --- a/sdk/@launchdarkly/observability-react-native/src/constants/featureFlags.ts +++ b/sdk/@launchdarkly/observability-react-native/src/constants/featureFlags.ts @@ -19,9 +19,17 @@ export const FEATURE_FLAG_CONTEXT_ID_ATTR = `${FEATURE_FLAG_SCOPE}.context.id` export const FEATURE_FLAG_ENV_ATTR = `${FEATURE_FLAG_SCOPE}.environment.id` export const LD_SCOPE = 'launchdarkly' +// Span name for `track` events recorded via the LaunchDarkly client's +// afterTrack hook. Uses the simple "track" name to match the mobile SDKs +// (iOS / Android / Flutter). +export const LD_TRACK_SPAN_NAME = 'track' export const FEATURE_FLAG_APP_ID_ATTR = `${LD_SCOPE}.application.id` export const FEATURE_FLAG_APP_VERSION_ATTR = `${LD_SCOPE}.application.version` export const LD_IDENTIFY_RESULT_STATUS = `${LD_SCOPE}.identify.result.status` +// Universal, cross-SDK marker for telemetry produced internally by the +// LaunchDarkly SDK itself (e.g. flag-evaluation spans), so it can be filtered or +// hidden regardless of which SDK/instrumentation-scope emitted it. +export const LD_INTERNAL_ATTR = `${LD_SCOPE}.internal` export const FEATURE_FLAG_REASON_ATTRS: { [key in keyof LDEvaluationReason]: string diff --git a/sdk/@launchdarkly/observability-react-native/src/plugin/observability.ts b/sdk/@launchdarkly/observability-react-native/src/plugin/observability.ts index 379e0448d4..868068a6ba 100644 --- a/sdk/@launchdarkly/observability-react-native/src/plugin/observability.ts +++ b/sdk/@launchdarkly/observability-react-native/src/plugin/observability.ts @@ -26,6 +26,8 @@ import { getCanonicalKey, getContextKeys, LD_IDENTIFY_RESULT_STATUS, + LD_INTERNAL_ATTR, + LD_TRACK_SPAN_NAME, } from '../constants/featureFlags' import type { LDEvaluationReason } from '@launchdarkly/js-sdk-common' import { @@ -35,6 +37,7 @@ import { IdentifySeriesContext, EvaluationSeriesContext, EvaluationSeriesData, + TrackSeriesContext, } from '@launchdarkly/react-native-client-sdk' class TracingHook implements Hook { @@ -126,6 +129,9 @@ class TracingHook implements Hook { [FEATURE_FLAG_KEY_ATTR]: hookContext.flagKey, [FEATURE_FLAG_PROVIDER_ATTR]: 'LaunchDarkly', [FEATURE_FLAG_VALUE_ATTR]: JSON.stringify(detail.value), + // Mark this as SDK-internal telemetry so it can be filtered + // out universally (independent of instrumentation scope). + [LD_INTERNAL_ATTR]: true, }) span.setStatus({ code: 1 }) @@ -146,6 +152,41 @@ class TracingHook implements Hook { return data } + + afterTrack(hookContext: TrackSeriesContext): void { + try { + const trackAttributes: Attributes = { + ...this.metaAttributes, + ...(hookContext.context + ? getContextKeys(hookContext.context) + : {}), + // Spread user-supplied track data first so the LaunchDarkly event + // `key` and metric `value` set below always win over any + // same-named properties in the payload. Non-primitive members are + // ignored by OpenTelemetry. + ...(typeof hookContext.data === 'object' && + hookContext.data !== null + ? (hookContext.data as Attributes) + : {}), + key: hookContext.key, + ...(hookContext.metricValue !== undefined && + hookContext.metricValue !== null + ? { value: hookContext.metricValue } + : {}), + } + + _LDObserve.startActiveSpan(LD_TRACK_SPAN_NAME, (span) => { + span.setAttributes(trackAttributes) + span.setStatus({ code: 1 }) + span.end() + }) + } catch (error) { + _LDObserve.recordError(error as Error, { + 'track.key': hookContext.key, + 'error.context': 'track_tracing', + }) + } + } } export class Observability implements LDPlugin { diff --git a/sdk/@launchdarkly/observability-react-native/src/sdk/LDObserve.ts b/sdk/@launchdarkly/observability-react-native/src/sdk/LDObserve.ts index cd1e9e9f63..5b65532848 100644 --- a/sdk/@launchdarkly/observability-react-native/src/sdk/LDObserve.ts +++ b/sdk/@launchdarkly/observability-react-native/src/sdk/LDObserve.ts @@ -9,8 +9,10 @@ import { ObservabilityClient } from '../client/ObservabilityClient' import { Metric } from '../api/Metric' import { RequestContext } from '../api/RequestContext' import { Observe } from '../api/Observe' +import { SpanScope, WithSpanOptions } from '../api/SpanScope' import { BufferedClass } from './BufferedClass' import { noOpSpan } from '../utils/NoOpSpan' +import { NOOP_SPAN_OPS, runInSpan, SpanOps } from './withSpan' class LDObserveClass extends BufferedClass @@ -56,6 +58,10 @@ class LDObserveClass return this._bufferCall('recordLog', [message, level, attributes]) } + track(key: string, properties?: Attributes, metricValue?: number): void { + return this._bufferCall('track', [key, properties, metricValue]) + } + parseHeaders(headers: Record): RequestContext { const response = this._bufferCall('parseHeaders', [headers]) return this._isLoaded @@ -127,6 +133,18 @@ class LDObserveClass : fn(noOpSpan.setAttribute('method', 'startActiveSpan')) } + withSpan( + spanName: string, + fn: (scope: SpanScope) => T, + options?: WithSpanOptions, + ): T { + // When loaded, `this` provides real startSpan/recordError that execute + // immediately (no buffering). Before init we use no-op span ops so the + // callback still runs without enqueueing stray spans. + const ops: SpanOps = this._isLoaded ? this : NOOP_SPAN_OPS + return runInSpan(ops, spanName, fn, options) + } + getContextFromSpan(span: OtelSpan): Context { const response = this._bufferCall('getContextFromSpan', [span]) return this._isLoaded ? response : context.active() diff --git a/sdk/@launchdarkly/observability-react-native/src/sdk/withSpan.ts b/sdk/@launchdarkly/observability-react-native/src/sdk/withSpan.ts new file mode 100644 index 0000000000..a26bb9c564 --- /dev/null +++ b/sdk/@launchdarkly/observability-react-native/src/sdk/withSpan.ts @@ -0,0 +1,105 @@ +import { + Attributes, + context, + Context, + Span as OtelSpan, + SpanOptions, + SpanStatusCode, + trace, +} from '@opentelemetry/api' +import { SpanScope, WithSpanOptions } from '../api/SpanScope' +import { noOpSpan } from '../utils/NoOpSpan' + +/** + * The minimal surface {@link runInSpan} needs. Satisfied directly by the + * `LDObserve` singleton when loaded, and by {@link NOOP_SPAN_OPS} before init. + * + * Internal: not part of the public API. + */ +export interface SpanOps { + startSpan(spanName: string, options?: SpanOptions, ctx?: Context): OtelSpan + recordError( + error: Error, + attributes?: Attributes, + options?: { span: OtelSpan }, + ): void +} + +/** + * Used before the client is initialized: produces no-op spans and swallows + * errors, while still running the callback so application logic is unaffected. + * + * Internal: not part of the public API. + */ +export const NOOP_SPAN_OPS: SpanOps = { + startSpan: () => noOpSpan, + recordError: () => {}, +} + +/** + * Shared implementation behind `LDObserve.withSpan` and {@link SpanScope.child}. + * Parents the span off the supplied/active context, runs `fn` with the span + * active (so synchronous auto-instrumented calls are parented), and ends the + * span — handling both sync and promise returns. + * + * Internal: not part of the public API. + */ +export function runInSpan( + ops: SpanOps, + name: string, + fn: (scope: SpanScope) => T, + options?: WithSpanOptions, +): T { + const { parent: parentOption, ...spanOptions } = options ?? {} + const parent = parentOption ?? context.active() + const span = ops.startSpan(name, spanOptions, parent) + const ctx = trace.setSpan(parent, span) + + const scope: SpanScope = { + span, + ctx, + child: (childName, childFn, childOptions) => + runInSpan(ops, childName, childFn, { + ...childOptions, + parent: ctx, + }), + active: (activeFn) => context.with(ctx, activeFn), + } + + const succeed = () => { + span.setStatus({ code: SpanStatusCode.OK }) + span.end() + } + const failWith = (error: unknown) => { + try { + ops.recordError(error as Error, undefined, { span }) + } catch { + // recordError should never throw, but never mask the original error. + } + span.setStatus({ code: SpanStatusCode.ERROR }) + span.end() + } + + try { + // Make the span active for the synchronous window so auto-instrumented + // fetch/XHR calls started before the first `await` parent to it. + const result = context.with(ctx, () => fn(scope)) + if (result instanceof Promise) { + return result.then( + (value) => { + succeed() + return value + }, + (error) => { + failWith(error) + throw error + }, + ) as unknown as T + } + succeed() + return result + } catch (error) { + failWith(error) + throw error + } +} diff --git a/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.test.ts b/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.test.ts index 09f70e9401..7898a1aead 100644 --- a/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.test.ts +++ b/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.test.ts @@ -10,6 +10,36 @@ describe('getCorsUrlsPattern', () => { expect(getCorsUrlsPattern(false)).toEqual(/^$/) }) + it('anchors string entries to the origin position (host + subdomains)', () => { + const patterns = getCorsUrlsPattern(['api.example.com']) as RegExp[] + expect(patterns[0]).toEqual( + /^https?:\/\/([^/]+\.)?api\.example\.com([:/?#]|$)/, + ) + // The configured host (and its subdomains) match at the origin. + expect(patterns[0].test('https://api.example.com/posts')).toBe(true) + expect(patterns[0].test('https://eu.api.example.com/posts')).toBe(true) + // `.` must not act as a wildcard, so look-alike hosts do not match. + expect(patterns[0].test('https://apiXexampleYcom/posts')).toBe(false) + // Regression: these patterns are also matched against the full URL by + // the instrumentations' `propagateTraceHeaderCorsUrls`, so a third-party + // URL carrying the host as a query value must NOT match (otherwise + // baggage/trace headers leak to unintended origins). + expect(patterns[0].test('https://evil.test/?x=api.example.com')).toBe( + false, + ) + // A different host that merely contains the configured host as a suffix + // of a longer label must not match either. + expect(patterns[0].test('https://notapi.example.com.evil.test')).toBe( + false, + ) + }) + + it('passes RegExp entries through unchanged', () => { + const re = /^https?:\/\/backend\./ + const patterns = getCorsUrlsPattern([re]) as RegExp[] + expect(patterns[0]).toBe(re) + }) + it('anchors the page host at the origin position', () => { vi.stubGlobal('window', { location: { host: 'example.com' }, diff --git a/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.ts b/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.ts index 66b9750ffb..5e3179d46a 100644 --- a/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.ts +++ b/sdk/@launchdarkly/observability-shared/src/instrumentation/utils.ts @@ -87,6 +87,9 @@ export const shouldRecordRequest = ( ) } +const escapeRegExp = (s: string): string => + s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Helper to convert tracingOrigins to a RegExp or array of RegExp export function getCorsUrlsPattern( tracingOrigins: TracingOrigins, @@ -94,25 +97,33 @@ export function getCorsUrlsPattern( if (tracingOrigins === true) { const patterns = [/localhost/, /^\//] if (typeof window !== 'undefined' && window.location?.host) { - patterns.push(buildLocationHostPattern(window.location.host)) + patterns.push(buildOriginHostPattern(window.location.host)) } return patterns } else if (Array.isArray(tracingOrigins)) { + // Per the documented contract, string entries are hosts while RegExp + // entries are used as-is. Anchor string entries to the origin position + // (host + optional subdomains) rather than matching them as a bare + // substring. This matters because these patterns are also handed to the + // instrumentations' `propagateTraceHeaderCorsUrls`, which tests them + // against the *full* request URL — an unanchored `api.example.com` would + // otherwise match a third-party URL that merely carries the host as a + // query-parameter value (e.g. `https://evil.test/?x=api.example.com`), + // leaking trace and baggage headers to unintended origins. return tracingOrigins.map((pattern) => - typeof pattern === 'string' ? new RegExp(pattern) : pattern, + typeof pattern === 'string' + ? buildOriginHostPattern(pattern) + : pattern, ) } return /^$/ // Match nothing if tracingOrigins is false or undefined } -const escapeRegExp = (s: string): string => - s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - -// Build a regex that matches the page host (and its subdomains) at the origin -// position of a URL, not anywhere inside it. Anchoring matters: an unanchored +// Build a regex that matches a host (and its subdomains) at the origin position +// of a URL, not anywhere inside it. Anchoring matters: an unanchored // /example.com/ also matches https://third-party.io/?store=example.com. -const buildLocationHostPattern = (host: string): RegExp => +const buildOriginHostPattern = (host: string): RegExp => new RegExp('^https?://([^/]+\\.)?' + escapeRegExp(host) + '([:/?#]|$)') // Returns the URL's origin for absolute URLs, or the original string for diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec b/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec index f08f32fd67..00b0c17646 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec +++ b/sdk/@launchdarkly/react-native-ld-session-replay/SessionReplayReactNative.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" s.private_header_files = "ios/**/*.h" - s.dependency 'LaunchDarklySessionReplay', '~> 0.33.1' + s.dependency 'LaunchDarklySessionReplay', '~> 0.46.1' install_modules_dependencies(s) end diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/android/build.gradle b/sdk/@launchdarkly/react-native-ld-session-replay/android/build.gradle index be177b29d1..3c6f9506bb 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/android/build.gradle +++ b/sdk/@launchdarkly/react-native-ld-session-replay/android/build.gradle @@ -83,8 +83,8 @@ def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.launchdarkly:launchdarkly-observability-android:0.45.0" - implementation "com.launchdarkly:launchdarkly-android-client-sdk:5.12.2" + implementation "com.launchdarkly:launchdarkly-observability-android:0.59.0" + implementation "com.launchdarkly:launchdarkly-android-client-sdk:5.13.1" // compileOnly: OTel Attributes appears in ObservabilityOptions parameter types; provided at // runtime transitively through launchdarkly-observability-android. compileOnly "io.opentelemetry:opentelemetry-api:1.51.0" diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/android/src/main/java/com/sessionreplayreactnative/SessionReplayClientAdapter.kt b/sdk/@launchdarkly/react-native-ld-session-replay/android/src/main/java/com/sessionreplayreactnative/SessionReplayClientAdapter.kt index 3360f685f5..48d2f091be 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/android/src/main/java/com/sessionreplayreactnative/SessionReplayClientAdapter.kt +++ b/sdk/@launchdarkly/react-native-ld-session-replay/android/src/main/java/com/sessionreplayreactnative/SessionReplayClientAdapter.kt @@ -25,6 +25,12 @@ internal class SessionReplayClientAdapter private constructor() { private val lock = Any() private var mobileKey: String? = null private var serviceName: String = DEFAULT_SERVICE_NAME + // Optional `service.version` forwarded from JS. null keeps the SDK default. + private var serviceVersion: String? = null + // Optional session id forwarded from the JS observability SDK so the native observability + // instance (which emits e.g. `click` spans) seeds its session manager with the same + // `session.id`. null means the native SDK generates and owns its own session. + private var customSessionId: String? = null private var replayOptions: ReplayOptions? = null // Only accessed from the main thread (all reads/writes are inside Handler(mainLooper).post blocks). private var initialized = false @@ -40,6 +46,12 @@ internal class SessionReplayClientAdapter private constructor() { ?.getString("serviceName") ?.takeIf { it.isNotBlank() } ?: DEFAULT_SERVICE_NAME + this.serviceVersion = options?.takeIf { it.hasKey("serviceVersion") } + ?.getString("serviceVersion") + ?.takeIf { it.isNotBlank() } + this.customSessionId = options?.takeIf { it.hasKey("sessionId") } + ?.getString("sessionId") + ?.takeIf { it.isNotBlank() } this.replayOptions = replayOptionsFrom(options) } } @@ -47,6 +59,8 @@ internal class SessionReplayClientAdapter private constructor() { fun start(application: Application, activity: Activity?, completion: (Boolean, String?) -> Unit) { val localMobileKey: String? val localServiceName: String + val localServiceVersion: String? + val localCustomSessionId: String? val localReplayOptions: ReplayOptions? // Capture configuration under the lock, then release it before posting to the main thread. @@ -54,6 +68,8 @@ internal class SessionReplayClientAdapter private constructor() { localMobileKey = mobileKey localReplayOptions = replayOptions localServiceName = serviceName + localServiceVersion = serviceVersion + localCustomSessionId = customSessionId } if (localMobileKey == null || localReplayOptions == null) { val msg = "start: configure() was not called — mobile key or options are missing" @@ -68,7 +84,7 @@ internal class SessionReplayClientAdapter private constructor() { Handler(Looper.getMainLooper()).post { if (!initialized) { try { - initLDClient(application, localMobileKey, localServiceName, localReplayOptions) + initLDClient(application, localMobileKey, localServiceName, localServiceVersion, localCustomSessionId, localReplayOptions) } catch (e: Exception) { logger.error("start: LDClient.init() threw {0}: {1}", e::class.simpleName, e.message) completion(false, "Session replay failed to initialize.") @@ -126,8 +142,27 @@ internal class SessionReplayClientAdapter private constructor() { } } - private fun initLDClient(application: Application, mobileKey: String, serviceName: String, replayOptions: ReplayOptions) { + private fun initLDClient( + application: Application, + mobileKey: String, + serviceName: String, + serviceVersion: String?, + customSessionId: String?, + replayOptions: ReplayOptions + ) { logger.debug("initLDClient: calling LDClient.init()") + // Apply the forwarded service.version only when provided; otherwise keep the SDK default. + var observabilityOptions = ObservabilityOptions( + serviceName = serviceName, + logAdapter = LDObserveLogging.adapter(), + // Disable the OpenTelemetry Android CrashReporterInstrumentation + instrumentations = ObservabilityOptions.Instrumentations( + crashReporting = false, + ), + ) + if (serviceVersion != null) { + observabilityOptions = observabilityOptions.copy(serviceVersion = serviceVersion) + } val config = LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Enabled) .mobileKey(mobileKey) .offline(true) @@ -136,17 +171,14 @@ internal class SessionReplayClientAdapter private constructor() { listOf( // TODO: Pass JS ObservabilityOptions such as backendUrl, // resourceAttributes, and sessionBackgroundTimeout through to here. + // Forward the JS observability session id (when provided) so the native + // observability instance seeds its session manager with the same + // `session.id`, matching iOS. null lets the native SDK own its session. Observability( application = application, mobileKey = mobileKey, - options = ObservabilityOptions( - serviceName = serviceName, - logAdapter = LDObserveLogging.adapter(), - // Disable the OpenTelemetry Android CrashReporterInstrumentation - instrumentations = ObservabilityOptions.Instrumentations( - crashReporting = false, - ), - ) + options = observabilityOptions, + customSessionId = customSessionId ), SessionReplay(options = replayOptions), ) diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile b/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile index 6a4c5f1718..f8d88617a9 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile @@ -14,3 +14,6 @@ gem 'bigdecimal' gem 'logger' gem 'benchmark' gem 'mutex_m' + +# Ruby 4.0.0 removed nkf/kconv from the standard library; CFPropertyList needs it. +gem 'nkf' diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile.lock b/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile.lock deleted file mode 100644 index 90a574dbf8..0000000000 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/Gemfile.lock +++ /dev/null @@ -1,197 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.8) - activesupport (7.2.3.1) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1, < 6) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - addressable (2.9.0) - public_suffix (>= 2.0.2, < 8.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - base64 (0.3.0) - benchmark (0.5.0) - bigdecimal (4.1.2) - claide (1.1.0) - cocoapods (1.15.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 2.1, < 3.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.6.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) - activesupport (>= 5.0, < 8) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (2.1) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.3.3) - connection_pool (3.0.2) - drb (2.2.3) - escape (0.0.4) - ethon (0.18.0) - ffi (>= 1.15.0) - logger - ffi (1.17.4) - ffi (1.17.4-aarch64-linux-gnu) - ffi (1.17.4-aarch64-linux-musl) - ffi (1.17.4-arm-linux-gnu) - ffi (1.17.4-arm-linux-musl) - ffi (1.17.4-arm64-darwin) - ffi (1.17.4-x86-linux-gnu) - ffi (1.17.4-x86-linux-musl) - ffi (1.17.4-x86_64-darwin) - ffi (1.17.4-x86_64-linux-gnu) - ffi (1.17.4-x86_64-linux-musl) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.9.0) - mutex_m - i18n (1.14.8) - concurrent-ruby (~> 1.0) - json (2.19.4) - logger (1.7.0) - minitest (5.27.0) - molinillo (0.8.0) - mutex_m (0.3.0) - nanaimo (0.3.0) - nap (1.1.0) - netrc (0.11.0) - public_suffix (4.0.7) - rexml (3.4.4) - ruby-macho (2.5.1) - securerandom (0.4.1) - typhoeus (1.6.0) - ethon (>= 0.18.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - xcodeproj (1.25.1) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.6, < 4.0) - -PLATFORMS - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-musl - arm64-darwin - ruby - x86-linux-gnu - x86-linux-musl - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - activesupport (>= 6.1.7.5, != 7.1.0) - benchmark - bigdecimal - cocoapods (>= 1.13, != 1.15.1, != 1.15.0) - concurrent-ruby (< 1.3.4) - logger - mutex_m - xcodeproj (< 1.26.0) - -CHECKSUMS - CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 - activesupport (7.2.3.1) sha256=11ebed516a43a0bb47346227a35ebae4d9427465a7c9eb197a03d5c8d283cb34 - addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af - algoliasearch (1.27.5) sha256=26c1cddf3c2ec4bd60c148389e42702c98fdac862881dc6b07a4c0b89ffec853 - atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f - base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b - benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c - bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd - claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e - cocoapods (1.15.2) sha256=f0f5153de8d028d133b96f423e04f37fb97a1da0d11dda581a9f46c0cba4090a - cocoapods-core (1.15.2) sha256=322650d97fe1ad4c0831a09669764b888bd91c6d79d0f6bb07281a17667a2136 - cocoapods-deintegrate (1.0.5) sha256=517c2a448ef563afe99b6e7668704c27f5de9e02715a88ee9de6974dc1b3f6a2 - cocoapods-downloader (2.1) sha256=bb6ebe1b3966dc4055de54f7a28b773485ac724fdf575d9bee2212d235e7b6d1 - cocoapods-plugins (1.0.0) sha256=725d17ce90b52f862e73476623fd91441b4430b742d8a071000831efb440ca9a - cocoapods-search (1.0.1) sha256=1b133b0e6719ed439bd840e84a1828cca46425ab73a11eff5e096c3b2df05589 - cocoapods-trunk (1.6.0) sha256=5f5bda8c172afead48fa2d43a718cf534b1313c367ba1194cebdeb9bfee9ed31 - cocoapods-try (1.2.0) sha256=145b946c6e7747ed0301d975165157951153d27469e6b2763c83e25c84b9defe - colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a - concurrent-ruby (1.3.3) sha256=4f9cd28965c4dcf83ffd3ea7304f9323277be8525819cb18a3b61edcb56a7c6a - connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a - drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 - escape (0.0.4) sha256=e49f44ae2b4f47c6a3abd544ae77fe4157802794e32f19b8e773cbc4dcec4169 - ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2 - ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf - ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df - ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 - ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 - ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 - ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b - ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf - ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 - ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 - ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d - ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e - fourflusher (2.3.1) sha256=1b3de61c7c791b6a4e64f31e3719eb25203d151746bb519a0292bff1065ccaa9 - fuzzy_match (2.0.4) sha256=b5de4f95816589c5b5c3ad13770c0af539b75131c158135b3f3bbba75d0cfca5 - gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 - httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8 - i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 - json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac - logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 - molinillo (0.8.0) sha256=efbff2716324e2a30bccd3eba1ff3a735f4d5d53ffddbc6a2f32c0ca9433045d - mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 - nanaimo (0.3.0) sha256=aaaedc60497070b864a7e220f7c4b4cad3a0daddda2c30055ba8dae306342376 - nap (1.1.0) sha256=949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576 - netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f - public_suffix (4.0.7) sha256=8be161e2421f8d45b0098c042c06486789731ea93dc3a896d30554ee38b573b8 - rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 - ruby-macho (2.5.1) sha256=9075e52e0f9270b552a90b24fcc6219ad149b0d15eae1bc364ecd0ac8984f5c9 - securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 - typhoeus (1.6.0) sha256=bacc41c23e379547e29801dc235cd1699b70b955a1ba3d32b2b877aa844c331d - tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b - xcodeproj (1.25.1) sha256=9a2310dccf6d717076e86f602b17c640046b6f1dfe64480044596f6f2f13dc84 - -RUBY VERSION - ruby 3.2.2 - -BUNDLED WITH - 4.0.1 diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock b/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock deleted file mode 100644 index 7f79767e1a..0000000000 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock +++ /dev/null @@ -1,2892 +0,0 @@ -PODS: - - boost (1.84.0) - - DoubleConversion (1.1.6) - - fast_float (8.0.0) - - FBLazyVector (0.83.0) - - fmt (11.0.2) - - glog (0.3.5) - - hermes-engine (0.14.0): - - hermes-engine/Pre-built (= 0.14.0) - - hermes-engine/Pre-built (0.14.0) - - KSCrash (2.5.1): - - KSCrash/Installations (= 2.5.1) - - KSCrash/Core (2.5.1) - - KSCrash/DemangleFilter (2.5.1): - - KSCrash/Recording - - KSCrash/Filters (2.5.1): - - KSCrash/Recording - - KSCrash/RecordingCore - - KSCrash/ReportingCore - - KSCrash/Installations (2.5.1): - - KSCrash/DemangleFilter - - KSCrash/Filters - - KSCrash/Recording - - KSCrash/Sinks - - KSCrash/Recording (2.5.1): - - KSCrash/RecordingCore - - KSCrash/RecordingCore (2.5.1): - - KSCrash/Core - - KSCrash/ReportingCore (2.5.1): - - KSCrash/Core - - KSCrash/Sinks (2.5.1): - - KSCrash/Filters - - KSCrash/Recording - - LaunchDarkly (11.1.1): - - LaunchDarkly/Core (= 11.1.1) - - LaunchDarkly/Core (11.1.1): - - LDSwiftEventSource (= 3.3.0) - - LaunchDarklyObservability (0.24.0): - - LaunchDarklyObservability/LaunchDarklyObservability (= 0.24.0) - - LaunchDarklyObservability/Common (0.24.0): - - LaunchDarkly (~> 11.1.0) - - LaunchDarklyObservability/LaunchDarklyObservability (0.24.0): - - LaunchDarklyObservability/Common - - LaunchDarklyObservability/Misc - - LaunchDarklyObservability/OpenTelemetry - - LaunchDarklyObservability/OpenTelemetryProtocolExporterCommon - - LaunchDarklyObservability/SDKResourceExtension - - LaunchDarklyObservability/URLSessionInstrumentation - - LaunchDarklyObservability/Misc (0.24.0): - - KSCrash - - LaunchDarklyObservability/NetworkStatus (0.24.0): - - OpenTelemetry-Swift-Api (~> 2.3.0) - - LaunchDarklyObservability/OpenTelemetry (0.24.0): - - OpenTelemetry-Swift-Api (~> 2.3.0) - - OpenTelemetry-Swift-Sdk (~> 2.3.0) - - LaunchDarklyObservability/OpenTelemetryProtocolExporterCommon (0.24.0): - - OpenTelemetry-Swift-Sdk (~> 2.3.0) - - SwiftProtobuf - - LaunchDarklyObservability/SDKResourceExtension (0.24.0): - - LaunchDarklyObservability/OpenTelemetry - - LaunchDarklyObservability/URLSessionInstrumentation (0.24.0): - - LaunchDarklyObservability/NetworkStatus - - OpenTelemetry-Swift-Sdk (~> 2.3.0) - - LaunchDarklySessionReplay (0.26.2): - - LaunchDarklySessionReplay/LaunchDarklySessionReplay (= 0.26.2) - - LaunchDarklySessionReplay/LaunchDarklySessionReplay (0.26.2): - - LaunchDarklyObservability - - LaunchDarklySessionReplay/SessionReplayC - - LaunchDarklySessionReplay/SessionReplayC (0.26.2) - - LDSwiftEventSource (3.3.0) - - OpenTelemetry-Swift-Api (2.3.0) - - OpenTelemetry-Swift-Sdk (2.3.0): - - OpenTelemetry-Swift-Api (= 2.3.0) - - RCT-Folly (2024.11.18.00): - - boost - - DoubleConversion - - fast_float (= 8.0.0) - - fmt (= 11.0.2) - - glog - - RCT-Folly/Default (= 2024.11.18.00) - - RCT-Folly/Default (2024.11.18.00): - - boost - - DoubleConversion - - fast_float (= 8.0.0) - - fmt (= 11.0.2) - - glog - - RCT-Folly/Fabric (2024.11.18.00): - - boost - - DoubleConversion - - fast_float (= 8.0.0) - - fmt (= 11.0.2) - - glog - - RCTDeprecation (0.83.0) - - RCTRequired (0.83.0) - - RCTSwiftUI (0.83.0) - - RCTSwiftUIWrapper (0.83.0): - - RCTSwiftUI - - RCTTypeSafety (0.83.0): - - FBLazyVector (= 0.83.0) - - RCTRequired (= 0.83.0) - - React-Core (= 0.83.0) - - React (0.83.0): - - React-Core (= 0.83.0) - - React-Core/DevSupport (= 0.83.0) - - React-Core/RCTWebSocket (= 0.83.0) - - React-RCTActionSheet (= 0.83.0) - - React-RCTAnimation (= 0.83.0) - - React-RCTBlob (= 0.83.0) - - React-RCTImage (= 0.83.0) - - React-RCTLinking (= 0.83.0) - - React-RCTNetwork (= 0.83.0) - - React-RCTSettings (= 0.83.0) - - React-RCTText (= 0.83.0) - - React-RCTVibration (= 0.83.0) - - React-callinvoker (0.83.0) - - React-Core (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default (= 0.83.0) - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/CoreModulesHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/Default (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/DevSupport (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default (= 0.83.0) - - React-Core/RCTWebSocket (= 0.83.0) - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTActionSheetHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTAnimationHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTBlobHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTImageHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTLinkingHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTNetworkHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTSettingsHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTTextHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTVibrationHeaders (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-Core/RCTWebSocket (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTDeprecation - - React-Core/Default (= 0.83.0) - - React-cxxreact - - React-featureflags - - React-hermes - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsinspectorcdp - - React-jsitooling - - React-perflogger - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-CoreModules (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - RCTTypeSafety (= 0.83.0) - - React-Core/CoreModulesHeaders (= 0.83.0) - - React-debug - - React-jsi (= 0.83.0) - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-NativeModulesApple - - React-RCTBlob - - React-RCTFBReactNativeSpec - - React-RCTImage (= 0.83.0) - - React-runtimeexecutor - - React-utils - - ReactCommon - - SocketRocket - - React-cxxreact (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker (= 0.83.0) - - React-debug (= 0.83.0) - - React-jsi (= 0.83.0) - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-logger (= 0.83.0) - - React-perflogger (= 0.83.0) - - React-runtimeexecutor - - React-timing (= 0.83.0) - - React-utils - - SocketRocket - - React-debug (0.83.0) - - React-defaultsnativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-domnativemodule - - React-featureflags - - React-featureflagsnativemodule - - React-idlecallbacksnativemodule - - React-intersectionobservernativemodule - - React-jsi - - React-jsiexecutor - - React-microtasksnativemodule - - React-RCTFBReactNativeSpec - - React-webperformancenativemodule - - SocketRocket - - Yoga - - React-domnativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-Fabric - - React-Fabric/bridging - - React-FabricComponents - - React-graphics - - React-jsi - - React-jsiexecutor - - React-RCTFBReactNativeSpec - - React-runtimeexecutor - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-Fabric (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric/animated (= 0.83.0) - - React-Fabric/animationbackend (= 0.83.0) - - React-Fabric/animations (= 0.83.0) - - React-Fabric/attributedstring (= 0.83.0) - - React-Fabric/bridging (= 0.83.0) - - React-Fabric/componentregistry (= 0.83.0) - - React-Fabric/componentregistrynative (= 0.83.0) - - React-Fabric/components (= 0.83.0) - - React-Fabric/consistency (= 0.83.0) - - React-Fabric/core (= 0.83.0) - - React-Fabric/dom (= 0.83.0) - - React-Fabric/imagemanager (= 0.83.0) - - React-Fabric/leakchecker (= 0.83.0) - - React-Fabric/mounting (= 0.83.0) - - React-Fabric/observers (= 0.83.0) - - React-Fabric/scheduler (= 0.83.0) - - React-Fabric/telemetry (= 0.83.0) - - React-Fabric/templateprocessor (= 0.83.0) - - React-Fabric/uimanager (= 0.83.0) - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/animated (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/animationbackend (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/animations (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/attributedstring (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/bridging (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/componentregistry (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/componentregistrynative (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/components (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric/components/legacyviewmanagerinterop (= 0.83.0) - - React-Fabric/components/root (= 0.83.0) - - React-Fabric/components/scrollview (= 0.83.0) - - React-Fabric/components/view (= 0.83.0) - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/components/legacyviewmanagerinterop (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/components/root (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/components/scrollview (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/components/view (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-renderercss - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-Fabric/consistency (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/core (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/dom (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/imagemanager (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/leakchecker (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/mounting (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/observers (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric/observers/events (= 0.83.0) - - React-Fabric/observers/intersection (= 0.83.0) - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/observers/events (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/observers/intersection (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/scheduler (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric/observers/events - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-performancecdpmetrics - - React-performancetimeline - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/telemetry (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/templateprocessor (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/uimanager (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric/uimanager/consistency (= 0.83.0) - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererconsistency - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-Fabric/uimanager/consistency (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-rendererconsistency - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - React-FabricComponents (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-FabricComponents/components (= 0.83.0) - - React-FabricComponents/textlayoutmanager (= 0.83.0) - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-FabricComponents/components/inputaccessory (= 0.83.0) - - React-FabricComponents/components/iostextinput (= 0.83.0) - - React-FabricComponents/components/modal (= 0.83.0) - - React-FabricComponents/components/rncore (= 0.83.0) - - React-FabricComponents/components/safeareaview (= 0.83.0) - - React-FabricComponents/components/scrollview (= 0.83.0) - - React-FabricComponents/components/switch (= 0.83.0) - - React-FabricComponents/components/text (= 0.83.0) - - React-FabricComponents/components/textinput (= 0.83.0) - - React-FabricComponents/components/unimplementedview (= 0.83.0) - - React-FabricComponents/components/virtualview (= 0.83.0) - - React-FabricComponents/components/virtualviewexperimental (= 0.83.0) - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/inputaccessory (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/iostextinput (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/modal (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/rncore (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/safeareaview (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/scrollview (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/switch (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/text (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/textinput (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/unimplementedview (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/virtualview (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/components/virtualviewexperimental (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricComponents/textlayoutmanager (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-FabricImage (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired (= 0.83.0) - - RCTTypeSafety (= 0.83.0) - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-jsi - - React-jsiexecutor (= 0.83.0) - - React-logger - - React-rendererdebug - - React-utils - - ReactCommon - - SocketRocket - - Yoga - - React-featureflags (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - SocketRocket - - React-featureflagsnativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-featureflags - - React-jsi - - React-jsiexecutor - - React-RCTFBReactNativeSpec - - ReactCommon/turbomodule/core - - SocketRocket - - React-graphics (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-jsi - - React-jsiexecutor - - React-utils - - SocketRocket - - React-hermes (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact (= 0.83.0) - - React-jsi - - React-jsiexecutor (= 0.83.0) - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-oscompat - - React-perflogger (= 0.83.0) - - React-runtimeexecutor - - SocketRocket - - React-idlecallbacksnativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-jsi - - React-jsiexecutor - - React-RCTFBReactNativeSpec - - React-runtimeexecutor - - React-runtimescheduler - - ReactCommon/turbomodule/core - - SocketRocket - - React-ImageManager (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-Core/Default - - React-debug - - React-Fabric - - React-graphics - - React-rendererdebug - - React-utils - - SocketRocket - - React-intersectionobservernativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact - - React-Fabric - - React-Fabric/bridging - - React-graphics - - React-jsi - - React-jsiexecutor - - React-RCTFBReactNativeSpec - - React-runtimeexecutor - - React-runtimescheduler - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - React-jserrorhandler (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact - - React-debug - - React-featureflags - - React-jsi - - ReactCommon/turbomodule/bridging - - SocketRocket - - React-jsi (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - SocketRocket - - React-jsiexecutor (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact - - React-debug - - React-jsi - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-perflogger - - React-runtimeexecutor - - React-utils - - SocketRocket - - React-jsinspector (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-featureflags - - React-jsi - - React-jsinspectorcdp - - React-jsinspectornetwork - - React-jsinspectortracing - - React-oscompat - - React-perflogger (= 0.83.0) - - React-runtimeexecutor - - React-utils - - SocketRocket - - React-jsinspectorcdp (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - SocketRocket - - React-jsinspectornetwork (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-jsinspectorcdp - - SocketRocket - - React-jsinspectortracing (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-jsi - - React-jsinspectornetwork - - React-oscompat - - React-timing - - SocketRocket - - React-jsitooling (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact (= 0.83.0) - - React-debug - - React-jsi (= 0.83.0) - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-runtimeexecutor - - React-utils - - SocketRocket - - React-jsitracing (0.83.0): - - React-jsi - - React-logger (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - SocketRocket - - React-Mapbuffer (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-debug - - SocketRocket - - React-microtasksnativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-jsi - - React-jsiexecutor - - React-RCTFBReactNativeSpec - - ReactCommon/turbomodule/core - - SocketRocket - - React-NativeModulesApple (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker - - React-Core - - React-cxxreact - - React-debug - - React-featureflags - - React-jsi - - React-jsinspector - - React-jsinspectorcdp - - React-runtimeexecutor - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - SocketRocket - - React-networking (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-featureflags - - React-jsinspectornetwork - - React-jsinspectortracing - - React-performancetimeline - - React-timing - - SocketRocket - - React-oscompat (0.83.0) - - React-perflogger (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - SocketRocket - - React-performancecdpmetrics (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-jsi - - React-performancetimeline - - React-runtimeexecutor - - React-timing - - SocketRocket - - React-performancetimeline (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-featureflags - - React-jsinspectortracing - - React-perflogger - - React-timing - - SocketRocket - - React-RCTActionSheet (0.83.0): - - React-Core/RCTActionSheetHeaders (= 0.83.0) - - React-RCTAnimation (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - RCTTypeSafety - - React-Core/RCTAnimationHeaders - - React-featureflags - - React-jsi - - React-NativeModulesApple - - React-RCTFBReactNativeSpec - - ReactCommon - - SocketRocket - - React-RCTAppDelegate (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-CoreModules - - React-debug - - React-defaultsnativemodule - - React-Fabric - - React-featureflags - - React-graphics - - React-hermes - - React-jsitooling - - React-NativeModulesApple - - React-RCTFabric - - React-RCTFBReactNativeSpec - - React-RCTImage - - React-RCTNetwork - - React-RCTRuntime - - React-rendererdebug - - React-RuntimeApple - - React-RuntimeCore - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - ReactCommon - - SocketRocket - - React-RCTBlob (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-Core/RCTBlobHeaders - - React-Core/RCTWebSocket - - React-jsi - - React-jsinspector - - React-jsinspectorcdp - - React-NativeModulesApple - - React-RCTFBReactNativeSpec - - React-RCTNetwork - - ReactCommon - - SocketRocket - - React-RCTFabric (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTSwiftUIWrapper - - React-Core - - React-debug - - React-Fabric - - React-FabricComponents - - React-FabricImage - - React-featureflags - - React-graphics - - React-ImageManager - - React-jsi - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-networking - - React-performancecdpmetrics - - React-performancetimeline - - React-RCTAnimation - - React-RCTFBReactNativeSpec - - React-RCTImage - - React-RCTText - - React-rendererconsistency - - React-renderercss - - React-rendererdebug - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - Yoga - - React-RCTFBReactNativeSpec (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-jsi - - React-NativeModulesApple - - React-RCTFBReactNativeSpec/components (= 0.83.0) - - ReactCommon - - SocketRocket - - React-RCTFBReactNativeSpec/components (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-NativeModulesApple - - React-rendererdebug - - React-utils - - ReactCommon - - SocketRocket - - Yoga - - React-RCTImage (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - RCTTypeSafety - - React-Core/RCTImageHeaders - - React-jsi - - React-NativeModulesApple - - React-RCTFBReactNativeSpec - - React-RCTNetwork - - ReactCommon - - SocketRocket - - React-RCTLinking (0.83.0): - - React-Core/RCTLinkingHeaders (= 0.83.0) - - React-jsi (= 0.83.0) - - React-NativeModulesApple - - React-RCTFBReactNativeSpec - - ReactCommon - - ReactCommon/turbomodule/core (= 0.83.0) - - React-RCTNetwork (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - RCTTypeSafety - - React-Core/RCTNetworkHeaders - - React-debug - - React-featureflags - - React-jsi - - React-jsinspectorcdp - - React-jsinspectornetwork - - React-NativeModulesApple - - React-networking - - React-RCTFBReactNativeSpec - - ReactCommon - - SocketRocket - - React-RCTRuntime (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-Core - - React-debug - - React-jsi - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-jsitooling - - React-RuntimeApple - - React-RuntimeCore - - React-runtimeexecutor - - React-RuntimeHermes - - React-utils - - SocketRocket - - React-RCTSettings (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - RCTTypeSafety - - React-Core/RCTSettingsHeaders - - React-jsi - - React-NativeModulesApple - - React-RCTFBReactNativeSpec - - ReactCommon - - SocketRocket - - React-RCTText (0.83.0): - - React-Core/RCTTextHeaders (= 0.83.0) - - Yoga - - React-RCTVibration (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-Core/RCTVibrationHeaders - - React-jsi - - React-NativeModulesApple - - React-RCTFBReactNativeSpec - - ReactCommon - - SocketRocket - - React-rendererconsistency (0.83.0) - - React-renderercss (0.83.0): - - React-debug - - React-utils - - React-rendererdebug (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-debug - - SocketRocket - - React-RuntimeApple (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker - - React-Core/Default - - React-CoreModules - - React-cxxreact - - React-featureflags - - React-jserrorhandler - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsitooling - - React-Mapbuffer - - React-NativeModulesApple - - React-RCTFabric - - React-RCTFBReactNativeSpec - - React-RuntimeCore - - React-runtimeexecutor - - React-RuntimeHermes - - React-runtimescheduler - - React-utils - - SocketRocket - - React-RuntimeCore (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact - - React-Fabric - - React-featureflags - - React-jserrorhandler - - React-jsi - - React-jsiexecutor - - React-jsinspector - - React-jsitooling - - React-performancetimeline - - React-runtimeexecutor - - React-runtimescheduler - - React-utils - - SocketRocket - - React-runtimeexecutor (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - React-debug - - React-featureflags - - React-jsi (= 0.83.0) - - React-utils - - SocketRocket - - React-RuntimeHermes (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-featureflags - - React-hermes - - React-jsi - - React-jsinspector - - React-jsinspectorcdp - - React-jsinspectortracing - - React-jsitooling - - React-jsitracing - - React-RuntimeCore - - React-runtimeexecutor - - React-utils - - SocketRocket - - React-runtimescheduler (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker - - React-cxxreact - - React-debug - - React-featureflags - - React-jsi - - React-jsinspectortracing - - React-performancetimeline - - React-rendererconsistency - - React-rendererdebug - - React-runtimeexecutor - - React-timing - - React-utils - - SocketRocket - - React-timing (0.83.0): - - React-debug - - React-utils (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-debug - - React-jsi (= 0.83.0) - - SocketRocket - - React-webperformancenativemodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-cxxreact - - React-jsi - - React-jsiexecutor - - React-performancetimeline - - React-RCTFBReactNativeSpec - - React-runtimeexecutor - - ReactCommon/turbomodule/core - - SocketRocket - - ReactAppDependencyProvider (0.83.0): - - ReactCodegen - - ReactCodegen (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-FabricImage - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-NativeModulesApple - - React-RCTAppDelegate - - React-rendererdebug - - React-utils - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - SocketRocket - - ReactCommon (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - RCT-Folly - - RCT-Folly/Fabric - - ReactCommon/turbomodule (= 0.83.0) - - SocketRocket - - ReactCommon/turbomodule (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker (= 0.83.0) - - React-cxxreact (= 0.83.0) - - React-jsi (= 0.83.0) - - React-logger (= 0.83.0) - - React-perflogger (= 0.83.0) - - ReactCommon/turbomodule/bridging (= 0.83.0) - - ReactCommon/turbomodule/core (= 0.83.0) - - SocketRocket - - ReactCommon/turbomodule/bridging (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker (= 0.83.0) - - React-cxxreact (= 0.83.0) - - React-jsi (= 0.83.0) - - React-logger (= 0.83.0) - - React-perflogger (= 0.83.0) - - SocketRocket - - ReactCommon/turbomodule/core (0.83.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - React-callinvoker (= 0.83.0) - - React-cxxreact (= 0.83.0) - - React-debug (= 0.83.0) - - React-featureflags (= 0.83.0) - - React-jsi (= 0.83.0) - - React-logger (= 0.83.0) - - React-perflogger (= 0.83.0) - - React-utils (= 0.83.0) - - SocketRocket - - SessionReplayReactNative (0.5.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - LaunchDarklySessionReplay (~> 0.26.2) - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-jsi - - React-NativeModulesApple - - React-RCTFabric - - React-renderercss - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - - SocketRocket (0.7.1) - - SwiftProtobuf (1.36.1) - - Yoga (0.0.0) - -DEPENDENCIES: - - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - - RCTRequired (from `../node_modules/react-native/Libraries/Required`) - - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) - - RCTSwiftUIWrapper (from `../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) - - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../node_modules/react-native/`) - - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - - React-Core (from `../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) - - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) - - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) - - React-Fabric (from `../node_modules/react-native/ReactCommon`) - - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) - - React-FabricImage (from `../node_modules/react-native/ReactCommon`) - - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) - - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) - - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) - - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) - - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) - - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) - - React-intersectionobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) - - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) - - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) - - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) - - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) - - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) - - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) - - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - - React-networking (from `../node_modules/react-native/ReactCommon/react/networking`) - - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) - - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) - - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) - - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) - - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - - React-RCTFabric (from `../node_modules/react-native/React`) - - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) - - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) - - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) - - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) - - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) - - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) - - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) - - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) - - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) - - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) - - ReactCodegen (from `build/generated/ios/ReactCodegen`) - - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - SessionReplayReactNative (from `../..`) - - SocketRocket (~> 0.7.1) - - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) - -SPEC REPOS: - trunk: - - KSCrash - - LaunchDarkly - - LaunchDarklyObservability - - LaunchDarklySessionReplay - - LDSwiftEventSource - - OpenTelemetry-Swift-Api - - OpenTelemetry-Swift-Sdk - - SocketRocket - - SwiftProtobuf - -EXTERNAL SOURCES: - boost: - :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" - DoubleConversion: - :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" - fast_float: - :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" - FBLazyVector: - :path: "../node_modules/react-native/Libraries/FBLazyVector" - fmt: - :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" - glog: - :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" - hermes-engine: - :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" - :tag: hermes-v0.14.0 - RCT-Folly: - :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" - RCTDeprecation: - :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" - RCTRequired: - :path: "../node_modules/react-native/Libraries/Required" - RCTSwiftUI: - :path: "../node_modules/react-native/ReactApple/RCTSwiftUI" - RCTSwiftUIWrapper: - :path: "../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" - RCTTypeSafety: - :path: "../node_modules/react-native/Libraries/TypeSafety" - React: - :path: "../node_modules/react-native/" - React-callinvoker: - :path: "../node_modules/react-native/ReactCommon/callinvoker" - React-Core: - :path: "../node_modules/react-native/" - React-CoreModules: - :path: "../node_modules/react-native/React/CoreModules" - React-cxxreact: - :path: "../node_modules/react-native/ReactCommon/cxxreact" - React-debug: - :path: "../node_modules/react-native/ReactCommon/react/debug" - React-defaultsnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" - React-domnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" - React-Fabric: - :path: "../node_modules/react-native/ReactCommon" - React-FabricComponents: - :path: "../node_modules/react-native/ReactCommon" - React-FabricImage: - :path: "../node_modules/react-native/ReactCommon" - React-featureflags: - :path: "../node_modules/react-native/ReactCommon/react/featureflags" - React-featureflagsnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" - React-graphics: - :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" - React-hermes: - :path: "../node_modules/react-native/ReactCommon/hermes" - React-idlecallbacksnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" - React-ImageManager: - :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" - React-intersectionobservernativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" - React-jserrorhandler: - :path: "../node_modules/react-native/ReactCommon/jserrorhandler" - React-jsi: - :path: "../node_modules/react-native/ReactCommon/jsi" - React-jsiexecutor: - :path: "../node_modules/react-native/ReactCommon/jsiexecutor" - React-jsinspector: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" - React-jsinspectorcdp: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" - React-jsinspectornetwork: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" - React-jsinspectortracing: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" - React-jsitooling: - :path: "../node_modules/react-native/ReactCommon/jsitooling" - React-jsitracing: - :path: "../node_modules/react-native/ReactCommon/hermes/executor/" - React-logger: - :path: "../node_modules/react-native/ReactCommon/logger" - React-Mapbuffer: - :path: "../node_modules/react-native/ReactCommon" - React-microtasksnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" - React-NativeModulesApple: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" - React-networking: - :path: "../node_modules/react-native/ReactCommon/react/networking" - React-oscompat: - :path: "../node_modules/react-native/ReactCommon/oscompat" - React-perflogger: - :path: "../node_modules/react-native/ReactCommon/reactperflogger" - React-performancecdpmetrics: - :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" - React-performancetimeline: - :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" - React-RCTActionSheet: - :path: "../node_modules/react-native/Libraries/ActionSheetIOS" - React-RCTAnimation: - :path: "../node_modules/react-native/Libraries/NativeAnimation" - React-RCTAppDelegate: - :path: "../node_modules/react-native/Libraries/AppDelegate" - React-RCTBlob: - :path: "../node_modules/react-native/Libraries/Blob" - React-RCTFabric: - :path: "../node_modules/react-native/React" - React-RCTFBReactNativeSpec: - :path: "../node_modules/react-native/React" - React-RCTImage: - :path: "../node_modules/react-native/Libraries/Image" - React-RCTLinking: - :path: "../node_modules/react-native/Libraries/LinkingIOS" - React-RCTNetwork: - :path: "../node_modules/react-native/Libraries/Network" - React-RCTRuntime: - :path: "../node_modules/react-native/React/Runtime" - React-RCTSettings: - :path: "../node_modules/react-native/Libraries/Settings" - React-RCTText: - :path: "../node_modules/react-native/Libraries/Text" - React-RCTVibration: - :path: "../node_modules/react-native/Libraries/Vibration" - React-rendererconsistency: - :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" - React-renderercss: - :path: "../node_modules/react-native/ReactCommon/react/renderer/css" - React-rendererdebug: - :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" - React-RuntimeApple: - :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" - React-RuntimeCore: - :path: "../node_modules/react-native/ReactCommon/react/runtime" - React-runtimeexecutor: - :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" - React-RuntimeHermes: - :path: "../node_modules/react-native/ReactCommon/react/runtime" - React-runtimescheduler: - :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" - React-timing: - :path: "../node_modules/react-native/ReactCommon/react/timing" - React-utils: - :path: "../node_modules/react-native/ReactCommon/react/utils" - React-webperformancenativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" - ReactAppDependencyProvider: - :path: build/generated/ios/ReactAppDependencyProvider - ReactCodegen: - :path: build/generated/ios/ReactCodegen - ReactCommon: - :path: "../node_modules/react-native/ReactCommon" - SessionReplayReactNative: - :path: "../.." - Yoga: - :path: "../node_modules/react-native/ReactCommon/yoga" - -SPEC CHECKSUMS: - boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb - fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 - FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458 - fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd - glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 30639e02d0cb4542ae6f523e1bd4d7f7064f6c62 - KSCrash: 8c4464fd5da7de520f2ce4a00fdf63f169a80f18 - LaunchDarkly: 489abfe0f131952adeac976077f953d188ee4731 - LaunchDarklyObservability: 7806e37693d38674b2b1f78187af2e145b2173e5 - LaunchDarklySessionReplay: a5b9d7698455dab185963a1644f5d794c662579f - LDSwiftEventSource: b0826ca826ca890609a9833a05cae1f357fd3b99 - OpenTelemetry-Swift-Api: 3d77582ab6837a63b65bf7d2eacc57d8f2595edd - OpenTelemetry-Swift-Sdk: 69d60f0242e830366e359481edd575d6776eb983 - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 - RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36 - RCTRequired: f3540eee8094231581d40c5c6d41b0f170237a81 - RCTSwiftUI: 5928f7ca7e9e2f1a82d85d4c79ea3065137ad81c - RCTSwiftUIWrapper: 8ff2f9da84b47db66d11ece1589d8e5515c0ab8b - RCTTypeSafety: 6359ff3fcbe18c52059f4d4ce301e47f9da5f0d5 - React: f6f8fc5c01e77349cdfaf49102bcb928ac31d8ed - React-callinvoker: 032b6d1d03654b9fb7de9e2b3b978d3cb1a893ad - React-Core: 418c9278f8a071b44a88a87be9a4943234cc2e77 - React-CoreModules: 925b8cb677649f967f6000f9b1ef74dc4ff60c30 - React-cxxreact: 21f6f0cb2a7d26fbed4d09e04482e5c75662beaf - React-debug: 8fc21f2fecd3d6244e988dc55d60cb117d122588 - React-defaultsnativemodule: 05c6115a2d3a7f4a2cc3f96022261570700dbfa5 - React-domnativemodule: f19d7fd59facf19a4e6cb75bf48357c329acaea7 - React-Fabric: 94acdbc0b889bdcec2d5b1a90ae48f1032c5a5a1 - React-FabricComponents: 9754fb783979b88fb82ed3d0c50ae5f5d775a86f - React-FabricImage: d8f5bcb5006eafc0e2262c11bf4dedaa610fd66c - React-featureflags: 8bd4abaf8adf3cf5cc115f128e8761fd3d95b848 - React-featureflagsnativemodule: 0062ca1dc92cb5aae22df8aed4e8f261759cb3bd - React-graphics: 318048b8f98e040c093adcb77ffeb46d78961c30 - React-hermes: 05ca52f53557a31b8ef8bac8f94c3f9db1ff00ed - React-idlecallbacksnativemodule: d3c5ba0150555ce9b7db85008aeb170a02bbf2d8 - React-ImageManager: 225b19fcb16fd353851d664c344025a6d4d79870 - React-intersectionobservernativemodule: d490ebd28572754dfdad4a8d0771573345b1ec92 - React-jserrorhandler: caafb9c1d42c24422829e71e8178de3dd1c7ea12 - React-jsi: 749de748ad3b760011255326c63bf7b7dd6f8f9d - React-jsiexecutor: 02a5ee45bffcae98197eaa253fbf13b65c95073d - React-jsinspector: 4a031b0605009d4bcd079c99df85eb55d142cd12 - React-jsinspectorcdp: 6d25166ec876053b7b6e290eb57f41a9f9496846 - React-jsinspectornetwork: 5c481d208eade7a338f545b2645a2cf134fdf265 - React-jsinspectortracing: b4d2404ecd64a0dd65e2746d9867fbc3a7cd0927 - React-jsitooling: e0d93e78a5a231e4089459ddbed8d4844be9e238 - React-jsitracing: f3c4aae144b86799e9e23eb5ef16bae6b474d4e2 - React-logger: 9e597cbeda7b8cc8aa8fb93860dade97190f69cc - React-Mapbuffer: 20046c0447efaa7aace0b76085aa9bb35b0e8105 - React-microtasksnativemodule: 0e837de56519c92d8a2e3097717df9497feb33cb - React-NativeModulesApple: 1a378198515f8e825c5931a7613e98da69320cee - React-networking: bfd1695ada5a57023006ce05823ac5391c3ce072 - React-oscompat: aedc0afbded67280de6bb6bfac8cfde0389e2b33 - React-perflogger: c174462de00c0b7d768f0b2d61b8e2240717a667 - React-performancecdpmetrics: 2607a034407d55049f1820b7ec86db1efd3d22e1 - React-performancetimeline: 6ebdcdf745dbe372508ad7164e732362e7eeae6f - React-RCTActionSheet: 175c74d343e92793d3187b3a819d565f534e0b1d - React-RCTAnimation: d67919cddb7da39c949b8010b4fd4ea39815fe4e - React-RCTAppDelegate: 5f7b1e4b7ee5a44faf5f9518a7d3cabafb801adf - React-RCTBlob: 7ceb93e0918511163f036cfd295973f132a2bc57 - React-RCTFabric: f2250d34e1143c659b845af7e369b3f8f015950c - React-RCTFBReactNativeSpec: b0fc0c9c8adaf8b9183f9e9fb5455ca5deedc7a0 - React-RCTImage: d6297035168312fc3089f8ca0ee7a75216f21715 - React-RCTLinking: 619a2553c4ef83acaccfb551ada1b7d45cf1cce3 - React-RCTNetwork: 7df41788a194dc5b628f58db6a765224b6b37eac - React-RCTRuntime: f75ec08d991c611f1d74154dfeb852e30b1825dd - React-RCTSettings: fa7882ce3d73f1e3482fe05f9cb3167a35a60869 - React-RCTText: 4d659598d9b7730343d465c43d97b3f4aad13938 - React-RCTVibration: 968c3184bfe5005bedd86c913a3b52438222e3a4 - React-rendererconsistency: 1204c62facf6168b69bc5022e0020f19c92f138e - React-renderercss: 36c02a3c55402fdb06226c2ef04d82fc06c4e2fc - React-rendererdebug: 11b54233498d961d939d2f2ec6c640d44efa3c12 - React-RuntimeApple: 5287d92680f4b08c8e882afe9791a41eab69d4a7 - React-RuntimeCore: 402b658d8e9cefb44824624e39a0804f2237e205 - React-runtimeexecutor: a1ce75c4e153ede11be957ef31bb72eef9cc4daf - React-RuntimeHermes: c987b19a1284c685062d3eaad79fd9300a3aa82f - React-runtimescheduler: a12722da46f562626f5897edf9b8fa02219de065 - React-timing: a453a65192dbe400d61d299024e95a302e726661 - React-utils: 43479e74f806f6633ee04c212c48811530041170 - React-webperformancenativemodule: bd1ad71ea9e217e55f66233e99d02581ee3d5cb7 - ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568 - ReactCodegen: 11c08ff43a62009d48c71de000352e4515918801 - ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f - SessionReplayReactNative: 60664a81e02ef6a64da725c84fe6e709c6f0cd5d - SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38 - Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e - -PODFILE CHECKSUM: 53fee6f649d87604b0c5ad827154b6c454d1a29a - -COCOAPODS: 1.16.2 diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/metro.config.js b/sdk/@launchdarkly/react-native-ld-session-replay/example/metro.config.js index 2da198e82c..8723f6e149 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/metro.config.js +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/metro.config.js @@ -4,13 +4,67 @@ const { withMetroConfig } = require('react-native-monorepo-config'); const root = path.resolve(__dirname, '..'); +// The example imports sibling workspace packages (e.g. +// @launchdarkly/observability-react-native and @launchdarkly/observability-shared), +// which live outside `root`. Metro must watch their real paths to resolve the +// workspace symlinks; the sdk/@launchdarkly directory covers them all without +// dragging in the much larger rrweb/ and e2e/ trees. +const sdkPackages = path.resolve(__dirname, '../..'); +// Several of those packages' dependencies (@opentelemetry/*, graphql) are +// hoisted to the monorepo root node_modules, so it must be resolvable too. +const monorepoNodeModules = path.resolve(__dirname, '../../../../node_modules'); + /** * Metro configuration * https://facebook.github.io/metro/docs/configuration * * @type {import('metro-config').MetroConfig} */ -module.exports = withMetroConfig(getDefaultConfig(__dirname), { +const config = withMetroConfig(getDefaultConfig(__dirname), { root, dirname: __dirname, }); + +config.watchFolders = [ + ...(config.watchFolders || []), + sdkPackages, + monorepoNodeModules, +]; + +config.resolver.nodeModulesPaths = [ + ...(config.resolver.nodeModulesPaths || []), + monorepoNodeModules, +]; + +// This monorepo has SEVERAL different react-native versions installed (the root +// has 0.76.x, @launchdarkly/observability-react-native ships its own 0.79.x as a +// dev dependency, the example builds against 0.83.x, etc.). If any package +// resolves react / react-native from its own copy, the bundle ends up with a +// duplicate JS runtime whose TurboModule registry isn't wired to the native +// binary — producing "getEnforcing(...) could not be found" for both our module +// AND core modules like PlatformConstants (used by AppState in the observability +// plugin). Force every react / react-native import, from anywhere in the graph, +// to resolve to the example's single copy. +const forcedSingletons = { + 'react': path.resolve(__dirname, 'node_modules/react'), + 'react-native': path.resolve(__dirname, 'node_modules/react-native'), +}; +const defaultResolveRequest = config.resolver.resolveRequest; +config.resolver.resolveRequest = (context, moduleName, platform) => { + for (const [name, target] of Object.entries(forcedSingletons)) { + if (moduleName === name || moduleName.startsWith(`${name}/`)) { + return context.resolveRequest( + context, + target + moduleName.slice(name.length), + platform + ); + } + } + return (defaultResolveRequest || context.resolveRequest)( + context, + moduleName, + platform + ); +}; + +module.exports = config; diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/package.json b/sdk/@launchdarkly/react-native-ld-session-replay/example/package.json index feb5d53dc2..362435ec9a 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/package.json +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/package.json @@ -10,8 +10,10 @@ "build:ios": "react-native build-ios --mode Debug" }, "dependencies": { + "@launchdarkly/observability-react-native": "workspace:*", "@launchdarkly/react-native-client-sdk": "^10.12.5", "@launchdarkly/session-replay-react-native": "workspace:*", + "@opentelemetry/api": "^1.9.0", "react": "19.2.0", "react-native": "0.83.0", "react-native-webview": "13.16.1" diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/src/ApiScreen.tsx b/sdk/@launchdarkly/react-native-ld-session-replay/example/src/ApiScreen.tsx new file mode 100644 index 0000000000..e41f2db7fe --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/src/ApiScreen.tsx @@ -0,0 +1,499 @@ +import { useState } from 'react'; +import { + Platform, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { LDObserve } from '@launchdarkly/observability-react-native'; +import type { SpanScope } from '@launchdarkly/observability-react-native'; +import { useLDClient } from '@launchdarkly/react-native-client-sdk'; +import { context } from '@opentelemetry/api'; + +/** + * Manual test screen that mirrors the iOS TestApp `MainMenuViewModel` + * (swift-launchdarkly-observability/TestApp/Sources/MainMenuViewModel.swift), + * exercising the public Observability + LaunchDarkly client APIs from React + * Native. + * + * The iOS sample has both `LDObserve.track` and `LDClient.track` variants. The + * React Native Observability API does not expose `track`, so every track recipe + * here goes through the LaunchDarkly client's `track` (the Observability plugin + * turns it into a span via its afterTrack hook). + */ + +export default function ApiScreen() { + const ldClient = useLDClient(); + const [lines, setLines] = useState([]); + + const log = (msg: string) => + setLines((prev) => + [`${new Date().toLocaleTimeString()} ${msg}`, ...prev].slice(0, 200) + ); + + // Wrap each recipe so a thrown error is surfaced in the log instead of + // crashing the screen. + const run = (label: string, fn: () => void | Promise) => async () => { + try { + await fn(); + } catch (err) { + log(`✗ ${label} threw: ${(err as Error).message}`); + } + }; + + // -- Errors -------------------------------------------------------------- + const recordError = () => { + LDObserve.recordError(new Error('Demo failure: crash'), {}); + log('[error] recordError(Demo failure)'); + }; + + // -- Span + flag variation ---------------------------------------------- + const recordSpanAndVariation = () => { + const span = LDObserve.startSpan('button-pressed'); + const value = ldClient.boolVariation('feature1', false); + span.setAttribute('feature1', value); + span.end(); + log(`[span] button-pressed + boolVariation(feature1)=${value}`); + }; + + // -- Nested spans (counter + log + network inside) ----------------------- + // Uses `withSpan` / `scope.child` so the hierarchy survives the `await`s. On + // React Native the active OTel context is tracked only synchronously, so + // reading it back after an `await` (e.g. plain nested `startActiveSpan`) + // would re-root the inner spans — `scope.child` captures the parent context + // explicitly instead. See the distributed tracing guide. + const triggerNestedSpans = async () => { + await LDObserve.withSpan('NestedSpan', async (scope0) => { + scope0.span.setAttribute('test-true', true); + scope0.span.setAttribute('test-double', 3.14); + await scope0.child('NestedSpan1', async (scope1) => { + await scope1.child('NestedSpan2', async (scope2) => { + LDObserve.recordCount({ name: 'NestedCounter', value: 10.0 }); + LDObserve.recordLog('NestedLog', 'info', {}); + await fetchUrlsForNestedSpanDemo(scope2); + }); + }); + }); + log( + '[nested] NestedSpan > NestedSpan1 > NestedSpan2 (+ counter, log, http)' + ); + }; + + // Auto-instrumented fetch spans only attach to the active span when `fetch` + // is *invoked* synchronously. The active context is lost across each `await`, + // so wrap every call in `scope.active(...)` to keep both requests parented to + // NestedSpan2 (the second one would otherwise become its own root trace). + const fetchUrlsForNestedSpanDemo = async (scope: SpanScope) => { + try { + await scope.active(() => fetch('https://www.google.com')); + await scope.active(() => fetch('https://www.android.com/')); + } catch { + // ignore network errors in the demo + } + }; + + // -- Metrics ------------------------------------------------------------- + const recordMetric = () => { + LDObserve.recordMetric({ name: 'test-gauge', value: 50.0 }); + log('[metric] gauge test-gauge=50'); + }; + + const recordHistogramMetric = () => { + LDObserve.recordHistogram({ name: 'test-histogram', value: 15.0 }); + log('[metric] histogram test-histogram=15'); + }; + + const recordCounterMetric = () => { + LDObserve.recordCount({ name: 'test-counter', value: 10.0 }); + log('[metric] counter test-counter=10'); + }; + + const recordIncrementalMetric = () => { + LDObserve.recordIncr({ name: 'test-incremental-counter', value: 12.0 }); + log('[metric] incr test-incremental-counter=12'); + }; + + const recordUpDownCounterMetric = () => { + LDObserve.recordUpDownCounter({ + name: 'test-up-down-counter', + value: 25.0, + }); + log('[metric] up/down test-up-down-counter=25'); + }; + + // -- Logs ---------------------------------------------------------------- + const recordLogWithContext = () => { + const span = LDObserve.startSpan('log-context-demo', { + attributes: { demo: 'log-with-context' }, + }); + const capturedContext = LDObserve.getContextFromSpan(span); + span.end(); + + // Simulate a detached task where the active OTel context is lost; re-attach + // the captured span context so the log stays correlated to the span. + setTimeout(() => { + context.with(capturedContext, () => { + LDObserve.recordLog('Log with span context', 'warn', { + source: 'detached-queue-demo', + }); + }); + log('[log] recordLog correlated to log-context-demo span'); + }, 0); + }; + + const recordLogs = () => { + LDObserve.recordLog('logs-button-pressed', 'info', { + 'test-string': 'react-native', + 'test-true': true, + 'test-false': false, + 'test-integer': 42, + 'test-long': 9_000_000_000, + 'test-double': 3.14, + // OTel attributes only allow primitives or homogeneous arrays of + // primitives, so the iOS sample's nested map is represented as JSON here. + 'test-array': [3.14], + 'test-nested': JSON.stringify({ array: [1] }), + }); + log('[log] recordLog(logs-button-pressed) with mixed attributes'); + }; + + // -- Track (via LaunchDarkly client) ------------------------------------- + const trackViaLDClient = () => { + ldClient.track('track-via-ld-client', { + 'test-string': 'react-native', + 'test-true': true, + 'test-false': false, + 'test-integer': 42, + 'test-double': 3.14, + 'test-long-number': 9_000_000_000_123, + }); + log('[track] ldClient.track(track-via-ld-client)'); + }; + + const trackNested = () => { + ldClient.track('checkout-started', { + name: 'Checkout Started', + order_id: 'ord_5521', + value: 72.0, + currency: 'USD', + products: [ + { product_id: 'SKU-1234', quantity: 2, price: 24.0 }, + { product_id: 'SKU-9876', quantity: 1, price: 24.0 }, + ], + }); + log('[track] ldClient.track(checkout-started) nested payload'); + }; + + // -- Network ------------------------------------------------------------- + const performNetworkRequest = async () => { + try { + await fetch('https://launchdarkly.com/'); + log('[network] GET launchdarkly.com'); + } catch { + log('[network] GET launchdarkly.com (failed, ignored)'); + } + }; + + // -- Identify ------------------------------------------------------------ + const identifyUser = async () => { + await ldClient.identify({ + kind: 'user', + key: 'single-userkey', + firstName: 'Bob', + lastName: 'Bobberson', + }); + log('[identify] user single-userkey'); + }; + + const identifyAnonymous = async () => { + await ldClient.identify({ kind: 'user', anonymous: true }); + log('[identify] anonymous user'); + }; + + const identifyMulti = async () => { + await ldClient.identify({ + kind: 'multi', + user: { + key: 'multi-username', + name: 'multi-username', + anonymous: false, + customerNumber: '654321', + firstName: 'Bob', + lastName: 'Bobberson', + email: 'multi@multi.com', + }, + device: { + key: 'iphone', + name: 'iphone', + anonymous: false, + platform: 'ios', + appVersion: '10.3.2.1', + }, + }); + log('[identify] multi (user + device)'); + }; + + // -- Crash --------------------------------------------------------------- + const crash = () => { + // Throw outside the `run` try/catch so it surfaces as an unhandled error, + // mirroring the iOS `fatalError()` demo button. + setTimeout(() => { + throw new Error('Forced crash from API demo'); + }, 0); + log('[crash] scheduling unhandled error…'); + }; + + const flush = run('flush', async () => { + await LDObserve.flush(); + log('[i] flushed telemetry'); + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setLines([])} + variant="danger" + /> + + + + + Output log + + {lines.length === 0 ? ( + No output yet — tap a button. + ) : ( + lines.map((line, i) => ( + + {line} + + )) + )} + + + + ); +} + +function SectionHeader({ + title, + topSpacing, +}: { + title: string; + topSpacing?: boolean; +}) { + return ( + <> + + {title} + + + + ); +} + +function Btn({ + label, + onPress, + variant, +}: { + label: string; + onPress: () => void; + variant?: 'default' | 'danger' | 'identify'; +}) { + return ( + + + {label} + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: '#000', + }, + scroll: { + padding: 16, + paddingBottom: 24, + }, + sectionTitle: { + color: '#fff', + fontSize: 20, + fontWeight: 'bold', + }, + divider: { + height: 1, + backgroundColor: '#555', + marginTop: 4, + marginBottom: 8, + }, + col: { + gap: 8, + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + btn: { + backgroundColor: '#6650A4', + borderRadius: 8, + paddingVertical: 10, + paddingHorizontal: 16, + alignItems: 'center', + }, + btnDanger: { + backgroundColor: '#F2B8B5', + }, + // Matches the iOS TestApp identify buttons (Colors.identifyBgColor / + // Colors.identifyTextColor in MainMenuView.swift). + btnIdentify: { + backgroundColor: '#121D61', + }, + btnIdentifyText: { + color: '#8A9EFF', + }, + btnText: { + color: '#fff', + fontWeight: '600', + }, + logContainer: { + height: 200, + borderTopWidth: 1, + borderTopColor: '#333', + backgroundColor: '#0B0B0B', + paddingHorizontal: 12, + paddingTop: 8, + }, + logTitle: { + color: '#888', + fontSize: 12, + fontWeight: '600', + textTransform: 'uppercase', + marginBottom: 4, + }, + log: { + flex: 1, + }, + logScroll: { + paddingBottom: 12, + }, + logEmpty: { + color: '#555', + fontSize: 13, + fontStyle: 'italic', + }, + logLine: { + color: '#A5D6A7', + fontSize: 12, + fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', + paddingVertical: 1, + }, +}); diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/src/App.tsx b/sdk/@launchdarkly/react-native-ld-session-replay/example/src/App.tsx index 6e6b9eee57..f2d4059121 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/example/src/App.tsx +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/src/App.tsx @@ -12,31 +12,53 @@ import { } from '@launchdarkly/react-native-client-sdk'; import { useEffect, useState } from 'react'; import { createSessionReplayPlugin } from '@launchdarkly/session-replay-react-native'; +import { Observability } from '@launchdarkly/observability-react-native'; import DialogsScreen from './DialogsScreen'; import MaskingScreen from './MaskingScreen'; +import TracingScreen from './TracingScreen'; +import ApiScreen from './ApiScreen'; const plugin = createSessionReplayPlugin({ isEnabled: true, + // Forwarded to the native observability + session replay instances so their + // spans report the same service.name / service.version as the JS observability + // plugin below. serviceVersion only affects observability-emitted signals. + serviceName: 'session-replay-rn-example', + serviceVersion: '1.0.0', maskTextInputs: true, maskWebViews: true, - maskLabels: true, - maskImages: true, + maskLabels: false, + maskImages: false, maskTestIDs: ['password', 'ssn'], unmaskTestIDs: ['safe'], minimumAlpha: 0.05, }); +// The observability plugin powers the distributed tracing examples on the +// "Tracing" tab. `tracingOrigins` opts the demo API hosts into W3C +// `traceparent` / `baggage` header propagation so device spans can link to a +// backend trace (see the tracing guide, sections 11 and 12). +const observability = new Observability({ + serviceName: 'session-replay-rn-example', + serviceVersion: '1.0.0', + debug: true, + tracingOrigins: ['jsonplaceholder.typicode.com', 'reactnative.dev'], +}); + // Replace with your LaunchDarkly mobile key. // You can also set the LAUNCHDARKLY_MOBILE_KEY environment variable. const MOBILE_KEY = process.env.LAUNCHDARKLY_MOBILE_KEY || 'YOUR_LAUNCHDARKLY_MOBILE_KEY_HERE'; +// Observability must come before the session replay plugin: the replay plugin +// reads the observability session id during registration so the native replay / +// observability instance can adopt it (shared `session.id` across JS + native). const client = new ReactNativeLDClient(MOBILE_KEY, AutoEnvAttributes.Enabled, { - plugins: [plugin], + plugins: [observability, plugin], }); const context = { kind: 'user', key: 'user-key-123abc' }; -type Tab = 'masking' | 'dialogs'; +type Tab = 'masking' | 'dialogs' | 'api' | 'tracing'; export default function App() { const [tab, setTab] = useState('masking'); @@ -59,8 +81,21 @@ export default function App() { active={tab === 'dialogs'} onPress={() => setTab('dialogs')} /> + setTab('api')} + /> + setTab('tracing')} + /> - {tab === 'masking' ? : } + {tab === 'masking' && } + {tab === 'dialogs' && } + {tab === 'api' && } + {tab === 'tracing' && } ); diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/example/src/TracingScreen.tsx b/sdk/@launchdarkly/react-native-ld-session-replay/example/src/TracingScreen.tsx new file mode 100644 index 0000000000..f7ec4b6cc7 --- /dev/null +++ b/sdk/@launchdarkly/react-native-ld-session-replay/example/src/TracingScreen.tsx @@ -0,0 +1,626 @@ +import { useEffect, useRef, useState } from 'react'; +import { + Platform, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { LDObserve } from '@launchdarkly/observability-react-native'; +import { + context, + propagation, + SpanStatusCode, + trace, + type Span, +} from '@opentelemetry/api'; + +/** + * Manual test screen that exercises every recipe from the React Native + * tracing guide: + * sdk/@launchdarkly/observability-react-native/guides/tracing.md + * + * Each button runs one recipe and appends a line (with the resulting trace/span + * IDs where relevant) to the on-screen log. The observability plugin is wired up + * in App.tsx with `tracingOrigins` set to the demo API hosts used below, so the + * backend-propagation recipes (11, 12) actually inject `traceparent` / `baggage` + * headers. + * + * Note: with a placeholder mobile key the SDK still creates spans locally; export + * to LaunchDarkly only happens once a real key is configured. + */ + +// Recipes below use literal demo endpoints so each one stands on its own. The +// jsonplaceholder host is listed in `tracingOrigins` (see App.tsx), so requests +// to it carry trace/baggage headers. +const short = (id?: string) => (id ? id.slice(0, 8) : 'n/a'); +const traceOf = (span: Span) => short(span.spanContext().traceId); + +export default function TracingScreen() { + const [lines, setLines] = useState([]); + const intervalRef = useRef | null>(null); + + const log = (msg: string) => + setLines((prev) => + [`${new Date().toLocaleTimeString()} ${msg}`, ...prev].slice(0, 200) + ); + + // Wrap each recipe so a thrown error is surfaced in the log instead of + // crashing the screen. + const run = (label: string, fn: () => void | Promise) => async () => { + try { + await fn(); + } catch (err) { + log(`✗ ${label} threw: ${(err as Error).message}`); + } + }; + + useEffect( + () => () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }, + [] + ); + + // -- 1. Root span -------------------------------------------------------- + const rootSpan = () => { + LDObserve.startActiveSpan( + 'app-cold-start', + (span) => { + span.setAttribute('launch_type', 'cold'); + span.setAttribute('device_os', Platform.OS); + span.addEvent('splash_rendered'); + span.addEvent('home_screen_ready'); + log(`[1] root span "app-cold-start" trace=${traceOf(span)}`); + span.end(); + }, + { root: true } + ); + }; + + // -- 2. Nested spans ----------------------------------------------------- + const nestedSpans = async () => { + // `withSpan` ends each span automatically and `scope.child` parents off the + // captured context, so the LoadProducts > FetchFromApi > DeserializeJson / + // RenderUI hierarchy survives the `await`s without threading context by hand + // (React Native's StackContextManager only tracks the active span + // synchronously). + const count = await LDObserve.withSpan('LoadProducts', async (load) => { + const items = await load.child('FetchFromApi', async (fetchScope) => { + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts' + ); + fetchScope.span.setAttribute('http.status_code', response.status); + const json = await response.text(); + // Parents to FetchFromApi even though we are past two awaits. + return fetchScope.child('DeserializeJson', (parseScope) => { + const result = JSON.parse(json) as unknown[]; + parseScope.span.setAttribute('product_count', result.length); + return result; + }); + }); + // Parents to LoadProducts (not FetchFromApi) — uses the captured context. + load.child('RenderUI', (renderScope) => { + renderScope.span.setAttribute('product_count', items.length); + }); + return items.length; + }); + log(`[2] LoadProducts > FetchFromApi > DeserializeJson (${count})`); + }; + + // -- 3. HTTP span with manual error handling ----------------------------- + const httpWithErrorHandling = async () => { + await LDObserve.startActiveSpan('FetchUserProfile', async (span) => { + span.setAttribute('user.id', '1'); + span.setAttribute('http.method', 'GET'); + try { + const url = 'https://jsonplaceholder.typicode.com/users/1'; + span.setAttribute('http.url', url); + const response = await fetch(url); + span.setAttribute('http.status_code', response.status); + if (!response.ok) { + span.setStatus({ code: SpanStatusCode.ERROR }); + span.setAttribute('error.type', `HTTP ${response.status}`); + log(`[3] FetchUserProfile failed: HTTP ${response.status}`); + return; + } + await response.json(); + span.setStatus({ code: SpanStatusCode.OK }); + log(`[3] FetchUserProfile OK trace=${traceOf(span)}`); + } catch (err) { + span.recordException(err as Error); + span.setStatus({ code: SpanStatusCode.ERROR }); + log(`[3] FetchUserProfile threw: ${(err as Error).message}`); + } finally { + span.end(); + } + }); + }; + + // -- 4. Auto fetch instrumentation under a custom parent ----------------- + const autoInstrumentedChild = async () => { + await LDObserve.withSpan('SyncOrders', async ({ span }) => { + span.setAttribute('sync.direction', 'pull'); + // `withSpan` makes SyncOrders active for the synchronous window, so this + // fetch (started before the first await) auto-parents to it. + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts?_limit=5' + ); + span.setAttribute('http.status_code', response.status); + const orders = (await response.json()) as unknown[]; + span.setAttribute('order_count', orders.length); + log(`[4] SyncOrders + auto HTTP child (${orders.length} orders)`); + }); + }; + + // -- 5. Record exception and mark span failed ---------------------------- + const recordException = async () => { + await LDObserve.startActiveSpan('ProcessPayment', async (span) => { + span.setAttribute('order.id', 'order-42'); + span.setAttribute('payment.amount', 19.99); + try { + // Simulate a gateway failure. + throw new Error('Payment gateway timeout'); + } catch (err) { + const error = err as Error; + span.recordException(error); + span.setStatus({ code: SpanStatusCode.ERROR }); + span.setAttribute('error.category', error.name); + LDObserve.recordError(error, { 'order.id': 'order-42' }, { span }); + log(`[5] ProcessPayment recorded exception: ${error.message}`); + } finally { + span.end(); + } + }); + }; + + // -- 6. Correlated logs inside the active span --------------------------- + const correlatedLogs = async () => { + await LDObserve.withSpan('ImportCatalog', async ({ span, active }) => { + // Logs correlate to a span via the active context. The first log is in the + // synchronous window so it correlates automatically. + LDObserve.recordLog('Import started', 'info', { source: 'demo' }); + let imported = 0; + for (const _row of [1, 2, 3, 4, 5]) { + await new Promise((r) => setTimeout(r, 5)); + imported++; + } + span.setAttribute('imported_count', imported); + // After the awaits the active context is gone — re-establish it with + // `active()` so this log still correlates to ImportCatalog. + active(() => + LDObserve.recordLog('Import completed', 'info', { + imported_count: imported, + }) + ); + log(`[6] ImportCatalog: 2 correlated logs, trace=${traceOf(span)}`); + }); + }; + + // -- 7. Re-establish context to correlate logs across async boundaries ---- + const correlateAcrossAsync = () => { + const span = LDObserve.startSpan('UploadReport'); + span.setAttribute('report.type', 'daily'); + const capturedContext = LDObserve.getContextFromSpan(span); + const tid = traceOf(span); + span.end(); + + setTimeout(() => { + // Active context is empty here -- re-establish it explicitly. + context.with(capturedContext, () => { + LDObserve.recordLog('Upload processing on background tick', 'info', { + phase: 'start', + }); + LDObserve.recordLog('Upload complete', 'info', { phase: 'end' }); + }); + log(`[7] Re-established context for logs, trace=${tid}`); + }, 0); + }; + + // -- 8a. Child span where automatic propagation won't work --------------- + const detachedChildSpan = () => { + LDObserve.withSpan('ScheduleSync', ({ span, ctx }) => { + span.setAttribute('sync.mode', 'background'); + const tid = traceOf(span); + + // setTimeout drops the active context -> capture ScheduleSync's `ctx` and + // pass it as the explicit `parent` once the timer fires. + setTimeout(async () => { + await LDObserve.withSpan( + 'BackgroundSync', + async ({ span: childSpan }) => { + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts/1' + ); + childSpan.setAttribute('http.status_code', response.status); + childSpan.addEvent('sync.complete'); + }, + { parent: ctx } + ); + log(`[8a] BackgroundSync re-parented to ScheduleSync trace=${tid}`); + }, 0); + }); + }; + + // -- 8b. Bounded polling with re-parented tick spans --------------------- + const startBoundedPolling = () => { + if (intervalRef.current) { + log('[8b] polling already running'); + return; + } + const span = LDObserve.startSpan('StartPolling'); + const parentContext = LDObserve.getContextFromSpan(span); + const tid = traceOf(span); + span.end(); + + let ticks = 0; + intervalRef.current = setInterval(() => { + ticks++; + LDObserve.withSpan( + 'PollTick', + ({ span: pollSpan }) => { + pollSpan.setAttribute('tick.time', new Date().toISOString()); + pollSpan.setAttribute('tick.number', ticks); + }, + { parent: parentContext } + ); + log(`[8b] PollTick #${ticks} parent trace=${tid}`); + if (ticks >= 3 && intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }, 1000); + }; + + // -- 9. Sequential independent root spans -------------------------------- + // Created inside an active parent span on purpose: with `{ root: true }` each + // analytics span must start a brand-new trace instead of nesting under the + // parent. We then assert every child trace differs from the parent's and from + // each other, so this actually exercises `root` rather than relying on there + // being no ambient context. + const independentRootSpans = () => { + const events = [ + { type: 'view', userId: 'u1' }, + { type: 'click', userId: 'u2' }, + { type: 'purchase', userId: 'u3' }, + ]; + LDObserve.startActiveSpan('AnalyticsBatch', (parent) => { + const parentTrace = parent.spanContext().traceId; + const childTraces: string[] = []; + for (const evt of events) { + LDObserve.startActiveSpan( + `Analytics:${evt.type}`, + (span) => { + span.setAttribute('event.type', evt.type); + span.setAttribute('event.timestamp', new Date().toISOString()); + span.setAttribute('event.user_id', evt.userId); + span.setStatus({ code: SpanStatusCode.OK }); + childTraces.push(span.spanContext().traceId); + span.end(); + }, + { root: true } + ); + } + parent.end(); + + const detachedFromParent = childTraces.every((t) => t !== parentTrace); + const allUnique = new Set(childTraces).size === childTraces.length; + const verdict = detachedFromParent && allUnique ? 'PASS' : 'FAIL'; + log( + `[9] ${verdict} independence: parent=${short(parentTrace)} ` + + `children=[${childTraces.map(short).join(', ')}] ` + + `(detachedFromParent=${detachedFromParent}, allUnique=${allUnique})` + ); + }); + }; + + // -- 10. Span events as lightweight checkpoints -------------------------- + const spanEvents = async () => { + await LDObserve.withSpan('DownloadAndCacheImage', async ({ span }) => { + const imageUrl = 'https://reactnative.dev/img/tiny_logo.png'; + span.setAttribute('image.url', imageUrl); + span.addEvent('download.started'); + const response = await fetch(imageUrl); + const blob = await response.blob(); + span.addEvent('download.completed'); + span.setAttribute('image.size_bytes', blob.size); + span.addEvent('cache.write.started'); + // (no real filesystem write in the demo) + span.addEvent('cache.write.completed', { bytes: blob.size }); + log(`[10] DownloadAndCacheImage: ${blob.size} bytes, 4 events`); + }); + }; + + // -- 11. Connecting mobile traces to your backend ------------------------ + const backendDistributedTrace = async () => { + await LDObserve.withSpan('Checkout', async ({ span }) => { + span.setAttribute('cart.id', 'cart-7'); + // traceparent is injected automatically because the host is a tracing + // origin -> a backend span would join this trace. + const response = await fetch( + 'https://jsonplaceholder.typicode.com/posts', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cartId: 'cart-7' }), + } + ); + span.setAttribute('http.status_code', response.status); + log(`[11] Checkout POST (traceparent propagated) trace=${traceOf(span)}`); + }); + }; + + // -- 11b. Continuing a trace from incoming headers ----------------------- + const incomingHeaders = () => { + const headers = { + 'x-session-id': 'sess-abc', + 'x-request-id': 'req-123', + }; + LDObserve.runWithHeaders('HandlePushPayload', headers, (span) => { + span.setAttribute('payload.kind', 'promo'); + log(`[11b] runWithHeaders span, trace=${traceOf(span)}`); + }); + const ctx = LDObserve.parseHeaders(headers); + log(`[11b] parseHeaders -> session=${ctx.sessionId} req=${ctx.requestId}`); + }; + + // -- 12. Propagating baggage -------------------------------------------- + const baggage = async () => { + const bag = propagation.createBaggage({ + 'app.tenant_id': { value: 'acme' }, + 'app.user_tier': { value: 'gold' }, + }); + await context.with( + propagation.setBaggage(context.active(), bag), + async () => { + // Read it back. + const tenant = propagation + .getActiveBaggage() + ?.getEntry('app.tenant_id')?.value; + + await LDObserve.startActiveSpan('LoadDashboard', async (span) => { + // Baggage is not copied onto spans automatically -- do it explicitly. + propagation + .getActiveBaggage() + ?.getAllEntries() + .forEach(([key, entry]) => span.setAttribute(key, entry.value)); + + // Outgoing request to a tracing origin also carries the baggage header. + await fetch('https://jsonplaceholder.typicode.com/posts/1'); + log(`[12] baggage tenant=${tenant} copied onto LoadDashboard span`); + span.end(); + }); + } + ); + }; + + // -- Utilities ----------------------------------------------------------- + const showSessionInfo = () => { + const info = LDObserve.getSessionInfo(); + const active = trace.getActiveSpan(); + log( + `[i] initialized=${LDObserve.isInitialized()} session=${JSON.stringify( + info + )} activeSpan=${active ? 'yes' : 'none'}` + ); + }; + + const flush = run('flush', async () => { + await LDObserve.flush(); + log('[i] flushed telemetry'); + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setLines([])} + variant="danger" + /> + + + + + Output log + + {lines.length === 0 ? ( + No output yet — tap a button. + ) : ( + lines.map((line, i) => ( + + {line} + + )) + )} + + + + ); +} + +function SectionHeader({ + title, + topSpacing, +}: { + title: string; + topSpacing?: boolean; +}) { + return ( + <> + + {title} + + + + ); +} + +function Btn({ + label, + onPress, + variant, +}: { + label: string; + onPress: () => void; + variant?: 'default' | 'danger'; +}) { + return ( + + {label} + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: '#000', + }, + scroll: { + padding: 16, + paddingBottom: 24, + }, + sectionTitle: { + color: '#fff', + fontSize: 20, + fontWeight: 'bold', + }, + divider: { + height: 1, + backgroundColor: '#555', + marginTop: 4, + marginBottom: 8, + }, + col: { + gap: 8, + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + btn: { + backgroundColor: '#6650A4', + borderRadius: 8, + paddingVertical: 10, + paddingHorizontal: 16, + alignItems: 'center', + }, + btnDanger: { + backgroundColor: '#F2B8B5', + }, + btnText: { + color: '#fff', + fontWeight: '600', + }, + logContainer: { + height: 200, + borderTopWidth: 1, + borderTopColor: '#333', + backgroundColor: '#0B0B0B', + paddingHorizontal: 12, + paddingTop: 8, + }, + logTitle: { + color: '#888', + fontSize: 12, + fontWeight: '600', + textTransform: 'uppercase', + marginBottom: 4, + }, + log: { + flex: 1, + }, + logScroll: { + paddingBottom: 12, + }, + logEmpty: { + color: '#555', + fontSize: 13, + fontStyle: 'italic', + }, + logLine: { + color: '#A5D6A7', + fontSize: 12, + fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', + paddingVertical: 1, + }, +}); diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/ios/SessionReplayClientAdapter.swift b/sdk/@launchdarkly/react-native-ld-session-replay/ios/SessionReplayClientAdapter.swift index b4a8dac52f..91385262a7 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/ios/SessionReplayClientAdapter.swift +++ b/sdk/@launchdarkly/react-native-ld-session-replay/ios/SessionReplayClientAdapter.swift @@ -11,6 +11,14 @@ public class SessionReplayClientAdapter: NSObject { private let lock = NSLock() private var mobileKey: String? private var sessionReplayOptions: SessionReplayOptions? + // Optional session id forwarded from the JS observability SDK so the native + // observability instance (which emits e.g. `click` spans) reports the same + // `session.id`. nil means the native SDK uses its own generated session. + private var customSessionId: String? + // Optional `service.version` forwarded from JS. Applied to the observability + // plugin only (the session replay options have no version). nil keeps the + // SDK default. + private var serviceVersion: String? // Each start()/stop() appends a new Task that awaits the previous one, serializing all work. private var lastTask: Task = Task {} @@ -32,6 +40,18 @@ public class SessionReplayClientAdapter: NSObject { } self.mobileKey = key self.sessionReplayOptions = sessionReplayOptionsFrom(dictionary: options) + if let sessionId = (options?["sessionId"] as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines), !sessionId.isEmpty { + self.customSessionId = sessionId + } else { + self.customSessionId = nil + } + if let serviceVersion = (options?["serviceVersion"] as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines), !serviceVersion.isEmpty { + self.serviceVersion = serviceVersion + } else { + self.serviceVersion = nil + } } private func makeConfig(mobileKey: String, options: SessionReplayOptions) -> LDConfig { @@ -39,15 +59,24 @@ public class SessionReplayClientAdapter: NSObject { mobileKey: mobileKey, autoEnvAttributes: .enabled ) + var observabilityOptions = ObservabilityOptions( + // Disable the plugin's auto-start so we can start observability ourselves + // (see start()) and inject the JS session id. The native `startSession` is + // guarded by `task == nil`, so a session id can only be supplied on the + // first start — which the auto-start would consume. + isEnabled: false, + serviceName: options.serviceName, + sessionBackgroundTimeout: 10, + /// Disable the underlying KSCrash-based crash reporter that + crashReporting: .init(source: .none) + ) + // The session replay options carry no version, so apply the forwarded + // `service.version` to the observability plugin only. + if let serviceVersion = self.serviceVersion { + observabilityOptions.serviceVersion = serviceVersion + } config.plugins = [ - Observability( - options: .init( - serviceName: options.serviceName, - sessionBackgroundTimeout: 10, - /// Disable the underlying KSCrash-based crash reporter that - crashReporting: .init(source: .none) - ) - ), + Observability(options: observabilityOptions), SessionReplay(options: options) ] /// we set the LDClient offline to stop communication with the LaunchDarkly servers. @@ -93,6 +122,7 @@ public class SessionReplayClientAdapter: NSObject { completion(false, "Client not initialized. Call SetMobileKey first.") return } + let customSessionId = self.customSessionId let prev = lastTask lastTask = Task { @MainActor [weak self] in await prev.value @@ -105,6 +135,16 @@ public class SessionReplayClientAdapter: NSObject { cont.resume() } } + // The Observability plugin is configured with isEnabled=false (see + // makeConfig), so it did not auto-start. Start it now: when a session id + // was forwarded from the JS observability SDK, adopt it so native spans + // (e.g. `click`) share the same `session.id`; otherwise start with a + // generated session. + if let customSessionId { + LDObserve.shared.start(sessionId: customSessionId) + } else { + LDObserve.shared.start() + } self.initialized = true } else { NSLog( diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/src/NativeSessionReplayReactNative.ts b/sdk/@launchdarkly/react-native-ld-session-replay/src/NativeSessionReplayReactNative.ts index 517288f362..8a50501334 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/src/NativeSessionReplayReactNative.ts +++ b/sdk/@launchdarkly/react-native-ld-session-replay/src/NativeSessionReplayReactNative.ts @@ -2,7 +2,19 @@ import { TurboModuleRegistry, type TurboModule } from 'react-native'; export type SessionReplayOptions = { isEnabled?: boolean; + /** + * The OpenTelemetry `service.name` reported by the native session replay / + * observability instance. Applied to both the observability and session replay + * plugins on iOS and Android. + */ serviceName?: string; + /** + * The OpenTelemetry `service.version` reported by the native observability + * instance. Applied to the observability plugin on iOS and Android. The native + * session replay plugin does not expose a version, so this only affects + * observability-emitted signals. + */ + serviceVersion?: string; maskTextInputs?: boolean; maskWebViews?: boolean; maskLabels?: boolean; @@ -24,6 +36,18 @@ export type SessionReplayOptions = { * Defaults to `0.02`. Has no effect on Android. */ minimumAlpha?: number; + + /** + * Session id to adopt for the native session replay / observability instance, + * so its spans (e.g. `click`) share the same `session.id` as the JS + * observability SDK. When provided, the native side seeds its observability + * session with this id so both pipelines report a single session. + * + * Supported on both platforms: iOS starts observability with + * `start(sessionId:)`, and Android forwards it as the `Observability` plugin's + * `customSessionId` (seeding its session manager). + */ + sessionId?: string; }; export interface Spec extends TurboModule { diff --git a/sdk/@launchdarkly/react-native-ld-session-replay/src/index.tsx b/sdk/@launchdarkly/react-native-ld-session-replay/src/index.tsx index bda4fdfa7b..a92eeade46 100644 --- a/sdk/@launchdarkly/react-native-ld-session-replay/src/index.tsx +++ b/sdk/@launchdarkly/react-native-ld-session-replay/src/index.tsx @@ -2,9 +2,10 @@ import { View, type StyleProp, type ViewStyle } from 'react-native'; import type { ReactNode } from 'react'; import SessionReplayReactNative from './NativeSessionReplayReactNative'; import type { SessionReplayOptions } from './NativeSessionReplayReactNative'; -import type { - LDPlugin, - LDClientMin, +import { + LDObserve, + type LDPlugin, + type LDClientMin, } from '@launchdarkly/observability-react-native'; import type { LDContext, @@ -201,7 +202,16 @@ class SessionReplayPluginAdapter implements LDPlugin { console.error('[SessionReplay]', MOBILE_KEY_REQUIRED_MESSAGE); return; } - configureSessionReplay(key, this.options) + // Adopt the JS observability session id so native spans (e.g. `click`) share + // the same `session.id`. This requires the observability plugin to have been + // registered before this one; if it hasn't initialized yet the id is empty + // and native keeps its own session. + const sessionId = this.resolveObservabilitySessionId(); + const options: SessionReplayOptions = sessionId + ? { ...this.options, sessionId } + : this.options; + + configureSessionReplay(key, options) .then(() => { return startSessionReplay(); }) @@ -212,6 +222,15 @@ class SessionReplayPluginAdapter implements LDPlugin { ); }); } + + private resolveObservabilitySessionId(): string | undefined { + try { + const id = LDObserve.getSessionInfo()?.sessionId; + return typeof id === 'string' && id.length > 0 ? id : undefined; + } catch { + return undefined; + } + } } export function createSessionReplayPlugin( diff --git a/yarn.lock b/yarn.lock index a77025f3d9..b621e408bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42783,8 +42783,10 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" + "@launchdarkly/observability-react-native": "workspace:*" "@launchdarkly/react-native-client-sdk": "npm:^10.12.5" "@launchdarkly/session-replay-react-native": "workspace:*" + "@opentelemetry/api": "npm:^1.9.0" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0"