Applying the Android agent
Applying the WhaTap mobile agent to an Android app lets you collect screen loading, network calls, crashes, ANRs, resource usage, and WebView page performance. This document is for app developers and covers the whole procedure, from adding the library to verifying that data is collected. Collecting the web pages inside a WebView also requires work from the web owner. That is covered in Setting up WebView page collection.
Prerequisites
App settings
| Item | Value |
|---|---|
minSdk | 21 (Android 5.0) or later |
| External dependencies | None |
| AAR size | About 380 – 400KB |
Build tools
Android Gradle Plugin (AGP) 7.0 or later is required. The JDK and Gradle versions follow the requirements of the AGP you use.
| AGP | JDK | Gradle |
|---|---|---|
| 7.x | 11 or later | 7.0 or later |
| 8.x | 17 or later | 8.x |
| 9.x | 17 or later | 9.5 or later |
Versions required for WebView integration
To collect the WebView pages inside the app as well, both the Android agent and the browser agent must meet the required versions. If either one is lower, some items are not collected, and no error is left in either the app log or the web log.
| Component | Minimum version | Owner |
|---|---|---|
| Android agent | 2.3.5 | App developer |
| Browser agent | 3.2.0 | Web developer |
After finishing the setup, check the version actually running in the agent_version value on the dashboard. Replacing the file alone does not tell you whether it took effect. Check the version of the browser agent file in the banner of the first 4 lines.
head -4 whatap-browser-agent.js
Installing the agent
Proceed in the following order.
- Adding the library
- Manifest settings
- Initializing the SDK
- ProGuard settings
- Verifying the installation
Once you finish this far, the collection of screen transitions, resources, crashes, and ANRs starts. Network, WebView, and method tracing require extra work. For details, see Collected items.
1. Adding the library
Choose one of two methods. The Maven Central method is recommended.
Maven Central (recommended)
If mavenCentral() is already configured, you do not need to configure the repository again. Check the latest version in Maven Central and put it in place of <latest version>.
dependencies {
implementation("io.whatap.android:whatap-android-agent:<latest version>")
}
dependencies {
implementation 'io.whatap.android:whatap-android-agent:<latest version>'
}
If the repository declaration is missing, add it.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
Do not specify the version as + or latest.release. The agent version then changes on every build, which makes it hard to find the cause when a problem occurs. Check the latest version in Maven Central and specify it explicitly.
Adding the AAR file directly
Use this when you cannot reach Maven Central from a closed or internal network. Copy the AAR file you were given into app/libs/. The file name in the example below is only an example; the actual file name you receive may differ. Replace it with the actual name of the file you copied.
dependencies {
implementation(files("libs/whatap-agent-bom-complete.aar"))
}
dependencies {
implementation files('libs/whatap-agent-bom-complete.aar')
}
2. Manifest settings
Register the permissions and the Application class in 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>
If you do not register android:name, the Application class does not run and the SDK is not initialized. In an app with WebView integration, the bridge then returns "no agent." Web data falls back to standard HTTP transmission and the dashboard classifies it as a regular browser (BROWSER). If you turn on the debug log, Agent is not initialized is left in the app log.
3. Initializing the SDK
Call it only once in Application.onCreate(). If your app already has an Application subclass, add the code to that class's 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) // This is an Int. An L suffix causes a compile error
.setServerUrl("https://<collection server domain>/m") // trailing /m required
.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://<collection server domain>/m")
.setSampling(1.0)
.build(this);
}
}
There are three required values.
| Method | Description |
|---|---|
setProjectKey(String) | The issued project access key |
setPCode(int) | Project code. Type int |
setServerUrl(String) | Base URL of the collection server. No data is sent if it is missing |
For the other options, see Builder options.
Follow the initialization order.
All the APIs from Network library instrumentation onward must be called after build(this) finishes. If you call them earlier, the network instrumentation is ignored and an IllegalStateException occurs in custom logs.
4. ProGuard settings
An app with minifyEnabled false can skip this section. R8 does not run, so class and method names are not changed.
With minifyEnabled true, the consumer ProGuard rules inside the AAR are applied automatically. If you use only native instrumentation such as screens, network, crashes, ANRs, and resources, no separate keep rule is required.
Apps that use a WebView
The consumer rules bundled with the AAR do not include a rule that preserves the WebView bridge methods. The browser agent calls the bridge methods by name, so if R8 renames the @JavascriptInterface method names, every bridge call fails. In that case the window.whatapBridge object still remains, so the transmission path stays on the bridge. Because only the individual calls fail, the HTTP fallback does not work either and all WebView data is lost.
The symptom appears as "0 WebView records in release builds only, debug builds are fine." Add the following rules.
# WhaTap WebView bridge. JS calls the methods by name, so preserving the names is required
-keep class io.whatap.android.agent.webview.** { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keepattributes *Annotation*
-dontwarn io.whatap.android.**
If proguardFiles in build.gradle includes getDefaultProguardFile("proguard-android-optimize.txt") or getDefaultProguardFile("proguard-android.txt"), the AGP default file already contains the @JavascriptInterface rule. The rules above are required for projects that do not include the default file and use only custom rules. Even for projects that use the default file, adding them explicitly is recommended in case the proguardFiles configuration changes.
Screen name readability
The screen names and ScreenGroup names on the dashboard are built from the simple name of the Activity class. If your configuration lets R8 rename Activity and Fragment class names, screen names land as obfuscated values. No data is lost, but it is hard to tell which screen it is. Add the following rules if you need to. They are not required for the bridge to work.
-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. Verifying the installation
Run the app and check with logcat. All of the agent's logcat tags start with whatap.
adb logcat | grep -i whatap
If you use the Logcat window in Android Studio, enter tag:whatap in the filter.
Initialization succeeded
✅ WhatapAgent has been initialized successfully.
Transmission to the server succeeded
✅ [SpanExporter] HTTP 200 Success (123ms)
Data is sent every 10 seconds by default, so this log does not appear right after you start the app. Move between a few screens and check again. If this log appears, the HTTP request succeeded. To see whether the collection server processed the data properly, check whether the data appears on the dashboard.
Network request collection
🌐 [SpanExporter] Starting export for Span: GET (TraceId: ...)
- network_library: OkHttp / Volley / HttpUrlConnection
- http_request_method: GET
- status_code: 200
Collected items
When build(this) finishes normally, the following items are collected automatically.
- Activity and Fragment screen loading, ScreenGroup
- Crashes and ANRs
- User logs
- Resource usage such as CPU, memory, and temperature
- heartbeat
Turning off setCollectScreenLoading or setCollectHeartbeat stops the collection of only that item. Crash and ANR collection is always on when build(this) succeeds. There is no builder option to turn crash and ANR collection off or to choose the reporter type. useNativeCrashReporter() and usePLCrashReporter() are iOS-only and cannot be carried over to Android code.
The following three items are not turned on automatically and require extra work.
| Item | Required work |
|---|---|
| Network library request and response information | Call wrap() or onRequest() in Network library instrumentation, or apply automatic instrumentation |
| Method execution time | Call CallStackTracer in Tracing method execution time |
| WebView page load and performance information | Register the bridge in Setting up WebView page collection |
Builder options
All builder options are optional; if you do not specify one, the default value applies.
Basic settings
| Method | Default | Description |
|---|---|---|
setProjectKey(String) | (required) | The issued project access key |
setPCode(int) | (required) | Project code |
setServerUrl(String) | (required) | Base URL. /trace and /log are appended automatically |
setTraceServerUrl(String) | (automatic) | Use it only when specifying the trace endpoint directly |
setLogServerUrl(String) | (automatic) | Use it only when specifying the log endpoint directly |
setSampling(double) | 1.0 | Collection rate. On a 0.0 – 1.0 scale, so 50% is 0.5. An out-of-range value is ignored and the default is kept |
setSampling(double) is on a 0.0 – 1.0 scale. The browser agent's sampleRate is on a 0 – 100 scale, so you must not copy the value across. Putting the mobile 1.0 into the web as is makes the collection rate 1%.
Turning collected items on and off
| Method | Default | Description |
|---|---|---|
setCollectScreenLoading(boolean) | true | Screen loading collection |
setCollectNetwork(boolean) | true | Network collection |
setCollectHeartbeat(boolean) | true | Heartbeat every 30 seconds |
setScreenGroupDelaySeconds(int) | 0 | Screen group close delay (seconds). 0 disables it |
setExcludeLifecycleEventsFromScreenGroup(boolean) | true | Excludes lifecycle events from the screen group |
If you leave setCollectNetwork(false), all the wrap() and onRequest() calls in Network library instrumentation do nothing. To instrument the network, keep the default true.
The default of setScreenGroupDelaySeconds(int) is 0. With 0, the ScreenGroup closes as soon as the last task finishes. If it takes a long time from entering the screen until you call the WebView's loadUrl(), the WebView data may be grouped separately from the native screen. In that case, specify around 3 so the group does not close immediately.
Transmission and buffering
Adjust these when you need to reduce traffic.
| Method | Default | Description |
|---|---|---|
setFlushIntervalMs(long) | 10000 | Batch transmission interval (ms) |
setQueueSize(int) | 1000 | Memory queue size |
setMaxDiskBytes(int) | 512000 | Offline disk buffer limit (bytes) |
setMaxDiskFiles(int) | 5 | Number of disk buffer files. Rotated when exceeded |
setKeepAliveEnabled(boolean) | true | HTTP keep-alive |
setMaxConnections(int) | 5 | Maximum number of concurrent connections |
setDisconnectAfterSend(boolean) | false | Closes the connection immediately after sending |
WhatapAgent.Builder.newBuilder()
.setProjectKey("<PROJECT_ACCESS_KEY>")
.setPCode(12345)
.setServerUrl("https://<collection server domain>/m")
.setCollectHeartbeat(false)
.setFlushIntervalMs(60_000L)
.build(this)
The heartbeat and the resource sampler may be sent even in the background. To reduce traffic during low-usage hours, turn off heartbeat collection or increase the flush interval as in the example above.
setKeepAliveEnabled(boolean) and setMaxConnections(int) set the http.keepAlive and http.maxConnections JVM system properties globally. So if your app uses Volley or HttpURLConnection, their behavior changes too. Using the default values is recommended.
Specifying values for setUserId(String) and setSessionId(String) is not reflected in the values actually sent (as of the current release). The session ID is overwritten with a newly created value inside build(). The user ID is determined by a SharedPreferences value. To reflect the logged-in user, use WhatapExtra.set(WhatapExtra.USER_ID, ...) in Logged-in user ID.
Network header capture
Supported from agent 2.3.4. Earlier versions do not have the three methods below, so the build fails.
| Method | Default | Description |
|---|---|---|
setCaptureRequestHeaders(boolean) | false | Captures request headers as http.request.header.* attributes |
setCaptureResponseHeaders(boolean) | false | Captures response headers as http.response.header.* attributes |
setHeaderValueMaxLength(int) | 512 | Maximum header value length (bytes). Truncated when exceeded. 0 means unlimited |
When you turn the options on, every HTTP header that passes through OkHttp, HttpURLConnection, Volley, or Apache HttpClient is attached according to the following rules. These rules follow the OpenTelemetry Semantic Conventions.
| Rule | Example |
|---|---|
Lowercase the header name and convert - to _ | X-Request-ID → http.request.header.x_request_id |
| The same rule applies to response headers | Set-Cookie → http.response.header.set_cookie |
| Multi-value headers are joined with commas | a, b, c |
| When the length is exceeded, only the beginning is kept and marked | ...[TRUNCATED to <N>B] |
There is no automatic masking of sensitive headers.
Sensitive headers such as Authorization, Cookie, Set-Cookie, X-API-Key, and Proxy-Authorization are also captured with their values as is and sent to the collection server once you turn the option on. Be sure to review the capture scope before turning it on, and leave the default false if you do not need it.
Regardless of the options above, the following attributes are always attached automatically (2.3.4 or later).
| Attribute | Type | Description |
|---|---|---|
http.request.body.size | long | Request body size (bytes) |
http.response.body.size | long | Response body size (bytes) |
Network library instrumentation
Adding the AAR alone does not collect network data. You must add the code for the network library you use yourself. build(this) handles the instrumentation initialization automatically, so the app does not need to call init() separately.
OkHttp3 and 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()
You must use the client returned by wrap() for it to be instrumented. If you use the original base as is, nothing is collected.
If you need per-request attributes, use the wrap(client, Config) overload. See Per-request custom attributes.
Volley
import io.whatap.android.agent.instrumentation.volley.VolleyInstrumentation;
StringRequest req = new StringRequest(Request.Method.GET, url,
response -> {
// Handle the response
VolleyInstrumentation.onResponseExit(req, response);
},
error -> {
// Handle the error
VolleyInstrumentation.onErrorExit(req, error);
});
VolleyInstrumentation.onRequest(req);
queue.add(req);
Call onRequest() right before queue.add(), and onResponseExit() and onErrorExit() inside each callback. All three points must be in place.
Volley cannot tell the actual status code in the success callback, so the status_code of a successful response is uniformly recorded as 200. For failures, the exact value is taken from VolleyError.networkResponse.statusCode. For OkHttp and HttpURLConnection, the actual code is recorded as is.
HttpURLConnection
import io.whatap.android.agent.instrumentation.httpurlconnection.HttpUrlConnectionInstrumentation;
HttpURLConnection raw = (HttpURLConnection) url.openConnection();
HttpURLConnection conn = HttpUrlConnectionInstrumentation.wrap(raw);
try {
int code = conn.getResponseCode();
// handle the body
} finally {
conn.disconnect(); // wrap closes the span automatically
}
You must use the conn returned by wrap() for it to be instrumented. If you use the original raw as is, nothing is collected.
Apache HttpClient
There are two methods.
import io.whatap.android.agent.instrumentation.httpclient.ApacheHttpClientInstrumentation;
import io.whatap.android.agent.instrumentation.httpclient.InstrumentedDefaultHttpClient;
// Method 1. Wrap an existing HttpClient
HttpClient client = ApacheHttpClientInstrumentation.wrap(existingClient);
// Method 2. If you were using DefaultHttpClient, a drop-in replacement (2.2.6 or later)
HttpClient client2 = new InstrumentedDefaultHttpClient();
InstrumentedDefaultHttpClient provides attribute chaining.
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 was removed from the platform in Android 6.0, so the following is required in the build configuration.
android {
useLibrary("org.apache.http.legacy")
}
If you cannot apply this prerequisite, use a different network client such as OkHttp or HttpURLConnection.
Per-request custom attributes
From agent 2.2.6, you can attach extra attributes to each request. The following is an example based on 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);
For OkHttp and HttpURLConnection, pass a Config of the same form to the wrap(client, Config) overload. The attached values go into metrics.extras of the transmitted JSON and are displayed on the dashboard.
Automatic instrumentation (optional)
Applying the Gradle plugin injects bytecode to instrument network libraries and method execution time automatically. So you do not have to put the wrap() or onRequest() calls described in the previous sections into your code directly.
plugins {
id("io.whatap.android") version "<latest version>" apply false
}
plugins {
id("com.android.application")
id("io.whatap.android")
}
plugins {
id 'io.whatap.android' version '<latest version>' apply false
}
plugins {
id 'com.android.application'
id 'io.whatap.android'
}
To use the plugin JAR directly on a closed network, place it in libs/ or an internal Maven repository and refer to it from buildscript.
buildscript {
dependencies {
classpath(files("libs/whatap-android-plugin-<latest version>.jar"))
}
}
WebView bridge registration is not handled automatically even when you apply the plugin. Carry out Setting up WebView page collection separately.
Tracing method execution time
Measures how long a specific method takes to run. If WhatapAgent is already initialized, it is prepared automatically on the first call, so no separate initialization is needed.
Instrumentation patterns
Patterns 1 through 3 are supported from agent 2.2.8, and pattern 4 from 2.2.9.
import java.util.concurrent.Callable;
import io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer;
import io.whatap.android.agent.instrumentation.stacktrace.StackSpan;
// Pattern 1. try-with-resources. To wrap the whole method body
try (AutoCloseable s = CallStackTracer.scope("PaymentService", "charge")) {
// the logic to measure
}
// Pattern 2. No return value
CallStackTracer.measure("PaymentService", "charge", () -> doCharge());
// Pattern 3. With a return value, propagating checked exceptions
Receipt r = CallStackTracer.measure("PaymentService", "charge",
(Callable<Receipt>) () -> doChargeReturn());
// Pattern 4. StackSpan. When the start point and the end point are far apart
StackSpan span = CallStackTracer.start("PaymentService", "charge");
try {
// the logic to measure
span.end();
} catch (RuntimeException e) {
span.endWithError(e); // end with the error information
}
| Situation | Recommended pattern |
|---|---|
| Wrapping the whole method body in one line | Pattern 1, scope |
| Can be wrapped in a lambda, no return value | Pattern 2, measure(Runnable) |
| Can be wrapped in a lambda, with a return value | Pattern 3, measure(Callable) |
| Start and end are far apart, or it ends on another thread | Pattern 4, StackSpan |
StackSpan inherits AutoCloseable, so it can also be used with try-with-resources. Calling a span that has already ended or been canceled again does nothing. span.cancel() neither records nor sends anything.
Choosing what to instrument
You do not need to instrument every method. Instrumenting too many methods only increases unnecessary data.
| Priority | Target | Example |
|---|---|---|
| 1 | User action entry points | Activity.onCreate, Fragment.onViewCreated, click handlers |
| 2 | Business transaction boundaries | Payment, transfer, authentication, settlement |
| 3 | Network and DB call wrappers | Repository.findX, ApiService.callY |
| 4 | Heavy background work | Image processing, encryption/decryption, serialization |
| Not suitable | Getters, setters, simple conversions, short loops | Filtered out by the 10ms filter, only adding noise |
How it works
- If the execution time is under 10ms, it is not sent. This cannot be changed by configuration, and it applies to spans that ended with
endWithError()too. - It is sent in batches every 10 seconds.
- It works independently on background threads too. It is ThreadLocal-based.
- Recursive calls are safe. Just watch out for accumulating depth.
You can check the collection status in logcat as follows.
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 is normal behavior. It means methods under 10ms were filtered out.
Screen group settings
Screen transition data is collected just by calling build(this). The settings in this section are needed only when you want to group a flow that spans several Activities or Fragments into one group.
In agent 2.3.x, the previous startGroup(), addTask(), and endGroup() APIs were changed to startChain() and endChain().
Managing the flow yourself
Call startChain() on the starting screen and endChain() with the same taskId on the ending screen. If you leave taskId as null, it is generated automatically and you can look it up with 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)
}
}
}
Only one chain is kept at a time. If you call startChain() again before ending it, you lose the previous chain's taskId and endChain() does not work.
Chain state and close delay
isChainActive() lets you check whether a chain is currently in progress. If there is a short gap between screens, increase the close delay to keep them in one group. The default is 0, in which case the group closes as soon as the last screen ends.
val isChainActive = ChainView.getInstance().isChainActive()
WhatapAgent.Builder.newBuilder()
.setScreenGroupDelaySeconds(3)
.build(this)
Custom logs and global attributes
The APIs in this section must be called after build(this) completes. Calling them before initialization causes an IllegalStateException.
Custom logs
import io.whatap.android.agent.instrumentation.userlog.UserLogger;
UserLogger.print("Payment started");
Map<Object, Object> m = new HashMap<>();
m.put("event_name", "checkout");
m.put("amount", 12000);
UserLogger.print(m);
One print() call is one log record. Transmission happens on the setFlushIntervalMs cycle (10 seconds by default), so it does not appear on the dashboard right after the call.
The keys of print(Map) are sent as <key>.c, and print(String) is sent as messages.c. Adding event_name does not overwrite the value the SDK attaches.
Global attributes
You can add the same value in common to every span and log sent afterwards.
import io.whatap.android.agent.extra.WhatapExtra;
WhatapExtra.set("tenant_id", "acme");
WhatapExtra.remove("tenant_id");
The value is sent as metrics.extras.<key>.c. For per-request attributes, see Per-request custom attributes.
Logged-in user ID
WhatapExtra.USER_ID is a reserved key treated specially.
WhatapExtra.set(WhatapExtra.USER_ID, "user ID");
The transmission meta and the WebView bridge's getUserId() are updated together, so native screens and WebView screens are grouped under the same user. If you do not specify it, an anonymous per-device ID is generated automatically and kept.
Setting up WebView page collection
Web pages displayed in the app's WebView are collected by the browser agent, not by the native SDK. To link native screens and WebView pages into the same session, you must configure both the app and the web page.
The integration is complete only when all three steps are done. If any one of them is missing, data that cannot reach the bridge is sent over standard HTTP (the browser agent option webViewHttpFallback, default true). In that case the data itself is still collected. However, the dashboard classifies it as a regular browser (BROWSER) and it is not linked to the native session. The fallback is only a safety net against data disappearing; it does not mean the integration is complete.
The required versions are Android agent 2.3.5 or later and browser agent 3.2.0 or later. Check Versions required for WebView integration first.
Collected items by combination
These are the results verified on a physical Android 14 device against a real collection server.
| Item | Standard setup | Integrated setup, bridge connected | Integrated setup, bridge not reached |
|---|---|---|---|
| Page load | ○ | ○ | ○ (fallback) |
| Resources, AJAX | ○ | ○ | ○ (fallback) |
| Screen transitions | ○ | ○ | ○ (fallback) |
| 5 types of JS errors | ○ | ○ | ○ (fallback) |
| User events | ○ | ○ | ○ (fallback) |
| Session replay | ○ | ○ | ○ (fallback) |
| Core Web Vitals | ○ | ○ | ○ (fallback) |
| Custom logs | ○ | ✗ | ○ (fallback) |
| Dashboard classification | BROWSER | WEBVIEW | BROWSER |
| Project it lands in | Web page pcode | The app's mobile project | Web page pcode |
| Session integration with native | - | ○ | ✗ |
The standard setup is the state where the browser agent is initialized without isWebView, and the integrated setup is the state where it is initialized with isWebView: true. The 5 types of JS errors are uncaught errors, unhandled promise rejections, console.error(), noticeError(), and CSP violations.
Custom logs are not collected in the integrated setup.
The browser agent sends custom logs (enableCustomLog and logger) through the bridge method log, but the mobile bridge has no such interface, so they are silently dropped. It shows up as bridgeRequest:androidMethodMissing in debug mode. Leave pages that need custom logs in the standard setup, without specifying isWebView.
The project the data lands in
While the integration is working, web data lands in the app's mobile project. It does not go into the pcode or projectAccessKey set on the page. Data that goes through the bridge has its project identifier aligned with the app's setPCode and setProjectKey values just before upload. sessionID and userID are also unified to the native values. To see native screens and WebView screens in one session, both sets of data must be in the same project. For this, both components use the same specification.
| Case | Project to look in |
|---|---|
Standard setup (isWebView not set) | The web page's pcode |
| Integrated setup, bridge connected | The app's mobile project pcode |
| Integrated setup, bridge not reached (fallback) | The web page's pcode |
So the page's pcode and projectAccessKey are used only when sending over the fallback. They are not used while the integration is working normally. Still, you must enter valid values of a browser (RUM) project in case the bridge is broken. An arbitrary value or the app's mobile pcode also passes the format check at the initialization stage, but data sent by the fallback then lands nowhere.
If the iOS app and the Android app use different mobile projects, the WebView data of the same web page also lands separately per platform. You do not need to split the HTML per app. To see the full metrics of one web screen, look in both projects.
1. Checking the collection server address
This is the mobile agent's setServerUrl value. WebView data also goes out to this address, so it is especially important for the integration.
| Rule | Example |
|---|---|
Ends with scheme, host (port), then /m | https://collection-server-address:9443/m |
| A reverse proxy path may be included | https://internal-host:9443/22/m |
Do not add a trailing / | https://…/m/ becomes …/m//pageLoad and fails |
The Android agent corrects /m only for WebView data. If the address already ends with /m, it is not appended again. So if you leave out /m, WebView data still arrives but native data may be lost. Always include /m.
You may also put an internal reverse proxy in front and use a value that includes the proxy path. The agent appends the required path after this value as is, so you only need to configure the proxy to forward all of the following paths.
| Data | Path appended after <serverUrl> |
|---|---|
| Native traces, logs | /trace /log |
| WebView page load | /pageLoad |
| Resources, screen transitions, errors, events, memory | /resource /routeChange /onError /event /memory |
| Web Vitals | /webVitals /v2/webVitals |
| Session replay | /sessionreplay /v2/sessionreplay |
2. Registering the WebView
You must register each WebView instance. Be sure to register it on the UI thread before calling loadUrl(). For the bridge to work, Initializing the SDK and Manifest settings must be finished first.
First decide which method to use. The method depends on whether your app has a custom WebViewClient.
Existing WebViewClient | Registration method |
|---|---|
| None | setupWebView(webView, null) |
| Overrides only the 6 callbacks below | setupWebView(webView, myClient) |
| Overrides any other callback | attachBridgeOnly(webView) alone |
If you have a custom WebViewClient, be sure to check this before using setupWebView() or wrapWebViewClient().
The wrapper forwards only the following 6 callbacks to the existing client.
onPageStartedonPageFinishedonReceivedError(WebView, int, String, String)onPageCommitVisibleonLoadResourceshouldOverrideUrlLoading(WebView, String)
The callbacks it does not forward are shouldOverrideUrlLoading(WebView, WebResourceRequest), onReceivedError(WebView, WebResourceRequest, WebResourceError), onReceivedSslError, shouldInterceptRequest, onReceivedHttpError, onReceivedHttpAuthRequest, onRenderProcessGone, doUpdateVisitedHistory, onFormResubmission, and onReceivedClientCertRequest.
In particular, if you override shouldOverrideUrlLoading(WebView, WebResourceRequest), which is used on API 24 and later, URL interception, moving to an external app, and download interception do not work. If you were letting internal certificates through with onReceivedSslError, those pages do not load either. In such cases, use only attachBridgeOnly().
The following is the attachBridgeOnly() method, which is safe regardless of whether you have a custom WebViewClient.
import android.webkit.CookieManager;
import android.webkit.WebView;
import io.whatap.android.agent.webview.WhatapWebviewBridge; // Webview. the v is lowercase
private WhatapWebviewBridge whatapBridge; // instance field (do not share as static)
private boolean whatapAttached; // re-entry guard
private void initWhatap(WebView webView) {
if (whatapAttached) return;
// The Android default is false. The agent does not turn this on for you
webView.getSettings().setJavaScriptEnabled(true);
// The two lines below are used for session persistence and sampling continuity in the HTTP fallback path
webView.getSettings().setDomStorageEnabled(true);
CookieManager.getInstance().setAcceptCookie(true);
// applicationContext, not the Activity/Fragment this (the bridge keeps the Context permanently)
whatapBridge = new WhatapWebviewBridge(webView.getContext().getApplicationContext());
whatapBridge.attachBridgeOnly(webView); // does not touch the existing WebViewClient
whatapAttached = true;
}
private void openPage(WebView webView, String url) {
initWhatap(webView); // register first
webView.loadUrl(url); // then load
}
In Jetpack Compose, register it inside the factory lambda.
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) // register before loadUrl
loadUrl(url)
}
}
)
}
If you have no custom WebViewClient or use only the 6 callbacks above, replace attachBridgeOnly(webView) with setupWebView(webView, null) or setupWebView(webView, myClient). It connects the bridge and also collects the native page load span.
Even with attachBridgeOnly() alone, all web data — page loads, resources, errors, Web Vitals, session replay, and so on — is collected. The only thing not collected is the one native page load span the app creates. That data can be replaced by calling endPageLoad() on the web side.
To collect the native page load span as well while using attachBridgeOnly(), use inheritance instead of wrapWebViewClient(). This way the latest callbacks your app overrides are kept as they are.
import android.graphics.Bitmap;
import android.webkit.WebView;
import io.whatap.android.agent.webview.WhatapWebViewClient; // WebView. the V is uppercase
import io.whatap.android.agent.webview.WhatapWebviewBridge; // Webview. the v is lowercase
class MyWebViewClient extends WhatapWebViewClient {
MyWebViewClient(WhatapWebviewBridge bridge) { super(bridge); }
@Override
public void onPageStarted(WebView v, String url, Bitmap favicon) {
super.onPageStarted(v, url, favicon); // the super call is required
// existing logic
}
}
Add one line after attachBridgeOnly() in initWhatap() above. Using inheritance does not remove the need for attachBridgeOnly(). Bridge injection and page load tracking are different jobs, so both are needed.
whatapBridge.attachBridgeOnly(webView); // bridge injection (required)
webView.setWebViewClient(new MyWebViewClient(whatapBridge)); // adds page load tracking
Registration methods
| Method | Description |
|---|---|
attachBridgeOnly(WebView) | Connects only the bridge and does not change the WebViewClient. Use this method if you have a custom client |
setupWebView(WebView, WebViewClient) | Handles the bridge connection and the WebViewClient wrapping at once. Use it only when you use just the 6 callbacks above |
wrapWebViewClient(WebViewClient) | Returns a wrapper that adds page load tracking to an existing WebViewClient. Same restrictions as setupWebView() |
configureWebView(WebView) | A discontinued method. It behaves the same as attachBridgeOnly() but leaves out page load tracking. Do not use it |
WebView settings
| Setting | Why it is needed |
|---|---|
setJavaScriptEnabled(true) | The Android default is false and the agent does not turn it on for you |
setDomStorageEnabled(true) | Used for session persistence and sampling continuity in the HTTP fallback path |
CookieManager.setAcceptCookie(true) | Same as above |
You do not need to turn on third-party cookies. The cookies the browser agent uses are host-only cookies with no domain attribute and SameSite=Lax. It does not use credentials when sending data to the collection server either. CookieManager.setAcceptThirdPartyCookies() is not required.
Saving cookies before the app exits keeps the anonymous user ID of the fallback path after the app restarts.
CookieManager.getInstance().flush();
Registration rules
| Rule | If you do not follow it |
|---|---|
Register before loadUrl() | The page loads without the bridge and that page's data is lost |
| Call it on the UI thread | The bridge constructor creates a Handler and attachBridgeOnly() calls addJavascriptInterface(), so calling it from another thread propagates an exception to the app. For an asynchronous flow, wrap it as webView.post(() -> initWhatap(webView)) |
| Register only once per WebView instance | Session and page load tracking get out of sync and unnecessary resources remain. Even if you load several URLs in one WebView, registering once at first is enough. A re-entry guard such as whatapAttached in the example above is needed |
Pass applicationContext to the constructor | The bridge keeps the Context permanently, so passing an Activity's this leaves a reference after the screen is gone and causes a leak |
| Keep the bridge instance in a field and do not share it as static | If several WebViews share the same bridge, the screen linkage gets out of sync |
Do not call setWebViewClient() again after setupWebView() | The wrapper is replaced and page load tracking disappears. The bridge is unaffected, so it shows up as "web data arrives but there is no page load" |
| Register only WebViews that load your own content | Content loaded inside a registered WebView, including iframes, can access the bridge. Do not register WebViews that open external links, ads, or terms-of-service links |
You do not have to register in onCreate(). In a Fragment, register in onViewCreated(). If you create the WebView dynamically, register right after new WebView(context), and in Compose, inside the factory lambda. restoreState() also reloads the page, so register before it.
Do not call startDataUploadTimer(). This code remains in the old installation screen examples, but the current Android agent sends data immediately whenever a bridge method is called. So it does not use the internal queue that this timer drains. Calling startDataUploadTimer() only adds one more timer that checks an empty queue.
For builds that use minify, also check the keep rules in ProGuard settings.
3. Adding the browser agent script
Put the following script at the very top of the head tag of every HTML page the WebView opens. This is work carried out by the web owner.
The asynchronous method does not affect page load performance. However, ajax and error data that occur before the agent runs may not be collected.
<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>
To collect the data at page load time without any gaps, use the synchronous method. It may affect page load performance.
<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>
Copy the configuration keys and the script address exactly.
isWebViewis case-sensitive. If you write it asisWebview, it is ignored as an unknown key without a warning and is not linked to the native session.- The script address for WebView is
/rum/prod/v2/whatap-browser-agent.js. Thev1address has no bridge integration. - Do not change the fifth argument
'WhatapBrowserAgent'of the async snippet. That value is the name of the global object that holds the configuration. If you replace this value with a bundle path, the agent cannot find the configuration. In that case no message appears in the console either, and initialization does not happen. If you self-host the agent file, change the fourth argument, which specifies the bundle path.
| Config | Description |
|---|---|
projectAccessKey | Access key of the browser (RUM) project. Used for fallback transmission |
pcode | Project code of the browser (RUM) project. Used for fallback transmission |
sampleRate | Collection rate (%). On a 0 – 100 scale, which differs from the mobile agent's setSampling (0.0 – 1.0). Use 100 while verifying the setup |
isWebView | The switch that delegates data transmission to the mobile agent. Set it to true in the integrated setup |
proxyBaseUrl | The address to send data to on fallback transmission. Required even in the integrated setup. If it is not set, no data is sent at all |
You can check the projectAccessKey, pcode, and proxyBaseUrl values on the Management > Agent installation screen of the browser (RUM) project, filled in with the current project's values.
Check whether it is applied from the browser console of that page.
typeof window.WhatapBrowserAgent // 'object' means it is applied
Checking the WebView bridge
To check whether the bridge is actually injected, turn on WebView debugging and check from Chrome on your PC.
WebView.setWebContentsDebuggingEnabled(true); // be sure to remove it after checking
Go to chrome://inspect in Chrome on your PC, inspect that WebView, and check in the console.
typeof window.whatapBridge; // 'object'
typeof window.whatapBridge.pageLoad; // 'function'
typeof window.whatapBridge.getSessionId; // 'function'
If you use minify, check down to the method level.
Debug builds work fine even without keep rules, so check with the method above on minifyEnabled true builds as well. If window.whatapBridge shows as 'object' but pageLoad is 'undefined', the keep rule is missing. Check the ProGuard settings.
Troubleshooting
No data is collected at all
- Check that you specified
setServerUrl(). Nothing is sent if it is missing. - Check the
<application android:name=".MyApplication">registration inAndroidManifest.xml. - Check that the
INTERNETpermission is present. - Check that
✅ WhatapAgent has been initialized successfully.appears in logcat. - Check that the
setProjectKeyandsetPCodevalues match the values you were issued. - Check that transmission is not blocked by a proxy or firewall setting.
Only network data is not collected
- Check that you are using the object returned by
wrap(). Nothing is collected if you use the original. - Check that you did not turn it off with
setCollectNetwork(false). - For Volley, all three points —
onRequest(),onResponseExit(), andonErrorExit()— must be in place.
Method tracing events are not shown
- Check that the target takes 10ms or longer. Anything shorter is excluded automatically.
- Transmission is in 10-second batches. Wait at least 10 seconds.
- If you see the
[CallStackTracer] ⏭️ Skipping (< 10ms)log, it was filtered out normally.
If too many events are collected, reduce the instrumentation points. It is better not to instrument getters, setters, and simple conversion methods. See Choosing what to instrument.
WebView data is not collected
| Symptom | Cause | Action |
|---|---|---|
| WebView data seems to be nowhere | You are looking in the web page's pcode | Look in the app's mobile project |
| Data arrives but is classified as a regular browser | The WebView registration is later than the page load | Check the order: register, then load. The browser agent determines the WebView environment only once at initialization, so injecting the bridge after the load does not change the transmission path |
| Data arrives but is classified as a regular browser | The SDK initialization is missing. android:name is not registered or build(this) was not called | Check Manifest settings and Initializing the SDK |
| No data is collected at all | proxyBaseUrl is not set | Set it in the integrated setup too |
| 0 WebView records in release builds only | R8 renamed the @JavascriptInterface methods | Check the keep rules in ProGuard settings |
| Only native data is lost (WebView is fine) | setServerUrl does not end with /m | Add /m |
| Every request returns 404 | A trailing / in setServerUrl creates a // in the path | Remove the trailing / |
| Almost no data arrives (about 1%) | The mobile 1.0 was entered into the web sampleRate as is | The web uses a 0 – 100 scale. Specify sampleRate: 100 |
| The app's URL interception and SSL exception handling are broken | setupWebView() and wrapWebViewClient() do not delegate the latest WebViewClient callbacks | Switch to attachBridgeOnly() alone |
| Only custom logs are not collected | In the integrated setup the bridge does not handle log | Keep that page in the standard setup |
| WebView data is captured in a different group from the native screen | The WebView data arrives after the native screen loading finished and the ScreenGroup already closed | Delay the close with setScreenGroupDelaySeconds(3), or shorten the gap between entering the screen and loadUrl() |
| A new session is created on every app restart | Cookie saving is missing or DOM storage is disabled | Check CookieManager.flush() and setDomStorageEnabled(true) |
When the integration works correctly, native screens and WebView pages appear in the same session in chronological order on the Mobile dashboard. On the WebView dashboard you can check page loads, Web Vitals, and resource timing.
NoClassDefFoundError occurs at app start
If you added the AAR directly, check that the file you were given is about 380 – 400KB. If the file size differs greatly from this range, the file is not intact, so ask for it to be sent again. This problem does not occur when you use the Maven Central coordinates.
Build errors occur
Plugin not found error
Add the following configuration when the Gradle plugin from automatic instrumentation cannot be found.
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
Java version errors
First check the correspondence between AGP, JDK, and Gradle in Build tools. The class file has wrong version error occurs when the JDK version used for the build is too low. The JDK used for the build and the app's sourceCompatibility are separate things.
Namespace warning
You can ignore the Namespace 'io.whatap.android.agent' is used in multiple modules warning. It means the library is used in several modules and does not affect app execution.
Other build errors
- Check that the Gradle version and the AGP version meet the Build tools requirements.
- Check the network connection and configure a proxy if necessary.
- Clean & Rebuild the project.
- If the app crashes only in release builds, check for conflicts with other libraries' ProGuard rules.
Requesting support
Providing the following information when you request technical support helps resolve the issue faster.
- Project access key
- Agent version and Android SDK version
- Gradle version and Android Gradle Plugin version
- The full stack trace from logcat
- The contents of the
build.gradlefile