Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RUM-7023] Session Replay Start and Stop API #737

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,20 @@ class DdSessionReplayImplementation(
* @param replaySampleRate The sample rate applied for session replay.
* @param defaultPrivacyLevel The privacy level used for replay.
* @param customEndpoint Custom server url for sending replay data.
* @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled.
*/
fun enable(replaySampleRate: Double, defaultPrivacyLevel: String, customEndpoint: String, promise: Promise) {
fun enable(
replaySampleRate: Double,
defaultPrivacyLevel: String,
customEndpoint: String,
startRecordingImmediately: Boolean,
promise: Promise
) {
val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
val logger = sdkCore.internalLogger
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
.configurePrivacy(defaultPrivacyLevel)
.startRecordingImmediately(startRecordingImmediately)
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext, logger))

if (customEndpoint != "") {
Expand All @@ -46,6 +54,26 @@ class DdSessionReplayImplementation(
promise.resolve(null)
}

/**
* Manually start recording the current session.
*/
fun startRecording(promise: Promise) {
sessionReplayProvider().startRecording(
DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
)
promise.resolve(null)
}

/**
* Manually stop recording the current session.
*/
fun stopRecording(promise: Promise) {
sessionReplayProvider().stopRecording(
DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
)
promise.resolve(null)
}

@Deprecated("Privacy should be set with separate properties mapped to " +
"`setImagePrivacy`, `setTouchPrivacy`, `setTextAndInputPrivacy`, but they are" +
" currently unavailable.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,18 @@ internal class SessionReplaySDKWrapper : SessionReplayWrapper {
sdkCore,
)
}

/**
* Manually start recording the current session.
*/
override fun startRecording(sdkCore: SdkCore) {
SessionReplay.startRecording(sdkCore)
}

/**
* Manually stop recording the current session.
*/
override fun stopRecording(sdkCore: SdkCore) {
SessionReplay.stopRecording(sdkCore)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ interface SessionReplayWrapper {
sessionReplayConfiguration: SessionReplayConfiguration,
sdkCore: SdkCore
)

/**
* Manually start recording the current session.
*/
fun startRecording(sdkCore: SdkCore)

/**
* Manually stop recording the current session.
*/
fun stopRecording(sdkCore: SdkCore)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,38 @@ class DdSessionReplay(
* @param replaySampleRate The sample rate applied for session replay.
* @param defaultPrivacyLevel The privacy level used for replay.
* @param customEndpoint Custom server url for sending replay data.
* @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled.
*/
@ReactMethod
fun enable(
replaySampleRate: Double,
defaultPrivacyLevel: String,
customEndpoint: String,
startRecordingImmediately: Boolean,
promise: Promise
) {
implementation.enable(replaySampleRate, defaultPrivacyLevel, customEndpoint, promise)
implementation.enable(
replaySampleRate,
defaultPrivacyLevel,
customEndpoint,
startRecordingImmediately,
promise
)
}

/**
* Manually start recording the current session.
*/
@ReactMethod
fun startRecording(promise: Promise) {
implementation.startRecording(promise)
}

/**
* Manually stop recording the current session.
*/
@ReactMethod
fun stopRecording(promise: Promise) {
implementation.stopRecording(promise)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule
import fr.xgouchet.elmyr.annotation.BoolForgery
import fr.xgouchet.elmyr.annotation.DoubleForgery
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeExtension
Expand Down Expand Up @@ -72,31 +73,50 @@ internal class DdSessionReplayImplementationTest {
@Test
fun `M enable session replay W privacy = ALLOW`(
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
@StringForgery(regex = ".+") customEndpoint: String
@StringForgery(regex = ".+") customEndpoint: String,
@BoolForgery startRecordingImmediately: Boolean
) {
testSessionReplayEnable("ALLOW", replaySampleRate, customEndpoint)
testSessionReplayEnable(
"ALLOW",
replaySampleRate,
customEndpoint,
startRecordingImmediately
)
}

@Test
fun `M enable session replay W privacy = MASK`(
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
@StringForgery(regex = ".+") customEndpoint: String
@StringForgery(regex = ".+") customEndpoint: String,
@BoolForgery startRecordingImmediately: Boolean
) {
testSessionReplayEnable("MASK", replaySampleRate, customEndpoint)
testSessionReplayEnable(
"MASK",
replaySampleRate,
customEndpoint,
startRecordingImmediately
)
}

@Test
fun `M enable session replay W privacy = MASK_USER_INPUT`(
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
@StringForgery(regex = ".+") customEndpoint: String
@StringForgery(regex = ".+") customEndpoint: String,
@BoolForgery startRecordingImmediately: Boolean
) {
testSessionReplayEnable("MASK_USER_INPUT", replaySampleRate, customEndpoint)
testSessionReplayEnable(
"MASK_USER_INPUT",
replaySampleRate,
customEndpoint,
startRecordingImmediately
)
}

private fun testSessionReplayEnable(
privacy: String,
replaySampleRate: Double,
customEndpoint: String
customEndpoint: String,
startRecordingImmediately: Boolean
) {
// Given
val sessionReplayConfigCaptor = argumentCaptor<SessionReplayConfiguration>()
Expand All @@ -106,6 +126,7 @@ internal class DdSessionReplayImplementationTest {
replaySampleRate,
privacy,
customEndpoint,
startRecordingImmediately,
mockPromise
)

Expand Down Expand Up @@ -144,19 +165,27 @@ internal class DdSessionReplayImplementationTest {
fun `M enable session replay without custom endpoint W empty string()`(
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
// Not ALLOW nor MASK_USER_INPUT
@StringForgery(regex = "^/(?!ALLOW|MASK_USER_INPUT)([a-z0-9]+)$/i") privacy: String
@StringForgery(regex = "^/(?!ALLOW|MASK_USER_INPUT)([a-z0-9]+)$/i") privacy: String,
@BoolForgery startRecordingImmediately: Boolean
) {
// Given
val sessionReplayConfigCaptor = argumentCaptor<SessionReplayConfiguration>()

// When
testedSessionReplay.enable(replaySampleRate, privacy, "", mockPromise)
testedSessionReplay.enable(
replaySampleRate,
privacy,
"",
startRecordingImmediately,
mockPromise
)

// Then
verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture(), any())
assertThat(sessionReplayConfigCaptor.firstValue)
.hasFieldEqualTo("sampleRate", replaySampleRate.toFloat())
.hasFieldEqualTo("privacy", SessionReplayPrivacy.MASK)
.hasFieldEqualTo("startRecordingImmediately", startRecordingImmediately)
.doesNotHaveField("customEndpointUrl")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,26 @@ @implementation DdSessionReplay
RCT_REMAP_METHOD(enable, withEnableReplaySampleRate:(double)replaySampleRate
withDefaultPrivacyLevel:(NSString*)defaultPrivacyLevel
withCustomEndpoint:(NSString*)customEndpoint
withStartRecordingImmediately:(BOOL)startRecordingImmediately
withResolver:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
{
[self enable:replaySampleRate defaultPrivacyLevel:defaultPrivacyLevel customEndpoint:customEndpoint resolve:resolve reject:reject];
[self enable:replaySampleRate
defaultPrivacyLevel:defaultPrivacyLevel
customEndpoint:customEndpoint
startRecordingImmediately:startRecordingImmediately
resolve:resolve
reject:reject];
}

RCT_EXPORT_METHOD(startRecording:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
{
[self startRecordingWithResolver:resolve reject:reject];
}

RCT_EXPORT_METHOD(stopRecording:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
{
[self stopRecordingWithResolver:resolve reject:reject];
}

// Thanks to this guard, we won't compile this code when we build for the old architecture.
Expand All @@ -47,8 +63,26 @@ + (BOOL)requiresMainQueueSetup {
return NO;
}

- (void)enable:(double)replaySampleRate defaultPrivacyLevel:(NSString *)defaultPrivacyLevel customEndpoint:(NSString*)customEndpoint resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self.ddSessionReplayImplementation enableWithReplaySampleRate:replaySampleRate defaultPrivacyLevel:defaultPrivacyLevel customEndpoint:customEndpoint resolve:resolve reject:reject];
- (void)enable:(double)replaySampleRate
defaultPrivacyLevel:(NSString *)defaultPrivacyLevel
customEndpoint:(NSString*)customEndpoint
startRecordingImmediately:(BOOL)startRecordingImmediately
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
[self.ddSessionReplayImplementation enableWithReplaySampleRate:replaySampleRate
defaultPrivacyLevel:defaultPrivacyLevel
customEndpoint:customEndpoint
startRecordingImmediately:startRecordingImmediately
resolve:resolve
reject:reject];
}

- (void)startRecordingWithResolver:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self.ddSessionReplayImplementation startRecordingWithResolve:resolve reject:reject];
}

- (void)stopRecordingWithResolver:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self.ddSessionReplayImplementation stopRecordingWithResolve:resolve reject:reject];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@ public class DdSessionReplayImplementation: NSObject {
}

@objc
public func enable(replaySampleRate: Double, defaultPrivacyLevel: String, customEndpoint: String, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
public func enable(
replaySampleRate: Double,
defaultPrivacyLevel: String,
customEndpoint: String,
startRecordingImmediately: Bool,
resolve:RCTPromiseResolveBlock,
reject:RCTPromiseRejectBlock
) -> Void {
var customEndpointURL: URL? = nil
if (customEndpoint != "") {
customEndpointURL = URL(string: "\(customEndpoint)/api/v2/replay" as String)
}
var sessionReplayConfiguration = SessionReplay.Configuration(
replaySampleRate: Float(replaySampleRate),
defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString),
startRecordingImmediately: startRecordingImmediately,
customEndpoint: customEndpointURL
)

Expand All @@ -55,6 +63,28 @@ public class DdSessionReplayImplementation: NSObject {
resolve(nil)
}

@objc
public func startRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
if let core = DatadogSDKWrapper.shared.getCoreInstance() {
sessionReplay.startRecording(in: core)
} else {
consolePrint("Core instance was not found when calling startRecording in Session Replay.", .critical)
}

resolve(nil)
}

@objc
public func stopRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
if let core = DatadogSDKWrapper.shared.getCoreInstance() {
sessionReplay.stopRecording(in: core)
} else {
consolePrint("Core instance was not found when calling stopRecording in Session Replay.", .critical)
}

resolve(nil)
}

func buildPrivacyLevel(privacyLevel: NSString) -> SessionReplayPrivacyLevel {
switch privacyLevel.lowercased {
case "mask":
Expand All @@ -74,10 +104,21 @@ internal protocol SessionReplayProtocol {
with configuration: SessionReplay.Configuration,
in core: DatadogCoreProtocol
)

func startRecording(in core: DatadogCoreProtocol)
func stopRecording(in core: DatadogCoreProtocol)
}

internal class NativeSessionReplay: SessionReplayProtocol {
func enable(with configuration: DatadogSessionReplay.SessionReplay.Configuration, in core: DatadogCoreProtocol) {
SessionReplay.enable(with: configuration, in: core)
}

func startRecording(in core: any DatadogInternal.DatadogCoreProtocol) {
SessionReplay.startRecording(in: core)
}

func stopRecording(in core: any DatadogInternal.DatadogCoreProtocol) {
SessionReplay.stopRecording(in: core)
}
}
Loading
Loading