Skip to content

Commit 97de6c8

Browse files
authored
feat: Link errors with session replay (#3851)
Added replay id as error event tag and to baggage and trace headers
1 parent d9308fd commit 97de6c8

16 files changed

+121
-36
lines changed

Samples/iOS-Swift/iOS-Swift/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2525
options.debug = true
2626

2727
if #available(iOS 16.0, *) {
28-
options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true)
28+
options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1, redactAllText: true, redactAllImages: true)
2929
}
3030

3131
if #available(iOS 15.0, *) {

Sentry.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4325,6 +4325,7 @@
43254325
7BCFBD6F2681D0EE00BC27D8 /* SentryCrashScopeObserver.m in Sources */,
43264326
7BD86ED1264A7CF6005439DB /* SentryAppStartMeasurement.m in Sources */,
43274327
7DC27EC723997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.m in Sources */,
4328+
D820CE142BB2F13C00BA339D /* SentryCoreGraphicsHelper.m in Sources */,
43284329
63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */,
43294330
7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */,
43304331
7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */,

Sources/Sentry/Public/SentryScope.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ NS_SWIFT_NAME(Scope)
2121
*/
2222
@property (nullable, nonatomic, strong) id<SentrySpan> span;
2323

24+
/**
25+
* The id of current session replay.
26+
*/
27+
@property (nullable, nonatomic, strong) NSString *replayId;
28+
2429
/**
2530
* Gets the dictionary of currently set tags.
2631
*/

Sources/Sentry/SentryBaggage.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId
1919
userSegment:(nullable NSString *)userSegment
2020
sampleRate:(nullable NSString *)sampleRate
2121
sampled:(nullable NSString *)sampled
22+
replayId:(nullable NSString *)replayId
2223
{
2324

2425
if (self = [super init]) {
@@ -30,6 +31,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId
3031
_userSegment = userSegment;
3132
_sampleRate = sampleRate;
3233
_sampled = sampled;
34+
_replayId = replayId;
3335
}
3436

3537
return self;
@@ -67,6 +69,10 @@ - (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalB
6769
[information setValue:_sampled forKey:@"sentry-sampled"];
6870
}
6971

72+
if (_replayId != nil) {
73+
[information setValue:_replayId forKey:@"sentry-replay_id"];
74+
}
75+
7076
return [SentrySerialization baggageEncodedDictionary:information];
7177
}
7278

Sources/Sentry/SentryScope.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ - (instancetype)initWithScope:(SentryScope *)scope
103103
self.environmentString = scope.environmentString;
104104
self.levelEnum = scope.levelEnum;
105105
self.span = scope.span;
106+
self.replayId = scope.replayId;
106107
}
107108
return self;
108109
}
@@ -428,6 +429,7 @@ - (void)clearAttachments
428429
[serializedData setValue:[self.userObject serialize] forKey:@"user"];
429430
[serializedData setValue:self.distString forKey:@"dist"];
430431
[serializedData setValue:self.environmentString forKey:@"environment"];
432+
[serializedData setValue:self.replayId forKey:@"replay_id"];
431433
if (self.fingerprints.count > 0) {
432434
[serializedData setValue:[self fingerprints] forKey:@"fingerprint"];
433435
}

Sources/Sentry/SentrySessionReplay.m

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
#import "SentryAttachment+Private.h"
33
#import "SentryDependencyContainer.h"
44
#import "SentryDisplayLinkWrapper.h"
5+
#import "SentryEnvelopeItemType.h"
56
#import "SentryFileManager.h"
67
#import "SentryHub+Private.h"
78
#import "SentryLog.h"
89
#import "SentryRandom.h"
910
#import "SentryReplayEvent.h"
1011
#import "SentryReplayRecording.h"
1112
#import "SentrySDK+Private.h"
13+
#import "SentryScope+Private.h"
1214
#import "SentrySwift.h"
15+
#import "SentryTraceContext.h"
1316

1417
#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
1518

