본문으로 건너뛰기

Android 에이전트 적용

WhaTap 모바일 에이전트를 Android 앱에 적용하면 화면 로딩, 네트워크 호출, 크래시, ANR, 리소스 사용량, WebView 페이지 성능을 수집할 수 있습니다. 이 문서는 앱 개발자를 대상으로 라이브러리 추가부터 데이터 수집 확인까지 전체 절차를 안내합니다. WebView 안의 웹 페이지까지 수집하려면 웹 담당자의 작업도 필요합니다. 자세한 내용은 WebView 페이지 수집 설정에서 다룹니다.

지원 환경​

앱 설정​

ItemValue
minSdk21(Android 5.0) 이상
외부 의존성없음
AAR 크기약 380 ~ 400KB

빌드 도구​

Android Gradle Plugin(AGP) 7.0 이상이 필요합니다. JDK와 Gradle 버전은 사용하는 AGP의 요건을 그대로 따릅니다.

AGPJDKGradle
7.x11 이상7.0 이상
8.x17 이상8.x
9.x17 이상9.5 이상

WebView 연동 요구 버전​

앱 안의 WebView 페이지까지 함께 수집하려면 Android 에이전트와 브라우저 에이전트가 모두 요구 버전을 충족해야 합니다. 한쪽이라도 버전이 낮으면 일부 항목이 수집되지 않으며, 앱과 웹 어느 로그에도 오류가 남지 않습니다.

ComponentMinimum versionOwner
Android 에이전트2.3.5앱 개발자
브라우저 에이전트3.2.0웹 개발자

적용을 마친 뒤에는 대시보드의 agent_version 값으로 실제 동작 중인 버전을 확인하세요. 파일을 교체하는 것만으로는 반영 여부를 알 수 없습니다. 브라우저 에이전트 파일의 버전은 첫 4줄의 배너로 확인합니다.

head -4 whatap-browser-agent.js

에이전트 설치​

다음 순서로 진행합니다.

  1. 라이브러리 추가
  2. Manifest 설정
  3. SDK 초기화
  4. ProGuard 설정
  5. 설치 확인

여기까지 마치면 화면 전환, 리소스, 크래시, ANR 수집이 시작됩니다. 네트워크, WebView, 메서드 추적에는 추가 작업이 필요합니다. 자세한 내용은 수집 항목에서 확인하세요.

1. 라이브러리 추가​

두 가지 방법 중 하나를 선택합니다. Maven Central 방식을 권장합니다.

Maven Central(권장)​

mavenCentral()이 이미 설정되어 있다면 저장소를 추가로 설정하지 않아도 됩니다. Maven Central에서 최신 버전을 확인해 <최신 버전> 자리에 넣으세요.

app/build.gradle.kts
dependencies {
implementation("io.whatap.android:whatap-android-agent:<최신 버전>")
}
app/build.gradle
dependencies {
implementation 'io.whatap.android:whatap-android-agent:<최신 버전>'
}

저장소 선언이 없다면 추가합니다.

settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
주의

버전을 +나 latest.release로 지정하지 마세요. 빌드할 때마다 에이전트 버전이 달라져 문제가 생겼을 때 원인을 찾기 어렵습니다. Maven Central에서 최신 버전을 확인하고 명시적으로 지정하세요.

AAR 파일 직접 추가​

폐쇄망이나 사내망으로 Maven Central에 접근할 수 없을 때 사용합니다. 전달받은 AAR 파일을 app/libs/에 복사합니다. 아래 예제의 파일명은 예시이며, 실제 전달받는 파일명은 상황에 따라 다를 수 있습니다. 복사한 파일의 실제 이름으로 바꿔 지정하세요.

app/build.gradle.kts
dependencies {
implementation(files("libs/whatap-agent-bom-complete.aar"))
}
app/build.gradle
dependencies {
implementation files('libs/whatap-agent-bom-complete.aar')
}

2. Manifest 설정​

AndroidManifest.xml에 권한과 Application 클래스를 등록합니다.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:name=".MyApplication"
...>
</application>
</manifest>

android:name을 등록하지 않으면 Application 클래스가 실행되지 않아 SDK가 초기화되지 않습니다. WebView를 연동한 앱에서는 브릿지가 "에이전트 없음"을 반환합니다. 그러면 웹 데이터가 표준 HTTP 전송으로 폴백하고, 대시보드에서는 일반 브라우저(BROWSER)로 분류됩니다. 디버그 로그를 켜면 앱 로그에 Agent가 초기화되지 않았습니다가 남습니다.

3. SDK 초기화​

Application.onCreate()에서 한 번만 호출합니다. 앱에 이미 Application 서브클래스가 있다면 그 클래스의 onCreate()에 코드를 추가하세요.

Kotlin
import android.app.Application
import io.whatap.android.agent.WhatapAgent

class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345) // Int 입니다. L 접미어를 붙이면 컴파일 오류
.setServerUrl("https://<수집 서버 도메인>/m") // 끝의 /m 필수
.setSampling(1.0)
.build(this)
}
}
Java
import android.app.Application;
import io.whatap.android.agent.WhatapAgent;

public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();

WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m")
.setSampling(1.0)
.build(this);
}
}

필수 값은 세 가지입니다.

MethodDescription
setProjectKey(String)발급받은 project access key
setPCode(int)프로젝트 코드. int 타입
setServerUrl(String)수집 서버 base URL. 빠지면 데이터가 전송되지 않음

나머지 옵션은 Builder 옵션에서 확인하세요.

주의

초기화 순서를 지키세요.

네트워크 라이브러리 계측부터 나오는 모든 API는 build(this)가 끝난 뒤 호출해야 합니다. 그 전에 호출하면 네트워크 계측은 무시되며, 커스텀 로그에서 IllegalStateException이 발생합니다.

4. ProGuard 설정​

minifyEnabled false인 앱은 이 절을 건너뛰어도 됩니다. R8이 실행되지 않아 클래스와 메서드 이름이 바뀌지 않습니다.

minifyEnabled true인 경우, AAR 안에 consumer ProGuard 규칙이 들어 있어 자동으로 적용됩니다. 화면, 네트워크, 크래시, ANR, 리소스 같은 네이티브 계측만 쓴다면 별도 keep 규칙이 필요 없습니다.

WebView를 사용하는 앱​

AAR에 동봉된 consumer 규칙에는 WebView 브릿지 메서드를 보존하는 규칙이 없습니다. 브라우저 에이전트는 브릿지 메서드를 이름으로 호출하므로, R8이 @JavascriptInterface 메서드 이름을 바꾸면 브릿지 호출이 모두 실패합니다. 이때 window.whatapBridge 객체는 그대로 남아 전송 경로는 브릿지로 유지됩니다. 개별 호출만 실패하기 때문에 HTTP 폴백도 동작하지 않고 WebView 데이터가 전량 유실됩니다.

증상은 "release 빌드에서만 웹뷰 데이터 0건, 디버그 빌드는 정상"으로 나타납니다. 다음 규칙을 추가하세요.

