Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

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

## 8.54.1-alpha.1

Expand Down
24 changes: 24 additions & 0 deletions Sources/Sentry/Public/SentryFrame.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ NS_SWIFT_NAME(Frame)
*/
@property (nonatomic, copy) NSNumber *_Nullable columnNumber;

/**
* Source code line at the error location.
* Mostly used for Godot errors.
*/
@property (nonatomic, copy) NSString *_Nullable contextLine;

/**
* Source code lines before the error location (up to 5 lines).
* Mostly used for Godot errors.
*/
@property (nonatomic, copy) NSArray<NSString *> *_Nullable preContext;

/**
* Source code lines after the error location (up to 5 lines).
* Mostly used for Godot errors.
*/
@property (nonatomic, copy) NSArray<NSString *> *_Nullable postContext;

/**
* Determines if the Frame is inApp or not
*/
Expand All @@ -84,6 +102,12 @@ NS_SWIFT_NAME(Frame)
*/
@property (nonatomic, copy) NSNumber *_Nullable stackStart;

/**
* A mapping of variables which were available within this frame.
* Mostly used for Godot errors.
*/
@property (nonatomic, copy) NSDictionary<NSString *, id> *_Nullable vars;

- (instancetype)init;
+ (instancetype)new NS_UNAVAILABLE;

Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryFrame.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "SentryFrame.h"
#import "NSMutableDictionary+Sentry.h"
#import "SentryNSDictionarySanitize.h"

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -27,6 +28,10 @@ - (instancetype)init
[serializedData setValue:self.imageAddress forKey:@"image_addr"];
[serializedData setValue:self.instructionAddress forKey:@"instruction_addr"];
[serializedData setValue:self.platform forKey:@"platform"];
[serializedData setValue:self.contextLine forKey:@"context_line"];
[serializedData setValue:self.preContext forKey:@"pre_context"];
[serializedData setValue:self.postContext forKey:@"post_context"];
[serializedData setValue:sentry_sanitize(self.vars) forKey:@"vars"];
[SentryDictionary setBoolValue:self.inApp forKey:@"in_app" intoDictionary:serializedData];
[SentryDictionary setBoolValue:self.stackStart
forKey:@"stack_start"
Expand Down
10 changes: 10 additions & 0 deletions Sources/Swift/Protocol/Codable/SentryFrameCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ extension FrameDecodable: Decodable {
// https://github.com/getsentry/sentry-cocoa/issues/4738
case lineNumber = "lineno"
case columnNumber = "colno"
case contextLine = "context_line"
case preContext = "pre_context"
case postContext = "post_context"
case vars
case inApp = "in_app"
case stackStart = "stack_start"
}
Expand All @@ -51,6 +55,12 @@ extension FrameDecodable: Decodable {
self.instructionAddress = try container.decodeIfPresent(String.self, forKey: .instructionAddress)
self.lineNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .lineNumber))?.value
self.columnNumber = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .columnNumber))?.value
self.contextLine = try container.decodeIfPresent(String.self, forKey: .contextLine)
self.preContext = try container.decodeIfPresent([String].self, forKey: .preContext)
self.postContext = try container.decodeIfPresent([String].self, forKey: .postContext)
self.vars = decodeArbitraryData {
try container.decodeIfPresent([String: ArbitraryData].self, forKey: .vars)
}
self.inApp = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .inApp))?.value
self.stackStart = (try container.decodeIfPresent(NSNumberDecodableWrapper.self, forKey: .stackStart))?.value
}
Expand Down
72 changes: 69 additions & 3 deletions Tests/SentryTests/Protocol/SentryFrameTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,40 @@ class SentryFrameTests: XCTestCase {
XCTAssertEqual(frame.imageAddress, actual["image_addr"] as? String)
XCTAssertEqual(frame.instructionAddress, actual["instruction_addr"] as? String)
XCTAssertEqual(frame.platform, actual["platform"] as? String)
XCTAssertEqual(frame.contextLine, actual["context_line"] as? String)
XCTAssertEqual(frame.preContext, actual["pre_context"] as? [String])
XCTAssertEqual(frame.postContext, actual["post_context"] as? [String])
XCTAssertEqual(frame.vars as? [String: AnyHashable], actual["vars"] as? [String: AnyHashable])
XCTAssertEqual(frame.inApp, actual["in_app"] as? NSNumber)
XCTAssertEqual(frame.stackStart, actual["stack_start"] as? NSNumber)
}


