Skip to content
🤔 Documentation issue? Report or edit

Uploading analysis results

You can add an EventListener to upload the analysis result to a server of your choosing:

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    val analysisUploadListener = EventListener { event ->
      if (event is HeapAnalysisSucceeded) {
        val heapAnalysis = event.heapAnalysis
        TODO("Upload heap analysis to server")
      }
    }

    LeakCanary.config = LeakCanary.config.run {
      copy(eventListeners = eventListeners + analysisUploadListener)
    }
  }
}

Uploading to Bugsnag

A leak trace has a lot in common with a stack trace, so if you lack the engineering resources to build a backend for LeakCanary, you can instead upload leak traces to a crash reporting backend. The client needs to support grouping via custom client-side hashing as well as custom metadata with support for newlines.

Info

As of this writing, the only known library suitable for uploading leaks is the Bugsnag client. If you managed to make it work with another library, please file an issue.

Create a Bugsnag account, create a new project for leak reporting and grab an API key. Make sure the app has the android.permission.INTERNET permission then add the latest version of the Bugsnag Android client library to build.gradle:

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
  debugImplementation "com.bugsnag:bugsnag-android:$bugsnagVersion"
}

Info

If you’re only using Bugsnag for uploading leaks, then you do not need to set up the Bugsnag Gradle plugin or to configure the API key in your app manifest.

Create a new BugsnagLeakUploader:

import android.app.Application
import com.bugsnag.android.Bugsnag
import com.bugsnag.android.Configuration
import com.bugsnag.android.ErrorTypes
import com.bugsnag.android.Event
import com.bugsnag.android.ThreadSendPolicy
import shark.HeapAnalysis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.Leak
import shark.LeakTrace
import shark.LeakTraceReference
import shark.LibraryLeak

class BugsnagLeakUploader(applicationContext: Application) {

  private val bugsnagClient = Bugsnag.start(
    applicationContext,
    Configuration("YOUR_BUGSNAG_API_KEY").apply {
      enabledErrorTypes = ErrorTypes(
        anrs = false,
        ndkCrashes = false,
        unhandledExceptions = false,
        unhandledRejections = false
      )
      sendThreads = ThreadSendPolicy.NEVER
    }
  )

  fun upload(heapAnalysis: HeapAnalysis) {
    when (heapAnalysis) {
      is HeapAnalysisSuccess -> {
        val allLeakTraces = heapAnalysis
          .allLeaks
          .toList()
          .flatMap { leak ->
            leak.leakTraces.map { leakTrace -> leak to leakTrace }
          }
        if (allLeakTraces.isEmpty()) {
          // Track how often we perform a heap analysis that yields no result.
          bugsnagClient.notify(NoLeakException()) { event ->
            event.addHeapAnalysis(heapAnalysis)
            true
          }
        } else {
          allLeakTraces.forEach { (leak, leakTrace) ->
            val message = "Memory leak: ${leak.shortDescription}. See LEAK tab."
            val exception = leakTrace.asFakeException(message)
            bugsnagClient.notify(exception) { event ->
              event.addHeapAnalysis(heapAnalysis)
              event.addLeak(leak)
              event.addLeakTrace(leakTrace)
              event.groupingHash = leak.signature
              true
            }
          }
        }
      }
      is HeapAnalysisFailure -> {
        // Please file any reported failure to
        // https://github.com/square/leakcanary/issues
        bugsnagClient.notify(heapAnalysis.exception)
      }
    }
  }

  class NoLeakException : RuntimeException()

  private fun Event.addHeapAnalysis(heapAnalysis: HeapAnalysisSuccess) {
    addMetadata("Leak", "heapDumpPath", heapAnalysis.heapDumpFile.absolutePath)
    heapAnalysis.metadata.forEach { (key, value) ->
      addMetadata("Leak", key, value)
    }
    addMetadata("Leak", "analysisDurationMs", heapAnalysis.analysisDurationMillis)
  }

  private fun Event.addLeak(leak: Leak) {
    addMetadata("Leak", "libraryLeak", leak is LibraryLeak)
    if (leak is LibraryLeak) {
      addMetadata("Leak", "libraryLeakPattern", leak.pattern.toString())
      addMetadata("Leak", "libraryLeakDescription", leak.description)
    }
  }

  private fun Event.addLeakTrace(leakTrace: LeakTrace) {
    addMetadata("Leak", "retainedHeapByteSize", leakTrace.retainedHeapByteSize)
    addMetadata("Leak", "signature", leakTrace.signature)
    addMetadata("Leak", "leakTrace", leakTrace.toString())
  }

  private fun LeakTrace.asFakeException(message: String): RuntimeException {
    val exception = RuntimeException(message)
    val stackTrace = mutableListOf<StackTraceElement>()
    stackTrace.add(StackTraceElement("GcRoot", gcRootType.name, "GcRoot.kt", 42))
    for (cause in referencePath) {
      stackTrace.add(buildStackTraceElement(cause))
    }
    exception.stackTrace = stackTrace.toTypedArray()
    return exception
  }

  private fun buildStackTraceElement(reference: LeakTraceReference): StackTraceElement {
    val file = reference.owningClassName.substringAfterLast(".") + ".kt"
    return StackTraceElement(reference.owningClassName, reference.referenceDisplayName, file, 42)
  }
}

Then add an EventListener to upload the analysis result to Bugsnag:

class DebugExampleApplication : ExampleApplication() {

  override fun onCreate() {
    super.onCreate()
    LeakCanary.config = LeakCanary.config.copy(
        onHeapAnalyzedListener = BugsnagLeakUploader(applicationContext = this)
    )
  }
}

You should start seeing leaks reported into Bugsnag, grouped by their leak signature:

list

The LEAK tab contains the leak trace:

leak