Skip to content
Draft
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 @@ -10,7 +10,6 @@ import com.datadog.android.Datadog
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.SdkCore
import com.datadog.android.flags.EvaluationContextCallback
import com.datadog.android.flags.Flags
import com.datadog.android.flags.FlagsClient
import com.datadog.android.flags.FlagsConfiguration
import com.datadog.android.flags._FlagsInternalProxy
Expand Down Expand Up @@ -41,9 +40,15 @@ class DdFlagsImplementation(
configuration: ReadableMap,
promise: Promise,
) {
val flagsConfig = buildFlagsConfiguration(configuration.toMap())
val configMap = configuration.toMap()
val flagsConfig = buildFlagsConfiguration(configMap)
if (flagsConfig != null) {
Flags.enable(flagsConfig, sdkCore)
_FlagsInternalProxy.enable(
flagsConfig,
sdkCore,
"dd-sdk-reactnative",
configMap["_ddFlagsSdkVersion"] as? String ?: "unknown",
)
} else {
InternalLogger.UNBOUND.log(
InternalLogger.Level.ERROR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,27 @@
package com.datadog.reactnative

import com.datadog.android.Datadog
import com.datadog.android.api.SdkCore
import com.datadog.android.flags.Flags
import com.datadog.android.flags.FlagsConfiguration
import com.datadog.android.DatadogSite
import com.datadog.android.api.context.DatadogContext
import com.datadog.android.api.feature.Feature
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.flags.model.EvaluationContext
import com.datadog.tools.unit.toReadableMap
import com.facebook.react.bridge.Promise
import okhttp3.Request
import okio.Buffer
import org.assertj.core.api.Assertions.assertThat
import org.json.JSONObject
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.same
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@Extensions(
ExtendWith(MockitoExtension::class)
Expand Down Expand Up @@ -51,31 +56,68 @@ internal class DdFlagsImplementationTest {
@Test
fun `M resolve current SDK core W enable() called after construction`() {
// Given
val staleCore = mock<SdkCore>()
val initializedCore = mock<SdkCore>()
val staleCore = mock<FeatureSdkCore>()
val initializedCore = mock<FeatureSdkCore>()
whenever(initializedCore.internalLogger).thenReturn(mock())
var currentCore = staleCore
val configuration = mapOf("enabled" to true).toReadableMap()
val configuration = mapOf(
"enabled" to true,
"_ddFlagsSdkVersion" to "4.2.0-js.1"
).toReadableMap()
val datadogMock = Mockito.mockStatic(Datadog::class.java)
val flagsMock = Mockito.mockStatic(Flags::class.java)

try {
datadogMock.`when`<SdkCore> { Datadog.getInstance() }.thenAnswer { currentCore }
flagsMock.`when`<Unit> { Flags.enable(any<FlagsConfiguration>(), any()) }.then { }
datadogMock.`when`<FeatureSdkCore> { Datadog.getInstance() }.thenAnswer { currentCore }
val testedImplementation = DdFlagsImplementation()
currentCore = initializedCore

// When
testedImplementation.enable(configuration, mockPromise)

// Then
flagsMock.verify { Flags.enable(any<FlagsConfiguration>(), same(initializedCore)) }
assertSource(initializedCore, "4.2.0-js.1")
verify(mockPromise).resolve(null)
} finally {
flagsMock.close()
datadogMock.close()
}
}

@Test
fun `M report unknown reader W enable() from older JS`() {
val core = mock<FeatureSdkCore>()
whenever(core.internalLogger).thenReturn(mock())
DdFlagsImplementation(core).enable(mapOf("enabled" to true).toReadableMap(), mockPromise)
assertSource(core, "unknown")
}

private fun assertSource(core: FeatureSdkCore, expectedJsVersion: String) {
val features = argumentCaptor<Feature>()
verify(core, org.mockito.kotlin.times(2)).registerFeature(features.capture())
val flags = features.allValues.single { it.name == "flags" }
// Exercise the real native serializer without exposing a test API in the SDK.
val getter = flags.javaClass.methods.single {
it.name.startsWith(
"getPrecomputedRequestFactory"
)
}
val factory = getter.invoke(flags)
val create = factory.javaClass.methods.single { it.name == "create" }
val context = mock<DatadogContext>()
whenever(context.site).thenReturn(DatadogSite.US1)
whenever(context.env).thenReturn("test")
whenever(context.clientToken).thenReturn("local-test-only")
whenever(context.featuresContext).thenReturn(emptyMap())
val evaluationContext = EvaluationContext("athlete", emptyMap())
val request = create.invoke(factory, evaluationContext, context) as Request
val buffer = Buffer()
checkNotNull(request.body).writeTo(buffer)
val source = JSONObject(buffer.readUtf8()).getJSONObject("data")
.getJSONObject("attributes").getJSONObject("source")
assertThat(source.getString("sdk_name")).isEqualTo("dd-sdk-reactnative")
assertThat(source.getString("sdk_version")).isEqualTo(expectedJsVersion)
assertThat(source.length()).isEqualTo(2)
}

@Test
fun `M omit null attributes W buildEvaluationContext()`() {
// Given
Expand Down
10 changes: 9 additions & 1 deletion packages/core/ios/Sources/DdFlagsImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ public class DdFlagsImplementation: NSObject {
clientProviders.removeAll()

if let config = configuration.asFlagsConfiguration() {
Flags.enable(with: config)
Flags.enable(
with: config,
in: core,
wrapperSDKName: "dd-sdk-reactnative",
wrapperSDKVersion: configuration["_ddFlagsSdkVersion"] as? String ?? "unknown"
)
} else {
consolePrint("Invalid configuration provided for Flags. Feature initialization skipped.", .error)
}
Expand Down Expand Up @@ -91,6 +96,9 @@ public class DdFlagsImplementation: NSObject {
case .invalidResponse:
errorCode = "INVALID_RESPONSE"
errorMessage = "The flags service returned an invalid response for client '\(clientName)'."
case .initializationTimedOut:
errorCode = "INITIALIZATION_TIMED_OUT"
errorMessage = "Flag initialization timed out for client '\(clientName)'."
case .networkError:
errorCode = "NETWORK_ERROR"
errorMessage = "A network error occurred while fetching feature flags for client '\(clientName)'."
Expand Down
16 changes: 16 additions & 0 deletions packages/core/ios/Tests/DdFlagsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ class DdFlagsTests: XCTestCase {
waitForExpectations(timeout: 1, handler: nil)
}

func testInitializationTimeoutIsReturnedToJavaScript() {
let completion = expectation(description: "Timeout rejects")
let client = MockFlagsClient()
client.errorToReturn = .initializationTimedOut
implementation.clientProviders["test_client"] = { client }
implementation.setEvaluationContext(
"test_client", targetingKey: "athlete", attributes: [:],
resolve: { _ in XCTFail("Expected rejection") },
reject: { code, _, _ in
XCTAssertEqual(code, "INITIALIZATION_TIMED_OUT")
completion.fulfill()
}
)
waitForExpectations(timeout: 1)
}

func testSetEvaluationContextSuccess() {
let expectation = self.expectation(description: "SetEvaluationContext resolves with flags")

Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/flags/DdFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InternalLog } from '../InternalLog';
import { SdkVerbosity } from '../config/types/SdkVerbosity';
import type { DdNativeFlagsType } from '../nativeModulesTypes';
import { getGlobalInstance } from '../utils/singletonUtils';
import { version } from '../version';

import { FlagsClient } from './FlagsClient';
import type { DdFlagsType, FlagsConfiguration } from './types';
Expand All @@ -32,7 +33,12 @@ class DdFlagsWrapper implements DdFlagsType {
private clients: Record<string, FlagsClient> = {};

enable = async (configuration: FlagsConfiguration = {}): Promise<void> => {
await this.nativeFlags.enable({ enabled: true, ...configuration });
await this.nativeFlags.enable({
enabled: true,
...configuration,
// The loaded bundle can differ from the compiled native bridge.
_ddFlagsSdkVersion: version
});

this.isFeatureEnabled = true;
};
Expand Down
16 changes: 13 additions & 3 deletions packages/core/src/flags/__tests__/DdFlags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import { NativeModules } from 'react-native';

import { version as packageVersion } from '../../../package.json';
import { InternalLog } from '../../InternalLog';
import { SdkVerbosity } from '../../config/types/SdkVerbosity';
import { version } from '../../version';
import { DdFlags } from '../DdFlags';

jest.mock('../../InternalLog', () => {
Expand All @@ -20,6 +22,10 @@ jest.mock('../../InternalLog', () => {
});

describe('DdFlags', () => {
it('should report the core package version', () => {
expect(version).toBe(packageVersion);
});

beforeEach(() => {
jest.clearAllMocks();
// Reset state of DdFlags instance.
Expand All @@ -33,7 +39,8 @@ describe('DdFlags', () => {
await DdFlags.enable();

expect(NativeModules.DdFlags.enable).toHaveBeenCalledWith({
enabled: true
enabled: true,
_ddFlagsSdkVersion: version
});
});

Expand All @@ -42,15 +49,18 @@ describe('DdFlags', () => {
customExposureEndpoint: 'https://example.com',
customFlagsEndpoint: 'https://example.com',
trackExposures: false,
rumIntegrationEnabled: false
rumIntegrationEnabled: false,
// An application value must not replace package identity.
...{ _ddFlagsSdkVersion: '99.99.99-application' }
});

expect(NativeModules.DdFlags.enable).toHaveBeenCalledWith({
enabled: true,
customExposureEndpoint: 'https://example.com',
customFlagsEndpoint: 'https://example.com',
trackExposures: false,
rumIntegrationEnabled: false
rumIntegrationEnabled: false,
_ddFlagsSdkVersion: version
});
});

Expand Down
Loading