@@ -31,7 +34,6 @@ @implementation SentrySessionReplay {
3134
NSDate *_videoSegmentStart;
3235
NSDate *_sessionStart;
3336
NSMutableArray<UIImage *> *imageCollection;
34-
SentryId *sessionReplayId;
3537
SentryReplayOptions *_replayOptions;
3638
SentryOnDemandReplay *_replayMaker;
3739
SentryDisplayLinkWrapper *_displayLink;
@@ -88,7 +90,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full
8890
_lastScreenShot = _dateProvider.date;
8991
_videoSegmentStart = nil;
9092
_currentSegmentId = 0;
91-
sessionReplayId = [[SentryId alloc] init];
93+
_sessionReplayId = [[SentryId alloc] init];
9294

9395
imageCollection = [NSMutableArray array];
9496
if (full) {
@@ -100,6 +102,8 @@ - (void)startFullReplay
100102
{
101103
_sessionStart = _lastScreenShot;
102104
_isFullSession = YES;
105+
[SentrySDK.currentHub configureScope:^(
106+
SentryScope *_Nonnull scope) { scope.replayId = [self->_sessionReplayId sentryIdString]; }];
103107
}
104108

105109
- (void)stop
@@ -112,7 +116,12 @@ - (void)stop
112116

113117
- (void)captureReplayForEvent:(SentryEvent *)event;
114118
{
115-
if (_isFullSession || !_isRunning) {
119+
if (!_isRunning) {
120+
return;
121+
}
122+
123+
if (_isFullSession) {
124+
[self setEventContext:event];
116125
return;
117126
}
118127

@@ -124,15 +133,33 @@ - (void)captureReplayForEvent:(SentryEvent *)event;
124133
return;
125134
}
126135

136+
[self startFullReplay];
137+
[self setEventContext:event];
138+
127139
NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"];
128140
NSDate *replayStart =
129141
[_dateProvider.date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration];
130142

131143
[self createAndCapture:finalPath
132144
duration:_replayOptions.errorReplayDuration
133145
startedAt:replayStart];
146+
}
134147

135-
[self startFullReplay];
148+
- (void)setEventContext:(SentryEvent *)event
149+
{
150+
if ([event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo]) {
151+
return;
152+
}
153+
154+
NSMutableDictionary *context = event.context.mutableCopy ?: [[NSMutableDictionary alloc] init];
155+
context[@"replay"] = @{ @"replay_id" : [_sessionReplayId sentryIdString] };
156+
event.context = context;
157+
158+
NSMutableDictionary *tags = @{ @"replayId" : [_sessionReplayId sentryIdString] }.mutableCopy;
159+
if (event.tags != nil) {
160+
[tags addEntriesFromDictionary:event.tags];
161+
}
162+
event.tags = tags;
136163
}
137164

138165
- (void)newFrame:(CADisplayLink *)sender
@@ -207,7 +234,7 @@ - (void)createAndCapture:(NSURL *)videoUrl
207234
} else {
208235
[self captureSegment:self->_currentSegmentId++
209236
video:videoInfo
210-
replayId:self->sessionReplayId
237+
replayId:self->_sessionReplayId
211238
replayType:kSentryReplayTypeSession];
212239

213240
[self->_replayMaker releaseFramesUntil:videoInfo.end];

Sources/Sentry/SentryTraceContext.m

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId
2424
userSegment:(nullable NSString *)userSegment
2525
sampleRate:(nullable NSString *)sampleRate
2626
sampled:(nullable NSString *)sampled
27+
replayId:(nullable NSString *)replayId
2728
{
2829
if (self = [super init]) {
2930
_traceId = traceId;
@@ -34,6 +35,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId
3435
_userSegment = userSegment;
3536
_sampleRate = sampleRate;
3637
_sampled = sampled;
38+
_replayId = replayId;
3739
}
3840
return self;
3941
}
@@ -80,7 +82,8 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer
8082
transaction:tracer.transactionContext.name
8183
userSegment:userSegment
8284
sampleRate:sampleRate
83-
sampled:sampled];
85+
sampled:sampled
86+
replayId:scope.replayId];
8487
}
8588

8689
- (instancetype)initWithTraceId:(SentryId *)traceId
@@ -94,7 +97,8 @@ - (instancetype)initWithTraceId:(SentryId *)traceId
9497
transaction:nil
9598
userSegment:userSegment
9699
sampleRate:nil
97-
sampled:nil];
100+
sampled:nil
101+
replayId:nil];
98102
}
99103