func testSerialize_WithGodotFrame() {
// Arrange
let frame = TestData.godotFrame

// Act
let actual = frame.serialize()

// Assert
XCTAssertEqual(frame.symbolAddress, actual["symbol_addr"] as? String)
XCTAssertEqual(frame.fileName, actual["filename"] as? String)
XCTAssertEqual(frame.function, actual["function"] as? String)
XCTAssertEqual(frame.module, actual["module"] as? String)
XCTAssertEqual(frame.lineNumber, actual["lineno"] as? NSNumber)
XCTAssertEqual(frame.columnNumber, actual["colno"] as? NSNumber)
XCTAssertEqual(frame.package, actual["package"] as? String)
XCTAssertEqual(frame.imageAddress, actual["image_addr"] as? String)
XCTAssertEqual(frame.instructionAddress, actual["instruction_addr"] as? String)
XCTAssertEqual(frame.platform, actual["platform"] as? String)
XCTAssertEqual(frame.contextLine, actual["context_line"] as? String)
XCTAssertEqual(frame.preContext, actual["pre_context"] as? [String])
XCTAssertEqual(frame.postContext, actual["post_context"] as? [String])
XCTAssertEqual(frame.vars as? [String: AnyHashable], actual["vars"] as? [String: AnyHashable])
XCTAssertEqual(frame.inApp, actual["in_app"] as? NSNumber)
XCTAssertEqual(frame.stackStart, actual["stack_start"] as? NSNumber)
}

func testDecode_WithAllProperties() throws {
// Arrange
let frame = TestData.mainFrame
Expand All @@ -42,11 +72,43 @@ class SentryFrameTests: XCTestCase {
XCTAssertEqual(frame.columnNumber, decoded.columnNumber)
XCTAssertEqual(frame.package, decoded.package)
XCTAssertEqual(frame.imageAddress, decoded.imageAddress)
XCTAssertEqual(frame.instructionAddress, decoded.instructionAddress)
XCTAssertEqual(frame.platform, decoded.platform)
XCTAssertEqual(frame.contextLine, decoded.contextLine)
XCTAssertEqual(frame.preContext, decoded.preContext)
XCTAssertEqual(frame.postContext, decoded.postContext)
XCTAssertEqual(frame.vars as? [String: AnyHashable], decoded.vars as? [String: AnyHashable])
XCTAssertEqual(frame.inApp, decoded.inApp)
XCTAssertEqual(frame.stackStart, decoded.stackStart)
}


func testDecode_WithGodotFrame() throws {
// Arrange
let frame = TestData.godotFrame
let data = try XCTUnwrap(SentrySerialization.data(withJSONObject: frame.serialize()))

// Act
let decoded = try XCTUnwrap(decodeFromJSONData(jsonData: data) as Frame?)

// Assert
XCTAssertEqual(frame.symbolAddress, decoded.symbolAddress)
XCTAssertEqual(frame.fileName, decoded.fileName)
XCTAssertEqual(frame.function, decoded.function)
XCTAssertEqual(frame.module, decoded.module)
XCTAssertEqual(frame.lineNumber, decoded.lineNumber)
XCTAssertEqual(frame.columnNumber, decoded.columnNumber)
XCTAssertEqual(frame.package, decoded.package)
XCTAssertEqual(frame.imageAddress, decoded.imageAddress)
XCTAssertEqual(frame.instructionAddress, decoded.instructionAddress)
XCTAssertEqual(frame.platform, decoded.platform)
XCTAssertEqual(frame.contextLine, decoded.contextLine)
XCTAssertEqual(frame.preContext, decoded.preContext)
XCTAssertEqual(frame.postContext, decoded.postContext)
XCTAssertEqual(frame.vars as? [String: AnyHashable], decoded.vars as? [String: AnyHashable])
XCTAssertEqual(frame.inApp, decoded.inApp)
XCTAssertEqual(frame.stackStart, decoded.stackStart)
}

func testDecode_WithAllPropertiesNil() throws {
// Arrange
let frame = Frame()
Expand All @@ -66,12 +128,16 @@ class SentryFrameTests: XCTestCase {
XCTAssertNil(decoded.imageAddress)
XCTAssertNil(decoded.instructionAddress)
XCTAssertNil(decoded.platform)
XCTAssertNil(decoded.contextLine)
XCTAssertNil(decoded.preContext)
XCTAssertNil(decoded.postContext)
XCTAssertNil(decoded.vars)
XCTAssertNil(decoded.inApp)
XCTAssertNil(decoded.stackStart)
}

func testSerialize_Bools() {
SentryBooleanSerialization.test(Frame(), property: "inApp", serializedProperty: "in_app")
SentryBooleanSerialization.test(Frame(), property: "stackStart", serializedProperty: "stack_start")
}
}
}
20 changes: 20 additions & 0 deletions Tests/SentryTests/Protocol/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,26 @@ class TestData {
return frame
}

static var godotFrame: Frame {
let frame = Frame()
frame.fileName = "player/player.gd"
frame.function = "take_damage"
frame.lineNumber = 42
frame.columnNumber = 15
frame.platform = "gdscript"
frame.inApp = true
frame.contextLine = " health -= damage"
frame.preContext = [
"func take_damage(damage):",
" if damage <= 0:",
" return"]
frame.postContext = [
" if health <= 0:",
" die()"]
frame.vars = ["damage": 25, "health": 75, "player_name": "Hero"]
return frame
}

static var outsideFrame: Frame {
let frame = Frame()
frame.columnNumber = 1
Expand Down
Loading
Loading