Skip to main content

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​

ItemValue
minSdk21 (Android 5.0) or later
External dependenciesNone
AAR sizeAbout 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.

AGPJDKGradle
7.x11 or later7.0 or later
8.x17 or later8.x
9.x17 or later9.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.

ComponentMinimum versionOwner
Android agent2.3.5App developer
Browser agent3.2.0Web 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.

  1. Adding the library
  2. Manifest settings
  3. Initializing the SDK
  4. ProGuard settings
  5. 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.

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>.

app/build.gradle.kts
dependencies {
implementation("io.whatap.android:whatap-android-agent:<latest version>")
}
app/build.gradle
dependencies {
implementation 'io.whatap.android:whatap-android-agent:<latest version>'
}

If the repository declaration is missing, add it.

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

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.

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 settings​

Register the permissions and the Application class in AndroidManifest.xml.

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().

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) // 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)
}
}
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://<collection server domain>/m")
.setSampling(1.0)
.build(this);
}
}

There are three required values.

MethodDescription
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.

Caution

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.

app/proguard-rules.pro
# 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.

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. 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.

ItemRequired work
Network library request and response informationCall wrap() or onRequest() in Network library instrumentation, or apply automatic instrumentation
Method execution timeCall CallStackTracer in Tracing method execution time
WebView page load and performance informationRegister 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​

MethodDefaultDescription
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.0Collection 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
Caution

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​

MethodDefaultDescription
setCollectScreenLoading(boolean)trueScreen loading collection
setCollectNetwork(boolean)trueNetwork collection
setCollectHeartbeat(boolean)trueHeartbeat every 30 seconds
setScreenGroupDelaySeconds(int)0Screen group close delay (seconds). 0 disables it
setExcludeLifecycleEventsFromScreenGroup(boolean)trueExcludes 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.

MethodDefaultDescription
setFlushIntervalMs(long)10000Batch transmission interval (ms)
setQueueSize(int)1000Memory queue size
setMaxDiskBytes(int)512000Offline disk buffer limit (bytes)
setMaxDiskFiles(int)5Number of disk buffer files. Rotated when exceeded
setKeepAliveEnabled(boolean)trueHTTP keep-alive
setMaxConnections(int)5Maximum number of concurrent connections
setDisconnectAfterSend(boolean)falseCloses 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.

Caution

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.

MethodDefaultDescription
setCaptureRequestHeaders(boolean)falseCaptures request headers as http.request.header.* attributes
setCaptureResponseHeaders(boolean)falseCaptures response headers as http.response.header.* attributes
setHeaderValueMaxLength(int)512Maximum 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.

RuleExample
Lowercase the header name and convert - to _X-Request-ID → http.request.header.x_request_id
The same rule applies to response headersSet-Cookie → http.response.header.set_cookie
Multi-value headers are joined with commasa, b, c
When the length is exceeded, only the beginning is kept and marked...[TRUNCATED to <N>B]
Danger

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).

AttributeTypeDescription
http.request.body.sizelongRequest body size (bytes)
http.response.body.sizelongResponse 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.

app/build.gradle.kts
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.

Project-level build.gradle.kts
plugins {
id("io.whatap.android") version "<latest version>" apply false
}
app/build.gradle.kts
plugins {
id("com.android.application")
id("io.whatap.android")
}
Project-level build.gradle
plugins {
id 'io.whatap.android' version '<latest version>' apply false
}
app/build.gradle
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.

Project-level build.gradle.kts
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
}
SituationRecommended pattern
Wrapping the whole method body in one linePattern 1, scope
Can be wrapped in a lambda, no return valuePattern 2, measure(Runnable)
Can be wrapped in a lambda, with a return valuePattern 3, measure(Callable)
Start and end are far apart, or it ends on another threadPattern 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.

PriorityTargetExample
1User action entry pointsActivity.onCreate, Fragment.onViewCreated, click handlers
2Business transaction boundariesPayment, transfer, authentication, settlement
3Network and DB call wrappersRepository.findX, ApiService.callY
4Heavy background workImage processing, encryption/decryption, serialization
Not suitableGetters, setters, simple conversions, short loopsFiltered 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.

Note

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)
}
}
}
Caution

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​

Caution

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.

  1. Checking the collection server address
  2. Registering the WebView
  3. Adding the browser agent script

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.

