Skip to content

Commit

Permalink
Merge pull request #544 from DataDog/louiszawadzki/rum-1815/implement…
Browse files Browse the repository at this point in the history
…-native-sr-with-no-extra-mapper

Implement native SR without any custom mapper
  • Loading branch information
louiszawadzki authored Oct 30, 2023
2 parents 62c2833 + 3b509bb commit d070ae2
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 9 deletions.
1 change: 1 addition & 0 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ end

target 'ddSdkReactnativeExample' do
pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests']
pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests']

config = use_native_modules!

Expand Down
17 changes: 16 additions & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ PODS:
- DatadogTrace (~> 2.2.1)
- DatadogWebViewTracking (~> 2.2.1)
- React-Core
- DatadogSDKReactNativeSessionReplay (1.8.5):
- DatadogSessionReplay (~> 2.2.1)
- React-Core
- DatadogSDKReactNativeSessionReplay/Tests (1.8.5):
- DatadogSessionReplay (~> 2.2.1)
- React-Core
- DatadogSessionReplay (2.2.1):
- DatadogInternal (= 2.2.1)
- DatadogTrace (2.2.1):
- DatadogInternal (= 2.2.1)
- DatadogWebViewTracking (2.2.1):
Expand Down Expand Up @@ -411,6 +419,8 @@ DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DatadogSDKReactNative (from `../../packages/core/DatadogSDKReactNative.podspec`)
- DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`)
- DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`)
- DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
Expand Down Expand Up @@ -461,6 +471,7 @@ SPEC REPOS:
- DatadogInternal
- DatadogLogs
- DatadogRUM
- DatadogSessionReplay
- DatadogTrace
- DatadogWebViewTracking
- fmt
Expand All @@ -473,6 +484,8 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DatadogSDKReactNative:
:path: "../../packages/core/DatadogSDKReactNative.podspec"
DatadogSDKReactNativeSessionReplay:
:path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
FBLazyVector:
Expand Down Expand Up @@ -562,6 +575,8 @@ SPEC CHECKSUMS:
DatadogLogs: a0eafa7bd2103511eac07bcd2ff95c851123e29b
DatadogRUM: 1e027ccfe4ba1eb81a185f3c58e0909bb12811be
DatadogSDKReactNative: 6f16f15e8b3d5a60c5799d604843a0feb2010c9b
DatadogSDKReactNativeSessionReplay: ec9e93b87abbb2f4935bd02e651b28a9503b48de
DatadogSessionReplay: 4b29318297ad20189a69153b4b0475f0fab7d3f5
DatadogTrace: 74dc91a7a80e746dc4ef1af6d0db1735b5bfd993
DatadogWebViewTracking: 9ca93299a2c900c68ba080f6e800fae1fa3c6b61
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
Expand Down Expand Up @@ -609,6 +624,6 @@ SPEC CHECKSUMS:
RNScreens: f7ad633b2e0190b77b6a7aab7f914fad6f198d8d
Yoga: e7ea9e590e27460d28911403b894722354d73479

PODFILE CHECKSUM: 59a4878659fbb7b053887dd9eec3df44ca9e0b28
PODFILE CHECKSUM: c13458ce5aca9de130799f13a0494e537a800b3c

COCOAPODS: 1.12.1
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Pod::Spec.new do |s|
s.source_files = "ios/Sources/*.{h,m,mm,swift}"

s.dependency "React-Core"
s.dependency 'DatadogSessionReplay', '~> 2.2.1'

s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'ios/Tests/*.swift'
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-session-replay/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ dependencies {
api "com.facebook.react:react-android:$reactNativeVersion"
}
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.datadoghq:dd-sdk-android-session-replay:2.0.0"

testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,44 @@

package com.datadog.reactnative.sessionreplay

import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.facebook.react.bridge.Promise
import java.util.Locale

