From f92f0524d832c1f78f18c124aca184b89e66c9e0 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Thu, 10 Sep 2026 14:59:00 +0200 Subject: [PATCH] Expose native profilers to RN SDK --- .gitlab-ci.yml | 13 + bump-native-dd-sdk.sh | 2 + example-new-architecture/App.tsx | 6 + example-new-architecture/ios/Podfile.lock | 31 + example-new-architecture/package.json | 1 + example/ios/Podfile | 1 + example/ios/Podfile.lock | 19 +- example/package.json | 1 + example/src/App.tsx | 6 + example/src/ddUtils.tsx | 7 + example/src/screens/MainScreen.tsx | 26 + .../DatadogSDKReactNativeProfiling.podspec | 43 ++ packages/react-native-profiling/README.md | 90 +++ .../__mocks__/react-native.ts | 18 + .../android/build.gradle | 258 +++++++ .../react-native-profiling/android/detekt.yml | 721 ++++++++++++++++++ .../android/gradle.properties | 6 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + .../react-native-profiling/android/gradlew | 240 ++++++ .../android/gradlew.bat | 91 +++ .../android/settings.gradle | 4 + .../android/src/main/AndroidManifest.xml | 11 + .../profiling/DdProfilingImplementation.kt | 72 ++ .../profiling/ProfilingSDKWrapper.kt | 21 + .../reactnative/profiling/ProfilingWrapper.kt | 36 + .../reactnative/profiling/DdProfiling.kt | 42 + .../reactnative/profiling/DdProfiling.kt | 45 ++ .../DdSDKReactNativeProfilingPackage.kt | 53 ++ .../DdSDKReactNativeProfilingPackage.kt | 30 + .../DdProfilingImplementationTest.kt | 151 ++++ .../reactnative/tools/unit/GenericAssert.kt | 69 ++ .../reactnative/tools/unit/ReflectUtils.kt | 266 +++++++ .../react-native-profiling/babel.config.js | 3 + .../ios/Sources/DdProfiling.h | 24 + .../ios/Sources/DdProfiling.mm | 66 ++ .../Sources/DdProfilingImplementation.swift | 70 ++ .../ios/Tests/DdProfilingTests.swift | 147 ++++ packages/react-native-profiling/package.json | 90 +++ .../react-native-profiling/src/Profiling.ts | 105 +++ .../src/__tests__/Profiling.test.ts | 66 ++ packages/react-native-profiling/src/index.ts | 12 + .../src/nativeModulesTypes.ts | 26 + .../src/specs/NativeDdProfiling.ts | 25 + packages/react-native-profiling/tsconfig.json | 3 + update-native-sdk-versions.sh | 2 + update-version.sh | 2 + yarn.lock | 13 + 48 files changed, 3038 insertions(+), 1 deletion(-) create mode 100644 packages/react-native-profiling/DatadogSDKReactNativeProfiling.podspec create mode 100644 packages/react-native-profiling/README.md create mode 100644 packages/react-native-profiling/__mocks__/react-native.ts create mode 100644 packages/react-native-profiling/android/build.gradle create mode 100644 packages/react-native-profiling/android/detekt.yml create mode 100644 packages/react-native-profiling/android/gradle.properties create mode 100644 packages/react-native-profiling/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 packages/react-native-profiling/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 packages/react-native-profiling/android/gradlew create mode 100644 packages/react-native-profiling/android/gradlew.bat create mode 100644 packages/react-native-profiling/android/settings.gradle create mode 100644 packages/react-native-profiling/android/src/main/AndroidManifest.xml create mode 100644 packages/react-native-profiling/android/src/main/kotlin/com/datadog/reactnative/profiling/DdProfilingImplementation.kt create mode 100644 packages/react-native-profiling/android/src/main/kotlin/com/datadog/reactnative/profiling/ProfilingSDKWrapper.kt create mode 100644 packages/react-native-profiling/android/src/main/kotlin/com/datadog/reactnative/profiling/ProfilingWrapper.kt create mode 100644 packages/react-native-profiling/android/src/newarch/kotlin/com/datadog/reactnative/profiling/DdProfiling.kt create mode 100644 packages/react-native-profiling/android/src/oldarch/kotlin/com/datadog/reactnative/profiling/DdProfiling.kt create mode 100644 packages/react-native-profiling/android/src/rnpost74/kotlin/com/datadog/reactnative/profiling/DdSDKReactNativeProfilingPackage.kt create mode 100644 packages/react-native-profiling/android/src/rnpre74/kotlin/com/datadog/reactnative/profiling/DdSDKReactNativeProfilingPackage.kt create mode 100644 packages/react-native-profiling/android/src/test/kotlin/com/datadog/reactnative/profiling/DdProfilingImplementationTest.kt create mode 100644 packages/react-native-profiling/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt create mode 100644 packages/react-native-profiling/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt create mode 100644 packages/react-native-profiling/babel.config.js create mode 100644 packages/react-native-profiling/ios/Sources/DdProfiling.h create mode 100644 packages/react-native-profiling/ios/Sources/DdProfiling.mm create mode 100644 packages/react-native-profiling/ios/Sources/DdProfilingImplementation.swift create mode 100644 packages/react-native-profiling/ios/Tests/DdProfilingTests.swift create mode 100644 packages/react-native-profiling/package.json create mode 100644 packages/react-native-profiling/src/Profiling.ts create mode 100644 packages/react-native-profiling/src/__tests__/Profiling.test.ts create mode 100644 packages/react-native-profiling/src/index.ts create mode 100644 packages/react-native-profiling/src/nativeModulesTypes.ts create mode 100644 packages/react-native-profiling/src/specs/NativeDdProfiling.ts create mode 100644 packages/react-native-profiling/tsconfig.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 576b454cb..4cdd9e939 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -91,10 +91,12 @@ test:native-android: - /opt/homebrew/opt/openjdk@17/bin/java --version - echo "org.gradle.java.home=/opt/homebrew/opt/openjdk@17" >> packages/core/android/gradle.properties - echo "org.gradle.java.home=/opt/homebrew/opt/openjdk@17" >> packages/react-native-session-replay/android/gradle.properties + - echo "org.gradle.java.home=/opt/homebrew/opt/openjdk@17" >> packages/react-native-profiling/android/gradle.properties - echo "org.gradle.java.home=/opt/homebrew/opt/openjdk@17" >> packages/internal-testing-tools/android/gradle.properties - yarn - (cd packages/core/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24) - (cd packages/react-native-session-replay/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24 -PDatadogSDKReactNativeSessionReplay_minSdkVersion=24) + - (cd packages/react-native-profiling/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24 -PDatadogSDKReactNativeProfiling_minSdkVersion=24) - (cd packages/internal-testing-tools/android && ./gradlew build -PDdSdkReactNative_minSdkVersion=24 -PDatadogInternalTesting_minSdkVersion=24) test:native-ios: @@ -119,6 +121,17 @@ test:native-ios-sr: - (cd example/ios && RCT_NEW_ARCH_ENABLED=0 pod install --repo-update) - set -o pipefail && xcodebuild -workspace example/ios/DdSdkReactNativeExample.xcworkspace -scheme DatadogSDKReactNativeSessionReplay test -destination "$IOS_SIMULATOR_DESTINATION" -derivedDataPath "$IOS_DERIVED_DATA_PATH" | xcbeautify +test:native-ios-profiling: + tags: ['macos:sequoia-arm64', 'specific:true'] + stage: test + rules: + - if: '$BUILD_BENCHMARK != "true"' + timeout: 1h + script: + - yarn + - (cd example/ios && RCT_NEW_ARCH_ENABLED=0 pod install --repo-update) + - set -o pipefail && xcodebuild -workspace example/ios/DdSdkReactNativeExample.xcworkspace -scheme DatadogSDKReactNativeProfiling test -destination "$IOS_SIMULATOR_DESTINATION" -derivedDataPath "$IOS_DERIVED_DATA_PATH" | xcbeautify + test:native-ios-newarch: tags: ['macos:sequoia-arm64', 'specific:true'] stage: test diff --git a/bump-native-dd-sdk.sh b/bump-native-dd-sdk.sh index fabaf9600..0e2c2efae 100755 --- a/bump-native-dd-sdk.sh +++ b/bump-native-dd-sdk.sh @@ -13,6 +13,7 @@ new_version="$2" build_gradle_files=( "packages/core/android/build.gradle" "packages/react-native-session-replay/android/build.gradle" + "packages/react-native-profiling/android/build.gradle" "packages/react-native-webview/android/build.gradle" "packages/internal-testing-tools/android/build.gradle" "benchmarks/android/app/build.gradle" @@ -21,6 +22,7 @@ build_gradle_files=( podspec_files=( "packages/core/DatadogSDKReactNative.podspec" "packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" + "packages/react-native-profiling/DatadogSDKReactNativeProfiling.podspec" "packages/react-native-webview/DatadogSDKReactNativeWebView.podspec" ) diff --git a/example-new-architecture/App.tsx b/example-new-architecture/App.tsx index f753110f7..b331300aa 100644 --- a/example-new-architecture/App.tsx +++ b/example-new-architecture/App.tsx @@ -39,6 +39,7 @@ import { LearnMoreLinks, ReloadInstructions, } from 'react-native/Libraries/NewAppScreen'; +import {DdProfiling} from '@datadog/mobile-react-native-profiling'; // @ts-ignore import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; @@ -73,6 +74,11 @@ import {APPLICATION_ID, CLIENT_TOKEN, ENVIRONMENT} from './ddCredentials'; // Initialize the Datadog SDK. await DdSdkReactNative.initialize(config); + await DdProfiling.enable({ + applicationLaunchSampleRate: 100, + continuousSampleRate: 100, + }); + // Enable Datadog Flags feature. await DdFlags.enable(); diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index a07eb2d20..2128f8a05 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -11,6 +11,8 @@ PODS: - DatadogInternal (3.16.0) - DatadogLogs (3.16.0): - DatadogInternal (= 3.16.0) + - DatadogProfiling (3.16.0): + - DatadogInternal (= 3.16.0) - DatadogRUM (3.16.0): - DatadogInternal (= 3.16.0) - DatadogSDKReactNative (3.9.0): @@ -69,6 +71,29 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - DatadogSDKReactNativeProfiling (3.9.0): + - DatadogProfiling (= 3.16.0) + - DatadogSDKReactNative + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DatadogTrace (3.16.0): - DatadogInternal (= 3.16.0) - OpenTelemetry-Swift-Api (~> 2.5.0) @@ -1651,6 +1676,7 @@ 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`) + - "DatadogSDKReactNativeProfiling (from `../node_modules/@datadog/mobile-react-native-profiling`)" - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -1724,6 +1750,7 @@ SPEC REPOS: - DatadogFlags - DatadogInternal - DatadogLogs + - DatadogProfiling - DatadogRUM - DatadogTrace - DatadogWebViewTracking @@ -1736,6 +1763,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" + DatadogSDKReactNativeProfiling: + :path: "../node_modules/@datadog/mobile-react-native-profiling" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -1871,8 +1900,10 @@ SPEC CHECKSUMS: DatadogFlags: 033cf48e769c271843e77068c015519880011866 DatadogInternal: e5a4652ee266986e29c6b8491aff7f78dc3b731d DatadogLogs: 52792add5827042f619771a23656e2bfb6b0f4e7 + DatadogProfiling: 60f18d5cca882178bf89ec4e6d0e539d18062e9d DatadogRUM: b78d608e013f39c5679ed31ad97dad323c6113e8 DatadogSDKReactNative: a4037e006d9dda1073be1d3b8a411461899d3181 + DatadogSDKReactNativeProfiling: ca98911adbc8eac6613f73be27fc3b87e104dbfb DatadogTrace: 5ef84e254e2e1d337bc52e0cdf16396ca102aeb1 DatadogWebViewTracking: e0470157f55a83680a549d79ba2985e0d17465db DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 diff --git a/example-new-architecture/package.json b/example-new-architecture/package.json index 83a990a29..b21767cca 100644 --- a/example-new-architecture/package.json +++ b/example-new-architecture/package.json @@ -10,6 +10,7 @@ "dependencies": { "@datadog/mobile-react-native": "workspace:packages/core", "@datadog/mobile-react-native-openfeature": "workspace:packages/react-native-openfeature", + "@datadog/mobile-react-native-profiling": "workspace:packages/react-native-profiling", "@openfeature/react-sdk": "^1.1.0", "react": "18.3.1", "react-native": "0.76.9" diff --git a/example/ios/Podfile b/example/ios/Podfile index 0c9182f55..9229dd357 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -19,6 +19,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'] + pod 'DatadogSDKReactNativeProfiling', :path => '../../packages/react-native-profiling/DatadogSDKReactNativeProfiling.podspec', :testspecs => ['Tests'] pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests'] config = use_native_modules! diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a50252c34..4e853301e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -11,6 +11,8 @@ PODS: - DatadogInternal (3.16.0) - DatadogLogs (3.16.0): - DatadogInternal (= 3.16.0) + - DatadogProfiling (3.16.0): + - DatadogInternal (= 3.16.0) - DatadogRUM (3.16.0): - DatadogInternal (= 3.16.0) - DatadogSDKReactNative (3.9.0): @@ -31,6 +33,14 @@ PODS: - DatadogTrace (= 3.16.0) - DatadogWebViewTracking (= 3.16.0) - React-Core + - DatadogSDKReactNativeProfiling (3.9.0): + - DatadogProfiling (= 3.16.0) + - DatadogSDKReactNative + - React-Core + - DatadogSDKReactNativeProfiling/Tests (3.9.0): + - DatadogProfiling (= 3.16.0) + - DatadogSDKReactNative + - React-Core - DatadogSDKReactNativeSessionReplay (3.9.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.16.0) @@ -1720,6 +1730,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`) + - DatadogSDKReactNativeProfiling (from `../../packages/react-native-profiling/DatadogSDKReactNativeProfiling.podspec`) + - DatadogSDKReactNativeProfiling/Tests (from `../../packages/react-native-profiling/DatadogSDKReactNativeProfiling.podspec`) - DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`) - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`) @@ -1804,6 +1816,7 @@ SPEC REPOS: - DatadogFlags - DatadogInternal - DatadogLogs + - DatadogProfiling - DatadogRUM - DatadogSessionReplay - DatadogTrace @@ -1818,6 +1831,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DatadogSDKReactNative: :path: "../../packages/core/DatadogSDKReactNative.podspec" + DatadogSDKReactNativeProfiling: + :path: "../../packages/react-native-profiling/DatadogSDKReactNativeProfiling.podspec" DatadogSDKReactNativeSessionReplay: :path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec" DatadogSDKReactNativeWebView: @@ -1971,8 +1986,10 @@ SPEC CHECKSUMS: DatadogFlags: 033cf48e769c271843e77068c015519880011866 DatadogInternal: e5a4652ee266986e29c6b8491aff7f78dc3b731d DatadogLogs: 52792add5827042f619771a23656e2bfb6b0f4e7 + DatadogProfiling: 60f18d5cca882178bf89ec4e6d0e539d18062e9d DatadogRUM: b78d608e013f39c5679ed31ad97dad323c6113e8 DatadogSDKReactNative: 9094f70c8bf4be4021a85876d78781a87164efc7 + DatadogSDKReactNativeProfiling: 62f83e20f0720f2f08bac96e0aef91397924eb12 DatadogSDKReactNativeSessionReplay: 6a33bb577187438cd2023414e7148f5159a963d0 DatadogSDKReactNativeWebView: 845b47fc1ac69aaca1a0179628b8ee9b14407a5e DatadogSessionReplay: 932b2077b1a5367de1450479d72093621b9fd618 @@ -2053,6 +2070,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: a2729cbf6c0a559932ef8f49d27110873b77ff7d +PODFILE CHECKSUM: 47138531a1c82b754b0b3598ba2f51c2f0c36642 COCOAPODS: 1.16.2 diff --git a/example/package.json b/example/package.json index 0fc34e3f2..e608abaa5 100644 --- a/example/package.json +++ b/example/package.json @@ -13,6 +13,7 @@ "@datadog/mobile-react-native": "workspace:packages/core", "@datadog/mobile-react-native-navigation": "workspace:packages/react-native-navigation", "@datadog/mobile-react-native-openfeature": "workspace:packages/react-native-openfeature", + "@datadog/mobile-react-native-profiling": "workspace:packages/react-native-profiling", "@datadog/mobile-react-native-session-replay": "workspace:packages/react-native-session-replay", "@datadog/mobile-react-native-webview": "workspace:packages/react-native-webview", "@datadog/mobile-react-navigation": "workspace:packages/react-navigation", diff --git a/example/src/App.tsx b/example/src/App.tsx index dc2f6acb5..6d61430b0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -16,6 +16,7 @@ import { TextAndInputPrivacyLevel, TouchPrivacyLevel, } from '@datadog/mobile-react-native-session-replay'; +import { DdProfiling } from '@datadog/mobile-react-native-profiling'; import { Route } from "@react-navigation/native"; import { NestedNavigator } from './screens/NestedNavigator/NestedNavigator'; @@ -89,6 +90,11 @@ const handleDatadogInitialization = async () => { enableHeatmaps: true, }); + await DdProfiling.enable({ + applicationLaunchSampleRate: 100, + continuousSampleRate: 100, + }); + // Enable Datadog Flags feature. await DdFlags.enable(); diff --git a/example/src/ddUtils.tsx b/example/src/ddUtils.tsx index 4933cec10..638a3ef5d 100644 --- a/example/src/ddUtils.tsx +++ b/example/src/ddUtils.tsx @@ -36,6 +36,13 @@ export function getDatadogConfig(trackingConsent: TrackingConsent) { trackErrors: true, sessionSampleRate: 100, nativeCrashReportEnabled: true, + // Report JavaScript long tasks. These are forwarded to the native RUM + // monitor, which is what continuous profiling correlates profiles against: + // a continuous profile is only uploaded if the profiling window captured a + // long task, an app hang/ANR, or a vital. Leaving this at its default of 0 + // disables JS long task reporting, so profiles would be collected and then + // discarded. See the "Trigger JS long task" button on the main screen. + longTaskThresholdMs: 200, firstPartyHosts: [{ match: "example.com", propagatorTypes: [PropagatorType.B3MULTI, PropagatorType.TRACECONTEXT] diff --git a/example/src/screens/MainScreen.tsx b/example/src/screens/MainScreen.tsx index 37f14b8b0..8a4ae4d38 100644 --- a/example/src/screens/MainScreen.tsx +++ b/example/src/screens/MainScreen.tsx @@ -26,6 +26,7 @@ interface MainScreenState { resultTouchableOpacityAction: string, resultTouchableWithoutFeedback: string, resultTouchableNativeFeedback: string, + resultLongTaskAction: string, trackingConsent: TrackingConsent, trackingConsentModalVisible: boolean } @@ -93,6 +94,25 @@ export default class MainScreen extends Component { .then((response) => response.data); } + /** + * Blocks the JavaScript thread synchronously, which the SDK reports as a JS long task + * (see `longTaskThresholdMs` in ddUtils.tsx). The long task is forwarded to the native + * RUM monitor, and continuous profiling attaches the in-flight profile to it: a + * continuous profile is only uploaded when its window captured a long task, an app + * hang/ANR, or a vital. Without an event like this, profiles are collected and dropped. + */ + triggerJsLongTask(durationMs: number = 500) { + const start = Date.now(); + // Busy-wait on purpose: yielding would let the JS thread breathe and no long task + // would be recorded. + while (Date.now() - start < durationMs) { + // no-op + } + this.setState({ + resultLongTaskAction: `Blocked the JS thread for ${durationMs}ms` + } as MainScreenState); + } + componentDidMount() { this.updateTrackingConsent() DdLogs.debug("[DATADOG SDK] Test React Native Debug Log"); @@ -203,6 +223,12 @@ export default class MainScreen extends Component { Click me + {this.state.resultLongTaskAction} +