For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
Sign inTry it free
DocsGuidesSDKsIntegrationsAPI docsTutorialsFlagship blog
DocsGuidesSDKsIntegrationsAPI docsTutorialsFlagship blog
  • Tutorials
    • The AI Iteration Loop for Deploying Reliable Agents with LangGraph
    • Using LaunchDarkly feature flags and Experimentation with Wordpress
    • Migrate a Hardcoded LangGraph Agent to LaunchDarkly AgentControl in 20 Minutes
    • Offline Evaluation of RAG-Grounded Answers in AgentControl
    • Beyond n8n for Workflow Automation: Agent Graphs as Your Universal Agent Harness
    • Catch your first silent AI failure with Vega AI in under 10 minutes
    • Evaluate LLM code generation with LLM-as-judge evaluators
    • OpenTelemetry for LLM Applications: A Practical Guide with LaunchDarkly and Langfuse
    • Use LaunchDarkly Agent Skills in Claude Code and Cursor
    • Detection to Resolution: Real World Debugging with Rage Clicks and Session Replay
    • Compare AI orchestrators: LangGraph vs Strands vs OpenAI Swarm
    • Building a data extraction pipeline with LaunchDarkly
    • Day 12 | 🎊 New Year, New Observability
    • Day 11 | ✉️ Letters to Santa: What engineering teams really want from Observability in 2026
    • Day 10 | Why observability and feature flags go together like milk and cookies
    • Day 9 | 👻 The Three Ghosts Haunting Your AI This Holiday Season
    • Day 7 | 🎄✨The Rockefeller tree in NYC: SLOs that actually drive decisions
    • Day 6 | 💸 The famous green character that stole your cloud budget: the cardinality problem
    • Day 5 | 🧹 Using a Popular Tidying Method to Consolidate Your Observability Stack
    • Day 4 | ❄️ Tracing the impact of holiday styling in your Node.js app
    • Day 8 | 🎁 Observable Multi-Modal Agentic Systems
    • Day 3 | 🔔 Jingle All the Way to Zero-Config Observability
    • Day 2 | 🎅 He knows if you have been bad or good... But what if he gets it wrong?
    • Collecting user feedback in your app with feature flags
    • Day 1 | 🎄 Observability Under the Tree: What Changed in 2025
    • Build a User Frustration Detection & Response System
    • When to Add Online Evals to Your AgentControl
    • Detecting User Frustration: Understanding Rage Clicks and Session Replay
    • AgentControl config CI/CD Pipeline: Automated Quality Gates and Safe Deployment
    • A Deeper Look at LaunchDarkly Architecture: More than Feature Flags
    • Add Observability to Your React Native App in 5 minutes
    • Smart AI Agent Targeting with MCP Tools
    • Build a LangGraph Multi-Agent System in 20 Minutes with LaunchDarkly AgentControl
    • Snowflake Cortex Completion API + LaunchDarkly SDK Integration
    • Using AgentControl to review database changes
    • How to implement WebSockets and kill switches in a Python application
    • 4 hacks to turbocharge your Cursor productivity
    • Create a feature flag in your IDE in 5 minutes with LaunchDarkly's MCP server
    • Observability for Your Go ORM: OpenTelemetry Integration with GORM
    • The complete guide to OpenTelemetry in Next.js
    • How to instrument your React Native app with OpenTelemetry
    • The complete guide to OpenTelemetry in Python
    • Monitoring Browser Applications with OpenTelemetry
    • How to Use OpenTelemetry to Monitor Next.js Applications
    • What is OpenTelemetry and Why Should I Care?
    • Distributed Tracing in Next.js Apps
    • Tracing Distributed Systems in Next.js
    • Real-time Monitoring in Django: Essential Tools and Techniques
    • DeepSeek vs Qwen: local model showdown featuring LaunchDarkly AgentControl
    • Application Tracing in .NET for Performance Monitoring
    • The Ultimate Guide to Ruby Logging: Best Libraries and Practices
    • Using Materialized Views in ClickHouse (vs. Postgres)
    • Filtering and Sampling LaunchDarkly Ingest
    • How to Set Up Your Production AWS MSK Kafka Cluster
    • Publishing an NPM Package with Private pnpm Monorepo Dependencies
    • How To Use The Chrome Inspector & Debugger
    • 3 Levels of Data Validation in a Full Stack Application With React
    • The power of the monorepo: Keep your fullstack app in sync!
    • Compression: The simple, powerful upgrade for your web stack
    • Video tutorials