app/proguard-rules.pro
# WhaTap WebView 브릿지. JS가 메서드명으로 직접 호출하므로 이름 보존 필수
-keep class io.whatap.android.agent.webview.** { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keepattributes *Annotation*
-dontwarn io.whatap.android.**

build.gradle의 proguardFiles에 getDefaultProguardFile("proguard-android-optimize.txt") 또는 getDefaultProguardFile("proguard-android.txt")가 포함되어 있다면 AGP 기본 파일이 @JavascriptInterface 규칙을 이미 담고 있습니다. 위 규칙이 필수인 경우는 기본 파일을 포함하지 않고 커스텀 규칙만 사용하는 프로젝트입니다. 기본 파일을 쓰는 프로젝트도 proguardFiles 구성이 바뀔 때를 대비해 명시적으로 추가하는 것을 권장합니다.

화면 이름 가독성​

대시보드의 화면 이름과 ScreenGroup 이름은 Activity 클래스의 단순 이름으로 만들어집니다. R8이 Activity와 Fragment 클래스명을 바꾸는 구성이라면 화면 이름이 난독화된 값으로 적재됩니다. 데이터가 유실되지는 않지만 어느 화면인지 알아보기 어렵습니다. 필요하면 다음 규칙을 추가하세요. 브릿지 동작에는 필요하지 않습니다.

app/proguard-rules.pro
-keep class * extends android.app.Activity
-keep class * extends androidx.fragment.app.Fragment
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

5. 설치 확인​

앱을 실행한 뒤 logcat으로 확인합니다. 에이전트의 logcat 태그는 모두 whatap으로 시작합니다.

adb logcat | grep -i whatap

Android Studio의 Logcat 창을 사용한다면 필터에 tag:whatap을 입력하세요.

초기화 성공

✅ WhatapAgent가 성공적으로 초기화되었습니다.

서버 전송 성공

✅ [SpanExporter] HTTP 200 Success (123ms)

데이터는 기본적으로 10초마다 전송되므로 앱을 실행한 직후에는 이 로그가 나오지 않습니다. 화면을 몇 번 이동한 뒤 다시 확인하세요. 이 로그가 나오면 HTTP 요청이 성공한 것입니다. 수집 서버가 데이터를 정상적으로 처리했는지는 대시보드에 데이터가 보이는지 확인하세요.

네트워크 요청 수집

🌐 [SpanExporter] Starting export for Span: GET (TraceId: ...)
- network_library: OkHttp / Volley / HttpUrlConnection
- http_request_method: GET
- status_code: 200

수집 항목​

build(this)가 정상 종료되면 다음 항목을 자동으로 수집합니다.

  • Activity와 Fragment 화면 로딩, ScreenGroup
  • 크래시와 ANR
  • 사용자 로그
  • CPU, 메모리, 온도 등 리소스 사용량
  • heartbeat

setCollectScreenLoading 또는 setCollectHeartbeat를 끄면 해당 항목만 수집하지 않습니다. 크래시와 ANR 수집은 build(this)가 성공하면 항상 켜집니다. 크래시와 ANR 수집을 끄거나 리포터 종류를 선택하는 Builder 옵션은 없습니다. iOS의 useNativeCrashReporter(), usePLCrashReporter()는 iOS 전용이므로 Android 코드로 옮겨 쓸 수 없습니다.

다음 세 항목은 자동으로 켜지지 않으며 추가 작업이 필요합니다.

Item필요한 작업
네트워크 라이브러리 요청, 응답 정보네트워크 라이브러리 계측의 wrap(), onRequest() 호출 또는 자동 계측 적용
메서드 실행시간메서드 실행시간 추적의 CallStackTracer 호출
WebView 페이지 로드와 성능 정보WebView 페이지 수집 설정의 브릿지 등록

Builder 옵션​

모든 Builder 옵션은 선택 사항이며, 지정하지 않으면 기본값이 적용됩니다.

기본 설정​

MethodDefaultDescription
setProjectKey(String)(필수)발급받은 project access key
setPCode(int)(필수)프로젝트 코드
setServerUrl(String)(필수)base URL. /trace, /log가 자동으로 붙음
setTraceServerUrl(String)(자동)trace 엔드포인트를 직접 지정할 때만 사용
setLogServerUrl(String)(자동)log 엔드포인트를 직접 지정할 때만 사용
setSampling(double)1.0수집 비율. 0.0 ~ 1.0 스케일이며 50%는 0.5. 범위를 벗어난 값은 무시하고 기본값 유지
주의

setSampling(double)은 0.0 ~ 1.0 스케일입니다. 브라우저 에이전트의 sampleRate는 0 ~ 100 스케일이므로 값을 그대로 옮겨 적으면 안 됩니다. 모바일의 1.0을 웹에 그대로 넣으면 수집 비율이 1%가 됩니다.

수집 항목 켜고 끄기​

MethodDefaultDescription
setCollectScreenLoading(boolean)true화면 로딩 수집
setCollectNetwork(boolean)true네트워크 수집
setCollectHeartbeat(boolean)true30초 주기 heartbeat
setScreenGroupDelaySeconds(int)0화면 그룹 닫힘 지연(초). 0은 비활성화
setExcludeLifecycleEventsFromScreenGroup(boolean)true화면 그룹에서 생명주기 이벤트 제외

setCollectNetwork(false)로 두면 네트워크 라이브러리 계측의 wrap(), onRequest() 호출이 전부 아무 일도 하지 않습니다. 네트워크를 계측하려면 기본값 true를 유지하세요.

setScreenGroupDelaySeconds(int)의 기본값은 0입니다. 0이면 마지막 task가 끝나는 즉시 ScreenGroup이 닫힙니다. 화면에 진입한 뒤 WebView의 loadUrl()을 호출하기까지 시간이 오래 걸리면, 웹뷰 데이터가 네이티브 화면과 다른 그룹으로 묶일 수 있습니다. 이 경우 3 정도를 지정해 그룹이 바로 닫히지 않도록 하세요.

전송과 버퍼링​

트래픽을 줄여야 할 때 조정합니다.

MethodDefaultDescription
setFlushIntervalMs(long)10000배치 전송 주기(ms)
setQueueSize(int)1000메모리 큐 크기
setMaxDiskBytes(int)512000오프라인 디스크 버퍼 상한(bytes)
setMaxDiskFiles(int)5디스크 버퍼 파일 개수. 초과 시 회전
setKeepAliveEnabled(boolean)trueHTTP keep-alive
setMaxConnections(int)5최대 동시 연결 수
setDisconnectAfterSend(boolean)false전송 후 연결 즉시 종료
WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<수집 서버 도메인>/m")
.setCollectHeartbeat(false)
.setFlushIntervalMs(60_000L)
.build(this)

백그라운드 상태에서도 heartbeat와 리소스 샘플러가 전송될 수 있습니다. 사용량이 적은 시간대의 트래픽을 줄이려면 위 예제처럼 heartbeat 수집을 끄거나 flush 간격을 늘리세요.

setKeepAliveEnabled(boolean)과 setMaxConnections(int)는 각각 http.keepAlive와 http.maxConnections JVM system property를 전역으로 설정합니다. 따라서 앱에서 Volley나 HttpURLConnection을 사용하고 있다면 이들의 동작도 함께 바뀝니다. 기본값을 사용하는 것을 권장합니다.

주의

setUserId(String)과 setSessionId(String)에 값을 지정해도 실제 전송되는 값에는 반영되지 않습니다(현재 배포 버전 기준). 세션 ID는 build() 안에서 새로 만든 값으로 덮어씁니다. 사용자 ID는 SharedPreferences 값으로 결정됩니다. 로그인 사용자를 반영하려면 로그인 사용자 ID의 WhatapExtra.set(WhatapExtra.USER_ID, ...)를 사용하세요.

네트워크 헤더 캡처​

에이전트 2.3.4부터 지원합니다. 이전 버전에는 아래 세 메서드가 없어 빌드가 실패합니다.

MethodDefaultDescription
setCaptureRequestHeaders(boolean)falserequest 헤더를 http.request.header.* attribute로 캡처
setCaptureResponseHeaders(boolean)falseresponse 헤더를 http.response.header.* attribute로 캡처
setHeaderValueMaxLength(int)512헤더 값 최대 길이(bytes). 초과 시 절삭. 0은 무제한

옵션을 켜면 OkHttp, HttpURLConnection, Volley, Apache HttpClient를 통과하는 모든 HTTP 헤더가 다음 규칙에 따라 부착됩니다. 이 규칙은 OpenTelemetry Semantic Conventions를 따릅니다.

RuleExample
헤더 이름을 소문자로 바꾸고 -를 _로 변환X-Request-ID → http.request.header.x_request_id
응답 헤더도 같은 규칙 적용Set-Cookie → http.response.header.set_cookie
다중값 헤더는 쉼표로 연결a, b, c
길이 초과 시 앞부분만 남기고 표시...[TRUNCATED to <N>B]
위험

민감 헤더 자동 마스킹이 없습니다.

Authorization, Cookie, Set-Cookie, X-API-Key, Proxy-Authorization 같은 민감 헤더도 옵션을 켜면 값 그대로 캡처되어 수집 서버로 전송됩니다. 켜기 전에 캡처 범위를 반드시 검토하고, 필요 없다면 기본값 false로 두세요.

위 옵션과 무관하게 다음 attribute는 항상 자동으로 부착됩니다(2.3.4 이상).

AttributeTypeDescription
http.request.body.sizelongrequest body 크기(bytes)
http.response.body.sizelongresponse body 크기(bytes)

네트워크 라이브러리 계측​

AAR을 추가하는 것만으로는 네트워크 데이터가 수집되지 않습니다. 사용하는 네트워크 라이브러리에 맞는 코드를 직접 추가해야 합니다. 계측 초기화는 build(this)가 자동으로 처리하므로 앱에서 init()을 따로 호출할 필요는 없습니다.

OkHttp3와 Retrofit​

import io.whatap.android.agent.instrumentation.okhttp.OkHttp3Instrumentation

val base = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build()

val client = OkHttp3Instrumentation.wrap(base)

val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.build()

wrap()이 반환한 client를 사용해야 계측됩니다. 원본 base를 그대로 쓰면 아무것도 수집되지 않습니다.

요청 단위 attribute가 필요하면 wrap(client, Config) 오버로드를 사용하세요. 요청 단위 사용자 정의 attribute를 참조하세요.

Volley​

import io.whatap.android.agent.instrumentation.volley.VolleyInstrumentation;

StringRequest req = new StringRequest(Request.Method.GET, url,
response -> {
// 응답 처리
VolleyInstrumentation.onResponseExit(req, response);
},
error -> {
// 에러 처리
VolleyInstrumentation.onErrorExit(req, error);
});

VolleyInstrumentation.onRequest(req);
queue.add(req);

onRequest()는 queue.add() 직전에, onResponseExit()와 onErrorExit()는 각 콜백 안에서 호출합니다. 세 지점이 모두 들어가야 합니다.

Volley는 성공 콜백에서 실제 상태 코드를 알 수 없어, 성공 응답의 status_code는 일률적으로 200으로 기록됩니다. 실패는 VolleyError.networkResponse.statusCode에서 정확한 값을 가져옵니다. OkHttp와 HttpURLConnection은 실제 코드가 그대로 들어갑니다.

HttpURLConnection​

import io.whatap.android.agent.instrumentation.httpurlconnection.HttpUrlConnectionInstrumentation;

HttpURLConnection raw = (HttpURLConnection) url.openConnection();
HttpURLConnection conn = HttpUrlConnectionInstrumentation.wrap(raw);
try {
int code = conn.getResponseCode();
// body 처리
} finally {
conn.disconnect(); // span 은 wrap 이 자동으로 닫습니다
}

wrap()이 반환한 conn을 사용해야 계측됩니다. 원본 raw를 그대로 쓰면 수집되지 않습니다.

Apache HttpClient​

두 가지 방법이 있습니다.

import io.whatap.android.agent.instrumentation.httpclient.ApacheHttpClientInstrumentation;
import io.whatap.android.agent.instrumentation.httpclient.InstrumentedDefaultHttpClient;

// 방법 1. 기존 HttpClient 를 감쌉니다
HttpClient client = ApacheHttpClientInstrumentation.wrap(existingClient);

// 방법 2. DefaultHttpClient 를 쓰고 있었다면 드롭인 교체 (2.2.6 이상)
HttpClient client2 = new InstrumentedDefaultHttpClient();

InstrumentedDefaultHttpClient는 attribute 체이닝을 제공합니다.

HttpClient client3 = new InstrumentedDefaultHttpClient()
.addAttribute("module", "payment")
.setResponseAttributeExtractor(resp -> {
Map<String, Object> a = new HashMap<>();
a.put("biz.status", resp.getStatusLine().getStatusCode());
return a;
});

Apache HttpClient는 Android 6.0부터 플랫폼에서 제거되었으므로 빌드 설정에 다음이 필요합니다.

app/build.gradle.kts
android {
useLibrary("org.apache.http.legacy")
}

이 선행 조건을 적용할 수 없으면 OkHttp나 HttpURLConnection 같은 다른 네트워크 클라이언트를 사용하세요.

요청 단위 사용자 정의 attribute​

에이전트 2.2.6부터 요청마다 추가 attribute를 붙일 수 있습니다. 다음은 Volley 기준 예시입니다.

VolleyInstrumentation.configure(req)
.addAttribute("module", "payment")
.addAttribute("tenant_id", "demo")
.setRequestAttributeExtractor(r -> {
Map<String, Object> a = new HashMap<>();
a.put("biz.user_id", extractFromQuery(r.getUrl()));
return a;
})
.setResponseAttributeExtractor(resp -> {
Map<String, Object> a = new HashMap<>();
if (resp instanceof String) a.put("biz.body_len", ((String) resp).length());
return a;
});
queue.add(req);

OkHttp와 HttpURLConnection은 wrap(client, Config) 오버로드에 같은 형태의 Config를 넘깁니다. 부착한 값은 전송 JSON의 metrics.extras로 들어가 대시보드에 표시됩니다.

자동 계측(선택)​

Gradle 플러그인을 적용하면 바이트코드를 주입해 네트워크 라이브러리와 메서드 실행시간을 자동으로 계측합니다. 따라서 앞 절에서 설명한 wrap()이나 onRequest() 호출을 코드에 직접 넣지 않아도 됩니다.

Project 레벨 build.gradle.kts
plugins {
id("io.whatap.android") version "<최신 버전>" apply false
}
app/build.gradle.kts
plugins {
id("com.android.application")
id("io.whatap.android")
}
Project 레벨 build.gradle
plugins {
id 'io.whatap.android' version '<최신 버전>' apply false
}
app/build.gradle
plugins {
id 'com.android.application'
id 'io.whatap.android'
}

폐쇄망에서 플러그인 JAR을 직접 사용하려면 libs/ 또는 사내 Maven 저장소에 배치하고 buildscript에서 참조합니다.

Project 레벨 build.gradle.kts
buildscript {
dependencies {
classpath(files("libs/whatap-android-plugin-<최신 버전>.jar"))
}
}

WebView 브릿지 등록은 플러그인을 적용해도 자동으로 처리되지 않습니다. WebView 페이지 수집 설정을 따로 진행하세요.

메서드 실행시간 추적​

특정 메서드가 실행되는 데 걸리는 시간을 측정합니다. WhatapAgent가 이미 초기화되어 있다면 처음 호출할 때 자동으로 준비됩니다. 따라서 별도로 초기화할 필요가 없습니다.

계측 패턴​

패턴 1부터 3은 에이전트 2.2.8, 패턴 4는 2.2.9부터 지원합니다.

import java.util.concurrent.Callable;
import io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer;
import io.whatap.android.agent.instrumentation.stacktrace.StackSpan;

// 패턴 1. try-with-resources. 메서드 본문 전체를 감쌀 때
try (AutoCloseable s = CallStackTracer.scope("PaymentService", "charge")) {
// 측정할 로직
}

// 패턴 2. 반환값 없음
CallStackTracer.measure("PaymentService", "charge", () -> doCharge());

// 패턴 3. 반환값 있음, checked exception 전파
Receipt r = CallStackTracer.measure("PaymentService", "charge",
(Callable<Receipt>) () -> doChargeReturn());

// 패턴 4. StackSpan. 시작 지점과 종료 지점이 떨어져 있을 때
StackSpan span = CallStackTracer.start("PaymentService", "charge");
try {
// 측정할 로직
span.end();
} catch (RuntimeException e) {
span.endWithError(e); // 에러 정보와 함께 종료
}
상황권장 패턴
메서드 본문 전체를 한 줄로 감싸기패턴 1 scope
람다로 감쌀 수 있고 반환값 없음패턴 2 measure(Runnable)
람다로 감쌀 수 있고 반환값 있음패턴 3 measure(Callable)
시작과 종료가 떨어져 있거나 다른 스레드에서 종료패턴 4 StackSpan

StackSpan은 AutoCloseable을 상속하므로 try-with-resources로도 쓸 수 있습니다. 이미 종료하거나 취소한 span에 다시 호출해도 아무 일도 일어나지 않습니다. span.cancel()은 적재도 전송도 하지 않습니다.

계측 대상 고르기​

모든 메서드를 계측할 필요는 없습니다. 너무 많은 메서드를 계측하면 불필요한 데이터만 늘어납니다.

우선순위대상예시
1사용자 액션 진입점Activity.onCreate, Fragment.onViewCreated, 클릭 핸들러
2비즈니스 트랜잭션 경계결제, 송금, 인증, 정산
3네트워크와 DB 호출 wrapperRepository.findX, ApiService.callY
4백그라운드 무거운 작업이미지 처리, 암복호화, 직렬화
부적합getter, setter, 단순 변환, 짧은 루프10ms 필터에 걸려 버려지고 노이즈만 증가

동작 방식​

  • 실행 시간이 10ms 미만이면 전송하지 않습니다. 설정으로 바꿀 수 없고, endWithError()로 끝난 span도 같습니다.
  • 10초마다 배치로 전송합니다.
  • 백그라운드 스레드에서도 독립 동작합니다. ThreadLocal 기반입니다.
  • 재귀 호출은 안전합니다. 다만 깊이 누적에 주의하세요.

logcat에서 다음과 같이 수집 상태를 확인할 수 있습니다.

CallStackLogRecordCollector initialized
CallStack LogRecord Collector started - interval: 10000ms
First log added: MainActivity.loadData
Scheduled flush triggered - buffer size: 3
Collected 3 records from buffer
Sending batch with 3 records
✅ [LogExporter] HTTP 200 Success (85ms)

Skipping (< 10ms): MainActivity.onCreate duration=4ms는 정상 동작입니다. 10ms 미만 메서드를 걸러낸 것입니다.

화면 그룹 설정​

화면 전환 데이터는 build(this)만 호출해도 수집됩니다. 여러 Activity나 Fragment에 걸친 흐름을 하나의 그룹으로 묶고 싶을 때만 이 절의 설정이 필요합니다.

노트

에이전트 2.3.x에서 기존 startGroup(), addTask(), endGroup() API가 startChain(), endChain()으로 바뀌었습니다.

흐름 직접 관리​

시작 화면에서 startChain(), 종료 화면에서 같은 taskId로 endChain()을 호출합니다. taskId를 null로 두면 자동 생성되며 getCurrentChainTaskId()로 조회합니다.

import android.content.Intent
import io.whatap.android.agent.instrumentation.screengroup.ChainView

private const val EXTRA_CHAIN_TASK_ID = "whatap_chain_task_id"

class LoginActivity : AppCompatActivity() {
private fun continueToConfirmation() {
ChainView.getInstance().startChain("LoginFlow", null)
val taskId = ChainView.getInstance().getCurrentChainTaskId() ?: return

startActivity(Intent(this, ConfirmationActivity::class.java)
.putExtra(EXTRA_CHAIN_TASK_ID, taskId))
}
}

class ConfirmationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

intent.getStringExtra(EXTRA_CHAIN_TASK_ID)?.let { taskId ->
ChainView.getInstance().endChain(taskId)
}
}
}
주의

