Repository: getsentry/sentry-capacitor (ts)

Issue:
Ivestigate dist being set on android but not on iOS 
It seems like when the user doesn't inform the dist, on Android it'll automatically set the app revision code as the dist tag where on iOS it'll not be set, this leads to an app with two dist values.
Check if this is an expected behavior, otherwise we need to fix it.

Pull request description:
Feat: Native Release
This feature adds the release and dist information from the native side if those values weren't informed by the user.

This is where the following information comes from:

Android
- release: `packageInfo.packageName` + `packageInfo.versionName` +  `packageInfo.versionCode`
- dist: `packageInfo.versionCode`

iOS:
- release: `CFBundleIdentifier` + `CFBundleShortVersionString` +  `CFBundleVersion`
- dist: `CFBundleVersion`

Close #229

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bc7d41..4e409cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,27 @@
 
 ## Unreleased
 
+### Features
+
+- Add Native Release information if not defined by the user ([#480](https://github.com/getsentry/sentry-capacitor/pull/480))
+
+  The release data comes from the following parameters:
+  Android
+
+  - release: `PackageInfo.packageName` + `PackageInfo.versionName` + `PackageInfo.versionCode`
+  - dist: `PackageInfo.versionCode`
+
+  iOS:
+
+  - release: `CFBundleIdentifier` + `CFBundleShortVersionString` + `CFBundleVersion`
+  - dist: `CFBundleVersion`
+
 ### Fixes
 
 - op and breadcrumb errors on Android ([#478](https://github.com/getsentry/sentry-capacitor/pull/478))
 - Add sentry-angular-ivy to optional dependencies ([#469](https://github.com/getsentry/sentry-capacitor/pull/469))
+- (iOS) UI API called on a background thread ([#448](https://github.com/getsentry/sentry-capacitor/pull/448))
+- enableWatchdogTerminationTracking (replaces enableOutOfMemoryTracking) is now properly set on iOS. ([#454](https://github.com/getsentry/sentry-capacitor/pull/454))
 
 ### Dependencies
 
diff --git a/src/integrations/index.ts b/src/integrations/index.ts
index aef5f81..c1c807f 100644
--- a/src/integrations/index.ts
+++ b/src/integrations/index.ts
@@ -1,3 +1,4 @@
 export { EventOrigin } from './eventorigin';
 export { SdkInfo } from './sdkinfo';
+export { Release } from './release';
 export { DeviceContext } from './devicecontext';
diff --git a/src/integrations/release.ts b/src/integrations/release.ts
new file mode 100644
index 0000000..e7dba11
--- /dev/null
+++ b/src/integrations/release.ts
@@ -0,0 +1,62 @@
+import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
+import type { Event, Integration } from '@sentry/types';
+
+import { NATIVE } from '../wrapper';
+
+/** Release integration responsible to load release from file. */
+export class Release implements Integration {
+  /**
+   * @inheritDoc
+   */
+  public static id: string = 'Release';
+  /**
+   * @inheritDoc
+   */
+  public name: string = Release.id;
+
+  /**
+   * @inheritDoc
+   */
+  public setupOnce(): void {
+    addGlobalEventProcessor(async (event: Event) => {
+      const self = getCurrentHub().getIntegration(Release);
+      if (!self) {
+        return event;
+      }
+
+      const options = getCurrentHub().getClient()?.getOptions();
+
+      /*
+        Check for the release and dist in the options passed on init.
+        Otherwise, we get the release and dist from the native package.
+      */
+      if (typeof options?.release === 'string') {
+        event.release = options.release;
+      }
+
+      if (typeof options?.dist === 'string') {
+        event.dist = options.dist;
+      }
+
+      if (event.release && event.dist) {
+        return event;
+      }
+
+      try {
+        const nativeRelease = await NATIVE.fetchNativeRelease();
+        if (nativeRelease) {
+          if (!event.release) {
+            event.release = `${nativeRelease.id}@${nativeRelease.version}+${nativeRelease.build}`;
+          }
+          if (!event.dist) {
+            event.dist = `${nativeRelease.build}`;
+          }
+        }
+      } catch (_Oo) {
+        // Something went wrong, we just continue
+      }
+
+      return event;
+    });
+  }
+}
diff --git a/src/sdk.ts b/src/sdk.ts
index ae8894f..1fb15b1 100644
--- a/src/sdk.ts
+++ b/src/sdk.ts
@@ -6,7 +6,7 @@ import {
 import { Hub, makeMain } from '@sentry/core';
 import { RewriteFrames } from '@sentry/integrations';
 
-import { DeviceContext, EventOrigin, SdkInfo } from './integrations';
+import { DeviceContext, EventOrigin, Release, SdkInfo } from './integrations';
 import type { CapacitorOptions } from './options';
 import { CapacitorScope } from './scope';
 import { DEFAULT_BUFFER_SIZE, makeNativeTransport } from './transports/native';
@@ -71,6 +71,7 @@ export function init<O>(
         return frame;
       },
     }),
+    new Release(),
     new SdkInfo(),
     new EventOrigin(),
   ];


Test patch:
diff --git a/test/integrations/release.test.ts b/test/integrations/release.test.ts
new file mode 100644
index 0000000..b942c99
--- /dev/null
+++ b/test/integrations/release.test.ts
@@ -0,0 +1,126 @@
+import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
+import type { EventProcessor } from '@sentry/types';
+
+import { Release } from '../../src/integrations/release';
+
+const mockRelease = Release;
+
+jest.mock('@sentry/core', () => {
+  const client = {
+    getOptions: jest.fn(),
+  };
+
+  const hub = {
+    getClient: () => client,
+    // out-of-scope variables have to be prefixed with `mock` caseSensitive
+    getIntegration: () => mockRelease,
+  };
+
+  return {
+    addGlobalEventProcessor: jest.fn(),
+    getCurrentHub: () => hub,
+  };
+});
+
+jest.mock('../../src/wrapper', () => ({
+  NATIVE: {
+    fetchNativeRelease: async () => ({
+      build: 'native_build',
+      id: 'native_id',
+      version: 'native_version',
+    }),
+  },
+}));
+
+describe('Tests the Release integration', () => {
+  test('Uses release from native SDK if release/dist are not present in options.', async () => {
+    const releaseIntegration = new Release();
+
+    let eventProcessor: EventProcessor = () => null;
+
+    // @ts-expect-error Mock
+    addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e));
+    releaseIntegration.setupOnce();
+
+    expect(addGlobalEventProcessor).toBeCalled();
+
+    const client = getCurrentHub().getClient();
+
+    // @ts-expect-error Mock
+    client.getOptions.mockImplementation(() => ({}));
+
+    const event = await eventProcessor({}, {});
+
+    expect(event?.release).toBe('native_id@native_version+native_build');
+    expect(event?.dist).toBe('native_build');
+  });
+
+  test('Uses release from native SDK if release is not present in options.', async () => {
+    const releaseIntegration = new Release();
+
+    let eventProcessor: EventProcessor = () => null;
+
+    // @ts-expect-error Mock
+    addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e));
+    releaseIntegration.setupOnce();
+
+    const client = getCurrentHub().getClient();
+
+    // @ts-expect-error Mock
+    client.getOptions.mockImplementation(() => ({
+      dist: 'options_dist',
+    }));
+
+    const event = await eventProcessor({}, {});
+
+    expect(event?.release).toBe('native_id@native_version+native_build');
+    expect(event?.dist).toBe('options_dist');
+  });
+
+  test('Uses dist from native SDK if dist is not present in options.', async () => {
+    const releaseIntegration = new Release();
+
+    let eventProcessor: EventProcessor = () => null;
+
+    // @ts-expect-error Mock
+    addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e));
+    releaseIntegration.setupOnce();
+
+    const client = getCurrentHub().getClient();
+
+    // @ts-expect-error Mock
+    client.getOptions.mockImplementation(() => ({
+      release: 'options_release',
+    }));
+
+    const event = await eventProcessor({}, {});
+
+    expect(event?.release).toBe('options_release');
+    expect(event?.dist).toBe('native_build');
+  });
+
+  test('Uses release and dist from options', async () => {
+    const releaseIntegration = new Release();
+
+    let eventProcessor: EventProcessor = () => null;
+
+    // @ts-expect-error Mock
+    addGlobalEventProcessor.mockImplementation(e => (eventProcessor = e));
+    releaseIntegration.setupOnce();
+
+    expect(addGlobalEventProcessor).toBeCalled();
+
+    const client = getCurrentHub().getClient();
+
+    // @ts-expect-error Mock
+    client.getOptions.mockImplementation(() => ({
+      dist: 'options_dist',
+      release: 'options_release',
+    }));
+
+    const event = await eventProcessor({}, {});
+
+    expect(event?.release).toBe('options_release');
+    expect(event?.dist).toBe('options_dist');
+  });
+});