React Native アプリ

React Native アプリは、Android および iOS デバイスのユーザーがデモのサービスと対話するためのモバイル UI を提供します。 Expo で構築されており、Expo のファイルベースのルーティングを使用してアプリの画面をレイアウトしています。

React Native アプリのソース

計装

アプリケーションは OpenTelemetry パッケージを使用して、JS レイヤーで計装を行っています。

アプリケーションのメインエントリーポイントは app/_layout.tsx で、フックを使用して計装を初期化し、UI を表示する前にロードが完了していることを確認しています。

import { useTracer } from '@/hooks/useTracer';

const { loaded: tracerLoaded } = useTracer();

hooks/useTracer.ts には、TracerProvider の初期化、OTLP エクスポートの確立、トレースコンテキスト伝搬の登録、ネットワークリクエストの自動計装の登録など、計装のセットアップに必要なすべてのコードが含まれています。

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 { resourceFromAttributes } 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 = resourceFromAttributes({
    [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: [
      // ここでは少し注意が必要で、propagateTraceHeaderCorsUrls はモバイルアプリのコンテキストで計装を
      // 動作させるために必要です(実際には CORS リクエストを行っていないにもかかわらず)。
      // `clearTimingResources` はウェブ専用の Performance API の使用を避けるためにオフにする必要があります。
      new FetchInstrumentation({
        propagateTraceHeaderCorsUrls: /.*/,
        clearTimingResources: false,
      }),

      // React Native の fetch 実装は単に XMLHttpRequest 上のポリフィルです:
      // https://github.com/facebook/react-native/blob/7ccc5934d0f341f9bc8157f18913a7b340f5db2d/packages/react-native/Libraries/Network/fetch.js#L17
      // このため、`fetch` を使用してリクエストを行うと、基盤となる
      // XMLHttpRequest で行われるリクエストに対して追加のスパンが作成されます。
      // このデモでは /api/ へのリクエストは fetch を使って行われるため、
      // 余分なスパンを避けるためにそのパスの計装をオフにしています。
      new XMLHttpRequestInstrumentation({
        ignoreUrls: [/\/api\/.*/],
      }),
    ],
  });
};

export interface TracerResult {
  loaded: boolean;
}

export const useTracer = (): TracerResult => {
  const [loaded, setLoaded] = useState<boolean>(false);

  useEffect(() => {
    if (!loaded) {
      Tracer()
        .catch(() => console.warn('failed to setup tracer'))
        .finally(() => setLoaded(true));
    }
  }, [loaded]);

  return {
    loaded,
  };
};