체인은 한 번에 하나만 유지됩니다. 끝내기 전에 다시 startChain()을 호출하면 앞 체인의 taskId를 잃어 endChain()이 동작하지 않습니다.

체인 상태와 종료 대기 시간​

isChainActive()를 사용하면 현재 진행 중인 체인이 있는지 확인할 수 있습니다. 화면과 화면 사이에 짧은 공백이 있다면 종료 대기 시간을 늘려 하나의 그룹으로 유지하세요. 기본값은 0이며, 이 경우 마지막 화면이 끝나는 즉시 그룹이 닫힙니다.

val isChainActive = ChainView.getInstance().isChainActive()

WhatapAgent.Builder.newBuilder()
.setScreenGroupDelaySeconds(3)
.build(this)

커스텀 로그와 전역 attribute​

주의

이 절의 API는 build(this) 완료 후에 호출해야 합니다. 초기화 전에 호출하면 IllegalStateException이 발생합니다.

커스텀 로그​

import io.whatap.android.agent.instrumentation.userlog.UserLogger;

UserLogger.print("결제 시작");

Map<Object, Object> m = new HashMap<>();
m.put("event_name", "checkout");
m.put("amount", 12000);
UserLogger.print(m);

print() 호출 한 번이 로그 한 건입니다. 전송은 setFlushIntervalMs(기본 10초) 주기이므로 호출 직후 대시보드에 바로 나타나지 않습니다.

