22@_spi ( Private) import SentryTestUtils
33import XCTest
44
5+ // swiftlint:disable cyclomatic_complexity
6+
57final class SentryLoggerTests : XCTestCase {
68
79 private class Fixture {
@@ -324,6 +326,83 @@ final class SentryLoggerTests: XCTestCase {
324326 XCTAssertEqual ( capturedLog. traceId, expectedTraceId)
325327 }
326328
329+ func testCaptureLog_AddsOSAndDeviceAttributes( ) {
330+ // Set up OS context
331+ let osContext = [
332+ " name " : " iOS " ,
333+ " version " : " 16.0.1 "
334+ ]
335+
336+ // Set up device context
337+ let deviceContext = [
338+ " family " : " iOS " ,
339+ " model " : " iPhone14,4 "
340+ ]
341+
342+ // Set up scope context
343+ fixture. hub. scope. setContext ( value: osContext, key: " os " )
344+ fixture. hub. scope. setContext ( value: deviceContext, key: " device " )
345+
346+ sut. info ( " Test log message " )
347+
348+ let capturedLog = getLastCapturedLog ( )
349+
350+ // Verify OS attributes
351+ XCTAssertEqual ( capturedLog. attributes [ " os.name " ] ? . value as? String , " iOS " )
352+ XCTAssertEqual ( capturedLog. attributes [ " os.version " ] ? . value as? String , " 16.0.1 " )
353+
354+ // Verify device attributes
355+ XCTAssertEqual ( capturedLog. attributes [ " device.brand " ] ? . value as? String , " Apple " )
356+ XCTAssertEqual ( capturedLog. attributes [ " device.model " ] ? . value as? String , " iPhone14,4 " )
357+ XCTAssertEqual ( capturedLog. attributes [ " device.family " ] ? . value as? String , " iOS " )
358+ }
359+
360+ func testCaptureLog_HandlesPartialOSAndDeviceAttributes( ) {
361+ // Set up partial OS context (missing version)
362+ let osContext = [
363+ " name " : " macOS "
364+ ]
365+
366+ // Set up partial device context (missing model)
367+ let deviceContext = [
368+ " family " : " macOS "
369+ ]
370+
371+ // Set up scope context
372+ fixture. hub. scope. setContext ( value: osContext, key: " os " )
373+ fixture. hub. scope. setContext ( value: deviceContext, key: " device " )
374+
375+ sut. info ( " Test log message " )
376+
377+ let capturedLog = getLastCapturedLog ( )
378+
379+ // Verify only available OS attributes are added
380+ XCTAssertEqual ( capturedLog. attributes [ " os.name " ] ? . value as? String , " macOS " )
381+ XCTAssertNil ( capturedLog. attributes [ " os.version " ] )
382+
383+ // Verify only available device attributes are added
384+ XCTAssertEqual ( capturedLog. attributes [ " device.brand " ] ? . value as? String , " Apple " )
385+ XCTAssertNil ( capturedLog. attributes [ " device.model " ] )
386+ XCTAssertEqual ( capturedLog. attributes [ " device.family " ] ? . value as? String , " macOS " )
387+ }
388+
389+ func testCaptureLog_HandlesMissingOSAndDeviceContext( ) {
390+ // Clear any OS and device context that might be automatically populated
391+ fixture. hub. scope. removeContext ( key: " os " )
392+ fixture. hub. scope. removeContext ( key: " device " )
393+
394+ sut. info ( " Test log message " )
395+
396+ let capturedLog = getLastCapturedLog ( )
397+
398+ // Verify no OS or device attributes are added when context is missing
399+ XCTAssertNil ( capturedLog. attributes [ " os.name " ] )
400+ XCTAssertNil ( capturedLog. attributes [ " os.version " ] )
401+ XCTAssertNil ( capturedLog. attributes [ " device.brand " ] )
402+ XCTAssertNil ( capturedLog. attributes [ " device.model " ] )
403+ XCTAssertNil ( capturedLog. attributes [ " device.family " ] )
404+ }
405+
327406 // MARK: - Helper Methods
328407
329408 private func assertLogCaptured(
@@ -345,9 +424,31 @@ final class SentryLoggerTests: XCTestCase {
345424 XCTAssertEqual ( capturedLog. body, expectedBody, " Log body mismatch " , file: file, line: line)
346425 XCTAssertEqual ( capturedLog. timestamp, fixture. dateProvider. date ( ) , " Log timestamp mismatch " , file: file, line: line)
347426
348- let numberOfDefaultAttributes = 4
427+ // Count expected default attributes dynamically
428+ var expectedDefaultAttributeCount = 3 // sdk.name, sdk.version, environment are always present
429+ if fixture. options. releaseName != nil {
430+ expectedDefaultAttributeCount += 1 // sentry.release
431+ }
432+ if fixture. hub. scope. span != nil {
433+ expectedDefaultAttributeCount += 1 // sentry.trace.parent_span_id
434+ }
435+ // OS and device attributes (up to 5 more if context is available)
436+ if let contextDictionary = fixture. hub. scope. serialize ( ) [ " context " ] as? [ String : [ String : Any ] ] {
437+ if let osContext = contextDictionary [ " os " ] {
438+ if osContext [ " name " ] != nil { expectedDefaultAttributeCount += 1 }
439+ if osContext [ " version " ] != nil { expectedDefaultAttributeCount += 1 }
440+ }
441+ if contextDictionary [ " device " ] != nil {
442+ expectedDefaultAttributeCount += 1 // device.brand (always "Apple")
443+ if let deviceContext = contextDictionary [ " device " ] {
444+ if deviceContext [ " model " ] != nil { expectedDefaultAttributeCount += 1 }
445+ if deviceContext [ " family " ] != nil { expectedDefaultAttributeCount += 1 }
446+ }
447+ }
448+ }
449+
349450 // Compare attributes
350- XCTAssertEqual ( capturedLog. attributes. count, expectedAttributes. count + numberOfDefaultAttributes , " Attribute count mismatch " , file: file, line: line)
451+ XCTAssertEqual ( capturedLog. attributes. count, expectedAttributes. count + expectedDefaultAttributeCount , " Attribute count mismatch " , file: file, line: line)
351452
352453 for (key, expectedAttribute) in expectedAttributes {
353454 guard let actualAttribute = capturedLog. attributes [ key] else {
0 commit comments