Skip to content

Instantly share code, notes, and snippets.

@pyricau
Created May 9, 2015 00:03
Show Gist options
  • Save pyricau/06c2c486d24f5f85f7f0 to your computer and use it in GitHub Desktop.
Save pyricau/06c2c486d24f5f85f7f0 to your computer and use it in GitHub Desktop.
Sending Leak Traces to a Slack Channel (and HipChat, see the comments)
import android.util.Log;
import com.squareup.leakcanary.AnalysisResult;
import com.squareup.leakcanary.DisplayLeakService;
import com.squareup.leakcanary.HeapDump;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.http.Multipart;
import retrofit.http.POST;
import retrofit.http.Part;
import retrofit.mime.TypedFile;
public final class LeakSlackUploadService extends DisplayLeakService {
/** See https://api.slack.com/ for documentation. */
public interface SlackApi {
String TOKEN = "xoxp-SOME-USER-TOKEN";
String MEMORY_LEAK_CHANNEL = "SOME-CHANNEL-TOKEN";
@Multipart @POST("/api/files.upload") UploadFileResponse uploadFile(@Part("token") String token,
@Part("file") TypedFile file, @Part("filetype") String filetype,
@Part("filename") String filename, @Part("title") String title,
@Part("initial_comment") String initialComment, @Part("channels") String channels);
}
public static class UploadFileResponse {
boolean ok;
String error;
@Override public String toString() {
return "UploadFileResponse{" +
"ok=" + ok +
", error='" + error + '\'' +
'}';
}
}
private static final String TAG = "LeakListenerService";
private static String classSimpleName(String className) {
int separator = className.lastIndexOf('.');
return separator == -1 ? className : className.substring(separator + 1);
}
private SlackApi slackApi;
@Override public void onCreate() {
super.onCreate();
slackApi = new RestAdapter.Builder() //
.setEndpoint("https://slack.com") //
.build() //
.create(SlackApi.class);
}
@Override
protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
if (!result.leakFound || result.excludedLeak) {
return;
}
String name = classSimpleName(result.className);
if (!heapDump.referenceName.equals("")) {
name += "(" + heapDump.referenceName + ")";
}
String title = name + " has leaked";
String initialComment = leakInfo;
try {
slackApi.uploadFile(SlackApi.TOKEN,
new TypedFile("application/octet-stream", heapDump.heapDumpFile), null,
heapDump.heapDumpFile.getName(), title, initialComment, SlackApi.MEMORY_LEAK_CHANNEL);
} catch (RetrofitError e) {
Log.e(TAG, "Error when uploading heap dump", e);
}
}
}
public class SquareDebugApplication extends SquareApplication {
@Override protected RefWatcher installLeakCanary() {
return LeakCanary.install(app, LeakSlackUploadService.class);
}
}
@mullender
Copy link

Same thing for hipchat:

import android.util.Log;
import com.squareup.leakcanary.AnalysisResult;
import com.squareup.leakcanary.DisplayLeakService;
import com.squareup.leakcanary.HeapDump;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.http.Field;
import retrofit.http.FormUrlEncoded;
import retrofit.http.Header;
import retrofit.http.POST;
import retrofit.http.Path;

public final class HipChatNotifyService extends DisplayLeakService {

    /**
     * See https://api.slack.com/ for documentation.
     */
    public interface HipChatApi {
        String ENDPOINT = "https://api.hipchat.com";
        String TOKEN = "Bearer <room auth token>";
        String MEMORY_LEAK_CHANNEL = "<room number or id>";