print(Map)의 키는 <key>.c로, print(String)은 messages.c로 전송됩니다. event_name을 넣어도 SDK가 붙이는 값을 덮어쓰지 않습니다.

전역 attribute​

이후 전송되는 모든 span과 log에 같은 값을 공통으로 추가할 수 있습니다.

import io.whatap.android.agent.extra.WhatapExtra;

WhatapExtra.set("tenant_id", "acme");
WhatapExtra.remove("tenant_id");

값은 metrics.extras.<key>.c로 전송됩니다. 요청 단위 attribute는 요청 단위 사용자 정의 attribute를 참조하세요.

로그인 사용자 ID​

WhatapExtra.USER_ID는 특별 취급하는 예약 키입니다.

WhatapExtra.set(WhatapExtra.USER_ID, "사용자ID");

전송 meta와 WebView 브릿지의 getUserId()가 함께 갱신되어, 네이티브 화면과 WebView 화면이 같은 사용자로 묶입니다. 지정하지 않으면 기기별 익명 ID가 자동 생성되어 유지됩니다.

WebView 페이지 수집 설정​

앱의 WebView에 표시되는 웹 페이지는 네이티브 SDK가 아니라 브라우저 에이전트가 수집합니다. 네이티브 화면과 WebView 페이지를 같은 세션으로 연결하려면 앱과 웹을 모두 설정해야 합니다.

  1. 수집 서버 주소 확인
  2. WebView 등록
  3. 브라우저 에이전트 스크립트 추가

