log capture and email reporting

This commit is contained in:
dosiecki 2017-02-28 18:18:43 -08:00
parent 7db2a7347e
commit 1e07b8a34c
9 changed files with 239 additions and 40 deletions

View file

@ -24,7 +24,17 @@
<item android:id="@+id/menu_feedback"
android:title="@string/menu_feedback"
android:showAsAction="never" />
android:showAsAction="never" >
<menu>
<item android:id="@+id/menu_feedback_post"
android:title="@string/menu_feedback_post"
android:showAsAction="never" />
<item android:id="@+id/menu_feedback_email"
android:title="@string/menu_feedback_email"
android:showAsAction="never" />
</menu>
</item>
<item android:id="@+id/menu_loginas"
android:title="@string/menu_loginas"

View file

@ -149,6 +149,8 @@
<string name="menu_mark_all_as_read">Mark all as read</string>
<string name="menu_logout">Log out</string>
<string name="menu_feedback">Send app feedback</string>
<string name="menu_feedback_post">Create a support post</string>
<string name="menu_feedback_email">Email a bug report</string>
<string name="menu_loginas">Login as...</string>
<string name="loginas_title">Login As User</string>

View file

@ -238,7 +238,10 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
Intent settingsIntent = new Intent(this, Settings.class);
startActivity(settingsIntent);
return true;
} else if (item.getItemId() == R.id.menu_feedback) {
} else if (item.getItemId() == R.id.menu_feedback_email) {
PrefsUtils.sendLogEmail(this);
return true;
} else if (item.getItemId() == R.id.menu_feedback_post) {
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(PrefsUtils.createFeedbackLink(this)));

View file

@ -54,7 +54,7 @@ public class APIResponse {
this.responseCode = response.code();
if (responseCode != expectedReturnCode) {
Log.e(this.getClass().getName(), "API returned error code " + response.code() + " calling " + request.url().toString() + " - expected " + expectedReturnCode);
com.newsblur.util.Log.e(this.getClass().getName(), "API returned error code " + response.code() + " calling " + request.url().toString() + " - expected " + expectedReturnCode);
this.isError = true;
return;
}
@ -66,7 +66,7 @@ public class APIResponse {
this.responseBody = response.body().string();
readTime = System.currentTimeMillis() - startTime;
} catch (Exception e) {
Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + request.url().toString(), e);
com.newsblur.util.Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + request.url().toString(), e);
this.isError = true;
return;
}
@ -88,7 +88,7 @@ public class APIResponse {
}
} catch (IOException ioe) {
Log.e(this.getClass().getName(), "Error (" + ioe.getMessage() + ") calling " + request.url().toString(), ioe);
com.newsblur.util.Log.e(this.getClass().getName(), "Error (" + ioe.getMessage() + ") calling " + request.url().toString(), ioe);
this.isError = true;
return;
}

View file

