Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ Sentry.init({

### Fixes

- Added missing integrations `inboundFiltersIntegration`, `functionToStringIntegration`, `browserApiErrorsIntegration`, `breadcrumbsIntegration`, `globalHandlersIntegration`, `linkedErrorsIntegration`, `dedupeIntegration` and `browserSessionIntegration` ([#1047](https://github.com/getsentry/sentry-capacitor/pull/1047))
- This fixes the following option parameters that weren't working: `ignoreErrors`, `ignoreTransactions`, `allowUrls`, `denyUrls`
- For more information about the Integrations, check the following link: https://docs.sentry.io/platforms/javascript/configuration/integrations.

- Breadcrumbs are now showing and are tied with native breadcrumbs too ([#1047](https://github.com/getsentry/sentry-capacitor/pull/1047))
- Init now showing the correct JSDoc for Vue/Nuxt init parameters. ([#1046](https://github.com/getsentry/sentry-capacitor/pull/1046))
- Replays/Logs/Sessions now have the `capacitor` SDK name as the source of the event. ([#1043](https://github.com/getsentry/sentry-capacitor/pull/1043))
- Sentry Capacitor integrations are now exposed to `@sentry/capacitor` ([#1039](https://github.com/getsentry/sentry-capacitor/pull/1039))
Expand Down
140 changes: 140 additions & 0 deletions android/src/main/java/io/sentry/capacitor/CapSentryMapConverter.java
Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding it that this is the same code as RNSentryMapConverter. Correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

correct, with only minor changes since we don't depend on React Native imports.

Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.sentry.capacitor;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;

import com.getcapacitor.JSObject;
import org.json.JSONArray;
import org.json.JSONObject;

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

public final class CapSentryMapConverter {
public static final String NAME = "CapSentry.MapConverter";
private static final ILogger logger = new AndroidLogger(NAME);

private CapSentryMapConverter() {
throw new AssertionError("Utility class should not be instantiated");
}

public static Object convertToWritable(Object serialized) {
if (serialized instanceof List) {
JSONArray writable = new JSONArray();
for (Object item : (List<?>) serialized) {
addValueToWritableArray(writable, convertToWritable(item));
}
return writable;
} else 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 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(JSONArray writableArray, Object value) {
try {
if (value == null) {
writableArray.put(JSONObject.NULL);
} else if (value instanceof Boolean) {
writableArray.put((Boolean) value);
} else if (value instanceof Double) {
writableArray.put((Double) value);
} else if (value instanceof Float) {
writableArray.put(((Float) value).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) {
writableArray.put(((Long) value).doubleValue());
} else if (value instanceof BigInteger) {
writableArray.put(((BigInteger) value).doubleValue());
} else if (value instanceof BigDecimal) {
writableArray.put(((BigDecimal) value).doubleValue());
} else if (value instanceof String) {
writableArray.put((String) value);
} else if (value instanceof JSObject || value instanceof JSONObject) {
writableArray.put(value instanceof JSObject ? (JSObject) value : (JSONObject) value);
} else if (value instanceof JSONArray) {
writableArray.put((JSONArray) value);
} else {
logger.log(SentryLevel.ERROR, "Could not convert object: " + value);
}
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Error adding value to array: " + e.getMessage(), e);
}
}

private static void addValueToWritableMap(JSObject writableMap, String key, Object value) {
try {
if (value == null) {
writableMap.put(key, JSONObject.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 JSONArray) {
writableMap.put(key, (JSONArray) value);
} else if (value instanceof JSObject || value instanceof JSONObject) {
writableMap.put(key, value instanceof JSObject ? (JSObject) value : (JSONObject) value);
} else {
logger.log(SentryLevel.ERROR, "Could not convert object: " + value);
}
} catch (Exception e) {
logger.log(SentryLevel.ERROR, "Error adding value to map: " + e.getMessage(), e);
}
}
}
68 changes: 65 additions & 3 deletions android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import android.content.pm.PackageInfo;

import io.sentry.ILogger;
import io.sentry.ScopesAdapter;
import io.sentry.SentryOptions;
import io.sentry.android.core.AndroidLogger;
import io.sentry.android.core.SentryAndroidOptions;
import io.sentry.vendor.Base64;

import com.getcapacitor.JSObject;
Expand All @@ -13,6 +16,8 @@
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;

import org.json.JSONArray;

import io.sentry.Breadcrumb;
import io.sentry.IScope;
import io.sentry.Integration;
Expand All @@ -29,9 +34,11 @@
import io.sentry.protocol.SentryPackage;
import io.sentry.protocol.User;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

@CapacitorPlugin
Expand Down Expand Up @@ -267,18 +274,26 @@ public void addBreadcrumb(final PluginCall breadcrumb) {
Sentry.configureScope(scope -> {
Breadcrumb breadcrumbInstance = new Breadcrumb();

if (breadcrumb.getData().has("message")) {
JSObject breadcrumbData = breadcrumb.getData();

if (breadcrumbData.has("message")) {
breadcrumbInstance.setMessage(breadcrumb.getString("message"));
}

if (breadcrumb.getData().has("type")) {
if (breadcrumbData.has("type")) {
breadcrumbInstance.setType(breadcrumb.getString("type"));
}

if (breadcrumb.getData().has("category")) {
if (breadcrumbData.has("category")) {
breadcrumbInstance.setCategory(breadcrumb.getString("category"));
}

if (breadcrumbData.has("origin")) {
breadcrumbInstance.setOrigin(breadcrumbData.getString("origin"));
} else {
breadcrumbInstance.setOrigin("capacitor");
}

if (breadcrumb.getData().has("level")) {
switch (breadcrumb.getString("level")) {
case "fatal":
Expand Down Expand Up @@ -361,6 +376,52 @@ public void setTag(PluginCall call) {
call.resolve();
}

@PluginMethod
public void fetchNativeDeviceContexts(PluginCall call) {
final SentryOptions options = ScopesAdapter.getInstance().getOptions();
final IScope currentScope = InternalSentrySdk.getCurrentScope();


JSObject callData = call.getData();
if (options == null || currentScope == null) {
call.resolve();
return;
}
final Map<String, Object> serialized =
InternalSentrySdk.serializeScope(context, (SentryAndroidOptions) options, currentScope);

This comment was marked as outdated.


// Filter out breadcrumbs with origin "capacitor" from the serialized data before conversion
if (serialized.containsKey("breadcrumbs")) {
Object breadcrumbsObj = serialized.get("breadcrumbs");
if (breadcrumbsObj instanceof List) {
List<?> breadcrumbs = (List<?>) breadcrumbsObj;
List<Object> filteredBreadcrumbs = new ArrayList<>();
for (Object breadcrumbObj : breadcrumbs) {
if (breadcrumbObj instanceof Map) {
Map<?, ?> breadcrumb = (Map<?, ?>) breadcrumbObj;
Object origin = breadcrumb.get("origin");
if (!"capacitor".equals(origin)) {
filteredBreadcrumbs.add(breadcrumb);
}
} else {
// If it's not a Map, keep it as-is
filteredBreadcrumbs.add(breadcrumbObj);
}
}
serialized.put("breadcrumbs", filteredBreadcrumbs);
}
}

final Object deviceContext = CapSentryMapConverter.convertToWritable(serialized);

if (deviceContext instanceof JSObject) {
call.resolve((JSObject) deviceContext);
}
else {
call.resolve();
}
}

public void setEventOriginTag(SentryEvent event) {
SdkVersion sdk = event.getSdk();
if (sdk != null) {
Expand Down Expand Up @@ -400,3 +461,4 @@ public void addPackages(SentryEvent event, SdkVersion sdk) {
}
}
}

21 changes: 19 additions & 2 deletions ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
}

let extraContext = PrivateSentrySDKOnly.getExtraContext()
var context = contexts["context"] as? [String: Any] ?? [:]
var context = contexts["contexts"] as? [String: Any] ?? [:]

if let deviceExtraContext = extraContext["device"] as? [String: Any] {
var deviceContext = context["device"] as? [String: Any] ?? [:]
Expand All @@ -200,7 +200,18 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
context["app"] = appContext
}

contexts["context"] = context
// Remove capacitor breadcrumbs
if let breadcrumbs = contexts["breadcrumbs"] as? [[String: Any]] {
let filteredBreadcrumbs = breadcrumbs.filter { breadcrumb in
guard let origin = breadcrumb["origin"] as? String else {
return true
}
return origin != "capacitor"
}
contexts["breadcrumbs"] = filteredBreadcrumbs
}

contexts["contexts"] = context

call.resolve(contexts as PluginCallResultData)
}
Expand Down Expand Up @@ -293,6 +304,12 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
breadcrumb.category = category
}

if let origin = call.getString("origin") {
breadcrumb.origin = origin
} else {
breadcrumb.origin = "capacitor"
}

breadcrumb.type = call.getString("type")
breadcrumb.message = call.getString("message")
breadcrumb.data = call.getObject("data")
Expand Down
41 changes: 40 additions & 1 deletion src/breadcrumb.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
import type { SeverityLevel } from '@sentry/core';
import type { Breadcrumb, SeverityLevel } from '@sentry/core';
import { severityLevelFromString } from '@sentry/core';

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;
}
24 changes: 22 additions & 2 deletions src/integrations/default.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type Integration,consoleLoggingIntegration } from '@sentry/core';
import { breadcrumbsIntegration, browserApiErrorsIntegration, browserSessionIntegration, globalHandlersIntegration } from '@sentry/browser';
import { type Integration,dedupeIntegration, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration } from '@sentry/core';
import type { CapacitorOptions } from '../options';
import { deviceContextIntegration } from './devicecontext';
import { eventOriginIntegration } from './eventorigin';
Expand All @@ -18,11 +19,30 @@ export function getDefaultIntegrations(
integrations.push(nativeReleaseIntegration());
integrations.push(eventOriginIntegration());
integrations.push(sdkInfoIntegration());
integrations.push(consoleLoggingIntegration());

if (options.enableNative) {
integrations.push(deviceContextIntegration());
}

// @sentry/browser integrations
integrations.push(
// eslint-disable-next-line deprecation/deprecation
inboundFiltersIntegration(),
functionToStringIntegration(),
browserApiErrorsIntegration(),
breadcrumbsIntegration(),
globalHandlersIntegration(),
linkedErrorsIntegration(),
dedupeIntegration(),
);

if (options.enableAutoSessionTracking && !options.enableNative) {
integrations.push(browserSessionIntegration());
}
// end @sentry/browser integrations

// @sentry/vue integrations must be added manually.
// @sentry/react, @sentry/angular @sentry/nuxt dont require any integrations.

return integrations;
}
Loading
Loading