세 단계를 모두 마쳐야 연동이 완료됩니다. 하나라도 빠지면 브릿지에 도달하지 못한 데이터는 표준 HTTP 방식으로 전송됩니다(브라우저 에이전트 옵션 webViewHttpFallback, 기본값 true). 이 경우 데이터 자체는 수집됩니다. 하지만 대시보드에서는 일반 브라우저(BROWSER)로 분류되고 네이티브 세션과 연결되지 않습니다. 폴백은 데이터가 사라지는 것을 막기 위한 안전망일 뿐, 연동이 완료되었다는 뜻은 아닙니다.

요구 버전은 Android 에이전트 2.3.5 이상, 브라우저 에이전트 3.2.0 이상입니다. WebView 연동 요구 버전을 먼저 확인하세요.

조합별 수집 항목​

Android 14 실기기와 실제 수집 서버로 검증한 결과입니다.

Item표준 적용연동 적용, 브릿지 연결연동 적용, 브릿지 미도달
페이지 로드○○○ (폴백)
리소스, AJAX○○○ (폴백)
화면 전환○○○ (폴백)
JS 에러 5종○○○ (폴백)
사용자 이벤트○○○ (폴백)
세션 리플레이○○○ (폴백)
Core Web Vitals○○○ (폴백)
커스텀 로그○✗○ (폴백)
대시보드 분류BROWSERWEBVIEWBROWSER
적재 프로젝트웹 페이지 pcode앱의 모바일 프로젝트웹 페이지 pcode
네이티브와 세션 통합-○✗

표준 적용은 브라우저 에이전트를 isWebView 없이 초기화한 상태이고, 연동 적용은 isWebView: true로 초기화한 상태입니다. JS 에러 5종은 uncaught 에러, unhandled promise rejection, console.error(), noticeError(), CSP violation입니다.

주의

커스텀 로그는 연동 적용에서 수집되지 않습니다.

브라우저 에이전트는 커스텀 로그(enableCustomLog와 logger)를 브릿지 메서드 log로 보내지만, 모바일 브릿지에 해당 인터페이스가 없어 조용히 버려집니다. 디버그 모드에서 bridgeRequest:androidMethodMissing으로 드러납니다. 커스텀 로그가 필요한 페이지는 isWebView를 지정하지 않고 표준 적용으로 두세요.

데이터가 적재되는 프로젝트​

연동이 동작하는 동안 웹 데이터는 앱의 모바일 프로젝트에 적재됩니다. 페이지에 설정한 pcode나 projectAccessKey로는 들어가지 않습니다. 브릿지로 넘어간 데이터는 업로드하기 직전에 프로젝트 식별값을 앱의 setPCode, setProjectKey 값에 맞춥니다. sessionID와 userID도 네이티브 값으로 통일됩니다. 네이티브 화면과 웹뷰 화면을 하나의 세션으로 보려면 두 데이터가 같은 프로젝트에 있어야 합니다. 이를 위해 두 구성요소가 같은 규격을 사용합니다.

Case조회할 프로젝트
표준 적용(isWebView 미설정)웹 페이지의 pcode
연동 적용, 브릿지 연결됨앱의 모바일 프로젝트 pcode
연동 적용, 브릿지 미도달(폴백)웹 페이지의 pcode

따라서 페이지의 pcode와 projectAccessKey는 폴백으로 전송할 때만 사용합니다. 연동이 정상적으로 동작하는 동안에는 사용하지 않습니다. 하지만 브릿지가 끊기는 경우를 대비해 브라우저(RUM) 프로젝트의 유효한 값을 넣어야 합니다. 임의의 값이나 앱의 모바일 pcode를 넣어도 초기화 단계의 형식 검사는 통과합니다. 하지만 폴백으로 전송된 데이터는 어디에도 적재되지 않습니다.

iOS 앱과 Android 앱이 서로 다른 모바일 프로젝트를 사용한다면, 같은 웹 페이지의 웹뷰 데이터도 플랫폼별로 나뉘어 적재됩니다. HTML을 앱별로 나눌 필요는 없습니다. 웹 화면 하나의 전체 지표를 보려면 두 프로젝트를 각각 조회하세요.

1. 수집 서버 주소 확인​

모바일 에이전트의 setServerUrl 값입니다. 웹뷰 데이터도 이 주소로 나가므로 연동에서 특히 중요합니다.

RuleExample
스킴, 호스트(포트), /m 순서로 끝냄https://수집서버-주소:9443/m
리버스 프록시 경로 포함 가능https://내부호스트:9443/22/m
끝에 /를 붙이지 않음https://…/m/은 …/m//pageLoad가 되어 실패

Android 에이전트는 웹뷰 데이터에만 /m을 보정합니다. 주소가 이미 /m으로 끝나면 다시 붙이지 않습니다. 따라서 /m을 빠뜨리면 웹뷰 데이터는 들어오지만 네이티브 데이터는 유실될 수 있습니다. 항상 /m을 포함하세요.

사내 리버스 프록시를 앞단에 두고 프록시 경로를 포함한 값을 사용해도 됩니다. 에이전트는 이 값 뒤에 필요한 경로를 그대로 이어 붙입니다. 따라서 프록시가 다음 경로를 모두 전달하도록 설정하면 됩니다.

