package de.vanmar.android.yarrn.sentry;
import org.acra.ACRA;
import org.acra.ReportField;
import org.acra.collector.CrashReportData;
import org.acra.sender.HttpSender.Method;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderException;
import org.acra.util.HttpRequest;
import org.json.JSONException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SentrySender implements ReportSender {
public final static String TAG = ACRA.LOG_TAG + "/SentrySender";
private SentryConfig config;
public static final ReportField[] SENTRY_TAGS_FIELDS = {
ReportField.ANDROID_VERSION,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.BRAND,
ReportField.INSTALLATION_ID,
ReportField.IS_SILENT,
ReportField.PACKAGE_NAME,
ReportField.PHONE_MODEL,
ReportField.PRODUCT,
ReportField.USER_EMAIL,
};
public static final ReportField[] SENTRY_EXTRA_FIELDS = {
ReportField.STACK_TRACE,
ReportField.AVAILABLE_MEM_SIZE,
ReportField.TOTAL_MEM_SIZE,
ReportField.USER_APP_START_DATE
};
public static final ReportField[] SENTRY_MAPPED_EXTRA_FIELDS = {
ReportField.CUSTOM_DATA,
// ReportField.SETTINGS_GLOBAL,
// ReportField.SETTINGS_SECURE,
// ReportField.ENVIRONMENT,
// ReportField.DISPLAY,
// ReportField.CRASH_CONFIGURATION,
// ReportField.BUILD,
ReportField.SHARED_PREFERENCES
};
/**
* Takes in a sentryDSN
*
* @param sentryDSN '{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}/{PROJECT_ID}'
*/
public SentrySender(String sentryDSN) {
if ((sentryDSN == null) || sentryDSN.isEmpty()) {
return;
}
try {
config = new SentryConfig(sentryDSN);
} catch (MalformedURLException e) {
ACRA.log.e(TAG, String.format("Failed to parse Sentry DSN %s", sentryDSN), e);
}
}
/**
* Sets up a base HttpRequest with required headers and options
*
* @return HttpRequest
*/
protected HttpRequest createHttpRequest() {
HttpRequest request = new HttpRequest();
request.setConnectionTimeOut(ACRA.getConfig().connectionTimeout());
request.setSocketTimeOut(ACRA.getConfig().socketTimeout());
request.setMaxNrRetries(ACRA.getConfig().maxNumberOfRequestRetries());
HashMap<String, String> headers = new HashMap<String, String>();
headers.put("X-Sentry-Auth", buildAuthHeader());
request.setHeaders(headers);
return request;
}
@Override
public void send(CrashReportData errorContent) throws ReportSenderException {
if (config == null) {
return;
}
final HttpRequest request = createHttpRequest();
String jsonData;
try {
jsonData = buildJSON(errorContent);
} catch (JSONException e) {
throw new ReportSenderException("Error while compiling the output data", e);
}
ACRA.log.d(TAG, jsonData);
try {
request.send(config.getSentryURL(), Method.POST, jsonData, org.acra.sender.HttpSender.Type.JSON);
} catch (MalformedURLException e) {
throw new ReportSenderException("Error while sending report to Sentry.", e);
} catch (IOException e) {
throw new ReportSenderException("Error while sending report to Sentry.", e);
}
}
/**
* Build up the sentry auth header in the following format.
* <p/>
* The header is composed of the timestamp from when the message was generated, and an
* arbitrary client version string. The client version should be something distinct to your client,
* and is simply for reporting purposes.
* <p/>
* X-Sentry-Auth: Sentry sentry_version=3,
* sentry_timestamp=<signature timestamp>[,
* sentry_key=<public api key>,[
* sentry_client=<client version/arbitrary>]]
*
* @return String version of the sentry auth header
*/
protected String buildAuthHeader() {
String authHeaderFormat = "Sentry sentry_version=%s, " +
"sentry_timestamp=%d, " +
"sentry_key=%s, " +
"sentry_secret=%s, " +
"sentry_client=%s/%d";
return String.format(authHeaderFormat, 4, (new Date().getTime() / 1000L),
config.getPublicKey(), config.getSecretKey(), this.getClass().toString(), 1);
}
private String buildJSON(CrashReportData crashReportData) throws JSONException {
Report sentryReport = new Report(crashReportData, SENTRY_TAGS_FIELDS);
Throwable throwable = SentryHandler.getLatestThrowable();
String userComment = crashReportData.getProperty(ReportField.USER_COMMENT);
if (userComment != null) {
sentryReport.setMessage(userComment);
} else if (crashReportData.getProperty(ReportField.STACK_TRACE) != null) {
String firstStackTraceElement = crashReportData.getProperty(ReportField.STACK_TRACE).split("\\s")[0];
sentryReport.setMessage(firstStackTraceElement);
} else {
sentryReport.setMessage(throwable != null ? throwable.getMessage() : "No message available");
}
/* Add the exceptions and determine the culprit */
List<de.vanmar.android.yarrn.sentry.Exception> exceptions = new ArrayList<Exception>();
String culprit = "";
while (throwable != null) {
Exception exception = new Exception(throwable);
exceptions.add(exception);
if (exception.getFrames().size() > 0) {
culprit = exception.getFrames().get(0).toString();
}
throwable = throwable.getCause();
}
// Sentry outputs the exceptions in reversed order
Collections.reverse(exceptions);
sentryReport.setExceptions(exceptions);
sentryReport.setCulprit(culprit);
// Accumulate extra fields and values
Map<String, String> extraValues = new HashMap<String, String>();
for (ReportField reportField : SENTRY_MAPPED_EXTRA_FIELDS) {
if (!crashReportData.containsKey(reportField)) {
continue;
}
extraValues.putAll(parseMappedString(crashReportData.getProperty(reportField)));
}
for (ReportField reportField : SENTRY_EXTRA_FIELDS) {
if (!crashReportData.containsKey(reportField)) {
continue;
}
extraValues.put(reportField.toString(), crashReportData.getProperty(reportField));
}
sentryReport.setExtra(extraValues);
return sentryReport.toString();
}
protected Map<String, String> parseMappedString(String mappedData) {
Map<String, String> mappedValues = new HashMap<String, String>();
for (String line : mappedData.split("\n")) {
if (!line.contains("=")) {
continue;
}
String[] components = line.split("=", 2);
mappedValues.put(components[0], (components.length >= 2) ? components[1] : "");
}
return mappedValues;
}
private class SentryConfig {
private String host, protocol, publicKey, secretKey, prefix;
private Integer port, projectId;
private final String API_FORMAT = "/api/%d/store/";
/**
* Takes in a sentryDSN and builds up the configuration
*
* @param sentryDSN '{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}/{PROJECT_ID}'
*/
public SentryConfig(String sentryDSN) throws MalformedURLException {
URL url = new URL(sentryDSN);
String path = url.getPath();
int lastSeparator = path.lastIndexOf("/");
Integer projectId = Integer.parseInt(path.substring(lastSeparator + 1, path.length()));
String prefix = "";
if (lastSeparator > 0) {
prefix = path.substring(0, lastSeparator);
}
String userInfo = url.getUserInfo();
if (userInfo.isEmpty() || !userInfo.contains(":")) {
throw new MalformedURLException("Missing secret or public keys");
}
String[] userParts = userInfo.split(":");
setHost(url.getHost());
setProtocol(url.getProtocol());
setProjectId(projectId);
setPrefix(prefix);
setPublicKey(userParts[0]);
setSecretKey(userParts[1]);
setPort(url.getPort());
}
/**
* The Sentry server URL that we post the message to.
*
* @return sentry server url
* @throws MalformedURLException
*/
public URL getSentryURL() throws MalformedURLException {
String path = getPrefix() + String.format(API_FORMAT, getProjectId());
return new URL(getProtocol(), getHost(), getPort(), path);
}
/**
* The sentry server host
*
* @return server host
*/
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
/**
* Sentry server protocol http https?
*
* @return http or https
*/
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* The Sentry public key
*
* @return Sentry public key
*/
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
/**
* The Sentry secret key
*
* @return Sentry secret key
*/
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
/**
* sentry url path
*
* @return url path
*/
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
/**
* Sentry project Id
*
* @return project Id
*/
public Integer getProjectId() {
return projectId;
}
public void setProjectId(Integer projectId) {
this.projectId = projectId;
}
/**
* sentry server port
*
* @return server port
*/
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
}
}