From 938ab508b1429040dfb64d09e2fcd76f523134a2 Mon Sep 17 00:00:00 2001 From: Marius Constantin Date: Tue, 26 Sep 2023 13:32:39 +0200 Subject: [PATCH] RUM-1187 Add more information into the batch telemetry --- .../metrics/BatchMetricsDispatcher.kt | 32 +- .../file/advanced/FeatureFileOrchestrator.kt | 7 +- .../metrics/BatchMetricsDispatcherTest.kt | 334 +++++++++++++----- 3 files changed, 285 insertions(+), 88 deletions(-) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt index 527cf8d567..5aa6b02c31 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcher.kt @@ -11,11 +11,13 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.core.internal.configuration.DataUploadConfiguration import com.datadog.android.core.internal.lifecycle.ProcessLifecycleMonitor import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig +import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOrchestrator import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.lengthSafe import com.datadog.android.core.internal.time.TimeProvider import com.datadog.android.core.sampling.RateBasedSampler import com.datadog.android.core.sampling.Sampler +import com.datadog.android.privacy.TrackingConsent import java.io.File import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean @@ -99,7 +101,10 @@ internal class BatchMetricsDispatcher( UPLOADER_WINDOW_KEY to filePersistenceConfig.recentDelayMs, BATCH_REMOVAL_KEY to deletionReason.toString(), - IN_BACKGROUND_KEY to isInBackground.get() + IN_BACKGROUND_KEY to isInBackground.get(), + TRACKING_CONSENT_KEY to file.resolveFileOriginAsConsent(), + FILE_NAME to file.name, + THREAD_NAME to Thread.currentThread().name ) } @@ -118,7 +123,10 @@ internal class BatchMetricsDispatcher( // be sent as a batch_delete telemetry later BATCH_SIZE_KEY to file.lengthSafe(internalLogger), BATCH_EVENTS_COUNT_KEY to batchMetadata.eventsCount, - FORCE_NEW_KEY to batchMetadata.forcedNew + FORCE_NEW_KEY to batchMetadata.forcedNew, + TRACKING_CONSENT_KEY to file.resolveFileOriginAsConsent(), + FILE_NAME to file.name, + THREAD_NAME to Thread.currentThread().name ) } @@ -144,6 +152,17 @@ internal class BatchMetricsDispatcher( } } + private fun File.resolveFileOriginAsConsent(): String? { + val fileDirectory = this.parentFile?.name ?: return null + return if (fileDirectory.matches(FeatureFileOrchestrator.IS_PENDING_DIR_REG_EX)) { + TrackingConsent.PENDING.toString().lowercase(Locale.US) + } else if (fileDirectory.matches(FeatureFileOrchestrator.IS_GRANTED_DIR_REG_EX)) { + TrackingConsent.GRANTED.toString().lowercase(Locale.US) + } else { + null + } + } + // endregion companion object { @@ -218,6 +237,15 @@ internal class BatchMetricsDispatcher( /* The value for the type of the metric.*/ internal const val BATCH_CLOSED_TYPE_VALUE = "batch closed" + /* The value of the tracking consent according with this file origin.*/ + internal const val TRACKING_CONSENT_KEY = "consent" + + /* The file name.*/ + internal const val FILE_NAME = "filename" + + /* The thread name from which the current metric was sent.*/ + internal const val THREAD_NAME = "thread" + // endregion } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt index f3c26733d5..e463da8078 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/advanced/FeatureFileOrchestrator.kt @@ -64,8 +64,13 @@ internal class FeatureFileOrchestrator( ) companion object { + private const val BASE_DIR_NAME_REG_EX = "([a-z]+[-|_])+" + internal val IS_GRANTED_DIR_REG_EX = Regex("${BASE_DIR_NAME_REG_EX}v[0-9]+") + internal val IS_PENDING_DIR_REG_EX = Regex("${BASE_DIR_NAME_REG_EX}pending-v[0-9]+") + internal const val VERSION = 2 - internal const val PENDING_DIR = "%s-pending-v$VERSION" + internal const val PENDING_DIR_SUFFIX = "-pending-v$VERSION" + internal const val PENDING_DIR = "%s$PENDING_DIR_SUFFIX" internal const val GRANTED_DIR = "%s-v$VERSION" private val PERSISTENCE_CONFIG = FilePersistenceConfig() diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt index a553063aa5..4d649a8b39 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/metrics/BatchMetricsDispatcherTest.kt @@ -95,24 +95,9 @@ internal class BatchMetricsDispatcherTest { // Given val fakeReason = forge.forgeIncludeInMetricReason() val fakeFile: File = forge.forgeValidFile() - val expectedAdditionalProperties = mapOf( - BatchMetricsDispatcher.TYPE_KEY to - BatchMetricsDispatcher.BATCH_DELETED_TYPE_VALUE, - BatchMetricsDispatcher.TRACK_KEY to - trackNameResolver(fakeFeatureName), - BatchMetricsDispatcher.BATCH_AGE_KEY to - (currentTimeInMillis - fakeFile.name.toLong()), - BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, - BatchMetricsDispatcher.UPLOADER_DELAY_KEY to mapOf( - BatchMetricsDispatcher.UPLOADER_DELAY_MIN_KEY to - fakeUploadConfiguration.minDelayMs, - BatchMetricsDispatcher.UPLOADER_DELAY_MAX_KEY to - fakeUploadConfiguration.maxDelayMs - ), - BatchMetricsDispatcher.BATCH_REMOVAL_KEY to fakeReason.toString(), - BatchMetricsDispatcher.IN_BACKGROUND_KEY to false - ) + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + } // When testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason) @@ -132,24 +117,10 @@ internal class BatchMetricsDispatcherTest { // Given val fakeReason = forge.forgeIncludeInMetricReason() val fakeFile: File = forge.forgeValidFile() - val expectedAdditionalProperties = mapOf( - BatchMetricsDispatcher.TYPE_KEY to - BatchMetricsDispatcher.BATCH_DELETED_TYPE_VALUE, - BatchMetricsDispatcher.TRACK_KEY to - trackNameResolver(fakeFeatureName), - BatchMetricsDispatcher.BATCH_AGE_KEY to - (currentTimeInMillis - fakeFile.name.toLong()), - BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, - BatchMetricsDispatcher.UPLOADER_DELAY_KEY to mapOf( - BatchMetricsDispatcher.UPLOADER_DELAY_MIN_KEY to - fakeUploadConfiguration.minDelayMs, - BatchMetricsDispatcher.UPLOADER_DELAY_MAX_KEY to - fakeUploadConfiguration.maxDelayMs - ), - BatchMetricsDispatcher.BATCH_REMOVAL_KEY to fakeReason.toString(), - BatchMetricsDispatcher.IN_BACKGROUND_KEY to true - ) + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.IN_BACKGROUND_KEY, true) + } testedBatchMetricsDispatcher.onPaused() // When @@ -171,25 +142,88 @@ internal class BatchMetricsDispatcherTest { testedBatchMetricsDispatcher.onPaused() val fakeReason = forge.forgeIncludeInMetricReason() val fakeFile: File = forge.forgeValidFile() - val expectedAdditionalProperties = mapOf( - BatchMetricsDispatcher.TYPE_KEY to - BatchMetricsDispatcher.BATCH_DELETED_TYPE_VALUE, - BatchMetricsDispatcher.TRACK_KEY to - trackNameResolver(fakeFeatureName), - BatchMetricsDispatcher.BATCH_AGE_KEY to - (currentTimeInMillis - fakeFile.name.toLong()), - BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, - BatchMetricsDispatcher.UPLOADER_DELAY_KEY to mapOf( - BatchMetricsDispatcher.UPLOADER_DELAY_MIN_KEY to - fakeUploadConfiguration.minDelayMs, - BatchMetricsDispatcher.UPLOADER_DELAY_MAX_KEY to - fakeUploadConfiguration.maxDelayMs - ), - BatchMetricsDispatcher.BATCH_REMOVAL_KEY to fakeReason.toString(), - BatchMetricsDispatcher.IN_BACKGROUND_KEY to false - ) + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + } + // When + testedBatchMetricsDispatcher.onResumed() + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason) + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture() + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { file is in pending folder }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile().apply { + val forgeAPendingDirName = forge.forgeAPendingDirName() + whenever(this.parentFile?.name).thenReturn(forgeAPendingDirName) + } + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, "pending") + } + // When + testedBatchMetricsDispatcher.onResumed() + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture() + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { file parent dir is unknown }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile().apply { + whenever(this.parentFile?.name).thenReturn(forge.anAlphabeticalString()) + } + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } + // When + testedBatchMetricsDispatcher.onResumed() + testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_DELETED_MESSAGE }, + capture() + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchDeletedMetric { file parent dir is null }`(forge: Forge) { + // Given + testedBatchMetricsDispatcher.onPaused() + val fakeReason = forge.forgeIncludeInMetricReason() + val fakeFile: File = forge.forgeValidFile().apply { + whenever(this.parentFile).thenReturn(null) + } + val expectedAdditionalProperties = resolveDefaultDeleteExtraProperties(fakeFile).apply { + put(BatchMetricsDispatcher.BATCH_REMOVAL_KEY, fakeReason.toString()) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } // When testedBatchMetricsDispatcher.onResumed() testedBatchMetricsDispatcher.sendBatchDeletedMetric(fakeFile, fakeReason) @@ -290,19 +324,10 @@ internal class BatchMetricsDispatcherTest { ) { // Given val fakeFile: File = forge.forgeValidClosedFile() - val expectedAdditionalProperties = mapOf( - BatchMetricsDispatcher.TYPE_KEY to - BatchMetricsDispatcher.BATCH_CLOSED_TYPE_VALUE, - BatchMetricsDispatcher.TRACK_KEY to - trackNameResolver(fakeFeatureName), - BatchMetricsDispatcher.BATCH_DURATION_KEY to - (fakeMetadata.lastTimeWasUsedInMs - fakeFile.name.toLong()), - BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, - BatchMetricsDispatcher.BATCH_SIZE_KEY to fakeFile.length(), - BatchMetricsDispatcher.FORCE_NEW_KEY to fakeMetadata.forcedNew, - BatchMetricsDispatcher.BATCH_EVENTS_COUNT_KEY to fakeMetadata.eventsCount - ) + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, fakeFile.length()) + } // When testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) @@ -319,28 +344,108 @@ internal class BatchMetricsDispatcherTest { @Test fun `M send metric W sendBatchClosedMetric{ file is broken }`( + forge: Forge, @Forgery fakeMetadata: BatchClosedMetadata, @Forgery fakeException: Exception ) { // Given - val fakeFile: File = mock { - whenever(it.length()).thenThrow(fakeException) - whenever(it.name).thenReturn(System.currentTimeMillis().toString()) - whenever(it.exists()).thenReturn(true) + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) } - val expectedAdditionalProperties = mapOf( - BatchMetricsDispatcher.TYPE_KEY to - BatchMetricsDispatcher.BATCH_CLOSED_TYPE_VALUE, - BatchMetricsDispatcher.TRACK_KEY to - trackNameResolver(fakeFeatureName), - BatchMetricsDispatcher.BATCH_DURATION_KEY to - (fakeMetadata.lastTimeWasUsedInMs - fakeFile.name.toLong()), - BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to - fakeFilePersistenceConfig.recentDelayMs, - BatchMetricsDispatcher.BATCH_SIZE_KEY to 0L, - BatchMetricsDispatcher.FORCE_NEW_KEY to fakeMetadata.forcedNew, - BatchMetricsDispatcher.BATCH_EVENTS_COUNT_KEY to fakeMetadata.eventsCount - ) + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture() + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file is in pending folder }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + whenever(this.parentFile?.name).thenReturn(forge.forgeAPendingDirName()) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, "pending") + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture() + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file parent dir is null }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + whenever(this.parentFile).thenReturn(null) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } + + // When + testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) + + // Then + argumentCaptor> { + verify(mockInternalLogger).logMetric( + argThat { this.invoke() == BatchMetricsDispatcher.BATCH_CLOSED_MESSAGE }, + capture() + ) + assertThat(firstValue).containsExactlyInAnyOrderEntriesOf(expectedAdditionalProperties) + } + } + + @Test + fun `M send metric W sendBatchClosedMetric{ file parent dir is unknown }`( + forge: Forge, + @Forgery fakeMetadata: BatchClosedMetadata, + @Forgery fakeException: Exception + ) { + // Given + val fakeFile: File = forge.forgeValidClosedFile().apply { + whenever(this.length()).thenThrow(fakeException) + whenever(this.parentFile?.name).thenReturn(forge.anAlphabeticalString()) + } + val expectedAdditionalProperties = + resolveDefaultCloseExtraProperties(fakeFile, fakeMetadata).apply { + put(BatchMetricsDispatcher.BATCH_SIZE_KEY, 0L) + put(BatchMetricsDispatcher.TRACKING_CONSENT_KEY, null) + } // When testedBatchMetricsDispatcher.sendBatchClosedMetric(fakeFile, fakeMetadata) @@ -442,20 +547,79 @@ internal class BatchMetricsDispatcherTest { verifyNoInteractions(mockInternalLogger) } + private fun resolveDefaultDeleteExtraProperties(file: File): MutableMap { + return mutableMapOf( + BatchMetricsDispatcher.TYPE_KEY to BatchMetricsDispatcher.BATCH_DELETED_TYPE_VALUE, + BatchMetricsDispatcher.TRACK_KEY to resolveTrackName(fakeFeatureName), + BatchMetricsDispatcher.BATCH_AGE_KEY to + (currentTimeInMillis - file.name.toLong()), + BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to + fakeFilePersistenceConfig.recentDelayMs, + BatchMetricsDispatcher.UPLOADER_DELAY_KEY to mapOf( + BatchMetricsDispatcher.UPLOADER_DELAY_MIN_KEY to + fakeUploadConfiguration.minDelayMs, + BatchMetricsDispatcher.UPLOADER_DELAY_MAX_KEY to + fakeUploadConfiguration.maxDelayMs + ), + BatchMetricsDispatcher.FILE_NAME to file.name, + BatchMetricsDispatcher.THREAD_NAME to Thread.currentThread().name, + BatchMetricsDispatcher.TRACKING_CONSENT_KEY to "granted", + BatchMetricsDispatcher.IN_BACKGROUND_KEY to false + ) + } + + private fun resolveDefaultCloseExtraProperties( + file: File, + batchClosedMetadata: BatchClosedMetadata + ): MutableMap { + return mutableMapOf( + BatchMetricsDispatcher.TYPE_KEY to BatchMetricsDispatcher.BATCH_CLOSED_TYPE_VALUE, + BatchMetricsDispatcher.TRACK_KEY to resolveTrackName(fakeFeatureName), + BatchMetricsDispatcher.BATCH_DURATION_KEY to + (batchClosedMetadata.lastTimeWasUsedInMs - file.name.toLong()), + BatchMetricsDispatcher.UPLOADER_WINDOW_KEY to + fakeFilePersistenceConfig.recentDelayMs, + BatchMetricsDispatcher.FORCE_NEW_KEY to batchClosedMetadata.forcedNew, + BatchMetricsDispatcher.BATCH_EVENTS_COUNT_KEY to batchClosedMetadata.eventsCount, + BatchMetricsDispatcher.FILE_NAME to file.name, + BatchMetricsDispatcher.THREAD_NAME to Thread.currentThread().name, + BatchMetricsDispatcher.TRACKING_CONSENT_KEY to "granted" + ) + } + private fun Forge.forgeValidFile(): File { val fileNameAsLong = currentTimeInMillis - aLong(min = 1000, max = 100000) val fileLength = aPositiveLong() + val dirName = forgeAGrantedDirName() + val parentDirectory: File = mock() { + whenever(it.isDirectory).thenReturn(true) + whenever(it.name).thenReturn(dirName) + } val fakeFile: File = mock { + whenever(it.parentFile).thenReturn(parentDirectory) whenever(it.name).thenReturn(fileNameAsLong.toString()) whenever(it.length()).thenReturn(fileLength) } return fakeFile } + private fun Forge.forgeValidClosedFile(): File { return forgeValidFile().apply { whenever(this.exists()).thenReturn(true) } } - private fun trackNameResolver(featureName: String): String? { + private fun Forge.forgeAGrantedDirName(): String { + val separator = if (aBool()) "_" else "-" + return aList(anInt(min = 1, max = 10)) { anAlphabeticalString() } + .joinToString(separator) + "-v" + aNumericalString() + } + + private fun Forge.forgeAPendingDirName(): String { + val separator = if (aBool()) "_" else "-" + return aList(anInt(min = 1, max = 10)) { anAlphabeticalString() } + .joinToString(separator) + "-pending-v" + aNumericalString() + } + + private fun resolveTrackName(featureName: String): String? { return when (featureName) { Feature.RUM_FEATURE_NAME -> BatchMetricsDispatcher.RUM_TRACK_NAME Feature.LOGS_FEATURE_NAME -> BatchMetricsDispatcher.LOGS_TRACK_NAME