Data<serverUrl> 뒤에 붙는 경로
네이티브 트레이스, 로그/trace /log
웹뷰 페이지 로드/pageLoad
리소스, 화면 전환, 에러, 이벤트, 메모리/resource /routeChange /onError /event /memory
Web Vitals/webVitals /v2/webVitals
세션 리플레이/sessionreplay /v2/sessionreplay

2. WebView 등록​

WebView 인스턴스마다 등록해야 합니다. 반드시 loadUrl()을 호출하기 전에 UI 스레드에서 등록하세요. 브릿지가 동작하려면 SDK 초기화와 Manifest 설정이 먼저 끝나 있어야 합니다.

먼저 사용할 메서드를 결정하세요. 앱에 커스텀 WebViewClient가 있는지에 따라 사용할 메서드가 달라집니다.

기존 WebViewClient등록 메서드
없음setupWebView(webView, null)
아래 6개 콜백만 오버라이드setupWebView(webView, myClient)
그 외 콜백을 하나라도 오버라이드attachBridgeOnly(webView) 단독
위험

커스텀 WebViewClient가 있으면 setupWebView()와 wrapWebViewClient()를 쓰기 전에 반드시 확인하세요.

래퍼가 기존 클라이언트로 전달하는 콜백은 다음 6개뿐입니다.

  • onPageStarted
  • onPageFinished
  • onReceivedError(WebView, int, String, String)
  • onPageCommitVisible
  • onLoadResource
  • shouldOverrideUrlLoading(WebView, String)

전달하지 않는 콜백은 shouldOverrideUrlLoading(WebView, WebResourceRequest), onReceivedError(WebView, WebResourceRequest, WebResourceError), onReceivedSslError, shouldInterceptRequest, onReceivedHttpError, onReceivedHttpAuthRequest, onRenderProcessGone, doUpdateVisitedHistory, onFormResubmission, onReceivedClientCertRequest입니다.

특히 API 24 이상에서 사용하는 shouldOverrideUrlLoading(WebView, WebResourceRequest)를 오버라이드한 경우에는 URL 가로채기, 외부 앱 이동, 다운로드 인터셉트가 동작하지 않습니다. onReceivedSslError로 사내 인증서를 통과시키고 있었다면 해당 페이지도 로드되지 않습니다. 이런 경우에는 attachBridgeOnly()만 사용하세요.

다음은 커스텀 WebViewClient 유무와 관계없이 안전한 attachBridgeOnly() 방식입니다.

import android.webkit.CookieManager;
import android.webkit.WebView;
import io.whatap.android.agent.webview.WhatapWebviewBridge; // Webview. v 가 소문자입니다

private WhatapWebviewBridge whatapBridge; // 인스턴스 필드 (static 공유 금지)
private boolean whatapAttached; // 재진입 가드

private void initWhatap(WebView webView) {
if (whatapAttached) return;

// Android 기본값이 false 입니다. 에이전트는 이 값을 대신 켜지 않습니다
webView.getSettings().setJavaScriptEnabled(true);

// 아래 두 줄은 HTTP 폴백 구간의 세션 유지와 샘플링 연속성에 쓰입니다
webView.getSettings().setDomStorageEnabled(true);
CookieManager.getInstance().setAcceptCookie(true);

// Activity/Fragment 의 this 가 아니라 applicationContext (브릿지가 Context 를 영구 보관)
whatapBridge = new WhatapWebviewBridge(webView.getContext().getApplicationContext());
whatapBridge.attachBridgeOnly(webView); // 기존 WebViewClient 는 건드리지 않음
whatapAttached = true;
}

private void openPage(WebView webView, String url) {
initWhatap(webView); // 등록을 먼저
webView.loadUrl(url); // 그 다음 로드
}

Jetpack Compose에서는 factory 람다 안에서 등록합니다.

import android.webkit.WebView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import io.whatap.android.agent.webview.WhatapWebviewBridge

@Composable
fun WhatapWebView(url: String) {
val appContext = LocalContext.current.applicationContext
val bridge = remember { WhatapWebviewBridge(appContext) }

AndroidView(
factory = { ctx ->
WebView(ctx).apply {
settings.javaScriptEnabled = true
bridge.attachBridgeOnly(this) // loadUrl 전에 등록
loadUrl(url)
}
}
)
}

커스텀 WebViewClient가 없거나 위 6개 콜백만 사용한다면 attachBridgeOnly(webView) 자리를 setupWebView(webView, null) 또는 setupWebView(webView, myClient)로 바꾸세요. 브릿지 연결과 함께 네이티브 페이지 로드 span까지 수집합니다.

attachBridgeOnly()만 사용해도 페이지 로드, 리소스, 에러, Web Vitals, 세션 리플레이 등 웹 데이터는 모두 수집됩니다. 수집되지 않는 것은 앱이 만드는 네이티브 페이지 로드 span 하나입니다. 이 데이터는 웹 쪽의 endPageLoad() 호출로 대체할 수 있습니다.

attachBridgeOnly()를 사용하면서 네이티브 페이지 로드 span까지 수집하려면 wrapWebViewClient() 대신 상속을 사용하세요. 이렇게 하면 앱이 오버라이드한 최신 콜백도 그대로 유지됩니다.

import android.graphics.Bitmap;
import android.webkit.WebView;
import io.whatap.android.agent.webview.WhatapWebViewClient; // WebView. V 가 대문자입니다
import io.whatap.android.agent.webview.WhatapWebviewBridge; // Webview. v 가 소문자입니다

class MyWebViewClient extends WhatapWebViewClient {
MyWebViewClient(WhatapWebviewBridge bridge) { super(bridge); }

@Override
public void onPageStarted(WebView v, String url, Bitmap favicon) {
super.onPageStarted(v, url, favicon); // super 호출 필수
// 기존 로직
}
}

위 initWhatap()의 attachBridgeOnly() 뒤에 한 줄을 추가합니다. 상속한다고 해서 attachBridgeOnly()가 필요 없어지는 것은 아닙니다. 브릿지 주입과 페이지 로드 추적은 서로 다른 작업이므로 둘 다 필요합니다.

whatapBridge.attachBridgeOnly(webView);                        // 브릿지 주입 (필수)
webView.setWebViewClient(new MyWebViewClient(whatapBridge)); // 페이지 로드 추적 추가

등록 메서드

MethodDescription
attachBridgeOnly(WebView)브릿지만 연결하고 WebViewClient는 변경하지 않음. 커스텀 클라이언트가 있으면 이 메서드 사용
setupWebView(WebView, WebViewClient)브릿지 연결과 WebViewClient 래핑을 한 번에 처리. 위 6개 콜백만 사용하는 경우에 한해 사용
wrapWebViewClient(WebViewClient)기존 WebViewClient에 페이지 로드 추적을 추가한 래퍼를 반환. setupWebView()와 같은 제약
configureWebView(WebView)지원이 끝난 메서드. attachBridgeOnly()와 동일하게 동작하며 페이지 로드 추적이 빠짐. 사용 금지

WebView 설정

