Skip to content

Commit

Permalink
Merge pull request #737 from DataDog/marcosaia/RUM-7023/sr-start-and-…
Browse files Browse the repository at this point in the history
…stop

[RUM-7023] Session Replay Start and Stop API
  • Loading branch information
marco-saia-datadog authored Nov 19, 2024
2 parents c75f8ba + b685a6e commit 2b8f309
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 44 deletions.
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

0 comments on commit 2b8f309

Please sign in to comment.