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

Implement native SR without any custom mapper #544

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
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
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"
0xnm marked this conversation as resolved.
Show resolved Hide resolved

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()`(
louiszawadzki marked this conversation as resolved.
Show resolved Hide resolved
@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
)
)
}
}