Setting필요한 이유
setJavaScriptEnabled(true)Android 기본값이 false이고 에이전트가 대신 켜지 않음
setDomStorageEnabled(true)HTTP 폴백 구간의 세션 유지와 샘플링 연속성에 사용
CookieManager.setAcceptCookie(true)위와 동일

서드파티 쿠키는 켜지 않아도 됩니다. 브라우저 에이전트가 사용하는 쿠키는 domain 속성이 없는 host-only, SameSite=Lax 쿠키입니다. 수집 서버로 데이터를 전송할 때도 자격 증명을 사용하지 않습니다. CookieManager.setAcceptThirdPartyCookies()는 필요하지 않습니다.

앱을 종료하기 전에 쿠키를 저장하면 폴백 구간의 익명 사용자 ID가 앱 재시작 후에도 유지됩니다.

CookieManager.getInstance().flush();

등록 규칙

Rule지키지 않으면
loadUrl() 이전에 등록페이지가 브릿지 없이 로드되어 그 페이지의 데이터가 유실됨
UI 스레드에서 호출브릿지 생성자는 Handler를 만들고 attachBridgeOnly()는 addJavascriptInterface()를 호출하므로 다른 스레드에서 호출하면 예외가 앱으로 전파됨. 비동기 흐름이라면 webView.post(() -> initWhatap(webView))로 감쌀 것
WebView 인스턴스당 1회만 등록세션과 페이지 로드 추적이 어긋나고 불필요한 리소스가 남음. 한 WebView로 여러 URL을 로드해도 등록은 최초 1회로 충분. 위 예제의 whatapAttached 같은 재진입 가드 필요
생성자에 applicationContext 전달브릿지가 Context를 영구 보관하므로 Activity의 this를 넘기면 화면이 사라진 뒤에도 참조가 남아 누수 발생
브릿지 인스턴스를 필드로 보관하고 static으로 공유하지 않기여러 WebView가 같은 브릿지를 공유하면 화면 연결이 어긋남
setupWebView() 뒤에 setWebViewClient()를 다시 호출하지 않기래퍼가 교체되어 페이지 로드 추적이 사라짐. 브릿지는 영향받지 않으므로 "웹 데이터는 오는데 페이지 로드만 없는" 형태로 나타남
자사 콘텐츠를 로드하는 WebView에만 등록등록한 WebView 안에서 로드되는 콘텐츠는 iframe을 포함해 브릿지에 접근 가능. 외부 링크, 광고, 약관 링크를 여는 WebView에는 등록 금지

반드시 onCreate()에서 등록할 필요는 없습니다. Fragment에서는 onViewCreated()에서 등록합니다. WebView를 동적으로 생성한다면 new WebView(context) 직후, Compose에서는 factory 람다 안에서 등록합니다. restoreState()도 페이지를 다시 로드하므로 그 전에 등록하세요.

노트

startDataUploadTimer()는 호출하지 마세요. 예전 설치 화면 예제에는 이 코드가 남아 있지만, 현재 Android 에이전트는 브릿지 메서드가 호출될 때마다 데이터를 바로 전송합니다. 따라서 이 타이머가 비우는 내부 큐를 사용하지 않습니다. startDataUploadTimer()를 호출해도 빈 큐를 확인하는 타이머만 하나 늘어납니다.

minify를 사용하는 빌드라면 ProGuard 설정의 keep 규칙을 함께 확인하세요.

3. 브라우저 에이전트 스크립트 추가​

WebView가 여는 모든 HTML 페이지의 head 태그 최상단에 다음 스크립트를 넣으세요. 웹 담당자가 진행하는 작업입니다.

비동기 방식은 페이지 로드 성능에 영향을 주지 않습니다. 다만 에이전트가 실행되기 전에 발생한 ajax와 error 데이터는 수집되지 않을 수 있습니다.

Async
<script>
(function (w, h, _a, t, a, b) {
w = w[a] = w[a] || {};
w.config = {
projectAccessKey: "{PROJECT_ACCESS_KEY}",
pcode: {PCODE},
sampleRate: 100,
isWebView: true,
proxyBaseUrl: "https://rum-ap-northeast-2.whatap-browser-agent.io/",
};
a = h.createElement(_a);
a.async = 1;
a.src = t;
t = h.getElementsByTagName(_a)[0];
t.parentNode.insertBefore(a, t);
})(window, document, 'script', 'https://repo.whatap-browser-agent.io/rum/prod/v2/whatap-browser-agent.js', 'WhatapBrowserAgent', '');
</script>

페이지 로드 시점의 데이터를 빠짐없이 수집하려면 동기 방식을 사용하세요. 페이지 로드 성능에 영향을 줄 수 있습니다.

Sync
<script>
window.WhatapBrowserAgent = {
config: {
projectAccessKey: "{PROJECT_ACCESS_KEY}",
pcode: {PCODE},
sampleRate: 100,
isWebView: true,
proxyBaseUrl: "https://rum-ap-northeast-2.whatap-browser-agent.io/",
},
};
</script>
<script src="https://repo.whatap-browser-agent.io/rum/prod/v2/whatap-browser-agent.js" type="text/javascript"></script>
주의

설정 키와 스크립트 주소를 그대로 옮겨 적으세요.

  • isWebView는 대소문자를 구분합니다. isWebview처럼 적으면 알 수 없는 키로 경고 없이 무시되어 네이티브 세션과 이어지지 않습니다.
  • WebView용 스크립트 주소는 /rum/prod/v2/whatap-browser-agent.js입니다. v1 주소에는 Bridge 연동이 없습니다.
  • Async 스니펫의 5번째 인자 'WhatapBrowserAgent'는 바꾸지 마세요. 이 값은 설정을 담는 전역 객체의 이름입니다. 이 값을 번들 경로로 바꾸면 에이전트가 설정을 찾지 못합니다. 이때 콘솔에 별도의 메시지도 나오지 않으며 초기화도 되지 않습니다. 에이전트 파일을 자체 호스팅한다면 번들 경로를 지정하는 4번째 인자를 바꾸세요.
ConfigDescription
projectAccessKey브라우저(RUM) 프로젝트의 액세스 키. 폴백 전송에 사용
pcode브라우저(RUM) 프로젝트의 프로젝트 코드. 폴백 전송에 사용
sampleRate수집 비율(%). 0 ~ 100 스케일이며 모바일 에이전트의 setSampling(0.0 ~ 1.0)과 다름. 적용 확인 중에는 100 사용
isWebView데이터 전송을 모바일 에이전트에 위임하는 스위치. 연동 적용에서 true 지정
proxyBaseUrl폴백 전송 시 데이터를 보낼 주소. 연동 적용에서도 필수. 설정하지 않으면 데이터가 전혀 전송되지 않음

projectAccessKey, pcode, proxyBaseUrl 값은 브라우저(RUM) 프로젝트의 관리 > 에이전트 설치 화면에서 현재 프로젝트 값이 채워진 상태로 확인할 수 있습니다.

적용 여부는 해당 페이지의 브라우저 콘솔에서 확인합니다.

typeof window.WhatapBrowserAgent   // 'object' 이면 적용된 상태

WebView 브릿지 확인​