        /**
         * @param token          A room notification token, generated via a room's admin page on the HipChat website
         * @param id_or_name     The id or name of the room.
         *                       Valid length range: 1 - 100.
         * @param message        The message body.
         *                       Valid length range: 1 - 10000.
         * @param message_format Determines how the message is treated by our server and rendered inside HipChat applications
         *                       html - Message is rendered as HTML and receives no special treatment. Must be valid HTML and entities must be escaped (e.g.: '&amp;' instead of '&'). May contain basic tags: a, b, i, strong, em, br, img, pre, code, lists, tables.
         *                       text - Message is treated just like a message sent by a user. Can include @mentions, emoticons, pastes, and auto-detected URLs (Twitter, YouTube, images, etc).
         *                       Valid values: html, text.
         *                       Defaults to 'html'.
         * @param color          Background color for message.
         *                       Valid values: yellow, green, red, purple, gray, random.
         *                       Defaults to 'yellow'.
         * @param notify         Whether this message should trigger a user notification (change the tab color, play a sound, notify mobile phones, etc). Each recipient's notification preferences are taken into account.
         *                       Defaults to false.
         * @return
         */
        @FormUrlEncoded
        @POST("/v2/room/{id_or_name}/notification")
        String postMessage(@Header("Authorization") String token, @Path("id_or_name") String id_or_name, @Field("message") String message,
                           @Field("message_format") String message_format, @Field("color") String color,
                           @Field("notify") boolean notify);
    }


    private static final String TAG = "LeakListenerService";

    private static String classSimpleName(String className) {
        int separator = className.lastIndexOf('.');
        return separator == -1 ? className : className.substring(separator + 1);
    }

    private HipChatApi hipChatApi;

    @Override
    public void onCreate() {
        super.onCreate();
        hipChatApi = new RestAdapter.Builder() //
            .setEndpoint(HipChatApi.ENDPOINT) //
            .build() //
            .create(HipChatApi.class);
    }

    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak) {
            return;
        }
        String name = classSimpleName(result.className);
        if (!heapDump.referenceName.equals("")) {
            name += "(" + heapDump.referenceName + ")";
        }

        try {
            hipChatApi.postMessage(HipChatApi.TOKEN, HipChatApi.MEMORY_LEAK_CHANNEL,
                                   String.format("%s has leaked:\n%s", name, leakInfo), "text", "yellow", true);
        } catch (RetrofitError e) {
            Log.e(TAG, "Error when uploading heap dump", e);
        }
    }
}

@pyricau
Copy link
Author

pyricau commented May 9, 2015

Thanks guys!

@c99koder
Copy link

Create a non-fatal issue ticket in Crashlytics for leaks:

public class CrashlyticsLeakService extends DisplayLeakService {
    private static String classSimpleName(String className) {
        int separator = className.lastIndexOf('.');
        return separator == -1 ? className : className.substring(separator + 1);
    }

    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak) {
            return;
        }
        Crashlytics.log("*** Memory Leak ***");
        for(String s : leakInfo.split("\n")) {
            Crashlytics.log(s);
        }
        Crashlytics.log("*** End Of Leak ***");

        String name = classSimpleName(result.className);
        if (!heapDump.referenceName.equals("")) {
            name += "(" + heapDump.referenceName + ")";
        }
        Crashlytics.logException(new Exception(name + " has leaked"));
    }
}

@lizheng90
Copy link

Hi, I followed the code trying to upload leak traces to HipChat room, but get the retrofit.RetrofitError: 400 Bad Request, do you know how to fix this issue?

Have resolved this, because of too long Http request body.

@kikoso
Copy link

kikoso commented Oct 23, 2015

@mullender, why are you setting up it to android:enabled="false" ?

@huangdaphne3
Copy link

Has anyone ever upload to flowdock or bug system (i.e. Asana, Jira)?

@parahall
Copy link

Hey :)
After some digging here the snippet for uploading leaks using retrofit 2:

