Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Breadcrumbs added on forked context are now captured #629

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Fixes

- Breadcrumbs added on hints are now captured. ([#629](https://github.com/getsentry/sentry-capacitor/pull/629))
- Event is enriched with all the Android context on the JS layer and you can filter/modify all the data in the `beforeSend`. ([#629](https://github.com/getsentry/sentry-capacitor/pull/629))

## 0.17.0

### Features
Expand Down
137 changes: 137 additions & 0 deletions android/src/main/java/io/sentry/capacitor/CapSentryMapConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package io.sentry.capacitor;

import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
import com.getcapacitor.PluginCall;
import org.json.JSONArray;
import org.json.JSONException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;

import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.android.core.AndroidLogger;

public class CapSentryMapConverter {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add unit tests for this class?

public static final String NAME = "CapSentry.MapConverter";

private static final ILogger logger = new AndroidLogger(NAME);

public static Object convertToWritable(Object serialized) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this return JSObject directly?

if (serialized instanceof Map) {
JSObject writable = new JSObject();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) serialized).entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();

if (key instanceof String) {
addValueToWritableMap(writable, (String) key, convertToWritable(value));
} else {
logger.log(SentryLevel.ERROR, "Only String keys are supported in Map.", key);
}
}
return writable;
} else if (serialized instanceof List) {
JSArray writable = new JSArray();
for (Object item : (List<?>) serialized) {
addValueToWritableArray(writable, convertToWritable(item));
}
return writable;
}
else if (serialized instanceof Byte) {
return Integer.valueOf((Byte) serialized);
} else if (serialized instanceof Short) {
return Integer.valueOf((Short) serialized);
} else if (serialized instanceof Float) {
return Double.valueOf((Float) serialized);
} else if (serialized instanceof Long) {
return Double.valueOf((Long) serialized);
} else if (serialized instanceof BigInteger) {
return ((BigInteger) serialized).doubleValue();
} else if (serialized instanceof BigDecimal) {
return ((BigDecimal) serialized).doubleValue();
} else if (serialized instanceof Integer
|| serialized instanceof Double
|| serialized instanceof Boolean
|| serialized == null
|| serialized instanceof String) {
return serialized;
} else {
logger.log(SentryLevel.ERROR, "Supplied serialized value could not be converted." + serialized);
return null;
}
}

private static void addValueToWritableArray(JSArray writableArray, Object value) {
if (value == null) {
writableArray.put(null);
} else if (value instanceof Boolean) {
writableArray.put((Boolean) value);
} else if (value instanceof Double) {
writableArray.put((Double) value);
} else if (value instanceof Float) {
final double doubleValue = ((Float) value).doubleValue();
writableArray.put(Double.valueOf(doubleValue));
} else if (value instanceof Integer) {
writableArray.put((Integer) value);
} else if (value instanceof Short) {
writableArray.put(((Short) value).intValue());
} else if (value instanceof Byte) {
writableArray.put(((Byte) value).intValue());
} else if (value instanceof Long) {
final double doubleValue = ((Long) value).doubleValue();
writableArray.put(Double.valueOf(doubleValue));
} else if (value instanceof BigInteger) {
final double doubleValue = ((BigDecimal) value).doubleValue();
writableArray.put(Double.valueOf(doubleValue));
} else if (value instanceof BigDecimal) {
final double doubleValue = ((BigDecimal) value).doubleValue();
writableArray.put(Double.valueOf(doubleValue));
} else if (value instanceof String) {
writableArray.put((String) value);
}
else if (value instanceof JSObject) {
writableArray.put((JSObject) value);
} else if (value instanceof JSArray) {
writableArray.put((JSArray) value);
} else {
logger.log(SentryLevel.ERROR,
"Could not convert object: " + value);
}
}

private static void addValueToWritableMap(JSObject writableMap, String key, Object value) {
if (value == null) {
writableMap.put(key, null);
} else if (value instanceof Boolean) {
writableMap.put(key, (Boolean) value);
} else if (value instanceof Double) {
writableMap.put(key, (Double) value);
} else if (value instanceof Float) {
writableMap.put(key, ((Float) value).doubleValue());
} else if (value instanceof Integer) {
writableMap.put(key, (Integer) value);
} else if (value instanceof Short) {
writableMap.put(key, ((Short) value).intValue());
} else if (value instanceof Byte) {
writableMap.put(key, ((Byte) value).intValue());
} else if (value instanceof Long) {
writableMap.put(key, ((Long) value).doubleValue());
} else if (value instanceof BigInteger) {
writableMap.put(key, ((BigInteger) value).doubleValue());
} else if (value instanceof BigDecimal) {
writableMap.put(key, ((BigDecimal) value).doubleValue());
} else if (value instanceof String) {
writableMap.put(key, (String) value);
} else if (value instanceof JSArray) {
writableMap.put(key, (JSArray) value);
} else if (value instanceof JSObject) {
writableMap.put(key, (JSObject) value);
} else {
logger.log(SentryLevel.ERROR,
"Could not convert object" + value);
}
}
}
81 changes: 50 additions & 31 deletions android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,28 @@
import io.sentry.Breadcrumb;
import io.sentry.HubAdapter;
import io.sentry.Integration;
import io.sentry.IScope;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.UncaughtExceptionHandlerIntegration;
import io.sentry.android.core.BuildConfig;
import io.sentry.android.core.AnrIntegration;
import io.sentry.android.core.InternalSentrySdk;
import io.sentry.android.core.SentryAndroidOptions;
import io.sentry.android.core.NdkIntegration;
import io.sentry.android.core.SentryAndroid;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryPackage;
import io.sentry.protocol.User;
import java.io.File;
import java.io.FileOutputStream;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -212,6 +215,36 @@ public void fetchNativeRelease(PluginCall call) {
call.resolve(release);
}

