Skip to content

Commit

Permalink
feat(Session Replay): ReplayEvent, ReplayRecording and Envelope handling
Browse files Browse the repository at this point in the history
  • Loading branch information
brustolin committed Feb 14, 2024
1 parent 663199a commit eae7f93
Show file tree
Hide file tree
Showing 23 changed files with 503 additions and 8 deletions.
44 changes: 44 additions & 0 deletions Sentry.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

50 changes: 47 additions & 3 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@
#import "SentryOptions+Private.h"
#import "SentryPropagationContext.h"
#import "SentryRandom.h"
#import "SentryReplayEnvelopeItemHeader.h"
#import "SentryReplayEvent.h"
#import "SentryReplayRecording.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 +476,53 @@ - (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
{
replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent
withScope:[[SentryScope alloc] init]
alwaysAttachStacktrace:NO];

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

// breadcrumbs for replay will be send with ReplayRecording
replayEvent.breadcrumbs = nil;

SentryEnvelopeItem *eventEnvelopeItem = [[SentryEnvelopeItem alloc] initWithEvent:replayEvent];

NSData *recording = [SentrySerialization dataWithJSONObject:[replayRecording serialize]];
SentryEnvelopeItem *recordingEnvelopeItem = [[SentryEnvelopeItem alloc]
initWithHeader:[SentryReplayEnvelopeItemHeader
replayRecordingHeaderWithSegmentId:replayRecording.segmentId
length:recording.length]
data:recording];

NSData *video = [NSData dataWithContentsOfURL:videoURL];
SentryEnvelopeItem *videoEnvelopeItem = [[SentryEnvelopeItem alloc]
initWithHeader:[SentryReplayEnvelopeItemHeader
replayVideoHeaderWithSegmentId:replayRecording.segmentId
length:video.length]
data:video];

SentryEnvelope *envelope = [[SentryEnvelope alloc]
initWithHeader:[SentryEnvelopeHeader empty]
items:@[ eventEnvelopeItem, recordingEnvelopeItem, videoEnvelopeItem ]];

[self captureEnvelope:envelope];
}

- (void)captureEnvelope:(SentryEnvelope *)envelope
{
if ([self isDisabled]) {
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryDateUtil.m
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_
}
}

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

@end

NS_ASSUME_NONNULL_END
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryEnvelope.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ - (instancetype)initWithId:(nullable SentryId *)eventId
return self;
}

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

@end

@implementation SentryEnvelopeItem
Expand Down
35 changes: 35 additions & 0 deletions Sources/Sentry/SentryReplayEnvelopeItemHeader.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#import "SentryReplayEnvelopeItemHeader.h"
#import "SentryEnvelopeItemType.h"

@implementation SentryReplayEnvelopeItemHeader

- (instancetype)initWithType:(NSString *)type
segmentId:(NSInteger)segmentId
length:(NSUInteger)length
{
if (self = [super initWithType:type length:length]) {
self.segmentId = segmentId;
}
return self;
}

+ (instancetype)replayRecordingHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length
{
return [[self alloc] initWithType:SentryEnvelopeItemTypeReplayRecording
segmentId:segmentId
length:length];
}

+ (instancetype)replayVideoHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length
{
return [[self alloc] initWithType:SentryEnvelopeItemTypeReplayVideo
segmentId:segmentId
length:length];
}

- (NSDictionary *)serialize
{
return @{ @"type" : self.type, @"length" : @(self.length), @"segment_id" : @(self.segmentId) };
}

@end
28 changes: 28 additions & 0 deletions Sources/Sentry/SentryReplayEvent.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#import "SentryReplayEvent.h"
#import "SentryDateUtil.h"
#import "SentryId.h"

@implementation SentryReplayEvent

