Skip to content

Commit

Permalink
feat: Session Replay (getsentry#3625)
Browse files Browse the repository at this point in the history
Added session replay

Co-authored-by: Philipp Hofmann <[email protected]>
  • Loading branch information
2 people authored and Dipak Kasabwala committed May 6, 2024
1 parent 333cd57 commit 246439c
Show file tree
Hide file tree
Showing 61 changed files with 2,407 additions and 38 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add Session Replay, which is **still experimental**. (#3625)

## 8.24.0

### Features
Expand Down
10 changes: 7 additions & 3 deletions Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let dsn = DSNStorage.shared.getDSN() ?? AppDelegate.defaultDSN
DSNStorage.shared.saveDSN(dsn: dsn)

SentrySDK.start { options in
SentrySDK.start(configureOptions: { options in
options.dsn = dsn
options.beforeSend = { event in
return event
}
options.debug = true

if #available(iOS 16.0, *) {
options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true)
}

if #available(iOS 15.0, *) {
options.enableMetricKit = true
}
Expand Down Expand Up @@ -60,7 +64,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
options.sessionTrackingIntervalMillis = 5_000
options.attachScreenshot = true
options.attachViewHierarchy = true

#if targetEnvironment(simulator)
options.enableSpotlight = true
options.environment = "test-app"
Expand Down Expand Up @@ -130,7 +134,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
return scope
}
}
})

SentrySDK.metrics.increment(key: "app.start", value: 1.0, tags: ["view": "app-delegate"])

Expand Down
6 changes: 4 additions & 2 deletions Sentry.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Pod::Spec.new do |s|

s.subspec 'Core' do |sp|
sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}",
"Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}",
"Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}"
sp.preserve_path = "Sources/Sentry/include/module.modulemap"
sp.public_header_files =
"Sources/Sentry/Public/*.h"

Expand All @@ -43,7 +44,8 @@ Pod::Spec.new do |s|
s.subspec 'HybridSDK' do |sp|
sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}",
"Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}"


sp.preserve_path = "Sources/Sentry/include/module.modulemap"
sp.public_header_files =
"Sources/Sentry/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h"

Expand Down
150 changes: 142 additions & 8 deletions Sentry.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions SentryTestUtils/TestCurrentDateProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public class TestCurrentDateProvider: SentryCurrentDateProvider {
setDate(date: date().addingTimeInterval(TimeInterval(nanoseconds) / 1e9))
internalSystemTime += nanoseconds
}

public func advanceBy(interval: TimeInterval) {
setDate(date: date().addingTimeInterval(interval))
}

public var timezoneOffsetValue = 0
public override func timezoneOffset() -> Int {
Expand Down
1 change: 1 addition & 0 deletions SentryTestUtils/TestTransport.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _SentryPrivate
import Foundation

@objc
Expand Down
5 changes: 5 additions & 0 deletions Sources/Configuration/SentryNoUI.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "Sentry.xcconfig"

//This is how we avoid linking UIKit from Swift code
//when compiling without UIKit
OTHER_SWIFT_FLAGS = -DSENTRY_NO_UIKIT
11 changes: 10 additions & 1 deletion Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

NS_ASSUME_NONNULL_BEGIN

@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope;
@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope,
SentryReplayOptions;
@class SentryExperimentalOptions;

NS_SWIFT_NAME(Options)
@interface SentryOptions : NSObject
Expand Down Expand Up @@ -269,6 +271,7 @@ NS_SWIFT_NAME(Options)
* @note Default value is @c NO .
*/
@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing;

#endif // SENTRY_UIKIT_AVAILABLE

/**
Expand Down Expand Up @@ -617,6 +620,12 @@ NS_SWIFT_NAME(Options)
*/
@property (nullable, nonatomic, copy) SentryBeforeEmitMetricCallback beforeEmitMetric;

/**
* This aggregates options for experimental features.
* Be aware that the options available for experimental can change at any time.
*/
@property (nonatomic, readonly) SentryExperimentalOptions *experimental;

@end

NS_ASSUME_NONNULL_END
14 changes: 14 additions & 0 deletions Sources/Sentry/SentryBaseIntegration.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "SentryBaseIntegration.h"
#import "SentryCrashWrapper.h"
#import "SentryLog.h"
#import "SentrySwift.h"
#import <Foundation/Foundation.h>
#import <SentryDependencyContainer.h>
#import <SentryOptions+Private.h>
Expand Down Expand Up @@ -140,6 +141,19 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options
[self logWithOptionName:@"attachViewHierarchy"];
return NO;
}