100104
- (nullable instancetype)initWithDict:(NSDictionary<NSString *, id> *)dictionary
@@ -120,7 +124,8 @@ - (nullable instancetype)initWithDict:(NSDictionary<NSString *, id> *)dictionary
120124
transaction:dictionary[@"transaction"]
121125
userSegment:userSegment
122126
sampleRate:dictionary[@"sample_rate"]
123-
sampled:dictionary[@"sampled"]];
127+
sampled:dictionary[@"sampled"]
128+
replayId:dictionary[@"replay_id"]];
124129
}
125130

126131
- (SentryBaggage *)toBaggage
@@ -132,7 +137,8 @@ - (SentryBaggage *)toBaggage
132137
transaction:_transaction
133138
userSegment:_userSegment
134139
sampleRate:_sampleRate
135-
sampled:_sampled];
140+
sampled:_sampled
141+
replayId:_replayId];
136142
return result;
137143
}
138144

@@ -165,6 +171,10 @@ - (SentryBaggage *)toBaggage
165171
[result setValue:_sampleRate forKey:@"sampled"];
166172
}
167173

174+
if (_replayId != nil) {
175+
[result setValue:_replayId forKey:@"replay_id"];
176+
}
177+
168178
return result;
169179
}
170180

Sources/Sentry/include/SentryBaggage.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ static NSString *const SENTRY_BAGGAGE_HEADER = @"baggage";
5454
*/
5555
@property (nullable, nonatomic, strong) NSString *sampled;
5656

57+
@property (nullable, nonatomic, strong) NSString *replayId;
58+
5759
- (instancetype)initWithTraceId:(SentryId *)traceId
5860
publicKey:(NSString *)publicKey
5961
releaseName:(nullable NSString *)releaseName
6062
environment:(nullable NSString *)environment
6163
transaction:(nullable NSString *)transaction
6264
userSegment:(nullable NSString *)userSegment
6365
sampleRate:(nullable NSString *)sampleRate
64-
sampled:(nullable NSString *)sampled;
66+
sampled:(nullable NSString *)sampled
67+
replayId:(nullable NSString *)replayId;
6568