ItemStandard setupIntegrated setup, bridge connectedIntegrated 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 classificationBROWSERWEBVIEWBROWSER
Project it lands inWeb page pcodeThe app's mobile projectWeb 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.

Caution

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.

CaseProject to look in
Standard setup (isWebView not set)The web page's pcode
Integrated setup, bridge connectedThe 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.

RuleExample
Ends with scheme, host (port), then /mhttps://collection-server-address:9443/m
A reverse proxy path may be includedhttps://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.

DataPath 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 WebViewClientRegistration method
NonesetupWebView(webView, null)
Overrides only the 6 callbacks belowsetupWebView(webView, myClient)
Overrides any other callbackattachBridgeOnly(webView) alone
Danger

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.

  • onPageStarted
  • onPageFinished
  • onReceivedError(WebView, int, String, String)
  • onPageCommitVisible
  • onLoadResource
  • shouldOverrideUrlLoading(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

MethodDescription
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

SettingWhy 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

RuleIf 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 threadThe 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 instanceSession 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 constructorThe 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 staticIf 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 contentContent 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.

Note

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.

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>

To collect the data at page load time without any gaps, use the synchronous method. It may affect page load performance.

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>
Caution

Copy the configuration keys and the script address exactly.

  • isWebView is case-sensitive. If you write it as isWebview, 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. The v1 address 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.
ConfigDescription
projectAccessKeyAccess key of the browser (RUM) project. Used for fallback transmission
pcodeProject code of the browser (RUM) project. Used for fallback transmission
sampleRateCollection rate (%). On a 0 – 100 scale, which differs from the mobile agent's setSampling (0.0 – 1.0). Use 100 while verifying the setup
isWebViewThe switch that delegates data transmission to the mobile agent. Set it to true in the integrated setup
proxyBaseUrlThe 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'
Caution

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​

  1. Check that you specified setServerUrl(). Nothing is sent if it is missing.
  2. Check the <application android:name=".MyApplication"> registration in AndroidManifest.xml.
  3. Check that the INTERNET permission is present.
  4. Check that ✅ WhatapAgent has been initialized successfully. appears in logcat.
  5. Check that the setProjectKey and setPCode values match the values you were issued.
  6. Check that transmission is not blocked by a proxy or firewall setting.

Only network data is not collected​

  1. Check that you are using the object returned by wrap(). Nothing is collected if you use the original.
  2. Check that you did not turn it off with setCollectNetwork(false).
  3. For Volley, all three points — onRequest(), onResponseExit(), and onErrorExit() — must be in place.

Method tracing events are not shown​

  1. Check that the target takes 10ms or longer. Anything shorter is excluded automatically.
  2. Transmission is in 10-second batches. Wait at least 10 seconds.
  3. 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​

SymptomCauseAction
WebView data seems to be nowhereYou are looking in the web page's pcodeLook in the app's mobile project
Data arrives but is classified as a regular browserThe WebView registration is later than the page loadCheck 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 browserThe SDK initialization is missing. android:name is not registered or build(this) was not calledCheck Manifest settings and Initializing the SDK
No data is collected at allproxyBaseUrl is not setSet it in the integrated setup too
0 WebView records in release builds onlyR8 renamed the @JavascriptInterface methodsCheck the keep rules in ProGuard settings
Only native data is lost (WebView is fine)setServerUrl does not end with /mAdd /m
Every request returns 404A trailing / in setServerUrl creates a // in the pathRemove the trailing /
Almost no data arrives (about 1%)The mobile 1.0 was entered into the web sampleRate as isThe web uses a 0 – 100 scale. Specify sampleRate: 100
The app's URL interception and SSL exception handling are brokensetupWebView() and wrapWebViewClient() do not delegate the latest WebViewClient callbacksSwitch to attachBridgeOnly() alone
Only custom logs are not collectedIn the integrated setup the bridge does not handle logKeep that page in the standard setup
WebView data is captured in a different group from the native screenThe WebView data arrives after the native screen loading finished and the ScreenGroup already closedDelay the close with setScreenGroupDelaySeconds(3), or shorten the gap between entering the screen and loadUrl()
A new session is created on every app restartCookie saving is missing or DOM storage is disabledCheck 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.

settings.gradle
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.gradle file