if (integrationOptions & kIntegrationOptionEnableReplay) {
if (@available(iOS 16.0, tvOS 16.0, *)) {
if (options.experimental.sessionReplay.errorSampleRate == 0
&& options.experimental.sessionReplay.sessionSampleRate == 0) {
[self logWithOptionName:@"sessionReplaySettings"];
return NO;
}
} else {
[self logWithReason:@"Session replay requires iOS 16 or above"];
return NO;
}
}
#endif

if ((integrationOptions & kIntegrationOptionEnableCrashHandler)
Expand Down
56 changes: 48 additions & 8 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#import "SentryDependencyContainer.h"
#import "SentryDispatchQueueWrapper.h"
#import "SentryDsn.h"
#import "SentryEnvelope.h"
#import "SentryEnvelope+Private.h"
#import "SentryEnvelopeItemType.h"
#import "SentryEvent.h"
#import "SentryException.h"
Expand All @@ -27,13 +27,16 @@
#import "SentryMechanismMeta.h"
#import "SentryMessage.h"
#import "SentryMeta.h"
#import "SentryMsgPackSerializer.h"
#import "SentryNSDictionarySanitize.h"
#import "SentryNSError.h"
#import "SentryOptions+Private.h"
#import "SentryPropagationContext.h"
#import "SentryRandom.h"
#import "SentryReplayEvent.h"
#import "SentrySDK+Private.h"
#import "SentryScope+Private.h"
#import "SentrySerialization.h"
#import "SentrySession.h"
#import "SentryStacktraceBuilder.h"
#import "SentrySwift.h"
Expand Down Expand Up @@ -472,13 +475,44 @@ - (void)captureSession:(SentrySession *)session
}

SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithSession:session];
SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:nil
traceContext:nil];
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader
SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty]
singleItem:item];
[self captureEnvelope:envelope];
}

- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent
replayRecording:(SentryReplayRecording *)replayRecording
video:(NSURL *)videoURL
withScope:(SentryScope *)scope
{
replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent
withScope:scope
alwaysAttachStacktrace:NO];

if (![replayEvent isKindOfClass:SentryReplayEvent.class]) {
SENTRY_LOG_DEBUG(@"The event preprocessor didn't update the replay event in place. The "
@"replay was discarded.");
return;
}

SentryEnvelopeItem *videoEnvelopeItem =
[[SentryEnvelopeItem alloc] initWithReplayEvent:replayEvent
replayRecording:replayRecording
video:videoURL];

if (videoEnvelopeItem == nil) {
SENTRY_LOG_DEBUG(@"The Session Replay segment will not be sent to Sentry because an "
@"Envelope Item could not be created.");
return;
}

SentryEnvelope *envelope = [[SentryEnvelope alloc]
initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId]
items:@[ videoEnvelopeItem ]];

[self captureEnvelope:envelope];
}

- (void)captureEnvelope:(SentryEnvelope *)envelope
{
if ([self isDisabled]) {
Expand Down Expand Up @@ -553,9 +587,11 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event

BOOL eventIsNotATransaction
= event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeTransaction];
BOOL eventIsNotReplay
= event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo];

