Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/troubleshooting_no_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ requests carry no tracing headers by design.
in the console. A token that is valid but belongs to another application produces a silent
no-op, not an error.
- **Sampling.** `sessionSamplingRate` is a percentage of *sessions*. At a low value most test
runs legitimately report nothing; set it to `100` while integrating.
runs legitimately report nothing; set it to `100` while integrating. With `sessionOnError`,
a session the rate left out sends nothing at all until it reports an error.
- **Consent.** Nothing is collected under `TrackingConsent.NOT_GRANTED`, and events collected
under `PENDING` are discarded unless consent is later granted.

Expand Down
4 changes: 4 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ datadogConfiguration.site = 'CN';
datadogConfiguration.nativeCrashReportEnabled = true;
// Optional: sample RUM sessions (here, 80% of sessions are reported. Default = 100%)
datadogConfiguration.sessionSamplingRate = 80;
// Optional: also keep the sessions left out by `sessionSamplingRate` that report an error. They are
// held in memory and send nothing until their first error, which uploads the minute before it.
// Default = false
datadogConfiguration.sessionOnError = true;
// Optional: sample the tracing integration for calls between your app and your backend (here, 80% of
// calls to your instrumented backend are linked from the RUM view to the trace. Default = 20%)
// You need to specify the hosts of your backends to enable tracing with these backends
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import java.net.Proxy
* @param trackNonFatalAnrs Enables tracking of non-fatal ANRs on Android.
* @param batchProcessingLevel The preferred number of batches of data that will be sent in a single upload (can be 'LOW', 'MEDIUM' (default), 'HIGH')
* @param initialResourceThreshold "The amount of time after a view starts where a Resource should be considered when calculating Time to Network-Settled (TNS)"
* @param sessionOnError Also keeps the sessions the sample rate leaves out, but only those that report an error.
* @param sessionReplayOnError Also keeps the replays the Session Replay sample rate leaves out, but only those of sessions that report an error.
*/
data class DdSdkConfiguration(
val clientToken: String,
Expand Down Expand Up @@ -73,7 +75,9 @@ data class DdSdkConfiguration(
val bundleLogsWithTraces: Boolean? = null,
val trackNonFatalAnrs: Boolean? = null,
val batchProcessingLevel: String? = null,
val initialResourceThreshold: Double? = null
val initialResourceThreshold: Double? = null,
val sessionOnError: Boolean? = null,
val sessionReplayOnError: Boolean? = null
)

internal data class JSONConfigurationFile(
Expand Down Expand Up @@ -107,7 +111,9 @@ internal data class JSONDdSdkConfiguration(
val bundleLogsWithTraces: Boolean? = null,
val trackNonFatalAnrs: Boolean? = null,
val batchProcessingLevel: String? = null,
val initialResourceThreshold: Double? = null
val initialResourceThreshold: Double? = null,
val sessionOnError: Boolean? = null,
val sessionReplayOnError: Boolean? = null
)

internal data class JSONProxyConfiguration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ internal fun ReadableMap.asDdSdkConfiguration(): DdSdkConfiguration {
bundleLogsWithTraces = getBoolean("bundleLogsWithTraces"),
trackNonFatalAnrs = getBooleanOrNull("trackNonFatalAnrs"),
batchProcessingLevel = getString("batchProcessingLevel"),
initialResourceThreshold = getDoubleOrNull("initialResourceThreshold")
initialResourceThreshold = getDoubleOrNull("initialResourceThreshold"),
sessionOnError = getBooleanOrNull("sessionOnError"),
sessionReplayOnError = getBooleanOrNull("sessionReplayOnError")
)
}

Expand Down Expand Up @@ -140,6 +142,8 @@ internal object DefaultConfiguration {
const val bundleLogsWithRum = true
const val bundleLogsWithTraces = true
const val initialResourceThreshold = 0.1
const val sessionOnError = false
const val sessionReplayOnError = false
}

@Suppress("CyclomaticComplexMethod")
Expand Down Expand Up @@ -176,7 +180,9 @@ internal fun JSONDdSdkConfiguration.asDdSdkConfiguration(): DdSdkConfiguration {
this.bundleLogsWithTraces ?: DefaultConfiguration.bundleLogsWithTraces,
this.trackNonFatalAnrs,
this.batchProcessingLevel,
this.initialResourceThreshold ?: DefaultConfiguration.initialResourceThreshold
this.initialResourceThreshold ?: DefaultConfiguration.initialResourceThreshold,
this.sessionOnError ?: DefaultConfiguration.sessionOnError,
this.sessionReplayOnError ?: DefaultConfiguration.sessionReplayOnError
)
}