@PluginMethod
public void fetchNativeSdkInfo(PluginCall call) {
final SdkVersion sdkVersion = HubAdapter.getInstance().getOptions().getSdkVersion();
if (sdkVersion == null) {
call.resolve(null);
} else {
final JSObject sdkInfo = new JSObject();
sdkInfo.put("name", sdkVersion.getName());
sdkInfo.put("version", sdkVersion.getVersion());
call.resolve(sdkInfo);
}
}

@PluginMethod
public void fetchNativeDeviceContexts(PluginCall call) {
final SentryOptions options = HubAdapter.getInstance().getOptions();
if (!(options instanceof SentryAndroidOptions)) {
call.resolve(null);
return;
}

final IScope currentScope = InternalSentrySdk.getCurrentScope();
final Map<String, Object> serialized = InternalSentrySdk.serializeScope(
context,
(SentryAndroidOptions) options,
currentScope);
final JSObject deviceContext = (JSObject)CapSentryMapConverter.convertToWritable(serialized);
call.resolve(deviceContext);
}

@PluginMethod
public void captureEnvelope(PluginCall call) {
try {
Expand All @@ -220,31 +253,14 @@ public void captureEnvelope(PluginCall call) {
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) rawIntegers.getInt(i);
}

final String outboxPath = HubAdapter.getInstance().getOptions().getOutboxPath();

if (outboxPath == null || outboxPath.isEmpty()) {
logger.info("Error when writing envelope, no outbox path is present.");
call.reject("Missing outboxPath");
return;
}

final File installation = new File(outboxPath, UUID.randomUUID().toString());

try (FileOutputStream out = new FileOutputStream(installation)) {
out.write(bytes);
logger.info("Successfully captured envelope.");
} catch (Exception e) {
logger.info("Error writing envelope.");
call.reject(String.valueOf(e));
return;
}
} catch (Exception e) {
logger.info("Error reading envelope.");
call.reject(String.valueOf(e));
return;
}
call.resolve();
InternalSentrySdk.captureEnvelope(bytes);
call.resolve();
}
catch (Throwable e) {
final String errorMessage ="Error while capturing envelope";
logger.log(Level.WARNING, errorMessage);
call.reject(errorMessage);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see previously this was call.reject(String.valueOf(e));? Does this change the error in any way?

}
}