Sign inTry it free
LogoLogo
On this page
  • Provider
  • Exporter
  • Processor
  • Instrumenting your application
  • Tracing
  • Logging
  • Errors
  • Autoinstrumentation of console functions
  • Conclusion
Tutorials

How to instrument your React Native app with OpenTelemetry

Was this page helpful?
Previous

The complete guide to OpenTelemetry in Python

Next
Built with

Published January 22, 2025

portrait of Spencer Amarantides.

by Spencer Amarantides

1LaunchDarkly is an [open source](https://github.com/highlight/highlight) monitoring platform. If you're interested in learning more, get started at [LaunchDarkly](https://launchdarkly.com). Check out the React Native [example app](https://github.com/highlight/highlight/tree/main/e2e/react-native) and [LaunchDarkly code snippets](https://github.com/highlight/highlight/blob/main/e2e/react-native/app/highlight.ts) to follow along.

OpenTelemetry is an open-source observability framework that provides tools, APIs, and SDKs to collect, process, and export telemetry data like traces, metrics, and logs from applications. It is designed to help developers monitor and troubleshoot distributed systems by providing standardized data formats and integration points for observability tools. If you’re new to OpenTelemetry, you can learn more about it here.

Today, we’ll go through a guide to using OpenTelemetry in React Native, including the high-level concepts as well as how to send traces, errors, and logs to your OpenTelemetry backend of choice.

Provider

A provider is the API entry point that holds the configuration for telemetry data. The provider is responsible for setting up the environment and ensuring that all necessary configurations are in place. This can include configuring a vendor specific api key, or something as simple as setting the service name and environment. In our case, we will be using a TraceProvider to send traces that will be processed by the LaunchDarkly backend and converted to logs, errors, and traces.

In our example, the TracerProvider creates a resource, that builds attributes that we want to include with every trace. This includes the highlight.project_id to let LaunchDarkly know which project the traces below to, and other identifying data, such as service name and environment to help with monitoring and debugging.

Here’s a quick example of what this looks like in code:

1import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'
2import { Resource } from '@opentelemetry/resources'
3
4const resource = new Resource({
5 'highlight.project_id': '<YOUR_PROJECT_ID>',
6 'service.name': 'reactnativeapp',
7 'environment': 'production',
8})
9
10const tracerProvider = new BasicTracerProvider({ resource })

Exporter

An exporter sends the telemetry data to the backend. This is where you configure the endpoint and any other necessary settings related to the backend you’re sending data to. In our example, we built a custom exporter as a workaround to some OpenTelemetry package issues with the React Native’s bundler, Metro. A bundler-based solution is also in progress to use OpenTelemetry’s OTLPTraceExporter, and substitute out our custom exporter.

Here’s an example of how you might build a custom React Native exporter class that serializes and sends traces over http. Notice the majority of logic is serializing the batched spans:

1import {
2 SpanExporter,
3 ReadableSpan,
4 TimedEvent,
5} from '@opentelemetry/sdk-trace-base'
6import type { Link, Attributes } from '@opentelemetry/api'
7import { ExportResultCode } from '@opentelemetry/core'
8
9type KeyValue = {
10 key: string
11 value: KeyValue
12}
13
14class ReactNativeOTLPTraceExporter implements SpanExporter {
15 url: string
16
17 constructor(options: { url: string }) {
18 this.url = options.url
19
20 this._buildResourceSpans = this._buildResourceSpans.bind(this)
21 this._convertEvent = this._convertEvent.bind(this)
22 this._convertToOTLPFormat = this._convertToOTLPFormat.bind(this)
23 this._convertLink = this._convertLink.bind(this)
24 this._convertAttributes = this._convertAttributes.bind(this)
25 this._convertKeyValue = this._convertKeyValue.bind(this)
26 this._toAnyValue = this._toAnyValue.bind(this)
27 }
28
29 export(spans: ReadableSpan[], resultCallback: any) {
30 fetch(this.url, {
31 method: 'POST',
32 headers: { 'Content-Type': 'application/json' },
33 body: this._buildResourceSpans(spans),
34 })
35 .then(() => {
36 resultCallback({ code: ExportResultCode.SUCCESS })
37 })
38 .catch((err) => {
39 resultCallback({ code: ExportResultCode.FAILED, error: err })
40 })
41 }
42
43 shutdown() {
44 return Promise.resolve()
45 }
46
47 _buildResourceSpans(spans: ReadableSpan[] = []) {
48 const resource = spans[0]?.resource
49 const scope = spans[0]?.instrumentationLibrary
50
51 return JSON.stringify({
52 resourceSpans: [
53 {
54 resource: {
55 attributes: resource.attributes
56 ? this._convertAttributes(resource.attributes)
57 : [],
58 },
59 scopeSpans: [
60 {
61 scope: {
62 name: scope?.name,
63 version: scope?.version,
64 },
65 spans: spans.map(this._convertToOTLPFormat),
66 },
67 ],
68 },
69 ],
70 })
71 }
72
73 _convertToOTLPFormat(span: ReadableSpan) {
74 const spanContext = span.spanContext()
75 const status = span.status
76
77 return {
78 traceId: spanContext.traceId,
79 spanId: spanContext.spanId,
80 parentSpanId: span.parentSpanId,
81 traceState: spanContext.traceState?.serialize(),
82 name: span.name,
83 // Span kind is offset by 1 because the API does not define a value for unset
84 kind: span.kind == null ? 0 : span.kind + 1,
85 startTimeUnixNano: span.startTime[0] * 1e9 + span.startTime[1],
86 endTimeUnixNano: span.endTime[0] * 1e9 + span.endTime[1],
87 attributes: span.attributes
88 ? this._convertAttributes(span.attributes)
89 : [],
90 droppedAttributesCount: span.droppedAttributesCount || 0,
91 events: span.events?.map(this._convertEvent) || [],
92 droppedEventsCount: span.droppedEventsCount || 0,
93 status: {
94 code: status.code,
95 message: status.message,
96 },
97 links: span.links?.map(this._convertLink) || [],
98 droppedLinksCount: span.droppedLinksCount,
99 }
100 }
101
102 _convertEvent(timedEvent: TimedEvent) {
103 return {
104 attributes: timedEvent.attributes
105 ? this._convertAttributes(timedEvent.attributes)
106 : [],
107 name: timedEvent.name,
108 timeUnixNano: timedEvent.time[0] * 1e9 + timedEvent.time[1],
109 droppedAttributesCount: timedEvent.droppedAttributesCount || 0,
110 }
111 }
112
113 _convertLink(link: Link) {
114 return {
115 attributes: link.attributes
116 ? this._convertAttributes(link.attributes)
117 : [],
118 spanId: link.context.spanId,
119 traceId: link.context.traceId,
120 traceState: link.context.traceState?.serialize(),
121 droppedAttributesCount: link.droppedAttributesCount || 0,
122 }
123 }
124
125 _convertAttributes(attributes: Attributes) {
126 return Object.keys(attributes).map((key) =>
127 this._convertKeyValue(key, attributes[key]),
128 )
129 }
130
131 _convertKeyValue(key: string, value: any): KeyValue {
132 return {
133 key: key,
134 value: this._toAnyValue(value),
135 }
136 }
137
138 _toAnyValue(value: any): any {
139 const t = typeof value
140 if (t === 'string') return { stringValue: value as string }
141 if (t === 'number') {
142 if (!Number.isInteger(value))
143 return { doubleValue: value as number }
144 return { intValue: value as number }
145 }
146 if (t === 'boolean') return { boolValue: value as boolean }
147 if (value instanceof Uint8Array) return { bytesValue: value }
148 if (Array.isArray(value))
149 return { arrayValue: { values: value.map(this._toAnyValue) } }
150 if (t === 'object' && value != null)
151 return {
152 kvlistValue: {
153 values: Object.entries(value as object).map(([k, v]) =>
154 this._convertKeyValue(k, v),
155 ),
156 },
157 }
158
159 return {}
160 }
161}

After we build our exporter, we need to create an instance with the correct url to send the traces. This is done in the example below:

1const otlpExporter = new ReactNativeOTLPTraceExporter({
2 url: 'https://otel.highlight.io:4318/v1/traces',
3})

Processor

Finally, a processor defines any pre-processing that should be done on the created traces, such as batching, sampling, filtering or even enriching data. This is important because you may have specific needs on the machine that you’re sending data from that require customization. In our example, we will use a BatchSpanProcessor to collect spans in batches and send them to the exporter, which is more efficient than sending each span individually.

Here’s how we initialized the BatchSpanProcessor, and registered the traceProvider:

1import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
2
3tracerProvider.addSpanProcessor(new BatchSpanProcessor(otlpExporter))
4tracerProvider.register()
5
6export const tracer = tracerProvider.getTracer('react-native-tracer')

Instrumenting your application

After we created our tracer, we can now use it to send LaunchDarkly traces, logs, and errors. We can also monkeypatch javascript’s console methods, so they will send to LaunchDarkly by default.

Tracing

Tracing is possible by calling the created tracer’s startSpan method. This accepts a parameter for the name of the span, and returns the span itself. From there, the span can record an error, add attributes, and much more. We will signal the end of a span by calling the end method.

Here is an example, assuming the tracer is exportable from a file called highlight.ts:

1import { tracer } from "./highlight"
2
3const span = H.tracer.startSpan('Blog Post')
4// ...some code
5span.setAttributes({ name: "How to instrument your React Native app with OpenTelemetry" })
6span.recordException(
7 new Error('this is an error in the Blog Post span'),
8)
9span.end()

Logging

Logging can be sent to LaunchDarkly with a few configurations to the trace method. First, the span name should be highlight.log to let the LaunchDarkly backend know it is, in fact, a log. Second, we will pass in a log.severity and a log.message attribute to be used when constructing the log object. It is recommended you set up a log function to complete this.

Here is an example of a log function below:

1const ConsoleLevels = {
2 debug: 'debug',
3 info: 'info',
4 log: 'info',
5 count: 'info',
6 dir: 'info',
7 warn: 'warn',
8 assert: 'warn',
9 error: 'error',
10 trace: 'trace',
11} as const
12
13export const log = (
14 level: keyof typeof ConsoleLevels,
15 message: string,
16 attributes = {},
17) => {
18 const span = tracer.startSpan('highlight.log')
19 span.addEvent(
20 'log',
21 {
22 ...attributes,
23 ['log.severity']: level,
24 ['log.message']: message,
25 },
26 new Date(),
27 )
28 span.end()
29}

The benefit of using this log function is being able to pass in attributes more cleanly to be searched across in LaunchDarkly. However, this will only send to LaunchDarkly and will not be recorded in the dev tools. We will set up the monkeypatch to record console logs later. After you have created your function, you can export this function to be called in your application code.

1import { log } from "./highlight"
2
3log("warn", "we are almost finished", { minutesRead: 10 })

Errors

In our solution, errors are also sent via traces to LaunchDarkly with some configuration details. Again, we will call the trace highlight.log to ensure this trace will create a log for you in LaunchDarkly. Second, we will record an exception and add any attributes to the span.

Here is an example of a error function below:

1export const error = (message: string, attributes = {}) => {
2 const span = tracer.startSpan('highlight.log')
3 span.recordException(new Error(message), new Date())
4 span.setAttributes(attributes)
5 span.end()
6}

Again, the benefit of using this error function is flexibility of passing in a custom error name as well as attributes associated with the error. Monkeypatching the console method will send any error logs to LaunchDarkly which will then be processed as an error. After you have created your function, you can export this function to be called in your application code.

1import { error } from "./highlight"
2
3error("user unset", { defaultAvatar: "batman" })

Autoinstrumentation of console functions

The functions above are great for flexibility and customization, but maybe all you want is to report what is happening in the console to LaunchDarkly. We have you covered. We created a hook to call in your code that will monkeypatch the console methods to send to LaunchDarkly in addition to printing in the console dev tools. This should be called on app load or early on in the lifecycle of the session.

Here is an example of how to monkeypatch the console methods:

1type ConsoleFn = (...data: any) => void
2
3let consoleHooked = false
4
5export function hookConsole() {
6 if (consoleHooked) return
7 consoleHooked = true
8 for (const [level, highlightLevel] of Object.entries(ConsoleLevels)) {
9 const origWrite = console[level as keyof Console] as ConsoleFn
10 ;(console[level as keyof Console] as ConsoleFn) = function (
11 ...data: any[]
12 ) {
13 try {
14 return origWrite(...data)
15 } finally {
16 const o: { stack: any } = { stack: {} }
17 Error.captureStackTrace(o)
18 const message = data.map((o) =>
19 typeof o === 'object' ? safeStringify(o) : o,
20 )
21
22 const attributes = data
23 .filter((d) => typeof d === 'object')
24 .reduce((a, b) => ({ ...a, ...b }), {})
25
26 if (level === 'error') {
27 attributes['exception.type'] = 'Error'
28 attributes['exception.message'] = message.join('')
29 attributes['exception.stacktrace'] = JSON.stringify(o.stack)
30 }
31
32 log(highlightLevel, message.join(' '), attributes)
33 }
34 }
35 }
36}
37
38// https://stackoverflow.com/a/2805230
39const MAX_RECURSION = 128
40
41export function safeStringify(obj: any): string {
42 function replacer(input: any, depth?: number): any {
43 if ((depth ?? 0) > MAX_RECURSION) {
44 throw new Error('max recursion exceeded')
45 }
46 if (input && typeof input === 'object') {
47 for (const k in input) {
48 if (typeof input[k] === 'object') {
49 replacer(input[k], (depth ?? 0) + 1)
50 } else if (!canStringify(input[k])) {
51 input[k] = input[k].toString()
52 }
53 }
54 }
55 return input
56 }
57
58 function canStringify(value: any): boolean {
59 try {
60 JSON.stringify(value)
61 return true
62 } catch (e) {
63 return false
64 }
65 }
66
67 try {
68 return JSON.stringify(replacer(obj))
69 } catch (e) {
70 return obj.toString()
71 }
72}

In this function, we overwrite the console function if it has not been overwritten yet. It will then process the data, determine if its an error, and call the log function from above. There is some additional logic to safely stringify any data without going to deep into recursion.

Finally, this can be called in your application to start recording console data to LaunchDarkly:

1import { hookConsole } from "./highlight"
2import { useEffect } from "react"
3
4// within component
5useEffect(() => {
6 hookConsole()
7}, [])
8
9console.log("I'm sending to Highlight")

Conclusion

In this guide, we’ve gone through everything you need to use OpenTelemetry in React Native to be able to send LaunchDarkly your logs, traces, and errors. OpenTelemetry is flexible, so this solution is not the only one. Feel free to edit resources, methods, and classes to what works best for your application.

Check out the example app and LaunchDarkly code snippets.

If you have any questions, please feel free to reach out to us on Twitter or Discord.