네트워크와 분산 추적

fetch와 XMLHttpRequest로 보낸 요청이 어떻게 수집되는지, traceparent 헤더가 Android와 iOS에서 백엔드 트레이스와 어떻게 연결되는지, JavaScript 스팬을 API로 전파하는 방법, SDK가 볼 수 없는 요청을 기록하는 방법을 설명합니다.

React Native에는 자체 네트워크 스택이 없습니다. fetch, XMLHttpRequest, 그리고 axios처럼 이를 기반으로 한 라이브러리는 Android에서는 OkHttp에, iOS에서는 URLSession에 요청을 넘기며, 네이티브 SDK는 둘 다 이미 계측합니다. JavaScript에서 감쌀 것은 없습니다. 설정할 것은 어떤 요청을 수집할지, 그중 어떤 요청에 백엔드용 traceparent 헤더를 붙일지입니다.

수집 대상

요청마다 perf.network_request 타입의 스팬이 하나 생기고, 이름은 GET /v1/orders처럼 METHOD /path 형태이며 URL, 메서드, 상태 코드, 본문 크기, 소요 시간이 담깁니다. HTTP 응답을 받지 못한 요청(DNS 실패, 타임아웃, 연결 거부)은 에러 타입과 메시지와 함께 기록됩니다.

AndroidiOS
계측하는 클라이언트Gradle 플러그인이 빌드 시점에 계측하는 OkHttpURLSession
함께 수집되는 것OkHttp나 HttpURLConnection을 쓰는 네이티브 라이브러리URLSession을 쓰는 네이티브 라이브러리
수집되지 않는 것WebSocket, 둘 다 쓰지 않는 네이티브 클라이언트WebSocket, 다른 방식을 쓰는 네이티브 클라이언트
SDK 자체 업로드자동 제외자동 제외

수집이 네이티브에서 이루어지므로, SDK를 네이티브에서 시작했다면 JavaScript가 아직 로드되는 중에 보낸 요청도 기록됩니다.

요청 제외

기록하고 싶지 않은 호스트는 제외하세요. 분석 비콘, 두 번째 OTLP 백엔드, 헬스 체크 같은 곳입니다.

플랫폼설정매칭 방식
Androidsophonz-config.json의 sdk_config.networking.disabled_url_patterns정규식
iOS, JS 시작sdkConfig.ios.disabledUrlPatternsURL의 부분 문자열
iOS, 네이티브 시작URLSessionCaptureService.Options(ignoredURLs:)URL의 부분 문자열
android/app/src/main/sophonz-config.json
{
  "sdk_config": {
    "networking": {
      "disabled_url_patterns": ["analytics\\.example\\.com", "otlp\\.example\\.com"]
    }
  }
}
await initialize({
  sdkConfig: {
    ios: {
      collectorUrl: "https://in.sophonz.ai",
      appKey: "sk_ios_replace_me",
      disabledUrlPatterns: ["analytics.example.com", "otlp.example.com"],
    },
  },
});

분산 추적

OpenTelemetry로 계측한 백엔드는 요청에 W3C traceparent 헤더가 있으면 트레이스를 이어 갑니다. 네이티브 SDK는 수집하는 요청에 요청 스팬 자신의 트레이스 ID와 스팬 ID로 이 헤더를 붙입니다. 그래서 백엔드 스팬이 앱 네트워크 스팬의 자식이 되고, 포털에서 탭부터 데이터베이스까지 한 요청을 따라갈 수 있습니다.

두 플랫폼의 기본값은 정반대입니다.

Android

헤더 주입은 기본으로 꺼져 있습니다. sophonz-config.json에서 켜고, 직접 관리하는 호스트로 제한하세요.

android/app/src/main/sophonz-config.json
{
  "sdk_config": {
    "networking": {
      "enable_traceparent_injection": true,
      "traceparent_only_allow_domains": ["api.example.com"],
      "enable_network_span_forwarding": true
    }
  }
}
키기본값효과
enable_traceparent_injectionfalse수집한 OkHttp 요청에 traceparent를 붙입니다
traceparent_only_allow_domains없음이 호스트에만 붙입니다
enable_network_span_forwardingfalserecordNetworkRequest로 기록한 요청을 포함해 요청 스팬에 traceparent를 spz.w3c_traceparent로도 남깁니다

Expo에서는 같은 networking 객체를 플러그인의 androidSDKConfig 속성에 넣으세요.

iOS

헤더 주입은 기본으로 모든 호스트에 켜져 있습니다. JavaScript에서는 끄는 스위치만 있습니다.

await initialize({
  sdkConfig: {
    ios: {
      collectorUrl: "https://in.sophonz.ai",
      appKey: "sk_ios_replace_me",
      disableNetworkSpanForwarding: true, // 어떤 요청에도 traceparent를 붙이지 않음
    },
  },
});

자체 호스트에만 보내려면 SDK를 네이티브에서 시작하고 허용 목록을 넘기세요.

ios/MyApp/SophonzInitializer.swift
import Foundation
import SophonzIO
 
