Android 에이전트 적용
WhaTap 모바일 에이전트를 Android 앱에 적용하면 화면 로딩, 네트워크 호출, 크래시, ANR, 리소스 사용량, WebView 페이지 성능을 수집할 수 있습니다. 라이브러리 추가부터 데이터 수집 확인까지 적용 절차를 안내합니다. WebView 안의 웹 페이지까지 수집하려면 웹 담당자의 작업도 필요합니다. 자세한 내용은 WebView 페이지 수집 설정에서 다룹니다.
지원 환경
앱 설정
| Item | Value |
|---|---|
minSdk | 21(Android 5.0) 이상 |
| 외부 의존성 | 없음 |
| AAR 크기 | 약 380 ~ 400KB |
빌드 도구
Android Gradle Plugin(AGP) 7.0 이상이 필요합니다. JDK와 Gradle 버전은 사용하는 AGP의 요건을 그대로 따릅니다.
| AGP | JDK | Gradle |
|---|---|---|
| 7.x | 11 이상 | 7.0 이상 |
| 8.x | 17 이상 | 8.x |
| 9.x | 17 이상 | 9.5 이상 |
WebView 연동 요구 버전
앱 안의 WebView 페이지까지 함께 수집하려면 Android 에이전트와 브라우저 에이전트가 모두 요구 버전을 충족해야 합니다. 한쪽이라도 버전이 낮으면 일부 항목이 수집되지 않으며, 앱과 웹 어느 로그에도 오류가 남지 않습니다.
| Component | Minimum version | Owner |
|---|---|---|
| Android 에이전트 | 2.3.5 | 앱 개발자 |
| 브라우저 에이전트 | 3.2.0 | 웹 개발자 |
적용을 마친 뒤에는 대시보드의 agent_version 값으로 실제 동작 중인 버전을 확인하세요. 파일을 교체하는 것만으로는 반영 여부를 알 수 없습니다. 브라우저 에이전트 파일의 버전은 첫 4줄의 배너로 확인합니다.
head -4 whatap-browser-agent.js
에이전트 설치
다음 순서로 진행합니다.
여기까지 마치면 화면 전환, 리소스, 크래시, ANR 수집이 시작됩니다. 네트워크, WebView, 메서드 추적에는 추가 작업이 필요합니다. 자세한 내용은 수집 항목에서 확인하세요.
1. 라이브러리 추가
두 가지 방법 중 하나를 선택합니다. Maven Central 방식을 권장합니다.
Maven Central(권장)
mavenCentral()이 이미 설정되어 있다면 저장소를 추가로 설정하지 않아도 됩니다. Maven Central에서 최신 버전을 확인해 <최신 버전> 자리에 넣으세요.
dependencies {
implementation("io.whatap.android:whatap-android-agent:<최신 버전>")
}
dependencies {
implementation 'io.whatap.android:whatap-android-agent:<최신 버전>'
}
저장소 선언이 없다면 추가합니다.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
버전을 +나 latest.release로 지정하지 마세요. 빌드할 때마다 에이전트 버전이 달라져 문제가 생겼을 때 원인을 찾기 어렵습니다. Maven Central에서 최신 버전을 확인하고 명시적으로 지정하세요.
AAR 파일 직접 추가
폐쇄망이나 사내망으로 Maven Central에 접근할 수 없을 때 사용합니다. 전달받은 AAR 파일을 app/libs/에 복사합니다. 아래 예제의 파일명은 예시이며, 실제 전달받는 파일명은 상황에 따라 다를 수 있습니다. 복사한 파일의 실제 이름으로 바꿔 지정하세요.
dependencies {
implementation(files("libs/whatap-agent-bom-complete.aar"))
}
dependencies {
implementation files('libs/whatap-agent-bom-complete.aar')
}
2. Manifest 설정
AndroidManifest.xml에 권한과 Application 클래스를 등록합니다.
<?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()에 코드를 추가하세요.
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)
}
}
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);
}
}
필수 값은 세 가지입니다.
| Method | Description |
|---|---|
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건, 디버그 빌드는 정상"으로 나타납니다. 다음 규칙을 추가하세요.
# 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 클래스명을 바꾸는 구성이라면 화면 이름이 난독화된 값으로 적재됩니다. 데이터가 유실되지는 않지만 어느 화면인지 알아보기 어렵습니다. 필요하면 다음 규칙을 추가하세요. 브릿지 동작에는 필요하지 않습니다.
-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 옵션은 선택 사항이며, 지정하지 않으면 기본값이 적용됩니다.
기본 설정
| Method | Default | Description |
|---|---|---|
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%가 됩니다.
수집 항목 켜고 끄기
| Method | Default | Description |
|---|---|---|
setCollectScreenLoading(boolean) | true | 화면 로딩 수집 |
setCollectNetwork(boolean) | true | 네트워크 수집 |
setCollectHeartbeat(boolean) | true | 30초 주기 heartbeat |
setScreenGroupDelaySeconds(int) | 0 | 화면 그룹 닫힘 지연(초). 0은 비활성화 |
setExcludeLifecycleEventsFromScreenGroup(boolean) | true | 화면 그룹에서 생명주기 이벤트 제외 |
setCollectNetwork(false)로 두면 네트워크 라이브러리 계측의 wrap(), onRequest() 호출이 전부 아무 일도 하지 않습니다. 네트워크를 계측하려면 기본값 true를 유지하세요.
setScreenGroupDelaySeconds(int)의 기본값은 0입니다. 0이면 마지막 task가 끝나는 즉시 ScreenGroup이 닫힙니다. 화면에 진입한 뒤 WebView의 loadUrl()을 호출하기까지 시간이 오래 걸리면, 웹뷰 데이터가 네이티브 화면과 다른 그룹으로 묶일 수 있습니다. 이 경우 3 정도를 지정해 그 룹이 바로 닫히지 않도록 하세요.
전송과 버퍼링
트래픽을 줄여야 할 때 조정합니다.
| Method | Default | Description |
|---|---|---|
setFlushIntervalMs(long) | 10000 | 배치 전송 주기(ms) |
setQueueSize(int) | 1000 | 메모리 큐 크기 |
setMaxDiskBytes(int) | 512000 | 오프라인 디스크 버퍼 상한(bytes) |
setMaxDiskFiles(int) | 5 | 디스크 버퍼 파일 개수. 초과 시 회전 |
setKeepAliveEnabled(boolean) | true | HTTP 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부터 지원합니다. 이전 버전에는 아래 세 메서드가 없어 빌드가 실패합니다.
| Method | Default | Description |
|---|---|---|
setCaptureRequestHeaders(boolean) | false | request 헤더를 http.request.header.* attribute로 캡처 |
setCaptureResponseHeaders(boolean) | false | response 헤더를 http.response.header.* attribute로 캡처 |
setHeaderValueMaxLength(int) | 512 | 헤더 값 최대 길이(bytes). 초과 시 절삭. 0은 무제한 |
옵션을 켜면 OkHttp, HttpURLConnection, Volley, Apache HttpClient를 통과하는 모든 HTTP 헤더가 다음 규칙에 따라 부착됩니다. 이 규칙은 OpenTelemetry Semantic Conventions를 따릅니다.
| Rule | Example |
|---|---|
헤더 이름을 소문자로 바꾸고 -를 _로 변환 | 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 이상).
| Attribute | Type | Description |
|---|---|---|
http.request.body.size | long | request body 크기(bytes) |
http.response.body.size | long | response 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부터 플랫폼에서 제거되었으므로 빌드 설정에 다음이 필요합니다.
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() 호출을 코드에 직접 넣지 않아도 됩니다.
plugins {
id("io.whatap.android") version "<최신 버전>" apply false
}
plugins {
id("com.android.application")
id("io.whatap.android")
}
plugins {
id 'io.whatap.android' version '<최신 버전>' apply false
}
plugins {
id 'com.android.application'
id 'io.whatap.android'
}
저장소를 따로 선언할 필요는 없습니다. 기본 gradlePluginPortal()이 Maven Central을 프록시합니다.
플러그인을 적용한 모듈의 소스만 계측합니다.
다음 코드는 자동 계측되지 않습니다. 해당 구간에는 네트워크 라이브러리 계측의 패턴을 직접 넣으세요.
- 플러그인을 적용하지 않은 다른 Gradle 모듈. 모듈마다
id("io.whatap.android")가 필요합니다. 앱 모듈에만 넣으면:network모듈의 요청은 0건입니다. - 의존성 JAR과 AAR 내부 코드. Retrofit이 내부에서 만드는 기본
OkHttpClient등이 해당합니다. OkHttpClient()생성자. 플러그인이 바꾸는 것은OkHttpClient.Builder.build()를 호출하는 지점뿐입니다.
폐쇄망에서는 와탭이 전달한 whatap-android-plugin-<버전>-offline-repo.zip의 압축을 풀고, 그 폴더를 settings.gradle.kts의 pluginManagement 저장소로 등록하세요. 플러그인이 ASM에 의존하므로 플러그인 JAR 파일 하나만 두면 동작하지 않습니다. 앞의 plugins {} 블록은 그대로 사용합니다.
pluginManagement {
repositories {
maven { url = uri("whatap-plugin-repo-<version>") }
gradlePluginPortal()
}
}
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 호출 wrapper | Repository.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 페이지를 같은 세션으로 연결하려면 앱과 웹을 모두 설정해야 합니다.
세 단계를 모두 마쳐야 연동이 완료됩니다. 하나라도 빠지면 브릿지에 도달하지 못한 데이터는 표준 HTTP 방식으로 전송됩니다(브라우저 에이전트 옵션 webViewHttpFallback, 기본값 true). 이 경우 데이터 자체는 수집됩니다. 하지만 대시보드에서는 일반 브라우저(BROWSER)로 분류되고 네이티브 세션과 연결되지 않습니다. 폴백은 데이터가 사라지는 것을 막기 위한 안전망일 뿐, 연동이 완료되었다는 뜻은 아닙니다.
요구 버전은 Android 에이전트 2.3.5 이상, 브라우저 에이전트 3.2.0 이상입니다. WebView 연동 요구 버전을 먼저 확인하세요.
조합별 수집 항목
Android 14 실기기와 실제 수집 서버로 검증한 결과입니다.
| Item | 표준 적용 | 연동 적용, 브릿지 연결 | 연동 적용, 브릿지 미도달 |
|---|---|---|---|
| 페이지 로드 | ○ | ○ | ○ (폴백) |
| 리소스, AJAX | ○ | ○ | ○ (폴백) |
| 화면 전환 | ○ | ○ | ○ (폴백) |
| JS 에러 5종 | ○ | ○ | ○ (폴백) |
| 사용자 이벤트 | ○ | ○ | ○ (폴백) |
| 세션 리플레이 | ○ | ○ | ○ (폴백) |
| Core Web Vitals | ○ | ○ | ○ (폴백) |
| 커스텀 로그 | ○ | ✗ | ○ (폴백) |
| 대시보드 분류 | BROWSER | WEBVIEW | BROWSER |
| 적재 프로젝트 | 웹 페이지 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 값입니다. 웹뷰 데이터도 이 주소로 나가므로 연동에서 특히 중요합니다.
| Rule | Example |
|---|---|
스킴, 호스트(포트), /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개뿐입니다.
onPageStartedonPageFinishedonReceivedError(WebView, int, String, String)onPageCommitVisibleonLoadResourceshouldOverrideUrlLoading(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)); // 페이지 로드 추적 추가
등록 메서드
| Method | Description |
|---|---|
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 데이터는 수집되지 않을 수 있습니다.
<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>
페이지 로드 시점의 데이터를 빠짐없이 수집하려면 동기 방식을 사용하세요. 페이지 로드 성능에 영향을 줄 수 있습니다.
<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번째 인자를 바꾸세요.
| Config | Description |
|---|---|
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 설정을 확인하세요.
문제 해결
데이터가 전혀 수집되지 않습니다
setServerUrl()을 지정했는지 확인하세요. 빠지면 전송되지 않습니다.AndroidManifest.xml의<application android:name=".MyApplication">등록을 확인하세요.INTERNET권한이 있는지 확인하세요.- logcat에
✅ WhatapAgent가 성공적으로 초기화되었습니다.가 찍히는지 확인하세요. setProjectKey와setPCode값이 발급받은 값과 일치하는지 확인하세요.- 프록시나 방화벽 설정으로 전송이 막히지 않았는지 확인하세요.
네트워크 데이터만 수집되지 않습니다
wrap()이 반환한 객체를 사용하고 있는지 확인하세요. 원본을 그대로 쓰면 수집되지 않습니다.setCollectNetwork(false)로 꺼두지 않았는지 확인하세요.- Volley는
onRequest(),onResponseExit(),onErrorExit()세 지점이 모두 들어가야 합니다.
메서드 추적 이벤트가 보이지 않습니다
- 측정 대상이 10ms 이상 걸리는지 확인하세요. 미만은 자동 제외됩니다.
- 전송은 10초 배치입니다. 최소 10초 이상 기다려 보세요.
[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 추가 |
| 모든 요청이 404 | setServerUrl 끝의 /로 경로가 //로 생성됨 | 끝의 / 제거 |
| 데이터가 거의 안 옴(약 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 플러 그인을 찾을 수 없을 때 다음 설정을 추가합니다.
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파일 내용