Repository: getsentry/sentry-capacitor
Pull request #438 (2023-08-08 14:01:54)

Fix error when serializing objects with circular referencing
When trying to serialize an object like this

```typescript
const subObject: { sub1: string, sub2: any } = { sub1: 'subObjectValue', sub2: undefined }
const givenObject = { root1: 'some value', root2: subObject };
subObject.sub2 = givenObject;
```

one will get an error like this

```
    TypeError: Converting circular structure to JSON
        --> starting at object with constructor 'Object'
        |     property 'b' -> object with constructor 'Object'
        --- property 'd' closes the circle
        at JSON.stringify (<anonymous>)
```

This PR fixes this behavior. A "flattened" output can be seen in the added [unit test](https://github.com/mlostekk/sentry-capacitor/blob/0eb8660011376c315d80a33c6a2b1b77750e69f0/test/wrapper.test.ts#L632).

lucas-zimerman: Hey thank you for opening the PR!
Sorry to ask for this change, but since recently this was addressed on React Native, could you also use the way it was solved on React Native?

Basically copy the normalise to util/normalise
https://github.com/getsentry/sentry-react-native/blob/9ee4637908664fd8c5c34d592589352f6112e102/src/js/utils/normalize.ts
and instead of calling `JSON.stringify(value);`

Additionally, we could also add the tests that were used here:
https://github.com/getsentry/sentry-react-native/blob/9ee4637908664fd8c5c34d592589352f6112e102/test/utils/normalize.test.ts
mlostekk: @lucas-zimerman Not entirely sure if i covered all areas. But I have updated it accordingly to the RN solution

Patch:
diff --git a/src/scope.ts b/src/scope.ts
index bbfb5a3..1b7f276 100644
--- a/src/scope.ts
+++ b/src/scope.ts
@@ -1,6 +1,7 @@
 import { Scope } from '@sentry/core';
 import type { Breadcrumb, User } from '@sentry/types';
 
+import { convertToNormalizedObject } from './utils/normalize';
 import { NATIVE } from './wrapper';
 
 /**
@@ -69,9 +70,13 @@ export class CapacitorScope extends Scope {
    * @inheritDoc
    */
   public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this {
-    /* eslint-disable no-console */
-    NATIVE.addBreadcrumb(breadcrumb);
-    return super.addBreadcrumb(breadcrumb, maxBreadcrumbs);
+    const mergedBreadcrumb: Breadcrumb = {
+      ...breadcrumb,
+      data: breadcrumb.data ? convertToNormalizedObject(breadcrumb.data) : undefined,
+    };
+
+    NATIVE.addBreadcrumb(mergedBreadcrumb);
+    return super.addBreadcrumb(mergedBreadcrumb, maxBreadcrumbs);
   }
 
   /**
diff --git a/src/utils/normalize.ts b/src/utils/normalize.ts
new file mode 100644
index 0000000..fb3245f
--- /dev/null
+++ b/src/utils/normalize.ts
@@ -0,0 +1,19 @@
+import { normalize } from '@sentry/utils';
+
+const KEY = 'value';
+
+/**
+ * Converts any input into a valid record with string keys.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function convertToNormalizedObject(data: unknown): Record<string, any> {
+  const normalized: unknown = normalize(data);
+  if (normalized === null || typeof normalized !== 'object') {
+    return {
+      [KEY]: normalized,
+    };
+  } else {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    return normalized as Record<string, any>;
+  }
+}
diff --git a/src/wrapper.ts b/src/wrapper.ts
index b6055e7..6403a72 100644
--- a/src/wrapper.ts
+++ b/src/wrapper.ts
@@ -6,6 +6,7 @@ import type { NativeDeviceContextsResponse } from './definitions';
 import { FilterNativeOptions } from './nativeOptions';
 import type { CapacitorOptions } from './options';
 import { SentryCapacitor } from './plugin';
+import { convertToNormalizedObject } from './utils/normalize';
 import { utf8ToBytes } from './vendor';
 
 /**
@@ -253,7 +254,7 @@ export const NATIVE = {
         ? this._processLevel(breadcrumb.level)
         : undefined,
       data: breadcrumb.data
-        ? this._serializeObject(breadcrumb.data)
+        ? convertToNormalizedObject(breadcrumb.data)
         : undefined,
     });
   },


Test patch:
diff --git a/test/utils/normalize.test.ts b/test/utils/normalize.test.ts
new file mode 100644
index 0000000..6e72042
--- /dev/null
+++ b/test/utils/normalize.test.ts
@@ -0,0 +1,25 @@
+import { convertToNormalizedObject } from '../../src/utils/normalize';
+
+describe('normalize', () => {
+  describe('convertToNormalizedObject', () => {
+    test('output equals input for normalized objects', () => {
+      const actualResult = convertToNormalizedObject({ foo: 'bar' });
+      expect(actualResult).toEqual({ foo: 'bar' });
+    });
+
+    test('converted output is normalized', () => {
+      const actualResult = convertToNormalizedObject({ foo: NaN });
+      expect(actualResult).toEqual({ foo: '[NaN]' });
+    });
+
+    test('converts a value to an object', () => {
+      const actualResult = convertToNormalizedObject('foo');
+      expect(actualResult).toEqual({ value: 'foo' });
+    });
+
+    test('converts null to an object', () => {
+      const actualResult = convertToNormalizedObject(null);
+      expect(actualResult).toEqual({ value: null });
+    });
+  });
+});