6669
- (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalBaggage;
6770

Sources/Sentry/include/SentrySessionReplay.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@class SentryCurrentDateProvider;
1010
@class SentryDisplayLinkWrapper;
1111
@class SentryVideoInfo;
12+
@class SentryId;
1213

1314
@protocol SentryRandom;
1415
@protocol SentryRedactOptions;
@@ -35,6 +36,8 @@ NS_ASSUME_NONNULL_BEGIN
3536
API_AVAILABLE(ios(16.0), tvos(16.0))
3637
@interface SentrySessionReplay : NSObject
3738

39+
@property (nonatomic, strong, readonly) SentryId *sessionReplayId;
40+
3841
- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions
3942
replayFolderPath:(NSURL *)folderPath
4043
screenshotProvider:(id<SentryViewScreenshotProvider>)photographer

Sources/Sentry/include/SentryTraceContext.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ NS_ASSUME_NONNULL_BEGIN
5252
*/
5353
@property (nullable, nonatomic, readonly) NSString *sampled;
5454

55+
/**
56+
* Id of the current session replay.
57+
*/
58+
@property (nullable, nonatomic, readonly) NSString *replayId;
59+
5560
/**
5661
* Initializes a SentryTraceContext with given properties.
5762
*/
@@ -62,7 +67,8 @@ NS_ASSUME_NONNULL_BEGIN
6267
transaction:(nullable NSString *)transaction
6368
userSegment:(nullable NSString *)userSegment
6469
sampleRate:(nullable NSString *)sampleRate
65-
sampled:(nullable NSString *)sampled;
70+
sampled:(nullable NSString *)sampled
71+
replayId:(nullable NSString *)replayId;
6672

6773
/**
6874
* Initializes a SentryTraceContext with data from scope and options.

Tests/SentryTests/Helper/SentrySerializationTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class SentrySerializationTests: XCTestCase {
55

66
private class Fixture {
77
static var invalidData = "hi".data(using: .utf8)!
8-
static var traceContext = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: "some segment", sampleRate: "0.25", sampled: "true")
8+
static var traceContext = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: "some segment", sampleRate: "0.25", sampled: "true", replayId: nil)
99
}
1010

1111
func testSerializationFailsWithInvalidJSONObject() {
@@ -124,7 +124,7 @@ class SentrySerializationTests: XCTestCase {
124124
}
125125

126126
func testSentryEnvelopeSerializer_TraceStateWithoutUser() {
127-
let trace = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil)
127+
let trace = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil, replayId: nil)
128128

129129
let envelopeHeader = SentryEnvelopeHeader(id: nil, traceContext: trace)
130130
let envelope = SentryEnvelope(header: envelopeHeader, singleItem: createItemWithEmptyAttachment())

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class SentrySessionReplayTests: XCTestCase {
6363
let screenshotProvider = ScreenshotProvider()
6464
let displayLink = TestDisplayLinkWrapper()
6565
let rootView = UIView()
66-
let hub = ReplayHub(client: nil, andScope: nil)
66+
let hub = ReplayHub(client: SentryClient(options: Options()), andScope: nil)
6767
let replayMaker = TestReplayMaker()
6868
let cacheFolder = FileManager.default.temporaryDirectory
6969

@@ -117,8 +117,10 @@ class SentrySessionReplayTests: XCTestCase {
117117
@available(iOS 16.0, tvOS 16, *)
118118
func testSentReplay_FullSession() {
119119
let fixture = startFixture()
120+
120121
let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1))
121122
sut.start(fixture.rootView, fullSession: true)
123+
expect(fixture.hub.scope.replayId) == sut.sessionReplayId.sentryIdString
122124

123125
fixture.dateProvider.advance(by: 1)
124126

@@ -148,6 +150,8 @@ class SentrySessionReplayTests: XCTestCase {
148150
let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1))
149151
sut.start(fixture.rootView, fullSession: false)
150152

153+
expect(fixture.hub.scope.replayId) == nil
154+
151155
fixture.dateProvider.advance(by: 1)
152156

153157
Dynamic(sut).newFrame(nil)
@@ -165,10 +169,12 @@ class SentrySessionReplayTests: XCTestCase {
165169
let fixture = startFixture()
166170
let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1))
167171
sut.start(fixture.rootView, fullSession: false)
168-
172+
expect(fixture.hub.scope.replayId) == nil
169173
let event = Event(error: NSError(domain: "Some error", code: 1))
170174

171175
sut.capture(for: event)
176+
expect(fixture.hub.scope.replayId) == sut.sessionReplayId.sentryIdString
177+
expect(event.context?["replay"]?["replay_id"] as? String) == sut.sessionReplayId.sentryIdString
172178
assertFullSession(sut, expected: true)
173179
}
174180

Tests/SentryTests/Protocol/SentryEnvelopeTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class SentryEnvelopeTests: XCTestCase {
157157

158158
func testInitSentryEnvelopeHeader_SetIdAndTraceState() {
159159
let eventId = SentryId()
160-
let traceContext = SentryTraceContext(trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil)
160+
let traceContext = SentryTraceContext(trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil, replayId: nil)
161161

162162
let envelopeHeader = SentryEnvelopeHeader(id: eventId, traceContext: traceContext)
163163
XCTAssertEqual(eventId, envelopeHeader.eventId)

Tests/SentryTests/SentryScopeTests.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ - (void)testEnvironmentSerializes
154154
XCTAssertEqualObjects([[scope serialize] objectForKey:@"environment"], expectedEnvironment);
155155
}
156156

157+
- (void)testReplaySerializes
158+
{
159+
SentryScope *scope = [[SentryScope alloc] init];
160+
NSString *expectedReplayId = @"Some_replay_id";
161+
[scope setReplayId:expectedReplayId];
162+
XCTAssertEqualObjects([[scope serialize] objectForKey:@"replay_id"], expectedReplayId);
163+
}
164+
157165
- (void)testClearBreadcrumb
158166
{
159167
SentryScope *scope = [[SentryScope alloc] init];

0 commit comments

Comments
 (0)