Skip to content

Commit 4b92944

Browse files
authored
Merge 0a08f99 into 83acf3e
2 parents 83acf3e + 0a08f99 commit 4b92944

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1111
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Features
1212

13+
- Add Session Replay (#3625)
1314
- Add support for Sentry [Spotlight](https://spotlightjs.com/) (#3642), which is basically Sentry
1415
for development. Read our [blog post](https://blog.sentry.io/sentry-for-development/) to find out more.
1516
- Add field `SentrySDK.detectedStartUpCrash` (#3644)

Sentry.xcodeproj/project.pbxproj

Lines changed: 70 additions & 0 deletions
Large diffs are not rendered by default.

Sources/Sentry/Public/Sentry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[];
3030
#import "SentryMessage.h"
3131
#import "SentryNSError.h"
3232
#import "SentryOptions.h"
33+
#import "SentryReplayOptions.h"
3334
#import "SentryRequest.h"
3435
#import "SentrySDK.h"
3536
#import "SentrySampleDecision.h"

Sources/Sentry/Public/SentryOptions.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
NS_ASSUME_NONNULL_BEGIN
55

6-
@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope;
6+
@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope,
7+
SentryReplayOptions;
78

89
NS_SWIFT_NAME(Options)
910
@interface SentryOptions : NSObject
@@ -269,6 +270,15 @@ NS_SWIFT_NAME(Options)
269270
* @note Default value is @c NO .
270271
*/
271272
@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing;
273+
274+
/**
275+
* @warning This is an experimental feature and may still have bugs.
276+
* Settings to configure the session replay.
277+
* @node Default value is @c nil .
278+
*/
279+
@property (nonatomic, strong)
280+
SentryReplayOptions *sessionReplayOptions API_AVAILABLE(ios(16.0), tvos(16.0));
281+
272282
#endif // SENTRY_UIKIT_AVAILABLE
273283

274284
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#import <Foundation/Foundation.h>
2+
3+
NS_ASSUME_NONNULL_BEGIN
4+
5+
@interface SentryReplayOptions : NSObject
6+
7+
/**
8+
* Indicates the percentage in which the replay for the session will be created.
9+
* @discussion Specifying @c 0 means never, @c 1.0 means always.
10+
* @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it
11+
* to the default.
12+
* @note The default is @c 0.
13+
*/
14+
@property (nonatomic) float replaysSessionSampleRate;
15+
16+
/**
17+
* Indicates the percentage in which a 30 seconds replay will be send with error events.
18+
* @discussion Specifying @c 0 means never, @c 1.0 means always.
19+
* @note The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it
20+
* to the default.
21+
* @note The default is @c 0.
22+
*/
23+
@property (nonatomic) float replaysOnErrorSampleRate;
24+
25+
/**
26+
* Inittialize the settings of session replay
27+
*
28+
* @param sessionSampleRate Indicates the percentage in which the replay for the session will be
29+
* created.
30+
* @param errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with
31+
* error events.
32+
*/
33+
- (instancetype)initWithReplaySessionSampleRate:(float)sessionSampleRate
34+
replaysOnErrorSampleRate:(float)errorSampleRate;
35+
36+
@end
37+
38+
NS_ASSUME_NONNULL_END

Sources/Sentry/SentryBaseIntegration.m

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#import "SentryBaseIntegration.h"
22
#import "SentryCrashWrapper.h"
33
#import "SentryLog.h"
4+
#import "SentryReplayOptions.h"
45
#import <Foundation/Foundation.h>
56
#import <SentryDependencyContainer.h>
67
#import <SentryOptions+Private.h>
@@ -140,6 +141,19 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options
140141
[self logWithOptionName:@"attachViewHierarchy"];
141142
return NO;
142143
}
144+
145+
if (integrationOptions & kIntegrationOptionEnableReplay) {
146+
if (@available(iOS 16.0, tvOS 16.0, *)) {
147+
if (options.sessionReplayOptions.replaysOnErrorSampleRate == 0
148+
&& options.sessionReplayOptions.replaysSessionSampleRate == 0) {
149+
[self logWithOptionName:@"sessionReplaySettings"];
150+
return NO;
151+
}
152+
} else {
153+
[self logWithReason:@"Session replay requires iOS 16 or above"];
154+
return NO;
155+
}
156+
}
143157
#endif
144158

145159
if ((integrationOptions & kIntegrationOptionEnableCrashHandler)

Sources/Sentry/SentryClient.m

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
#import "SentryDependencyContainer.h"
1414
#import "SentryDispatchQueueWrapper.h"
1515
#import "SentryDsn.h"
16-
#import "SentryEnvelope.h"
16+
#import "SentryEnvelope+Private.h"
1717
#import "SentryEnvelopeItemType.h"
1818
#import "SentryEvent.h"
1919
#import "SentryException.h"
@@ -30,12 +30,15 @@
3030
#import "SentryMechanismMeta.h"
3131
#import "SentryMessage.h"
3232
#import "SentryMeta.h"
33+
#import "SentryMsgPackSerializer.h"
3334
#import "SentryNSError.h"
3435
#import "SentryOptions+Private.h"
3536
#import "SentryPropagationContext.h"
3637
#import "SentryRandom.h"
38+
#import "SentryReplayEvent.h"
3739
#import "SentrySDK+Private.h"
3840
#import "SentryScope+Private.h"
41+
#import "SentrySerialization.h"
3942
#import "SentrySession.h"
4043
#import "SentryStacktraceBuilder.h"
4144
#import "SentrySwift.h"
@@ -472,13 +475,43 @@ - (void)captureSession:(SentrySession *)session
472475
}
473476

474477
SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithSession:session];
475-
SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:nil
476-
traceContext:nil];
477-
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader
478+
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty]
478479
singleItem:item];
479480
[self captureEnvelope:envelope];
480481
}
481482

