Skip to content
Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
> [migration guide](https://docs.sentry.io/platforms/javascript/guides/capacitor/migration/) first.
<!-- prettier-ignore-end -->

## Unreleased

### Fixes

- Duplicated session When running Capacitor as an app ([#1088](https://github.com/getsentry/sentry-capacitor/pull/1088))

## 3.0.0-beta.2

### Features
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", "7.0.0"..<"9.0.0"),
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.0.0")
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.2.0")
],
targets: [
.target(
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 61 additions & 7 deletions ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import Foundation
import Capacitor
@preconcurrency import Sentry

// Keep compatibility with CocoaPods.
#if SWIFT_PACKAGE
import Sentry._Hybrid
#endif

/**
* Please read the Capacitor iOS Plugin Development Guide
* here: https://capacitorjs.com/docs/plugins/ios
Expand Down Expand Up @@ -70,7 +65,7 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
}

do {
let options = try SentryOptionsInternal.initWithDict(optionsDict)
let options = try createOptions(from: optionsDict)
let sdkVersion = PrivateSentrySDKOnly.getSdkVersionString()
PrivateSentrySDKOnly.setSdkName(nativeSdkName, andVersionString: sdkVersion)

Expand Down Expand Up @@ -102,8 +97,67 @@ public class SentryCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {

call.resolve()
} catch {
call.reject("Failed to start native SDK")
call.reject("Failed to start native SDK: \(error.localizedDescription)")
}
}

private func createOptions(from dict: [AnyHashable: Any]) throws -> Options {
guard let dsn = dict["dsn"] as? String else {
throw NSError(domain: "SentryCapacitor", code: 1, userInfo: [NSLocalizedDescriptionKey: "DSN is required"])
}

let options = Options()
options.dsn = dsn

if let debug = dict["debug"] as? Bool {
options.debug = debug
}

if let environment = dict["environment"] as? String {
options.environment = environment
}

if let release = dict["release"] as? String {
options.releaseName = release
}

if let dist = dict["dist"] as? String {
options.dist = dist
}

if let enableAutoSessionTracking = dict["enableAutoSessionTracking"] as? Bool {
options.enableAutoSessionTracking = enableAutoSessionTracking
}

if let sessionTrackingIntervalMillis = dict["sessionTrackingIntervalMillis"] as? Int {
options.sessionTrackingIntervalMillis = UInt(sessionTrackingIntervalMillis)
}

if let maxBreadcrumbs = dict["maxBreadcrumbs"] as? UInt {
options.maxBreadcrumbs = maxBreadcrumbs
}
Comment on lines +132 to +138
Copy link

Choose a reason for hiding this comment

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

Bug: A negative value for sessionTrackingIntervalMillis or maxBreadcrumbs from JavaScript will cause a fatal crash on iOS due to an unsafe Int to UInt cast.
Severity: HIGH

Suggested Fix

Before casting sessionTrackingIntervalMillis and maxBreadcrumbs to UInt, validate that the Int values are non-negative. If a negative value is received, it should be ignored or handled gracefully instead of attempting the cast. For example: if let value = dict["maxBreadcrumbs"] as? Int, value >= 0 { options.maxBreadcrumbs = UInt(value) }.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: ios/Sources/SentryCapacitorPlugin/SentryCapacitorPlugin.swift#L132-L138

Potential issue: The `createOptions` method manually parses
`sessionTrackingIntervalMillis` and `maxBreadcrumbs` from a dictionary. It casts these
values from `Int` to `UInt` using a direct initializer `UInt()`. In Swift, attempting to
initialize a `UInt` with a negative `Int` value triggers a fatal runtime error, which
will crash the application. The JavaScript layer, which provides these options, does not
perform any validation to prevent negative numbers from being passed. This means a
negative value for either of these options will lead to an unavoidable crash on the iOS
platform.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd rather limit this on the JavaScript side.
The PR is not changing the behavior of sessionTrackingIntervalMillis and maxBreadcrumbs so lets leave it as is and fix it on a separated PR.


if let enableNativeCrashHandling = dict["enableNativeCrashHandling"] as? Bool {
options.enableCrashHandler = enableNativeCrashHandling
}

if let attachStacktrace = dict["attachStacktrace"] as? Bool {
options.attachStacktrace = attachStacktrace
}

if let sampleRate = dict["sampleRate"] as? Double {
options.sampleRate = NSNumber(value: sampleRate)
}

if let tracesSampleRate = dict["tracesSampleRate"] as? Double {
options.tracesSampleRate = NSNumber(value: tracesSampleRate)
}

if let enableAutoPerformanceTracing = dict["enableAutoPerformanceTracing"] as? Bool {
options.enableAutoPerformanceTracing = enableAutoPerformanceTracing
}

return options
}

@objc func captureEnvelope(_ call: CAPPluginCall) {
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/default.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { breadcrumbsIntegration, browserApiErrorsIntegration, browserSessionIntegration, globalHandlersIntegration } from '@sentry/browser';
import { type Integration,dedupeIntegration, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration } from '@sentry/core';
import { type Integration, dedupeIntegration, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration } from '@sentry/core';
import type { CapacitorOptions } from '../options';
import { deviceContextIntegration } from './devicecontext';
import { eventOriginIntegration } from './eventorigin';
Expand Down
71 changes: 35 additions & 36 deletions src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { BrowserOptions } from '@sentry/browser';
import { init as browserInit } from '@sentry/browser';
import type { Integration } from '@sentry/core';
import { debug, getClient, getGlobalScope, getIntegrationsToSetup, getIsolationScope } from '@sentry/core';
import { debug, getClient, getGlobalScope, getIsolationScope } from '@sentry/core';
import { sdkInit } from './client';
import { getDefaultIntegrations } from './integrations/default';
import type { CapacitorClientOptions, CapacitorOptions } from './options';
Expand All @@ -19,48 +18,38 @@ import { NATIVE } from './wrapper';
*/
export function init(
passedOptions: CapacitorOptions,
originalInit: (passedOptions:BrowserOptions) => void = browserInit,
originalInit: (passedOptions: BrowserOptions) => void = browserInit,
): void {

const finalOptions = {
/**
* Shared options are the options that are shared between the browser and native SDKs.
*/
const sharedOptions = {
enableAutoSessionTracking: true,
enableWatchdogTerminationTracking: true,
enableCaptureFailedRequests: false,
...passedOptions,
};
finalOptions.siblingOptions && delete finalOptions.siblingOptions;
sharedOptions.siblingOptions && delete sharedOptions.siblingOptions;

if (finalOptions.enabled === false || NATIVE.platform === 'web') {
finalOptions.enableNative = false;
finalOptions.enableNativeNagger = false;
if (sharedOptions.enabled === false || NATIVE.platform === 'web') {
sharedOptions.enableNative = false;
sharedOptions.enableNativeNagger = false;
} else {
// keep the original value if user defined it.
finalOptions.enableNativeNagger ??= true;
finalOptions.enableNative ??= true;
sharedOptions.enableNativeNagger ??= true;
sharedOptions.enableNative ??= true;
}
// const capacitorHub = new Hub(undefined, new CapacitorScope());
// makeMain(capacitorHub);
const defaultIntegrations: false | Integration[] =
passedOptions.defaultIntegrations === undefined
? getDefaultIntegrations(finalOptions)
: passedOptions.defaultIntegrations;

finalOptions.integrations = getIntegrationsToSetup({
integrations: safeFactory(passedOptions.integrations, {
loggerMessage: 'The integrations threw an error',
}),
defaultIntegrations,
});

if (
finalOptions.enableNative &&
!passedOptions.transport &&
sharedOptions.enableNative &&
!passedOptions.transport &&
NATIVE.platform !== 'web'
) {
finalOptions.transport = passedOptions.transport || makeNativeTransport;
sharedOptions.transport = passedOptions.transport || makeNativeTransport;

finalOptions.transportOptions = {
...(passedOptions.transportOptions ?? {}),
sharedOptions.transportOptions = {
...(passedOptions.transportOptions ?? {}),
bufferSize: DEFAULT_BUFFER_SIZE,
};
}
Expand All @@ -69,25 +58,35 @@ finalOptions.transport = passedOptions.transport || makeNativeTransport;
useEncodePolyfill();
}

if (finalOptions.enableNative) {
if (sharedOptions.enableNative) {
enableSyncToNative(getGlobalScope());
enableSyncToNative(getIsolationScope());
}

/**
* Browser options are the options that are only used by the browser SDK.
*/
const browserOptions = {
...passedOptions.siblingOptions?.vueOptions,
...passedOptions.siblingOptions?.nuxtClientOptions,
...finalOptions,
autoSessionTracking:
NATIVE.platform === 'web' && finalOptions.enableAutoSessionTracking,
enableMetrics: finalOptions._experiments?.enableMetrics,
beforeSendMetric: finalOptions._experiments?.beforeSendMetric,
...sharedOptions,
integrations: safeFactory(passedOptions.integrations, { loggerMessage: 'The integrations threw an error' }),
enableMetrics: sharedOptions._experiments?.enableMetrics,
beforeSendMetric: sharedOptions._experiments?.beforeSendMetric,
} as BrowserOptions;


browserOptions.defaultIntegrations = passedOptions.defaultIntegrations === undefined
? getDefaultIntegrations(sharedOptions)
: passedOptions.defaultIntegrations;

/**
* Mobile options are the options that are only used by the native SDK.
*/
const mobileOptions = {
...finalOptions,
...sharedOptions,
enableAutoSessionTracking:
NATIVE.platform !== 'web' && finalOptions.enableAutoSessionTracking,
NATIVE.platform !== 'web' && sharedOptions.enableAutoSessionTracking,
} as CapacitorClientOptions;

sdkInit(browserOptions, mobileOptions, originalInit, passedOptions.transport);
Expand Down
Loading
Loading