Skip to content

Commit

Permalink
Merge pull request #1757 from embrace-io/disable-sdk
Browse files Browse the repository at this point in the history
Add ability to disable data export
  • Loading branch information
fractalwrench authored Dec 16, 2024
2 parents aba5549 + a62357c commit 9a865ad
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 40 deletions.
1 change: 1 addition & 0 deletions embrace-android-api/api/embrace-android-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/Bread
}

public abstract interface class io/embrace/android/embracesdk/internal/api/EmbraceAndroidApi {
public abstract fun disable ()V
public abstract fun endView (Ljava/lang/String;)Z
public abstract fun start (Landroid/content/Context;)V
public abstract fun start (Landroid/content/Context;Lio/embrace/android/embracesdk/AppFramework;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,19 @@ public interface EmbraceAndroidApi {
* @param name the name of the view to log
*/
public fun endView(name: String): Boolean

/**
* If a user wishes to opt-out of exporting data to Embrace, you should:
*
* (1) Persist this user preference somewhere that can be readily accessed between processes
* (2) On the next process launch, read this preference & only initialize the Embrace SDK if you wish to capture data
* (3) Call this function if the SDK has already been initialized.
*
* When the SDK has already been initialized this function will prevent the SDK from exporting any further data
* via HTTP requests or OTel exports, and will delete any persisted data that has not yet been exported.
*
* The SDK makes a best effort attempt. Some data capture/handlers may remain active until the next process launch
* due to technical reasons, but any captured data will not be exported.
*/
public fun disable()
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import io.opentelemetry.sdk.logs.export.LogRecordExporter
internal class EmbraceLogRecordExporter(
private val logSink: LogSink,
private val externalLogRecordExporter: LogRecordExporter,
private val exportCheck: () -> Boolean,
) : LogRecordExporter {

override fun export(logs: Collection<LogRecordData>): CompletableResultCode {
if (!exportCheck()) {
return CompletableResultCode.ofSuccess()
}
val result = logSink.storeLogs(logs.toList())
if (result == CompletableResultCode.ofSuccess()) {
return externalLogRecordExporter.export(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,19 @@ class OpenTelemetryConfiguration(
private val externalSpanExporters = mutableListOf<SpanExporter>()
private val externalLogExporters = mutableListOf<LogRecordExporter>()

private var exportEnabled: Boolean = true
private val exportCheck: () -> Boolean = { exportEnabled }

fun disableDataExport() {
exportEnabled = false
}

val spanProcessor: SpanProcessor by lazy {
EmbraceSpanProcessor(
EmbraceSpanExporter(
spanSink = spanSink,
externalSpanExporter = SpanExporter.composite(externalSpanExporters)
externalSpanExporter = SpanExporter.composite(externalSpanExporters),
exportCheck = exportCheck,
),
processIdentifier
)
Expand All @@ -69,7 +77,8 @@ class OpenTelemetryConfiguration(
EmbraceLogRecordProcessor(
EmbraceLogRecordExporter(
logSink = logSink,
externalLogRecordExporter = LogRecordExporter.composite(externalLogExporters)
externalLogRecordExporter = LogRecordExporter.composite(externalLogExporters),
exportCheck = exportCheck,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import io.opentelemetry.sdk.trace.export.SpanExporter
internal class EmbraceSpanExporter(
private val spanSink: SpanSink,
private val externalSpanExporter: SpanExporter,
private val exportCheck: () -> Boolean,
) : SpanExporter {
@Synchronized
override fun export(spans: MutableCollection<SpanData>): CompletableResultCode {
if (!exportCheck()) {
return CompletableResultCode.ofSuccess()
}
val result = spanSink.storeCompletedSpans(spans.toList())
if (result == CompletableResultCode.ofSuccess()) {
return externalSpanExporter.export(spans.filterNot { it.hasFixedAttribute(PrivateSpan) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal class EmbraceLogRecordExporterTest {
fun `export() should store logs in LogSink`() {
val logSink: LogSink = LogSinkImpl()
val embraceLogRecordExporter =
EmbraceLogRecordExporter(logSink, LogRecordExporter.composite(emptyList()))
EmbraceLogRecordExporter(logSink, LogRecordExporter.composite(emptyList())) { true }
val logRecordData = FakeLogRecordData()

embraceLogRecordExporter.export(listOf(logRecordData))
Expand All @@ -31,7 +31,7 @@ internal class EmbraceLogRecordExporterTest {
val logSink: LogSink = LogSinkImpl()
val externalExporter = FakeLogRecordExporter()
val embraceLogRecordExporter =
EmbraceLogRecordExporter(logSink, LogRecordExporter.composite(externalExporter))
EmbraceLogRecordExporter(logSink, LogRecordExporter.composite(externalExporter)) { true }
val logRecordData = FakeLogRecordData()
val privateLogRecordData = FakeLogRecordData(
log = testLog.copy(
Expand Down
1 change: 1 addition & 0 deletions embrace-android-sdk/api/embrace-android-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em
public fun clearUsername ()V
public fun createSpan (Ljava/lang/String;Lio/embrace/android/embracesdk/spans/AutoTerminationMode;)Lio/embrace/android/embracesdk/spans/EmbraceSpan;
public fun createSpan (Ljava/lang/String;Lio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/embrace/android/embracesdk/spans/AutoTerminationMode;)Lio/embrace/android/embracesdk/spans/EmbraceSpan;
public fun disable ()V
public fun endSession ()V
public fun endSession (Z)V
public fun endView (Ljava/lang/String;)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,19 @@ internal class ActivityFeatureTest {
val message = getSingleSessionEnvelope()
val viewSpan = message.findSpanOfType(EmbType.Ux.View)

viewSpan.attributes?.assertMatches(mapOf(
"view.name" to "android.app.Activity"
))
viewSpan.attributes?.assertMatches(
mapOf(
"view.name" to "android.app.Activity"
)
)

with(viewSpan) {
assertEquals(startTimeMs, startTimeNanos?.nanosToMillis())
assertEquals(startTimeMs + 30000L, endTimeNanos?.nanosToMillis())
}
},
otelExportAssertion = {
val spans = awaitSpansWithType(EmbType.Ux.View, 1)
val spans = awaitSpansWithType(1, EmbType.Ux.View)
assertSpansMatchGoldenFile(spans, "ux-view-export.json")
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.embrace.android.embracesdk.testcases.features

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.assertions.returnIfConditionMet
import io.embrace.android.embracesdk.fakes.FakeEmbLogger
import io.embrace.android.embracesdk.internal.delivery.storage.StorageLocation
import io.embrace.android.embracesdk.testframework.IntegrationTestRule
import java.io.File
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
internal class DisableSdkFeatureTest {

private companion object {
private const val TEST_PREFIX = "emb_test_"
private const val SPAN_1 = "${TEST_PREFIX}1"
private const val SPAN_2 = "${TEST_PREFIX}2"
private const val SPAN_3 = "${TEST_PREFIX}3"
private const val LOG_1 = "${TEST_PREFIX}1"
private const val LOG_2 = "${TEST_PREFIX}2"
private const val LOG_3 = "${TEST_PREFIX}3"
private const val TEST_FILE_NAME = "test_file"
private const val DUMMY_CONTENT = "Hello, world!"
}

@Rule
@JvmField
val testRule: IntegrationTestRule = IntegrationTestRule()

private lateinit var embraceDirs: List<File>

@Before
fun setUp() {
val ctx = ApplicationProvider.getApplicationContext<Context>()
embraceDirs = StorageLocation.values().map { it.asFile(ctx, FakeEmbLogger()).value }
}

@Test
fun `disabling SDK stops data export`() {
testRule.runTest(
setupAction = {
// create some dummy values in embrace directories to see if they get deleted
embraceDirs.forEach {
File(it, TEST_FILE_NAME).writeText(DUMMY_CONTENT)
}
},
testCaseAction = {
embraceDirs.forEach {
assertEquals(DUMMY_CONTENT, File(it, TEST_FILE_NAME).readText())
}
recordSession {
embrace.startSpan(SPAN_1)?.stop()
embrace.logInfo(LOG_1)
embrace.startSpan(SPAN_2)?.stop()
embrace.logInfo(LOG_2)

// disable SDK at this point
embrace.disable()

// log some more data
embrace.startSpan(SPAN_3)?.stop()
embrace.logInfo(LOG_3)
}
},
assertAction = {
// ensure that the files were deleted by waiting for the background thread
returnIfConditionMet(
desiredValueSupplier = { true },
dataProvider = {
embraceDirs.all {
!File(it, TEST_FILE_NAME).exists()
}
},
condition = { true },
)

assertEquals(0, getLogEnvelopes(0).size)
assertEquals(0, getSessionEnvelopes(0).size)
},
otelExportAssertion = {
val spanData = awaitSpans(2) { spanData ->
spanData.name.startsWith(TEST_PREFIX)
}
val spans = spanData.map { it.name }
assertEquals(listOf(SPAN_1, SPAN_2), spans)

val logData = awaitLogs(2) { logData ->
val msg = logData.bodyValue?.value.toString()
msg.startsWith(TEST_PREFIX)
}
assertEquals(listOf(LOG_1, LOG_2), logData.map { it.bodyValue?.value })
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class LowPowerFeatureTest {
assertEquals(startTimeMs + tickTimeMs, span.endTimeNanos?.nanosToMillis())
},
otelExportAssertion = {
val spans = awaitSpansWithType(EmbType.System.LowPower, 1)
val spans = awaitSpansWithType(1, EmbType.System.LowPower)
assertSpansMatchGoldenFile(spans, "system-low-power-export.json")
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.embrace.android.embracesdk.testframework.actions.EmbraceOtelExportAsse
import io.embrace.android.embracesdk.testframework.actions.EmbracePayloadAssertionInterface
import io.embrace.android.embracesdk.testframework.actions.EmbracePreSdkStartInterface
import io.embrace.android.embracesdk.testframework.actions.EmbraceSetupInterface
import io.embrace.android.embracesdk.testframework.export.FilteredLogExporter
import io.embrace.android.embracesdk.testframework.export.FilteredSpanExporter
import io.embrace.android.embracesdk.testframework.server.FakeApiServer
import java.io.File
Expand Down Expand Up @@ -98,6 +99,7 @@ internal class IntegrationTestRule(
lateinit var preSdkStart: EmbracePreSdkStartInterface
private lateinit var otelAssertion: EmbraceOtelExportAssertionInterface
private lateinit var spanExporter: FilteredSpanExporter
private lateinit var logExporter: FilteredLogExporter
private lateinit var embraceImpl: EmbraceImpl
private lateinit var baseUrl: String

Expand Down Expand Up @@ -139,14 +141,16 @@ internal class IntegrationTestRule(
action = EmbraceActionInterface(setup, bootstrapper)
payloadAssertion = EmbracePayloadAssertionInterface(bootstrapper, apiServer)
spanExporter = FilteredSpanExporter()
otelAssertion = EmbraceOtelExportAssertionInterface(spanExporter)
logExporter = FilteredLogExporter()
otelAssertion = EmbraceOtelExportAssertionInterface(spanExporter, logExporter)

setupAction(setup)
with(setup) {
embraceImpl = EmbraceImpl(bootstrapper)
EmbraceHooks.setImpl(embraceImpl)
preSdkStartAction(preSdkStart)
embraceImpl.addSpanExporter(spanExporter)
embraceImpl.addLogRecordExporter(logExporter)

// persist config here before the SDK starts up
persistConfig(persistedRemoteConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package io.embrace.android.embracesdk.testframework.actions

import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.testframework.export.ExportedSpanValidator
import io.embrace.android.embracesdk.testframework.export.FilteredLogExporter
import io.embrace.android.embracesdk.testframework.export.FilteredSpanExporter
import io.opentelemetry.sdk.logs.data.LogRecordData
import io.opentelemetry.sdk.trace.data.SpanData

/**
Expand All @@ -11,16 +13,13 @@ import io.opentelemetry.sdk.trace.data.SpanData
*/
internal class EmbraceOtelExportAssertionInterface(
private val spanExporter: FilteredSpanExporter,
private val logExporter: FilteredLogExporter,
private val validator: ExportedSpanValidator = ExportedSpanValidator(),
) {

/**
* Retrieves spans with the specified type and waits until either the expected
* number of spans is reached or a timeout is exceeded.
*/
fun awaitSpansWithType(type: EmbType, expectedCount: Int): List<SpanData> {
return spanExporter.awaitSpansWithType(type, expectedCount)
}
fun awaitLogs(expectedCount: Int, filter: (LogRecordData) -> Boolean) = logExporter.awaitLogs(expectedCount, filter)
fun awaitSpans(expectedCount: Int, filter: (SpanData) -> Boolean) = spanExporter.awaitSpans(expectedCount, filter)
fun awaitSpansWithType(expectedCount: Int, type: EmbType) = spanExporter.awaitSpansWithType(expectedCount, type)

/**
* Asserts that the provided spans match the golden file.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.embrace.android.embracesdk.testframework.export

import io.embrace.android.embracesdk.assertions.returnIfConditionMet
import io.opentelemetry.sdk.common.CompletableResultCode
import io.opentelemetry.sdk.logs.data.LogRecordData
import io.opentelemetry.sdk.logs.export.LogRecordExporter

internal class FilteredLogExporter: LogRecordExporter {

private val logData = mutableListOf<LogRecordData>()

override fun export(logs: MutableCollection<LogRecordData>): CompletableResultCode {
logData.addAll(logs)
return CompletableResultCode.ofSuccess()
}

override fun flush(): CompletableResultCode {
return CompletableResultCode.ofSuccess()
}

override fun shutdown(): CompletableResultCode {
return CompletableResultCode.ofSuccess()
}

fun awaitLogs(expectedCount: Int, filter: (LogRecordData) -> Boolean): List<LogRecordData> {
val supplier = { logData.filter(filter) }
return returnIfConditionMet(
desiredValueSupplier = supplier,
dataProvider = supplier,
condition = { data ->
data.size == expectedCount
},
errorMessageSupplier = {
"Timeout. Expected $expectedCount logs, but got ${supplier().size}."
}
)
}
}
Loading

0 comments on commit 9a865ad

Please sign in to comment.