// Transactions have their own sampleRate
if (eventIsNotATransaction && [self isSampled:self.options.sampleRate]) {
// Transactions and replays have their own sampleRate
if (eventIsNotATransaction && eventIsNotReplay && [self isSampled:self.options.sampleRate]) {
SENTRY_LOG_DEBUG(@"Event got sampled, will not send the event");
[self recordLostEvent:kSentryDataCategoryError reason:kSentryDiscardReasonSampleRate];
return nil;
Expand All @@ -582,8 +618,8 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event

[self setSdk:event];

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

Expand Down Expand Up @@ -623,6 +659,10 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event

event = [scope applyToEvent:event maxBreadcrumb:self.options.maxBreadcrumbs];

if (!eventIsNotReplay) {
event.breadcrumbs = nil;
}

if ([self isWatchdogTermination:event isCrashEvent:isCrashEvent]) {
// Remove some mutable properties from the device/app contexts which are no longer
// applicable
Expand Down
18 changes: 18 additions & 0 deletions Sources/Sentry/SentryCoreGraphicsHelper.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#import "SentryCoreGraphicsHelper.h"
#if SENTRY_HAS_UIKIT
@implementation SentryCoreGraphicsHelper
+ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path
{
# if (TARGET_OS_IOS || TARGET_OS_TV)
# ifdef __IPHONE_16_0
if (@available(iOS 16.0, tvOS 16.0, *)) {
CGPathRef exclude = CGPathCreateWithRect(rectangle, nil);
CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES);
return CGPathCreateMutableCopy(newPath);
}
# endif // defined(__IPHONE_16_0)
# endif // (TARGET_OS_IOS || TARGET_OS_TV)
return path;
}
@end
#endif // SENTRY_HAS_UIKIT
9 changes: 9 additions & 0 deletions Sources/Sentry/SentryDataCategoryMapper.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
NSString *const kSentryDataCategoryNameAttachment = @"attachment";
NSString *const kSentryDataCategoryNameUserFeedback = @"user_report";
NSString *const kSentryDataCategoryNameProfile = @"profile";
NSString *const kSentryDataCategoryNameReplay = @"replay";
NSString *const kSentryDataCategoryNameMetricBucket = @"metric_bucket";
NSString *const kSentryDataCategoryNameUnknown = @"unknown";

Expand All @@ -34,6 +35,9 @@
if ([itemType isEqualToString:SentryEnvelopeItemTypeProfile]) {
return kSentryDataCategoryProfile;
}
if ([itemType isEqualToString:SentryEnvelopeItemTypeReplayVideo]) {
return kSentryDataCategoryReplay;
}
// The envelope item type used for metrics is statsd whereas the client report category for
// discarded events is metric_bucket.
if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) {
Expand Down Expand Up @@ -79,6 +83,9 @@
if ([value isEqualToString:kSentryDataCategoryNameProfile]) {
return kSentryDataCategoryProfile;
}
if ([value isEqualToString:kSentryDataCategoryNameReplay]) {
return kSentryDataCategoryReplay;
}
if ([value isEqualToString:kSentryDataCategoryNameMetricBucket]) {
return kSentryDataCategoryMetricBucket;
}
Expand Down Expand Up @@ -114,6 +121,8 @@
return kSentryDataCategoryNameMetricBucket;
case kSentryDataCategoryUnknown:
return kSentryDataCategoryNameUnknown;
case kSentryDataCategoryReplay:
return kSentryDataCategoryNameReplay;
}
}

Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryDateUtil.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_
}
}

+ (long)millisecondsSince1970:(NSDate *)date
{
return (long)([date timeIntervalSince1970] * 1000);
}

@end

NS_ASSUME_NONNULL_END
40 changes: 40 additions & 0 deletions Sources/Sentry/SentryEnvelope.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#import "SentryLog.h"
#import "SentryMessage.h"
#import "SentryMeta.h"
#import "SentryMsgPackSerializer.h"
#import "SentryReplayEvent.h"
#import "SentryReplayRecording.h"
#import "SentrySdkInfo.h"
#import "SentrySerialization.h"
#import "SentrySession.h"
Expand Down Expand Up @@ -48,6 +51,11 @@ - (instancetype)initWithId:(nullable SentryId *)eventId
return self;
}

+ (instancetype)empty
{
return [[SentryEnvelopeHeader alloc] initWithId:nil traceContext:nil];
}

@end

@implementation SentryEnvelopeItem
Expand Down Expand Up @@ -198,6 +206,38 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment
return [self initWithHeader:itemHeader data:data];
}

- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent
replayRecording:(SentryReplayRecording *)replayRecording
video:(NSURL *)videoURL
{
NSData *replayEventData = [SentrySerialization dataWithJSONObject:[replayEvent serialize]];
NSData *recording = [SentrySerialization dataWithReplayRecording:replayRecording];
NSURL *envelopeContentUrl =
[[videoURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"dat"];

BOOL success = [SentryMsgPackSerializer serializeDictionaryToMessagePack:@{
@"replay_event" : replayEventData,
@"replay_recording" : recording,
@"replay_video" : videoURL
}
intoFile:envelopeContentUrl];
if (success == NO) {
SENTRY_LOG_ERROR(@"Could not create MessagePack for session replay envelope item.");
return nil;
}

NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl];

NSError *error;
if (![NSFileManager.defaultManager removeItemAtURL:envelopeContentUrl error:&error]) {
SENTRY_LOG_ERROR(@"Cound not delete temporary replay content from disk: %@", error);
}
return [self initWithHeader:[[SentryEnvelopeItemHeader alloc]
initWithType:SentryEnvelopeItemTypeReplayVideo
length:envelopeItemContent.length]
data:envelopeItemContent];
}

@end

@implementation SentryEnvelope
Expand Down
Loading

0 comments on commit 246439c

Please sign in to comment.