예제

앱에 Apple SDK를 붙인 완성된 예제 — 최소 구성, 자체 익스포터, 캡처 서비스 조정, 화면과 스팬, SwiftUI, 크래시 맥락.

아래 예제는 모두 그대로 복사해 동작합니다. 각 설정이 무엇을 뜻하는지는 설정에서 다루므로, 여기서는 완성된 형태만 보입니다.

최소 구성

AppDelegate.swift
import UIKit
import SophonzIO
 
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    do {
      try Sophonz.start(options: Sophonz.Options.fromPlist() ?? .withCollector(
        url: "https://in.sophonz.ai",
        appKey: "sk_…"
      ))
    } catch {
      // 계측이 없는 것보다 앱이 뜨지 않는 것이 나쁩니다.
      print("[sophonz] 시작 실패: \(error)")
    }
    return true
  }
}

이것만으로 네트워크 요청, 화면 전환, 탭, WebView 로드, 메모리 경고, 저전력 모드, 크래시가 수집됩니다.

자체 익스포터로 보내기

Sophonz 수집기 전송이 연결되기 전까지 데이터를 실제로 받아 보는 방법입니다. appId 없이 시작하고 익스포터를 직접 지정합니다.

AppDelegate.swift
import SophonzIO
import OpenTelemetrySdk
import OpenTelemetryProtocolExporterHttp
 
func startSophonz() {
  let endpoint = URL(string: "https://in.sophonz.ai/v1/traces")!
  let spanExporter = OtlpHttpTraceExporter(endpoint: endpoint)
 
  let otel = Sophonz.OTelOptions(
    resource: Resource(attributes: [
      "deployment.environment.name": .string("production")
    ]),
    spanExporters: [spanExporter]
  )
 
  do {
    try Sophonz.start(options: .withLocalConfiguration(otel: otel))
  } catch {
    print("[sophonz] 시작 실패: \(error)")
  }
}

service.name, service.version, telemetry.sdk.language는 SDK가 항상 설정하므로 resource에 넣어도 덮어쓰이지 않습니다.

캡처 서비스 조정

행 감지를 켜고, 내부 화면 하나를 화면 계측에서 빼고, 헬스체크 요청을 무시하는 구성입니다.

import SophonzIO
 
let services = Sophonz.CaptureServicesOptionsBuilder()
  .addDefaults()
  .addHangCaptureService()
  .addUrlSessionCaptureService(
    withOptions: URLSessionCaptureService.Options(
      ignoredURLs: ["/health", "/ready"],
      traceparent: .init(onlyAllowDomains: ["api.example.com"])
    )
  )
  .addViewCaptureService(
    withOptions: ViewCaptureService.Options(
      viewControllerBlockList: ViewControllerBlockList(names: ["DebugMenu"])
    )
  )
  .build()
 
try Sophonz.start(options: .withCollector(
  url: "https://in.sophonz.ai",
  appKey: "sk_…",
  captureServices: services
))

addDefaults()를 먼저 부르는 것이 중요합니다. 빌더는 비어 있는 상태에서 시작하므로, 이것을 빠뜨리면 여기서 추가한 셋만 설치됩니다.

onlyAllowDomains로 traceparent를 붙일 대상을 좁혔습니다. 통제 범위 밖의 서드파티 API로 내부 트레이스 ID가 나가지 않게 하는 편이 안전합니다.

사용자와 세션 속성

로그인 시점에 식별자와 분류를 붙입니다.

func onSignIn(user: User) {
  Sophonz.shared.userIdentifier = user.internalId
  Sophonz.shared.addPersona("subscriber", lifespan: .permanent)
  Sophonz.shared.setProperty(
    key: "plan",
    value: user.plan,
    lifespan: .session
  )
}
 
func onSignOut() {
  Sophonz.shared.userIdentifier = nil
  Sophonz.shared.removeAllPersonas(lifespans: [.permanent, .session])
  Sophonz.shared.endUserSession()
}

직접 만드는 스팬

자동 계측이 보지 못하는 구간을 스팬으로 남깁니다.

func checkout(cart: Cart) async throws -> Order {
  let span = Sophonz.shared.createSpan(
    name: "checkout",
    type: .performance,
    attributes: ["cart.items": String(cart.items.count)]
  )
 
  do {
    let order = try await api.submit(cart)
    span?.end()
    return order
  } catch {
    Sophonz.shared.log(
      "결제 실패",
      severity: .error,
      attributes: ["cart.items": String(cart.items.count)]
    )
    span?.end(errorCode: .failure)
    throw error
  }
}

앱 시작 트레이스 보강

앱이 실제로 쓸 수 있게 된 시점까지를 시작 트레이스에 남깁니다.

func warmUpCaches() async {
  let start = Date()
  await cache.preload()
 
  Sophonz.shared.createStartupChildSpan(
    name: "cache-preload",
    startTime: start,
    endTime: Date()
  )
  Sophonz.shared.addAttributesToStartupTrace(["cache.warm": "true"])
}

SwiftUI 화면

화면 계측은 UIHostingController를 기본으로 제외합니다. SwiftUI 화면은 뷰 수정자로 표시합니다.

import SwiftUI
import SophonzIO
 
struct OrderListView: View {
  @State private var orders: [Order] = []
 
  var body: some View {
    List(orders) { OrderRow(order: $0) }
      .task { orders = await loadOrders() }
      // orders가 채워지는 시점까지를 로딩 구간으로 봅니다.
      .sophonzTrace("OrderListView", contentComplete: orders.count)
  }
}

크래시에 맥락 남기기

크래시 보고에 함께 실릴 값을 미리 남겨 둡니다. 크래시가 난 뒤에는 아무것도 실행되지 않으므로, 상태가 바뀔 때마다 갱신해 두는 방식입니다.

func didOpenScreen(_ name: String) {
  Sophonz.shared.appendCrashInfo(key: "last.screen", value: name)
  Sophonz.shared.addBreadcrumb("화면 열림: \(name)")
}

앱이 시작될 때 직전 실행이 어떻게 끝났는지 확인할 수 있습니다.

if Sophonz.shared.lastRunEndState() == .crash {
  // 복구 화면을 보여주거나, 저장하지 못한 작업을 되살립니다.
}

푸시 알림 직접 기록

자동 수집을 켜지 않았거나 처리 지점을 직접 통제하고 싶을 때 씁니다.

extension AppDelegate: UNUserNotificationCenterDelegate {
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {
    Sophonz.shared.addPushNotificationEvent(
      notification: response.notification,
      captureData: false
    )
    completionHandler()
  }
}

captureData를 false로 두면 제목과 본문을 담지 않습니다. 알림 내용에 개인정보가 들어갈 수 있다면 이렇게 두세요.

확인

시뮬레이터에서 앱을 띄우고 화면을 몇 번 이동한 뒤, 지정한 익스포터의 목적지에서 스팬을 찾습니다. Sophonz.shared.state가 .started인지, isSDKEnabled가 true인지 먼저 확인하세요.

포털의 앱 화면만 비어 있고 수집기는 데이터를 받고 있다면, AppName이 포털에 등록한 이름과 같은지 확인하세요. 포털은 (프로젝트, service.name)으로 앱을 찾습니다.

레거시 withAppId 모드로 시작했다면 기본 엔드포인트가 .invalid 도메인이라 아무 데도 도달하지 않습니다. 앱 키로 시작하세요 — 설정을 참고하세요.

포그라운드에 떠 있는 동안에는 업로드가 일어나지 않습니다. 앱을 백그라운드로 보내면 그때까지 쌓인 것이 올라갑니다.