@@ -1312,6 +1312,186 @@ class SentryClientTests: XCTestCase {
13121312 XCTAssertEqual ( [ ] , actual. threads)
13131313 }
13141314
1315+ func testBeforeSendTransaction_ReadTags( ) throws {
1316+ // Arrange
1317+ let transaction = fixture. transaction
1318+ transaction. trace. setTag ( value: " tracer-value " , key: " tracer-key " )
1319+ let scope = Scope ( )
1320+ scope. setTag ( value: " event-value " , key: " event-key " )
1321+
1322+ // Act
1323+ fixture. getSut ( configureOptions: { options in
1324+ options. beforeSend = { event in
1325+ guard let transaction = event as? Transaction else {
1326+ return event
1327+ }
1328+ // Read tags - should return merged tags from tracer and event
1329+ guard let tags = transaction. tags else {
1330+ XCTFail ( " Tags should not be nil " )
1331+ return transaction
1332+ }
1333+ XCTAssertEqual ( tags [ " tracer-key " ] , " tracer-value " )
1334+ XCTAssertEqual ( tags [ " event-key " ] , " event-value " )
1335+ return transaction
1336+ }
1337+ } ) . capture ( event: transaction, scope: scope)
1338+
1339+ // Assert
1340+ let actual = try lastSentEvent ( )
1341+ let serialized = actual. serialize ( )
1342+ let tags = try XCTUnwrap ( serialized [ " tags " ] as? [ String : String ] )
1343+ XCTAssertEqual ( tags [ " tracer-key " ] , " tracer-value " )
1344+ XCTAssertEqual ( tags [ " event-key " ] , " event-value " )
1345+ }
1346+
1347+ func testBeforeSendTransaction_AddTags( ) throws {
1348+ // Arrange
1349+ let transaction = fixture. transaction
1350+ transaction. trace. setTag ( value: " existing-value " , key: " existing-key " )
1351+
1352+ // Act
1353+ fixture. getSut ( configureOptions: { options in
1354+ options. beforeSend = { event in
1355+ guard let transaction = event as? Transaction else {
1356+ return event
1357+ }
1358+ // Add new tag
1359+ // Note: `transaction.tags?["key"] = "value"` is syntactic sugar that:
1360+ // 1. Gets the dictionary (calls getter)
1361+ // 2. Creates a mutable copy
1362+ // 3. Modifies the copy
1363+ // 4. Calls setter with the modified copy: `transaction.tags = modifiedDict`
1364+ // This is why modifications persist - the setter is called!
1365+ transaction. tags ? [ " new-key " ] = " new-value "
1366+ return transaction
1367+ }
1368+ } ) . capture ( event: transaction)
1369+
1370+ // Assert
1371+ let actual = try lastSentEvent ( )
1372+ let serialized = actual. serialize ( )
1373+ let tags = try XCTUnwrap ( serialized [ " tags " ] as? [ String : String ] )
1374+ XCTAssertEqual ( tags [ " existing-key " ] , " existing-value " )
1375+ XCTAssertEqual ( tags [ " new-key " ] , " new-value " )
1376+ }
1377+
1378+ func testBeforeSendTransaction_ModifyTags( ) throws {
1379+ // Arrange
1380+ let transaction = fixture. transaction
1381+ transaction. trace. setTag ( value: " original-value " , key: " tracer-key " )
1382+ let scope = Scope ( )
1383+ scope. setTag ( value: " original-event-value " , key: " event-key " )
1384+
1385+ // Act
1386+ fixture. getSut ( configureOptions: { options in
1387+ options. beforeSend = { event in
1388+ guard let transaction = event as? Transaction else {
1389+ return event
1390+ }
1391+ // Modify both tracer tag and event tag
1392+ transaction. tags ? [ " tracer-key " ] = " modified-tracer-value "
1393+ transaction. tags ? [ " event-key " ] = " modified-event-value "
1394+ return transaction
1395+ }
1396+ } ) . capture ( event: transaction, scope: scope)
1397+
1398+ // Assert
1399+ let actual = try lastSentEvent ( )
1400+ let serialized = actual. serialize ( )
1401+ let tags = try XCTUnwrap ( serialized [ " tags " ] as? [ String : String ] )
1402+ XCTAssertEqual ( tags [ " tracer-key " ] , " modified-tracer-value " )
1403+ XCTAssertEqual ( tags [ " event-key " ] , " modified-event-value " )
1404+ }
1405+
1406+ func testBeforeSendTransaction_RemoveTags( ) throws {
1407+ // Arrange
1408+ let transaction = fixture. transaction
1409+ transaction. trace. setTag ( value: " tracer-value " , key: " tracer-key " )
1410+ let scope = Scope ( )
1411+ scope. setTag ( value: " event-value " , key: " event-key " )
1412+
1413+ // Act
1414+ fixture. getSut ( configureOptions: { options in
1415+ options. beforeSend = { event in
1416+ guard let transaction = event as? Transaction else {
1417+ return event
1418+ }
1419+ // Remove both tracer tag and event tag
1420+ transaction. tags ? [ " tracer-key " ] = nil
1421+ transaction. tags ? [ " event-key " ] = nil
1422+ return transaction
1423+ }
1424+ } ) . capture ( event: transaction, scope: scope)
1425+
1426+ // Assert
1427+ let actual = try lastSentEvent ( )
1428+ let serialized = actual. serialize ( )
1429+ let tags = try XCTUnwrap ( serialized [ " tags " ] as? [ String : String ] )
1430+ XCTAssertNil ( tags [ " tracer-key " ] )
1431+ XCTAssertNil ( tags [ " event-key " ] )
1432+ }
1433+
1434+ func testBeforeSendTransaction_ReplaceTags( ) throws {
1435+ // Arrange
1436+ let transaction = fixture. transaction
1437+ transaction. trace. setTag ( value: " tracer-value " , key: " tracer-key " )
1438+ let scope = Scope ( )
1439+ scope. setTag ( value: " event-value " , key: " event-key " )
1440+
1441+ // Act
1442+ fixture. getSut ( configureOptions: { options in
1443+ options. beforeSend = { event in
1444+ guard let transaction = event as? Transaction else {
1445+ return event
1446+ }
1447+ // Replace all tags
1448+ transaction. tags = [ " replaced-key " : " replaced-value " ]
1449+ return transaction
1450+ }
1451+ } ) . capture ( event: transaction, scope: scope)
1452+
1453+ // Assert
1454+ let actual = try lastSentEvent ( )
1455+ let serialized = actual. serialize ( )
1456+ let tags = try XCTUnwrap ( serialized [ " tags " ] as? [ String : String ] )
1457+ XCTAssertEqual ( tags. count, 1 )
1458+ XCTAssertEqual ( tags [ " replaced-key " ] , " replaced-value " )
1459+ XCTAssertNil ( tags [ " tracer-key " ] )
1460+ XCTAssertNil ( tags [ " event-key " ] )
1461+ }
1462+
1463+ func testBeforeSendTransaction_DictionarySubscriptAssignmentCallsSetter( ) throws {
1464+ // Arrange
1465+ // This test verifies that `transaction.tags?["key"] = "value"` actually calls the setter
1466+ // In Swift, dictionary subscript assignment with optional chaining expands to:
1467+ // if var tags = transaction.tags {
1468+ // tags["key"] = "value"
1469+ // transaction.tags = tags // <-- setter is called here!
1470+ // }
1471+ let transaction = fixture. transaction
1472+ transaction. trace. setTag ( value: " original " , key: " key " )
1473+
1474+ // Act - modify via subscript assignment
1475+ fixture. getSut ( configureOptions: { options in
1476+ options. beforeSend = { event in
1477+ guard let transaction = event as? Transaction else {
1478+ return event
1479+ }
1480+ // This subscript assignment calls the setter under the hood
1481+ transaction. tags ? [ " key " ] = " modified "
1482+ transaction. tags ? [ " new-key " ] = " new-value "
1483+ return transaction
1484+ }
1485+ } ) . capture ( event: transaction)
1486+
1487+ // Assert - verify the setter was called and changes persisted
1488+ let actual = try lastSentEvent ( )
1489+ let serialized = actual. serialize ( )
1490+ let tags = try XCTUnwrap ( serialized [ " tags " ] as? [ String : String ] )
1491+ XCTAssertEqual ( tags [ " key " ] , " modified " , " Subscript assignment should call setter and persist changes " )
1492+ XCTAssertEqual ( tags [ " new-key " ] , " new-value " , " New tags added via subscript should persist " )
1493+ }
1494+
13151495 func testBeforeSendSpanDitchOneSpan_OtherChangedSpanSent( ) throws {
13161496 let spanOne = getSpan ( operation: " operation.one " , tracer: fixture. trace)
13171497 let spanTwo = getSpan ( operation: " operation.two " , tracer: fixture. trace)
0 commit comments