diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d1a506cbe..f1013691a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -13,11 +13,11 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: | - This issue has been automatically marked as stale because it has not had recent activity and has the `awaiting-response` label. + This issue has been automatically marked as stale because it has not had recent activity and has the `awaiting response` label. It will be closed if no further activity occurs within 3 days. @@ -25,7 +25,7 @@ jobs: stale-issue-label: 'stale' # The label that will be added to the issues when automatically marked as stale close-issue-label: 'automatically closed' - # Only target issues with 'awaiting response' label + # Only target issues with 'awaiting-response' label only-labels: 'awaiting response' # Mark issues as stale after 14 days days-before-issue-stale: 14 @@ -33,6 +33,8 @@ jobs: days-before-issue-close: 3 # Automatically remove the stale label when the issues or the pull requests are updated remove-stale-when-updated: true + # Specify the reason used when closing issues: `completed` or `not_planned` + close-issue-reason: completed # Run the stale workflow as dry-run. # No GitHub API calls that can alter the issues and pull requests will happen. # Useful to debug or when you want to configure the stale workflow safely. diff --git a/NATIVE_SDK_VERSIONS.md b/NATIVE_SDK_VERSIONS.md index 678affc0e..6ebb13627 100644 --- a/NATIVE_SDK_VERSIONS.md +++ b/NATIVE_SDK_VERSIONS.md @@ -1,5 +1,6 @@ | React Native | iOS Bridge / iOS SDK | Android Bridge / Android SDK | |-------------|---------------------|-----------------------------| +| 2.14.0 | 2.30.2 | 2.26.2 | | 2.13.2 | 2.30.2 | 2.26.2 | | 2.13.1 | 2.30.2 | 2.26.2 | | 2.13.0 | 2.30.2 | 2.26.2 | diff --git a/benchmarks/ios/Podfile.lock b/benchmarks/ios/Podfile.lock index 4651f040a..5be948cb8 100644 --- a/benchmarks/ios/Podfile.lock +++ b/benchmarks/ios/Podfile.lock @@ -1,22 +1,22 @@ PODS: - boost (1.84.0) - - DatadogCore (3.3.0): - - DatadogInternal (= 3.3.0) - - DatadogCrashReporting (3.3.0): - - DatadogInternal (= 3.3.0) + - DatadogCore (3.4.0): + - DatadogInternal (= 3.4.0) + - DatadogCrashReporting (3.4.0): + - DatadogInternal (= 3.4.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (3.3.0) - - DatadogLogs (3.3.0): - - DatadogInternal (= 3.3.0) - - DatadogRUM (3.3.0): - - DatadogInternal (= 3.3.0) - - DatadogSDKReactNative (2.13.2): - - DatadogCore (= 3.3.0) - - DatadogCrashReporting (= 3.3.0) - - DatadogLogs (= 3.3.0) - - DatadogRUM (= 3.3.0) - - DatadogTrace (= 3.3.0) - - DatadogWebViewTracking (= 3.3.0) + - DatadogInternal (3.4.0) + - DatadogLogs (3.4.0): + - DatadogInternal (= 3.4.0) + - DatadogRUM (3.4.0): + - DatadogInternal (= 3.4.0) + - DatadogSDKReactNative (2.14.0): + - DatadogCore (= 3.4.0) + - DatadogCrashReporting (= 3.4.0) + - DatadogLogs (= 3.4.0) + - DatadogRUM (= 3.4.0) + - DatadogTrace (= 3.4.0) + - DatadogWebViewTracking (= 3.4.0) - DoubleConversion - glog - hermes-engine @@ -37,9 +37,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeSessionReplay (2.13.2): + - DatadogSDKReactNativeSessionReplay (2.14.0): - DatadogSDKReactNative - - DatadogSessionReplay (= 3.3.0) + - DatadogSessionReplay (= 3.4.0) - DoubleConversion - glog - hermes-engine @@ -60,10 +60,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.2): - - DatadogInternal (= 3.3.0) + - DatadogSDKReactNativeWebView (2.14.0): + - DatadogInternal (= 3.4.0) - DatadogSDKReactNative - - DatadogWebViewTracking (= 3.3.0) + - DatadogWebViewTracking (= 3.4.0) - DoubleConversion - glog - hermes-engine @@ -84,13 +84,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSessionReplay (3.3.0): - - DatadogInternal (= 3.3.0) - - DatadogTrace (3.3.0): - - DatadogInternal (= 3.3.0) + - DatadogSessionReplay (3.4.0): + - DatadogInternal (= 3.4.0) + - DatadogTrace (3.4.0): + - DatadogInternal (= 3.4.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (3.3.0): - - DatadogInternal (= 3.3.0) + - DatadogWebViewTracking (3.4.0): + - DatadogInternal (= 3.4.0) - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.78.2) @@ -2070,17 +2070,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DatadogCore: 9b1256ac9c27a07087d6214c8546acf756e40be7 - DatadogCrashReporting: 89a00886ef40808bffb8ccb4b6531e472f52e213 - DatadogInternal: 21dac5a7db548da6368a096d0714bdbec66deb6c - DatadogLogs: 355a4ac6bce3f0cb8231819e475c03dbbdd7957c - DatadogRUM: 1b3a47a9b9a5a25890f7fb3aa1f2bd86009d1086 - DatadogSDKReactNative: b364ddf133b4d774f3f7bfb5fc98232f960f5331 - DatadogSDKReactNativeSessionReplay: 73b5b7d46abe2ea8ffcaccb0c6232e49c0e27591 - DatadogSDKReactNativeWebView: 0310cc142fb39e185112e79f196f99f856a96c31 - DatadogSessionReplay: 85e63d3c5e5618c3029726d00595750a73c0920a - DatadogTrace: f13e8c09981787d6cb0a4b7fd1991351fab6d64b - DatadogWebViewTracking: 08fe084b5f57da05c1610fab49ce7bc84226141e + DatadogCore: 8c384b6338c49534e43fdf7f9a0508b62bf1d426 + DatadogCrashReporting: 103bfb4077db2ccee1846f71e53712972732d3b7 + DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc + DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a + DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f + DatadogSDKReactNative: 92d5aa9086ff036530cb78c5439aeee24de93e34 + DatadogSDKReactNativeSessionReplay: ccfc4d275a97d516ebcb643d03f86210db2380a8 + DatadogSDKReactNativeWebView: 4cccdf90e3bb877f5d79eb688a28aad7f74ff4cf + DatadogSessionReplay: 462a3a2e39e9e2193528cf572c8d1acfd6cdace1 + DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980 + DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: e32d34492c519a2194ec9d7f5e7a79d11b73f91c diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 8c3eede06..1e33a803d 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.4.0) - DatadogRUM (3.4.0): - DatadogInternal (= 3.4.0) - - DatadogSDKReactNative (2.13.2): + - DatadogSDKReactNative (2.14.0): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) - DatadogLogs (= 3.4.0) @@ -37,7 +37,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNative/Tests (2.13.2): + - DatadogSDKReactNative/Tests (2.14.0): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) - DatadogLogs (= 3.4.0) @@ -1855,7 +1855,7 @@ SPEC CHECKSUMS: DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f - DatadogSDKReactNative: 02cd4d00e3eecdc8ac57042db50b921054bbe709 + DatadogSDKReactNative: 8bfd30eecba2ecc242140b7b4319d5f49be5a179 DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980 DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -1866,62 +1866,62 @@ SPEC CHECKSUMS: hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: 814830ccbc3fb08d67d0190e63b179ee4098c67b - React-domnativemodule: 270acf94bd0960b026bc3bfb327e703665d27fb4 - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 18a684542f82ce1897552a1c4b847be414c9566e + React-domnativemodule: 90bdd4ec3ab38c47cfc3461c1e9283a8507d613f + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 0d7091ae344d6160c0557048e127897654a5c00f - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 6b84add48971da9c40403bd1860d4896462590f2 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: a645237a841d733861c70b69908ab4a1707b52ad + React-featureflagsnativemodule: 742a8325b3c821d2a1ca13a6d2a0fc72d04555e0 + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: d61d9c9816131bf70d3d80cd04889fc625ee523f + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 1774aa421a29a41a704ecaf789811ef73c4634b6 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 731cda82aed592aacce2d32ead69d78cde5d9274 - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 345a6f1b82abc578437df0ce7e9c48740eca827c + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: 007b1a98201cc49b5bc6e1417d7fe3f6fc6e2b78 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f302e30e8..3c07476b2 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -10,7 +10,7 @@ PODS: - DatadogInternal (= 3.4.0) - DatadogRUM (3.4.0): - DatadogInternal (= 3.4.0) - - DatadogSDKReactNative (2.13.2): + - DatadogSDKReactNative (2.14.0): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) - DatadogLogs (= 3.4.0) @@ -18,7 +18,7 @@ PODS: - DatadogTrace (= 3.4.0) - DatadogWebViewTracking (= 3.4.0) - React-Core - - DatadogSDKReactNative/Tests (2.13.2): + - DatadogSDKReactNative/Tests (2.14.0): - DatadogCore (= 3.4.0) - DatadogCrashReporting (= 3.4.0) - DatadogLogs (= 3.4.0) @@ -26,7 +26,7 @@ PODS: - DatadogTrace (= 3.4.0) - DatadogWebViewTracking (= 3.4.0) - React-Core - - DatadogSDKReactNativeSessionReplay (2.13.2): + - DatadogSDKReactNativeSessionReplay (2.14.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.4.0) - DoubleConversion @@ -49,7 +49,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeSessionReplay/Tests (2.13.2): + - DatadogSDKReactNativeSessionReplay/Tests (2.14.0): - DatadogSDKReactNative - DatadogSessionReplay (= 3.4.0) - DoubleConversion @@ -73,12 +73,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - DatadogSDKReactNativeWebView (2.13.2): + - DatadogSDKReactNativeWebView (2.14.0): - DatadogInternal (= 3.4.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.4.0) - React-Core - - DatadogSDKReactNativeWebView/Tests (2.13.2): + - DatadogSDKReactNativeWebView/Tests (2.14.0): - DatadogInternal (= 3.4.0) - DatadogSDKReactNative - DatadogWebViewTracking (= 3.4.0) @@ -1993,9 +1993,9 @@ SPEC CHECKSUMS: DatadogInternal: b0372935ad8dde5ad06960fe8d88c39b2cc92bcc DatadogLogs: 484bb1bfe0c9a7cb2a7d9733f61614e8ea7b2f3a DatadogRUM: 00069b27918e0ce4a9223b87b4bfa7929d6a0a1f - DatadogSDKReactNative: 2d02290e38b30c2f118555ac81f6d767dfd0c95a - DatadogSDKReactNativeSessionReplay: fcbd7cf17dc515949607e112c56374354c8d08ef - DatadogSDKReactNativeWebView: 24fefd471c18d13d950a239a51712c830bf6bf7a + DatadogSDKReactNative: 177767e5521ac53b759baef08e74f218231f2d35 + DatadogSDKReactNativeSessionReplay: 7f69610ddcd542ad0b1652cad3cf2a969b32335a + DatadogSDKReactNativeWebView: 87b2c59fc708a79c74a9b7730de4ea812f470e80 DatadogSessionReplay: 462a3a2e39e9e2193528cf572c8d1acfd6cdace1 DatadogTrace: 852cb80f9370eb1321eb30a73c82c8e3d9e4e980 DatadogWebViewTracking: 32dfeaf7aad47a605a689ed12e0d21ee8eb56141 @@ -2008,69 +2008,69 @@ SPEC CHECKSUMS: HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 - RCT-Folly: 7b4f73a92ad9571b9dbdb05bb30fad927fa971e1 + RCT-Folly: ea9d9256ba7f9322ef911169a9f696e5857b9e17 RCTDeprecation: ebe712bb05077934b16c6bf25228bdec34b64f83 RCTRequired: ca91e5dd26b64f577b528044c962baf171c6b716 RCTTypeSafety: e7678bd60850ca5a41df9b8dc7154638cb66871f React: 4641770499c39f45d4e7cde1eba30e081f9d8a3d React-callinvoker: 4bef67b5c7f3f68db5929ab6a4d44b8a002998ea - React-Core: 0a06707a0b34982efc4a556aff5dae4b22863455 - React-CoreModules: 907334e94314189c2e5eed4877f3efe7b26d85b0 - React-cxxreact: 3a1d5e8f4faa5e09be26614e9c8bbcae8d11b73d + React-Core: a68cea3e762814e60ecc3fa521c7f14c36c99245 + React-CoreModules: d81b1eaf8066add66299bab9d23c9f00c9484c7c + React-cxxreact: 984f8b1feeca37181d4e95301fcd6f5f6501c6ab React-debug: 817160c07dc8d24d020fbd1eac7b3558ffc08964 - React-defaultsnativemodule: a965cb39fb0a79276ab611793d39f52e59a9a851 - React-domnativemodule: d647f94e503c62c44f54291334b1aa22a30fa08b - React-Fabric: 64586dc191fc1c170372a638b8e722e4f1d0a09b - React-FabricComponents: b0ebd032387468ea700574c581b139f57a7497fb - React-FabricImage: 81f0e0794caf25ad1224fa406d288fbc1986607f + React-defaultsnativemodule: 21f216e8db975897eb32b5f13247f5bbfaa97f41 + React-domnativemodule: 19270ad4b8d33312838d257f24731a0026809d49 + React-Fabric: f6dade7007533daeb785ba5925039d83f343be4b + React-FabricComponents: b0655cc3e1b5ae12a4a1119aa7d8308f0ad33520 + React-FabricImage: 9b157c4c01ac2bf433f834f0e1e5fe234113a576 React-featureflags: f2792b067a351d86fdc7bec23db3b9a2f2c8d26c - React-featureflagsnativemodule: 95a02d895475de8ace78fedd76143866838bb720 - React-graphics: cbebe910e4a15b65b0bff94a4d3ed278894d6386 - React-hermes: ec18c10f5a69d49fb9b5e17ae95494e9ea13d4d3 - React-idlecallbacksnativemodule: 0c1ae840cc5587197cd926a3cb76828ad059d116 - React-ImageManager: f2a4c01c2ccb2193e60a20c135da74c7ca4d36f2 - React-jserrorhandler: 61d205b5a7cbc57fed3371dd7eed48c97f49fc64 - React-jsi: 95f7676103137861b79b0f319467627bcfa629ee - React-jsiexecutor: 41e0fe87cda9ea3970ffb872ef10f1ff8dbd1932 - React-jsinspector: 15578208796723e5c6f39069b6e8bf36863ef6e2 - React-jsitracing: 3758cdb155ea7711f0e77952572ea62d90c69f0b - React-logger: dbca7bdfd4aa5ef69431362bde6b36d49403cb20 - React-Mapbuffer: 6efad4a606c1fae7e4a93385ee096681ef0300dc - React-microtasksnativemodule: 8732b71aa66045da4bb341ddee1bb539f71e5f38 - react-native-crash-tester: 3ffaa64141427ca362079cb53559fe9a532487ae - react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 - react-native-webview: 1e12de2fad74c17b4f8b1b53ebd1e3baa0148d71 + React-featureflagsnativemodule: 3a8731d8fd9f755be57e00d9fa8a7f92aa77e87d + React-graphics: 68969e4e49d73f89da7abef4116c9b5f466aa121 + React-hermes: ac0bcba26a5d288ebc99b500e1097da2d0297ddf + React-idlecallbacksnativemodule: 9a2c5b5c174c0c476f039bedc1b9497a8272133e + React-ImageManager: e906eec93a9eb6102a06576b89d48d80a4683020 + React-jserrorhandler: ac5dde01104ff444e043cad8f574ca02756e20d6 + React-jsi: 496fa2b9d63b726aeb07d0ac800064617d71211d + React-jsiexecutor: dd22ab48371b80f37a0a30d0e8915b6d0f43a893 + React-jsinspector: 4629ac376f5765e684d19064f2093e55c97fd086 + React-jsitracing: 7a1c9cd484248870cf660733cd3b8114d54c035f + React-logger: c4052eb941cca9a097ef01b59543a656dc088559 + React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de + React-microtasksnativemodule: 5c3d795318c22ab8df55100e50b151384a4a60b3 + react-native-crash-tester: 48bde9d6f5256c61ef2e0c52dfc74256b26e55eb + react-native-safe-area-context: e134b241010ebe2aacdcea013565963d13826faa + react-native-webview: 2ea635bc43fd8a4b89de61133e8cc0607084e9f8 React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 - React-NativeModulesApple: 958d4f6c5c2ace4c0f427cf7ef82e28ae6538a22 - React-perflogger: 9b4f13c0afe56bc7b4a0e93ec74b1150421ee22d - React-performancetimeline: 359db1cb889aa0282fafc5838331b0987c4915a9 + React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e + React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 + React-performancetimeline: cd6a9374a72001165995d2ab632f672df04076dc React-RCTActionSheet: aacf2375084dea6e7c221f4a727e579f732ff342 - React-RCTAnimation: d8c82deebebe3aaf7a843affac1b57cb2dc073d4 - React-RCTAppDelegate: 6c0377d9c4058773ea7073bb34bb9ebd6ddf5a84 - React-RCTBlob: 70a58c11a6a3500d1a12f2e51ca4f6c99babcff8 - React-RCTFabric: 7eb6dd2c8fda98cb860a572e3f4e4eb60d62c89e - React-RCTImage: 5e9d655ba6a790c31e3176016f9b47fd0978fbf0 - React-RCTLinking: 2a48338252805091f7521eaf92687206401bdf2a - React-RCTNetwork: 0c1282b377257f6b1c81934f72d8a1d0c010e4c3 - React-RCTSettings: f757b679a74e5962be64ea08d7865a7debd67b40 - React-RCTText: e7d20c490b407d3b4a2daa48db4bcd8ec1032af2 - React-RCTVibration: 8228e37144ca3122a91f1de16ba8e0707159cfec + React-RCTAnimation: 395ab53fd064dff81507c15efb781c8684d9a585 + React-RCTAppDelegate: 1e5b43833e3e36e9fa34eec20be98174bc0e14a2 + React-RCTBlob: 13311e554c1a367de063c10ee7c5e6573b2dd1d6 + React-RCTFabric: bd906861a4e971e21d8df496c2d8f3ca6956f840 + React-RCTImage: 1b1f914bcc12187c49ba5d949dac38c2eb9f5cc8 + React-RCTLinking: 4ac7c42beb65e36fba0376f3498f3cd8dd0be7fa + React-RCTNetwork: 938902773add4381e84426a7aa17a2414f5f94f7 + React-RCTSettings: e848f1ba17a7a18479cf5a31d28145f567da8223 + React-RCTText: 7e98fafdde7d29e888b80f0b35544e0cb07913cf + React-RCTVibration: cd7d80affd97dc7afa62f9acd491419558b64b78 React-rendererconsistency: b4917053ecbaa91469c67a4319701c9dc0d40be6 - React-rendererdebug: 81becbc8852b38d9b1b68672aa504556481330d5 + React-rendererdebug: aa181c36dd6cf5b35511d1ed875d6638fd38f0ec React-rncore: 120d21715c9b4ba8f798bffe986cb769b988dd74 - React-RuntimeApple: 52ed0e9e84a7c2607a901149fb13599a3c057655 - React-RuntimeCore: ca6189d2e53d86db826e2673fe8af6571b8be157 + React-RuntimeApple: d033becbbd1eba6f9f6e3af6f1893030ce203edd + React-RuntimeCore: 38af280bb678e66ba000a3c3d42920b2a138eebb React-runtimeexecutor: 877596f82f5632d073e121cba2d2084b76a76899 - React-RuntimeHermes: 3b752dc5d8a1661c9d1687391d6d96acfa385549 - React-runtimescheduler: 8321bb09175ace2a4f0b3e3834637eb85bf42ebe + React-RuntimeHermes: 37aad735ff21ca6de2d8450a96de1afe9f86c385 + React-runtimescheduler: 8ec34cc885281a34696ea16c4fd86892d631f38d React-timing: 331cbf9f2668c67faddfd2e46bb7f41cbd9320b9 - React-utils: 54df9ada708578c8ad40d92895d6fed03e0e8a9e - ReactCodegen: 21a52ccddc6479448fc91903a437dd23ddc7366c - ReactCommon: bfd3600989d79bc3acbe7704161b171a1480b9fd - ReactNativeNavigation: 50c1eef68b821e7265eff3a391d27ed18fdce459 - RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996 - RNGestureHandler: 950dfa674dbf481460ca389c65b9036ac4ab8ada - RNScreens: 606ab1cf68162f7ba0d049a31f2a84089a6fffb4 + React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f + ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b + ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 + ReactNativeNavigation: 445f86273eb245d15b14023ee4ef9d6e4f891ad6 + RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f + RNGestureHandler: cb711d56ee3b03a5adea1d38324d4459ab55653f + RNScreens: f75b26fd4777848c216e27b0a09e1bf9c9f4760a SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a diff --git a/lerna.json b/lerna.json index 25572f225..08e58b2d8 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "npmClient": "yarn", - "version": "2.13.2", + "version": "2.14.0", "packages": [ "packages/*" ], diff --git a/package.json b/package.json index 4d77a4ac4..807253ffd 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "snyk": "^1.1298.3", "form-data": "4.0.4", "on-headers": "1.1.0", - "tmp": "0.2.4" + "tmp": "0.2.4", + "jws": "^4.0.1" } } diff --git a/packages/codepush/package.json b/packages/codepush/package.json index b52a8f017..0dbcc5b45 100644 --- a/packages/codepush/package.json +++ b/packages/codepush/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native-code-push", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to interact with Appcenter Codepush and Datadog", "keywords": [ "datadog", @@ -38,7 +38,7 @@ "prepare": "rm -rf lib && yarn bob build" }, "devDependencies": { - "@datadog/mobile-react-native": "workspace:2.13.2", + "@datadog/mobile-react-native": "workspace:2.14.0", "@testing-library/react-native": "7.0.2", "react-native-builder-bob": "0.26.0", "react-native-code-push": "7.1.0" diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt index 9d46bb1df..cf0154af7 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkImplementation.kt @@ -351,7 +351,6 @@ class DdSdkImplementation( } } - /** * Normalizes frameTime values so when are turned into FPS metrics they are normalized on a range of zero to 60fps. * @param frameTimeSeconds: the frame time to normalize. In seconds. 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 f1f47e06c..3c4be8539 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 @@ -7,4 +7,4 @@ package com.datadog.reactnative // This is automatically updated by the update-version.sh script -internal const val SDK_VERSION = "2.13.2" +internal const val SDK_VERSION = "2.14.0" diff --git a/packages/core/ios/Sources/DatadogSDKWrapper.swift b/packages/core/ios/Sources/DatadogSDKWrapper.swift index 894f0a09f..b63d47f10 100644 --- a/packages/core/ios/Sources/DatadogSDKWrapper.swift +++ b/packages/core/ios/Sources/DatadogSDKWrapper.swift @@ -4,10 +4,9 @@ * Copyright 2016-Present Datadog, Inc. */ - import DatadogCore -import DatadogRUM import DatadogLogs +import DatadogRUM import DatadogTrace import DatadogCrashReporting import DatadogInternal @@ -32,7 +31,7 @@ public class DatadogSDKWrapper { private init() { } - public func addOnSdkInitializedListener(listener:@escaping OnSdkInitializedListener) { + public func addOnSdkInitializedListener(listener: @escaping OnSdkInitializedListener) { onSdkInitializedListeners.append(listener) } @@ -64,5 +63,3 @@ public class DatadogSDKWrapper { } #endif } - - diff --git a/packages/core/ios/Sources/SdkVersion.swift b/packages/core/ios/Sources/SdkVersion.swift index 0d6ed2d5b..c71ba5440 100644 --- a/packages/core/ios/Sources/SdkVersion.swift +++ b/packages/core/ios/Sources/SdkVersion.swift @@ -7,4 +7,4 @@ import Foundation // This is automatically updated by the update-version.sh script -let SdkVersion = "2.13.2" +let SdkVersion = "2.14.0" diff --git a/packages/core/package.json b/packages/core/package.json index 56818b180..7e47b7244 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to interact with Datadog", "keywords": [ "datadog", diff --git a/packages/core/src/logs/DdLogs.ts b/packages/core/src/logs/DdLogs.ts index 9b99dc5ee..f00cbfdfa 100644 --- a/packages/core/src/logs/DdLogs.ts +++ b/packages/core/src/logs/DdLogs.ts @@ -7,6 +7,7 @@ import { DdAttributes } from '../DdAttributes'; import { DATADOG_MESSAGE_PREFIX, InternalLog } from '../InternalLog'; import { SdkVerbosity } from '../SdkVerbosity'; +import { debugId } from '../metro/debugIdResolver'; import type { DdNativeLogsType } from '../nativeModulesTypes'; import { encodeAttributes } from '../sdk/AttributesEncoding/attributesEncoding'; import type { ErrorSource, LogEventMapper } from '../types'; @@ -217,6 +218,11 @@ class DdLogsWrapper implements DdLogsType { updatedContext[DdAttributes.errorFingerprint] = fingerprint; } + const _debugId = debugId; + if (_debugId) { + updatedContext[DdAttributes.debugId] = _debugId; + } + return await this.nativeLogs[`${status}WithError`]( mappedEvent.message, (mappedEvent as NativeLogWithError).errorKind, diff --git a/packages/core/src/rum/DdRum.ts b/packages/core/src/rum/DdRum.ts index e4204f55a..36f116bbf 100644 --- a/packages/core/src/rum/DdRum.ts +++ b/packages/core/src/rum/DdRum.ts @@ -206,6 +206,7 @@ class DdRumWrapper implements DdRumType { if (!mappedEvent) { return generateEmptyPromise(); } + InternalLog.log( `Adding RUM Action “${name}” (${type})`, SdkVerbosity.DEBUG diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 00d00d874..f52edffe8 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,2 +1,2 @@ // generated by genversion -export const version = '2.13.2'; +export const version = '2.14.0'; diff --git a/packages/internal-testing-tools/package.json b/packages/internal-testing-tools/package.json index 7d647fa0a..bebc02165 100644 --- a/packages/internal-testing-tools/package.json +++ b/packages/internal-testing-tools/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/react-native-internal-testing-tools", - "version": "2.13.2", + "version": "2.14.0", "description": "Internal tools for testing the Datadog React Native SDK.", "keywords": [ "datadog", diff --git a/packages/react-native-apollo-client/package.json b/packages/react-native-apollo-client/package.json index f7dfa6095..c8814a0ce 100644 --- a/packages/react-native-apollo-client/package.json +++ b/packages/react-native-apollo-client/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native-apollo-client", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to interact with Apollo Client and Datadog", "keywords": [ "datadog", diff --git a/packages/react-native-babel-plugin/package.json b/packages/react-native-babel-plugin/package.json index 17507d192..83287d9e9 100644 --- a/packages/react-native-babel-plugin/package.json +++ b/packages/react-native-babel-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native-babel-plugin", - "version": "2.13.2", + "version": "2.14.0", "description": "A Babel plugin that enhances Datadog's React Native SDK by automatically enriching React components with contextual metadata.", "keywords": [ "babel", diff --git a/packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts b/packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts index c94485c6d..c6cffe00e 100644 --- a/packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts +++ b/packages/react-native-babel-plugin/src/cli/generate-sr-assets.ts @@ -15,6 +15,7 @@ import { clearAssetsDir, getAssetsPath } from '../libraries/react-native-svg/processing/fs'; +import { ReactNativeSVG } from '../libraries/react-native-svg'; type SvgIndexEntry = { offset: number; @@ -23,6 +24,182 @@ type SvgIndexEntry = { type SvgIndex = Record; +export type CliOptions = { + ignore: string[]; + verbose: boolean; + path: string | null; + followSymlinks: boolean; +}; + +/** + * Converts a user-provided ignore pattern to a glob pattern. + * + * Handling rules: + * - Glob patterns (containing * ? [ ] { } ( )) are left unchanged + * - Absolute paths are appended with /** if not already present + * - Relative paths (./ or ../ or containing path separator) are resolved to absolute and appended with /** + * - Simple folder names (no slashes, no globs) are wrapped with ** /name/** for wildcard matching + * + * @param pattern - The user-provided pattern + * @param cwd - The base directory for resolving relative paths (required) + * @returns A glob-compatible pattern + */ +export function normalizeIgnorePattern(pattern: string, cwd: string): string { + // Detect glob patterns - leave unchanged + const isGlob = /[*?[\]{}()]/.test(pattern); + if (isGlob) { + return pattern; + } + + const isAbsolute = path.isAbsolute(pattern); + const isRelative = + pattern.startsWith('./') || + pattern.startsWith('../') || + pattern.startsWith('.\\') || + pattern.startsWith('..\\') || + pattern.includes(path.sep) || + pattern.includes('/'); // Also check for forward slash on all platforms + + // Absolute paths - append /** if needed + if (isAbsolute) { + if (pattern.endsWith('/**') || pattern.endsWith(`${path.sep}**`)) { + return pattern; + } + return path.join(pattern, '**'); + } + + // Relative paths - resolve to absolute and append /** + if (isRelative) { + const resolved = path.resolve(cwd, pattern); + + if (resolved.endsWith('/**') || resolved.endsWith(`${path.sep}**`)) { + return resolved; + } + + return path.join(resolved, '**'); + } + + // Simple folder names (no slashes, no glob characters) - wildcard match + return `**/${pattern}/**`; +} + +export const DEFAULT_IGNORE_PATTERNS = [ + // Dependencies and build output + '**/node_modules/**', + '**/lib/**', + '**/dist/**', + '**/build/**', + '**/vendor/**', + // Native code (not React components) + '**/ios/**', + '**/android/**', + '**/Pods/**', + // Expo + '**/.expo/**', + // Cache and metadata + '**/.git/**', + '**/.cache/**', + '**/.yarn/**', + // TypeScript declarations + '**/*.d.ts', + // Test files and directories + '**/*.test.*', + '**/*.spec.*', + '**/__tests__/**', + '**/__mocks__/**', + '**/__snapshots__/**', + '**/coverage/**', + // Config files + '**/*.config.js', + '**/*.config.ts' +]; + +/** + * Parses command line arguments for the generate-sr-assets CLI. + * + * Supported arguments: + * --ignore Additional glob patterns to ignore (can be specified multiple times) + * --verbose, -v Enable verbose output for debugging + * --path, -p Path to the root directory to scan (defaults to current working directory) + * --followSymlinks Follow symbolic links during traversal (default: false) + * --help, -h Show help message + * + * @param args - Optional array of arguments (defaults to process.argv.slice(2)) + * @returns Parsed CLI options + */ +export function parseCliArgs(args?: string[]): CliOptions { + const argv = args ?? process.argv.slice(2); + const options: CliOptions = { + ignore: [], + verbose: false, + path: null, + followSymlinks: false + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + + if (arg === '--help' || arg === '-h') { + printHelp(); + process.exit(0); + } else if (arg === '--ignore' || arg === '-i') { + const value = argv[++i]; + if (value && !value.startsWith('-')) { + options.ignore.push(value); + } else { + console.warn( + 'Warning: --ignore flag requires a pattern argument' + ); + i--; // Reprocess this arg if it's another flag + } + } else if (arg === '--verbose' || arg === '-v') { + options.verbose = true; + } else if (arg === '--path' || arg === '-p') { + const value = argv[++i]; + if (value && !value.startsWith('-')) { + options.path = value; + } else { + console.warn('Warning: --path flag requires a directory path'); + i--; // Reprocess this arg if it's another flag + } + } else if (arg === '--followSymlinks') { + options.followSymlinks = true; + } + } + + return options; +} + +/** + * Prints the help message for the CLI. + */ +function printHelp(): void { + console.info(` +Usage: npx datadog-generate-sr-assets [options] + +Pre-generate SVG assets for Datadog Session Replay. + +Options: + --ignore, -i Additional patterns to ignore during scanning. + Can be a folder name or a glob pattern. + Folder names are auto-converted to glob patterns. + Can be specified multiple times. + --path, -p Path to the root directory to scan. + Defaults to the current working directory. + --verbose, -v Enable verbose output for debugging. + --followSymlinks Follow symbolic links during directory traversal. + Default: false (symlinks are ignored). + --help, -h Show this help message. + +Examples: + npx datadog-generate-sr-assets + npx datadog-generate-sr-assets --path ./src + npx datadog-generate-sr-assets --ignore legacy --ignore vendor + npx datadog-generate-sr-assets --ignore "**/custom-pattern/**" --verbose + npx datadog-generate-sr-assets -p ./src -i old-code -v +`); +} + /** * Merges all individual SVG files into assets.bin and creates an index in assets.json. * This function reads all .svg files from the assets directory and packs them into @@ -104,43 +281,96 @@ function mergeSvgAssets(assetsDir: string) { * references are available during the build process. * * Usage: - * npx @datadog/mobile-react-native-babel-plugin generate-sr-assets + * npx @datadog/mobile-react-native-babel-plugin generate-sr-assets [options] * or - * npx datadog-generate-sr-assets + * npx datadog-generate-sr-assets [options] + * + * Options: + * --ignore, -i pattern Additional glob patterns to ignore during scanning. + * Can be specified multiple times. + * Example: --ignore "**\/legacy\/**" --ignore "**\/vendor\/**" + * --verbose, -v Enable verbose output for debugging. + * --path, -p path Path to the root directory to scan. + * Defaults to the current working directory. + * Example: --path ./src + * --followSymlinks Follow symbolic links during directory traversal. + * Default: false (symlinks are ignored). */ function generateSessionReplayAssets() { - const rootDir = process.cwd(); + const cliOptions = parseCliArgs(); + const { verbose } = cliOptions; + + // Resolve the root directory from --path flag or default to cwd + const rootDir = cliOptions.path + ? path.resolve(process.cwd(), cliOptions.path) + : process.cwd(); + + // Validate the path exists + if (cliOptions.path && !fs.existsSync(rootDir)) { + console.error(`Error: Path does not exist: ${rootDir}`); + process.exit(1); + } + + if (cliOptions.path && !fs.statSync(rootDir).isDirectory()) { + console.error(`Error: Path is not a directory: ${rootDir}`); + process.exit(1); + } + const assetsPath = getAssetsPath(); + const startTime = Date.now(); + if (!assetsPath) { + if (verbose) { + console.info( + '[verbose] No assets path found. Session Replay module may not be installed.' + ); + } process.exit(0); } console.info(`Scanning for session replay assets in ${rootDir}...`); + if (verbose) { + console.info(`[verbose] Assets output path: ${assetsPath}`); + } + // Clear existing assets to ensure a fresh state clearAssetsDir(assetsPath); + // Merge default ignore patterns with user-provided ones + // Convert folder names/paths to glob patterns based on type: + // - Simple names: "legacy" → "**/legacy/**" + // - Relative paths: "./android" → "/abs/path/android/**" + // - Absolute paths: "/home/dev/android" → "/home/dev/android/**" + const userIgnorePatterns = cliOptions.ignore.map(pattern => + normalizeIgnorePattern(pattern, rootDir) + ); + const ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...userIgnorePatterns]; + + if (verbose) { + console.info(`[verbose] Follow symlinks: ${cliOptions.followSymlinks}`); + console.info('[verbose] Ignore patterns:'); + ignorePatterns.forEach(pattern => console.info(` - ${pattern}`)); + } + const files = glob.sync(['**/*.{js,jsx,ts,tsx}'], { cwd: rootDir, absolute: true, - ignore: [ - '**/node_modules/**', - '**/lib/**', - '**/dist/**', - '**/build/**', - '**/*.d.ts', - '**/*.test.*', - '**/*.spec.*', - '**/*.config.js', - '**/__tests__/**', - '**/__mocks__/**' - ] + ignore: ignorePatterns, + followSymbolicLinks: cliOptions.followSymlinks }); + if (verbose) { + console.info(`[verbose] Found ${files.length} files to scan`); + } + let errorCount = 0; + let processedCount = 0; const errors: Array<{ file: string; error: string }> = []; + const reactNativeSVG = new ReactNativeSVG(rootDir, assetsPath, true); + for (const file of files) { try { const code = fs.readFileSync(file, 'utf8'); @@ -155,7 +385,8 @@ function generateSessionReplayAssets() { sessionReplay: { svgTracking: true }, - __internal_saveSvgMapToDisk: true + __internal_saveSvgMapToDisk: true, + __internal_reactNativeSVG: reactNativeSVG } ] ], @@ -170,11 +401,19 @@ function generateSessionReplayAssets() { code: false, ast: false }); + + processedCount++; } catch (error) { errorCount++; const errorMessage = error instanceof Error ? error.message : String(error); errors.push({ file, error: errorMessage }); + + if (verbose) { + const relativePath = path.relative(rootDir, file); + console.warn(`[verbose] Error processing ${relativePath}:`); + console.warn(` ${errorMessage}`); + } } } @@ -185,6 +424,32 @@ function generateSessionReplayAssets() { // Merge all individual SVG files into assets.bin and assets.json mergeSvgAssets(assetsPath); + const duration = Date.now() - startTime; + + if (verbose) { + console.info('\n[verbose] Summary:'); + console.info(` Files scanned: ${files.length}`); + console.info(` Files processed successfully: ${processedCount}`); + console.info(` Files with errors: ${errorCount}`); + console.info(` Duration: ${duration}ms`); + + if (errors.length > 0 && errors.length <= 10) { + console.info('\n[verbose] Files with errors:'); + errors.forEach(({ file }) => { + const relativePath = path.relative(rootDir, file); + console.info(` - ${relativePath}`); + }); + } else if (errors.length > 10) { + console.info( + `\n[verbose] ${errors.length} files had errors (showing first 10):` + ); + errors.slice(0, 10).forEach(({ file }) => { + const relativePath = path.relative(rootDir, file); + console.info(` - ${relativePath}`); + }); + } + } + if (errorCount > 0) { console.info( 'Asset generation finished, but some files encountered errors.' @@ -194,5 +459,7 @@ function generateSessionReplayAssets() { console.info('Your assets are now ready to be used by Session Replay.'); } -// TODO: Add flag support [e.g., --verbose] (RUM-12186) -generateSessionReplayAssets(); +// Only run when executed directly (not when imported for testing) +if (require.main === module) { + generateSessionReplayAssets(); +} diff --git a/packages/react-native-babel-plugin/src/index.ts b/packages/react-native-babel-plugin/src/index.ts index 8379c600f..d9be0d57b 100644 --- a/packages/react-native-babel-plugin/src/index.ts +++ b/packages/react-native-babel-plugin/src/index.ts @@ -39,7 +39,7 @@ export default declare( } }; - let reactNativeSVG: ReactNativeSVG | null = null; + let reactNativeSVG: ReactNativeSVG | undefined; let assetsPath: string | null = null; @@ -53,14 +53,16 @@ export default declare( assetsPath = getAssetsPath(); } + reactNativeSVG = options.__internal_reactNativeSVG; if (!reactNativeSVG && assetsPath) { reactNativeSVG = new ReactNativeSVG( - api.types, process.cwd(), assetsPath, options.__internal_saveSvgMapToDisk || false ); + reactNativeSVG.buildSvgMap(); } + reactNativeSVG?.setApiTypes(api.types); }, visitor: { Program: { diff --git a/packages/react-native-babel-plugin/src/libraries/react-native-svg/handlers/RNSvgHandler.ts b/packages/react-native-babel-plugin/src/libraries/react-native-svg/handlers/RNSvgHandler.ts index d65c3cfdb..659fec676 100644 --- a/packages/react-native-babel-plugin/src/libraries/react-native-svg/handlers/RNSvgHandler.ts +++ b/packages/react-native-babel-plugin/src/libraries/react-native-svg/handlers/RNSvgHandler.ts @@ -106,9 +106,10 @@ export class RNSvgHandler implements SvgHandler { if (isJSXIdentifierOpen) { openingNode.name = convertAttributeCasing(openingNode.name); if (!svgElements.has(openingNode.name)) { - throw new Error( - `RNSvgHandler[transformElement]: Failed to transform element: "${openingNode.name}" is not supported` + console.warn( + `RNSvgHandler[transformElement]: Skipping unsupported element: "${openingNode.name}"` ); + return; // Skip unsupported elements instead of crashing } } diff --git a/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts b/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts index 1ef61ff28..325f9f2d9 100644 --- a/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts +++ b/packages/react-native-babel-plugin/src/libraries/react-native-svg/index.ts @@ -42,13 +42,16 @@ export class ReactNativeSVG { localSvgMap: Record = {}; + t: typeof Babel.types | null = null; + constructor( - private t: typeof Babel.types, private rootDir: string, private assetsPath: string, private saveSvgMapToDisk: boolean = false - ) { - this.buildSvgMap(); + ) {} + + setApiTypes(t: typeof Babel.types) { + this.t = t; } /** @@ -70,6 +73,10 @@ export class ReactNativeSVG { * after scanning. */ buildSvgMap() { + if (!this.t) { + return; + } + // If not saving to disk, try to load from existing svg-map.json first if (!this.saveSvgMapToDisk) { // Resolve to package root: from lib/commonjs/libraries/react-native-svg -> package root @@ -126,6 +133,9 @@ export class ReactNativeSVG { traverse(ast, { ImportDeclaration: path => { + if (!this.t) { + return; + } const source = path.node.source.value; if (!source.endsWith('.svg')) { return; @@ -145,6 +155,9 @@ export class ReactNativeSVG { } }, ExportNamedDeclaration: path => { + if (!this.t) { + return; + } const source = path.node.source?.value; if (!source?.endsWith('.svg')) { return; @@ -213,6 +226,10 @@ export class ReactNativeSVG { * or `undefined` if no transformation could be performed. */ processItem(path: Babel.NodePath, name: string) { + if (!this.t) { + return; + } + try { const dimensions: { width?: string; height?: string } = {}; @@ -236,41 +253,49 @@ export class ReactNativeSVG { const id = uuidv4(); - const optimized = output.startsWith('http') - ? output - : optimize(output, { - multipass: true, - plugins: ['preset-default'] - }).data; + try { + const optimized = output.startsWith('http') + ? output + : optimize(output, { + multipass: true, + plugins: ['preset-default'] + }).data; + + const hash = createHash('md5') + .update(optimized, 'utf8') + .digest('hex'); + + const wrapper = this.wrapElementForSessionReplay( + this.t, + path, + id, + hash, + dimensions + ); - const hash = createHash('md5') - .update(optimized, 'utf8') - .digest('hex'); + path.replaceWith(wrapper); - const wrapper = this.wrapElementForSessionReplay( - this.t, - path, - id, - hash, - dimensions - ); - path.replaceWith(wrapper); + path.node.extra = { + __wrappedForSR: true + }; - path.node.extra = { - __wrappedForSR: true - }; + this.svgMap[id] = { + file: optimized, + ...dimensions + }; - this.svgMap[id] = { - file: optimized, - ...dimensions - }; + writeAssetToDisk(this.assetsPath, id, hash, optimized); - writeAssetToDisk(this.assetsPath, id, hash, optimized); - - return { original: output, optimized }; - } catch (err) { - console.warn(err); - return { original: null, optimized: null }; + return { original: output, optimized }; + } catch (err) { + console.warn(err); + return { original: null, optimized: null }; + } + } catch (svgoError) { + console.warn( + 'ReactNativeSVG[processItem]: Skipping SVG with dynamic expressions (cannot be optimized)' + ); + return; } } diff --git a/packages/react-native-babel-plugin/src/types/general.ts b/packages/react-native-babel-plugin/src/types/general.ts index 0d17ff8c2..e9869e5b0 100644 --- a/packages/react-native-babel-plugin/src/types/general.ts +++ b/packages/react-native-babel-plugin/src/types/general.ts @@ -44,6 +44,7 @@ export type PluginOptions = { }; // Internal option used by CLI - not meant for end users __internal_saveSvgMapToDisk?: boolean; + __internal_reactNativeSVG?: ReactNativeSVG; }; export type PluginPassState = Babel.PluginPass & { diff --git a/packages/react-native-babel-plugin/test/generate-sr-assets.test.ts b/packages/react-native-babel-plugin/test/generate-sr-assets.test.ts new file mode 100644 index 000000000..973ff0579 --- /dev/null +++ b/packages/react-native-babel-plugin/test/generate-sr-assets.test.ts @@ -0,0 +1,391 @@ +/* + * 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. + */ + +import { + parseCliArgs, + DEFAULT_IGNORE_PATTERNS, + normalizeIgnorePattern +} from '../src/cli/generate-sr-assets'; + +describe('generate-sr-assets CLI', () => { + describe('parseCliArgs', () => { + describe('default values', () => { + it('should return default options when no arguments are provided', () => { + const result = parseCliArgs([]); + + expect(result).toEqual({ + ignore: [], + verbose: false, + path: null, + followSymlinks: false + }); + }); + }); + + describe('--ignore flag', () => { + it('should parse single --ignore flag', () => { + const result = parseCliArgs(['--ignore', '**/legacy/**']); + + expect(result.ignore).toEqual(['**/legacy/**']); + }); + + it('should parse -i shorthand flag', () => { + const result = parseCliArgs(['-i', '**/vendor/**']); + + expect(result.ignore).toEqual(['**/vendor/**']); + }); + + it('should parse multiple --ignore flags', () => { + const result = parseCliArgs([ + '--ignore', + '**/legacy/**', + '--ignore', + '**/vendor/**', + '-i', + '**/old/**' + ]); + + expect(result.ignore).toEqual([ + '**/legacy/**', + '**/vendor/**', + '**/old/**' + ]); + }); + + it('should warn and skip when --ignore has no value', () => { + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(); + + const result = parseCliArgs(['--ignore']); + + expect(result.ignore).toEqual([]); + expect(warnSpy).toHaveBeenCalledWith( + 'Warning: --ignore flag requires a pattern argument' + ); + + warnSpy.mockRestore(); + }); + + it('should warn and skip when --ignore is followed by another flag', () => { + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(); + + const result = parseCliArgs(['--ignore', '--verbose']); + + expect(result.ignore).toEqual([]); + expect(result.verbose).toBe(true); + expect(warnSpy).toHaveBeenCalledWith( + 'Warning: --ignore flag requires a pattern argument' + ); + + warnSpy.mockRestore(); + }); + }); + + describe('--verbose flag', () => { + it('should parse --verbose flag', () => { + const result = parseCliArgs(['--verbose']); + + expect(result.verbose).toBe(true); + }); + + it('should parse -v shorthand flag', () => { + const result = parseCliArgs(['-v']); + + expect(result.verbose).toBe(true); + }); + }); + + describe('--path flag', () => { + it('should parse --path flag', () => { + const result = parseCliArgs(['--path', './src']); + + expect(result.path).toBe('./src'); + }); + + it('should parse -p shorthand flag', () => { + const result = parseCliArgs(['-p', '/absolute/path']); + + expect(result.path).toBe('/absolute/path'); + }); + + it('should warn and skip when --path has no value', () => { + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(); + + const result = parseCliArgs(['--path']); + + expect(result.path).toBeNull(); + expect(warnSpy).toHaveBeenCalledWith( + 'Warning: --path flag requires a directory path' + ); + + warnSpy.mockRestore(); + }); + + it('should warn and skip when --path is followed by another flag', () => { + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(); + + const result = parseCliArgs(['--path', '-v']); + + expect(result.path).toBeNull(); + expect(result.verbose).toBe(true); + expect(warnSpy).toHaveBeenCalledWith( + 'Warning: --path flag requires a directory path' + ); + + warnSpy.mockRestore(); + }); + }); + + describe('--followSymlinks flag', () => { + it('should parse --followSymlinks flag', () => { + const result = parseCliArgs(['--followSymlinks']); + + expect(result.followSymlinks).toBe(true); + }); + + it('should default to false when not provided', () => { + const result = parseCliArgs([]); + + expect(result.followSymlinks).toBe(false); + }); + }); + + describe('combined flags', () => { + it('should parse all flags together', () => { + const result = parseCliArgs([ + '--path', + './src', + '--ignore', + '**/legacy/**', + '--verbose', + '--followSymlinks', + '-i', + '**/vendor/**' + ]); + + expect(result).toEqual({ + path: './src', + ignore: ['**/legacy/**', '**/vendor/**'], + verbose: true, + followSymlinks: true + }); + }); + + it('should parse shorthand flags together', () => { + const result = parseCliArgs([ + '-p', + './app', + '-i', + '**/test/**', + '-v' + ]); + + expect(result).toEqual({ + path: './app', + ignore: ['**/test/**'], + verbose: true, + followSymlinks: false + }); + }); + }); + + describe('unknown flags', () => { + it('should ignore unknown flags', () => { + const result = parseCliArgs([ + '--unknown', + '--verbose', + '--another-unknown', + 'value' + ]); + + expect(result.verbose).toBe(true); + expect(result.ignore).toEqual([]); + expect(result.path).toBeNull(); + }); + }); + }); + + describe('DEFAULT_IGNORE_PATTERNS', () => { + it('should include node_modules', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/node_modules/**'); + }); + + it('should include lib directory', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/lib/**'); + }); + + it('should include dist directory', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/dist/**'); + }); + + it('should include build directory', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/build/**'); + }); + + it('should include vendor directory', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/vendor/**'); + }); + + it('should include native code directories', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/ios/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/android/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/Pods/**'); + }); + + it('should include Expo directory', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/.expo/**'); + }); + + it('should include cache and metadata directories', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/.git/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/.cache/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/.yarn/**'); + }); + + it('should include TypeScript declaration files', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/*.d.ts'); + }); + + it('should include test files and directories', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/*.test.*'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/*.spec.*'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/__tests__/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/__mocks__/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/__snapshots__/**'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/coverage/**'); + }); + + it('should include config files', () => { + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/*.config.js'); + expect(DEFAULT_IGNORE_PATTERNS).toContain('**/*.config.ts'); + }); + + it('should have expected number of patterns', () => { + expect(DEFAULT_IGNORE_PATTERNS).toHaveLength(21); + }); + }); + + describe('normalizeIgnorePattern', () => { + const mockCwd = '/home/user/project'; + + describe('simple folder names (no slashes, no glob)', () => { + it('should convert simple folder name to wildcard glob pattern', () => { + expect(normalizeIgnorePattern('legacy', mockCwd)).toBe( + '**/legacy/**' + ); + }); + + it('should convert folder name with hyphen to wildcard glob pattern', () => { + expect(normalizeIgnorePattern('old-code', mockCwd)).toBe( + '**/old-code/**' + ); + }); + + it('should convert folder name with underscore to wildcard glob pattern', () => { + expect(normalizeIgnorePattern('temp_files', mockCwd)).toBe( + '**/temp_files/**' + ); + }); + }); + + describe('relative paths (with ./ or ../)', () => { + it('should resolve relative path starting with ./', () => { + const result = normalizeIgnorePattern('./android', mockCwd); + expect(result).toBe('/home/user/project/android/**'); + }); + + it('should resolve relative path starting with ../', () => { + const result = normalizeIgnorePattern('../other', mockCwd); + expect(result).toBe('/home/user/other/**'); + }); + + it('should resolve nested relative path', () => { + const result = normalizeIgnorePattern('./src/legacy', mockCwd); + expect(result).toBe('/home/user/project/src/legacy/**'); + }); + }); + + describe('paths containing slashes (treated as relative)', () => { + it('should resolve path with forward slash as relative', () => { + const result = normalizeIgnorePattern('packages/app', mockCwd); + expect(result).toBe('/home/user/project/packages/app/**'); + }); + + it('should resolve nested path as relative', () => { + const result = normalizeIgnorePattern( + 'src/components/legacy', + mockCwd + ); + expect(result).toBe( + '/home/user/project/src/components/legacy/**' + ); + }); + }); + + describe('absolute paths', () => { + it('should append /** to absolute path', () => { + const result = normalizeIgnorePattern( + '/home/dev/app/android', + mockCwd + ); + expect(result).toBe('/home/dev/app/android/**'); + }); + + it('should keep absolute path with /** unchanged', () => { + const result = normalizeIgnorePattern( + '/home/dev/app/android/**', + mockCwd + ); + expect(result).toBe('/home/dev/app/android/**'); + }); + }); + + describe('glob patterns (with * ? [ ] { } ( ))', () => { + it('should keep pattern with ** as-is', () => { + expect(normalizeIgnorePattern('**/custom/**', mockCwd)).toBe( + '**/custom/**' + ); + }); + + it('should keep pattern with single * as-is', () => { + expect(normalizeIgnorePattern('*.backup', mockCwd)).toBe( + '*.backup' + ); + }); + + it('should keep pattern with ? as-is', () => { + expect(normalizeIgnorePattern('file?.txt', mockCwd)).toBe( + 'file?.txt' + ); + }); + + it('should keep pattern with brackets as-is', () => { + expect(normalizeIgnorePattern('[abc].txt', mockCwd)).toBe( + '[abc].txt' + ); + }); + + it('should keep pattern with braces as-is', () => { + expect(normalizeIgnorePattern('*.{js,ts}', mockCwd)).toBe( + '*.{js,ts}' + ); + }); + + it('should keep complex glob pattern as-is', () => { + expect( + normalizeIgnorePattern('**/src/**/*.test.ts', mockCwd) + ).toBe('**/src/**/*.test.ts'); + }); + }); + }); +}); diff --git a/packages/react-native-babel-plugin/test/react-native-svg.test.ts b/packages/react-native-babel-plugin/test/react-native-svg.test.ts index e3070a95f..c57542a82 100644 --- a/packages/react-native-babel-plugin/test/react-native-svg.test.ts +++ b/packages/react-native-babel-plugin/test/react-native-svg.test.ts @@ -729,9 +729,21 @@ describe('React Native SVG Processing - RNSvgHandler', () => { }); describe('Error Handling', () => { - it('should throw error or warn for unsupported element names', () => { + it('should warn for unsupported element names but still include them in output', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const input = ''; - expect(() => transformSvg(input)).toThrow(); + const output = transformSvg(input); + + // Unsupported elements are converted to lowercase and included + expect(output).toMatchInlineSnapshot( + `""` + ); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Skipping unsupported element') + ); + + warnSpy.mockRestore(); }); it('should handle malformed transform array gracefully', () => { diff --git a/packages/react-native-navigation/package.json b/packages/react-native-navigation/package.json index 715145949..a2839ad4d 100644 --- a/packages/react-native-navigation/package.json +++ b/packages/react-native-navigation/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native-navigation", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to interact with Datadog", "keywords": [ "datadog", @@ -36,7 +36,7 @@ "prepare": "rm -rf lib && yarn bob build" }, "devDependencies": { - "@datadog/mobile-react-native": "^2.13.2", + "@datadog/mobile-react-native": "^2.14.0", "@testing-library/react-native": "7.0.2", "react-native-builder-bob": "0.26.0", "react-native-gesture-handler": "1.10.3", diff --git a/packages/react-native-session-replay/package.json b/packages/react-native-session-replay/package.json index afec1307f..99532c842 100644 --- a/packages/react-native-session-replay/package.json +++ b/packages/react-native-session-replay/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native-session-replay", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to enable session replay with Datadog", "keywords": [ "datadog", diff --git a/packages/react-native-session-replay/release-content.txt b/packages/react-native-session-replay/release-content.txt index 62b4912f4..9b51b3854 100644 --- a/packages/react-native-session-replay/release-content.txt +++ b/packages/react-native-session-replay/release-content.txt @@ -41,6 +41,8 @@ package/android/src/rn79/kotlin/com/datadog/reactnative/sessionreplay/extensions package/android/src/rn79/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt package/android/src/rn80/kotlin/com/datadog/reactnative/sessionreplay/extensions/ComputedBorderRadiusExt.kt package/android/src/rn80/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +package/android/src/rn83/kotlin/com/datadog/reactnative/sessionreplay/extensions/ComputedBorderRadiusExt.kt +package/android/src/rn83/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt package/android/src/rnpost74/kotlin/com/datadog/reactnative/sessionreplay/DdSDKReactNativeSessionReplayPackage.kt package/android/src/rnpre74/kotlin/com/datadog/reactnative/sessionreplay/DdSDKReactNativeSessionReplayPackage.kt diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index bebe19aab..f4bbbe79d 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -134,7 +134,13 @@ android { } test { - java.srcDir("src/test/kotlin") + java.srcDir('src/test/kotlin') + + if (isNewArchitectureEnabled()) { + java.srcDirs += ['src/testNewArch/kotlin'] + } else { + java.srcDirs += ['src/testOldArch/kotlin'] + } } } @@ -208,6 +214,7 @@ dependencies { testImplementation "com.github.xgouchet.Elmyr:jvm:1.3.1" testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testImplementation 'org.json:json:20160810' detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8" detektPlugins "io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.8" diff --git a/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt new file mode 100644 index 000000000..71c30c406 --- /dev/null +++ b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -0,0 +1,125 @@ +/* +* 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.webview + +import android.annotation.SuppressLint +import com.datadog.android.api.SdkCore +import com.datadog.android.webview.WebViewTracking +import com.datadog.reactnative.DatadogSDKWrapperStorage +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativecommunity.webview.RNCWebView +import com.reactnativecommunity.webview.RNCWebViewClient +import com.reactnativecommunity.webview.RNCWebViewManager +import com.reactnativecommunity.webview.RNCWebViewWrapper +import org.json.JSONArray + +/** + * The entry point to use Datadog auto-instrumented WebView feature. + */ +class DdSdkReactNativeWebViewManager( + private val reactContext: ReactContext +) : RNCWebViewManager() { + // The name used to reference this custom View from React Native. + override fun getName(): String { + return VIEW_NAME + } + + /** + * The instance of Datadog SDK Core. + */ + @Volatile private var _datadogCore: SdkCore? = null + val datadogCore: SdkCore? + get() = _datadogCore + + init { + DatadogSDKWrapperStorage.addOnInitializedListener { core -> + _datadogCore = core + } + } + + /** + * Intercepts the WebView wrapper instance before it is returned and ensures that + * JavaScript is enabled on the underlying WebView. JavaScript must be enabled + * for Datadog WebView tracking to function correctly. + */ + @SuppressLint("SetJavaScriptEnabled") + override fun createViewInstance(context: ThemedReactContext): RNCWebViewWrapper { + val viewInstance = super.createViewInstance(context) + viewInstance.webView.settings.javaScriptEnabled = true + return viewInstance + } + + /** + * Intercepts the JavaScript injected before the WebView loads. + * + * In the New Architecture, WebView props from React Native are ignored, + * so this callback is the only reliable place to extract the + * `// #allowedHosts=` configuration and apply Datadog WebView tracking. + */ + override fun setInjectedJavaScriptBeforeContentLoaded( + view: RNCWebViewWrapper?, + value: String? + ) { + val allowedHosts = value?.let { extractAllowedHosts(it) } + val webView = view?.webView + + if (allowedHosts != null && webView != null) { + configureWebViewTracking(webView, allowedHosts) + } + + super.setInjectedJavaScriptBeforeContentLoaded(view, value) + } + + private fun configureWebViewTracking(webView: RNCWebView, allowedHosts: List) { + val datadogCore = _datadogCore + if (datadogCore != null) { + WebViewTracking.enable( + webView, + allowedHosts = allowedHosts, + sdkCore = datadogCore + ) + } else { + DatadogSDKWrapperStorage.addOnInitializedListener { core -> + reactContext.runOnUiQueueThread { + WebViewTracking.enable( + webView, + allowedHosts = allowedHosts, + sdkCore = core + ) + } + } + } + } + + override fun addEventEmitters( + reactContext: ThemedReactContext, + view: RNCWebViewWrapper + ) { + view.webView.webViewClient = RNCWebViewClient() + } + + companion object { + // The name used to reference this custom View from React Native. + const val VIEW_NAME = "DdReactNativeWebView" + + private fun extractAllowedHosts(input: String): List? { + // Regex that captures everything after "// #allowedHosts=" + val regex = Regex("""//\s*#allowedHosts\s*=\s*(.+)""") + + val match = regex.find(input) ?: return null + val jsonString = match.groupValues[1].trim() + + return try { + val jsonArray = JSONArray(jsonString) + (0 until jsonArray.length()).map { jsonArray.getString(it) } + } catch (e: Exception) { + null + } + } + } +} diff --git a/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt index ada4cc97c..be3b22ee2 100644 --- a/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt +++ b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt @@ -3,7 +3,6 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - package com.datadog.reactnative.webview import com.facebook.react.TurboReactPackage @@ -12,17 +11,18 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager -import com.reactnativecommunity.webview.RNCWebViewManager +import com.reactnativecommunity.webview.RNCWebViewModule +import com.reactnativecommunity.webview.RNCWebViewModuleImpl class DdSdkReactNativeWebViewPackage : TurboReactPackage() { override fun createViewManagers( reactContext: ReactApplicationContext ): MutableList> { return mutableListOf( - RNCWebViewManager() + DdSdkReactNativeWebViewManager(reactContext) ) } - + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return null } diff --git a/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt index 911ccb989..094e95b91 100644 --- a/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt +++ b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -26,10 +26,9 @@ class DdSdkReactNativeWebViewManager( private val reactContext: ReactContext ) : RNCWebViewManager() { // The name used to reference this custom View from React Native. - companion object { - const val VIEW_NAME = "DdReactNativeWebView" + override fun getName(): String { + return VIEW_NAME } - /** * The instance of Datadog SDK Core. */ @@ -37,13 +36,6 @@ class DdSdkReactNativeWebViewManager( val datadogCore: SdkCore? get() = _datadogCore - /** - * Whether WebView tracking has been enabled or not. - */ - @Volatile private var _isWebViewTrackingEnabled: Boolean = false - val isWebViewTrackingEnabled: Boolean - get() = _isWebViewTrackingEnabled - init { DatadogSDKWrapperStorage.addOnInitializedListener { core -> _datadogCore = core @@ -92,21 +84,15 @@ class DdSdkReactNativeWebViewManager( sdkCore: SdkCore, allowedHosts: List ) { - if (_isWebViewTrackingEnabled) { - return - } - WebViewTracking.enable( webView, allowedHosts = allowedHosts, sdkCore = sdkCore ) - - _isWebViewTrackingEnabled = true } - // The name used to reference this custom View from React Native. - override fun getName(): String { - return VIEW_NAME + companion object { + // The name used to reference this custom View from React Native. + const val VIEW_NAME = "DdReactNativeWebView" } } diff --git a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt b/packages/react-native-webview/android/src/test/kotlin/main/com/datadog/reactnative/tools/unit/GenericAssert.kt similarity index 92% rename from packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt rename to packages/react-native-webview/android/src/test/kotlin/main/com/datadog/reactnative/tools/unit/GenericAssert.kt index 8248b6224..4756fb86b 100644 --- a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt +++ b/packages/react-native-webview/android/src/test/kotlin/main/com/datadog/reactnative/tools/unit/GenericAssert.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.reactnative.tools.unit +package main.reactnative.tools.unit import org.assertj.core.api.AbstractAssert diff --git a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt b/packages/react-native-webview/android/src/test/webview/DatadogWebViewTest.kt similarity index 92% rename from packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt rename to packages/react-native-webview/android/src/test/webview/DatadogWebViewTest.kt index e9cf9ce03..8bd514379 100644 --- a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt +++ b/packages/react-native-webview/android/src/test/webview/DatadogWebViewTest.kt @@ -10,7 +10,8 @@ import com.datadog.android.api.SdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.webview.WebViewTracking import com.datadog.reactnative.DatadogSDKWrapperStorage -import com.datadog.reactnative.tools.unit.GenericAssert.Companion.assertThat +import com.datadog.reactnative.webview.DdSdkReactNativeWebViewManager +import main.reactnative.tools.unit.GenericAssert.Companion.assertThat import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.uimanager.ThemedReactContext import com.reactnativecommunity.webview.RNCWebView @@ -90,9 +91,6 @@ internal class DatadogWebViewTest { // When first initialized, the WebView manager core should be null assertThat(manager.datadogCore).isNull() - // When first initialized, the WebView tracking should be disabled - assertThat(manager.isWebViewTrackingEnabled).isEqualTo(false) - // ========= // When // ========= @@ -119,8 +117,5 @@ internal class DatadogWebViewTest { webViewTrackingMockedStatic.verify { WebViewTracking.enable(rncWebView, listOf("example.com"), 100.0f, datadogCore) } - - // At this point 'isWebViewTrackingEnabled' should be true. - assertThat(manager.isWebViewTrackingEnabled).isEqualTo(true) } } diff --git a/packages/react-native-webview/android/src/testNewArch/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt b/packages/react-native-webview/android/src/testNewArch/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt new file mode 100644 index 000000000..581add1f7 --- /dev/null +++ b/packages/react-native-webview/android/src/testNewArch/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt @@ -0,0 +1,122 @@ +/* + * 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.webview + +import com.datadog.android.api.SdkCore +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.webview.WebViewTracking +import com.datadog.reactnative.DatadogSDKWrapperStorage +import main.reactnative.tools.unit.GenericAssert.Companion.assertThat +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativecommunity.webview.RNCWebView +import com.reactnativecommunity.webview.RNCWebViewWrapper +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +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.MockedStatic +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class DatadogWebViewTest { + + @Mock + lateinit var themedReactContext: ThemedReactContext + + @Mock + lateinit var datadogCore: InternalSdkCore + + private lateinit var webViewTrackingMockedStatic: MockedStatic + + @BeforeEach + fun `set up`() { + whenever(themedReactContext.runOnUiQueueThread(any())).thenAnswer { answer -> + answer.getArgument(0).run() + true + } + + webViewTrackingMockedStatic = Mockito.mockStatic(WebViewTracking::class.java) + webViewTrackingMockedStatic.`when` { + WebViewTracking.enable( + webView = any(), // Mock the WebView parameter + allowedHosts = any(), // Mock the list of allowed hosts + logsSampleRate = any(), // Mock the logsSampleRate parameter + sdkCore = any() // Mock the SdkCore parameter + ) + }.then {} // Return Unit as the function has no return value + } + + @AfterEach + fun `tear down`() { + webViewTrackingMockedStatic.close() + } + + @Test + fun `Datadog Core is set once initialized`() { + val manager = DdSdkReactNativeWebViewManager(themedReactContext) + assertThat(manager.datadogCore).isNull() + + DatadogSDKWrapperStorage.notifyOnInitializedListeners(datadogCore) + + assertThat(manager.datadogCore).isNotNull() + assertThat(manager.datadogCore).isInstanceOf(SdkCore::class.java) + } + + @Test + fun `Registers to SdkCore listener if the SDK is not initialized`() { + // ========= + // Given + // ========= + val manager = DdSdkReactNativeWebViewManager(themedReactContext) + + // When first initialized, the WebView manager core should be null + assertThat(manager.datadogCore).isNull() + + // ========= + // When + // ========= + val rncWebView = mock(RNCWebView::class.java) + val rncWebViewWrapper = mock(RNCWebViewWrapper::class.java) + whenever(rncWebViewWrapper.webView) doReturn rncWebView + + // When JS sends allowedHosts through 'injectedJavaScriptBeforeContentLoaded' prop + manager.setInjectedJavaScriptBeforeContentLoaded( + rncWebViewWrapper, + "// #allowedHosts=[\"example.com\",\"test.com\"]" + ) + + // ========= + // Then + // ========= + + // When we notify listeners that the core is available... + DatadogSDKWrapperStorage.notifyOnInitializedListeners(datadogCore) + + // ...the WebView should enable WebView tracking in the UI Thread. + verify(themedReactContext).runOnUiQueueThread(any()) + + // Native WebView tracking should be called + val arrayList = ArrayList(listOf("example.com", "test.com")) + webViewTrackingMockedStatic.verify { + WebViewTracking.enable(rncWebView, arrayList, 100.0f, datadogCore) + } + } +} diff --git a/packages/react-native-webview/package.json b/packages/react-native-webview/package.json index e0fb2182b..cdff26f17 100644 --- a/packages/react-native-webview/package.json +++ b/packages/react-native-webview/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-native-webview", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to interact with react-native-webview and Datadog", "keywords": [ "datadog", diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx index f3627a3c8..9feb45a22 100644 --- a/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx +++ b/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx @@ -3,13 +3,11 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - import { render } from '@testing-library/react-native'; import { WebView as RNWebView } from 'react-native-webview'; import React from 'react'; import { WebView } from '../index'; -import { isNewArchitecture } from '../utils/env-utils'; import { dedent } from './__utils__/string-utils'; @@ -28,7 +26,7 @@ jest.mock('react-native-webview', () => { const callfunction = jest.fn(); const postMessageMock = jest.fn(); -window['ReactNativeWebView'] = { +(window as any)['ReactNativeWebView'] = { postMessage: postMessageMock }; @@ -129,19 +127,18 @@ describe('Webview', () => { const realInjectedJs = dedent( mockedWebView.mock.calls[0][0].injectedJavaScript ?? '' ); - const expected = dedent(` - try{ - testInjectedJavaScript() - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - }`); + const expected = dedent(`try{ + testInjectedJavaScript() +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); expect(realInjectedJs).toBe(expected); }); @@ -167,51 +164,20 @@ describe('Webview', () => { mockedWebView.mock.calls[0][0] .injectedJavaScriptBeforeContentLoaded ?? '' ); - let expected; - - if (isNewArchitecture()) { - expected = dedent(` - window.DatadogEventBridge = { - send(msg) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'NATIVE_EVENT', - message: msg - })); - true; - }, - getAllowedWebViewHosts() { - return '["localhost","example.com"]' - } - }; - try{ - testInjectedJavaScript() - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - } - `); - } else { - expected = dedent(` - try{ - testInjectedJavaScript() - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - }`); - } + const expected = dedent(`// #allowedHosts=["localhost","example.com"] + +try{ + testInjectedJavaScript() +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); expect(realInjectedJs).toBe(expected); }); diff --git a/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts index b09ea8dbc..6091ebd53 100644 --- a/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts +++ b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts @@ -4,7 +4,10 @@ * Copyright 2016-Present Datadog, Inc. */ -import { wrapJsCodeInTryAndCatch } from '../utils/webview-js-utils'; +import { + wrapJsCodeInTryAndCatch, + wrapJsCodeWithAllowedHosts +} from '../utils/webview-js-utils'; import { dedent } from './__utils__/string-utils'; @@ -13,13 +16,73 @@ describe('WebView JS Utils', () => { jest.clearAllMocks(); }); - describe('M wrapJsCodeInTryCatch wraps JS code in try & catch with DD messaging W jsCode is not null', () => { - it('M returns the JS code wrapped in try and catch', () => { + describe('M wrapJsCodeWithAllowedHosts wraps JS code in try & catch with DD messaging W jsCode is not null', () => { + it('M returns the allowedHosts JS comment W { jsCode = undefined & valid allowedHosts }', () => { // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const jsCode = undefined; + + // When + const wrappedCode = wrapJsCodeWithAllowedHosts(jsCode, [ + 'example.com', + 'test.com' + ]); + + // Then + const expected = '// #allowedHosts=["example.com","test.com"]\n'; + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('M returns the JS code wrapped in try and catch with allowedHosts comment W { custom JS code & valid allowedHosts }', () => { + // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); const jsCode = "console.log('test')"; // When - const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + const wrappedCode = wrapJsCodeWithAllowedHosts(jsCode, [ + 'example.com', + 'test.com' + ]); + + // Then + const expected = dedent(`// #allowedHosts=["example.com","test.com"] + +try{ + console.log('test') +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('M returns the JS code wrapped in try and catch W { custom JS code & allowedHosts = undefined }', () => { + // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const jsCode = "console.log('test')"; + + // When + const wrappedCode = wrapJsCodeWithAllowedHosts(jsCode, undefined); // Then const expected = dedent(` @@ -37,15 +100,97 @@ describe('WebView JS Utils', () => { }`); expect(wrappedCode).toBeDefined(); expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).toHaveBeenCalledWith( + "[@datadog/mobile-react-native-webview] Invalid 'allowedHosts' format: Error: allowedHosts is undefined" + ); + + warnSpy.mockRestore(); }); - it('M returns undefined W { jsCode = undefined }', () => { + it('M returns the JS code wrapped in try and catch W { custom JS code & invalid JSON allowedHosts }', () => { + // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const jsCode = "console.log('test')"; + + // When + const wrappedCode = wrapJsCodeWithAllowedHosts( + jsCode, + (() => 'incompatible') as any + ); + + // Then + const expected = dedent(` + try{ + console.log('test') + } + catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; + }`); + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).toHaveBeenCalledWith( + "[@datadog/mobile-react-native-webview] Invalid 'allowedHosts' format: Error: JSON.stringify returned 'undefined' for the given hosts" + ); + + warnSpy.mockRestore(); + }); + + it('M returns undefined W { jsCode = undefined & allowedHosts = undefined }', () => { // Given const jsCode = undefined; + const allowedHosts = undefined; // When - const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + const wrappedCode = wrapJsCodeWithAllowedHosts( + jsCode, + allowedHosts + ); // Then expect(wrappedCode).toBe(undefined); }); }); + + describe('M wrapJsCodeInTryAndCatch wraps the given JS code in a try & catch block with DD messaging', () => { + it('M returns the JS code wrapped in try and catch W { custom JS code is defined }', () => { + // Given + const jsCode = "console.log('test')"; + + // When + const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + + // Then + const expected = dedent(`try{ + console.log('test') +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + }); + + it('M returns undefined W { custom JS code is undefined }', () => { + // Given + const jsCode = undefined; + + // When + const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + + // Then + expect(wrappedCode).toBeUndefined(); + }); + }); }); diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx index 3812e952c..775dd9ab9 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -5,15 +5,14 @@ */ import type { WebViewMessageEvent, WebViewProps } from 'react-native-webview'; import { WebView as RNWebView } from 'react-native-webview'; -import React, { forwardRef, useCallback } from 'react'; +import React, { forwardRef, useCallback, useMemo } from 'react'; import NativeDdLogs from './ext-specs/NativeDdLogs'; import { NativeDdSdk } from './ext-specs/NativeDdSdk'; import { NativeDdWebView } from './specs/NativeDdWebView'; -import { isNewArchitecture } from './utils/env-utils'; import { - getWebViewEventBridgingJS, - wrapJsCodeInTryAndCatch + wrapJsCodeInTryAndCatch, + wrapJsCodeWithAllowedHosts } from './utils/webview-js-utils'; import type { DatadogMessageFormat } from './utils/webview-js-utils'; @@ -71,18 +70,16 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { [userDefinedOnMessage, props.logUserCodeErrors] ); - const getInjectedJavascriptBeforeContentLoaded = (): string | undefined => { - if (isNewArchitecture()) { - return getWebViewEventBridgingJS( - props.allowedHosts, - props.injectedJavaScriptBeforeContentLoaded - ); - } else { - return wrapJsCodeInTryAndCatch( - props.injectedJavaScriptBeforeContentLoaded - ); - } - }; + const injectedJavascript = useMemo(() => { + return wrapJsCodeInTryAndCatch(props.injectedJavaScript); + }, [props.injectedJavaScript]); + + const injectedJavascriptBeforeContentLoaded = useMemo(() => { + return wrapJsCodeWithAllowedHosts( + props.injectedJavaScriptBeforeContentLoaded, + props.allowedHosts + ); + }, [props.injectedJavaScriptBeforeContentLoaded, props.allowedHosts]); return ( >) => { allowedHosts: props.allowedHosts } }} - injectedJavaScript={wrapJsCodeInTryAndCatch( - props.injectedJavaScript - )} - injectedJavaScriptBeforeContentLoaded={getInjectedJavascriptBeforeContentLoaded()} + injectedJavaScript={injectedJavascript} + injectedJavaScriptBeforeContentLoaded={ + injectedJavascriptBeforeContentLoaded + } ref={ref} /> ); diff --git a/packages/react-native-webview/src/specs/NativeDdWebView.ts b/packages/react-native-webview/src/specs/NativeDdWebView.ts index 351b26a16..a3c3ed28a 100644 --- a/packages/react-native-webview/src/specs/NativeDdWebView.ts +++ b/packages/react-native-webview/src/specs/NativeDdWebView.ts @@ -7,10 +7,8 @@ import type { CommonNativeWebViewProps } from 'react-native-webview/lib/WebViewTypes'; import { requireNativeComponent } from 'react-native'; -import { isNewArchitecture } from '../utils/env-utils'; - -const NativeDdWebView = !isNewArchitecture() - ? requireNativeComponent('DdReactNativeWebView') - : undefined; +const NativeDdWebView = requireNativeComponent( + 'DdReactNativeWebView' +); export { NativeDdWebView }; diff --git a/packages/react-native-webview/src/utils/env-utils.ts b/packages/react-native-webview/src/utils/env-utils.ts deleted file mode 100644 index d30542bc6..000000000 --- a/packages/react-native-webview/src/utils/env-utils.ts +++ /dev/null @@ -1,9 +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. - */ - -export const isNewArchitecture = (): boolean => { - return (global as any)?.nativeFabricUIManager !== undefined; -}; diff --git a/packages/react-native-webview/src/utils/webview-js-utils.ts b/packages/react-native-webview/src/utils/webview-js-utils.ts index 101ea1873..c6c9be3b4 100644 --- a/packages/react-native-webview/src/utils/webview-js-utils.ts +++ b/packages/react-native-webview/src/utils/webview-js-utils.ts @@ -29,70 +29,59 @@ export type DatadogMessageFormat = { }; /** - * Wraps the given JS Code in a try and catch block. + * Wraps the given JS Code in a try and catch block, including a + * comment with the given allowedHosts in JSON format * @param javascriptCode The JS Code to wrap in a try and catch block. * @returns the wrapped JS code. */ -export const wrapJsCodeInTryAndCatch = ( - javascriptCode?: string -): string | undefined => - javascriptCode - ? ` - try{ - ${javascriptCode} +export function wrapJsCodeWithAllowedHosts( + javascriptCode?: string, + allowedHosts?: string[] +): string | undefined { + let jsCode = ''; + const hosts = formatAllowedHosts(allowedHosts); + if (hosts) { + jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`; } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - }` + + return javascriptCode + ? `${jsCode}\n${wrapJsCodeInTryAndCatch(javascriptCode)}` + : jsCode.trim().length > 0 + ? jsCode : undefined; +} -/** - * Legacy JS code for bridging the WebView events to DataDog native SDKs for consumption. - * @param allowedHosts The list of allowed hosts. - * @param customJavaScriptCode Custom user JS code to inject along with the Datadog bridging logic. - * @returns The JS code block as a string. - */ -export const getWebViewEventBridgingJS = ( - allowedHosts?: string[], - customJavaScriptCode?: string -): string => - ` - window.DatadogEventBridge = { - send(msg) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'NATIVE_EVENT', - message: msg - })); - true; - }, - getAllowedWebViewHosts() { - return ${formatAllowedHosts(allowedHosts)} - } - }; - try{ - ${customJavaScriptCode} - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - } - `; +export function wrapJsCodeInTryAndCatch( + javascriptCode: string | undefined +): string | undefined { + return javascriptCode + ? `try{ + ${javascriptCode} +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}` + : undefined; +} -function formatAllowedHosts(allowedHosts?: string[]): string { +function formatAllowedHosts(allowedHosts?: string[]): string | undefined { try { - return `'${JSON.stringify(allowedHosts)}'`; + if (!allowedHosts) { + throw new Error('allowedHosts is undefined'); + } + const jsonHosts = JSON.stringify(allowedHosts); + if (!jsonHosts) { + throw new Error( + "JSON.stringify returned 'undefined' for the given hosts" + ); + } + return jsonHosts; } catch (e: any) { if (NativeDdSdk) { NativeDdSdk.telemetryError( @@ -101,7 +90,10 @@ function formatAllowedHosts(allowedHosts?: string[]): string { 'AllowedHostsError' ); } - return "'[]'"; + console.warn( + `[@datadog/mobile-react-native-webview] Invalid 'allowedHosts' format: ${e}` + ); + return undefined; } } diff --git a/packages/react-navigation/package.json b/packages/react-navigation/package.json index b2f15d9dc..09bf15306 100644 --- a/packages/react-navigation/package.json +++ b/packages/react-navigation/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/mobile-react-navigation", - "version": "2.13.2", + "version": "2.14.0", "description": "A client-side React Native module to interact with Datadog", "keywords": [ "datadog", @@ -36,7 +36,7 @@ "prepare": "rm -rf lib && yarn bob build" }, "devDependencies": { - "@datadog/mobile-react-native": "^2.13.2", + "@datadog/mobile-react-native": "^2.14.0", "@react-navigation/native-v5": "npm:@react-navigation/native@5.9.8", "@react-navigation/native-v6": "npm:@react-navigation/native@6.1.2", "@react-navigation/stack-v5": "npm:@react-navigation/stack@5.14.2", diff --git a/yarn.lock b/yarn.lock index dfcec9595..22b120805 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3804,7 +3804,7 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/mobile-react-native-code-push@workspace:packages/codepush" dependencies: - "@datadog/mobile-react-native": "workspace:2.13.2" + "@datadog/mobile-react-native": "workspace:2.14.0" "@testing-library/react-native": 7.0.2 react-native-builder-bob: 0.26.0 react-native-code-push: 7.1.0 @@ -3820,7 +3820,7 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/mobile-react-native-navigation@workspace:packages/react-native-navigation" dependencies: - "@datadog/mobile-react-native": ^2.13.2 + "@datadog/mobile-react-native": ^2.14.0 "@testing-library/react-native": 7.0.2 react-native-builder-bob: 0.26.0 react-native-gesture-handler: 1.10.3 @@ -3862,7 +3862,7 @@ __metadata: languageName: unknown linkType: soft -"@datadog/mobile-react-native@^2.13.2, @datadog/mobile-react-native@workspace:2.13.2, @datadog/mobile-react-native@workspace:packages/core": +"@datadog/mobile-react-native@^2.14.0, @datadog/mobile-react-native@workspace:2.14.0, @datadog/mobile-react-native@workspace:packages/core": version: 0.0.0-use.local resolution: "@datadog/mobile-react-native@workspace:packages/core" dependencies: @@ -3879,7 +3879,7 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/mobile-react-navigation@workspace:packages/react-navigation" dependencies: - "@datadog/mobile-react-native": ^2.13.2 + "@datadog/mobile-react-native": ^2.14.0 "@react-navigation/native-v5": "npm:@react-navigation/native@5.9.8" "@react-navigation/native-v6": "npm:@react-navigation/native@6.1.2" "@react-navigation/stack-v5": "npm:@react-navigation/stack@5.14.2" @@ -15462,18 +15462,7 @@ __metadata: languageName: node linkType: hard -"jwa@npm:^1.4.1": - version: 1.4.2 - resolution: "jwa@npm:1.4.2" - dependencies: - buffer-equal-constant-time: ^1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: ^5.0.1 - checksum: fd1a6de6c649a4b16f0775439ac9173e4bc9aa0162c7f3836699af47736ae000fafe89f232a2345170de6c14021029cb94b488f7882c6caf61e6afef5fce6494 - languageName: node - linkType: hard - -"jwa@npm:^2.0.0": +"jwa@npm:^2.0.1": version: 2.0.1 resolution: "jwa@npm:2.0.1" dependencies: @@ -15484,23 +15473,13 @@ __metadata: languageName: node linkType: hard -"jws@npm:^3.2.2": - version: 3.2.2 - resolution: "jws@npm:3.2.2" - dependencies: - jwa: ^1.4.1 - safe-buffer: ^5.0.1 - checksum: f0213fe5b79344c56cd443428d8f65c16bf842dc8cb8f5aed693e1e91d79c20741663ad6eff07a6d2c433d1831acc9814e8d7bada6a0471fbb91d09ceb2bf5c2 - languageName: node - linkType: hard - -"jws@npm:^4.0.0": - version: 4.0.0 - resolution: "jws@npm:4.0.0" +"jws@npm:^4.0.1": + version: 4.0.1 + resolution: "jws@npm:4.0.1" dependencies: - jwa: ^2.0.0 + jwa: ^2.0.1 safe-buffer: ^5.0.1 - checksum: d68d07aa6d1b8cb35c363a9bd2b48f15064d342a5d9dc18a250dbbce8dc06bd7e4792516c50baa16b8d14f61167c19e851fd7f66b59ecc68b7f6a013759765f7 + checksum: c33a060b2cce1e0e49f85054a49a951f9d52a9e2ae732d720f0fc51843c9ac07a68aacd8e9d086ef4c7c4437d42978b698b57a3e7c9bc4a91c0b74276ea85a9a languageName: node linkType: hard