@objcMembers class SophonzInitializer: NSObject {
  static func start() {
    let builder = CaptureServicesOptionsBuilder()
    builder.addUrlSessionCaptureService(
      withOptions: URLSessionCaptureService.Options(
        ignoredURLs: ["otlp.example.com"],
        traceparent: URLSessionCaptureService.Traceparent(onlyAllowDomains: ["api.example.com"])
      )
    )
    builder.addDefaults()
 
    do {
      try Sophonz.start(
        options: .withCollector(
          url: "https://in.sophonz.ai",
          appKey: "sk_ios_replace_me",
          platform: .reactNative,
          captureServices: builder.build()
        )
      )
    } catch {
      print("[sophonz] failed to start: \(error)")
    }
  }
}

도메인 항목은 호스트 이름만 적어야 하며 하위 도메인도 매칭됩니다. /나 공백이 있거나 점으로 시작하는 항목은 경고와 함께 버려지고, 빈 배열이면 어디에도 헤더를 보내지 않습니다. iOS 설정을 참고하세요.

CAUTION — 서드파티 API가 트레이스 ID를 보게 됩니다

허용 목록이 없으면 결제 대행사, CDN, 분석 서비스 등 앱이 호출하는 모든 호스트가 traceparent를 받습니다. 내부 트레이스 ID가 새어 나가고, 모르는 헤더를 거부하는 까다로운 API도 일부 있습니다. 두 플랫폼 모두 헤더를 자체 도메인으로 제한하세요.

이미 있는 헤더는 유지됩니다

요청에 traceparent 헤더가 이미 있으면 두 SDK 모두 바꾸지 않습니다. 이 점을 이용하면 네트워크 스팬 대신 JavaScript에서 만든 스팬을 백엔드 스팬의 부모로 삼을 수 있습니다.

import {trace} from "@opentelemetry/api";
import type {SophonzNativeSpan} from "@sophonz/react-native-tracer-provider";
 
const tracer = trace.getTracer("checkout");
 
export async function submitOrder(order: Order) {
  const span = tracer.startSpan("submit-order");
  try {
    // ID는 네이티브 쪽에서 정해지므로 기다립니다
    const {traceId, spanId} = await (span as SophonzNativeSpan).spanContextAsync();
 
    const response = await fetch("https://api.example.com/v1/orders", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        traceparent: `00-${traceId}-${spanId}-01`,
      },
      body: JSON.stringify(order),
    });
    span.setAttribute("http.response.status_code", response.status);
    return await response.json();
  } finally {
    span.end();
  }
}

trace.getTracer가 tracer provider에 닿으려면 provider가 전역으로 등록되어 있어야 합니다. OpenTelemetry와 OTLP를 참고하세요.

백엔드 쪽

백엔드는 헤더를 받아 트레이스를 이어 가야 합니다. Sophonz 서버 SDK는 기본으로 그렇게 동작합니다.

앱이 직접 보내는 요청에는 CORS가 적용되지 않습니다. React Native의 fetch는 브라우저가 아니므로 preflight도 보내지 않습니다. 반면 WebView에서 불러온 페이지는 브라우저 SDK 규칙을 따르며, API에 Access-Control-Allow-Headers: traceparent도 필요합니다.

수동으로 요청 기록

네이티브 모듈이 자체 HTTP 클라이언트로 보내는 요청이나 WebSocket으로 중계하는 요청처럼 SDK가 볼 수 없는 요청은 결과를 직접 기록하세요.

import {logNetworkClientError, recordNetworkRequest} from "@sophonz/react-native";
 
const start = Date.now();
try {
  const result = await nativePaymentsClient.charge(cart);
  await recordNetworkRequest(
    "https://payments.example.com/v2/charges",
    "POST",
    start,
    Date.now(),
    result.requestBytes,
    result.responseBytes,
    result.statusCode,
  );
} catch (e) {
  await logNetworkClientError(
    "https://payments.example.com/v2/charges",
    "POST",
    start,
    Date.now(),
    "ConnectionError",
    (e as Error).message,
  );
}

시간은 epoch 밀리초입니다. 크기나 상태 코드가 0 이하면 알 수 없는 값으로 보고 스팬에 넣지 않습니다. 메서드는 표준 HTTP 메서드여야 하며, Android에서는 그 밖의 값이면 거부되어 false로 resolve됩니다.

수동으로 기록한 스팬에는 iOS에서는 disableNetworkSpanForwarding을 켜지 않는 한, Android에서는 enable_network_span_forwarding을 켰을 때 spz.w3c_traceparent가 붙습니다. 요청은 이미 끝났으므로 헤더가 전송되지는 않습니다.

NOTE — 이미 수집된 요청을 다시 기록하지 마세요

fetch 요청에 recordNetworkRequest를 호출하면 같은 요청의 스팬이 두 개 생깁니다. OkHttp와 URLSession을 거치지 않는 트래픽에만 쓰세요.

확인

  • 세션을 열어 API 경로 이름을 가진 perf.network_request 스팬이 있는지 봅니다.
  • 추적은 백엔드에서 요청을 확인합니다. traceparent: 00-<32 hex>-<16 hex>-01이 있어야 합니다. Android에서 헤더가 없다면 대개 enable_traceparent_injection이 아직 false이고, iOS에서는 호스트가 onlyAllowDomains에 없거나 disableNetworkSpanForwarding이 켜져 있는 경우입니다.
  • 두 번째 OTLP 백엔드가 네트워크 스팬으로 잔뜩 보인다면 그 호스트가 제외되지 않은 것입니다.