Skip to content

Commit 285ff55

Browse files
authored
feat: Add source context and vars fields to SentryFrame (#5853)
1 parent 9788cc8 commit 285ff55

File tree

8 files changed

+1159
-3
lines changed

8 files changed

+1159
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Structured Logs: Flush logs on SDK flush/close (#5834)
8+
- Add source context and vars fields to SentryFrame (#5853)
89

910
## 8.54.1-alpha.1
1011

Sources/Sentry/Public/SentryFrame.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,24 @@ NS_SWIFT_NAME(Frame)
7474
*/
7575
@property (nonatomic, copy) NSNumber *_Nullable columnNumber;
7676

77+
/**
78+
* Source code line at the error location.
79+
* Mostly used for Godot errors.
80+
*/
81+
@property (nonatomic, copy) NSString *_Nullable contextLine;
82+
83+
/**
84+
* Source code lines before the error location (up to 5 lines).
85+
* Mostly used for Godot errors.
86+
*/
87+
@property (nonatomic, copy) NSArray<NSString *> *_Nullable preContext;
88+
89+
/**
90+
* Source code lines after the error location (up to 5 lines).
91+
* Mostly used for Godot errors.
92+
*/
93+
@property (nonatomic, copy) NSArray<NSString *> *_Nullable postContext;
94+
7795
/**
7896
* Determines if the Frame is inApp or not
7997
*/
@@ -84,6 +102,12 @@ NS_SWIFT_NAME(Frame)
84102
*/
85103
@property (nonatomic, copy) NSNumber *_Nullable stackStart;
86104

105+
/**
106+
* A mapping of variables which were available within this frame.
107+
* Mostly used for Godot errors.
108+
*/
109+
@property (nonatomic, copy) NSDictionary<NSString *, id> *_Nullable vars;
110+
87111
- (instancetype)init;
88112
+ (instancetype)new NS_UNAVAILABLE;
89113

Sources/Sentry/SentryFrame.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import "SentryFrame.h"
22
#import "NSMutableDictionary+Sentry.h"
3+
#import "SentryNSDictionarySanitize.h"
34

45
NS_ASSUME_NONNULL_BEGIN
56

@@ -27,6 +28,10 @@ - (instancetype)init
2728
[serializedData setValue:self.imageAddress forKey:@"image_addr"];
2829
[serializedData setValue:self.instructionAddress forKey:@"instruction_addr"];
2930
[serializedData setValue:self.platform forKey:@"platform"];
31+
[serializedData setValue:self.contextLine forKey:@"context_line"];
32+
[serializedData setValue:self.preContext forKey:@"pre_context"];
33+
[serializedData setValue:self.postContext forKey:@"post_context"];
34+
[serializedData setValue:sentry_sanitize(self.vars) forKey:@"vars"];
3035
[SentryDictionary setBoolValue:self.inApp forKey:@"in_app" intoDictionary:serializedData];
3136
[SentryDictionary setBoolValue:self.stackStart
3237
forKey:@"stack_start"

Sources/Swift/Protocol/Codable/SentryFrameCodable.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ extension FrameDecodable: Decodable {
2727
// https://github.com/getsentry/sentry-cocoa/issues/4738
2828
case lineNumber = "lineno"
2929
case columnNumber = "colno"
30+
case contextLine = "context_line"
31+
case preContext = "pre_context"
32+
case postContext = "post_context"
33+
case vars
3034
case inApp = "in_app"
3135
case stackStart = "stack_start"
3236
}
@@ -51,6 +55,12 @@ extension FrameDecodable: Decodable {
5155
self.instructionAddress = try container.decodeIfPresent(String.self, forKey: .instructionAddress)
5256
self.lineNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .lineNumber))?.value
5357
self.columnNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .columnNumber))?.value
58+
self.contextLine = try container.decodeIfPresent(String.self, forKey: .contextLine)
59+
self.preContext = try container.decodeIfPresent([String].self, forKey: .preContext)
60+
self.postContext = try container.decodeIfPresent([String].self, forKey: .postContext)
61+
self.vars = decodeArbitraryData {
62+
try container.decodeIfPresent([String: ArbitraryData].self, forKey: .vars)
63+
}
5464
self.inApp = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .inApp))?.value
5565
self.stackStart = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .stackStart))?.value
5666
}