public final class LeakSlackUploadService extends DisplayLeakService
        implements Callback<LeakSlackUploadService.UploadFileResponse> {

    private SlackApi slackApi;


    @Override
    public void onCreate() {
        super.onCreate();
        slackApi = new Retrofit.Builder()
                .baseUrl("https://slack.com")
                .addConverterFactory(GsonConverterFactory.create())
                .build() //
                .create(SlackApi.class);
    }

    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        Log.e(TAG, "afterDefaultHandling " + leakInfo);
        if (!result.leakFound || result.excludedLeak) {
            Log.e(TAG, "!result.leakFound || result.excludedLeak");
            return;
        }
        String name = classSimpleName(result.className);
        if (!heapDump.referenceName.equals("")) {
            name += "(" + heapDump.referenceName + ")";
        }

        String title = name + " has leaked5";
        String initialComment = leakInfo.substring(0, leakInfo.indexOf("Details:", 0));
        RequestBody file = RequestBody
                .create(MediaType.parse("multipart/form-data"), heapDump.heapDumpFile);
        MultipartBody.Part body =
                MultipartBody.Part.createFormData("file", heapDump.heapDumpFile.getName(), file);
        final Call<UploadFileResponse> call = slackApi.uploadFile(SlackApi.TOKEN,
                body,
                null,
                heapDump.heapDumpFile.getName(), title, initialComment,
                SlackApi.MEMORY_LEAK_CHANNEL);
        call.enqueue(this);
    }

    @Override
    public void onResponse(Call<UploadFileResponse> call,
            Response<UploadFileResponse> response) {
        Log.d(TAG, response.body().toString());

    }

    @Override
    public void onFailure(Call<UploadFileResponse> call, Throwable t) {
        Log.d(TAG, t.getLocalizedMessage());
    }

    /** See https://api.slack.com/ for documentation. */
    public interface SlackApi {

        String TOKEN = "YOUR_TOKEN_SLACK";

        String MEMORY_LEAK_CHANNEL = "YOUR_CHANNEL_FOR_MEMORY_LEAKS";

        @Multipart
        @POST("/api/files.upload")
        Call<UploadFileResponse> uploadFile(
                @Query("token") String token,
                @Part MultipartBody.Part file, @Query("filetype") String filetype,
                @Query("filename") String filename, @Query("title") String title,
                @Query("initial_comment") String initialComment,
                @Query("channels") String channels);
    }

    public static class UploadFileResponse {

        boolean ok;

        String error;

        @Override
        public String toString() {
            return "UploadFileResponse{" +
                    "ok=" + ok +
                    ", error='" + error + '\'' +
                    '}';
        }
    }

    private static final String TAG = "LeakListenerService";

    private static String classSimpleName(String className) {
        int separator = className.lastIndexOf('.');
        return separator == -1 ? className : className.substring(separator + 1);
    }
}

@prabintim
Copy link

prabintim commented Nov 21, 2016

(Solution below) Since LeakCanary 1.5, the install method mentioned above is no longer available.

Deprecated source (below)

public static RefWatcher androidWatcher(Context context, Listener heapDumpListener, ExcludedRefs excludedRefs) {
        AndroidDebuggerControl debuggerControl = new AndroidDebuggerControl();
        AndroidHeapDumper heapDumper = new AndroidHeapDumper(context);
        heapDumper.cleanup();
        return new RefWatcher(new AndroidWatchExecutor(), debuggerControl, GcTrigger.DEFAULT, heapDumper, heapDumpListener, excludedRefs);
    }

Any ideas how to refactor the solution posted above to fit v1.5?
------------ Below is the solution -----------------

AndroidRefWatcherBuilder refWatcher = LeakCanary.refWatcher(this).listenerServiceClass(LeakSlackUploadService.class);
refWatcher.buildAndInstall();

@prabintim
Copy link

When I do LeakCanary.refWatcher(this).listenerServiceClass(LeakSlackUploadService.class); for builds with leakcanary-android-no-op, it throws no Static method exception.

@jiurchuk
Copy link

jiurchuk commented Feb 13, 2019

LeakCanary.refWatcher(application)
    .listenerServiceClass(LeakUploadService.class)
    .buildAndInstall();

@parahall
Copy link

I wonder if anyone created code recipe to upload leak to slack for version 2.7 and Kotlin :) @pyricau

@pyricau
Copy link
Author

pyricau commented Sep 15, 2021

no. Slack bot means no aggregation so it's not as helpful as say Bugsnag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment