OneAgent SDK for Android
The OneAgent SDK for Android can be used to report additional details about the mobile user sessions in your app. The OneAgent SDK for Android enables you to create custom user actions, measure web requests, report errors, and tag specific users. This topic explains how to enable these capabilities.
You can use the OneAgent SDK either with the DESK Gradle plugin or the command line. You must use the OneAgent SDK when you want to manually instrument your Android app.
If you've disabled auto-start with the property DTXAutoStart
or you're using manual instrumentation instead of auto-instrumentation, then you have to start OneAgent manually. We recommend that you start OneAgent in the Application.onCreate method.
public class YourApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
DESK.startup(this, new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
... // additional configuration
.buildConfiguration());
}
}
DESK Managed
For the HTTPS communication OneAgent verifies the server certificate and the host name. OneAgent communication will fail if the verification steps aren't successfully completed.
If you don't have a root certificate for your Cluster ActiveGate, you must provide a certificate for SSL communication. This can be done by either providing the server certificate in the Network Security Configuration (for Android Target SDK 24 or later) or by providing a KeyStore
object with the certificate included.
If you want to use the Network Security Configuration feature, just add a domain-config
section to your network-security-config
XML.
For example:
<domain-config>
<domain includeSubdomains="true">your.domain.com</domain>
<trust-anchors>
<certificates src="@raw/your_server_certificate" />
</trust-anchors>
</domain-config>
If you want to support Android API levels below 24 or if you don't want to use the Network Security Configuration feature, then you have to provide a KeyStore
object. This object must hold the certificate chain of the Cluster ActiveGate you want to connect to.
For example:
KeyStore trusted = KeyStore.getInstance("BKS");
InputStream in = getResources().openRawResource(R.raw.mykeystore);
try {
trusted.load(in, "myverysecretpassword".toCharArray());
} finally {
in.close();
}
DESK.startup(this, new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
.withKeyStore(trusted)
.buildConfiguration());
Note:
If you're using BouncyCastle as your keystore instance, ensure that the keystore is generated with BouncyCastle 1.45. The BouncyCastle 1.46 file format is not compatible with older versions of Android. For more details see Android AOSP issue.
Also note that if you define both, a Network Security Configuration and a keystore, the keystore takes precedence over the Network Security Configuration.
You can also deactivate the certificate validation with the withCertificateValidation(boolean)
method. Please use this option with caution and not in production code as it dismantles the connections authenticity. Host name verification can't be deactivated.
DESK.startup(this, new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
.withCertificateValidation(false)
.buildConfiguration());
Configure OneAgent
Use the class DESKConfigurationBuilder to customize all advanced OneAgent settings with different API methods.
new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
.withHybridMonitoring(true)
.withMonitoredDomains("https://www.foo.com", "https://www.bla.com")
.buildConfiguration();
Instead of the API methods you can also specify some advanced settings within a property file. The default name for the property file is DESK.properties
and can be loaded with the method loadDefaultProperties(Context)
. It has to be stored in the assets
folder.
DESK.startup(this, new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
.loadDefaultProperties(context)
.buildConfiguration());
The following table shows the available advanced configuration settings that can be specified via a properties file or the loadProperties
methods.
Key | Type | Default | Description |
---|---|---|---|
DTXAllowAnyCert |
boolean | false | Defines the certificate policy. If set to true , all certificates are accepted. If set to false , valid certificates from known certificate authorities are accepted. You can still use self-signed certificates, but they are checked for validity. |
DTXSetCookiesForDomain |
string | To be able to use JavaScript Agent or JavaScript bridge, you need to set cookies for each instrumented domain or server, communicating with your application. Specify these domains and servers here.
You can specify domains, hosts, or IP addresses. Domains or sub-domains must start with a dot. Separate the list elements with a comma. |
|
DTXHybridApplication |
boolean | false | Hybrid application flag. Set the value to true for hybrid applications. |
DTXLogLevel |
string | Defines the log level. Debug logs can be activated with the value debug . |
|
DTXUserOptIn |
boolean | false | Defines if the privacy settings for data collection and crash reporting can be changed via the privacy API methods. |
Note:
If you use a combination of manual and auto-instrumentation, the auto-instrumentation injects a DESK.startup
call into the Application.onCreate
method. In this case you will lose your manual configuration, because the DESK.startup
call from the auto-instrumentation is called before your DESK.startup
call.
The property DTXAutoStart
allows you to deactivate the auto-start feature from the auto-instrumentation. Then you can define a manual DESK.startup
call in the Application.onCreate
method. In this case you can't use the DESK.properties
file for customizing the configuration, because it will be used by the auto-instrumentation to configure OneAgent correctly. Therefore, it is important that you call the loadDefaultProperties(Context)
method for your DESKConfigurationBuilder
. If you omit this step, the auto-instrumentation features will not work properly.
You can define your own custom user actions. First, you have to create them. Then you can enrich them with additional information and finally you have to close them. OneAgent discards all action-related monitoring data, when the action isn't closed.
You must call enterAction
to start each action and leaveAction
to close each action. Timing is measured automatically.
// start user action
DTXAction searchAction = DESK.enterAction("search");
// ...do some work here...
// end the action after the search completed
searchAction.leaveAction();
With a user action you can perform the following monitoring operations:
- report an event
- report a value
- report an error
- attach a web request to the user action
- create a child action
Child actions
Child actions are similar to parent actions. When the parent action is closed, OneAgent automatically closes all nested/child actions of the parent action.
Child actions are generated using the method DESK.enterAction(String, DTXAction)
.
// start parent user action
DTXAction searchAction = DESK.enterAction("search");
// ...do some work here...
// start child user action
DTXAction parseAction = DESK.enterAction("parse", searchAction);
// ...do some work here...
// end the child action
parseAction.leaveAction();
// ...do some work here...
// end the parent action
searchAction.leaveAction();
User action sample
The following code snippet shows a sample instrumentation of the fictional method search, which makes a web request to an instrumented server and parses the received result.
public boolean search(String query) {
// [1a] start outer/parent action
DTXAction searchAction = DESK.enterAction("search");
// [2] report your own metric
searchAction.reportValue("query", query);
URL url = null;
try {
url = new URL("http://www.example.com/?query=" + query);
} catch (MalformedURLException e) {
// [3] report an error
searchAction.reportError("invalid url", e);
// [1b] end outer action
searchAction.leaveAction();
return false;
}
HttpURLConnection connection = null;
WebRequestTiming timing = null;
try {
connection = (HttpURLConnection) url.openConnection();
// [4a] generate the correct tag, so the web request can be attached to the user action
searchAction.tagRequest(connection);
// [4b] generate a WebRequestTiming instance (and attach the web request event to the user action "search")
timing = DESK.getWebRequestTiming(connection);
// [5] Call startWebRequestTiming() to begin the timing, and then handle the input stream from the connection
timing.startWebRequestTiming();
InputStream stream = connection.getInputStream();
readStream(stream);
// [6] Once done reading, timing can stop
timing.stopWebRequestTiming();
// [7a] start inner action
DTXAction parseAction = DESK.enterAction("parse result", searchAction);
parseResult();
// [7b] end inner action
parseAction.leaveAction();
return true;
} catch (IOException e) {
if(timing != null) {
// [6b] monitor the web request with the communication error
timing.stopWebRequestTiming(url, -1, e.toString());
}
if(connection != null) {
connection.disconnect();
}
return false;
}
finally {
// [1b] end outer action
searchAction.leaveAction();
}
}
The report event feature allows you to report the time point of a specific event. The event must be part of a user action.
// start user action
DTXAction searchAction = DESK.enterAction("search");
searchAction.reportEvent("query completed");
// end the action after the search completed
searchAction.leaveAction();
The report value feature allows you to report your own metrics. These metrics must be part of a user action.
The OneAgent SDK allows you to report int
, double
and String
values.
// start user action
DTXAction searchAction = DESK.enterAction("search");
searchAction.reportValue("query", query);
// end the action after the search completed
searchAction.leaveAction();
The reporting error feature is different from the reporting value feature in that it is specifically identified as an error type of event.
The OneAgent SDK allows you to report error codes with the method reportError(String, int)
or handled exceptions with the method reportError(String, Throwable)
.
// start user action
DTXAction searchAction = DESK.enterAction("search");
try {
// ... do something that might throw an exception ...
} catch (Exception e) {
// report the error
searchAction.reportError("search error", e);
}
// end the action after the search completed
searchAction.leaveAction();
You can also report errors as stand-alone error event.
The OneAgent SDK allows you to report error codes with the method reportError(String, int)
or handled exceptions with the method reportError(String, Throwable)
.
try {
// ... do something that might throw an exception ...
} catch (Exception e) {
// report the error
DESK.reportError("search error", e);
}
To monitor a web request the DESK tag has to be placed on the HTTP request. The tag is needed to correlate the server-side monitoring data to the corresponding mobile web request. Additionally, the timing values from the mobile side have to be measured.
Note:
You should not manually and auto-instrument the same web requests. This behavior can lead to incorrect monitoring data. Auto-instrumentation automatically instruments HttpUrlConnection
, HttpClient
(deprecated), and OkHttp3
web requests.
HttpURLConnection
URL url = new URL("http://www.example.com");
HttpURLConnection conn = null;
WebRequestTiming timing = null;
// [1] First, create an action
DTXAction webAction = DESK.enterAction("search request");
try {
conn = (HttpURLConnection) url.openConnection();
// [2] Once the connection object is obtained, tag it automatically and receive a WebRequestTiming instance
timing = DESK.getWebRequestTiming(conn);
// [3] Call startWebRequestTiming to begin the timing, and then handle the input stream from the connection
timing.startWebRequestTiming();
InputStream stream = new BufferedInputStream(conn.getInputStream());
readStream(stream);
// [3] Once done reading, timing can stop
timing.stopWebRequestTiming();
} catch (Exception e) {
if(timing != null) {
// [4a] call stopWebRequestTiming to monitor the communication error
timing.stopWebRequestTiming(url, -1, e.toString());
}
// user-defined exception handling
} finally {
if (conn != null) {
conn.disconnect();
}
// [5] Lastly, leave the action
webAction.leaveAction();
}
OkHttp3 synchronous
URL url = new URL("http://www.example.com");
// [1] First, create an action
DTXAction webAction = DESK.enterAction("search request");
// [2] Generate a new request tag
String requestTag = webAction.getRequestTag();
// [3] Associate the timing with the tag
WebRequestTiming timing = DESK.getWebRequestTiming(requestTag);
// [4] Fetch the request tag header name
String requestTagHeaderName = DESK.getRequestTagHeader();
// Define your okhttp3 request, this varies greatly depending on your implementation
Request request = new Request.Builder()
.url(url)
// Define your headers for the okhttp3 request
.addHeader(yourKey1, yourValue1)
.addHeader(yourKey2, yourValue2)
// [5] Add the desk header key and the tag value to the request
.addHeader(requestTagHeaderName, requestTag)
.build();
try {
// [6] Call startWebRequestTiming to begin the timing, and then handle the response body from the okhttp3 call
timing.startWebRequestTiming();
Response response = client.newCall(request).execute();
// handle response
String body = response.body().string();
// [7] Once done reading, timing can stop
timing.stopWebRequestTiming(url, response.code(), response.message());
} catch (IOException e) {
// [7a] call stopWebRequestTiming to monitor the communication error
timing.stopWebRequestTiming(url, -1, e.toString());
// user-defined exception handling
}
finally {
// [8] Lastly, leave the action
webAction.leaveAction();
}
OkHttp3 asynch
final URL url = new URL("http://www.example.com");
// [1] First, create an action
final DTXAction webAction = DESK.enterAction("search request");
// [2] Generate a new request tag
String requestTag = webAction.getRequestTag();
// [3] Associate the timing with the tag
final WebRequestTiming timing = DESK.getWebRequestTiming(requestTag);
// [4] Fetch the request tag header name
String requestTagHeaderName = DESK.getRequestTagHeader();
// Define your okhttp3 request, this varies greatly depending on your implementation
Request request = new Request.Builder()
.url(url)
// Define your headers for the okhttp3 request
.addHeader(yourKey1, yourValue1)
.addHeader(yourKey2, yourValue2)
// [5] Add the desk header key and the tag value to the request
.addHeader(requestTagHeaderName, requestTag)
.build();
// [6] Call startWebRequestTiming to begin the timing, and then handle the response body from the okhttp3 call
timing.startWebRequestTiming();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// [7a] call stopWebRequestTiming to monitor the communication error
timing.stopWebRequestTiming(url, -1, e.toString());
// user-defined exception handling
// [8] Lastly, leave the action
webAction.leaveAction();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
// handle response
String body = response.body().string();
// [7] Once done reading, timing can stop
timing.stopWebRequestTiming(url, response.code(), response.message());
// [8] Lastly, leave the action
webAction.leaveAction();
}
});
DESK enables you to tag each user of your mobile apps with a unique user name. This enables you to search and filter specific user sessions and analyze individual user behavior over time. The steps below explain how to manually tag an individual user via the DESK API.
DESK.identifyUser("john.doe@example.com");
The OneAgent SDK enables you to dynamically adjust data-privacy settings so that you can build your apps in compliance with GDPR data-privacy regulations. To activate this feature, enable the DTXUserOptIn
flag.
The privacy API methods allow you to dynamically activate/deactivate crash reporting and to change the data-collection level based on the individual preferences of your end users. Each end user can select from three data-privacy levels:
off
: OneAgent won't capture any monitoring data.performance
: OneAgent will only capture anonymous performance data. Monitoring data that can be used to identify individual users (for example, user tags or custom values), will not be captured.user behavior
: OneAgent will capture both performance and user data. In this mode OneAgent recognizes and reports on users who re-visit in future sessions.
When OneAgent starts for the first time, OneAgent deactivates crash reporting and sets the data-collection level to off
. You should change the privacy settings (based on each user's individual preference), when your app starts for the first time. OneAgent does not provide a privacy settings dialog or any similar UI component. You must integrate a privacy dialog into your app. We recommend that you display the privacy dialog before your app is displayed and then apply the user's privacy preference. You should also enable your users to change their privacy settings in the future.
OneAgent persists the privacy setting and automatically applies it when the app is restarted. Each time the user changes the data-collection level, a new session with the new privacy settings will be generated by OneAgent. Do not wrap this API method with a user action. Otherwise, OneAgent can't attach the user action to the correct session.
With the method DESK.setCrashReportingOptedIn(boolean)
you can activate or deactivate crash reporting. The data collection level can be changed with the method DESK.setDataCollectionLevel(DataCollectionLevel)
.
You can retrieve the privacy settings with the methods DESK.getDataCollectionLevel
and DESK.isCrashReportingOptedIn
. But you can't use the privacy API methods before OneAgent is started. Otherwise OneAgent will throw an exception.
If the HTTP requests of OneAgent do not fulfil the security requirements of your server infrastructure, you can modify the HTTP headers of OneAgent with the DESK.setBeaconHeaders(Map<String, String>)
method. This feature allows you to add an Authorization
header to the HTTP requests and immediately reconnect to the Cluster ActiveGate, when the token has expired. To delete the old headers call DESK.setBeaconHeaders(null)
.
Basic Authorization
When the authorization information is already available at the app start, you should call the DESK.setBeaconHeaders
method before the startup DESK.startup
method. Then it is guaranteed that every HTTP request of the OneAgent will have the correct headers.
Map<String, String> headers = new HashMap<>();
headers.put("Cookie", "n1=v1; n2=v2");
headers.put("ExampleHeader", "ExampleValue");
headers.put("Authorization", basicAuthorization(username, password));
DESK.setBeaconHeaders(headers);
DESK.startup(this, new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
.buildConfiguration());
If the authorization information is not available at the app start, you should call the DESK.setBeaconHeaders
method, when the information is available. The startup DESK.startup
method should still be called in the Application.onCreate
method to track the correct start time. OneAgent will be automatically deactivated, when the server sends an invalid status code response. The DESK.setBeaconHeaders
method will activate OneAgent and will immediately reconnect to the Cluster ActiveGate.
Authorization with a token
If you use an authorization procedure, which requires you to regularly update a token, then you should add a CommunicationProblemListener
. The listener must be added via the DESKConfigurationBuilder
in the DESK.startup
method.
DESK.startup(this, new DESKConfigurationBuilder("<YourApplicationID>", "<ProvidedBeaconURL>")
.withCommunicationProblemListener(new YourDESKListener())
.buildConfiguration());
When you use a CommunicationProblemListener, the OneAgent communication behavior is slightly different from the normal behavior. If the Cluster ActiveGate reacts with an invalid status code, like 403 Forbidden
, OneAgent won't reconnect to the server. Instead OneAgent will wait until you have specified the correct headers with the method DESK.setBeaconHeaders
. In this case OneAgent will notify the CommunicationProblemListener
asynchronously in a background thread via the onFailure(int, String, String)
interface method. The following code snippet show a sample implementation for the CommunicationProblemListener
interface:
public class YourDESKListener implements CommunicationProblemListener{
@Override
public void onFailure(int responseCode, String responseMessage, String body) {
String token = refreshToken();
DESK.setBeaconHeaders(generateAuthorizationHeader(token));
}
@Override
public void onError(Throwable throwable) {
//do nothing
}
}
The interface method onError(Throwable)
is asynchronously called, in case a communication problem occurs, for example a connection timeout, or a SSL handshake error. In this case, OneAgent will wait for certain time and then reconnect to the Cluster ActiveGate. Normally you don't have to react on this callback method.