Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

- Add SentryDistribution as Swift Package Manager target (#6149)
- Add option `enablePropagateTraceparent` to support OTel/W3C trace propagation (#6356)
- Add `sentry.replay_id` attribute to logs ([#6515](https://github.com/getsentry/sentry-cocoa/pull/6515))

### Fixes

Expand Down
7 changes: 7 additions & 0 deletions SentryTestUtils/TestHub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,11 @@ public class TestHub: SentryHub {
capturedReplayRecordingVideo.record((replayEvent, replayRecording, videoURL))
onReplayCapture?()
}

#if os(iOS) || os(tvOS)
public var mockReplayId: String?
public override func getSessionReplayId() -> String? {
return mockReplayId
}
#endif
}
13 changes: 13 additions & 0 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import "SentrySamplingContext.h"
#import "SentryScope+Private.h"
#import "SentrySerialization.h"
#import "SentrySessionReplayIntegration+Private.h"
#import "SentrySwift.h"
#import "SentryTraceOrigin.h"
#import "SentryTracer.h"
Expand Down Expand Up @@ -843,6 +844,18 @@ - (void)unregisterSessionListener:(id<SentrySessionListener>)listener
return integrations;
}

#if SENTRY_TARGET_REPLAY_SUPPORTED
- (NSString *__nullable)getSessionReplayId
{
SentrySessionReplayIntegration *integration =
[self getInstalledIntegration:[SentrySessionReplayIntegration class]];
if (integration == nil || integration.sessionReplay == nil) {
return nil;
}
return integration.sessionReplay.sessionReplayId.sentryIdString;
}
#endif

@end

NS_ASSUME_NONNULL_END
4 changes: 4 additions & 0 deletions Sources/Sentry/include/SentryHub+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)unregisterSessionListener:(id<SentrySessionListener>)listener;
- (nullable id<SentryIntegrationProtocol>)getInstalledIntegration:(Class)integrationClass;

#if SENTRY_TARGET_REPLAY_SUPPORTED
- (NSString *__nullable)getSessionReplayId;
#endif

@end

NS_ASSUME_NONNULL_END
14 changes: 14 additions & 0 deletions Sources/Swift/Tools/SentryLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
addOSAttributes(to: &logAttributes)
addDeviceAttributes(to: &logAttributes)
addUserAttributes(to: &logAttributes)
addReplayAttributes(to: &logAttributes)

let propagationContextTraceIdString = hub.scope.propagationContextTraceIdString
let propagationContextTraceId = SentryId(uuidString: propagationContextTraceIdString)
Expand Down Expand Up @@ -280,6 +281,19 @@
attributes["user.email"] = .init(string: userEmail)
}
}

private func addReplayAttributes(to attributes: inout [String: SentryLog.Attribute]) {
#if os(iOS) || os(tvOS)
if let scopeReplayId = hub.scope.replayId {
// Session mode: use scope replay ID
attributes["sentry.replay_id"] = .init(string: scopeReplayId)
} else if let sessionReplayId = hub.getSessionReplayId() {

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Check no UIKit linkage (DebugWithoutUIKit)

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Check no UIKit linkage (ReleaseWithoutUIKit)

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / maccatalyst

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / maccatalyst

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / iphoneos

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / iphoneos

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / appletvos

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / appletvos

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / iphonesimulator

value of type 'SentryHub' has no member 'getSessionReplayId'

Check failure on line 290 in Sources/Swift/Tools/SentryLogger.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / iphonesimulator

value of type 'SentryHub' has no member 'getSessionReplayId'
// Buffer mode: scope has no ID but integration does
attributes["sentry.replay_id"] = .init(string: sessionReplayId)
attributes["sentry._internal.replay_is_buffering"] = .init(boolean: true)
}
#endif
}
}

#if SWIFT_PACKAGE
Expand Down
61 changes: 61 additions & 0 deletions Tests/SentryTests/SentryLoggerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,67 @@ final class SentryLoggerTests: XCTestCase {
XCTAssertNil(nilCallback, "Dynamic access should allow setting to nil")
}

// MARK: - Replay Attributes Tests

#if os(iOS) || os(tvOS)
func testReplayAttributes_SessionMode_AddsReplayId() {
// Setup replay integration
let replayOptions = SentryReplayOptions(sessionSampleRate: 1.0, onErrorSampleRate: 0.0)
fixture.options.sessionReplay = replayOptions

let replayIntegration = SentrySessionReplayIntegration()
fixture.hub.addInstalledIntegration(replayIntegration, name: "SentrySessionReplayIntegration")

// Set replayId on scope (session mode)
let replayId = "12345678-1234-1234-1234-123456789012"
fixture.scope.replayId = replayId

sut.info("Test message")

let log = getLastCapturedLog()
XCTAssertEqual(log.attributes["sentry.replay_id"]?.value as? String, replayId)
XCTAssertNil(log.attributes["sentry._internal.replay_is_buffering"])
}

func testReplayAttributes_BufferMode_AddsReplayIdAndBufferingFlag() {
// Set up buffer mode: hub has an ID, but scope.replayId is nil
let mockReplayId = SentryId()
fixture.hub.mockReplayId = mockReplayId.sentryIdString
fixture.scope.replayId = nil

sut.info("Test message")

let log = getLastCapturedLog()
let replayIdString = log.attributes["sentry.replay_id"]?.value as? String
XCTAssertEqual(replayIdString, mockReplayId.sentryIdString)
XCTAssertEqual(log.attributes["sentry._internal.replay_is_buffering"]?.value as? Bool, true)
}

func testReplayAttributes_NoReplay_NoAttributesAdded() {
// Don't set up replay integration

sut.info("Test message")

let log = getLastCapturedLog()
XCTAssertNil(log.attributes["sentry.replay_id"])
XCTAssertNil(log.attributes["sentry._internal.replay_is_buffering"])
}

func testReplayAttributes_BothSessionAndScopeReplayId_SessionMode() {
// Session mode: scope has the ID, hub also has one
let replayId = "12345678-1234-1234-1234-123456789012"
fixture.hub.mockReplayId = replayId
fixture.scope.replayId = replayId

sut.info("Test message")

let log = getLastCapturedLog()
// Session mode should use scope's ID (takes precedence) and not add buffering flag
XCTAssertEqual(log.attributes["sentry.replay_id"]?.value as? String, replayId)
XCTAssertNil(log.attributes["sentry._internal.replay_is_buffering"])
}
#endif

// MARK: - Helper Methods

private func assertLogCaptured(
Expand Down
Loading