Skip to content

Commit

Permalink
feat(replay): add Hybrid SDKs Replay interface (#3879)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Apr 25, 2024
1 parent b4f8dba commit 1cafec7
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 5 deletions.
2 changes: 1 addition & 1 deletion SentryTestUtils/TestOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Sentry
public extension Options {
func setIntegrations(_ integrations: [AnyClass]) {
self.integrations = integrations.map {
String(describing: $0)
NSStringFromClass($0)
}
}

Expand Down
34 changes: 34 additions & 0 deletions Sources/Sentry/PrivateSentrySDKOnly.mm
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
#import <SentryBreadcrumb.h>
#import <SentryDependencyContainer.h>
#import <SentryFramesTracker.h>
#import <SentryScope+Private.h>
#import <SentryScreenshot.h>
#import <SentrySessionReplay.h>
#import <SentrySessionReplayIntegration.h>
#import <SentryUser.h>

@implementation PrivateSentrySDKOnly
Expand Down Expand Up @@ -238,4 +241,35 @@ + (SentryBreadcrumb *)breadcrumbWithDictionary:(NSDictionary *)dictionary
return [[SentryBreadcrumb alloc] initWithDictionary:dictionary];
}

+ (void)captureReplay
{
#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
if (@available(iOS 16.0, *)) {
NSArray *integrations = [[SentrySDK currentHub] installedIntegrations];
SentrySessionReplayIntegration *replayIntegration;
for (id obj in integrations) {
if ([obj isKindOfClass:[SentrySessionReplayIntegration class]]) {
replayIntegration = obj;
break;
}
}

[replayIntegration captureReplay];
}
#else
SENTRY_LOG_DEBUG(
@"PrivateSentrySDKOnly.captureReplay only works with UIKit enabled and target is "
@"not visionOS. Ensure you're using the right configuration of Sentry that links UIKit.");
#endif
}

+ (NSString *__nullable)getReplayId
{
__block NSString *__nullable replayId;

[SentrySDK configureScope:^(SentryScope *_Nonnull scope) { replayId = scope.replayId; }];

return replayId;
}

@end
1 change: 0 additions & 1 deletion Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
@property (nullable, nonatomic, strong) SentryClient *client;
@property (nullable, nonatomic, strong) SentryScope *scope;
@property (nonatomic, strong) SentryCrashWrapper *crashWrapper;
@property (nonatomic, strong) NSMutableArray<id<SentryIntegrationProtocol>> *installedIntegrations;
@property (nonatomic, strong) NSMutableSet<NSString *> *installedIntegrationNames;
@property (nonatomic) NSUInteger errorsBeforeSession;

Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ - (void)stop
[self.sessionReplay stop];
}

- (void)captureReplay
{
[self.sessionReplay captureReplay];
}

- (SentryIntegrationOption)integrationOptions
{
return kIntegrationOptionEnableReplay;
Expand Down
3 changes: 3 additions & 0 deletions Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ typedef void (^SentryOnAppStartMeasurementAvailable)(

+ (SentryBreadcrumb *)breadcrumbWithDictionary:(NSDictionary *)dictionary;

+ (void)captureReplay;
+ (NSString *__nullable)getReplayId;

@end

NS_ASSUME_NONNULL_END
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryHub+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ SentryHub ()

@property (nullable, nonatomic, strong) SentrySession *session;

@property (nonatomic, strong) NSMutableArray<id<SentryIntegrationProtocol>> *installedIntegrations;

/**
* Every integration starts with "Sentry" and ends with "Integration". To keep the payload of the
* event small we remove both.
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/include/SentrySessionReplayIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ NS_ASSUME_NONNULL_BEGIN
#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
@interface SentrySessionReplayIntegration : SentryBaseIntegration

/**
* Captures Replay. Used by the Hybrid SDKs.
*/
- (void)captureReplay;

@end
#endif // SENTRY_HAS_UIKIT && !TARGET_OS_VISION
NS_ASSUME_NONNULL_END
55 changes: 52 additions & 3 deletions Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,17 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
XCTAssertGreaterThan(images.count, 100)
}

#if SENTRY_HAS_UIKIT
#if canImport(UIKit)
func testGetAppStartMeasurement() {
let appStartMeasurement = TestData.getAppStartMeasurement(type: .warm)
let appStartMeasurement = TestData.getAppStartMeasurement(type: .warm, runtimeInitSystemTimestamp: 1)
SentrySDK.setAppStartMeasurement(appStartMeasurement)

XCTAssertEqual(appStartMeasurement, PrivateSentrySDKOnly.appStartMeasurement)

SentrySDK.setAppStartMeasurement(nil)
XCTAssertNil(PrivateSentrySDKOnly.appStartMeasurement)
}
#endif // SENTRY_HAS_UIKIT
#endif

func testGetInstallationId() {
XCTAssertEqual(SentryInstallation.id(withCacheDirectoryPath: PrivateSentrySDKOnly.options.cacheDirectoryPath), PrivateSentrySDKOnly.installationID)
Expand Down Expand Up @@ -204,4 +204,53 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
}

#endif

func testCaptureReplayShouldNotFailIfMissingReplayIntegration() {
PrivateSentrySDKOnly.captureReplay()
}

#if canImport(UIKit)
func testCaptureReplayShouldCallReplayIntegration() {
guard #available(iOS 16.0, tvOS 16.0, *) else { return }

let options = Options()
options.setIntegrations([SentrySessionReplayIntegrationTest.self])
SentrySDK.start(options: options)

PrivateSentrySDKOnly.captureReplay()

XCTAssertTrue(SentrySessionReplayIntegrationTest.captureReplayShouldBeCalledAtLeastOnce())
}

func testGetReplayIdShouldBeNil() {
XCTAssertNil(PrivateSentrySDKOnly.getReplayId())
}

func testGetReplayIdShouldExist() {
let client = TestClient(options: Options())
let scope = Scope()
scope.replayId = VALID_REPLAY_ID
SentrySDK.setCurrentHub(TestHub(client: client, andScope: scope))

XCTAssertEqual(PrivateSentrySDKOnly.getReplayId(), VALID_REPLAY_ID)
}

let VALID_REPLAY_ID = "0eac7ab503354dd5819b03e263627a29"

final class SentrySessionReplayIntegrationTest: SentrySessionReplayIntegration {
static var captureReplayCalledTimes = 0

override func install(with options: Options) -> Bool {
return true
}

override func captureReplay() {
SentrySessionReplayIntegrationTest.captureReplayCalledTimes += 1
}

static func captureReplayShouldBeCalledAtLeastOnce() -> Bool {
return captureReplayCalledTimes > 0
}
}
#endif
}

0 comments on commit 1cafec7

Please sign in to comment.