diff --git a/.gitignore b/.gitignore index 5377aa5..021a2cd 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,6 @@ local.properties .classpath .gradle -/local.properties -/.idea/workspace.xml -/.idea/misc.xml -/.idea/libraries .DS_Store -/build +*.iml +.idea/ diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 8d27c20..11b6830 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -10,11 +10,21 @@ + + - - \ No newline at end of file + diff --git a/.idea/modules.xml b/.idea/modules.xml index 8503bf1..b911b92 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -4,6 +4,8 @@ + + diff --git a/build.gradle b/build.gradle index 9441bcd..6243a6f 100644 --- a/build.gradle +++ b/build.gradle @@ -13,8 +13,37 @@ buildscript { allprojects { repositories { jcenter() + mavenLocal() maven { url 'https://dl.bintray.com/joshdholtz/maven/' } } + + ext { + bintrayRepo = 'maven' + bintrayName = 'sentry-android' + + publishedGroupId = 'com.joshdholtz.sentry' + + + siteUrl = 'https://github.com/nuuneoi/FBLikeAndroid' + gitUrl = 'https://github.com/nuuneoi/FBLikeAndroid.git' + + libraryVersion = '1.5.5' + + developerId = 'joshdholtz' + developerName = 'Josh Holtz' + developerEmail = 'josh@rokkincat.com' + + licenseName = 'The MIT License (MIT)' + licenseUrl = 'http://opensource.org/licenses/mit-license.php' + allLicenses = ["MIT"] + + ext_compileSdkVersion = 25 + ext_buildToolsVersion = "25.0.2" + + ext_minSdkVersion = 10 + ext_targetSdkVersion = 25 + } + } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bb7f185..54ff6f2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/sentry-android-okhttp-client/.gitignore b/sentry-android-okhttp-client/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sentry-android-okhttp-client/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sentry-android-okhttp-client/build.gradle b/sentry-android-okhttp-client/build.gradle new file mode 100644 index 0000000..adbee6f --- /dev/null +++ b/sentry-android-okhttp-client/build.gradle @@ -0,0 +1,54 @@ +plugins { + id "com.jfrog.bintray" version "1.7.3" +} +apply plugin: 'com.android.library' +apply plugin: 'com.github.dcendents.android-maven' + + +android { + compileSdkVersion ext_compileSdkVersion + buildToolsVersion ext_buildToolsVersion + + defaultConfig { + minSdkVersion ext_minSdkVersion + targetSdkVersion ext_targetSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + + +ext { + libraryName = 'sentry-android-okhttp-client' + artifact = 'sentry-android-okhttp-client' + libraryDescription = 'A Sentry okhttp client for Android' +} + +dependencies { + compile "com.squareup.okhttp3:okhttp:3.6.0" + compile project(":sentry-android") +// compile "com.joshdholtz.sentry:sentry-android:${libraryVersion}" +} + +apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' +apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' + +uploadArchives { + repositories { + mavenDeployer { + repository(url: nexusReleaseRepository) { + authentication(userName: nexusUserName, password: nexusPassword) + } + snapshotRepository(url: nexusSnapshotRepository) { + authentication(userName: nexusUserName, password: nexusPassword) + } + } + } +} diff --git a/sentry-android-okhttp-client/sentry-android-okhttp-client.iml b/sentry-android-okhttp-client/sentry-android-okhttp-client.iml new file mode 100644 index 0000000..f671aaa --- /dev/null +++ b/sentry-android-okhttp-client/sentry-android-okhttp-client.iml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sentry-android-okhttp-client/src/main/AndroidManifest.xml b/sentry-android-okhttp-client/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4360b07 --- /dev/null +++ b/sentry-android-okhttp-client/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/sentry-android-okhttp-client/src/main/java/com/joshdholtz/sentry/okhttp/OkHttpRequestSender.java b/sentry-android-okhttp-client/src/main/java/com/joshdholtz/sentry/okhttp/OkHttpRequestSender.java new file mode 100644 index 0000000..8497ea1 --- /dev/null +++ b/sentry-android-okhttp-client/src/main/java/com/joshdholtz/sentry/okhttp/OkHttpRequestSender.java @@ -0,0 +1,118 @@ +package com.joshdholtz.sentry.okhttp; + +import com.joshdholtz.sentry.HttpRequestSender; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; + +public class OkHttpRequestSender implements HttpRequestSender { + + private static class OkBuilder implements Builder { + + private static class OkResponse implements Response { + + private final int code; + private final String bodyContent; + + public OkResponse(int code, String bodyContent) { + this.code = code; + this.bodyContent = bodyContent; + } + + @Override + public int getStatusCode() { + return code; + } + + @Override + public String getContent() { + return bodyContent; + } + } + + private final okhttp3.Request.Builder builder; + private OkHttpClient okHttpClient; + private boolean useHttps; + private String url; + + private OkBuilder(OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + builder = new okhttp3.Request.Builder(); + } + + @Override + public Request build() throws Exception { + final Call call = okHttpClient.newCall(builder.build()); + return new Request() { + @Override + public Response execute() throws Exception { + okhttp3.Response response = call.execute(); + try { + ResponseBody body = response.body(); + String bodyContent = null; + if (body != null) { + bodyContent = body.string(); + } + + return new OkResponse(response.code(), bodyContent); + } finally { + if (response.body() != null) { + response.body().close(); + } + } + } + }; + } + + @Override + public Builder useHttps() { + useHttps = true; + if (url != null) { + addHttps(); + } + return this; + } + + private void addHttps() { + if (!url.startsWith("http")) { + url = "https://" + url; + } + } + + @Override + public Builder url(String url) { + this.url = url; + if (useHttps) { + addHttps(); + } + builder.url(this.url); + return this; + } + + @Override + public Builder header(String headerName, String headerValue) { + builder.header(headerName, headerValue); + return this; + } + + @Override + public Builder content(String requestData, String mediaType) { + builder.post(RequestBody.create(MediaType.parse(mediaType), requestData)); + return this; + } + } + + private OkHttpClient okHttpClient; + + public OkHttpRequestSender(OkHttpClient okHttpClient) { + this.okHttpClient = okHttpClient; + } + + @Override + public Builder newBuilder() { + return new OkBuilder(okHttpClient); + } +} diff --git a/sentry-android/build.gradle b/sentry-android/build.gradle index 44300b1..f36ac2f 100644 --- a/sentry-android/build.gradle +++ b/sentry-android/build.gradle @@ -1,26 +1,22 @@ plugins { - id "com.jfrog.bintray" version "1.7" + id "com.jfrog.bintray" version "1.7.3" } apply plugin: 'com.android.library' - -def SentryAndroidVersion = "1.5.4" - android { - compileSdkVersion 24 - buildToolsVersion "23.0.3" - - useLibrary 'org.apache.http.legacy' + compileSdkVersion ext_compileSdkVersion + buildToolsVersion ext_buildToolsVersion defaultConfig { - minSdkVersion 3 - targetSdkVersion 24 + minSdkVersion ext_minSdkVersion + targetSdkVersion ext_targetSdkVersion versionCode 1 versionName "1.0" - buildConfigField "String", "SENTRY_ANDROID_VERSION", "\"${SentryAndroidVersion}\"" + buildConfigField "String", "SENTRY_ANDROID_VERSION", "\"${libraryVersion}\"" } + buildTypes { release { minifyEnabled false @@ -31,35 +27,16 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - provided 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2' androidTestCompile 'com.google.guava:guava:19.0' - } ext { - bintrayRepo = 'maven' - bintrayName = 'sentry-android' - - publishedGroupId = 'com.joshdholtz.sentry' libraryName = 'sentry-android' artifact = 'sentry-android' libraryDescription = 'A Sentry client for Android' - - siteUrl = 'https://github.com/nuuneoi/FBLikeAndroid' - gitUrl = 'https://github.com/nuuneoi/FBLikeAndroid.git' - - libraryVersion = SentryAndroidVersion - - developerId = 'joshdholtz' - developerName = 'Josh Holtz' - developerEmail = 'josh@rokkincat.com' - - licenseName = 'The MIT License (MIT)' - licenseUrl = 'http://opensource.org/licenses/mit-license.php' - allLicenses = ["MIT"] } + apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' - diff --git a/sentry-android/sentry-android.iml b/sentry-android/sentry-android.iml index 0847d9a..972bc35 100644 --- a/sentry-android/sentry-android.iml +++ b/sentry-android/sentry-android.iml @@ -1,5 +1,5 @@ - + @@ -100,4 +100,4 @@ - \ No newline at end of file + diff --git a/sentry-android/src/androidTest/java/com/joshdholtz/sentry/SentryEventBuilderTest.java b/sentry-android/src/androidTest/java/com/joshdholtz/sentry/SentryEventBuilderTest.java index 48d0784..613206f 100644 --- a/sentry-android/src/androidTest/java/com/joshdholtz/sentry/SentryEventBuilderTest.java +++ b/sentry-android/src/androidTest/java/com/joshdholtz/sentry/SentryEventBuilderTest.java @@ -1,7 +1,5 @@ package com.joshdholtz.sentry; -import android.provider.Telephony; - import com.google.common.base.Joiner; import junit.framework.TestCase; @@ -11,7 +9,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.regex.Pattern; public class SentryEventBuilderTest extends TestCase { @@ -21,7 +18,7 @@ public void testAddExtra() throws JSONException { initialExtra.put("key1", "value1"); initialExtra.put("key2", "value2"); - Sentry.SentryEventBuilder builder = new Sentry.SentryEventBuilder() + Sentry.SentryEventBuilder builder = createSentryEventBuilder() .setMessage("Being awesome") .setExtra(initialExtra); @@ -41,20 +38,24 @@ public void testAddTag() throws JSONException { initialTags.put("tag1", "value1"); initialTags.put("tag2", "value2"); - Sentry.SentryEventBuilder builder = new Sentry.SentryEventBuilder() + Sentry.SentryEventBuilder builder = createSentryEventBuilder() .setMessage("Being awesome") .setTags(initialTags); JSONObject tags = builder.getTags(); - assertEquals("value1", tags.getString("key1")); - assertEquals("value2", tags.getString("key2")); + assertEquals("value1", tags.getString("tag1")); + assertEquals("value2", tags.getString("tag2")); builder.addTag("key3", "value3"); - JSONObject moreTags = builder.getExtra(); + JSONObject moreTags = builder.getTags(); assertEquals("value3", moreTags.getString("key3")); } + private Sentry.SentryEventBuilder createSentryEventBuilder() { + return new Sentry.SentryEventBuilder(true); + } + // Since regexes are very hard to read, we build the regex to recognize an internal package // name programatically. // Given a list of packages, return a regex to match them. @@ -99,7 +100,7 @@ public void testInternalPackageNameRegex() throws Exception { "com.android", "com.google.android", "dalvik.system"}; - assertEquals(Sentry.SentryEventBuilder.isInternalPackage, toPackageRegex(internalPackages)); + assertEquals(Sentry.SentryEventBuilder.IS_INTERNAL_PACKAGE.pattern(), toPackageRegex(internalPackages)); final String[] internalClasses = { @@ -110,7 +111,7 @@ public void testInternalPackageNameRegex() throws Exception { "dalvik.system.console" }; for (String c : internalClasses) { - assertTrue(c, c.matches(Sentry.SentryEventBuilder.isInternalPackage)); + assertTrue(c, Sentry.SentryEventBuilder.IS_INTERNAL_PACKAGE.matcher(c).matches()); } final String[] userClasses= { @@ -119,7 +120,7 @@ public void testInternalPackageNameRegex() throws Exception { }; for (String c : userClasses) { - assertFalse(c, c.matches(Sentry.SentryEventBuilder.isInternalPackage)); + assertFalse(c, Sentry.SentryEventBuilder.IS_INTERNAL_PACKAGE.matcher(c).matches()); } } -} \ No newline at end of file +} diff --git a/sentry-android/src/main/java/com/joshdholtz/sentry/AppInfoSentryCaptureListener.java b/sentry-android/src/main/java/com/joshdholtz/sentry/AppInfoSentryCaptureListener.java new file mode 100644 index 0000000..9122c7b --- /dev/null +++ b/sentry-android/src/main/java/com/joshdholtz/sentry/AppInfoSentryCaptureListener.java @@ -0,0 +1,169 @@ +package com.joshdholtz.sentry; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.res.Configuration; +import android.os.Build; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import org.json.JSONException; +import org.json.JSONObject; + +public class AppInfoSentryCaptureListener implements Sentry.SentryEventCaptureListener { + + + private final JSONObject contexts; + private Sentry.SentryEventCaptureListener otherListener; + + public AppInfoSentryCaptureListener(Context context,Sentry.SentryEventCaptureListener otherListener) { + this.otherListener = otherListener; + AppInfo appInfo = AppInfo.read(context); + contexts = readContexts(context, appInfo); + } + + public AppInfoSentryCaptureListener(Context context) { + this(context,null); + } + + @Override + public Sentry.SentryEventBuilder beforeCapture(Sentry.SentryEventBuilder builder) { + Sentry.SentryEventBuilder eventBuilder = builder.setContexts(contexts); + if (otherListener == null) { + return eventBuilder; + } + return otherListener.beforeCapture(eventBuilder); + } + + 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(Sentry.TAG, "Failed to build device contexts", e); + } + return contexts; + } + + /** + * Read the package data into map to be sent as an event context item. + * This is not a built-in context type. + */ + 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(Sentry.TAG, "Error reading package context", e); + } + return pack; + } + + + /** + * 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 + */ + 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 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. + *

+ * See https://docs.sentry.io/hosted/learn/breadcrumbs/ */ - private static boolean Present(String s) { - return s != null && s.length() > 0; + public void addBreadcrumb(String category, String message) { + breadcrumbs.push(new Breadcrumb( + System.currentTimeMillis() / 1000, + Breadcrumb.Type.Default, + message, + category, + SentryEventBuilder.SentryEventLevel.INFO)); } + + } diff --git a/sentry-android/src/main/java/com/joshdholtz/sentry/SentryInstance.java b/sentry-android/src/main/java/com/joshdholtz/sentry/SentryInstance.java new file mode 100644 index 0000000..8c21461 --- /dev/null +++ b/sentry-android/src/main/java/com/joshdholtz/sentry/SentryInstance.java @@ -0,0 +1,26 @@ +package com.joshdholtz.sentry; + +import android.content.Context; + +public final class SentryInstance { + + private static final String FILE_NAME = "unsent_requests"; + private static SentryInstance ourInstance = new SentryInstance(); + private Sentry sentry; + + public static void init(Context context, String dsnWithoutCredentials, HttpRequestSender httpRequestSender, String publicKey,String secretKey) { + ourInstance.sentry = Sentry.newInstance(context, dsnWithoutCredentials, httpRequestSender, FILE_NAME, publicKey, secretKey); + } + + /** + * {@link #init(Context, String, HttpRequestSender, String, String)} must be called before this + * + * @return {@code Sentry} instance created in {@link #init(Context, String, HttpRequestSender, String, String)} (Context, String, HttpRequestSender, Pair)} (Context, String, HttpRequestSender)} + */ + public static Sentry getInstance() { + return ourInstance.sentry; + } + + private SentryInstance() { + } +} diff --git a/sentry-apache-http-client/.gitignore b/sentry-apache-http-client/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sentry-apache-http-client/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sentry-apache-http-client/build.gradle b/sentry-apache-http-client/build.gradle new file mode 100644 index 0000000..553873c --- /dev/null +++ b/sentry-apache-http-client/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.library' + + +android { + compileSdkVersion ext_compileSdkVersion + buildToolsVersion ext_buildToolsVersion + + useLibrary 'org.apache.http.legacy' + + + defaultConfig { + minSdkVersion ext_minSdkVersion + targetSdkVersion ext_targetSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +ext { + libraryName = 'sentry-android-apache-http-client' + artifact = 'sentry-android-apache-http-client' + libraryDescription = 'A Sentry Apache http client for Android' +} + +dependencies { + provided 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2' +// compile "com.joshdholtz.sentry:sentry-android:${libraryVersion}" + compile project(":sentry-android") + +} diff --git a/sentry-apache-http-client/sentry-apache-http-client.iml b/sentry-apache-http-client/sentry-apache-http-client.iml new file mode 100644 index 0000000..1be95a4 --- /dev/null +++ b/sentry-apache-http-client/sentry-apache-http-client.iml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sentry-apache-http-client/src/main/AndroidManifest.xml b/sentry-apache-http-client/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9c929cc --- /dev/null +++ b/sentry-apache-http-client/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/sentry-apache-http-client/src/main/java/com/joshdholtz/sentry/http/apache/ApacheHttpRequestSender.java b/sentry-apache-http-client/src/main/java/com/joshdholtz/sentry/http/apache/ApacheHttpRequestSender.java new file mode 100644 index 0000000..5d0a8b7 --- /dev/null +++ b/sentry-apache-http-client/src/main/java/com/joshdholtz/sentry/http/apache/ApacheHttpRequestSender.java @@ -0,0 +1,214 @@ +package com.joshdholtz.sentry.http.apache; + +import com.joshdholtz.sentry.BaseHttpBuilder; +import com.joshdholtz.sentry.HttpRequestSender; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +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.client.DefaultHttpClient; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.net.UnknownHostException; +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.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class ApacheHttpRequestSender implements HttpRequestSender { + + private static final int TIMEOUT = 10000; + private static final String UTF_8 = "utf-8"; + + @Override + public Builder newBuilder() { + return new ApacheBuilder(); + } + + static class ApacheBuilder extends BaseHttpBuilder { + + private static class ApacheResponse implements Response { + + private final HttpResponse response; + + public ApacheResponse(HttpResponse response) { + this.response = response; + } + + @Override + public int getStatusCode() { + return response.getStatusLine().getStatusCode(); + } + + @Override + public String getContent() { + // Gets the input stream and unpackages the response into a command + byte[] byteResp = null; + if (response.getEntity() != null) { + try { + InputStream in = response.getEntity().getContent(); + byteResp = this.readBytes(in); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + 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(); + } + return stringResponse; + + } + + private byte[] readBytes(InputStream inputStream) throws IOException { + // this dynamically extends to take the bytes you read + ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); + + // this is storage overwritten on each iteration with bytes + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + + // 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); + } + + // and then we can return your byte array. + return byteBuffer.toByteArray(); + } + } + + @Override + protected Request build(String url, Map headers, String requestData, String mediaType, boolean useHttps) throws Exception { + final HttpClient client; + if (useHttps) { + client = getHttpsClient(); + } else { + client = new DefaultHttpClient(); + } + final HttpPost httpPost = new HttpPost(url); + + HttpParams httpParams = httpPost.getParams(); + HttpConnectionParams.setConnectionTimeout(httpParams, TIMEOUT); + HttpConnectionParams.setSoTimeout(httpParams, TIMEOUT); + + HttpProtocolParams.setContentCharset(httpParams, UTF_8); + HttpProtocolParams.setHttpElementCharset(httpParams, UTF_8); + + for (Map.Entry header : headers.entrySet()) { + httpPost.setHeader(header.getKey(), header.getValue()); + } + + httpPost.setEntity(new StringEntity(requestData, UTF_8)); + + return new Request() { + @Override + public Response execute() throws Exception { + final HttpResponse response = client.execute(httpPost); + return new ApacheResponse(response); + } + }; + } + + private static class ExSSLSocketFactory extends SSLSocketFactory { + + SSLContext sslContext = SSLContext.getInstance("TLS"); + + public ExSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + TrustManager x509TrustManager = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sslContext.init(null, new TrustManager[]{x509TrustManager}, null); + } + + public 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, UnknownHostException { + return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); + } + + @Override + public Socket createSocket() throws IOException { + return sslContext.getSocketFactory().createSocket(); + } + } + + public static HttpClient getHttpsClient() { + DefaultHttpClient defaultHttpClient = new DefaultHttpClient(); + try { + X509TrustManager x509TrustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + 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 = defaultHttpClient.getConnectionManager(); + SchemeRegistry schemeRegistry = clientConnectionManager.getSchemeRegistry(); + schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); + return new DefaultHttpClient(clientConnectionManager, defaultHttpClient.getParams()); + } catch (Exception ex) { + return defaultHttpClient; + } + } + } +} diff --git a/sentry-app/build.gradle b/sentry-app/build.gradle index d539c96..c47d7a1 100644 --- a/sentry-app/build.gradle +++ b/sentry-app/build.gradle @@ -1,28 +1,31 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" - useLibrary 'org.apache.http.legacy' + compileSdkVersion ext_compileSdkVersion + buildToolsVersion ext_buildToolsVersion defaultConfig { applicationId "com.joshdholtz.sentryapp" - minSdkVersion 15 - targetSdkVersion 23 + minSdkVersion ext_minSdkVersion + targetSdkVersion ext_targetSdkVersion versionCode 1 versionName "1.0" } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + useLibrary 'org.apache.http.legacy' + } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.1.1' - compile project(':sentry-android') -} \ No newline at end of file + compile 'com.android.support:appcompat-v7:25.1.1' + compile project(':sentry-apache-http-client') +} diff --git a/sentry-app/src/main/AndroidManifest.xml b/sentry-app/src/main/AndroidManifest.xml index 902db6f..2c35531 100644 --- a/sentry-app/src/main/AndroidManifest.xml +++ b/sentry-app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:name=".SentryApplication" android:theme="@style/AppTheme" >