Expand Down Expand Up @@ -241,6 +247,8 @@ internal fun DdSdkConfiguration.toReadableMap(): ReadableMap {
trackNonFatalAnrs?.let { map.putBoolean("trackNonFatalAnrs", it) }
additionalConfig?.let { map.putMap("additionalConfig", it.toWritableMap()) }
initialResourceThreshold?.let { map.putDouble("initialResourceThreshold", it)}
sessionOnError?.let { map.putBoolean("sessionOnError", it) }
sessionReplayOnError?.let { map.putBoolean("sessionReplayOnError", it) }
return map
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ class DdSdkImplementation(
internal const val DD_VERSION_SUFFIX = "_dd.version_suffix"
internal const val DD_DROP_RESOURCE = "_dd.resource.drop_resource"
internal const val DD_DROP_ACTION = "_dd.action.drop_action"
internal const val DD_ERROR_LOG_IS_CRASH = "_dd.error_log.is_crash"
internal const val MONITOR_JS_ERROR_MESSAGE = "Error monitoring JS refresh rate"
internal const val PACKAGE_INFO_NOT_FOUND_ERROR_MESSAGE = "Error getting package info"
internal const val NAME = "DdSdk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.datadog.android.rum._RumInternalProxy
import com.datadog.android.rum.configuration.VitalsUpdateFrequency
import com.datadog.android.rum.metric.networksettled.TimeBasedInitialResourceIdentifier
import com.datadog.android.rum.model.ActionEvent
import com.datadog.android.rum.model.ErrorEvent
import com.datadog.android.rum.model.ResourceEvent
import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy
import com.datadog.android.telemetry.model.TelemetryConfigurationEvent
Expand Down Expand Up @@ -117,6 +118,8 @@ class DdSdkNativeInitialization internal constructor(
if (configuration.sampleRate != null) {
configBuilder.setSessionSampleRate(configuration.sampleRate.toFloat())
}
configBuilder.setSessionOnError(configuration.sessionOnError ?: false)
configBuilder.setSessionReplayOnError(configuration.sessionReplayOnError ?: false)

configBuilder.trackFrustrations(configuration.trackFrustrations ?: true)
configBuilder.trackBackgroundEvents(configuration.trackBackgroundEvents ?: false)
Expand Down Expand Up @@ -169,6 +172,24 @@ class DdSdkNativeInitialization internal constructor(
}
)

configBuilder.setErrorEventMapper(
object : EventMapper<ErrorEvent> {
override fun map(event: ErrorEvent): ErrorEvent? {
// JS error tracking reports an error to RUM and, flagged with this attribute,
// to Logs at once; the native logger then forwards every error log back to
// RUM. Dropping that copy leaves one RUM error per JS error, the one the JS
// `errorEventMapper` decided on.
if (event.error.source == ErrorEvent.ErrorSource.LOGGER &&
event.context?.additionalProperties
?.containsKey(DdSdkImplementation.DD_ERROR_LOG_IS_CRASH) == true
) {
return null
}
return event
}
}
)

_RumInternalProxy.setTelemetryConfigurationEventMapper(
configBuilder,
object : EventMapper<TelemetryConfigurationEvent> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ internal class DdSdkNativeInitializationTest {
"[B3MULTI, TRACECONTEXT]"
)
assertThat(configuration.initialResourceThreshold).isEqualTo(0.5)
assertThat(configuration.sessionOnError).isEqualTo(true)
assertThat(configuration.sessionReplayOnError).isEqualTo(true)
}

@Test
Expand Down Expand Up @@ -162,6 +164,8 @@ internal class DdSdkNativeInitializationTest {
assertThat(configuration.proxyConfig).isNull()
assertThat(configuration.firstPartyHosts).isNull()
assertThat(configuration.initialResourceThreshold).isEqualTo(0.1)
assertThat(configuration.sessionOnError).isEqualTo(false)
assertThat(configuration.sessionReplayOnError).isEqualTo(false)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.datadog.android.rum._RumInternalProxy
import com.datadog.android.rum.configuration.VitalsUpdateFrequency
import com.datadog.android.rum.metric.networksettled.TimeBasedInitialResourceIdentifier
import com.datadog.android.rum.model.ActionEvent
import com.datadog.android.rum.model.ErrorEvent
import com.datadog.android.rum.model.ResourceEvent
import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy
import com.datadog.android.telemetry.model.TelemetryConfigurationEvent
Expand All @@ -33,6 +34,7 @@ import com.datadog.tools.unit.GenericAssert.Companion.assertThat
import com.datadog.tools.unit.MockRumMonitor
import com.datadog.tools.unit.TestUiThreadExecutor
import com.datadog.tools.unit.forge.BaseConfigurator
import com.datadog.tools.unit.getFieldValue
import com.datadog.tools.unit.setStaticValue
import com.datadog.tools.unit.toReadableArray
import com.datadog.tools.unit.toReadableJavaOnlyMap
Expand Down Expand Up @@ -1366,6 +1368,79 @@ internal class DdSdkTest {
}
}

@Test
fun `𝕄 initialize native SDK 𝕎 initialize() {sessionOnError, sessionReplayOnError}`(
@Forgery configuration: DdSdkConfiguration
) {
// Given
val bridgeConfiguration = configuration.copy(
sampleRate = 0.0,
sessionOnError = true,
sessionReplayOnError = true
)
val rumConfigCaptor = argumentCaptor<RumConfiguration>()

// When
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
verify(mockDatadog).enableRum(rumConfigCaptor.capture())
assertThat(rumConfigCaptor.firstValue)
.hasField("featureConfiguration") {
it.hasFieldEqualTo("sampleRate", 0f)
it.hasFieldEqualTo("sessionOnError", true)
it.hasFieldEqualTo("sessionReplayOnError", true)
}
}

@Test
fun `𝕄 initialize native SDK 𝕎 initialize() {one on-error switch}`(
@Forgery configuration: DdSdkConfiguration,
forge: Forge
) {
// Given
val sessionOnError = forge.aBool()
val bridgeConfiguration = configuration.copy(
sessionOnError = sessionOnError,
sessionReplayOnError = !sessionOnError
)
val rumConfigCaptor = argumentCaptor<RumConfiguration>()

// When
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
verify(mockDatadog).enableRum(rumConfigCaptor.capture())
assertThat(rumConfigCaptor.firstValue)
.hasField("featureConfiguration") {
it.hasFieldEqualTo("sessionOnError", sessionOnError)
it.hasFieldEqualTo("sessionReplayOnError", !sessionOnError)
}
}

@Test
fun `𝕄 initialize native SDK with on-error switches off 𝕎 initialize() {not set}`(
@Forgery configuration: DdSdkConfiguration
) {
// Given
val bridgeConfiguration = configuration.copy(
sessionOnError = null,
sessionReplayOnError = null
)
val rumConfigCaptor = argumentCaptor<RumConfiguration>()

// When
testedBridgeSdk.initialize(bridgeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
verify(mockDatadog).enableRum(rumConfigCaptor.capture())
assertThat(rumConfigCaptor.firstValue)
.hasField("featureConfiguration") {
it.hasFieldEqualTo("sessionOnError", false)
it.hasFieldEqualTo("sessionReplayOnError", false)
}
}

@Test
fun `𝕄 initialize native SDK 𝕎 initialize() {rare vitals frequency update}`(
@Forgery configuration: DdSdkConfiguration
Expand Down Expand Up @@ -1912,6 +1987,70 @@ internal class DdSdkTest {

// endregion

// region error mapper

@Test
fun `𝕄 set an error mapper that drops the logger copy of a tracked error 𝕎 initialize() {}`(
@Forgery errorEvent: ErrorEvent
) {
// Given
val loggerCopy = errorEvent.copy(
error = errorEvent.error.copy(source = ErrorEvent.ErrorSource.LOGGER)
)
loggerCopy.context?.additionalProperties?.put("_dd.error_log.is_crash", true)

// When
testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
assertThat(capturedErrorEventMapper().map(loggerCopy)).isNull()
}

@Test
fun `𝕄 set an error mapper that keeps a logger error 𝕎 initialize() {not from the JS SDK}`(
@Forgery errorEvent: ErrorEvent
) {
// Given
val loggerError = errorEvent.copy(
error = errorEvent.error.copy(source = ErrorEvent.ErrorSource.LOGGER)
)

// When
testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
assertThat(capturedErrorEventMapper().map(loggerError)).isSameAs(loggerError)
}

@Test
fun `𝕄 set an error mapper that keeps the tracked error itself 𝕎 initialize() {}`(
@Forgery errorEvent: ErrorEvent,
forge: Forge
) {
// Given
val source = forge.anElementFrom(
ErrorEvent.ErrorSource.values().filter { it != ErrorEvent.ErrorSource.LOGGER }
)
val trackedError = errorEvent.copy(error = errorEvent.error.copy(source = source))
trackedError.context?.additionalProperties?.put("_dd.error_log.is_crash", true)

// When
testedBridgeSdk.initialize(fakeConfiguration.toReadableJavaOnlyMap(), mockPromise)

// Then
assertThat(capturedErrorEventMapper().map(trackedError)).isSameAs(trackedError)
}

private fun capturedErrorEventMapper(): EventMapper<ErrorEvent> {
val rumConfigCaptor = argumentCaptor<RumConfiguration>()
verify(mockDatadog).enableRum(rumConfigCaptor.capture())
val featureConfiguration: Any = rumConfigCaptor.firstValue
.getFieldValue("featureConfiguration")
return featureConfiguration.getFieldValue("errorEventMapper")
}

// endregion

// region misc

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ fun DdSdkConfiguration.toReadableJavaOnlyMap(): ReadableMap {
map.put("bundleLogsWithTraces", bundleLogsWithTraces)

trackNonFatalAnrs?.let { map.put("trackNonFatalAnrs", it) }
sessionOnError?.let { map.put("sessionOnError", it) }
sessionReplayOnError?.let { map.put("sessionReplayOnError", it) }

if (batchProcessingLevel != null) {
map["batchProcessingLevel"] = batchProcessingLevel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ open class BaseConfigurator :
forge.addFactory(TelemetryConfigurationEventForgeryFactory())
forge.addFactory(ResourceEventForgeryFactory())
forge.addFactory(ActionEventForgeryFactory())
forge.addFactory(ErrorEventForgeryFactory())
forge.useJvmFactories()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class DdSdkConfigurationForgeryFactory : ForgeryFactory<DdSdkConfiguration> {
bundleLogsWithRum = forge.aBool(),
bundleLogsWithTraces = forge.aBool(),
trackNonFatalAnrs = forge.aBool(),
sessionOnError = forge.aNullable { aBool() },
sessionReplayOnError = forge.aNullable { aBool() },
batchProcessingLevel = forge.aNullable {
anElementFrom(
"LOW",
Expand Down
Loading
Loading