Android

The LogRocket SDK for Android allows you to capture session replays, network requests and logs from Android applications.

Quick Start

Register with LogRocket

Go to https://logrocket.com/signup to create a free trial account. If you already have a LogRocket account, you can use your existing account. All LogRocket accounts include 1,000 free mobile sessions.

Configure Gradle

The LogRocket Android SDK is distributed through Maven. To include the SDK in your application add our Maven repository and declare the dependency in your app/build.gradle file. New releases of the LogRocket Native SDKs are catalogued on our Mobile SDK Changelog (the current release is 0.23.2).

repositories {
    // Add this declaration to any existion repositories block.
    maven { url "https://storage.googleapis.com/logrocket-maven/" }
}

dependencies {
    // Add this declaration to any existing dependencies block.
    implementation "com.logrocket:logrocket:0.23.2"
}

Initializing the SDK

The LogRocket Android SDK must be initialized with an Application instance and a fully attached Context for the application. The simplest way to initialize the SDK is from a custom Application class in the attachBaseContext method.

Replace <APP_SLUG> with your LogRocket application slug, located in our dashboard's quick start guides.

import android.app.Application;
import com.logrocket.core.SDK;

public class App extends Application {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);

    SDK.init(
      this,
      base,
      options -> {
        options.setAppID("<APP_SLUG>");
      }
    );
  }
}

If you did not already have an existing custom Application class, add the name of the class as the android:name property for the <application> node in AndroidManifest.xml:

<application
  android:name=".App"
  ..>

Supported Android Versions

The LogRocket Android SDK supports Android API 19 (KitKat) and up.

Identifying Users

Associate a user identifier with the active user. You can use any string that uniquely identifies the user of your application such as a database ID, an email, or a username.

SDK.identify("28dvm2jfa");

User traits can be added with a map as the second argument to SDK.identify.

Map<String, String> userData = new HashMap<>();

userData.put("name", "Jane Smith");
userData.put("email", "[email protected]");
userData.put("subscriptionPlan", "premium");

SDK.identify("28dvm2jfa", userData);

For more detailed information on User Identication at LogRocket check out our identification reference documentation.

Visual Capture & Replay

The LogRocket Android SDK captures what your Application is displaying to the user for our Session Replay system.

Performance

Limiting potential user experience impact when using the LogRocket Android SDK is a high priority for us at LogRocket. The system that captures the Application's current screen must run in the UI Thread and could potentially block the UI from updating. This process is highly optimized to limit the work our SDK does in the UI Thread and our average time spent in this thread is well under 16ms, the target "frame time" to keep a User Interface updating at 60FPS. This process runs, on average, once every second to provide a reasonably paced video without "stealing" more than one frame per second from the UI.

Privacy

The LogRocket Android SDK has three approaches to handling sensitive data that should not be captured for replay: by View Tag, explicitly through SDK.redactView, and by stopping view capture completely.

By View Tag

By default the SDK will automatically skip capturing any view with a tag set to the string "lr-hide". Additional tags can be added when configuring the SDK with the options.addRedactionTag(Object tag) method. A non-String Object can be provided for programmatic checks on tags, our SDK will run tagObject.equals(view.getTag()) against this object.

Through SDK.redactView

If you have access to a View instance it can be explicitly redacted by calling SDK.redactView(view). Views that have been redacted this way are "forgotten" if the instance is destroyed it must be explicitly redacted again.

Pausing View Capture

To completely disabled the view capture system call SDK.pauseViewCapture(). If a capture is already in progress it will not be stopped, but no view captures will be created until the system is resumed with SDK.unpauseViewCapture().

Images

Images displayed to the user are currently captured "on-demand" the first time a User in a Session sees the image. To limit CPU and Bandwidth we have placed limits on the number of size of images we will capture: a single cannot be over 15KB, and the total size of all images in a single frame cannot exceed 150KB. The size of the image is determined after it is compressed to an efficient format for transmission.

One current limitation is with Vector Graphics: we do not currently capture Vector Graphics (such as SVGs) at any point. These are commonly found as Icons and may not appear in your session replay.

Selectors

Mobile sessions can be filtered on touch events through the Clicked filter. Currently only the "on selector" form of this filter is supported. Selectors are generated from hierarchical view details and support a subset of the CSS specification syntax.

Components of a View Selector

Component

Value

Note

Element

view.getClass().getSimpleName()

#resource-id

view.getResources().getResourceName(view.getId())

Only the portion after id/ is captured.

.tag-text

view.getTag()

Only instances of String are captured.

For example, an EditText with this definition:

  <EditText
    android:id="@+id/user_email"
    android:tag="test-email-input"
    ... />

the following Selector would be generated:

EditText.test-email-input#user_email

Querying Selectors

For each touch event, a hierarchy of views is computed from the touched view to the top view of the Activity. The following forms of querying are supported:

  • Single element: Button -- will match any Button element.
  • Specific ID: #signin -- will match any view with a Resource ID of
  • Specific Tag: .tag-value -- will match any view with that tag text
  • Combined selectors: an element may be followed by either or both of a Specific ID and Specific Tag.
  • Nesting: multiple selectors may be separated by a space, and will enforce a mathing hierarchy.
  • Child Of: a selector in the form of a > b matches any view b that is a direct child of the view a.

Accessing the Session URL

Integration with third party services can be accomplished by retrieving the Session URL and adding it as context to the third party library. In the Android SDK, the session URL can be accessed with the SDK.getSessionURL method. Session URLs are only made available when our backend has accepted the session, which can take 1-5 seconds from when the SDK is initialized.

SDK.getSessionURL(url -> {
  // Use the accepted Session URL.
});

