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, + }; +}; +```