@ -20,11 +20,11 @@ public class NewsBlurResponse {
public boolean isError() {
if (isProtocolError) return true;
if ((message != null) && (!message.equals(""))) {
Log.d(this.getClass().getName(), "Response interpreted as error due to 'message' field: " + message);
com.newsblur.util.Log.d(this.getClass().getName(), "Response interpreted as error due to 'message' field: " + message);
return true;
}
if ((errors != null) && (errors.length > 0) && (errors[0] != null)) {
Log.d(this.getClass().getName(), "Response interpreted as error due to 'errors' field: " + errors[0]);
com.newsblur.util.Log.d(this.getClass().getName(), "Response interpreted as error due to 'errors' field: " + errors[0]);
return true;
}
return false;

View file

@ -169,6 +169,7 @@ public class NBSyncService extends Service {
originalTextService = new OriginalTextService(this);
unreadsService = new UnreadsService(this);
imagePrefetchService = new ImagePrefetchService(this);
com.newsblur.util.Log.offerContext(this);
}
}
@ -261,7 +262,7 @@ public class NBSyncService extends Service {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync");
} catch (Exception e) {
Log.e(this.getClass().getName(), "Sync error.", e);
com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e);
} finally {
decrementRunningChild(startId);
}
@ -296,9 +297,9 @@ public class NBSyncService extends Service {
if (upgraded || autoVac) {
HousekeepingRunning = true;
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
Log.i(this.getClass().getName(), "rebuilding DB . . .");
com.newsblur.util.Log.i(this.getClass().getName(), "rebuilding DB . . .");
dbHelper.vacuum();
Log.i(this.getClass().getName(), ". . . . done rebuilding DB");
com.newsblur.util.Log.i(this.getClass().getName(), ". . . . done rebuilding DB");
PrefsUtils.updateLastVacuumTime(this);
}
} finally {
@ -331,7 +332,7 @@ public class NBSyncService extends Service {
try {
ra = ReadingAction.fromCursor(c);
} catch (IllegalArgumentException e) {
Log.e(this.getClass().getName(), "error unfreezing ReadingAction", e);
com.newsblur.util.Log.e(this.getClass().getName(), "error unfreezing ReadingAction", e);
dbHelper.clearAction(id);
continue actionsloop;
}
@ -339,20 +340,20 @@ public class NBSyncService extends Service {
// don't block story loading unless this is a brand new action
if ((ra.getTried() > 0) && (PendingFeed != null)) continue actionsloop;
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "attempting action: " + ra.toContentValues().toString());
com.newsblur.util.Log.d(this.getClass().getName(), "attempting action: " + ra.toContentValues().toString());
NewsBlurResponse response = ra.doRemote(apiManager);
if (response == null) {
Log.e(this.getClass().getName(), "Discarding reading action with client-side error.");
com.newsblur.util.Log.e(this.getClass().getName(), "Discarding reading action with client-side error.");
dbHelper.clearAction(id);
} else if (response.isProtocolError) {
// the network failed or we got a non-200, so be sure we retry
Log.i(this.getClass().getName(), "Holding reading action with server-side or network error.");
com.newsblur.util.Log.i(this.getClass().getName(), "Holding reading action with server-side or network error.");
dbHelper.incrementActionTried(id);
noteHardAPIFailure();
continue actionsloop;
} else if (response.isError()) {
Log.e(this.getClass().getName(), "Discarding reading action with user error.");
com.newsblur.util.Log.e(this.getClass().getName(), "Discarding reading action with user error.");
dbHelper.clearAction(id);
String message = response.getErrorMessage(null);
if (message != null) NbActivity.toastError(message);
@ -413,6 +414,8 @@ public class NBSyncService extends Service {
return;
}
com.newsblur.util.Log.i(this.getClass().getName(), "ready to sync feed list");
FFSyncRunning = true;
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
@ -434,7 +437,7 @@ public class NBSyncService extends Service {
// we should not have got this far without being logged in, so the server either
// expired or ignored out cookie. keep track of this.
isAuth = false;
Log.w(this.getClass().getName(), "Server ignored or rejected auth cookie.");
com.newsblur.util.Log.w(this.getClass().getName(), "Server ignored or rejected auth cookie.");
DoFeedsFolders = true;
noteHardAPIFailure();
return;
@ -528,6 +531,8 @@ public class NBSyncService extends Service {
lastFFWriteMillis = System.currentTimeMillis() - startTime;
lastFeedCount = feedValues.size();
com.newsblur.util.Log.i(this.getClass().getName(), "got feed list: " + getSpeedInfo());
UnreadsService.doMetadata();
unreadsService.start(startId);
cleanupService.start(startId);
@ -582,7 +587,7 @@ public class NBSyncService extends Service {
UnreadCountResponse apiResponse = apiManager.getFeedUnreadCounts(apiIds);
if ((apiResponse == null) || (apiResponse.isError())) {
Log.w(this.getClass().getName(), "Bad response to feed_unread_count");
com.newsblur.util.Log.w(this.getClass().getName(), "Bad response to feed_unread_count");
return;
}
if (apiResponse.feeds != null ) {
@ -633,7 +638,7 @@ public class NBSyncService extends Service {
}
try {
if (ExhaustedFeeds.contains(fs)) {
Log.i(this.getClass().getName(), "No more stories for feed set: " + fs);
com.newsblur.util.Log.i(this.getClass().getName(), "No more stories for feed set: " + fs);
finished = true;
return;
}
@ -719,11 +724,11 @@ public class NBSyncService extends Service {
private boolean isStoryResponseGood(StoriesResponse response) {
if (response == null) {
Log.e(this.getClass().getName(), "Null response received while loading stories.");
com.newsblur.util.Log.e(this.getClass().getName(), "Null response received while loading stories.");
return false;
}
if (response.stories == null) {
Log.e(this.getClass().getName(), "Null stories member received while loading stories.");
com.newsblur.util.Log.e(this.getClass().getName(), "Null stories member received while loading stories.");
return false;
}
return true;
@ -772,10 +777,12 @@ public class NBSyncService extends Service {
}
}
com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from main fetch loop: " + apiResponse.stories.length);
dbHelper.insertStories(apiResponse, true);
}
void insertStories(StoriesResponse apiResponse) {
com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from sub sync: " + apiResponse.stories.length);
dbHelper.insertStories(apiResponse, false);
}
@ -818,7 +825,7 @@ public class NBSyncService extends Service {
static boolean stopSync(Context context) {
if (HaltNow) {
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "stopping sync, soft interrupt set.");
com.newsblur.util.Log.i(NBSyncService.class.getName(), "stopping sync, soft interrupt set.");
return true;
}
if (context == null) return false;
@ -834,13 +841,14 @@ public class NBSyncService extends Service {
}
private void noteHardAPIFailure() {
com.newsblur.util.Log.w(this.getClass().getName(), "hard API failure");
lastAPIFailure = System.currentTimeMillis();
}
private boolean backoffBackgroundCalls() {
if (NbActivity.getActiveActivityCount() > 0) return false;
if (System.currentTimeMillis() > (lastAPIFailure + AppConstants.API_BACKGROUND_BACKOFF_MILLIS)) return false;
Log.i(this.getClass().getName(), "abandoning background sync due to recent API failures.");
com.newsblur.util.Log.i(this.getClass().getName(), "abandoning background sync due to recent API failures.");
return true;
}
@ -927,7 +935,7 @@ public class NBSyncService extends Service {
*/
public static boolean requestMoreForFeed(FeedSet fs, int desiredStoryCount, int callerSeen) {
if (ExhaustedFeeds.contains(fs)) {
if (AppConstants.VERBOSE_LOG) Log.i(NBSyncService.class.getName(), "rejecting request for feedset that is exhaused");
com.newsblur.util.Log.d(NBSyncService.class.getName(), "rejecting request for feedset that is exhaused");
return false;
}
@ -973,7 +981,7 @@ public class NBSyncService extends Service {
* Reset the API pagniation state for the given feedset, presumably because the order or filter changed.
*/
public static void resetFetchState(FeedSet fs) {
Log.d(NBSyncService.class.getName(), "requesting feed fetch state reset");
com.newsblur.util.Log.d(NBSyncService.class.getName(), "requesting feed fetch state reset");
ResetFeed = fs;
}
@ -996,7 +1004,7 @@ public class NBSyncService extends Service {
}
public static void softInterrupt() {
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "soft stop");
com.newsblur.util.Log.i(NBSyncService.class.getName(), "soft stop");
HaltNow = true;
}
@ -1041,7 +1049,7 @@ public class NBSyncService extends Service {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted");
super.onDestroy();
} catch (Exception ex) {
Log.e(this.getClass().getName(), "unclean shutdown", ex);
com.newsblur.util.Log.e(this.getClass().getName(), "unclean shutdown", ex);
}
}

View file

@ -61,6 +61,7 @@ public class UnreadsService extends SubService {
// the set of unreads from the API, we will mark them as read. note that this collection
// will be searched many times for new unreads, so it should be a Set, not a List.
Set<String> oldUnreadHashes = parent.dbHelper.getUnreadStoryHashesAsSet();
com.newsblur.util.Log.i(this.getClass().getName(), "starting unread count: " + oldUnreadHashes.size());
// a place to store and then sort unread hashes we aim to fetch. note the member format
// is made to match the format of the API response (a list of [hash, date] tuples). it
@ -69,6 +70,7 @@ public class UnreadsService extends SubService {
// process the api response, both bookkeeping no-longer-unread stories and populating
// the sortation list we will use to create the fetch list for step two
int count = 0;
feedloop: for (Entry<String, List<String[]>> entry : unreadHashes.unreadHashes.entrySet()) {
// the API gives us a list of unreads, split up by feed ID. the unreads are tuples of
// story hash and date
@ -84,8 +86,12 @@ public class UnreadsService extends SubService {
} else {
oldUnreadHashes.remove(newUnread[0]);
}
count++;
}
}
com.newsblur.util.Log.i(this.getClass().getName(), "new unread count: " + count);
com.newsblur.util.Log.i(this.getClass().getName(), "new unreads found: " + sortationList.size());
com.newsblur.util.Log.i(this.getClass().getName(), "unreads to retire: " + oldUnreadHashes.size());
// now sort the unreads we need to fetch so they are fetched roughly in the order
// the user is likely to read them. if the user reads newest first, those come first.

View file

@ -0,0 +1,135 @@
package com.newsblur.util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import android.content.Context;
/**
* A low-overhead, fail-fast, dependency-free, file-backed log collector. This utility
* will persist debug messages in a file in such a way that sacrifices any guarantees
* in order to have as few side-effects as possible. The resulting log file will have
* as many of the messages sent here as possible, but should not be expected to have
* all of them. The file is trimmed down to a set number of lines if it grows too big.
*/
public class Log {
private static final String D = "DEBUG ";
private static final String I = "INFO ";
private static final String W = "WARN ";
private static final String E = "ERROR ";
private static final String LOG_NAME_INTERNAL = "logbuffer.txt";
private static final int MAX_LINE_SIZE = 4 * 1024;
private static final int TRIM_LINES = 256; // trim the log down to 256 lines
private static final long MAX_SIZE = 512L * MAX_LINE_SIZE; // when it is at least 512 lines long
private static Queue<String> q;
private static ExecutorService executor;
private static File logloc = null;
static {
q = new ConcurrentLinkedQueue<String>();
executor = Executors.newFixedThreadPool(1);
}
private Log() {} // util class - no instances
public static void d(String tag, String m) {
if (AppConstants.VERBOSE_LOG) android.util.Log.d(tag, m);
add(D, tag, m, null);
}
public static void i(String tag, String m) {
android.util.Log.d(tag, m);
add(I, tag, m, null);
}
public static void w(String tag, String m) {
android.util.Log.w(tag, m);
add(W, tag, m, null);
}
public static void e(String tag, String m) {
android.util.Log.e(tag, m);
add(E, tag, m, null);
}
public static void e(String tag, String m, Throwable t) {
android.util.Log.e(tag, m, t);
add(E, tag, m, t);
}
private static void add(String lvl, String tag, String m, Throwable t) {
if (q.size() > TRIM_LINES) return;
if (m != null && m.length() > MAX_LINE_SIZE) m = m.substring(0, MAX_LINE_SIZE);
StringBuilder s = new StringBuilder();
s.append(Long.toString(System.currentTimeMillis()))
.append(" ")
.append(lvl)
.append(tag)
.append(" ");
if (t != null) {
s.append(t.getMessage());
s.append(" ");
}
s.append(m);
q.offer(s.toString());
Runnable r = new Runnable() {
public void run() {
proc();
}
};
executor.execute(r);
}
public static void offerContext(Context c) {
logloc = c.getExternalCacheDir();
}
private static void proc() {
synchronized (q) {
if (logloc == null) return; // not yet spun up
String line = q.poll();
if (line == null) return;
File f = new File(logloc, LOG_NAME_INTERNAL);
try (BufferedWriter w = new BufferedWriter(new FileWriter(f, true))) {
w.append(line);
w.newLine();
} catch (Throwable t) {
; // explicitly do nothing, log nothing, and fail fast. this is a utility to
// provice as much info as possible while having absolute minimal impact or
// side effect on performance or app operation
}
if (f.length() < MAX_SIZE) return;
android.util.Log.i(Log.class.getName(), "trimming");
List<String> lines = new ArrayList<String>(TRIM_LINES * 2);
try (BufferedReader r = new BufferedReader(new FileReader(f))) {
for (String l = r.readLine(); l != null; l = r.readLine()) {
lines.add(l);
}
} catch (Throwable t) {;}
int offset = lines.size() - TRIM_LINES;
try (BufferedWriter w = new BufferedWriter(new FileWriter(f, false))) {
for (int i = offset; i < lines.size(); i++) {
w.append(lines.get(i));
w.newLine();
}
} catch (Throwable t) {;}
}
}
public static File getLogfile() {
return new File(logloc, LOG_NAME_INTERNAL);
}
}

View file

@ -88,17 +88,48 @@ public class PrefsUtils {
public static String createFeedbackLink(Context context) {
StringBuilder s = new StringBuilder(AppConstants.FEEDBACK_URL);
s.append("<give us some feedback!>%0A%0A");
s.append("%0Aapp version: ").append(getVersion(context));
s.append("%0Aandroid version: ").append(Build.VERSION.RELEASE).append(" (" + Build.DISPLAY + ")");
s.append("%0Adevice: ").append(Build.MANUFACTURER + "+" + Build.MODEL + "+(" + Build.BOARD + ")");
s.append("%0Asqlite version: ").append(FeedUtils.dbHelper.getEngineVersion());
s.append("%0Ausername: ").append(getUserDetails(context).username);
s.append("%0Aserver: ").append(APIConstants.isCustomServer() ? "default" : "custom");
s.append("%0Amemory: ").append(NBSyncService.isMemoryLow() ? "low" : "normal");
s.append("%0Aspeed: ").append(NBSyncService.getSpeedInfo());
s.append("%0Apending actions: ").append(NBSyncService.getPendingInfo());
s.append("%0Apremium: ");
s.append("<give us some feedback!>%0A%0A%0A");
String info = getDebugInfo(context);
s.append(info.replace("\n", "%0A"));
return s.toString();
}
public static void sendLogEmail(Context context) {
File f = com.newsblur.util.Log.getLogfile();
if (f == null) return;
android.net.Uri localPath = android.net.Uri.fromFile(f);
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("*/*");
i.putExtra(Intent.EXTRA_EMAIL, new String[]{"android@newsblur.com"});
i.putExtra(Intent.EXTRA_SUBJECT, "Android logs");
i.putExtra(Intent.EXTRA_TEXT, getDebugInfo(context));
i.putExtra(Intent.EXTRA_STREAM, localPath);
if (i.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(i);
}
}
private static String getDebugInfo(Context context) {
StringBuilder s = new StringBuilder();
s.append("app version: ").append(getVersion(context));
s.append("\n");
s.append("android version: ").append(Build.VERSION.RELEASE).append(" (" + Build.DISPLAY + ")");
s.append("\n");
s.append("device: ").append(Build.MANUFACTURER + "+" + Build.MODEL + "+(" + Build.BOARD + ")");
s.append("\n");
s.append("sqlite version: ").append(FeedUtils.dbHelper.getEngineVersion());
s.append("\n");
s.append("username: ").append(getUserDetails(context).username);
s.append("\n");
s.append("server: ").append(APIConstants.isCustomServer() ? "default" : "custom");
s.append("\n");
s.append("memory: ").append(NBSyncService.isMemoryLow() ? "low" : "normal");
s.append("\n");
s.append("speed: ").append(NBSyncService.getSpeedInfo());
s.append("\n");
s.append("pending actions: ").append(NBSyncService.getPendingInfo());
s.append("\n");
s.append("premium: ");
if (NBSyncService.isPremium == Boolean.TRUE) {
s.append("yes");
} else if (NBSyncService.isPremium == Boolean.FALSE) {
@ -106,9 +137,13 @@ public class PrefsUtils {
} else {
s.append("unknown");
}
s.append("%0Aprefetch: ").append(isOfflineEnabled(context) ? "yes" : "no");
s.append("%0Akeepread: ").append(isKeepOldStories(context) ? "yes" : "no");
s.append("%0Athumbs: ").append(isShowThumbnails(context) ? "yes" : "no");
s.append("\n");
s.append("prefetch: ").append(isOfflineEnabled(context) ? "yes" : "no");
s.append("\n");
s.append("keepread: ").append(isKeepOldStories(context) ? "yes" : "no");
s.append("\n");
s.append("thumbs: ").append(isShowThumbnails(context) ? "yes" : "no");
s.append("\n");
return s.toString();
}