- (NSDictionary *)serialize
{
NSMutableDictionary *result = [[super serialize] mutableCopy];

NSMutableArray *trace_ids = [NSMutableArray array];

for (SentryId *traceId in self.traceIds) {
[trace_ids addObject:traceId.sentryIdString];
}

result[@"urls"] = self.urls;
result[@"replay_start_timestamp"] =
@([SentryDateUtil javascriptDate:self.replayStartTimestamp]);
result[@"trace_ids"] = trace_ids;
result[@"replay_id"] = self.replayId.sentryIdString;
result[@"segment_id"] = @(self.segmentId);
result[@"replay_type"] = @"buffer";

return result;
}

@end
64 changes: 64 additions & 0 deletions Sources/Sentry/SentryReplayRecording.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#import "SentryReplayRecording.h"
#import "SentryDateUtil.h"

@implementation SentryReplayRecording

- (instancetype)initWithSegmentId:(NSInteger)segmentId
size:(NSInteger)size
start:(NSDate *)start
duration:(NSTimeInterval)duration
frameCount:(NSInteger)frameCount
frameRate:(NSInteger)frameRate
height:(NSInteger)height
width:(NSInteger)width
{
if (self = [super init]) {
self.segmentId = segmentId;
self.size = size;
self.start = start;
self.duration = duration;
self.frameCount = frameCount;
self.frameRate = frameRate;
self.height = height;
self.width = width;
}
return self;
}

- (nonnull NSArray<NSDictionary<NSString *, id> *> *)serialize
{

long timestamp = [SentryDateUtil javascriptDate:self.start];

NSDictionary *metaInfo = @{
@"type" : @4,
@"timestamp" : @(timestamp),
@"data" : @ { @"href" : @"", @"height" : @(self.height), @"width" : @(self.width) }
};

NSDictionary *recordingInfo = @{
@"type" : @5,
@"timestamp" : @(timestamp),
@"data" : @ {
@"tag" : @"video",
@"payload" : @ {
@"segmentId" : @(self.segmentId),
@"size" : @(self.size),
@"duration" : @(self.duration),
@"encoding" : @"h264",
@"container" : @"mp4",
@"height" : @(self.height),
@"width" : @(self.width),
@"frameCount" : @(self.frameCount),
@"frameRateType" : @"constant",
@"frameRate" : @(self.frameRate),
@"left" : @0,
@"top" : @0,
}
}
};

return @[ metaInfo, recordingInfo ];
}

@end
6 changes: 3 additions & 3 deletions Sources/Sentry/SentrySerialization.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@

@implementation SentrySerialization