483+
- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent
484+
replayRecording:(SentryReplayRecording *)replayRecording
485+
video:(NSURL *)videoURL
486+
withScope:(SentryScope *)scope
487+
{
488+
replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent
489+
withScope:scope
490+
alwaysAttachStacktrace:NO];
491+
492+
if (![replayEvent isKindOfClass:SentryReplayEvent.class]) {
493+
SENTRY_LOG_DEBUG(@"The event preprocessor didn't update the replay event in place. The "
494+
@"replay was discarded.");
495+
return;
496+
}
497+
498+
SentryEnvelopeItem *videoEnvelopeItem =
499+
[[SentryEnvelopeItem alloc] initWithReplayEvent:replayEvent
500+
replayRecording:replayRecording
501+
video:videoURL];
502+
503+
if (videoEnvelopeItem == nil) {
504+
SENTRY_LOG_DEBUG(@"The Session Replay segment will not be sent to Sentry because an "
505+
@"Envelope Item could not be created.");
506+
return;
507+
}
508+
509+
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty]
510+
items:@[ videoEnvelopeItem ]];
511+
512+
[self captureEnvelope:envelope];
513+
}
514+
482515
- (void)captureEnvelope:(SentryEnvelope *)envelope
483516
{
484517
if ([self isDisabled]) {
@@ -553,9 +586,11 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
553586

554587
BOOL eventIsNotATransaction
555588
= event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeTransaction];
589+
BOOL eventIsNotReplay
590+
= event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo];
556591

557592
// Transactions have their own sampleRate
558-
if (eventIsNotATransaction && [self isSampled:self.options.sampleRate]) {
593+
if (eventIsNotATransaction && eventIsNotReplay && [self isSampled:self.options.sampleRate]) {
559594
SENTRY_LOG_DEBUG(@"Event got sampled, will not send the event");
560595
[self recordLostEvent:kSentryDataCategoryError reason:kSentryDiscardReasonSampleRate];
561596
return nil;
@@ -583,7 +618,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
583618
[self setSdk:event];
584619

585620
// We don't want to attach debug meta and stacktraces for transactions;
586-
if (eventIsNotATransaction) {
621+
if (eventIsNotATransaction && eventIsNotReplay) {
587622
BOOL shouldAttachStacktrace = alwaysAttachStacktrace || self.options.attachStacktrace
588623
|| (nil != event.exceptions && [event.exceptions count] > 0);
589624

Sources/Sentry/SentryDataCategoryMapper.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
NSString *const kSentryDataCategoryNameAttachment = @"attachment";
1212
NSString *const kSentryDataCategoryNameUserFeedback = @"user_report";
1313
NSString *const kSentryDataCategoryNameProfile = @"profile";
14+
NSString *const kSentryDataCategoryNameReplay = @"replay";
1415
NSString *const kSentryDataCategoryNameStatsd = @"statsd";
1516
NSString *const kSentryDataCategoryNameUnknown = @"unknown";
1617

@@ -34,6 +35,9 @@
3435
if ([itemType isEqualToString:SentryEnvelopeItemTypeProfile]) {
3536
return kSentryDataCategoryProfile;
3637
}
38+
if ([itemType isEqualToString:SentryEnvelopeItemTypeReplayVideo]) {
39+
return kSentryDataCategoryReplay;
40+
}
3741
if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) {
3842
return kSentryDataCategoryStatsd;
3943
}
@@ -77,6 +81,9 @@
7781
if ([value isEqualToString:kSentryDataCategoryNameProfile]) {
7882
return kSentryDataCategoryProfile;
7983
}
84+
if ([value isEqualToString:kSentryDataCategoryNameReplay]) {
85+
return kSentryDataCategoryReplay;
86+
}
8087
if ([value isEqualToString:kSentryDataCategoryNameStatsd]) {
8188
return kSentryDataCategoryStatsd;
8289
}
@@ -112,6 +119,8 @@
112119
return kSentryDataCategoryNameStatsd;
113120
case kSentryDataCategoryUnknown:
114121
return kSentryDataCategoryNameUnknown;
122+
case kSentryDataCategoryReplay:
123+
return kSentryDataCategoryNameReplay;
115124
}
116125
}
117126

Sources/Sentry/SentryDateUtil.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_
3838
}
3939
}
4040