Capturing Network Requests

The LogRocket Android SDK does not automatically capture Network Requests in your application. A fluent builder interface is provided by the SDK to simplify capturing network requests, and we have provided a sample OkHttp interceptor for capturing requests from the OkHttp library.

URL endpoint = new URL("https://example.com/");

IResponseBuilder responseBuilder =
    SDK.newRequestBuilder()
        .setUrl(endpoint.toString())
        .setMethod("GET")
        .capture();
long startTime = System.currentTimeMillis();

try {
  HttpURLConnection conn = (HttpURLConnection) endpoint.openConnection();

  // The body field on both the Request and Response builders must be a String. Correctly
  // reading
  // the response not represented here. Content must be sanitized before registering to the
  // response builder.
  String body = readResponseBody(conn);

  responseBuilder
      .setStatusCode(conn.getResponseCode())
      .setDuration(System.currentTimeMillis() - startTime)
      // The body field on both the Request and Response builders must be a String. Correctly
      // reading
      // the response not represented here. Content must be sanitized before registering to the
      // response builder.
      .setBody(readRedactedResponseBody(conn))
      // Redact sensitive fields before adding to the response builder. The HttpURLConnection
      // provides
      // an array of values for each key, but we only accept a single value.
      .setHeaders(flattenAndRedactHeaders(conn.getHeaderFields()))
      .capture();

  // Work with your response!
} catch (IOException err) {
  // Network failures are represented as Responses with a status code of 0. If a captured
  // request does not have a matching response captured it will appear as an unending request
  // during session playback.

  responseBuilder
      .setStatusCode(0)
      .setDuration(System.currentTimeMillis() - startTime)
      .capture();

  // Re-surface the actual IOException
  throw err;
}
import android.util.Log;
import com.logrocket.core.SDK;
import com.logrocket.core.network.IResponseBuilder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okio.Buffer;
import okio.BufferedSource;

/**
 * This example interceptor was built against OkHttp3 v4.9.0.
 *
 * @link https://square.github.io/okhttp/interceptors/
 */
public class NetworkInterceptor implements Interceptor {
  @Override
  public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    IResponseBuilder responseBuilder = this.captureRequest(request);

    try {
      Response response = chain.proceed(request);

      this.captureResponse(responseBuilder, response);

      return response;
    } catch (IOException e) {
      // Network failures are represented as Responses with a status code of 0. If a captured
      // request does not have a matching response captured it will appear as an unending request
      // during session playback.

      this.captureResponse(
          responseBuilder,
          new Response.Builder()
              .request(chain.request())
              .message("Failed request")
              .protocol(Protocol.HTTP_2)
              .code(0)
              .build());

      throw e;
    }
  }

  private IResponseBuilder captureRequest(Request request) {
    String body = "";

    if (request.body() != null) {
      Request copy = request.newBuilder().build();
      Buffer buffer = new Buffer();

      try {
        Objects.requireNonNull(copy.body()).writeTo(buffer);
        body = buffer.readUtf8();
      } catch (Throwable th) {
        Log.e("LogRocket-Interceptor", "Failed to read request body", th);
      }
    }

    return SDK.newRequestBuilder()
        .setUrl(request.url().toString())
        .setMethod(request.method())
        .setHeaders(collectHeaders(request.headers()))
        .setBody(body)
        .capture();
  }

  private void captureResponse(IResponseBuilder builder, Response response) {
    String body = "";

    if (response.body() != null) {
      try {
        BufferedSource source = Objects.requireNonNull(response.body()).source();
        source.request(Long.MAX_VALUE);
        Buffer buffer = source.getBuffer();
        body = buffer.clone().readString(StandardCharsets.UTF_8);
      } catch (Throwable th) {
        Log.e("LogRocket-Interceptor", "Failed to read response body", th);
      }
    }

    builder
        .setStatusCode(response.code())
        .setDuration(response.receivedResponseAtMillis() - response.sentRequestAtMillis())
        .setHeaders(collectHeaders(response.headers()))
        .setBody(body)
        .capture();
  }

  private static Map<String, String> collectHeaders(Headers headers) {
    Map<String, String> headersMap = new HashMap<>();

    for (Entry<String, List<String>> entry : headers.toMultimap().entrySet()) {
      String key = entry.getKey();

      // Do not capture auth related headers.
      if (key.toLowerCase().equals("authentication") || key.toLowerCase().equals("authorization")) {
        continue;
      }

      List<String> values = entry.getValue();
      headersMap.put(key, joinValues(values));
    }

    return headersMap;
  }

  private static String joinValues(List<String> values) {
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < values.size(); i++) {
      sb.append(values.get(i));
      if (i != values.size() - 1) {
        sb.append(",");
      }
    }

    return sb.toString();
  }
}

Capturing Logs

As with network requests, the LogRocket Android SDK does not automatically capture logs from the android.util.Log interface. Logs can be manually registered to the session using the com.logrocket.core.Logger utility:

import com.logrocket.core.Logger;

Logger.e(String tag, String msg);
Logger.w(String tag, String msg);
Logger.i(String tag, String msg);
Logger.d(String tag, String msg);
Logger.captureException(Throwable error);

Configuration Options

Several configuration options are available when calling SDK.init.

options.setEnableBitmapCapture(boolean enable)

Disable image capture completely by setting this to true.

options.setEnableIPCapture(boolean enable)

To disable capturing a User's IP Address set this to true.

options.addRedactionTag(Object tag)

Add additional View Tags that should be redacted when capturing the screen. The lr-hide tag will always redact views.

options.setConnectionType(SDK.ConnectionType)

Configure the required connection type for uploading session data: either Mobile (the default) or WiFi.