diff --git a/.cspell/en-words.txt b/.cspell/en-words.txt
index aa514245f43a..78a77f7d49b9 100644
--- a/.cspell/en-words.txt
+++ b/.cspell/en-words.txt
@@ -108,6 +108,7 @@ quantile
quantiles
quarkus
quoteservice
+react-native-app
recommendationservice
redis
relref
diff --git a/content/en/docs/demo/_index.md b/content/en/docs/demo/_index.md
index 5eaecbffd280..d21b43875c50 100644
--- a/content/en/docs/demo/_index.md
+++ b/content/en/docs/demo/_index.md
@@ -29,7 +29,8 @@ here.
| C++ | | | [Currency Service](services/currency/) |
| Go | | [Checkout Service](services/checkout/), [Product Catalog Service](services/product-catalog/) | [Checkout Service](services/checkout/), [Product Catalog Service](services/product-catalog/) |
| Java | [Ad Service](services/ad/) | | [Ad Service](services/ad/) |
-| JavaScript | | [Frontend](services/frontend/) | [Frontend](services/frontend/), [Payment Service](services/payment/) |
+| JavaScript | | | [Payment Service](services/payment/) |
+| TypeScript | | [Frontend](services/frontend/), [React Native App](services/react-native-app/) | [Frontend](services/frontend/) |
| Kotlin | | [Fraud Detection Service](services/fraud-detection/) | |
| PHP | | [Quote Service](services/quote/) | [Quote Service](services/quote/) |
| Python | [Recommendation Service](services/recommendation/) | | [Recommendation Service](services/recommendation/) |
@@ -54,6 +55,7 @@ found here:
- [Recommendation Service](services/recommendation/)
- [Shipping Service](services/shipping/)
- [Image Provider Service](services/imageprovider/)
+- [React Native App](services/react-native-app/)
## Scenarios
diff --git a/content/en/docs/demo/architecture.md b/content/en/docs/demo/architecture.md
index 245ff539c225..b3fd68c5cabe 100644
--- a/content/en/docs/demo/architecture.md
+++ b/content/en/docs/demo/architecture.md
@@ -32,6 +32,7 @@ quoteservice(Quote Service):::php
recommendationservice(Recommendation Service):::python
shippingservice(Shipping Service):::rust
queue[(queue
(Kafka))]:::java
+react-native-app(React Native App):::typescript
adservice ---->|gRPC| flagd
@@ -73,6 +74,8 @@ recommendationservice -->|gRPC| productcatalogservice
recommendationservice -->|gRPC| flagd
shippingservice -->|HTTP| quoteservice
+
+react-native-app -->|HTTP| frontendproxy
end
classDef dotnet fill:#178600,color:white;
diff --git a/content/en/docs/demo/services/_index.md b/content/en/docs/demo/services/_index.md
index b09a35f1c016..12ae06ed9b98 100644
--- a/content/en/docs/demo/services/_index.md
+++ b/content/en/docs/demo/services/_index.md
@@ -15,10 +15,11 @@ To visualize request flows, see the [Service Diagram](../architecture/).
| [currencyservice](currency/) | C++ | Converts one money amount to another currency. Uses real values fetched from European Central Bank. It's the highest QPS service. |
| [emailservice](email/) | Ruby | Sends users an order confirmation email (mock/). |
| [frauddetectionservice](fraud-detection/) | Kotlin | Analyzes incoming orders and detects fraud attempts (mock/). |
-| [frontend](frontend/) | JavaScript | Exposes an HTTP server to serve the website. Does not require sign up / login and generates session IDs for all users automatically. |
+| [frontend](frontend/) | TypeScript | Exposes an HTTP server to serve the website. Does not require sign up / login and generates session IDs for all users automatically. |
| [loadgenerator](load-generator/) | Python/Locust | Continuously sends requests imitating realistic user shopping flows to the frontend. |
| [paymentservice](payment/) | JavaScript | Charges the given credit card info (mock/) with the given amount and returns a transaction ID. |
| [productcatalogservice](product-catalog/) | Go | Provides the list of products from a JSON file and ability to search products and get individual products. |
| [quoteservice](quote/) | PHP | Calculates the shipping costs, based on the number of items to be shipped. |
| [recommendationservice](recommendation/) | Python | Recommends other products based on what's given in the cart. |
| [shippingservice](shipping/) | Rust | Gives shipping cost estimates based on the shopping cart. Ships items to the given address (mock/). |
+| [react-native-app](react-native-app/) | TypeScript | React Native mobile application that provides a UI on top of the shopping services. |
diff --git a/content/en/docs/demo/services/react-native-app.md b/content/en/docs/demo/services/react-native-app.md
new file mode 100644
index 000000000000..fe21d3ab1dd9
--- /dev/null
+++ b/content/en/docs/demo/services/react-native-app.md
@@ -0,0 +1,149 @@
+---
+title: React Native App
+cSpell:ignore: typeof
+---
+
+The React Native app provides a mobile UI for users on Android and iOS devices
+to interact with the demo's services. It is built with
+[Expo](https://docs.expo.dev/get-started/introduction/) and uses Expo's
+file-based routing to layout the screens for the app.
+
+[React Native app source](https://github.com/open-telemetry/opentelemetry-demo/blob/main/src/react-native-app/)
+
+## Instrumentation
+
+The application uses the OpenTelemetry packages to instrument the application at
+the JS layer
+
+{{% alert title="Important" color="warning" %}}
+
+The JS OTel packages are supported for node and web environments. While they
+work for React Native as well they are not currently explicitly supported for
+that environment and may break compatibility with minor version updates or
+require workarounds. Building more support for React Native is an area of active
+development.
+
+{{% /alert %}}
+
+The main entry point for the application is `app/_layout.tsx` where a hook is
+used to initialize the instrumentation and make sure it is loaded before
+displaying the UI:
+
+```typescript
+import { useTracer } from '@/hooks/useTracer';
+
+const { loaded: tracerLoaded } = useTracer();
+```
+
+`hooks/useTracer.ts` contains all the code for setting up instrumentation
+including initializing a TracerProvider, establishing an OTLP export,
+registering trace context propagators, and registering auto-instrumentation of
+network requests.
+
+```typescript
+import {
+ CompositePropagator,
+ W3CBaggagePropagator,
+ W3CTraceContextPropagator,
+} from '@opentelemetry/core';
+import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
+import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
+import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
+import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
+import { registerInstrumentations } from '@opentelemetry/instrumentation';
+import { Resource } from '@opentelemetry/resources';
+import {
+ ATTR_DEVICE_ID,
+ ATTR_OS_NAME,
+ ATTR_OS_VERSION,
+ ATTR_SERVICE_NAME,
+ ATTR_SERVICE_VERSION,
+} from '@opentelemetry/semantic-conventions/incubating';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import getLocalhost from '@/utils/Localhost';
+import { useEffect, useState } from 'react';
+import {
+ getDeviceId,
+ getSystemVersion,
+ getVersion,
+} from 'react-native-device-info';
+import { Platform } from 'react-native';
+import { SessionIdProcessor } from '@/utils/SessionIdProcessor';
+
+const Tracer = async () => {
+ const localhost = await getLocalhost();
+
+ const resource = new Resource({
+ [ATTR_SERVICE_NAME]: 'react-native-app',
+ [ATTR_OS_NAME]: Platform.OS,
+ [ATTR_OS_VERSION]: getSystemVersion(),
+ [ATTR_SERVICE_VERSION]: getVersion(),
+ [ATTR_DEVICE_ID]: getDeviceId(),
+ });
+
+ const provider = new WebTracerProvider({
+ resource,
+ spanProcessors: [
+ new BatchSpanProcessor(
+ new OTLPTraceExporter({
+ url: `http://${localhost}:${process.env.EXPO_PUBLIC_FRONTEND_PROXY_PORT}/otlp-http/v1/traces`,
+ }),
+ {
+ scheduledDelayMillis: 500,
+ },
+ ),
+ new SessionIdProcessor(),
+ ],
+ });
+
+ provider.register({
+ propagator: new CompositePropagator({
+ propagators: [
+ new W3CBaggagePropagator(),
+ new W3CTraceContextPropagator(),
+ ],
+ }),
+ });
+
+ registerInstrumentations({
+ instrumentations: [
+ // Some tiptoeing required here, propagateTraceHeaderCorsUrls is required to make the instrumentation
+ // work in the context of a mobile app even though we are not making CORS requests. `clearTimingResources` must
+ // be turned off to avoid using the web-only Performance API
+ new FetchInstrumentation({
+ propagateTraceHeaderCorsUrls: /.*/,
+ clearTimingResources: false,
+ }),
+
+ // The React Native implementation of fetch is simply a polyfill on top of XMLHttpRequest:
+ // https://github.com/facebook/react-native/blob/7ccc5934d0f341f9bc8157f18913a7b340f5db2d/packages/react-native/Libraries/Network/fetch.js#L17
+ // Because of this when making requests using `fetch` there will an additional span created for the underlying
+ // request made with XMLHttpRequest. Since in this demo calls to /api/ are made using fetch, turn off
+ // instrumentation for that path to avoid the extra spans.
+ new XMLHttpRequestInstrumentation({
+ ignoreUrls: [/\/api\/.*/],
+ }),
+ ],
+ });
+};
+
+export interface TracerResult {
+ loaded: boolean;
+}
+
+export const useTracer = (): TracerResult => {
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ if (!loaded) {
+ Tracer()
+ .catch(() => console.warn('failed to setup tracer'))
+ .finally(() => setLoaded(true));
+ }
+ }, [loaded]);
+
+ return {
+ loaded,
+ };
+};
+```