41+
+ (long)millisecondsSince1970:(NSDate *)date
42+
{
43+
return (NSInteger)([date timeIntervalSince1970] * 1000);
44+
}
45+
4146
@end
4247

4348
NS_ASSUME_NONNULL_END

Sources/Sentry/SentryEnvelope.m

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
#import "SentryLog.h"
1010
#import "SentryMessage.h"
1111
#import "SentryMeta.h"
12+
#import "SentryMsgPackSerializer.h"
13+
#import "SentryReplayEvent.h"
14+
#import "SentryReplayRecording.h"
1215
#import "SentrySdkInfo.h"
1316
#import "SentrySerialization.h"
1417
#import "SentrySession.h"
@@ -48,6 +51,11 @@ - (instancetype)initWithId:(nullable SentryId *)eventId
4851
return self;
4952
}
5053

54+
+ (instancetype)empty
55+
{
56+
return [[SentryEnvelopeHeader alloc] initWithId:nil traceContext:nil];
57+
}
58+
5159
@end
5260

5361
@implementation SentryEnvelopeItem
@@ -198,6 +206,33 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment
198206
return [self initWithHeader:itemHeader data:data];
199207
}
200208

209+
- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent
210+
replayRecording:(SentryReplayRecording *)replayRecording
211+
video:(NSURL *)videoURL
212+
{
213+
NSData *replayEventData = [SentrySerialization dataWithJSONObject:[replayEvent serialize]];
214+
NSData *recording = [SentrySerialization dataWithReplayRecording:replayRecording];
215+
NSURL *envelopeContentUrl =
216+
[[videoURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"dat"];
217+
218+
BOOL success = [SentryMsgPackSerializer serializeDictionaryToMessagePack:@{
219+
@"replay_event" : replayEventData,
220+
@"replay_recording" : recording,
221+
@"replay_video" : videoURL
222+
}
223+
intoFile:envelopeContentUrl];
224+
if (success == NO) {
225+
SENTRY_LOG_DEBUG(@"Could not create MessagePack for session replay envelope item.");
226+
return nil;
227+
}
228+
229+
NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl];
230+
return [self initWithHeader:[[SentryEnvelopeItemHeader alloc]
231+
initWithType:SentryEnvelopeItemTypeReplayVideo
232+
length:envelopeItemContent.length]
233+
data:envelopeItemContent];
234+
}
235+
201236
@end
202237

203238
@implementation SentryEnvelope

Sources/Sentry/SentryHub.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ - (SentryId *)captureEvent:(SentryEvent *)event
297297
return SentryId.empty;
298298
}
299299

300+
- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent
301+
replayRecording:(SentryReplayRecording *)replayRecording
302+
video:(NSURL *)videoURL
303+
{
304+
[_client captureReplayEvent:replayEvent
305+
replayRecording:replayRecording
306+
video:videoURL
307+
withScope:self.scope];
308+
}
309+
300310
- (id<SentrySpan>)startTransactionWithName:(NSString *)name operation:(NSString *)operation
301311
{
302312
return [self startTransactionWithContext:[[SentryTransactionContext alloc]

0 commit comments

Comments
 (0)