From db70e4df11e0df5a92e4f40a48b7c54849a3728f Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 2 Aug 2024 14:58:57 +0200 Subject: [PATCH] Config params for tracking non-fatal ANRs and app hangs --- .../com/datadog/reactnative/DdSdkBridgeExt.kt | 12 ++++++++ .../datadog/reactnative/DdSdkConfiguration.kt | 7 +++-- .../reactnative/DdSdkConfigurationExt.kt | 7 +++-- .../reactnative/DdSdkNativeInitialization.kt | 5 ++++ .../datadog/reactnative/DdSdkBridgeExtTest.kt | 28 +++++++++++++++++++ .../tools/unit/DdSdkConfigurationExt.kt | 2 ++ .../forge/DdSdkConfigurationForgeryFactory.kt | 3 +- .../core/datadog-configuration.schema.json | 8 ++++++ .../core/ios/Sources/DdSdkConfiguration.swift | 6 +++- .../Sources/DdSdkNativeInitialization.swift | 6 ++++ .../ios/Sources/RNDdSdkConfiguration.swift | 8 ++++-- packages/core/ios/Tests/DdSdkTests.swift | 6 ++-- packages/core/src/DdSdkReactNative.tsx | 4 ++- .../src/DdSdkReactNativeConfiguration.tsx | 24 ++++++++++++++++ .../__tests__/initialization.test.tsx | 2 ++ packages/core/src/types.tsx | 4 ++- 16 files changed, 120 insertions(+), 12 deletions(-) diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt index 1b4b4506c..09cec73fb 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkBridgeExt.kt @@ -215,3 +215,15 @@ internal fun ReadableArray.toList(): List<*> { return list } + +/** + * Returns the boolean for the given key, or null if the entry is + * not in the map. + */ +internal fun ReadableMap.getBooleanOrNull(key: String): Boolean? { + return if (hasKey(key)) { + getBoolean(key) + } else { + null + } +} diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt index f3c0ff447..ba1d7ae9c 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfiguration.kt @@ -37,6 +37,7 @@ import java.net.Proxy * @param firstPartyHosts List of backend hosts to enable tracing with. * @param bundleLogsWithRum Enables RUM correlation with logs. * @param bundleLogsWithTraces Enables Traces correlation with logs. + * @param trackNonFatalAnrs Enables tracking of non-fatal ANRs on Android. */ data class DdSdkConfiguration( val clientToken: String, @@ -64,7 +65,8 @@ data class DdSdkConfiguration( val serviceName: String? = null, val firstPartyHosts: Map>? = null, val bundleLogsWithRum: Boolean? = null, - val bundleLogsWithTraces: Boolean? = null + val bundleLogsWithTraces: Boolean? = null, + val trackNonFatalAnrs: Boolean? = null ) internal data class JSONConfigurationFile( @@ -95,7 +97,8 @@ internal data class JSONDdSdkConfiguration( val serviceName: String? = null, val firstPartyHosts: List? = null, val bundleLogsWithRum: Boolean? = null, - val bundleLogsWithTraces: Boolean? = null + val bundleLogsWithTraces: Boolean? = null, + val trackNonFatalAnrs: Boolean? = null ) internal data class JSONProxyConfiguration( diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt index d06bff82c..219cf6805 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkConfigurationExt.kt @@ -46,7 +46,8 @@ internal fun ReadableMap.asDdSdkConfiguration(): DdSdkConfiguration { serviceName = getString("serviceName"), firstPartyHosts = getArray("firstPartyHosts")?.asFirstPartyHosts(), bundleLogsWithRum = getBoolean("bundleLogsWithRum"), - bundleLogsWithTraces = getBoolean("bundleLogsWithTraces") + bundleLogsWithTraces = getBoolean("bundleLogsWithTraces"), + trackNonFatalAnrs = getBooleanOrNull("trackNonFatalAnrs") ) } @@ -169,7 +170,8 @@ internal fun JSONDdSdkConfiguration.asDdSdkConfiguration(): DdSdkConfiguration { this.serviceName, this.firstPartyHosts?.asFirstPartyHosts(), this.bundleLogsWithRum ?: DefaultConfiguration.bundleLogsWithRum, - this.bundleLogsWithTraces ?: DefaultConfiguration.bundleLogsWithTraces + this.bundleLogsWithTraces ?: DefaultConfiguration.bundleLogsWithTraces, + this.trackNonFatalAnrs ) } @@ -231,6 +233,7 @@ internal fun DdSdkConfiguration.toReadableMap(): ReadableMap { uploadFrequency?.let { map.putString("uploadFrequency", it) } batchSize?.let { map.putString("batchSize", it) } trackBackgroundEvents?.let { map.putBoolean("trackBackgroundEvents", it) } + trackNonFatalAnrs?.let { map.putBoolean("trackNonFatalAnrs", it) } additionalConfig?.let { map.putMap("additionalConfig", it.toWritableMap()) } return map } diff --git a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt index 1b352a1ce..1e2317503 100644 --- a/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt +++ b/packages/core/android/src/main/kotlin/com/datadog/reactnative/DdSdkNativeInitialization.kt @@ -96,6 +96,7 @@ class DdSdkNativeInitialization internal constructor( ?: DdSdkImplementation.DEFAULT_APP_VERSION } + @Suppress("ComplexMethod") private fun buildRumConfiguration(configuration: DdSdkConfiguration): RumConfiguration { val configBuilder = RumConfiguration.Builder( @@ -196,6 +197,10 @@ class DdSdkNativeInitialization internal constructor( configBuilder.useCustomEndpoint(it) } + configuration.trackNonFatalAnrs?.let { + configBuilder.trackNonFatalAnrs(it) + } + return configBuilder.build() } diff --git a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt index 0560b0c14..4571b75c3 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/reactnative/DdSdkBridgeExtTest.kt @@ -241,6 +241,34 @@ internal class DdSdkBridgeExtTest { assertThat(list).isEmpty() } + @Test + fun `M returns a boolean W getBooleanOrNull { entry in the map }`() { + // Given + val readableMap = mapOf( + "testKey" to true + ).toReadableMap() + + // When + val value = readableMap.getBooleanOrNull("testKey") + + // Then + assertThat(value).isTrue() + } + + @Test + fun `M returns null W getBooleanOrNull { entry not in the map }`() { + // Given + val readableMap = mapOf( + "dummy" to false + ).toReadableMap() + + // When + val value = readableMap.getBooleanOrNull("testKey") + + // Then + assertThat(value).isNull() + } + private fun getTestMap(): MutableMap = mutableMapOf( "null" to null, "int" to 1, diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt index 98eb6359b..416539468 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/DdSdkConfigurationExt.kt @@ -99,6 +99,8 @@ fun DdSdkConfiguration.toReadableJavaOnlyMap(): ReadableMap { map.put("bundleLogsWithRum", bundleLogsWithRum) map.put("bundleLogsWithTraces", bundleLogsWithTraces) + trackNonFatalAnrs?.let { map.put("trackNonFatalAnrs", it) } + return map.toReadableMap() } diff --git a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/DdSdkConfigurationForgeryFactory.kt b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/DdSdkConfigurationForgeryFactory.kt index 712170927..1d01c6b06 100644 --- a/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/DdSdkConfigurationForgeryFactory.kt +++ b/packages/core/android/src/test/kotlin/com/datadog/tools/unit/forge/DdSdkConfigurationForgeryFactory.kt @@ -79,7 +79,8 @@ class DdSdkConfigurationForgeryFactory : ForgeryFactory { serviceName = forge.aNullable { forge.anAlphabeticalString() }, firstPartyHosts = null, bundleLogsWithRum = forge.aBool(), - bundleLogsWithTraces = forge.aBool() + bundleLogsWithTraces = forge.aBool(), + trackNonFatalAnrs = forge.aBool() ) } } diff --git a/packages/core/datadog-configuration.schema.json b/packages/core/datadog-configuration.schema.json index 97a69ce93..c8724d147 100644 --- a/packages/core/datadog-configuration.schema.json +++ b/packages/core/datadog-configuration.schema.json @@ -233,6 +233,14 @@ "bundleLogsWithTraces": { "description": "Enables Traces correlation with logs.", "type": "boolean" + }, + "appHangThreshold": { + "description": "The app hang threshold in milliseconds for non-fatal app hangs on iOS.", + "type": "number" + }, + "trackNonFatalAnrs": { + "description": "Enables tracking of non-fatal ANRs on Android.", + "type": "boolean" } }, "required": [ diff --git a/packages/core/ios/Sources/DdSdkConfiguration.swift b/packages/core/ios/Sources/DdSdkConfiguration.swift index b8c2fdf81..f2593b363 100644 --- a/packages/core/ios/Sources/DdSdkConfiguration.swift +++ b/packages/core/ios/Sources/DdSdkConfiguration.swift @@ -38,6 +38,7 @@ import DatadogRUM - firstPartyHosts: List of backend hosts to enable tracing with. - bundleLogsWithRum: Correlates logs with RUM. - bundleLogsWithTraces: Correlates logs with traces. + - appHangThreshold: The threshold for non-fatal app hangs reporting in seconds. */ @objc(DdSdkConfiguration) public class DdSdkConfiguration: NSObject { @@ -68,6 +69,7 @@ public class DdSdkConfiguration: NSObject { public var resourceTracingSamplingRate: Double? = nil public var bundleLogsWithRum: Bool public var bundleLogsWithTraces: Bool + public var appHangThreshold: Double? = nil public init( clientToken: String, @@ -96,7 +98,8 @@ public class DdSdkConfiguration: NSObject { firstPartyHosts: [String: Set]?, resourceTracingSamplingRate: Double?, bundleLogsWithRum: Bool, - bundleLogsWithTraces: Bool + bundleLogsWithTraces: Bool, + appHangThreshold: Double? ) { self.clientToken = clientToken self.env = env @@ -125,6 +128,7 @@ public class DdSdkConfiguration: NSObject { self.resourceTracingSamplingRate = resourceTracingSamplingRate self.bundleLogsWithRum = bundleLogsWithRum self.bundleLogsWithTraces = bundleLogsWithTraces + self.appHangThreshold = appHangThreshold } } diff --git a/packages/core/ios/Sources/DdSdkNativeInitialization.swift b/packages/core/ios/Sources/DdSdkNativeInitialization.swift index 8d024db1d..275f968f1 100644 --- a/packages/core/ios/Sources/DdSdkNativeInitialization.swift +++ b/packages/core/ios/Sources/DdSdkNativeInitialization.swift @@ -150,6 +150,11 @@ public class DdSdkNativeInitialization: NSObject { } } + var appHangThreshold: Double? = nil + if let customAppHangThreshold = configuration.appHangThreshold { + appHangThreshold = customAppHangThreshold + } + return RUM.Configuration( applicationID: configuration.applicationId, sessionSampleRate: (configuration.sampleRate as? NSNumber)?.floatValue ?? Float(DefaultConfiguration.sessionSamplingRate), @@ -159,6 +164,7 @@ public class DdSdkNativeInitialization: NSObject { trackFrustrations: configuration.trackFrustrations ?? true, trackBackgroundEvents: configuration.trackBackgroundEvents ?? false, longTaskThreshold: longTaskThreshold, + appHangThreshold: appHangThreshold, vitalsUpdateFrequency: configuration.vitalsUpdateFrequency, resourceEventMapper: { resourceEvent in if resourceEvent.context?.contextInfo[InternalConfigurationAttributes.dropResource] != nil { diff --git a/packages/core/ios/Sources/RNDdSdkConfiguration.swift b/packages/core/ios/Sources/RNDdSdkConfiguration.swift index 51d7173e5..b277fef35 100644 --- a/packages/core/ios/Sources/RNDdSdkConfiguration.swift +++ b/packages/core/ios/Sources/RNDdSdkConfiguration.swift @@ -39,6 +39,7 @@ extension NSDictionary { let resourceTracingSamplingRate = object(forKey: "resourceTracingSamplingRate") as? Double let bundleLogsWithRum = object(forKey: "bundleLogsWithRum") as? Bool let bundleLogsWithTraces = object(forKey: "bundleLogsWithTraces") as? Bool + let appHangThreshold = object(forKey: "appHangThreshold") as? Double return DdSdkConfiguration( clientToken: (clientToken != nil) ? clientToken! : String(), @@ -67,7 +68,8 @@ extension NSDictionary { firstPartyHosts: firstPartyHosts?.asFirstPartyHosts(), resourceTracingSamplingRate: resourceTracingSamplingRate, bundleLogsWithRum: bundleLogsWithRum ?? DefaultConfiguration.bundleLogsWithRum, - bundleLogsWithTraces: bundleLogsWithTraces ?? DefaultConfiguration.bundleLogsWithTraces + bundleLogsWithTraces: bundleLogsWithTraces ?? DefaultConfiguration.bundleLogsWithTraces, + appHangThreshold: appHangThreshold ) } @@ -231,6 +233,7 @@ extension Dictionary where Key == String, Value == AnyObject { let resourceTracingSamplingRate = configuration["resourceTracingSamplingRate"] as? Double let bundleLogsWithRum = configuration["bundleLogsWithRum"] as? Bool let bundleLogsWithTraces = configuration["bundleLogsWithTraces"] as? Bool + let appHangThreshold = configuration["appHangThreshold"] as? Double return DdSdkConfiguration( clientToken: clientToken ?? String(), @@ -262,7 +265,8 @@ extension Dictionary where Key == String, Value == AnyObject { firstPartyHosts: firstPartyHosts?.asFirstPartyHosts() ?? DefaultConfiguration.firstPartyHosts, resourceTracingSamplingRate: resourceTracingSamplingRate ?? DefaultConfiguration.resourceTracingSamplingRate, bundleLogsWithRum: bundleLogsWithRum ?? DefaultConfiguration.bundleLogsWithRum, - bundleLogsWithTraces: bundleLogsWithTraces ?? DefaultConfiguration.bundleLogsWithTraces + bundleLogsWithTraces: bundleLogsWithTraces ?? DefaultConfiguration.bundleLogsWithTraces, + appHangThreshold: appHangThreshold ) } } diff --git a/packages/core/ios/Tests/DdSdkTests.swift b/packages/core/ios/Tests/DdSdkTests.swift index 04eb25f06..12e717797 100644 --- a/packages/core/ios/Tests/DdSdkTests.swift +++ b/packages/core/ios/Tests/DdSdkTests.swift @@ -984,7 +984,8 @@ extension DdSdkConfiguration { firstPartyHosts: [String: Set]? = nil, resourceTracingSamplingRate: Double? = nil, bundleLogsWithRum: Bool = true, - bundleLogsWithTraces: Bool = true + bundleLogsWithTraces: Bool = true, + appHangThreshold: Double? = nil ) -> DdSdkConfiguration { DdSdkConfiguration( clientToken: clientToken as String, @@ -1013,7 +1014,8 @@ extension DdSdkConfiguration { firstPartyHosts: firstPartyHosts, resourceTracingSamplingRate: resourceTracingSamplingRate, bundleLogsWithRum: bundleLogsWithRum, - bundleLogsWithTraces: bundleLogsWithTraces + bundleLogsWithTraces: bundleLogsWithTraces, + appHangThreshold: appHangThreshold ) } } diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index a2ed703be..e950f044a 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -279,7 +279,9 @@ export class DdSdkReactNative { configuration.serviceName, formatFirstPartyHosts(configuration.firstPartyHosts), configuration.bundleLogsWithRum, - configuration.bundleLogsWithTraces + configuration.bundleLogsWithTraces, + configuration.trackNonFatalAnrs, + configuration.appHangThreshold ); }; diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 9ac6fa450..e6c3d4c6c 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -267,6 +267,30 @@ export class DdSdkReactNativeConfiguration { */ public bundleLogsWithTraces: boolean = DEFAULTS.bundleLogsWithTraces; + /** + * Enables tracking of non-fatal ANRs on Android. + * By default, the reporting of non-fatal ANRs on Android 30+ is disabled because it would + * create too much noise over fatal ANRs. On Android 29 and below, however, + * the reporting of non-fatal ANRs is enabled by default, + * as fatal ANRs cannot be reported on those versions. + */ + public trackNonFatalAnrs?: boolean; + + /** + * The app hang threshold in milliseconds for non-fatal app hangs on iOS. + * + * App hangs are an iOS-specific type of error that happens when the application is unresponsive for too long. + * By default, app hangs reporting is disabled, but you can enable it and set your + * own threshold to monitor app hangs that last more than a specified + * duration by using the this parameter. + * + * Set the appHangThreshold parameter to the minimal duration you want + * app hangs to be reported. For example, enter 0.25 to report hangs lasting at least 250 ms. + * See [Configure the app hang threshold](https://docs.datadoghq.com/real_user_monitoring/error_tracking/mobile/ios/?tab=cocoapods#configure-the-app-hang-threshold) + * for more guidance on what to set this value to. + */ + public appHangThreshold?: number; + /** * Specifies a custom prop to name RUM actions on elements having an `onPress` prop. * diff --git a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx index f45ef9d82..6294e52e6 100644 --- a/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx +++ b/packages/core/src/sdk/DatadogProvider/__tests__/initialization.test.tsx @@ -66,6 +66,7 @@ describe('DatadogProvider', () => { "additionalConfiguration": { "_dd.source": "react-native", }, + "appHangThreshold": undefined, "applicationId": "fakeApplicationId", "batchSize": "MEDIUM", "bundleLogsWithRum": true, @@ -94,6 +95,7 @@ describe('DatadogProvider', () => { "telemetrySampleRate": 20, "trackBackgroundEvents": false, "trackFrustrations": true, + "trackNonFatalAnrs": undefined, "trackingConsent": "granted", "uploadFrequency": "AVERAGE", "verbosity": undefined, diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index 9d0b0087f..83abf0fd0 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -56,7 +56,9 @@ export class DdSdkConfiguration { propagatorTypes: string[]; }[], readonly bundleLogsWithRum: boolean, - readonly bundleLogsWithTraces: boolean + readonly bundleLogsWithTraces: boolean, + readonly trackNonFatalAnrs: boolean | undefined, + readonly appHangThreshold: number | undefined ) {} }