From c56e78440a34d02a1c7335dd08177aaa22d83f74 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 1 Oct 2026 22:13:38 -0400 Subject: [PATCH 1/2] fix(flags): report the loaded JavaScript package version --- .gitlab-ci.yml | 2 + package.json | 6 +- .../reactnative/DdFlagsImplementation.kt | 11 ++- .../com/datadog/reactnative/SdkVersion.kt | 2 +- .../reactnative/DdFlagsImplementationTest.kt | 68 +++++++++++++++---- .../ios/Sources/DdFlagsImplementation.swift | 10 ++- packages/core/ios/Sources/SdkVersion.swift | 2 +- packages/core/ios/Tests/DdFlagsTests.swift | 16 +++++ packages/core/package.json | 3 +- packages/core/scripts/SDK-IDENTITY.md | 33 +++++++++ .../core/scripts/generate-sdk-version.cjs | 47 +++++++++++++ .../scripts/generate-sdk-version.test.cjs | 41 +++++++++++ packages/core/src/flags/DdFlags.ts | 8 ++- .../core/src/flags/__tests__/DdFlags.test.ts | 11 ++- packages/core/src/version.ts | 2 +- update-version.sh | 4 -- 16 files changed, 234 insertions(+), 32 deletions(-) create mode 100644 packages/core/scripts/SDK-IDENTITY.md create mode 100644 packages/core/scripts/generate-sdk-version.cjs create mode 100644 packages/core/scripts/generate-sdk-version.test.cjs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 576b454cb..494f9e31e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,6 +65,8 @@ test:js: - if: '$BUILD_BENCHMARK != "true"' timeout: 1h script: + - node packages/core/scripts/generate-sdk-version.cjs --check + - node --test packages/core/scripts/generate-sdk-version.test.cjs - yarn - cp jestSetup.js.override node_modules/react-native-gesture-handler/jestSetup.js - NODE_OPTIONS='-r dd-trace/ci/init' DD_ENV=ci DD_SERVICE=dd-sdk-reactnative yarn test diff --git a/package.json b/package.json index 268603687..f0578a7b9 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ ] }, "scripts": { - "prepare": "genversion --es6 --semi packages/core/src/version.ts && lerna run prepare", - "test": "genversion --es6 --semi packages/core/src/version.ts && jest", - "lint": "genversion --es6 --semi packages/core/src/version.ts && eslint .", + "prepare": "node packages/core/scripts/generate-sdk-version.cjs && lerna run prepare", + "test": "node packages/core/scripts/generate-sdk-version.cjs --check && jest", + "lint": "node packages/core/scripts/generate-sdk-version.cjs --check && eslint .", "example": "yarn --cwd example", "postinstall": "./packages/react-navigation/fix-react-navigation-import-in-dependencies.sh" }, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt index 954e1eff5..b66a3b9c0 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt @@ -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 @@ -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, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt index 57c91847a..b1c03c5de 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt @@ -6,5 +6,5 @@ package com.datadog.reactnative -// This is automatically updated by the update-version.sh script +// Generated from packages/core/package.json. Do not edit. internal const val SDK_VERSION = "3.9.0" diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdFlagsImplementationTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdFlagsImplementationTest.kt index 4bede8d2c..b2b4f1da2 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdFlagsImplementationTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdFlagsImplementationTest.kt @@ -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) @@ -51,16 +56,18 @@ internal class DdFlagsImplementationTest { @Test fun `M resolve current SDK core W enable() called after construction`() { // Given - val staleCore = mock() - val initializedCore = mock() + val staleCore = mock() + val initializedCore = mock() + 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` { Datadog.getInstance() }.thenAnswer { currentCore } - flagsMock.`when` { Flags.enable(any(), any()) }.then { } + datadogMock.`when` { Datadog.getInstance() }.thenAnswer { currentCore } val testedImplementation = DdFlagsImplementation() currentCore = initializedCore @@ -68,14 +75,49 @@ internal class DdFlagsImplementationTest { testedImplementation.enable(configuration, mockPromise) // Then - flagsMock.verify { Flags.enable(any(), 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() + 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() + 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() + 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 diff --git a/packages/core/ios/Sources/DdFlagsImplementation.swift b/packages/core/ios/Sources/DdFlagsImplementation.swift index 34020f410..854d3e319 100644 --- a/packages/core/ios/Sources/DdFlagsImplementation.swift +++ b/packages/core/ios/Sources/DdFlagsImplementation.swift @@ -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) } @@ -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)'." diff --git a/packages/core/ios/Sources/SdkVersion.swift b/packages/core/ios/Sources/SdkVersion.swift index 9298a1fd6..e12f23daa 100644 --- a/packages/core/ios/Sources/SdkVersion.swift +++ b/packages/core/ios/Sources/SdkVersion.swift @@ -6,5 +6,5 @@ import Foundation -// This is automatically updated by the update-version.sh script +// Generated from packages/core/package.json. Do not edit. let SdkVersion = "3.9.0" diff --git a/packages/core/ios/Tests/DdFlagsTests.swift b/packages/core/ios/Tests/DdFlagsTests.swift index 3fca72ddc..d8e9407ac 100644 --- a/packages/core/ios/Tests/DdFlagsTests.swift +++ b/packages/core/ios/Tests/DdFlagsTests.swift @@ -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") diff --git a/packages/core/package.json b/packages/core/package.json index 74c6cabc3..75e2f63b9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,7 +61,8 @@ "scripts": { "test": "jest", "lint": "eslint .", - "prepare": "rm -rf lib && yarn bob build", + "prepare": "node scripts/generate-sdk-version.cjs && rm -rf lib && yarn bob build", + "prepack": "node scripts/generate-sdk-version.cjs --check && yarn bob build", "postinstall": "node scripts/replace-react-require.js # Use node to execute the script for Windows, see https://github.com/DataDog/dd-sdk-reactnative/issues/358" }, "peerDependencies": { diff --git a/packages/core/scripts/SDK-IDENTITY.md b/packages/core/scripts/SDK-IDENTITY.md new file mode 100644 index 000000000..4561d00d5 --- /dev/null +++ b/packages/core/scripts/SDK-IDENTITY.md @@ -0,0 +1,33 @@ +# Flags request identity + +`packages/core/package.json` is the version source. Normal `yarn prepare` generates the JavaScript, Swift, and Kotlin constants. +The release script calls this workflow after Lerna changes the package version. No customer step is required. +CI checks generated files before building. Packing checks the generated files and rebuilds JavaScript before creating the archive. + +Run the generator checks with: + +```sh +node packages/core/scripts/generate-sdk-version.cjs --check +node --test packages/core/scripts/generate-sdk-version.test.cjs +``` + +The JavaScript Flags reader sends its generated version to the native bridge automatically. +The native SDK places that version in the request without changing RUM telemetry metadata. +For example, an over-the-air JavaScript update can produce: + +```json +{ + "sdk_name": "dd-sdk-reactnative", + "sdk_version": "4.2.0" +} +``` + +These values are compatibility information, not authentication. +Before a React Native release advertises a response format, its native dependencies and bridge must support that format. +The JavaScript version alone cannot prove this after an over-the-air update with an older native binary. +Response-format rollout must handle that combination separately; this PR does not enable a new format. +An older JavaScript bundle reports an unknown reader version. The bridge must not substitute its own version for that reader. + +The internal native enable interfaces require the companion iOS and Android changes before this PR can merge. +Update the native dependency pins after those interfaces are released. Do not substitute a declared dependency version for the actual native artifact version. +Local validation can use the companion native builds. No native version is duplicated in this repository. diff --git a/packages/core/scripts/generate-sdk-version.cjs b/packages/core/scripts/generate-sdk-version.cjs new file mode 100644 index 000000000..6013501ea --- /dev/null +++ b/packages/core/scripts/generate-sdk-version.cjs @@ -0,0 +1,47 @@ +/* + * 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. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const license = `/* + * 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. + */ +`; + +function generate(packageRoot, check = false) { + const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); + const numeric = '(0|[1-9][0-9]*)'; + const prerelease = '(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'; + const semver = new RegExp(`^${numeric}\\.${numeric}\\.${numeric}(?:-${prerelease}(?:\\.${prerelease})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`); + if (typeof version !== 'string' || !semver.test(version)) { + throw new Error('The core package must have a valid SemVer version.'); + } + const generated = '// Generated from packages/core/package.json. Do not edit.\n'; + const outputs = { + 'src/version.ts': `${generated}export const version = '${version}';\n`, + 'ios/Sources/SdkVersion.swift': `${license}\nimport Foundation\n\n${generated}let SdkVersion = "${version}"\n`, + 'android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt': `${license}\npackage com.datadog.reactnative\n\n${generated}internal const val SDK_VERSION = "${version}"\n` + }; + for (const [relative, expected] of Object.entries(outputs)) { + const file = path.join(packageRoot, relative); + if (check) { + if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8') !== expected) { + throw new Error(`Missing or stale SDK version: ${relative}. Run yarn prepare.`); + } + } else { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, expected); + } + } +} + +if (require.main === module) { + generate(path.resolve(__dirname, '..'), process.argv.includes('--check')); +} +module.exports = { generate }; diff --git a/packages/core/scripts/generate-sdk-version.test.cjs b/packages/core/scripts/generate-sdk-version.test.cjs new file mode 100644 index 000000000..4c267c0eb --- /dev/null +++ b/packages/core/scripts/generate-sdk-version.test.cjs @@ -0,0 +1,41 @@ +/* + * 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. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { generate } = require('./generate-sdk-version.cjs'); + +for (const version of ['3.9.0', '4.0.0', '4.1.0-rc.2+build.42']) { + test(`generates all three shipped identities from ${version}`, t => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-sdk-version-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ version })); + generate(root); + generate(root, true); + for (const relative of ['src/version.ts', 'ios/Sources/SdkVersion.swift', 'android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt']) { + assert.ok(fs.readFileSync(path.join(root, relative), 'utf8').includes(version)); + } + fs.writeFileSync(path.join(root, 'src/version.ts'), 'stale'); + assert.throws(() => generate(root, true), /stale/); + generate(root); + fs.unlinkSync(path.join(root, 'ios/Sources/SdkVersion.swift')); + assert.throws(() => generate(root, true), /Missing/); + generate(root); + generate(root, true); + }); +} + +test('rejects absent and invalid package versions', t => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-sdk-version-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + for (const version of [undefined, '', '1.2', '01.2.3', '1.2.3-01']) { + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ version })); + assert.throws(() => generate(root), /SemVer/); + } +}); diff --git a/packages/core/src/flags/DdFlags.ts b/packages/core/src/flags/DdFlags.ts index 43714d598..9114a6667 100644 --- a/packages/core/src/flags/DdFlags.ts +++ b/packages/core/src/flags/DdFlags.ts @@ -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'; @@ -32,7 +33,12 @@ class DdFlagsWrapper implements DdFlagsType { private clients: Record = {}; enable = async (configuration: FlagsConfiguration = {}): Promise => { - 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; }; diff --git a/packages/core/src/flags/__tests__/DdFlags.test.ts b/packages/core/src/flags/__tests__/DdFlags.test.ts index 01e65d431..8a574e3c2 100644 --- a/packages/core/src/flags/__tests__/DdFlags.test.ts +++ b/packages/core/src/flags/__tests__/DdFlags.test.ts @@ -8,6 +8,7 @@ import { NativeModules } from 'react-native'; import { InternalLog } from '../../InternalLog'; import { SdkVerbosity } from '../../config/types/SdkVerbosity'; +import { version } from '../../version'; import { DdFlags } from '../DdFlags'; jest.mock('../../InternalLog', () => { @@ -33,7 +34,8 @@ describe('DdFlags', () => { await DdFlags.enable(); expect(NativeModules.DdFlags.enable).toHaveBeenCalledWith({ - enabled: true + enabled: true, + _ddFlagsSdkVersion: version }); }); @@ -42,7 +44,9 @@ 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({ @@ -50,7 +54,8 @@ describe('DdFlags', () => { customExposureEndpoint: 'https://example.com', customFlagsEndpoint: 'https://example.com', trackExposures: false, - rumIntegrationEnabled: false + rumIntegrationEnabled: false, + _ddFlagsSdkVersion: version }); }); diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 3ce867548..c6b20661d 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,2 +1,2 @@ -// generated by genversion +// Generated from packages/core/package.json. Do not edit. export const version = '3.9.0'; diff --git a/update-version.sh b/update-version.sh index 097fba0ee..5263c2f95 100755 --- a/update-version.sh +++ b/update-version.sh @@ -2,10 +2,6 @@ set -e -# Change version in native files -sed -i '' -e "s/\".*\"/\"$1\"/g" packages/core/ios/Sources/SdkVersion.swift -sed -i '' -e "s/\".*\"/\"$1\"/g" packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt - # Change version yarn run lerna version $1 --ignore-changes --no-git-tag-version --no-push yarn prepare From 61116e9b9bd3828f875448aa5c637dfd4df8162f Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 1 Oct 2026 23:10:54 -0400 Subject: [PATCH 2/2] fix(flags): reuse existing JavaScript version tooling --- .gitlab-ci.yml | 2 - package.json | 6 +-- .../com/datadog/reactnative/SdkVersion.kt | 2 +- packages/core/ios/Sources/SdkVersion.swift | 2 +- packages/core/package.json | 3 +- packages/core/scripts/SDK-IDENTITY.md | 33 ------------- .../core/scripts/generate-sdk-version.cjs | 47 ------------------- .../scripts/generate-sdk-version.test.cjs | 41 ---------------- .../core/src/flags/__tests__/DdFlags.test.ts | 5 ++ packages/core/src/version.ts | 2 +- update-version.sh | 4 ++ 11 files changed, 16 insertions(+), 131 deletions(-) delete mode 100644 packages/core/scripts/SDK-IDENTITY.md delete mode 100644 packages/core/scripts/generate-sdk-version.cjs delete mode 100644 packages/core/scripts/generate-sdk-version.test.cjs diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 494f9e31e..576b454cb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,8 +65,6 @@ test:js: - if: '$BUILD_BENCHMARK != "true"' timeout: 1h script: - - node packages/core/scripts/generate-sdk-version.cjs --check - - node --test packages/core/scripts/generate-sdk-version.test.cjs - yarn - cp jestSetup.js.override node_modules/react-native-gesture-handler/jestSetup.js - NODE_OPTIONS='-r dd-trace/ci/init' DD_ENV=ci DD_SERVICE=dd-sdk-reactnative yarn test diff --git a/package.json b/package.json index f0578a7b9..268603687 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ ] }, "scripts": { - "prepare": "node packages/core/scripts/generate-sdk-version.cjs && lerna run prepare", - "test": "node packages/core/scripts/generate-sdk-version.cjs --check && jest", - "lint": "node packages/core/scripts/generate-sdk-version.cjs --check && eslint .", + "prepare": "genversion --es6 --semi packages/core/src/version.ts && lerna run prepare", + "test": "genversion --es6 --semi packages/core/src/version.ts && jest", + "lint": "genversion --es6 --semi packages/core/src/version.ts && eslint .", "example": "yarn --cwd example", "postinstall": "./packages/react-navigation/fix-react-navigation-import-in-dependencies.sh" }, diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt index b1c03c5de..57c91847a 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt @@ -6,5 +6,5 @@ package com.datadog.reactnative -// Generated from packages/core/package.json. Do not edit. +// This is automatically updated by the update-version.sh script internal const val SDK_VERSION = "3.9.0" diff --git a/packages/core/ios/Sources/SdkVersion.swift b/packages/core/ios/Sources/SdkVersion.swift index e12f23daa..9298a1fd6 100644 --- a/packages/core/ios/Sources/SdkVersion.swift +++ b/packages/core/ios/Sources/SdkVersion.swift @@ -6,5 +6,5 @@ import Foundation -// Generated from packages/core/package.json. Do not edit. +// This is automatically updated by the update-version.sh script let SdkVersion = "3.9.0" diff --git a/packages/core/package.json b/packages/core/package.json index 75e2f63b9..74c6cabc3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,8 +61,7 @@ "scripts": { "test": "jest", "lint": "eslint .", - "prepare": "node scripts/generate-sdk-version.cjs && rm -rf lib && yarn bob build", - "prepack": "node scripts/generate-sdk-version.cjs --check && yarn bob build", + "prepare": "rm -rf lib && yarn bob build", "postinstall": "node scripts/replace-react-require.js # Use node to execute the script for Windows, see https://github.com/DataDog/dd-sdk-reactnative/issues/358" }, "peerDependencies": { diff --git a/packages/core/scripts/SDK-IDENTITY.md b/packages/core/scripts/SDK-IDENTITY.md deleted file mode 100644 index 4561d00d5..000000000 --- a/packages/core/scripts/SDK-IDENTITY.md +++ /dev/null @@ -1,33 +0,0 @@ -# Flags request identity - -`packages/core/package.json` is the version source. Normal `yarn prepare` generates the JavaScript, Swift, and Kotlin constants. -The release script calls this workflow after Lerna changes the package version. No customer step is required. -CI checks generated files before building. Packing checks the generated files and rebuilds JavaScript before creating the archive. - -Run the generator checks with: - -```sh -node packages/core/scripts/generate-sdk-version.cjs --check -node --test packages/core/scripts/generate-sdk-version.test.cjs -``` - -The JavaScript Flags reader sends its generated version to the native bridge automatically. -The native SDK places that version in the request without changing RUM telemetry metadata. -For example, an over-the-air JavaScript update can produce: - -```json -{ - "sdk_name": "dd-sdk-reactnative", - "sdk_version": "4.2.0" -} -``` - -These values are compatibility information, not authentication. -Before a React Native release advertises a response format, its native dependencies and bridge must support that format. -The JavaScript version alone cannot prove this after an over-the-air update with an older native binary. -Response-format rollout must handle that combination separately; this PR does not enable a new format. -An older JavaScript bundle reports an unknown reader version. The bridge must not substitute its own version for that reader. - -The internal native enable interfaces require the companion iOS and Android changes before this PR can merge. -Update the native dependency pins after those interfaces are released. Do not substitute a declared dependency version for the actual native artifact version. -Local validation can use the companion native builds. No native version is duplicated in this repository. diff --git a/packages/core/scripts/generate-sdk-version.cjs b/packages/core/scripts/generate-sdk-version.cjs deleted file mode 100644 index 6013501ea..000000000 --- a/packages/core/scripts/generate-sdk-version.cjs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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. - */ - -const fs = require('node:fs'); -const path = require('node:path'); - -const license = `/* - * 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. - */ -`; - -function generate(packageRoot, check = false) { - const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')); - const numeric = '(0|[1-9][0-9]*)'; - const prerelease = '(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'; - const semver = new RegExp(`^${numeric}\\.${numeric}\\.${numeric}(?:-${prerelease}(?:\\.${prerelease})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`); - if (typeof version !== 'string' || !semver.test(version)) { - throw new Error('The core package must have a valid SemVer version.'); - } - const generated = '// Generated from packages/core/package.json. Do not edit.\n'; - const outputs = { - 'src/version.ts': `${generated}export const version = '${version}';\n`, - 'ios/Sources/SdkVersion.swift': `${license}\nimport Foundation\n\n${generated}let SdkVersion = "${version}"\n`, - 'android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt': `${license}\npackage com.datadog.reactnative\n\n${generated}internal const val SDK_VERSION = "${version}"\n` - }; - for (const [relative, expected] of Object.entries(outputs)) { - const file = path.join(packageRoot, relative); - if (check) { - if (!fs.existsSync(file) || fs.readFileSync(file, 'utf8') !== expected) { - throw new Error(`Missing or stale SDK version: ${relative}. Run yarn prepare.`); - } - } else { - fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(file, expected); - } - } -} - -if (require.main === module) { - generate(path.resolve(__dirname, '..'), process.argv.includes('--check')); -} -module.exports = { generate }; diff --git a/packages/core/scripts/generate-sdk-version.test.cjs b/packages/core/scripts/generate-sdk-version.test.cjs deleted file mode 100644 index 4c267c0eb..000000000 --- a/packages/core/scripts/generate-sdk-version.test.cjs +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - */ - -const { test } = require('node:test'); -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const { generate } = require('./generate-sdk-version.cjs'); - -for (const version of ['3.9.0', '4.0.0', '4.1.0-rc.2+build.42']) { - test(`generates all three shipped identities from ${version}`, t => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-sdk-version-')); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ version })); - generate(root); - generate(root, true); - for (const relative of ['src/version.ts', 'ios/Sources/SdkVersion.swift', 'android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt']) { - assert.ok(fs.readFileSync(path.join(root, relative), 'utf8').includes(version)); - } - fs.writeFileSync(path.join(root, 'src/version.ts'), 'stale'); - assert.throws(() => generate(root, true), /stale/); - generate(root); - fs.unlinkSync(path.join(root, 'ios/Sources/SdkVersion.swift')); - assert.throws(() => generate(root, true), /Missing/); - generate(root); - generate(root, true); - }); -} - -test('rejects absent and invalid package versions', t => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-sdk-version-')); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - for (const version of [undefined, '', '1.2', '01.2.3', '1.2.3-01']) { - fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ version })); - assert.throws(() => generate(root), /SemVer/); - } -}); diff --git a/packages/core/src/flags/__tests__/DdFlags.test.ts b/packages/core/src/flags/__tests__/DdFlags.test.ts index 8a574e3c2..d8a220dc7 100644 --- a/packages/core/src/flags/__tests__/DdFlags.test.ts +++ b/packages/core/src/flags/__tests__/DdFlags.test.ts @@ -6,6 +6,7 @@ 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'; @@ -21,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. diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index c6b20661d..3ce867548 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,2 +1,2 @@ -// Generated from packages/core/package.json. Do not edit. +// generated by genversion export const version = '3.9.0'; diff --git a/update-version.sh b/update-version.sh index 5263c2f95..097fba0ee 100755 --- a/update-version.sh +++ b/update-version.sh @@ -2,6 +2,10 @@ set -e +# Change version in native files +sed -i '' -e "s/\".*\"/\"$1\"/g" packages/core/ios/Sources/SdkVersion.swift +sed -i '' -e "s/\".*\"/\"$1\"/g" packages/core/android/src/main/kotlin/com/datadog/reactnative/SdkVersion.kt + # Change version yarn run lerna version $1 --ignore-changes --no-git-tag-version --no-push yarn prepare