Tests/SentryTests/Protocol/SentryFrameTests.swift

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,40 @@ class SentryFrameTests: XCTestCase {
2121
XCTAssertEqual(frame.imageAddress, actual["image_addr"] as? String)
2222
XCTAssertEqual(frame.instructionAddress, actual["instruction_addr"] as? String)
2323
XCTAssertEqual(frame.platform, actual["platform"] as? String)
24+
XCTAssertEqual(frame.contextLine, actual["context_line"] as? String)
25+
XCTAssertEqual(frame.preContext, actual["pre_context"] as? [String])
26+
XCTAssertEqual(frame.postContext, actual["post_context"] as? [String])
27+
XCTAssertEqual(frame.vars as? [String: AnyHashable], actual["vars"] as? [String: AnyHashable])
2428
XCTAssertEqual(frame.inApp, actual["in_app"] as? NSNumber)
2529
XCTAssertEqual(frame.stackStart, actual["stack_start"] as? NSNumber)
2630
}
27-
31+
32+
func testSerialize_WithGodotFrame() {
33+
// Arrange
34+
let frame = TestData.godotFrame
35+
36+
// Act
37+
let actual = frame.serialize()
38+
39+
// Assert
40+
XCTAssertEqual(frame.symbolAddress, actual["symbol_addr"] as? String)
41+
XCTAssertEqual(frame.fileName, actual["filename"] as? String)
42+
XCTAssertEqual(frame.function, actual["function"] as? String)
43+
XCTAssertEqual(frame.module, actual["module"] as? String)
44+
XCTAssertEqual(frame.lineNumber, actual["lineno"] as? NSNumber)
45+
XCTAssertEqual(frame.columnNumber, actual["colno"] as? NSNumber)
46+
XCTAssertEqual(frame.package, actual["package"] as? String)
47+
XCTAssertEqual(frame.imageAddress, actual["image_addr"] as? String)
48+
XCTAssertEqual(frame.instructionAddress, actual["instruction_addr"] as? String)
49+
XCTAssertEqual(frame.platform, actual["platform"] as? String)
50+
XCTAssertEqual(frame.contextLine, actual["context_line"] as? String)
51+
XCTAssertEqual(frame.preContext, actual["pre_context"] as? [String])
52+
XCTAssertEqual(frame.postContext, actual["post_context"] as? [String])
53+
XCTAssertEqual(frame.vars as? [String: AnyHashable], actual["vars"] as? [String: AnyHashable])
54+
XCTAssertEqual(frame.inApp, actual["in_app"] as? NSNumber)
55+
XCTAssertEqual(frame.stackStart, actual["stack_start"] as? NSNumber)
56+
}
57+
2858
func testDecode_WithAllProperties() throws {
2959
// Arrange
3060
let frame = TestData.mainFrame
@@ -42,11 +72,43 @@ class SentryFrameTests: XCTestCase {
4272
XCTAssertEqual(frame.columnNumber, decoded.columnNumber)
4373
XCTAssertEqual(frame.package, decoded.package)
4474
XCTAssertEqual(frame.imageAddress, decoded.imageAddress)
75+
XCTAssertEqual(frame.instructionAddress, decoded.instructionAddress)
4576
XCTAssertEqual(frame.platform, decoded.platform)
77+
XCTAssertEqual(frame.contextLine, decoded.contextLine)
78+
XCTAssertEqual(frame.preContext, decoded.preContext)
79+
XCTAssertEqual(frame.postContext, decoded.postContext)
80+
XCTAssertEqual(frame.vars as? [String: AnyHashable], decoded.vars as? [String: AnyHashable])
4681
XCTAssertEqual(frame.inApp, decoded.inApp)
4782
XCTAssertEqual(frame.stackStart, decoded.stackStart)
4883
}
49-
84+
85+
func testDecode_WithGodotFrame() throws {
86+
// Arrange
87+
let frame = TestData.godotFrame
88+
let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: frame.serialize()))
89+
90+
// Act
91+
let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Frame?)
92+
93+
// Assert
94+
XCTAssertEqual(frame.symbolAddress, decoded.symbolAddress)
95+
XCTAssertEqual(frame.fileName, decoded.fileName)
96+
XCTAssertEqual(frame.function, decoded.function)
97+
XCTAssertEqual(frame.module, decoded.module)
98+
XCTAssertEqual(frame.lineNumber, decoded.lineNumber)
99+
XCTAssertEqual(frame.columnNumber, decoded.columnNumber)
100+
XCTAssertEqual(frame.package, decoded.package)
101+
XCTAssertEqual(frame.imageAddress, decoded.imageAddress)
102+
XCTAssertEqual(frame.instructionAddress, decoded.instructionAddress)
103+
XCTAssertEqual(frame.platform, decoded.platform)
104+
XCTAssertEqual(frame.contextLine, decoded.contextLine)
105+
XCTAssertEqual(frame.preContext, decoded.preContext)
106+
XCTAssertEqual(frame.postContext, decoded.postContext)
107+
XCTAssertEqual(frame.vars as? [String: AnyHashable], decoded.vars as? [String: AnyHashable])
108+
XCTAssertEqual(frame.inApp, decoded.inApp)
109+
XCTAssertEqual(frame.stackStart, decoded.stackStart)
110+
}
111+
50112
func testDecode_WithAllPropertiesNil() throws {
51113
// Arrange
52114
let frame = Frame()
@@ -66,12 +128,16 @@ class SentryFrameTests: XCTestCase {
66128
XCTAssertNil(decoded.imageAddress)
67129
XCTAssertNil(decoded.instructionAddress)
68130
XCTAssertNil(decoded.platform)
131+
XCTAssertNil(decoded.contextLine)
132+
XCTAssertNil(decoded.preContext)
133+
XCTAssertNil(decoded.postContext)
134+
XCTAssertNil(decoded.vars)
69135
XCTAssertNil(decoded.inApp)
70136
XCTAssertNil(decoded.stackStart)
71137
}
72138

73139
func testSerialize_Bools() {
74140
SentryBooleanSerialization.test(Frame(), property: "inApp", serializedProperty: "in_app")
75141
SentryBooleanSerialization.test(Frame(), property: "stackStart", serializedProperty: "stack_start")
76-
}
142+
}
77143
}

Tests/SentryTests/Protocol/TestData.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,26 @@ class TestData {
204204
return frame
205205
}
206206

207+
static var godotFrame: Frame {
208+
let frame = Frame()
209+
frame.fileName = "player/player.gd"
210+
frame.function = "take_damage"
211+
frame.lineNumber = 42
212+
frame.columnNumber = 15
213+
frame.platform = "gdscript"
214+
frame.inApp = true
215+
frame.contextLine = " health -= damage"
216+
frame.preContext = [
217+
"func take_damage(damage):",
218+
" if damage <= 0:",
219+
" return"]
220+
frame.postContext = [
221+
" if health <= 0:",
222+
" die()"]
223+
frame.vars = ["damage": 25, "health": 75, "player_name": "Hero"]
224+
return frame
225+
}
226+
207227
static var outsideFrame: Frame {
208228
let frame = Frame()
209229
frame.columnNumber = 1

0 commit comments

Comments
 (0)