@PluginMethod
Expand All @@ -266,7 +282,10 @@ public void getStringBytesLength(PluginCall call) {
@PluginMethod
public void addBreadcrumb(final PluginCall breadcrumb) {
Sentry.configureScope(scope -> {
Breadcrumb breadcrumbInstance = new Breadcrumb();
Date jsTimestamp =
new Date((long)(breadcrumb.getDouble("timestamp")*1000));

Breadcrumb breadcrumbInstance = new Breadcrumb(jsTimestamp);

if (breadcrumb.getData().has("message")) {
breadcrumbInstance.setMessage(breadcrumb.getString("message"));
Expand Down Expand Up @@ -316,8 +335,8 @@ public void addBreadcrumb(final PluginCall breadcrumb) {
}

scope.addBreadcrumb(breadcrumbInstance);
});
breadcrumb.resolve();
});
breadcrumb.resolve();
}

@PluginMethod
Expand Down
4 changes: 3 additions & 1 deletion example/ionic-angular-v5/src/app/tab1/tab1.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export class Tab1Page {
}

public sentryCapturedException(): void {
Sentry.captureException(new Error(`${Date.now()}: a test error occurred`));
Sentry.captureException(new Error(`${Date.now()}: a test error occurred`), (context) => {
return context.addBreadcrumb({ message: 'test' });
});
}

public errorWithUserData(): void {
Expand Down
42 changes: 42 additions & 0 deletions src/breadcrumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Breadcrumb, SeverityLevel } from '@sentry/types';
import { severityLevelFromString } from '@sentry/utils';

export const DEFAULT_BREADCRUMB_LEVEL: SeverityLevel = 'info';

type BreadcrumbCandidate = {
[K in keyof Partial<Breadcrumb>]: unknown;
};

/**
* Convert plain object to a valid Breadcrumb
*/
export function breadcrumbFromObject(candidate: BreadcrumbCandidate): Breadcrumb {
const breadcrumb: Breadcrumb = {};

if (typeof candidate.type === 'string') {
breadcrumb.type = candidate.type;
}
if (typeof candidate.level === 'string') {
breadcrumb.level = severityLevelFromString(candidate.level);
}
if (typeof candidate.event_id === 'string') {
breadcrumb.event_id = candidate.event_id;
}
if (typeof candidate.category === 'string') {
breadcrumb.category = candidate.category;
}
if (typeof candidate.message === 'string') {
breadcrumb.message = candidate.message;
}
if (typeof candidate.data === 'object' && candidate.data !== null) {
breadcrumb.data = candidate.data;
}
if (typeof candidate.timestamp === 'string') {
const timestampSeconds = Date.parse(candidate.timestamp) / 1000; // breadcrumb timestamp is in seconds
if (!isNaN(timestampSeconds)) {
breadcrumb.timestamp = timestampSeconds;
}
}

return breadcrumb;
}
25 changes: 24 additions & 1 deletion src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,30 @@ interface serializedObject {
}

export type NativeDeviceContextsResponse = {
[key: string]: Record<string, unknown>;
[key: string]: unknown;
tags?: Record<string, string>;
extra?: Record<string, unknown>;
contexts?: Record<string, Record<string, unknown>>;
user?: {
userId?: string;
email?: string;
username?: string;
ipAddress?: string;
segment?: string;
data?: Record<string, unknown>;
};
dist?: string;
environment?: string;
fingerprint?: string[];
level?: string;
breadcrumbs?: {
level?: string;
timestamp?: string;
category?: string;
type?: string;
message?: string;
data?: Record<string, unknown>;
}[];
};

export interface ISentryCapacitorPlugin {
Expand Down