Skip to content

Commit 0de47cc

Browse files
authored
Merge 87d0133 into 2de284c
2 parents 2de284c + 87d0133 commit 0de47cc

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

+1107
-20
lines changed

CHANGELOG.md

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

1616
### Features
1717

18+
- Add Session Replay (#3625)
1819
- Add support for Sentry [Spotlight](https://spotlightjs.com/) (#3642), which is basically Sentry
1920
for development. Read our [blog post](https://blog.sentry.io/sentry-for-development/) to find out more.
2021
- 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
@@ -28,6 +28,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[];
2828
#import "SentryMessage.h"
2929
#import "SentryNSError.h"
3030
#import "SentryOptions.h"
31+
#import "SentryReplayOptions.h"
3132
#import "SentryRequest.h"
3233
#import "SentrySDK.h"
3334
#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
@@ -12,7 +12,7 @@
1212
#import "SentryDependencyContainer.h"
1313
#import "SentryDispatchQueueWrapper.h"
1414
#import "SentryDsn.h"
15-
#import "SentryEnvelope.h"
15+
#import "SentryEnvelope+Private.h"
1616
#import "SentryEnvelopeItemType.h"
1717
#import "SentryEvent.h"
1818
#import "SentryException.h"
@@ -28,12 +28,15 @@
2828
#import "SentryMechanismMeta.h"
2929
#import "SentryMessage.h"
3030
#import "SentryMeta.h"
31+
#import "SentryMsgPackSerializer.h"
3132
#import "SentryNSError.h"
3233
#import "SentryOptions+Private.h"
3334
#import "SentryPropagationContext.h"
3435
#import "SentryRandom.h"
36+
#import "SentryReplayEvent.h"
3537
#import "SentrySDK+Private.h"
3638
#import "SentryScope+Private.h"
39+
#import "SentrySerialization.h"
3740
#import "SentrySession.h"
3841
#import "SentryStacktraceBuilder.h"
3942
#import "SentrySwift.h"
@@ -470,13 +473,43 @@ - (void)captureSession:(SentrySession *)session
470473
}
471474

472475
SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithSession:session];
473-
SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:nil
474-
traceContext:nil];
475-
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader
476+
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty]
476477
singleItem:item];
477478
[self captureEnvelope:envelope];
478479
}
479480

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

552585
BOOL eventIsNotATransaction
553586
= event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeTransaction];
587+
BOOL eventIsNotReplay
588+
= event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo];
554589

555590
// Transactions have their own sampleRate
556-
if (eventIsNotATransaction && [self isSampled:self.options.sampleRate]) {
591+
if (eventIsNotATransaction && eventIsNotReplay && [self isSampled:self.options.sampleRate]) {
557592
SENTRY_LOG_DEBUG(@"Event got sampled, will not send the event");
558593
[self recordLostEvent:kSentryDataCategoryError reason:kSentryDiscardReasonSampleRate];
559594
return nil;
@@ -581,7 +616,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
581616
[self setSdk:event];
582617

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

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

0 commit comments

Comments
 (0)