From 84662534b9301573ec7f833128d1002f1a546b5f Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Thu, 20 Nov 2025 10:34:53 +0100 Subject: [PATCH 1/7] feat: Add attributes data to `SentryScope` --- .../SentrySampleShared/SentrySDKWrapper.swift | 4 + .../iOS-Swift/Base.lproj/Main.storyboard | 120 ++++++++++++++++++ .../ViewControllers/ScopeViewController.swift | 61 +++++++++ Sources/Sentry/Public/SentryScope.h | 16 +++ Sources/Sentry/SentryScope.m | 37 ++++++ Tests/SentryTests/SentryScopeTests.m | 40 ++++++ 6 files changed, 278 insertions(+) create mode 100644 Samples/iOS-Swift/iOS-Swift/ViewControllers/ScopeViewController.swift diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 04199a023c5..7e94f7c8dee 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -196,6 +196,10 @@ public struct SentrySDKWrapper { } let data = Data("hello".utf8) scope.addAttachment(Attachment(data: data, filename: "log.txt")) + + scope.setAttribute(value: "\(Bundle.main.bundleIdentifier ?? "")-custom-attribute", key: "custom-attribute-text") + scope.setAttribute(value: Date.now.timeIntervalSince1970, key: "custom-attribute-numeric") + scope.setAttribute(value: true, key: "custom-attribute-boolean") return scope } diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index a71526f9884..6b403f1ed09 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -1241,6 +1241,16 @@ + @@ -1815,6 +1825,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/ScopeViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/ScopeViewController.swift new file mode 100644 index 00000000000..721c78831c1 --- /dev/null +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/ScopeViewController.swift @@ -0,0 +1,61 @@ +import Sentry +import UIKit + +class ScopeViewController: UIViewController { + + @IBOutlet var attributesTextView: UITextView! + @IBOutlet var attributeNameField: UITextField! + @IBOutlet var attributeValueField: UITextField! + + override func viewDidLoad() { + super.viewDidLoad() + + updateAttributesTextView() + } + + @IBAction func setAttribute(_ sender: Any?) { + guard let attributeName = attributeNameField.text, let attributeValue = attributeValueField.text else { + return + } + + SentrySDK.configureScope { scope in + scope.setAttribute(value: attributeValue, key: attributeName) + } + + updateAttributesTextView() + } + + @IBAction func removeAttribute(_ sender: Any?) { + guard let attributeName = attributeNameField.text else { + return + } + + SentrySDK.configureScope { scope in + scope.removeAttribute(key: attributeName) + } + + updateAttributesTextView() + } + + @IBAction func updateAttributesTextView(_ sender: Any?) { + updateAttributesTextView() + } + + private func updateAttributesTextView() { + SentrySDK.configureScope { [weak self] scope in + guard let self else { return } + + guard let jsonData = try? JSONSerialization.data(withJSONObject: scope.attributes, options: [.prettyPrinted]) else { + self.attributesTextView.text = "Error serializing attributes to JSON" + return + } + + guard let jsonString = String(data: jsonData, encoding: .utf8) else { + self.attributesTextView.text = "Error converting data to JSON text" + return + } + + self.attributesTextView.text = jsonString + } + } +} diff --git a/Sources/Sentry/Public/SentryScope.h b/Sources/Sentry/Public/SentryScope.h index 2acfa764e6a..e59ab3ff504 100644 --- a/Sources/Sentry/Public/SentryScope.h +++ b/Sources/Sentry/Public/SentryScope.h @@ -40,6 +40,11 @@ NS_SWIFT_NAME(Scope) */ @property (nonatomic, readonly, copy) NSDictionary *tags; +/** + * Gets the dictionary of currently set attributes. + */ +@property (nonatomic, readonly, copy) NSDictionary *attributes; + - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs NS_DESIGNATED_INITIALIZER; - (instancetype)init; - (instancetype)initWithScope:(SentryScope *)scope; @@ -136,6 +141,17 @@ NS_SWIFT_NAME(Scope) */ - (void)addAttachment:(SentryAttachment *)attachment NS_SWIFT_NAME(addAttachment(_:)); +/** + * Set global attributes. Attributes are searchable key/value string pairs attached to every log + * message. + */ +- (void)setAttributeValue:(id)value forKey:(NSString *)key NS_SWIFT_NAME(setAttribute(value:key:)); + +/** + * Remove the attribute for the specified key. + */ +- (void)removeAttributeForKey:(NSString *)key NS_SWIFT_NAME(removeAttribute(key:)); + /** * Clears all attachments in the scope. */ diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 8b5f312b504..5f16bf8fa32 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -28,6 +28,8 @@ @interface SentryScope () @property (atomic, strong) NSMutableArray *breadcrumbArray; +@property (atomic, strong) NSMutableDictionary *attributesDictionary; + @end @implementation SentryScope { @@ -49,6 +51,7 @@ - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs self.contextDictionary = [NSMutableDictionary new]; self.attachmentArray = [NSMutableArray new]; self.fingerprintArray = [NSMutableArray new]; + self.attributesDictionary = [NSMutableDictionary new]; _spanLock = [[NSObject alloc] init]; self.observers = [NSMutableArray new]; self.propagationContext = [[SentryPropagationContext alloc] init]; @@ -672,6 +675,40 @@ - (NSString *)propagationContextTraceIdString return [self.propagationContext.traceId sentryIdString]; } +- (NSDictionary *)attributes +{ + @synchronized(_attributesDictionary) { + return _attributesDictionary.copy; + } +} + +- (void)setAttributeValue:(id)value forKey:(NSString *)key +{ + if (key == nil || key.length == 0) { + SENTRY_LOG_ERROR(@"Attribute's key cannot be nil nor empty"); + return; + } + + @synchronized(_attributesDictionary) { + _attributesDictionary[key] = value; + + // ScopeObservers are not called since at this moment attributes are only used for Logs, + // which the LogBatcher obtains manually. At this moment not even Spans use this attributes. + // Should this change, we will need to call the observers. + } +} + +- (void)removeAttributeForKey:(NSString *)key +{ + @synchronized(_attributesDictionary) { + [_attributesDictionary removeObjectForKey:key]; + + // ScopeObservers are not called since at this moment attributes are only used for Logs, + // which the LogBatcher obtains manually. At this moment not even Spans use this attributes. + // Should this change, we will need to call the observers. + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryScopeTests.m b/Tests/SentryTests/SentryScopeTests.m index 92f5c273955..4a483b3f4da 100644 --- a/Tests/SentryTests/SentryScopeTests.m +++ b/Tests/SentryTests/SentryScopeTests.m @@ -137,4 +137,44 @@ - (void)testReplaySerializes XCTAssertEqualObjects([[scope serialize] objectForKey:@"replay_id"], expectedReplayId); } +- (void)testSetStringAttribute +{ + SentryScope *scope = [[SentryScope alloc] init]; + [scope setAttributeValue:@"test-string" forKey:@"a-string-key"]; + XCTAssertEqualObjects([scope attributes][@"a-string-key"], @"test-string"); +} + +- (void)testSetBoolAttribute +{ + SentryScope *scope = [[SentryScope alloc] init]; + [scope setAttributeValue:@(true) forKey:@"a-bool-key"]; + [scope setAttributeValue:@(false) forKey:@"a-bool-key-false"]; + XCTAssertEqualObjects([scope attributes][@"a-bool-key"], @(true)); + XCTAssertEqualObjects([scope attributes][@"a-bool-key-false"], @(false)); +} + +- (void)testSetDoubleAttribute +{ + SentryScope *scope = [[SentryScope alloc] init]; + [scope setAttributeValue:@(1.4728) forKey:@"a-double-key"]; + XCTAssertEqualObjects([scope attributes][@"a-double-key"], @(1.4728)); +} + +- (void)testSetIntegerAttribute +{ + SentryScope *scope = [[SentryScope alloc] init]; + [scope setAttributeValue:@(4) forKey:@"an-integer-key"]; + XCTAssertEqualObjects([scope attributes][@"an-integer-key"], @(4)); +} + +- (void)testRemoveAttribute +{ + SentryScope *scope = [[SentryScope alloc] init]; + [scope setAttributeValue:@"test-string" forKey:@"a-key"]; + XCTAssertEqualObjects([scope attributes][@"a-key"], @"test-string"); + + [scope removeAttributeForKey:@"a-key"]; + XCTAssertEqualObjects([scope attributes][@"a-key"], nil); +} + @end From 5be4764ad81ac78bd46b0e131b5c2100fe1f8728 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Thu, 20 Nov 2025 10:49:23 +0100 Subject: [PATCH 2/7] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a19b9eb515f..05c77f9cee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add attributes data to `SentryScope` (#6830) + ## 9.0.0-rc.0 ### Breaking Changes From 6566e2dfe0b426aa906e5f7846108b4bc0508bc9 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Thu, 20 Nov 2025 10:57:51 +0100 Subject: [PATCH 3/7] Update sdk_api.json --- sdk_api.json | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/sdk_api.json b/sdk_api.json index 79f901f0517..ffcc1f217cd 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -15160,6 +15160,80 @@ } ] }, + { + "kind": "Var", + "name": "attributes", + "printedName": "attributes", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Any]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "ProtocolComposition", + "printedName": "Any" + } + ], + "usr": "s:SD" + } + ], + "declKind": "Var", + "usr": "c:objc(cs)SentryScope(py)attributes", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "attributes", + "declAttributes": [ + "ObjC", + "Dynamic" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Any]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "ProtocolComposition", + "printedName": "Any" + } + ], + "usr": "s:SD" + } + ], + "declKind": "Accessor", + "usr": "c:objc(cs)SentryScope(im)attributes", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "attributes", + "declAttributes": [ + "DiscardableResult", + "ObjC", + "Dynamic" + ], + "accessorKind": "get" + } + ] + }, { "kind": "Constructor", "name": "init", @@ -15953,6 +16027,81 @@ ], "funcSelfKind": "NonMutating" }, + { + "kind": "Function", + "name": "setAttribute", + "printedName": "setAttribute(value:key:)", + "children": [ + { + "kind": "TypeNameAlias", + "name": "Void", + "printedName": "Swift.Void", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ] + }, + { + "kind": "TypeNominal", + "name": "ProtocolComposition", + "printedName": "Any" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "c:objc(cs)SentryScope(im)setAttributeValue:forKey:", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "setAttributeValue:forKey:", + "declAttributes": [ + "ObjC", + "Dynamic" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "removeAttribute", + "printedName": "removeAttribute(key:)", + "children": [ + { + "kind": "TypeNameAlias", + "name": "Void", + "printedName": "Swift.Void", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ] + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "c:objc(cs)SentryScope(im)removeAttributeForKey:", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "removeAttributeForKey:", + "declAttributes": [ + "ObjC", + "Dynamic" + ], + "funcSelfKind": "NonMutating" + }, { "kind": "Function", "name": "clearAttachments", From 6eadb48e80d3eaf6300e32108d47ac7ab7268bfd Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Thu, 20 Nov 2025 11:29:05 +0100 Subject: [PATCH 4/7] feat: Add Scope atributes into log --- Sources/Swift/Tools/SentryLogBatcher.swift | 8 ++++ Tests/SentryTests/SentryLogBatcherTests.swift | 41 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 5151907c0ea..1abdf694615 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -90,6 +90,7 @@ import Foundation addDeviceAttributes(to: &log.attributes, scope: scope) addUserAttributes(to: &log.attributes, scope: scope) addReplayAttributes(to: &log.attributes, scope: scope) + addScopeAttributes(to: &log.attributes, scope: scope) let propagationContextTraceIdString = scope.propagationContextTraceIdString log.traceId = SentryId(uuidString: propagationContextTraceIdString) @@ -187,6 +188,13 @@ import Foundation #endif #endif } + + private func addScopeAttributes(to attributes: inout [String: SentryLog.Attribute], scope: Scope) { + // Scope attributes should not override any existing attribute in the log + for (key, value) in scope.attributes where attributes[key] == nil { + attributes[key] = .init(value: value) + } + } // Only ever call this from the serial dispatch queue. private func encodeAndBuffer(log: SentryLog) { diff --git a/Tests/SentryTests/SentryLogBatcherTests.swift b/Tests/SentryTests/SentryLogBatcherTests.swift index 962eba5238d..350dcb20479 100644 --- a/Tests/SentryTests/SentryLogBatcherTests.swift +++ b/Tests/SentryTests/SentryLogBatcherTests.swift @@ -494,6 +494,47 @@ final class SentryLogBatcherTests: XCTestCase { XCTAssertNil(attributes["device.family"]) } + func testAddLog_AddsScopeAttributes() throws { + let scope = Scope() + scope.setAttribute(value: "aString", key: "string-attribute") + scope.setAttribute(value: false, key: "bool-attribute") + scope.setAttribute(value: 1.765, key: "double-attribute") + scope.setAttribute(value: 5, key: "integer-attribute") + + let log = createTestLog(body: "Test log message with user") + sut.addLog(log, scope: scope) + sut.captureLogs() + + let capturedLogs = testDelegate.getCapturedLogs() + let capturedLog = try XCTUnwrap(capturedLogs.first) + let attributes = capturedLog.attributes + + XCTAssertEqual(attributes["string-attribute"]?.value as? String, "aString") + XCTAssertEqual(attributes["string-attribute"]?.type, "string") + XCTAssertEqual(attributes["bool-attribute"]?.value as? Bool, false) + XCTAssertEqual(attributes["bool-attribute"]?.type, "boolean") + XCTAssertEqual(attributes["double-attribute"]?.value as? Double, 1.765) + XCTAssertEqual(attributes["double-attribute"]?.type, "double") + XCTAssertEqual(attributes["integer-attribute"]?.value as? Int, 5) + XCTAssertEqual(attributes["integer-attribute"]?.type, "integer") + } + + func testAddLog_ScopeAttributesDoNotOverrideLogAttribute() throws { + let scope = Scope() + scope.setAttribute(value: true, key: "log-attribute") + + let log = createTestLog(body: "Test log message with user", attributes: [ "log-attribute": .init(value: false)]) + sut.addLog(log, scope: scope) + sut.captureLogs() + + let capturedLogs = testDelegate.getCapturedLogs() + let capturedLog = try XCTUnwrap(capturedLogs.first) + let attributes = capturedLog.attributes + + XCTAssertEqual(attributes["log-attribute"]?.value as? Bool, false) + XCTAssertEqual(attributes["log-attribute"]?.type, "boolean") + } + // MARK: - Replay Attributes Tests #if canImport(UIKit) && !SENTRY_NO_UIKIT From 2854019473ff52fc5f44a928da05d151887e1c50 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Thu, 20 Nov 2025 11:32:38 +0100 Subject: [PATCH 5/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c77f9cee8..5942e815d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add attributes data to `SentryScope` (#6830) +- Add `SentryScope` attributes into log messages (#6834) ## 9.0.0-rc.0 From 9ec394459646393169a2d63defc0e392f0e2f7c6 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Mon, 1 Dec 2025 13:53:49 -0300 Subject: [PATCH 6/7] Undo changes from merge conflict --- Sources/Sentry/SentryScope.m | 78 ---------------------------- Tests/SentryTests/SentryScopeTests.m | 40 -------------- 2 files changed, 118 deletions(-) diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 5098e5f5392..e1bc15dbdb3 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -29,8 +29,6 @@ @interface SentryScope () @property (atomic, strong) NSMutableArray *breadcrumbArray; -@property (atomic, strong) NSMutableDictionary *attributesDictionary; - @end @implementation SentryScope { @@ -52,7 +50,6 @@ - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs self.contextDictionary = [NSMutableDictionary new]; self.attachmentArray = [NSMutableArray new]; self.fingerprintArray = [NSMutableArray new]; - self.attributesDictionary = [NSMutableDictionary new]; _spanLock = [[NSObject alloc] init]; self.observers = [NSMutableArray new]; self.propagationContext = [[SentryPropagationContext alloc] init]; @@ -77,7 +74,6 @@ - (instancetype)initWithScope:(SentryScope *)scope [_breadcrumbArray addObjectsFromArray:crumbs]; [_fingerprintArray addObjectsFromArray:[scope fingerprints]]; [_attachmentArray addObjectsFromArray:[scope attachments]]; - [_attributesDictionary addEntriesFromDictionary:[scope attributes]]; self.propagationContext = scope.propagationContext; self.maxBreadcrumbs = scope.maxBreadcrumbs; @@ -195,9 +191,6 @@ - (void)clear @synchronized(_spanLock) { _span = nil; } - @synchronized(_attributesDictionary) { - [_attributesDictionary removeAllObjects]; - } self.userObject = nil; self.distString = nil; @@ -474,40 +467,6 @@ - (void)clearAttachments } } -- (NSDictionary *)attributes -{ - @synchronized(_attributesDictionary) { - return _attributesDictionary.copy; - } -} - -- (void)setAttributeValue:(id)value forKey:(NSString *)key -{ - if (key == nil || key.length == 0) { - SENTRY_LOG_ERROR(@"Attribute's key cannot be nil nor empty"); - return; - } - - @synchronized(_attributesDictionary) { - _attributesDictionary[key] = value; - - for (id observer in self.observers) { - [observer setAttributes:_attributesDictionary]; - } - } -} - -- (void)removeAttributeForKey:(NSString *)key -{ - @synchronized(_attributesDictionary) { - [_attributesDictionary removeObjectForKey:key]; - - for (id observer in self.observers) { - [observer setAttributes:_attributesDictionary]; - } - } -} - - (NSDictionary *)serialize { NSMutableDictionary *serializedData = [NSMutableDictionary new]; @@ -550,9 +509,6 @@ - (void)removeAttributeForKey:(NSString *)key if (crumbs.count > 0) { [serializedData setValue:crumbs forKey:@"breadcrumbs"]; } - if (self.attributes.count > 0) { - [serializedData setValue:[self attributes] forKey:@"attributes"]; - } return serializedData; } @@ -712,40 +668,6 @@ - (NSString *)propagationContextTraceIdString return [self.propagationContext.traceId sentryIdString]; } -- (NSDictionary *)attributes -{ - @synchronized(_attributesDictionary) { - return _attributesDictionary.copy; - } -} - -- (void)setAttributeValue:(id)value forKey:(NSString *)key -{ - if (key == nil || key.length == 0) { - SENTRY_LOG_ERROR(@"Attribute's key cannot be nil nor empty"); - return; - } - - @synchronized(_attributesDictionary) { - _attributesDictionary[key] = value; - - // ScopeObservers are not called since at this moment attributes are only used for Logs, - // which the LogBatcher obtains manually. At this moment not even Spans use this attributes. - // Should this change, we will need to call the observers. - } -} - -- (void)removeAttributeForKey:(NSString *)key -{ - @synchronized(_attributesDictionary) { - [_attributesDictionary removeObjectForKey:key]; - - // ScopeObservers are not called since at this moment attributes are only used for Logs, - // which the LogBatcher obtains manually. At this moment not even Spans use this attributes. - // Should this change, we will need to call the observers. - } -} - @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/SentryScopeTests.m b/Tests/SentryTests/SentryScopeTests.m index 4a483b3f4da..92f5c273955 100644 --- a/Tests/SentryTests/SentryScopeTests.m +++ b/Tests/SentryTests/SentryScopeTests.m @@ -137,44 +137,4 @@ - (void)testReplaySerializes XCTAssertEqualObjects([[scope serialize] objectForKey:@"replay_id"], expectedReplayId); } -- (void)testSetStringAttribute -{ - SentryScope *scope = [[SentryScope alloc] init]; - [scope setAttributeValue:@"test-string" forKey:@"a-string-key"]; - XCTAssertEqualObjects([scope attributes][@"a-string-key"], @"test-string"); -} - -- (void)testSetBoolAttribute -{ - SentryScope *scope = [[SentryScope alloc] init]; - [scope setAttributeValue:@(true) forKey:@"a-bool-key"]; - [scope setAttributeValue:@(false) forKey:@"a-bool-key-false"]; - XCTAssertEqualObjects([scope attributes][@"a-bool-key"], @(true)); - XCTAssertEqualObjects([scope attributes][@"a-bool-key-false"], @(false)); -} - -- (void)testSetDoubleAttribute -{ - SentryScope *scope = [[SentryScope alloc] init]; - [scope setAttributeValue:@(1.4728) forKey:@"a-double-key"]; - XCTAssertEqualObjects([scope attributes][@"a-double-key"], @(1.4728)); -} - -- (void)testSetIntegerAttribute -{ - SentryScope *scope = [[SentryScope alloc] init]; - [scope setAttributeValue:@(4) forKey:@"an-integer-key"]; - XCTAssertEqualObjects([scope attributes][@"an-integer-key"], @(4)); -} - -- (void)testRemoveAttribute -{ - SentryScope *scope = [[SentryScope alloc] init]; - [scope setAttributeValue:@"test-string" forKey:@"a-key"]; - XCTAssertEqualObjects([scope attributes][@"a-key"], @"test-string"); - - [scope removeAttributeForKey:@"a-key"]; - XCTAssertEqualObjects([scope attributes][@"a-key"], nil); -} - @end From c2b162c136a8ec27d1c49f7e44b2d63f5fca95c9 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Mon, 1 Dec 2025 13:56:47 -0300 Subject: [PATCH 7/7] Fix `SentryScope.m` --- Sources/Sentry/SentryScope.m | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index e1bc15dbdb3..1afaa67e7c8 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -29,6 +29,8 @@ @interface SentryScope () @property (atomic, strong) NSMutableArray *breadcrumbArray; +@property (atomic, strong) NSMutableDictionary *attributesDictionary; + @end @implementation SentryScope { @@ -50,6 +52,7 @@ - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs self.contextDictionary = [NSMutableDictionary new]; self.attachmentArray = [NSMutableArray new]; self.fingerprintArray = [NSMutableArray new]; + self.attributesDictionary = [NSMutableDictionary new]; _spanLock = [[NSObject alloc] init]; self.observers = [NSMutableArray new]; self.propagationContext = [[SentryPropagationContext alloc] init]; @@ -74,6 +77,7 @@ - (instancetype)initWithScope:(SentryScope *)scope [_breadcrumbArray addObjectsFromArray:crumbs]; [_fingerprintArray addObjectsFromArray:[scope fingerprints]]; [_attachmentArray addObjectsFromArray:[scope attachments]]; + [_attributesDictionary addEntriesFromDictionary:[scope attributes]]; self.propagationContext = scope.propagationContext; self.maxBreadcrumbs = scope.maxBreadcrumbs; @@ -191,6 +195,9 @@ - (void)clear @synchronized(_spanLock) { _span = nil; } + @synchronized(_attributesDictionary) { + [_attributesDictionary removeAllObjects]; + } self.userObject = nil; self.distString = nil; @@ -467,6 +474,40 @@ - (void)clearAttachments } } +- (NSDictionary *)attributes +{ + @synchronized(_attributesDictionary) { + return _attributesDictionary.copy; + } +} + +- (void)setAttributeValue:(id)value forKey:(NSString *)key +{ + if (key == nil || key.length == 0) { + SENTRY_LOG_ERROR(@"Attribute's key cannot be nil nor empty"); + return; + } + + @synchronized(_attributesDictionary) { + _attributesDictionary[key] = value; + + for (id observer in self.observers) { + [observer setAttributes:_attributesDictionary]; + } + } +} + +- (void)removeAttributeForKey:(NSString *)key +{ + @synchronized(_attributesDictionary) { + [_attributesDictionary removeObjectForKey:key]; + + for (id observer in self.observers) { + [observer setAttributes:_attributesDictionary]; + } + } +} + - (NSDictionary *)serialize { NSMutableDictionary *serializedData = [NSMutableDictionary new]; @@ -509,6 +550,9 @@ - (void)clearAttachments if (crumbs.count > 0) { [serializedData setValue:crumbs forKey:@"breadcrumbs"]; } + if (self.attributes.count > 0) { + [serializedData setValue:[self attributes] forKey:@"attributes"]; + } return serializedData; }