+ * Not implemented:
+ * - battery_level
+ * If the device has a battery this can be an integer defining the battery level (in
+ * the range 0-100). (Android requires registration of an intent to query the battery).
+ * - name
+ * The name of the device. This is typically a hostname.
+ *
+ * See https://docs.getsentry.com/hosted/clientdev/interfaces/#context-types
+ */
+ private static JSONObject deviceContext(Context context) {
+ final JSONObject device = new JSONObject();
+ try {
+ // The family of the device. This is normally the common part of model names across
+ // generations. For instance iPhone would be a reasonable family, so would be Samsung Galaxy.
+ device.put("family", Build.BRAND);
+
+ // The model name. This for instance can be Samsung Galaxy S3.
+ device.put("model", Build.PRODUCT);
+
+ // An internal hardware revision to identify the device exactly.
+ device.put("model_id", Build.MODEL);
+
+ final String architecture = System.getProperty("os.arch");
+ if (!TextUtils.isEmpty(architecture)) {
+ device.put("arch", architecture);
+ }
+
+ final int orient = context.getResources().getConfiguration().orientation;
+ device.put("orientation", orient == Configuration.ORIENTATION_LANDSCAPE ?
+ "landscape" : "portrait");
+
+ // Read screen resolution in the format "800x600"
+ // Normalised to have wider side first.
+ final Object windowManager = context.getSystemService(Context.WINDOW_SERVICE);
+ if (windowManager instanceof WindowManager) {
+ final DisplayMetrics metrics = new DisplayMetrics();
+ ((WindowManager) windowManager).getDefaultDisplay().getMetrics(metrics);
+ device.put("screen_resolution",
+ String.format("%sx%s",
+ Math.max(metrics.widthPixels, metrics.heightPixels),
+ Math.min(metrics.widthPixels, metrics.heightPixels)));
+ }
+
+ } catch (Exception e) {
+ Log.e(Sentry.TAG, "Error reading device context", e);
+ }
+ return device;
+ }
+
+
+ private static JSONObject osContext() {
+ final JSONObject os = new JSONObject();
+ try {
+ os.put("type", "os");
+ os.put("name", "Android");
+ os.put("version", Build.VERSION.RELEASE);
+ os.put("build", Integer.toString(Build.VERSION.SDK_INT));
+
+ final String kernelVersion = System.getProperty("os.version");
+ if (!TextUtils.isEmpty(kernelVersion)) {
+ os.put("kernel_version", kernelVersion);
+ }
+
+ } catch (Exception e) {
+ Log.e(Sentry.TAG, "Error reading OS context", e);
+ }
+ return os;
+ }
+
+ /**
+ * Store a tuple of package version information captured from PackageInfo
+ *
+ * @see PackageInfo
+ */
+ private final static class AppInfo {
+ static final AppInfo Empty = new AppInfo("", "", 0);
+ final String name;
+ final String versionName;
+ final int versionCode;
+
+ AppInfo(String name, String versionName, int versionCode) {
+ this.name = name;
+ this.versionName = versionName;
+ this.versionCode = versionCode;
+ }
+
+ static AppInfo read(final Context context) {
+ try {
+ final PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ return new AppInfo(info.packageName, info.versionName, info.versionCode);
+ } catch (Exception e) {
+ Log.e(Sentry.TAG, "Error reading package context", e);
+ return Empty;
+ }
+ }
+ }
+}
diff --git a/sentry-android/src/main/java/com/joshdholtz/sentry/BaseHttpBuilder.java b/sentry-android/src/main/java/com/joshdholtz/sentry/BaseHttpBuilder.java
new file mode 100644
index 0000000..367050b
--- /dev/null
+++ b/sentry-android/src/main/java/com/joshdholtz/sentry/BaseHttpBuilder.java
@@ -0,0 +1,44 @@
+package com.joshdholtz.sentry;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class BaseHttpBuilder implements HttpRequestSender.Builder {
+ private String url;
+ private Map headers = new HashMap<>();
+ private String requestData;
+ private String mediaType;
+ private boolean useHttps;
+
+ @Override
+ public HttpRequestSender.Request build() throws Exception {
+ return build(url, headers, requestData, mediaType, useHttps);
+ }
+
+ protected abstract HttpRequestSender.Request build(String url, Map headers, String requestData, String mediaType, boolean useHttps) throws Exception;
+
+ @Override
+ public HttpRequestSender.Builder useHttps() {
+ useHttps = true;
+ return this;
+ }
+
+ @Override
+ public HttpRequestSender.Builder url(String url) {
+ this.url = url;
+ return this;
+ }
+
+ @Override
+ public HttpRequestSender.Builder header(String headerName, String headerValue) {
+ headers.put(headerName, headerValue);
+ return this;
+ }
+
+ @Override
+ public HttpRequestSender.Builder content(String requestData, String mediaType) {
+ this.requestData = requestData;
+ this.mediaType = mediaType;
+ return this;
+ }
+}
diff --git a/sentry-android/src/main/java/com/joshdholtz/sentry/HttpRequestSender.java b/sentry-android/src/main/java/com/joshdholtz/sentry/HttpRequestSender.java
new file mode 100644
index 0000000..6ae2b29
--- /dev/null
+++ b/sentry-android/src/main/java/com/joshdholtz/sentry/HttpRequestSender.java
@@ -0,0 +1,34 @@
+package com.joshdholtz.sentry;
+
+public interface HttpRequestSender
+{
+ interface Builder
+ {
+ Request build() throws Exception;
+
+ Builder useHttps();
+
+ Builder url(String url);
+
+ Builder header(String headerName, String headerValue);
+
+ Builder content(String requestData, String mediaType);
+ }
+
+ interface Response
+ {
+
+ int getStatusCode();
+
+ String getContent();
+ }
+
+ interface Request
+ {
+ Response execute() throws Exception;
+ }
+
+
+ Builder newBuilder();
+
+}
diff --git a/sentry-android/src/main/java/com/joshdholtz/sentry/Sentry.java b/sentry-android/src/main/java/com/joshdholtz/sentry/Sentry.java
index 661d31a..99c7109 100755
--- a/sentry-android/src/main/java/com/joshdholtz/sentry/Sentry.java
+++ b/sentry-android/src/main/java/com/joshdholtz/sentry/Sentry.java
@@ -1,63 +1,33 @@
package com.joshdholtz.sentry;
-import android.Manifest.permission;
+import android.Manifest;
+import android.app.Activity;
+import android.app.Application;
import android.content.Context;
-import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.res.Configuration;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
-import android.os.Build;
-import android.util.DisplayMetrics;
+import android.os.Bundle;
+import android.text.TextUtils;
import android.util.Log;
-import android.view.WindowManager;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URLEncodedUtils;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.conn.scheme.Scheme;
-import org.apache.http.conn.scheme.SchemeRegistry;
-import org.apache.http.conn.ssl.SSLSocketFactory;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.EnglishReasonPhraseCatalog;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
-import org.apache.http.params.HttpProtocolParams;
-import org.apache.http.protocol.HTTP;
+
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
+import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
-import java.io.Serializable;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
-import java.net.Socket;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetDecoder;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.text.DateFormat;
+import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -70,98 +40,95 @@
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-public class Sentry {
+public final class Sentry {
- private static final String TAG = "Sentry";
- private final static String sentryVersion = "7";
+ static final String TAG = "Sentry";
private static final int MAX_QUEUE_LENGTH = 50;
-
- public static boolean debug = false;
-
+ private static final String VERIFY_SSL = "verify_ssl";
+ private static final String MEDIA_TYPE = "application/json; charset=utf-8";
+ private static final int HTTP_OK = 200;
+ private static final String API = "/api/";
+ private static final String STORE = "/store/";
+ private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+ private static final String EVENT_ID = "event_id";
private Context context;
- private String baseUrl;
- private Uri dsn;
- private AppInfo appInfo = AppInfo.Empty;
- private boolean verifySsl;
+ private String sentryVersion = "7";
+ private String url;
+ private String packageName;
+ private final String publicKey;
+ private final String secretKey;
+ private int verifySsl;
private SentryEventCaptureListener captureListener;
- private JSONObject contexts = new JSONObject();
- private Executor executor;
- private final Breadcrumbs breadcrumbs = new Breadcrumbs();
-
- public enum SentryEventLevel {
-
- FATAL("fatal"),
- ERROR("error"),
- WARNING("warning"),
- INFO("info"),
- DEBUG("debug");
-
- private final String value;
-
- SentryEventLevel(String value) {
- this.value = value;
- }
- }
-
- private Sentry() {
- }
-
- private static void log(String text) {
- if (debug) {
- Log.d(TAG, text);
+ private HttpRequestSender httpRequestSender;
+ private ExecutorService executorService = fixedQueueDiscardingExecutor();
+ private InternalStorage internalStorage;
+ private Runnable sendUnsentRequests = new Runnable() {
+ @Override
+ public void run() {
+ List unsentRequests = internalStorage.getUnsentRequests();
+ logD("Sending up " + unsentRequests.size() + " cached response(s)");
+ for (JSONObject request : unsentRequests) {
+ sendRequest(request);
+ }
}
- }
+ };
+ private boolean enableDebugLogging;
+ private SentryUncaughtExceptionHandler uncaughtExceptionHandler;
+ private final Breadcrumbs breadcrumbs = new Breadcrumbs();
- private static Sentry getInstance() {
- return LazyHolder.instance;
+ private Sentry(Context applicationContext, String url, String publicKey,String secretKey, int verifySsl, String storageFileName, HttpRequestSender httpRequestSender) {
+ context = applicationContext;
+ this.url = url;
+ this.publicKey = publicKey;
+ this.secretKey = secretKey;
+ this.verifySsl = verifySsl;
+ this.httpRequestSender = httpRequestSender;
+ packageName = applicationContext.getPackageName();
+ internalStorage = new InternalStorage(storageFileName);
}
- private static class LazyHolder {
- private static final Sentry instance = new Sentry();
- }
+ /**
+ * This method returns new {@code Sentry} instance which can operate separately with other instances. Should be called once, usually in {@link Application#onCreate()} and obtained from some singleton. If you have only one instance than you can use {@link SentryInstance}. In {@link Activity#onCreate(Bundle)} you should call at least {@link #sendAllCachedCapturedEvents()} to try to send cached events
+ *
+ * @param context this can be any context because {@link Context#getApplicationContext()} is used to avoid memory leak
+ * @param dsnWithoutCredentials DSN of your project - remove credentials from it to avoid warning in Google Play console
+ * @param httpRequestSender used for sending events to Sentry server
+ * @param storageFileName unsent requests storage file - must be different for different instances
+ * @param publicKey public key from DSN
+ * @param secretKey secret key from DSN
+ * @return new {@code Sentry} instance
+ */
+ public static Sentry newInstance(Context context, String dsnWithoutCredentials, HttpRequestSender httpRequestSender, String storageFileName, String publicKey,String secretKey) {
+ Uri dsnUri = Uri.parse(dsnWithoutCredentials);
- public static void init(Context context, String dsn) {
- init(context, dsn, true);
+ Sentry sentry = new Sentry(context.getApplicationContext(), getUrl(dsnUri), publicKey, secretKey, getVerifySsl(dsnUri), storageFileName, httpRequestSender);
+ sentry.setupUncaughtExceptionHandler();
+ return sentry;
}
- public static void init(Context context, String dsn, boolean setupUncaughtExceptionHandler) {
- final Sentry sentry = Sentry.getInstance();
-
- sentry.context = context.getApplicationContext();
-
- Uri uri = Uri.parse(dsn);
+ private static String getUrl(Uri uri) {
String port = "";
if (uri.getPort() >= 0) {
port = ":" + uri.getPort();
}
- sentry.baseUrl = uri.getScheme() + "://" + uri.getHost() + port;
- sentry.dsn = uri;
- sentry.appInfo = AppInfo.Read(sentry.context);
- sentry.verifySsl = getVerifySsl(dsn);
- sentry.contexts = readContexts(sentry.context, sentry.appInfo);
- sentry.executor = fixedQueueDiscardingExecutor(MAX_QUEUE_LENGTH);
+ String path = uri.getPath();
- if (setupUncaughtExceptionHandler) {
- sentry.setupUncaughtExceptionHandler();
- }
+ String projectId = path.substring(path.lastIndexOf('/') + 1);
+ return uri.getScheme() + "://" + uri.getHost() + port + API + projectId + STORE;
}
- private static Executor fixedQueueDiscardingExecutor(int queueSize) {
+ private static ExecutorService fixedQueueDiscardingExecutor() {
// Name our threads so that it is easy for app developers to see who is creating threads.
final ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicLong count = new AtomicLong();
@@ -177,114 +144,94 @@ public Thread newThread(Runnable runnable) {
return new ThreadPoolExecutor(
0, 1, // Keep 0 threads alive. Max pool size is 1.
60, TimeUnit.SECONDS, // Kill unused threads after this length.
- new ArrayBlockingQueue(queueSize),
+ new ArrayBlockingQueue(MAX_QUEUE_LENGTH),
threadFactory, new ThreadPoolExecutor.DiscardPolicy()); // Discard exceptions
}
- private static boolean getVerifySsl(String dsn) {
- List params = getAllGetParams(dsn);
- for (NameValuePair param : params) {
- if (param.getName().equals("verify_ssl"))
- return Integer.parseInt(param.getValue()) != 0;
- }
- return false;
+ private static int getVerifySsl(Uri uri) {
+ int verifySsl = 1;
+ String queryParameter = uri.getQueryParameter(VERIFY_SSL);
+ return queryParameter == null ? verifySsl : Integer.parseInt(queryParameter);
}
- private static List getAllGetParams(String dsn) {
- List params = null;
- try {
- params = URLEncodedUtils.parse(new URI(dsn), HTTP.UTF_8);
- } catch (URISyntaxException e) {
- e.printStackTrace();
- }
- return params;
+ public void setSentryVersion(String sentryVersion) {
+ this.sentryVersion = sentryVersion;
}
- private void setupUncaughtExceptionHandler() {
+ public void setupUncaughtExceptionHandler() {
UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler();
if (currentHandler != null) {
- log("current handler class=" + currentHandler.getClass().getName());
+ logD("current handler class=" + currentHandler.getClass().getName());
+ if (currentHandler == uncaughtExceptionHandler) {
+ return;
+ }
}
- // don't register again if already registered
- if (!(currentHandler instanceof SentryUncaughtExceptionHandler)) {
- // Register default exceptions handler
- Thread.setDefaultUncaughtExceptionHandler(
- new SentryUncaughtExceptionHandler(currentHandler, InternalStorage.getInstance()));
+ //disable existing handler because we don't know if someone wrapped us
+ if (uncaughtExceptionHandler != null) {
+ uncaughtExceptionHandler.disabled = true;
}
+ //register new handler
+ uncaughtExceptionHandler = new SentryUncaughtExceptionHandler(currentHandler);
+ Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
sendAllCachedCapturedEvents();
}
- private static String createXSentryAuthHeader(Uri dsn) {
-
- final StringBuilder header = new StringBuilder();
-
- final String authority = dsn.getAuthority().replace("@" + dsn.getHost(), "");
+ private String createXSentryAuthHeader() {
- final String[] authorityParts = authority.split(":");
- final String publicKey = authorityParts[0];
- final String secretKey = authorityParts[1];
-
- header.append("Sentry ")
- .append(String.format("sentry_version=%s,", sentryVersion))
- .append(String.format("sentry_client=sentry-android/%s,", BuildConfig.SENTRY_ANDROID_VERSION))
- .append(String.format("sentry_key=%s,", publicKey))
- .append(String.format("sentry_secret=%s", secretKey));
-
- return header.toString();
+ return "Sentry " +
+ String.format("sentry_version=%s,", sentryVersion) +
+ String.format("sentry_client=sentry-android/%s,", BuildConfig.SENTRY_ANDROID_VERSION) +
+ String.format("sentry_key=%s,", publicKey) +
+ String.format("sentry_secret=%s", secretKey);
}
- private static String getProjectId(Uri dsn) {
- String path = dsn.getPath();
- String projectId = path.substring(path.lastIndexOf("/") + 1);
-
- return projectId;
- }
- public static void sendAllCachedCapturedEvents() {
- List unsentRequests = InternalStorage.getInstance().getUnsentRequests();
- log("Sending up " + unsentRequests.size() + " cached response(s)");
- for (SentryEventRequest request : unsentRequests) {
- Sentry.doCaptureEventPost(request);
+ public void sendAllCachedCapturedEvents() {
+ if (shouldAttemptPost()) {
+ submit(sendUnsentRequests);
}
}
/**
* @param captureListener the captureListener to set
*/
- public static void setCaptureListener(SentryEventCaptureListener captureListener) {
- Sentry.getInstance().captureListener = captureListener;
+ public void setCaptureListener(SentryEventCaptureListener captureListener) {
+ this.captureListener = captureListener;
}
- public static void captureMessage(String message) {
- Sentry.captureMessage(message, SentryEventLevel.INFO);
+ public void captureMessage(String message) {
+ captureMessage(message, SentryEventBuilder.SentryEventLevel.INFO);
}
- public static void captureMessage(String message, SentryEventLevel level) {
- Sentry.captureEvent(new SentryEventBuilder()
- .setMessage(message)
- .setLevel(level)
- );
+ public void captureMessage(String message, SentryEventBuilder.SentryEventLevel level) {
+ captureEvent(newEventBuilder().setMessage(message).setLevel(level));
+ }
+
+ public SentryEventBuilder newEventBuilder() {
+ return new SentryEventBuilder(enableDebugLogging);
}
- public static void captureException(Throwable t) {
- Sentry.captureException(t, t.getMessage(), SentryEventLevel.ERROR);
+ public void captureException(Throwable t) {
+ captureException(t, SentryEventBuilder.SentryEventLevel.ERROR);
}
- public static void captureException(Throwable t, String message) {
- Sentry.captureException(t, message, SentryEventLevel.ERROR);
+ public void captureException(Throwable t, SentryEventBuilder.SentryEventLevel level) {
+ String culprit = getCause(t, t.getMessage());
+
+ captureEvent(newEventBuilder().setMessage(t.getMessage()).setCulprit(culprit).setLevel(level).setException(t));
}
- public static void captureException(Throwable t, SentryEventLevel level) {
- captureException(t, t.getMessage(), level);
+ public void captureException(Throwable t, String message) {
+ captureException(t, message, SentryEventBuilder.SentryEventLevel.ERROR);
}
- public static void captureException(Throwable t, String message, SentryEventLevel level) {
+ public void captureException(Throwable t, String message, SentryEventBuilder.SentryEventLevel level) {
String culprit = getCause(t, t.getMessage());
- Sentry.captureEvent(new SentryEventBuilder()
+ captureEvent(newEventBuilder()
.setMessage(message)
.setCulprit(culprit)
.setLevel(level)
@@ -293,10 +240,32 @@ public static void captureException(Throwable t, String message, SentryEventLeve
}
- private static String getCause(Throwable t, String culprit) {
-
- final String packageName = Sentry.getInstance().appInfo.name;
+ public void captureUncaughtException(Context context, Throwable t) {
+ final Writer result = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(result);
+ t.printStackTrace(printWriter);
+ try {
+ // Random number to avoid duplicate files
+ long random = System.currentTimeMillis();
+
+ // Embed version in stacktrace filename
+ File stacktrace = new File(getStacktraceLocation(context), "raven-" + random + ".stacktrace");
+ logD("Writing unhandled exception to: " + stacktrace.getAbsolutePath());
+
+ // Write the stacktrace to disk
+ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(stacktrace));
+ oos.writeObject(t);
+ oos.flush();
+ // Close up everything
+ oos.close();
+ } catch (Exception ebos) {
+ // Nothing much we can do about this - the game is over
+ logW(ebos);
+ }
+ logD(result.toString());
+ }
+ private String getCause(Throwable t, String culprit) {
for (StackTraceElement stackTrace : t.getStackTrace()) {
if (stackTrace.toString().contains(packageName)) {
return stackTrace.toString();
@@ -306,35 +275,40 @@ private static String getCause(Throwable t, String culprit) {
return culprit;
}
- public static void captureEvent(SentryEventBuilder builder) {
- final Sentry sentry = Sentry.getInstance();
- final SentryEventRequest request;
- builder.event.put("contexts", sentry.contexts);
- builder.setRelease(sentry.appInfo.versionName);
- builder.event.put("breadcrumbs", Sentry.getInstance().breadcrumbs.current());
- if (sentry.captureListener != null) {
+ private static File getStacktraceLocation(Context context) {
+ return new File(context.getCacheDir(), "crashes");
+ }
- builder = sentry.captureListener.beforeCapture(builder);
- if (builder == null) {
- Log.e(Sentry.TAG, "SentryEventBuilder in captureEvent is null");
- return;
- }
+ public void captureEvent(SentryEventBuilder builder) {
+ JSONObject request = createEvent(builder);
- request = new SentryEventRequest(builder);
- } else {
- request = new SentryEventRequest(builder);
- }
- log("Request - " + request.getRequestData());
+ if (enableDebugLogging) {
+ //this string can be really big so there is no need to log if logging is disabled
+ logD("Request - " + request);
+ }
doCaptureEventPost(request);
}
+ private JSONObject createEvent(SentryEventBuilder builder) {
+ builder.setJsonArray("breadcrumbs", breadcrumbs.current());
+ if (captureListener != null) {
+
+ builder = captureListener.beforeCapture(builder);
+ if (builder == null) {
+ logW("SentryEventBuilder in captureEvent is null");
+ return null;
+ }
+ }
+ return createRequest(builder);
+ }
+
private boolean shouldAttemptPost() {
PackageManager pm = context.getPackageManager();
- int hasPerm = pm.checkPermission(permission.ACCESS_NETWORK_STATE, context.getPackageName());
- if (hasPerm != PackageManager.PERMISSION_GRANTED) {
- return false;
+ int hasPerm = pm.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, packageName);
+ if (hasPerm == PackageManager.PERMISSION_DENIED) {
+ return true;
}
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -342,523 +316,347 @@ private boolean shouldAttemptPost() {
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
- private static class ExSSLSocketFactory extends SSLSocketFactory {
- SSLContext sslContext = SSLContext.getInstance("TLS");
-
- ExSSLSocketFactory(SSLContext context) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
- super(null);
- sslContext = context;
- }
-
- @Override
- public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
- return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
- }
-
- @Override
- public Socket createSocket() throws IOException {
- return sslContext.getSocketFactory().createSocket();
- }
- }
-
- private static HttpClient getHttpsClient(HttpClient client) {
- try {
- X509TrustManager x509TrustManager = new X509TrustManager() {
- @Override
- public void checkClientTrusted(X509Certificate[] chain,
- String authType) throws CertificateException {
- }
+ private void doCaptureEventPost(final JSONObject request) {
+ if (shouldAttemptPost()) {
+ submit(new Runnable() {
@Override
- public void checkServerTrusted(X509Certificate[] chain,
- String authType) throws CertificateException {
+ public void run() {
+ sendRequest(request);
}
-
+ });
+ } else {
+ submit(new Runnable() {
@Override
- public X509Certificate[] getAcceptedIssuers() {
- return null;
+ public void run() {
+ addRequest(request);
}
- };
-
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(null, new TrustManager[]{x509TrustManager}, null);
- SSLSocketFactory sslSocketFactory = new ExSSLSocketFactory(sslContext);
- sslSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
- ClientConnectionManager clientConnectionManager = client.getConnectionManager();
- SchemeRegistry schemeRegistry = clientConnectionManager.getSchemeRegistry();
- schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
- return new DefaultHttpClient(clientConnectionManager, client.getParams());
- } catch (Exception ex) {
- return null;
+ });
}
}
- private static void doCaptureEventPost(final SentryEventRequest request) {
- final Sentry sentry = Sentry.getInstance();
-
- if (!sentry.shouldAttemptPost()) {
- InternalStorage.getInstance().addRequest(request);
- return;
- }
-
- sentry.executor.execute(new Runnable() {
- @Override
- public void run() {
- int projectId = Integer.parseInt(getProjectId(sentry.dsn));
- String url = sentry.baseUrl + "/api/" + projectId + "/store/";
-
- log("Sending to URL - " + url);
-
- HttpClient httpClient;
- if (Sentry.getInstance().verifySsl) {
- log("Using http client");
- httpClient = new DefaultHttpClient();
- } else {
- log("Using https client");
- httpClient = getHttpsClient(new DefaultHttpClient());
- }
-
- HttpPost httpPost = new HttpPost(url);
-
- int TIMEOUT_MILLISEC = (int)SECONDS.toMillis(10);
- HttpParams httpParams = httpPost.getParams();
- HttpConnectionParams.setConnectionTimeout(httpParams, TIMEOUT_MILLISEC);
- HttpConnectionParams.setSoTimeout(httpParams, TIMEOUT_MILLISEC);
-
- HttpProtocolParams.setContentCharset(httpParams, HTTP.UTF_8);
- HttpProtocolParams.setHttpElementCharset(httpParams, HTTP.UTF_8);
-
- boolean success = false;
- try {
- httpPost.setHeader("X-Sentry-Auth", createXSentryAuthHeader(sentry.dsn));
- httpPost.setHeader("User-Agent", "sentry-android/" + BuildConfig.SENTRY_ANDROID_VERSION);
- httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
-
- httpPost.setEntity(new StringEntity(request.getRequestData(), HTTP.UTF_8));
- HttpResponse httpResponse = httpClient.execute(httpPost);
+ private void submit(Runnable task) {
+ executorService.submit(task);
+ }
- int status = httpResponse.getStatusLine().getStatusCode();
- byte[] byteResp = null;
+ private void addRequest(JSONObject request) {
+ internalStorage.addRequest(request);
+ }
- // Gets the input stream and unpackages the response into a command
- if (httpResponse.getEntity() != null) {
- try {
- InputStream in = httpResponse.getEntity().getContent();
- byteResp = this.readBytes(in);
+ private void sendRequest(JSONObject request) {
+ logD("Sending to URL - " + url);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
+ HttpRequestSender.Builder builder = httpRequestSender.newBuilder();
+ if (verifySsl != 0) {
+ logD("Using http client");
+ } else {
+ logD("Using https client");
+ builder.useHttps();
+ }
- String stringResponse = null;
- Charset charsetInput = Charset.forName("UTF-8");
- CharsetDecoder decoder = charsetInput.newDecoder();
- CharBuffer cbuf = null;
- try {
- cbuf = decoder.decode(ByteBuffer.wrap(byteResp));
- stringResponse = cbuf.toString();
- } catch (CharacterCodingException e) {
- e.printStackTrace();
- }
+ builder.url(url);
+ boolean success = false;
+ try {
+ builder.header("X-Sentry-Auth", createXSentryAuthHeader());
+ builder.header("User-Agent", "sentry-android/" + BuildConfig.SENTRY_ANDROID_VERSION);
+ builder.header("Content-Type", MEDIA_TYPE);
- success = (status == 200);
+ builder.content(request.toString(), MEDIA_TYPE);
+ HttpRequestSender.Response httpResponse = builder.build().execute();
- log("SendEvent - " + status + " " + stringResponse);
- } catch (Exception e) {
- e.printStackTrace();
- }
+ int status = httpResponse.getStatusCode();
+ success = status == HTTP_OK;
- if (success) {
- InternalStorage.getInstance().removeBuilder(request);
- } else {
- InternalStorage.getInstance().addRequest(request);
- }
- }
+ logD("SendEvent - " + status + " " + httpResponse.getContent());
+ }catch (UnknownHostException unh) {
+ //LogCat does not log UnknownHostException
+ logW("UnknownHostException on sending event");
+ }
+ catch (Exception e) {
+ logW(e);
+ }
- private byte[] readBytes(InputStream inputStream) throws IOException {
- // this dynamically extends to take the bytes you read
- ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
+ if (success) {
+ internalStorage.removeBuilder(request);
+ } else {
+ addRequest(request);
+ }
+ }
- // this is storage overwritten on each iteration with bytes
- int bufferSize = 1024;
- byte[] buffer = new byte[bufferSize];
+ public void setDebugLogging(boolean enableDebugLogging) {
+ this.enableDebugLogging = enableDebugLogging;
+ }
- // we need to know how may bytes were read to write them to the byteBuffer
- int len = 0;
- while ((len = inputStream.read(buffer)) != -1) {
- byteBuffer.write(buffer, 0, len);
- }
+ private void logD(String message) {
+ if (enableDebugLogging) {
+ Log.d(TAG, message);
+ }
+ }
- // and then we can return your byte array.
- return byteBuffer.toByteArray();
- }
+ private void logW(String message) {
+ if (enableDebugLogging) {
+ Log.w(TAG, message);
+ }
+ }
- });
+ private void logW(Exception ex) {
+ if (enableDebugLogging) {
+ Log.w(TAG, ex);
+ }
+ }
+ private void logW(String message, Exception ex) {
+ logW(message, ex, enableDebugLogging);
+ }
+ private static void logW(String message, Exception ex, boolean enableDebugLogging) {
+ if (enableDebugLogging) {
+ Log.w(TAG, message, ex);
+ }
}
- private static class SentryUncaughtExceptionHandler implements UncaughtExceptionHandler {
+ private class SentryUncaughtExceptionHandler implements UncaughtExceptionHandler {
- private final InternalStorage storage;
- private final UncaughtExceptionHandler defaultExceptionHandler;
+ UncaughtExceptionHandler defaultExceptionHandler;
+ boolean disabled;
// constructor
- public SentryUncaughtExceptionHandler(UncaughtExceptionHandler pDefaultExceptionHandler, InternalStorage storage) {
+ public SentryUncaughtExceptionHandler(UncaughtExceptionHandler pDefaultExceptionHandler) {
defaultExceptionHandler = pDefaultExceptionHandler;
- this.storage = storage;
}
@Override
public void uncaughtException(Thread thread, Throwable e) {
-
- final Sentry sentry = Sentry.getInstance();
-
- // Here you should have a more robust, permanent record of problems
- SentryEventBuilder builder = new SentryEventBuilder(e, SentryEventLevel.FATAL);
- builder.setRelease(sentry.appInfo.versionName);
- builder.event.put("breadcrumbs", sentry.breadcrumbs.current());
-
- if (sentry.captureListener != null) {
- builder = sentry.captureListener.beforeCapture(builder);
+ if (disabled) {
+ return;
}
+ // Here you should have a more robust, permanent record of problems
+ SentryEventBuilder builder = newEventBuilder(e, SentryEventBuilder.SentryEventLevel.FATAL);
- if (builder != null) {
- builder.event.put("contexts", sentry.contexts);
- storage.addRequest(new SentryEventRequest(builder));
+ JSONObject event = createEvent(builder);
+ if (event != null) {
+ addRequest(event);
} else {
- Log.e(Sentry.TAG, "SentryEventBuilder in uncaughtException is null");
+ logW("SentryEventBuilder in uncaughtException is null");
}
//call original handler
- defaultExceptionHandler.uncaughtException(thread, e);
+ if (defaultExceptionHandler != null) {
+ defaultExceptionHandler.uncaughtException(thread, e);
+ }
}
+ }
+ private static JSONObject createRequest(SentryEventBuilder builder) {
+ return builder.toJson();
}
- private static class InternalStorage {
+ public SentryEventBuilder newEventBuilder(Throwable e, SentryEventBuilder.SentryEventLevel level) {
+ return new SentryEventBuilder(e, level, getCause(e, e.getMessage()), enableDebugLogging);
+ }
- private final static String FILE_NAME = "unsent_requests";
- private final List unsentRequests;
+ private final class InternalStorage {
- private static InternalStorage getInstance() {
- return LazyHolder.instance;
- }
+ private List unsentRequests;
+ private String fileName;
- private static class LazyHolder {
- private static final InternalStorage instance = new InternalStorage();
- }
-
- private InternalStorage() {
- Context context = Sentry.getInstance().context;
- try {
- File unsetRequestsFile = new File(context.getFilesDir(), FILE_NAME);
- if (!unsetRequestsFile.exists()) {
- writeObject(context, new ArrayList());
- }
- } catch (Exception e) {
- Log.e(TAG, "Error initializing storage", e);
- }
- this.unsentRequests = this.readObject(context);
+ private InternalStorage(String fileName) {
+ this.fileName = fileName;
}
/**
* @return the unsentRequests
*/
- public List getUnsentRequests() {
- final List copy = new ArrayList<>();
+ public List getUnsentRequests() {
synchronized (this) {
- copy.addAll(unsentRequests);
+ return new ArrayList(lazyGetUnsentRequests());
}
- return copy;
}
- public void addRequest(SentryEventRequest request) {
- synchronized (this) {
- log("Adding request - " + request.uuid);
- if (!this.unsentRequests.contains(request)) {
- this.unsentRequests.add(request);
- this.writeObject(Sentry.getInstance().context, this.unsentRequests);
- }
+ private List lazyGetUnsentRequests() {
+ if (unsentRequests == null) {
+ unsentRequests = readRequests();
}
+ return unsentRequests;
}
- public void removeBuilder(SentryEventRequest request) {
+ public void addRequest(JSONObject request) {
synchronized (this) {
- log("Removing request - " + request.uuid);
- this.unsentRequests.remove(request);
- this.writeObject(Sentry.getInstance().context, this.unsentRequests);
- }
- }
-
- private void writeObject(Context context, List requests) {
- try {
- FileOutputStream fos = context.openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
- ObjectOutputStream oos = new ObjectOutputStream(fos);
- oos.writeObject(requests);
- oos.close();
- fos.close();
- } catch (IOException e) {
- Log.e(TAG, "Error saving to storage", e);
+ logD("Adding request - " + getEventId(request));
+ if (!contains(request)) {
+ lazyGetUnsentRequests().add(request);
+ writeRequests();
+ }
}
}
- private List readObject(Context context) {
- try {
- FileInputStream fis = context.openFileInput(FILE_NAME);
- ObjectInputStream ois = new ObjectInputStream(fis);
- List requests = (ArrayList) ois.readObject();
- ois.close();
- fis.close();
- return requests;
- } catch (IOException | ClassNotFoundException e) {
- Log.e(TAG, "Error loading from storage", e);
+ private boolean contains(JSONObject request) {
+ List list = lazyGetUnsentRequests();
+ for (JSONObject object : list) {
+ if (getEventId(object).equals(getEventId(request))) {
+ return true;
+ }
}
- return new ArrayList<>();
+ return false;
}
- }
-
- public interface SentryEventCaptureListener {
-
- SentryEventBuilder beforeCapture(SentryEventBuilder builder);
-
- }
- private final static class Breadcrumb {
-
- enum Type {
-
- Default("default"),
- HTTP("http"),
- Navigation("navigation");
-
- private final String value;
-
- Type(String value) {
- this.value = value;
+ public void removeBuilder(JSONObject request) {
+ synchronized (this) {
+ logD("Removing request - " + getEventId(request));
+ lazyGetUnsentRequests().remove(request);
+ writeRequests();
}
}
- final long timestamp;
- final Type type;
- final String message;
- final String category;
- final SentryEventLevel level;
- final Map data = new HashMap<>();
-
- Breadcrumb(long timestamp, Type type, String message, String category, SentryEventLevel level) {
- this.timestamp = timestamp;
- this.type = type;
- this.message = message;
- this.category = category;
- this.level = level;
- }
- }
-
- private static class Breadcrumbs {
-
- // The max number of breadcrumbs that will be tracked at any one time.
- private static final int MAX_BREADCRUMBS = 10;
-
-
- // Access to this list must be thread-safe.
- // See GitHub Issue #110
- // This list is protected by the provided ReadWriteLock.
- private final LinkedList breadcrumbs = new LinkedList<>();
- private final ReadWriteLock lock = new ReentrantReadWriteLock();
-
- void push(Breadcrumb b) {
+ private void writeRequests() {
+ OutputStream fos = null;
try {
- lock.writeLock().lock();
- while (breadcrumbs.size() >= MAX_BREADCRUMBS) {
- breadcrumbs.removeFirst();
+
+ JSONArray jsonArray = new JSONArray();
+ Iterable extends JSONObject> requests = lazyGetUnsentRequests();
+ for (JSONObject request : requests) {
+ jsonArray.put(request);
}
- breadcrumbs.add(b);
+ String s = jsonArray.toString();
+ fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);
+ fos.write(s.getBytes("UTF-8"));
+ } catch (IOException e) {
+ logW(e);
} finally {
- lock.writeLock().unlock();
+ try {
+ if (fos != null) {
+ fos.close();
+ }
+ } catch (IOException e) {
+ logW(e);
+ }
}
}
- JSONArray current() {
- final JSONArray crumbs = new JSONArray();
+ private List readRequests() {
+ Reader reader = null;
try {
- lock.readLock().lock();
- for (Breadcrumb breadcrumb : breadcrumbs) {
- final JSONObject json = new JSONObject();
- json.put("timestamp", breadcrumb.timestamp);
- json.put("type", breadcrumb.type.value);
- json.put("message", breadcrumb.message);
- json.put("category", breadcrumb.category);
- json.put("level", breadcrumb.level.value);
- json.put("data", new JSONObject(breadcrumb.data));
- crumbs.put(json);
+ StringWriter sw = new StringWriter();
+ reader = new InputStreamReader(context.openFileInput(fileName));
+ char[] buffer = new char[DEFAULT_BUFFER_SIZE];
+ int n;
+ while (-1 != (n = reader.read(buffer))) {
+ sw.write(buffer, 0, n);
+ }
+ JSONArray jsonArray = new JSONArray(sw.toString());
+ List requests = new ArrayList<>(jsonArray.length());
+ for (int i = 0; i < jsonArray.length(); i++) {
+ requests.add(jsonArray.getJSONObject(i));
}
+ return requests;
} catch (Exception e) {
- Log.e(TAG, "Error serializing breadcrumbs", e);
+ logW(e);
} finally {
- lock.readLock().unlock();
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ logW(e);
+ }
+ }
}
- return crumbs;
+ return new ArrayList();
}
-
- }
-
- /**
- * Record a breadcrumb to log a navigation from `from` to `to`.
- * @param category A category to label the event under. This generally is similar to a logger
- * name, and will let you more easily understand the area an event took place, such as auth.
- * @param from A string representing the original application state / location.
- * @param to A string representing the new application state / location.
- *
- * @see com.joshdholtz.sentry.Sentry#addHttpBreadcrumb(String, String, int)
- */
- public static void addNavigationBreadcrumb(String category, String from, String to) {
- final Breadcrumb b = new Breadcrumb(
- System.currentTimeMillis() / 1000,
- Breadcrumb.Type.Navigation,
- "",
- category,
- SentryEventLevel.INFO);
-
- b.data.put("from", from);
- b.data.put("to", to);
- getInstance().breadcrumbs.push(b);
- }
-
- /**
- * Record a HTTP request breadcrumb. This represents an HTTP request transmitted from your
- * application. This could be an AJAX request from a web application, or a server-to-server HTTP
- * request to an API service provider, etc.
- *
- * @param url The request URL.
- * @param method The HTTP request method.
- * @param statusCode The HTTP status code of the response.
- *
- * @see com.joshdholtz.sentry.Sentry#addHttpBreadcrumb(String, String, int)
- */
- public static void addHttpBreadcrumb(String url, String method, int statusCode) {
- final String reason = EnglishReasonPhraseCatalog.INSTANCE.getReason(statusCode, Locale.US);
- final Breadcrumb b = new Breadcrumb(
- System.currentTimeMillis() / 1000,
- Breadcrumb.Type.HTTP,
- "",
- String.format("http.%s", method.toLowerCase()),
- SentryEventLevel.INFO);
-
- b.data.put("url", url);
- b.data.put("method", method);
- b.data.put("status_code", Integer.toString(statusCode));
- b.data.put("reason", reason);
- getInstance().breadcrumbs.push(b);
}
- /**
- * Sentry supports a concept called Breadcrumbs, which is a trail of events which happened prior
- * to an issue. Often times these events are very similar to traditional logs, but also have the
- * ability to record more rich structured data.
- *
- * @param category A category to label the event under. This generally is similar to a logger
- * name, and will let you more easily understand the area an event took place,
- * such as auth.
- *
- * @param message A string describing the event. The most common vector, often used as a drop-in
- * for a traditional log message.
- *
- * See https://docs.sentry.io/hosted/learn/breadcrumbs/
- *
- */
- public static void addBreadcrumb(String category, String message) {
- getInstance().breadcrumbs.push(new Breadcrumb(
- System.currentTimeMillis() / 1000,
- Breadcrumb.Type.Default,
- message,
- category,
- SentryEventLevel.INFO));
+ private static String getEventId(JSONObject object) {
+ return object.optString(EVENT_ID);
}
- public static class SentryEventRequest implements Serializable {
- private final String requestData;
- private final UUID uuid;
-
- public SentryEventRequest(SentryEventBuilder builder) {
- this.requestData = new JSONObject(builder.event).toString();
- this.uuid = UUID.randomUUID();
- }
-
- /**
- * @return the requestData
- */
- public String getRequestData() {
- return requestData;
- }
-
- /**
- * @return the uuid
- */
- public UUID getUuid() {
- return uuid;
- }
-
- @Override
- public boolean equals(Object other) {
- SentryEventRequest otherRequest = (SentryEventRequest) other;
-
- if (this.uuid != null && otherRequest.uuid != null) {
- return uuid.equals(otherRequest.uuid);
- }
-
- return false;
- }
+ public interface SentryEventCaptureListener {
+ SentryEventBuilder beforeCapture(SentryEventBuilder builder);
}
+
/**
* The Sentry server assumes the time is in UTC.
* The timestamp should be in ISO 8601 format, without a timezone.
*/
- private static DateFormat iso8601() {
+ private static SimpleDateFormat iso8601() {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
- public static class SentryEventBuilder implements Serializable {
+ public static final class SentryEventBuilder {
+
+ static final Pattern IS_INTERNAL_PACKAGE = Pattern.compile("^(java|android|com\\.android|com\\.google\\.android|dalvik\\.system)\\..*");
+ private static final ThreadLocal sdf = new ThreadLocal() {
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return iso8601();
+ }
+ };
+ private static final Pattern PATTERN = Pattern.compile("-", Pattern.LITERAL);
+ private JSONObject event;
+
+ // Convert a StackTraceElement to a sentry.interfaces.stacktrace.Stacktrace JSON object.
+ static JSONObject frameJson(StackTraceElement ste) throws JSONException {
+ final JSONObject frame = new JSONObject();
+
+ final String method = ste.getMethodName();
+ if (!TextUtils.isEmpty(method)) {
+ frame.put("function", method);
+ }
+
+ final String fileName = ste.getFileName();
+ if (!TextUtils.isEmpty(fileName)) {
+ frame.put("filename", fileName);
+ }
- private static final long serialVersionUID = -8589756678369463988L;
+ int lineno = ste.getLineNumber();
+ if (!ste.isNativeMethod() && lineno >= 0) {
+ frame.put("lineno", lineno);
+ }
- // Match packages names that start with some well-known internal class-paths:
- // java.*
- // android.*
- // com.android.*
- // com.google.android.*
- // dalvik.system.*
- static final String isInternalPackage = "^(java|android|com\\.android|com\\.google\\.android|dalvik\\.system)\\..*";
+ String className = ste.getClassName();
+ frame.put("module", className);
- private final static DateFormat timestampFormat = iso8601();
+ // Take out some of the system packages to improve the exception folding on the sentry server
+ frame.put("in_app", !IS_INTERNAL_PACKAGE.matcher(className).matches());
- private final Map event;
+ return frame;
+ }
- public JSONObject toJSON() {
- return new JSONObject(event);
+ public SentryEventBuilder setContexts(JSONObject contexts) {
+ return setJsonObject("contexts", contexts);
}
- public SentryEventBuilder() {
- event = new HashMap<>();
- event.put("event_id", UUID.randomUUID().toString().replace("-", ""));
- event.put("platform", "java");
- this.setTimestamp(System.currentTimeMillis());
+ public enum SentryEventLevel {
+
+ FATAL("fatal"),
+ ERROR("error"),
+ WARNING("warning"),
+ INFO("info"),
+ DEBUG("debug");
+ private String value;
+
+ SentryEventLevel(String value) {
+ this.value = value;
+ }
+
}
- public SentryEventBuilder(Throwable t, SentryEventLevel level) {
- this();
+ private boolean enableLogging;
+
+ SentryEventBuilder(boolean enableLogging) {
+ this.enableLogging = enableLogging;
+ event = new JSONObject();
+ setString(EVENT_ID, PATTERN.matcher(UUID.randomUUID().toString()).replaceAll(Matcher.quoteReplacement("")));
+ setString("platform", "java");
+ setTimestamp(System.currentTimeMillis());
+ }
- String culprit = getCause(t, t.getMessage());
+ private SentryEventBuilder(Throwable t, SentryEventLevel level, String cause, boolean enableLogging) {
+ this(enableLogging);
- this.setMessage(t.getMessage())
- .setCulprit(culprit)
- .setLevel(level)
- .setException(t);
+ setMessage(t.getMessage()).setCulprit(cause).setLevel(level).setException(t);
}
/**
@@ -868,8 +666,7 @@ public SentryEventBuilder(Throwable t, SentryEventLevel level) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setMessage(String message) {
- event.put("message", message);
- return this;
+ return setString("message", message);
}
/**
@@ -879,8 +676,7 @@ public SentryEventBuilder setMessage(String message) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setTimestamp(long timestamp) {
- event.put("timestamp", timestampFormat.format(new Date(timestamp)));
- return this;
+ return setString("timestamp", sdf.get().format(new Date(timestamp)));
}
/**
@@ -890,8 +686,7 @@ public SentryEventBuilder setTimestamp(long timestamp) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setLevel(SentryEventLevel level) {
- event.put("level", level.value);
- return this;
+ return setString("level", level.value);
}
/**
@@ -901,8 +696,7 @@ public SentryEventBuilder setLevel(SentryEventLevel level) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setLogger(String logger) {
- event.put("logger", logger);
- return this;
+ return setString("logger", logger);
}
/**
@@ -912,7 +706,20 @@ public SentryEventBuilder setLogger(String logger) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setCulprit(String culprit) {
- event.put("culprit", culprit);
+ return setString("culprit", culprit);
+ }
+
+ private SentryEventBuilder setString(String key, String culprit) {
+ return putSafely(key, culprit);
+ }
+
+ private SentryEventBuilder putSafely(String key, Object object) {
+ try {
+ event.put(key, object);
+ } catch (JSONException e) {
+ //there should be no exception
+ logW("", e, enableLogging);
+ }
return this;
}
@@ -926,16 +733,15 @@ public SentryEventBuilder setUser(Map user) {
}
public SentryEventBuilder setUser(JSONObject user) {
- event.put("user", user);
- return this;
+ return setJsonObject("user", user);
}
public JSONObject getUser() {
- if (!event.containsKey("user")) {
- setTags(new HashMap());
+ if (!event.has("user")) {
+ setUser(new JSONObject());
}
- return (JSONObject) event.get("user");
+ return (JSONObject) event.opt("user");
}
/**
@@ -947,11 +753,6 @@ public SentryEventBuilder setTags(Map tags) {
return this;
}
- public SentryEventBuilder setTags(JSONObject tags) {
- event.put("tags", tags);
- return this;
- }
-
public SentryEventBuilder addTag(String key, String value) {
try {
getTags().put(key, value);
@@ -962,12 +763,34 @@ public SentryEventBuilder addTag(String key, String value) {
return this;
}
+ public SentryEventBuilder addExtra(String key, String value) {
+ try {
+ getExtra().put(key, value);
+ } catch (JSONException e) {
+ Log.e(Sentry.TAG, "Error adding extra in SentryEventBuilder");
+ }
+
+ return this;
+ }
+
+ public SentryEventBuilder setTags(JSONObject tags) {
+ return setJsonObject("tags", tags);
+ }
+
+ public SentryEventBuilder setJsonObject(String key, JSONObject object) {
+ return putSafely(key, object);
+ }
+
+ public SentryEventBuilder setJsonArray(String key, JSONArray object) {
+ return putSafely(key, object);
+ }
+
public JSONObject getTags() {
- if (!event.containsKey("tags")) {
- setTags(new HashMap());
+ if (!event.has("tags")) {
+ setTags(new JSONObject());
}
- return (JSONObject) event.get("tags");
+ return (JSONObject) event.opt("tags");
}
/**
@@ -975,8 +798,15 @@ public JSONObject getTags() {
* @return SentryEventBuilder
*/
public SentryEventBuilder setServerName(String serverName) {
- event.put("server_name", serverName);
- return this;
+ return setString("server_name", serverName);
+ }
+
+ /**
+ * @param environment The environment name, such as production or staging
+ * @return SentryEventBuilder
+ */
+ public SentryEventBuilder setEnvironment(String environment) {
+ return setString("environment", environment);
}
/**
@@ -984,8 +814,7 @@ public SentryEventBuilder setServerName(String serverName) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setRelease(String release) {
- event.put("release", release);
- return this;
+ return setString("release", release);
}
/**
@@ -995,18 +824,22 @@ public SentryEventBuilder setRelease(String release) {
*/
public SentryEventBuilder addModule(String name, String version) {
JSONArray modules;
- if (!event.containsKey("modules")) {
- modules = new JSONArray();
- event.put("modules", modules);
- } else {
- modules = (JSONArray) event.get("modules");
- }
+ try {
- if (name != null && version != null) {
- String[] module = {name, version};
- modules.put(new JSONArray(Arrays.asList(module)));
- }
+ if (!event.has("modules")) {
+ modules = new JSONArray();
+ event.put("modules", modules);
+ } else {
+ modules = (JSONArray) event.get("modules");
+ }
+ if (name != null && version != null) {
+ String[] module = {name, version};
+ modules.put(new JSONArray(Arrays.asList(module)));
+ }
+ } catch (JSONException e) {
+ logW("", e, enableLogging);
+ }
return this;
}
@@ -1015,31 +848,19 @@ public SentryEventBuilder addModule(String name, String version) {
* @return SentryEventBuilder
*/
public SentryEventBuilder setExtra(Map extra) {
- setExtra(new JSONObject(extra));
- return this;
+ return setExtra(new JSONObject(extra));
}
public SentryEventBuilder setExtra(JSONObject extra) {
- event.put("extra", extra);
- return this;
- }
-
- public SentryEventBuilder addExtra(String key, String value) {
- try {
- getExtra().put(key, value);
- } catch (JSONException e) {
- Log.e(Sentry.TAG, "Error adding extra in SentryEventBuilder");
- }
-
- return this;
+ return setJsonObject("extra", extra);
}
public JSONObject getExtra() {
- if (!event.containsKey("extra")) {
- setExtra(new HashMap());
+ if (!event.has("extra")) {
+ setExtra(new JSONObject());
}
- return (JSONObject) event.get("extra");
+ return (JSONObject) event.opt("extra");
}
/**
@@ -1078,9 +899,24 @@ public SentryEventBuilder setException(Throwable t) {
return this;
}
+ /**
+ * Add a stack trace to the event.
+ * A stack trace for the current thread can be obtained by using
+ * `Thread.currentThread().getStackTrace()`.
+ *
+ * @param stackTrace stacktrace
+ * @return same builder
+ * @see Thread#currentThread()
+ * @see Thread#getStackTrace()
+ */
+ public SentryEventBuilder setStackTrace(StackTraceElement[] stackTrace) {
+ setJsonObject("stacktrace", getStackTrace(stackTrace));
+ return this;
+ }
+
private static JSONObject getStackTrace(StackTraceElement[] stackFrames) {
- JSONObject stacktrace = new JSONObject();
+ JSONObject stacktrace = new JSONObject();
try {
JSONArray frameList = new JSONArray();
@@ -1108,184 +944,157 @@ private static JSONObject getStackTrace(StackTraceElement[] stackFrames) {
return stacktrace;
}
- /**
- * Add a stack trace to the event.
- * A stack trace for the current thread can be obtained by using
- * `Thread.currentThread().getStackTrace()`.
- *
- * @see Thread#currentThread()
- * @see Thread#getStackTrace()
- */
- public SentryEventBuilder setStackTrace(StackTraceElement[] stackTrace) {
- this.event.put("stacktrace", getStackTrace(stackTrace));
- return this;
+ private JSONObject toJson() {
+ return event;
}
+ }
- // Convert a StackTraceElement to a sentry.interfaces.stacktrace.Stacktrace JSON object.
- static JSONObject frameJson(StackTraceElement ste) throws JSONException {
- final JSONObject frame = new JSONObject();
+ private final static class Breadcrumb {
- final String method = ste.getMethodName();
- if (Present(method)) {
- frame.put("function", method);
- }
+ enum Type {
- final String fileName = ste.getFileName();
- if (Present(fileName)) {
- frame.put("filename", fileName);
- }
+ Default("default"),
+ HTTP("http"),
+ Navigation("navigation");
- int lineno = ste.getLineNumber();
- if (!ste.isNativeMethod() && lineno >= 0) {
- frame.put("lineno", lineno);
- }
+ private final String value;
- String className = ste.getClassName();
- frame.put("module", className);
+ Type(String value) {
+ this.value = value;
+ }
+ }
- // Take out some of the system packages to improve the exception folding on the sentry server
- frame.put("in_app", !className.matches(isInternalPackage));
+ final long timestamp;
+ final Type type;
+ final String message;
+ final String category;
+ final SentryEventBuilder.SentryEventLevel level;
+ final Map data = new HashMap<>();
- return frame;
+ Breadcrumb(long timestamp, Type type, String message, String category, SentryEventBuilder.SentryEventLevel level) {
+ this.timestamp = timestamp;
+ this.type = type;
+ this.message = message;
+ this.category = category;
+ this.level = level;
}
}
- /**
- * Store a tuple of package version information captured from PackageInfo
- *
- * @see PackageInfo
- */
- private final static class AppInfo {
- final static AppInfo Empty = new AppInfo("", "", 0);
- final String name;
- final String versionName;
- final int versionCode;
+ private static class Breadcrumbs {
+
+ // The max number of breadcrumbs that will be tracked at any one time.
+ private static final int MAX_BREADCRUMBS = 10;
+
+
+ // Access to this list must be thread-safe.
+ // See GitHub Issue #110
+ // This list is protected by the provided ReadWriteLock.
+ private final LinkedList breadcrumbs = new LinkedList<>();
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
- AppInfo(String name, String versionName, int versionCode) {
- this.name = name;
- this.versionName = versionName;
- this.versionCode = versionCode;
+ void push(Breadcrumb b) {
+ try {
+ lock.writeLock().lock();
+ while (breadcrumbs.size() >= MAX_BREADCRUMBS) {
+ breadcrumbs.removeFirst();
+ }
+ breadcrumbs.add(b);
+ } finally {
+ lock.writeLock().unlock();
+ }
}
- static AppInfo Read(final Context context) {
+ JSONArray current() {
+ final JSONArray crumbs = new JSONArray();
try {
- final PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
- return new AppInfo(info.packageName, info.versionName, info.versionCode);
+ lock.readLock().lock();
+ for (Breadcrumb breadcrumb : breadcrumbs) {
+ final JSONObject json = new JSONObject();
+ json.put("timestamp", breadcrumb.timestamp);
+ json.put("type", breadcrumb.type.value);
+ json.put("message", breadcrumb.message);
+ json.put("category", breadcrumb.category);
+ json.put("level", breadcrumb.level.value);
+ json.put("data", new JSONObject(breadcrumb.data));
+ crumbs.put(json);
+ }
} catch (Exception e) {
- Log.e(TAG, "Error reading package context", e);
- return Empty;
+ Log.e(TAG, "Error serializing breadcrumbs", e);
+ } finally {
+ lock.readLock().unlock();
}
+ return crumbs;
}
- }
-
- private static JSONObject readContexts(Context context, AppInfo appInfo) {
- final JSONObject contexts = new JSONObject();
- try {
- contexts.put("os", osContext());
- contexts.put("device", deviceContext(context));
- contexts.put("package", packageContext(appInfo));
- } catch (JSONException e) {
- Log.e(TAG, "Failed to build device contexts", e);
- }
- return contexts;
}
/**
- * Read the device and build into a map.
- *
- * Not implemented:
- * - battery_level
- * If the device has a battery this can be an integer defining the battery level (in
- * the range 0-100). (Android requires registration of an intent to query the battery).
- * - name
- * The name of the device. This is typically a hostname.
- *
- * See https://docs.getsentry.com/hosted/clientdev/interfaces/#context-types
+ * Record a breadcrumb to log a navigation from `from` to `to`.
+ *
+ * @param category A category to label the event under. This generally is similar to a logger
+ * name, and will let you more easily understand the area an event took place, such as auth.
+ * @param from A string representing the original application state / location.
+ * @param to A string representing the new application state / location.
+ * @see com.joshdholtz.sentry.Sentry#addHttpBreadcrumb(String, String, int)
*/
- private static JSONObject deviceContext(Context context) {
- final JSONObject device = new JSONObject();
- try {
- // The family of the device. This is normally the common part of model names across
- // generations. For instance iPhone would be a reasonable family, so would be Samsung Galaxy.
- device.put("family", Build.BRAND);
-
- // The model name. This for instance can be Samsung Galaxy S3.
- device.put("model", Build.PRODUCT);
-
- // An internal hardware revision to identify the device exactly.
- device.put("model_id", Build.MODEL);
-
- final String architecture = System.getProperty("os.arch");
- if (Present(architecture)) {
- device.put("arch", architecture);
- }
-
- final int orient = context.getResources().getConfiguration().orientation;
- device.put("orientation", orient == Configuration.ORIENTATION_LANDSCAPE ?
- "landscape" : "portrait");
-
- // Read screen resolution in the format "800x600"
- // Normalised to have wider side first.
- final Object windowManager = context.getSystemService(Context.WINDOW_SERVICE);
- if (windowManager != null && windowManager instanceof WindowManager) {
- final DisplayMetrics metrics = new DisplayMetrics();
- ((WindowManager) windowManager).getDefaultDisplay().getMetrics(metrics);
- device.put("screen_resolution",
- String.format("%sx%s",
- Math.max(metrics.widthPixels, metrics.heightPixels),
- Math.min(metrics.widthPixels, metrics.heightPixels)));
- }
-
- } catch (Exception e) {
- Log.e(TAG, "Error reading device context", e);
- }
- return device;
- }
-
- private static JSONObject osContext() {
- final JSONObject os = new JSONObject();
- try {
- os.put("type", "os");
- os.put("name", "Android");
- os.put("version", Build.VERSION.RELEASE);
- if (Build.VERSION.SDK_INT < 4) {
- os.put("build", Build.VERSION.SDK);
- } else {
- os.put("build", Integer.toString(Build.VERSION.SDK_INT));
- }
- final String kernelVersion = System.getProperty("os.version");
- if (Present(kernelVersion)) {
- os.put("kernel_version", kernelVersion);
- }
+ public void addNavigationBreadcrumb(String category, String from, String to) {
+ final Breadcrumb b = new Breadcrumb(
+ System.currentTimeMillis() / 1000,
+ Breadcrumb.Type.Navigation,
+ "",
+ category,
+ SentryEventBuilder.SentryEventLevel.INFO);
- } catch (Exception e) {
- Log.e(TAG, "Error reading OS context", e);
- }
- return os;
+ b.data.put("from", from);
+ b.data.put("to", to);
+ breadcrumbs.push(b);
}
/**
- * Read the package data into map to be sent as an event context item.
- * This is not a built-in context type.
+ * Record a HTTP request breadcrumb. This represents an HTTP request transmitted from your
+ * application. This could be an AJAX request from a web application, or a server-to-server HTTP
+ * request to an API service provider, etc.
+ *
+ * @param url The request URL.
+ * @param method The HTTP request method.
+ * @param statusCode The HTTP status code of the response.
+ * @see com.joshdholtz.sentry.Sentry#addHttpBreadcrumb(String, String, int)
*/
- private static JSONObject packageContext(AppInfo appInfo) {
- final JSONObject pack = new JSONObject();
- try {
- pack.put("type", "package");
- pack.put("name", appInfo.name);
- pack.put("version_name", appInfo.versionName);
- pack.put("version_code", Integer.toString(appInfo.versionCode));
- } catch (JSONException e) {
- Log.e(TAG, "Error reading package context", e);
- }
- return pack;
+ public void addHttpBreadcrumb(String url, String method, int statusCode) {
+ final Breadcrumb b = new Breadcrumb(
+ System.currentTimeMillis() / 1000,
+ Breadcrumb.Type.HTTP,
+ "",
+ String.format("http.%s", method.toLowerCase()),
+ SentryEventBuilder.SentryEventLevel.INFO);
+
+ b.data.put("url", url);
+ b.data.put("method", method);
+ b.data.put("status_code", Integer.toString(statusCode));
+ breadcrumbs.push(b);
}
/**
- * Take the idea of `present?` from ActiveSupport.
+ * Sentry supports a concept called Breadcrumbs, which is a trail of events which happened prior
+ * to an issue. Often times these events are very similar to traditional logs, but also have the
+ * ability to record more rich structured data.
+ *
+ * @param category A category to label the event under. This generally is similar to a logger
+ * name, and will let you more easily understand the area an event took place,
+ * such as auth.
+ * @param message A string describing the event. The most common vector, often used as a drop-in
+ * for a traditional log message.
+ *