-
Notifications
You must be signed in to change notification settings - Fork 74
[FFL-1720] Evaluation Logging: Storage & Network Infrastructure #3146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: graphite-base/3146
Are you sure you want to change the base?
Changes from all commits
db50e7f
c644f0d
70d0be1
718b08a
17d0475
5f60edb
11db057
99da5be
727f037
f6e69ad
e55b8ea
7d2af2e
0bee77e
3a685c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* | ||
| * 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.android.flags.internal.storage | ||
|
|
||
| import com.datadog.android.api.feature.Feature | ||
| import com.datadog.android.api.feature.FeatureSdkCore | ||
| import com.datadog.android.api.storage.EventType | ||
| import com.datadog.android.api.storage.RawBatchEvent | ||
| import com.datadog.android.flags.internal.EvaluationEventWriter | ||
| import com.datadog.android.flags.model.FlagEvaluation | ||
|
|
||
| /** | ||
| * Persists serialized flag evaluation events to SDK Core storage. | ||
| * | ||
| * Events are written to the EVALUATIONS feature storage and will be uploaded | ||
| * to the EVP intake endpoint by the SDK Core's upload mechanism. | ||
| */ | ||
| internal class EvaluationEventRecordWriter(private val sdkCore: FeatureSdkCore) : EvaluationEventWriter { | ||
| override fun writeAll(events: List<FlagEvaluation>) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe can be just |
||
| if (events.isEmpty()) return | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one more thing: it is better to wrap a code block below |
||
| sdkCore.getFeature(Feature.FLAGS_EVALUATIONS_FEATURE_NAME) | ||
| ?.withWriteContext { _, writeScope -> | ||
| writeScope { batchWriter -> | ||
| for (event in events) { | ||
| val serializedRecord = event.toJson().toString().toByteArray(Charsets.UTF_8) | ||
| val rawBatchEvent = RawBatchEvent(data = serializedRecord) | ||
| batchWriter.write( | ||
| event = rawBatchEvent, | ||
| batchMetadata = null, | ||
| eventType = EventType.DEFAULT | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| /* | ||
| * 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.android.flags.internal.net | ||
|
|
||
| import com.datadog.android.api.InternalLogger | ||
| import com.datadog.android.api.context.DatadogContext | ||
| import com.datadog.android.api.net.RequestExecutionContext | ||
| import com.datadog.android.api.net.RequestFactory | ||
| import com.datadog.android.api.storage.RawBatchEvent | ||
| import com.datadog.android.core.internal.utils.join | ||
| import com.datadog.android.flags.utils.forge.ForgeConfigurator | ||
| import fr.xgouchet.elmyr.Forge | ||
| import fr.xgouchet.elmyr.annotation.Forgery | ||
| import fr.xgouchet.elmyr.annotation.StringForgery | ||
| import fr.xgouchet.elmyr.junit5.ForgeConfiguration | ||
| import fr.xgouchet.elmyr.junit5.ForgeExtension | ||
| import org.assertj.core.api.Assertions.assertThat | ||
| import org.junit.jupiter.api.BeforeEach | ||
| import org.junit.jupiter.api.Test | ||
| import org.junit.jupiter.api.extension.ExtendWith | ||
| import org.mockito.Mock | ||
| import org.mockito.junit.jupiter.MockitoExtension | ||
|
|
||
| @ExtendWith(MockitoExtension::class, ForgeExtension::class) | ||
| @ForgeConfiguration(ForgeConfigurator::class) | ||
| internal class EvaluationsRequestFactoryTest { | ||
|
|
||
| private lateinit var testedFactory: EvaluationsRequestFactory | ||
|
|
||
| @Forgery | ||
| lateinit var fakeDatadogContext: DatadogContext | ||
|
|
||
| @Mock | ||
| lateinit var mockInternalLogger: InternalLogger | ||
|
|
||
| @BeforeEach | ||
| fun `set up`() { | ||
| testedFactory = EvaluationsRequestFactory( | ||
| internalLogger = mockInternalLogger, | ||
| customEvaluationEndpoint = null | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `M create a proper request W create()`( | ||
| @Forgery batchData: List<RawBatchEvent>, | ||
| @Forgery executionContext: RequestExecutionContext, | ||
| @StringForgery batchMetadata: String, | ||
| forge: Forge | ||
| ) { | ||
| // Given | ||
| val metaData = forge.aNullable { batchMetadata.toByteArray() } | ||
|
|
||
| // When | ||
| val request = testedFactory.create(fakeDatadogContext, executionContext, batchData, metaData) | ||
|
|
||
| // Then | ||
| requireNotNull(request) | ||
| assertThat(request.url).isEqualTo("${fakeDatadogContext.site.intakeEndpoint}/api/v2/flagevaluations") | ||
| assertThat(request.contentType).isEqualTo(RequestFactory.CONTENT_TYPE_TEXT_UTF8) | ||
| assertThat(request.headers.minus(RequestFactory.HEADER_REQUEST_ID)).isEqualTo( | ||
| mapOf( | ||
| RequestFactory.HEADER_API_KEY to fakeDatadogContext.clientToken, | ||
| RequestFactory.HEADER_EVP_ORIGIN to fakeDatadogContext.source, | ||
| RequestFactory.HEADER_EVP_ORIGIN_VERSION to fakeDatadogContext.sdkVersion | ||
| ) | ||
| ) | ||
| assertThat(request.headers[RequestFactory.HEADER_REQUEST_ID]).isNotEmpty() | ||
| assertThat(request.id).isEqualTo(request.headers[RequestFactory.HEADER_REQUEST_ID]) | ||
| assertThat(request.description).isEqualTo("Evaluation Request") | ||
| assertThat(request.body).isEqualTo( | ||
| batchData.map { it.data }.join( | ||
| separator = "\n".toByteArray(Charsets.UTF_8), | ||
| internalLogger = mockInternalLogger | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `M generate unique request IDs W create() { multiple calls }`( | ||
| @Forgery batchData: List<RawBatchEvent>, | ||
| @Forgery executionContext: RequestExecutionContext, | ||
| @StringForgery batchMetadata: String, | ||
| forge: Forge | ||
| ) { | ||
| // Given | ||
| val metadata = forge.aNullable { batchMetadata.toByteArray() } | ||
|
|
||
| // When | ||
| val request1 = testedFactory.create(fakeDatadogContext, executionContext, batchData, metadata) | ||
| val request2 = testedFactory.create(fakeDatadogContext, executionContext, batchData, metadata) | ||
|
|
||
| // Then | ||
| assertThat(request1.id).isNotEqualTo(request2.id) | ||
| assertThat(request1.headers[RequestFactory.HEADER_REQUEST_ID]) | ||
| .isNotEqualTo(request2.headers[RequestFactory.HEADER_REQUEST_ID]) | ||
| assertThat(request1.id).isEqualTo(request1.headers[RequestFactory.HEADER_REQUEST_ID]) | ||
| assertThat(request2.id).isEqualTo(request2.headers[RequestFactory.HEADER_REQUEST_ID]) | ||
| } | ||
|
|
||
| @Test | ||
| fun `M preserve batch data order W create() { multiple batch events }`( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it actually matter? should we preserve the order? |
||
| @Forgery executionContext: RequestExecutionContext, | ||
| @StringForgery batchMetadata: String, | ||
| forge: Forge | ||
| ) { | ||
| // Given | ||
| val event1 = RawBatchEvent(data = "event1".toByteArray()) | ||
| val event2 = RawBatchEvent(data = "event2".toByteArray()) | ||
| val event3 = RawBatchEvent(data = "event3".toByteArray()) | ||
| val batchData = listOf(event1, event2, event3) | ||
| val metadata = forge.aNullable { batchMetadata.toByteArray() } | ||
|
|
||
| // When | ||
| val request = testedFactory.create(fakeDatadogContext, executionContext, batchData, metadata) | ||
|
|
||
| // Then | ||
| val expectedBody = "event1\nevent2\nevent3".toByteArray() | ||
| assertThat(request.body).isEqualTo(expectedBody) | ||
| } | ||
|
|
||
| @Test | ||
| fun `M use custom endpoint W create() { custom endpoint provided }`( | ||
| @Forgery batchData: List<RawBatchEvent>, | ||
| @Forgery executionContext: RequestExecutionContext, | ||
| @StringForgery(regex = "https://[a-z]+\\.com(/[a-z]+)+") customEndpoint: String, | ||
| @StringForgery batchMetadata: String, | ||
| forge: Forge | ||
| ) { | ||
| // Given | ||
| val factoryWithCustomEndpoint = EvaluationsRequestFactory( | ||
| internalLogger = mockInternalLogger, | ||
| customEvaluationEndpoint = customEndpoint | ||
| ) | ||
| val metadata = forge.aNullable { batchMetadata.toByteArray() } | ||
|
|
||
| // When | ||
| val request = factoryWithCustomEndpoint.create(fakeDatadogContext, executionContext, batchData, metadata) | ||
|
|
||
| // Then | ||
| requireNotNull(request) | ||
| assertThat(request.url).isEqualTo(customEndpoint) | ||
| assertThat(request.contentType).isEqualTo(RequestFactory.CONTENT_TYPE_TEXT_UTF8) | ||
| assertThat(request.headers.minus(RequestFactory.HEADER_REQUEST_ID)).isEqualTo( | ||
| mapOf( | ||
| RequestFactory.HEADER_API_KEY to fakeDatadogContext.clientToken, | ||
| RequestFactory.HEADER_EVP_ORIGIN to fakeDatadogContext.source, | ||
| RequestFactory.HEADER_EVP_ORIGIN_VERSION to fakeDatadogContext.sdkVersion | ||
| ) | ||
| ) | ||
| assertThat(request.headers[RequestFactory.HEADER_REQUEST_ID]).isNotEmpty() | ||
| assertThat(request.id).isEqualTo(request.headers[RequestFactory.HEADER_REQUEST_ID]) | ||
| assertThat(request.description).isEqualTo("Evaluation Request") | ||
| assertThat(request.body).isEqualTo( | ||
| batchData.map { it.data }.join( | ||
| separator = "\n".toByteArray(Charsets.UTF_8), | ||
| internalLogger = mockInternalLogger | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.