/**
* The entry point to use Datadog's Session Replay feature.
*/
class DdSessionReplayImplementation() {
class DdSessionReplayImplementation(
private val sessionReplayProvider: () -> SessionReplayWrapper = {
SessionReplaySDKWrapper()
}
) {
/**
* Enable session replay and start recording session.
* @param replaySampleRate The sample rate applied for session replay.
* @param defaultPrivacyLevel The privacy level used for replay.
*/
fun enable(replaySampleRate: Double, defaultPrivacyLevel: String, promise: Promise) {
val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
.setPrivacy(buildPrivacy(defaultPrivacyLevel))
.build()
sessionReplayProvider().enable(configuration)
promise.resolve(null)
}

private fun buildPrivacy(defaultPrivacyLevel: String): SessionReplayPrivacy {
return when (defaultPrivacyLevel?.lowercase(Locale.US)) {
"mask" -> SessionReplayPrivacy.MASK
"mask_user_input" -> SessionReplayPrivacy.MASK_USER_INPUT
"allow" -> SessionReplayPrivacy.ALLOW
else -> {
SessionReplayPrivacy.MASK
}
}

}

companion object {
internal const val NAME = "DdSessionReplay"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.reactnative.sessionreplay

import com.datadog.android.sessionreplay.SessionReplay
import com.datadog.android.sessionreplay.SessionReplayConfiguration

internal class SessionReplaySDKWrapper : SessionReplayWrapper {
/**
* Enables a SessionReplay feature based on the configuration provided.
* @param sessionReplayConfiguration Configuration to use for the feature.
*/
override fun enable(
sessionReplayConfiguration: SessionReplayConfiguration,
) {
SessionReplay.enable(
sessionReplayConfiguration,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.reactnative.sessionreplay

import com.datadog.android.sessionreplay.SessionReplayConfiguration

/**
* Wrapper around [SessionReplay].
*/
interface SessionReplayWrapper {
/**
* Enables a SessionReplay feature based on the configuration provided.
* @param sessionReplayConfiguration Configuration to use for the feature.
*/
fun enable(
sessionReplayConfiguration: SessionReplayConfiguration,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@

package com.datadog.reactnative.sessionreplay

import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.tools.unit.GenericAssert.Companion.assertThat
import com.facebook.react.bridge.Promise
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.verify
import fr.xgouchet.elmyr.annotation.DoubleForgery
import fr.xgouchet.elmyr.annotation.Forgery
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
Expand All @@ -30,18 +38,52 @@ internal class DdSessionReplayImplementationTest {
@Mock
lateinit var mockPromise: Promise

@Mock
lateinit var mockSessionReplay: SessionReplayWrapper

@BeforeEach
fun `set up`() {
testedSessionReplay = DdSessionReplayImplementation()
testedSessionReplay = DdSessionReplayImplementation { mockSessionReplay }
}

@AfterEach
fun `tear down`() {
}

@Test
fun `M do nothing W enable()`() {
fun `M enable session replay W enable()`(
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
@Forgery privacy: SessionReplayPrivacy
) {
// Given
val sessionReplayConfigCaptor = argumentCaptor<SessionReplayConfiguration>()

// When
testedSessionReplay.enable(replaySampleRate, privacy.toString(), mockPromise)

// Then
verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture())
assertThat(sessionReplayConfigCaptor.firstValue)
.hasFieldEqualTo("sampleRate", replaySampleRate.toFloat())
.hasFieldEqualTo("privacy", privacy)
}

@Test
fun `M enable session replay with mask W enable with bad privacy option()`(
@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,
) {
// Given
val sessionReplayConfigCaptor = argumentCaptor<SessionReplayConfiguration>()

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

// Then
verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture())
assertThat(sessionReplayConfigCaptor.firstValue)
.hasFieldEqualTo("sampleRate", replaySampleRate.toFloat())
.hasFieldEqualTo("privacy", SessionReplayPrivacy.MASK)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,56 @@
*/

import Foundation
import DatadogSessionReplay
import DatadogInternal

@objc
public class DdSessionReplayImplementation: NSObject {
private lazy var sessionReplay: SessionReplayProtocol = sessionReplayProvider()
private let sessionReplayProvider: () -> SessionReplayProtocol

internal init(_ sessionReplayProvider: @escaping () -> SessionReplayProtocol) {
self.sessionReplayProvider = sessionReplayProvider
}

@objc
public override convenience init() {
self.init({ NativeSessionReplay() })
}

@objc
public func enable(replaySampleRate: Double, defaultPrivacyLevel: String, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
sessionReplay.enable(
with: SessionReplay.Configuration(
replaySampleRate: Float(replaySampleRate),
defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString)
)
)
resolve(nil)
}

func buildPrivacyLevel(privacyLevel: NSString) -> SessionReplay.Configuration.PrivacyLevel {
switch privacyLevel.lowercased {
case "mask":
return .mask
case "mask_user_input":
return .maskUserInput
case "allow":
return .allow
default:
return .mask
}
}
}

internal protocol SessionReplayProtocol {
func enable(
with configuration: SessionReplay.Configuration
)
}

internal class NativeSessionReplay: SessionReplayProtocol {
func enable(with configuration: DatadogSessionReplay.SessionReplay.Configuration) {
SessionReplay.enable(with: configuration)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,67 @@
*/

import XCTest
@testable import DatadogSDKReactNativeSessionReplay
import DatadogSessionReplay

internal class DdSessionReplayTests: XCTestCase {
private lazy var sessionReplay = DdSessionReplayImplementation()

private func mockResolve(args: Any?) {}
private func mockReject(args: String?, arg: String?, err: Error?) {}

func testDoesNothing() {
sessionReplay.enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject)
func testEnablesSessionReplayWithZeroReplaySampleRate() {
let sessionReplayMock = MockSessionReplay()
DdSessionReplayImplementation({ sessionReplayMock })
.enable(replaySampleRate: 0, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject)

XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 0.0, privacyLevel: .mask))
}

func testEnablesSessionReplayWithMaskPrivacyLevel() {
let sessionReplayMock = MockSessionReplay()
DdSessionReplayImplementation({ sessionReplayMock })
.enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject)

XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask))
}

func testEnablesSessionReplayWithMaskUserInputPrivacyLevel() {
let sessionReplayMock = MockSessionReplay()
DdSessionReplayImplementation({ sessionReplayMock })
.enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK_USER_INPUT", resolve: mockResolve, reject: mockReject)

XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .maskUserInput))
}

func testEnablesSessionReplayWithAllowPrivacyLevel() {
let sessionReplayMock = MockSessionReplay()
DdSessionReplayImplementation({ sessionReplayMock })
.enable(replaySampleRate: 100, defaultPrivacyLevel: "ALLOW", resolve: mockResolve, reject: mockReject)

XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .allow))
}

func testEnablesSessionReplayWithBadPrivacyLevel() {
let sessionReplayMock = MockSessionReplay()
DdSessionReplayImplementation({ sessionReplayMock })
.enable(replaySampleRate: 100, defaultPrivacyLevel: "BAD_VALUE", resolve: mockResolve, reject: mockReject)

XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask))
}
}

private class MockSessionReplay: SessionReplayProtocol {
enum CalledMethod: Equatable {
case enable(replaySampleRate: Float, privacyLevel: SessionReplay.Configuration.PrivacyLevel)
}

public var calledMethods = [CalledMethod]()

func enable(with configuration: SessionReplay.Configuration) {
calledMethods.append(
.enable(
replaySampleRate: configuration.replaySampleRate,
privacyLevel: configuration.defaultPrivacyLevel
)
)
}
}

0 comments on commit d070ae2

Please sign in to comment.