브릿지가 실제로 주입됐는지 확인하려면 WebView 디버깅을 켠 뒤 PC의 크롬에서 확인합니다.

WebView.setWebContentsDebuggingEnabled(true);   // 확인 후 반드시 제거

PC 크롬에서 chrome://inspect에 접속해 해당 WebView를 inspect하고 콘솔에서 확인합니다.

typeof window.whatapBridge;              // 'object'
typeof window.whatapBridge.pageLoad; // 'function'
typeof window.whatapBridge.getSessionId; // 'function'
주의

minify를 쓴다면 메서드 단위까지 확인하세요.

디버그 빌드는 keep 규칙이 없어도 정상적으로 동작합니다. 따라서 minifyEnabled true 빌드에서도 위 방법으로 확인하세요. window.whatapBridge는 'object'로 나오는데 pageLoad가 'undefined'라면 keep 규칙이 빠진 것입니다. ProGuard 설정을 확인하세요.

문제 해결​

데이터가 전혀 수집되지 않습니다​

  1. setServerUrl()을 지정했는지 확인하세요. 빠지면 전송되지 않습니다.
  2. AndroidManifest.xml의 <application android:name=".MyApplication"> 등록을 확인하세요.
  3. INTERNET 권한이 있는지 확인하세요.
  4. logcat에 ✅ WhatapAgent가 성공적으로 초기화되었습니다.가 찍히는지 확인하세요.
  5. setProjectKey와 setPCode 값이 발급받은 값과 일치하는지 확인하세요.
  6. 프록시나 방화벽 설정으로 전송이 막히지 않았는지 확인하세요.

네트워크 데이터만 수집되지 않습니다​

  1. wrap()이 반환한 객체를 사용하고 있는지 확인하세요. 원본을 그대로 쓰면 수집되지 않습니다.
  2. setCollectNetwork(false)로 꺼두지 않았는지 확인하세요.
  3. Volley는 onRequest(), onResponseExit(), onErrorExit() 세 지점이 모두 들어가야 합니다.

메서드 추적 이벤트가 보이지 않습니다​

  1. 측정 대상이 10ms 이상 걸리는지 확인하세요. 미만은 자동 제외됩니다.
  2. 전송은 10초 배치입니다. 최소 10초 이상 기다려 보세요.
  3. [CallStackTracer] ⏭️ Skipping (< 10ms) 로그가 보이면 정상적으로 걸러진 것입니다.

이벤트가 너무 많이 수집된다면 계측하는 지점을 줄이세요. getter, setter, 단순 변환 메서드에는 계측을 넣지 않는 것이 좋습니다. 계측 대상 고르기를 참조하세요.

WebView 데이터가 수집되지 않습니다​

증상원인조치
웹뷰 데이터가 어디에도 없어 보임웹 페이지의 pcode를 조회 중앱의 모바일 프로젝트 조회
데이터는 오지만 일반 브라우저로 분류됨WebView 등록이 페이지 로드보다 늦음등록 후 로드 순서 확인. 브라우저 에이전트는 초기화 시점에 한 번만 WebView 환경을 판별하므로 로드 후에 브릿지가 주입되어도 전송 경로가 바뀌지 않음
데이터는 오지만 일반 브라우저로 분류됨SDK 초기화 누락. android:name 미등록 또는 build(this) 미호출Manifest 설정과 SDK 초기화 확인
데이터가 전혀 수집되지 않음proxyBaseUrl 미설정연동 적용에서도 설정
release 빌드에서만 웹뷰 데이터 0건R8이 @JavascriptInterface 메서드를 리네임ProGuard 설정의 keep 규칙 확인
네이티브 데이터만 유실(웹뷰는 정상)setServerUrl이 /m으로 끝나지 않음/m 추가
모든 요청이 404setServerUrl 끝의 /로 경로가 //로 생성됨끝의 / 제거
데이터가 거의 안 옴(약 1%)모바일의 1.0을 웹 sampleRate에 그대로 입력웹은 0 ~ 100 스케일. sampleRate: 100 지정
앱의 URL 가로채기, SSL 예외 처리가 깨짐setupWebView(), wrapWebViewClient()가 최신 WebViewClient 콜백을 위임하지 않음attachBridgeOnly() 단독으로 전환
커스텀 로그만 수집 안 됨연동 적용에서는 브릿지가 log를 처리하지 않음해당 페이지는 표준 적용으로 유지
웹뷰 데이터가 네이티브 화면과 다른 그룹으로 잡힘네이티브 화면 로딩이 끝나 ScreenGroup이 이미 닫힌 뒤 웹뷰 데이터가 도착setScreenGroupDelaySeconds(3)으로 종료 지연 또는 화면 진입과 loadUrl() 사이 간격 축소
앱 재시작마다 세션이 새로 생성쿠키 저장 누락 또는 DOM 저장소 비활성CookieManager.flush()와 setDomStorageEnabled(true) 확인

정상적으로 연동되면 Mobile 대시보드에서 네이티브 화면과 WebView 페이지가 같은 세션에 시간순으로 나타납니다. WebView 대시보드에서는 페이지 로드, Web Vitals, 리소스 타이밍을 확인할 수 있습니다.

앱 시작 시 NoClassDefFoundError가 발생합니다​

AAR을 직접 추가했다면 전달받은 파일의 크기가 약 380 ~ 400KB인지 확인하세요. 파일 크기가 이 범위와 크게 다르다면 파일이 온전하지 않은 것이므로 다시 전달해 달라고 요청하세요. Maven Central 좌표를 사용하면 이 문제는 발생하지 않습니다.

빌드 오류가 발생합니다​

Plugin not found 오류

자동 계측의 Gradle 플러그인을 찾을 수 없을 때 다음 설정을 추가합니다.

settings.gradle
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

Java 버전 오류

먼저 빌드 도구에서 AGP, JDK, Gradle의 대응 관계를 확인하세요. class file has wrong version 오류는 빌드에 사용하는 JDK 버전이 낮을 때 발생합니다. 빌드에 사용하는 JDK와 앱의 sourceCompatibility는 서로 별개입니다.

Namespace 경고

Namespace 'io.whatap.android.agent' is used in multiple modules 경고는 무시해도 됩니다. 라이브러리를 여러 모듈에서 사용하고 있다는 뜻이며 앱 실행에는 영향을 주지 않습니다.

그 밖의 빌드 오류

  • Gradle 버전과 AGP 버전이 빌드 도구 요건을 충족하는지 확인하세요.
  • 네트워크 연결 상태를 확인하고, 필요하면 프록시를 설정하세요.
  • 프로젝트를 Clean & Rebuild 하세요.
  • 릴리즈 빌드에서만 앱이 죽는다면 다른 라이브러리의 ProGuard 규칙과 충돌하는지 확인하세요.

지원 요청​

기술 지원을 요청할 때 다음 정보를 함께 전달하면 더 빠르게 해결할 수 있습니다.

  • 프로젝트 액세스 키
  • 에이전트 버전과 Android SDK 버전
  • Gradle 버전과 Android Gradle Plugin 버전
  • logcat 전체 스택트레이스
  • build.gradle 파일 내용