Skip to content

Commit 8fa1083

Browse files
Add detailed analysis of Sentry iOS stack trace capture issue #341
Co-authored-by: giancarlo.buenaflor <giancarlo.buenaflor@sentry.io>
1 parent 45eb764 commit 8fa1083

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

issue-341-analysis.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Analysis of Sentry Kotlin Multiplatform Issue #341
2+
3+
## Issue Summary
4+
When using `captureException()` on iOS, the captured stack trace shows where the exception was captured (site B) rather than where it was created (site A). This works correctly on Android but not on iOS.
5+
6+
## Root Cause Analysis
7+
8+
### The Core Problem
9+
The issue stems from a fundamental difference in how exceptions are handled between iOS and Android:
10+
11+
1. **On Android/JVM**: When `captureException` is called, the Sentry Java SDK correctly uses the exception's original stack trace that was captured when the exception was instantiated.
12+
13+
2. **On iOS**: The Sentry Cocoa SDK ignores the NSException's `callStackReturnAddresses` property and instead captures the current thread's stack trace at the time `captureException` is called.
14+
15+
### Technical Deep Dive
16+
17+
#### 1. Kotlin Multiplatform's Exception Conversion
18+
When a Kotlin `Throwable` is converted to an `NSException` for iOS, the KMP SDK does the right thing:
19+
20+
```kotlin
21+
// In NSException.kt
22+
internal fun Throwable.asNSException(appendCausedBy: Boolean = false): NSException {
23+
val returnAddresses = getFilteredStackTraceAddresses()
24+
// ...
25+
return ThrowableNSException(name, getReason(appendCausedBy), returnAddresses)
26+
}
27+
28+
internal class ThrowableNSException(
29+
name: String,
30+
reason: String?,
31+
private val returnAddresses: List<NSNumber>
32+
) : NSException(name, reason, null) {
33+
override fun callStackReturnAddresses(): List<NSNumber> = returnAddresses
34+
}
35+
```
36+
37+
The `ThrowableNSException` correctly overrides `callStackReturnAddresses()` to provide the original stack trace from when the Kotlin exception was created.
38+
39+
#### 2. Sentry Cocoa's Exception Handling
40+
However, in the Sentry Cocoa SDK, when `captureException` is called:
41+
42+
```objc
43+
// In SentryClient.m
44+
- (SentryEvent *)buildExceptionEvent:(NSException *)exception
45+
{
46+
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
47+
SentryException *sentryException = [[SentryException alloc] initWithValue:exception.reason
48+
type:exception.name];
49+
event.exceptions = @[ sentryException ];
50+
// Note: No stack trace is extracted from the NSException here!
51+
return event;
52+
}
53+
```
54+
55+
Later, when preparing the event:
56+
57+
```objc
58+
// In SentryClient.m, line 748-749
59+
if (!isFatalEvent && shouldAttachStacktrace && threadsNotAttached) {
60+
event.threads = [self.threadInspector getCurrentThreads];
61+
}
62+
```
63+
64+
The SDK attaches the CURRENT thread's stack trace instead of using the NSException's `callStackReturnAddresses`.
65+
66+
#### 3. Platform-Specific Behavior
67+
There is a special case for macOS only:
68+
69+
```objc
70+
#if TARGET_OS_OSX
71+
if ([exception isKindOfClass:[SentryUseNSExceptionCallstackWrapper class]]) {
72+
event.threads = [(SentryUseNSExceptionCallstackWrapper *)exception buildThreads];
73+
}
74+
#endif
75+
```
76+
77+
The `SentryUseNSExceptionCallstackWrapper` class properly uses `callStackReturnAddresses` to build the stack trace, but this is only available on macOS, not iOS.
78+
79+
## Why This Happens
80+
81+
1. **Design Philosophy**: The Sentry Cocoa SDK appears to be designed primarily for capturing crashes at the point they occur, not for capturing pre-existing exceptions with their original stack traces.
82+
83+
2. **iOS vs macOS**: The special handling for preserving NSException stack traces exists only for macOS (`TARGET_OS_OSX`), not for iOS.
84+
85+
3. **Missing Integration**: There's no code in the iOS path that checks if an NSException has `callStackReturnAddresses` and uses them instead of capturing the current thread state.
86+
87+
## Impact
88+
89+
This issue significantly reduces the usefulness of error reporting on iOS for Kotlin Multiplatform applications because:
90+
- Developers cannot see where exceptions were actually thrown
91+
- All exceptions appear to originate from the Sentry SDK itself
92+
- Debugging production issues becomes much more difficult
93+
94+
## Verification
95+
96+
The issue can be verified by:
97+
1. Creating an exception at one location in the code
98+
2. Passing it around and capturing it later
99+
3. Observing that the stack trace shows the capture location, not the creation location
100+
101+
This behavior is consistent with the code analysis above and explains why the user sees Sentry frames at the top of their stack traces.
102+
103+
## Potential Solutions
104+
105+
### Solution 1: Extend iOS Support in Sentry Cocoa
106+
The Sentry Cocoa SDK could be modified to:
107+
1. Check if an NSException has `callStackReturnAddresses`
108+
2. Use those addresses to build the stack trace instead of capturing current threads
109+
3. This would require changes similar to the macOS-only `SentryUseNSExceptionCallstackWrapper`
110+
111+
### Solution 2: Workaround in KMP SDK
112+
The Sentry KMP SDK could:
113+
1. Capture the stack trace immediately when an exception is created
114+
2. Store it separately and attach it as additional data
115+
3. This is less ideal as it wouldn't integrate as cleanly with Sentry's UI
116+
117+
### Solution 3: Use NSExceptionKt or Similar
118+
Third-party libraries like NSExceptionKt have already solved this problem by properly handling Kotlin exceptions on iOS, but this would require additional dependencies.
119+
120+
## The Proper Fix
121+
122+
The most straightforward fix would be to modify the Sentry Cocoa SDK's `buildExceptionEvent` method to utilize the NSException's `callStackReturnAddresses` property. Here's what needs to be changed:
123+
124+
### Current Implementation (SentryClient.m):
125+
```objc
126+
- (SentryEvent *)buildExceptionEvent:(NSException *)exception
127+
{
128+
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
129+
SentryException *sentryException = [[SentryException alloc] initWithValue:exception.reason
130+
type:exception.name];
131+
event.exceptions = @[ sentryException ];
132+
// Stack trace is not set here!
133+
return event;
134+
}
135+
```
136+
137+
### Proposed Implementation:
138+
```objc
139+
- (SentryEvent *)buildExceptionEvent:(NSException *)exception
140+
{
141+
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
142+
SentryException *sentryException = [[SentryException alloc] initWithValue:exception.reason
143+
type:exception.name];
144+
145+
// Check if the exception has callStackReturnAddresses
146+
NSArray *addresses = [exception callStackReturnAddresses];
147+
if (addresses && addresses.count > 0) {
148+
// Build a stack trace from the NSException's addresses
149+
SentryStacktrace *stacktrace = [self buildStacktraceFromAddresses:addresses];
150+
sentryException.stacktrace = stacktrace;
151+
}
152+
153+
event.exceptions = @[ sentryException ];
154+
return event;
155+
}
156+
```
157+
158+
This would need a helper method to convert the addresses to a proper `SentryStacktrace`, similar to what's done in `SentryUseNSExceptionCallstackWrapper` for macOS.
159+
160+
### Alternative Approach in Sentry Cocoa
161+
162+
Another approach would be to extend the existing macOS-only solution to iOS by:
163+
164+
1. Removing the `#if TARGET_OS_OSX` condition
165+
2. Making `SentryUseNSExceptionCallstackWrapper` available for iOS
166+
3. Updating the capture flow to check if an NSException has valid `callStackReturnAddresses`
167+
168+
This would ensure that Kotlin/Native exceptions (and any other NSExceptions with proper stack traces) are correctly captured on iOS with their original stack traces.
169+
170+
## Summary
171+
172+
The issue is that Sentry Cocoa SDK on iOS ignores the NSException's `callStackReturnAddresses` property and instead captures the current thread's stack trace. This results in misleading crash reports where all exceptions appear to originate from the Sentry SDK itself rather than showing where they were actually thrown.
173+
174+
The fix requires modifying the Sentry Cocoa SDK to respect the NSException's stack trace when available, similar to how it's already done for macOS but not for iOS.

sentry-cocoa

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 0b5fd21157838bbecf63088bc9a7a9210d0ed0d4

0 commit comments

Comments
 (0)