+ (NSData *_Nullable)dataWithJSONObject:(NSDictionary *)dictionary
+ (NSData *_Nullable)dataWithJSONObject:(id)jsonObject
{
if (![NSJSONSerialization isValidJSONObject:dictionary]) {
if (![NSJSONSerialization isValidJSONObject:jsonObject]) {
SENTRY_LOG_ERROR(@"Dictionary is not a valid JSON object.");
return nil;
}

NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error];
NSData *data = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:&error];
if (error) {
SENTRY_LOG_ERROR(@"Internal error while serializing JSON: %@", error);
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/HybridPublic/SentryEnvelope.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ SENTRY_NO_INIT
*/
@property (nullable, nonatomic, copy) NSDate *sentAt;

+ (instancetype)empty;

@end

@interface SentryEnvelopeItem : NSObject
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ static NSString *const SentryEnvelopeItemTypeTransaction = @"transaction";
static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment";
static NSString *const SentryEnvelopeItemTypeClientReport = @"client_report";
static NSString *const SentryEnvelopeItemTypeProfile = @"profile";
static NSString *const SentryEnvelopeItemTypeReplayVideo = @"replay_video";
static NSString *const SentryEnvelopeItemTypeReplayRecording = @"replay_recording";
6 changes: 5 additions & 1 deletion Sources/Sentry/include/SentryClient+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#import "SentryDiscardReason.h"

@class SentrySession, SentryEnvelopeItem, SentryId, SentryAttachment, SentryThreadInspector,
SentryEnvelope;
SentryReplayEvent, SentryReplayRecording, SentryEnvelope;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -42,6 +42,10 @@ SentryClient ()
additionalEnvelopeItems:(NSArray<SentryEnvelopeItem *> *)additionalEnvelopeItems
NS_SWIFT_NAME(capture(event:scope:additionalEnvelopeItems:));

- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent
replayRecording:(SentryReplayRecording *)replayRecording
video:(NSURL *)videoURL;

- (void)captureSession:(SentrySession *)session NS_SWIFT_NAME(capture(session:));

/**
Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryDateUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ NS_SWIFT_NAME(DateUtil)

+ (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_Nullable)second;

+ (long)javascriptDate:(NSDate *)date;

@end

NS_ASSUME_NONNULL_END
20 changes: 20 additions & 0 deletions Sources/Sentry/include/SentryReplayEnvelopeItemHeader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#import "SentryEnvelopeItemHeader.h"
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SentryReplayEnvelopeItemHeader : SentryEnvelopeItemHeader

@property (nonatomic) NSInteger segmentId;

- (instancetype)initWithType:(NSString *)type
segmentId:(NSInteger)segmentId
length:(NSUInteger)length;

+ (instancetype)replayRecordingHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length;

+ (instancetype)replayVideoHeaderWithSegmentId:(NSInteger)segmentId length:(NSUInteger)length;

@end

NS_ASSUME_NONNULL_END
39 changes: 39 additions & 0 deletions Sources/Sentry/include/SentryReplayEvent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#import "SentryEvent.h"
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class SentryId;

@interface SentryReplayEvent : SentryEvent

/**
* Start time of the replay segment
*/
@property (nonatomic, strong) NSDate *replayStartTimestamp;

/**
* Number of the segment in the replay.
* This is an incremental number
*/
@property (nonatomic) NSInteger segmentId;

/**
* This will be used to store the name of the screens
* that appear during the duration of the replay segment.
*/
@property (nonatomic, strong) NSArray<NSString *> *urls;

/**
* Trace ids happening during the duration of the replay segment.
*/
@property (nonatomic, strong) NSArray<SentryId *> *traceIds;

/**
* The replay id to which this segment belongs to.
*/
@property (nonatomic, strong) SentryId *replayId;

@end

NS_ASSUME_NONNULL_END
42 changes: 42 additions & 0 deletions Sources/Sentry/include/SentryReplayRecording.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#import "SentrySerializable.h"
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class SentryId;

@interface SentryReplayRecording : NSObject

@property (nonatomic) NSInteger segmentId;

/**
* Video file size
*/
@property (nonatomic) NSInteger size;

@property (nonatomic, strong) NSDate *start;

@property (nonatomic) NSTimeInterval duration;

@property (nonatomic) NSInteger frameCount;

@property (nonatomic) NSInteger frameRate;

@property (nonatomic) NSInteger height;

@property (nonatomic) NSInteger width;

- (instancetype)initWithSegmentId:(NSInteger)segmentId
size:(NSInteger)size
start:(NSDate *)start
duration:(NSTimeInterval)duration
frameCount:(NSInteger)frameCount
frameRate:(NSInteger)frameRate
height:(NSInteger)height
width:(NSInteger)width;

- (nonnull NSArray<NSDictionary<NSString *, id> *> *)serialize;

@end

NS_ASSUME_NONNULL_END
2 changes: 1 addition & 1 deletion Sources/Sentry/include/SentrySerialization.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ static int const SENTRY_BAGGAGE_MAX_SIZE = 8192;

@interface SentrySerialization : NSObject

+ (NSData *_Nullable)dataWithJSONObject:(NSDictionary *)dictionary;
+ (NSData *_Nullable)dataWithJSONObject:(id)jsonObject;

+ (NSData *_Nullable)dataWithSession:(SentrySession *)session;

Expand Down
Loading

0 comments on commit eae7f93

Please sign in to comment.