diff --git a/flank_wrapper/build.gradle.kts b/flank_wrapper/build.gradle.kts index e5cb9cff8f..baa319e835 100644 --- a/flank_wrapper/build.gradle.kts +++ b/flank_wrapper/build.gradle.kts @@ -24,7 +24,7 @@ shadowJar.apply { } } // .. -version = "1.2.1" +version = "1.2.2" group = "com.github.flank" repositories { diff --git a/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/CrashReporter.kt b/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/CrashReporter.kt index 1d42c9b64d..e975d9a06d 100644 --- a/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/CrashReporter.kt +++ b/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/CrashReporter.kt @@ -1,7 +1,7 @@ package com.github.flank.wrapper.internal import flank.common.config.isTest -import flank.tool.analytics.mixpanel.sessionId +import flank.tool.analytics.mixpanel.Mixpanel import io.sentry.Sentry private const val SESSION_ID = "session.id" @@ -17,7 +17,7 @@ fun setupCrashReporter() { } logTags( - SESSION_ID to sessionId, + SESSION_ID to Mixpanel.sessionId, OS_NAME to osName, FLANK_WRAPPER_VERSION to flankWrapperVersion, ) diff --git a/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/FlankWrapperAnalytics.kt b/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/FlankWrapperAnalytics.kt index 9b8a636199..76be4d0353 100644 --- a/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/FlankWrapperAnalytics.kt +++ b/flank_wrapper/src/main/kotlin/com/github/flank/wrapper/internal/FlankWrapperAnalytics.kt @@ -1,20 +1,19 @@ package com.github.flank.wrapper.internal -import flank.tool.analytics.mixpanel.send -import flank.tool.analytics.mixpanel.toEvent +import flank.tool.analytics.mixpanel.Mixpanel private const val FLANK_WRAPPER = "flank_wrapper" private const val EVENT_RUN = "flank run" private const val EVENT_NEW_FLANK_VERSION_DOWNLOADED = "new_version_downloaded" internal fun sendAnalyticsNewFlankVersionDownloaded() { - eventWithoutProperties(EVENT_NEW_FLANK_VERSION_DOWNLOADED).send() + Mixpanel.configure(FLANK_WRAPPER) + Mixpanel.add(FLANK_WRAPPER, EVENT_NEW_FLANK_VERSION_DOWNLOADED) + Mixpanel.send(FLANK_WRAPPER) } internal fun sendAnalyticsFlankRun() { - eventWithoutProperties(EVENT_RUN).send() + Mixpanel.configure(FLANK_WRAPPER) + Mixpanel.add(FLANK_WRAPPER, EVENT_RUN) + Mixpanel.send(FLANK_WRAPPER) } - -private fun eventWithoutProperties( - eventName: String -) = emptyMap().toEvent(FLANK_WRAPPER, eventName) diff --git a/flank_wrapper/src/main/resources/version.txt b/flank_wrapper/src/main/resources/version.txt index cb174d58a5..d2d61a7e8e 100644 --- a/flank_wrapper/src/main/resources/version.txt +++ b/flank_wrapper/src/main/resources/version.txt @@ -1 +1 @@ -1.2.1 \ No newline at end of file +1.2.2 \ No newline at end of file diff --git a/test_runner/src/main/kotlin/ftl/analytics/InitUsageStatistics.kt b/test_runner/src/main/kotlin/ftl/analytics/InitUsageStatistics.kt index 490b53f7a9..fd3c2a5277 100644 --- a/test_runner/src/main/kotlin/ftl/analytics/InitUsageStatistics.kt +++ b/test_runner/src/main/kotlin/ftl/analytics/InitUsageStatistics.kt @@ -1,17 +1,14 @@ package ftl.analytics -import flank.tool.analytics.mixpanel.analyticsReport -import flank.tool.analytics.mixpanel.configure -import flank.tool.analytics.mixpanel.initializeStatisticsClient +import flank.tool.analytics.mixpanel.Mixpanel import ftl.args.AndroidArgs import ftl.args.IArgs import ftl.args.IosArgs -import ftl.util.isGoogleAnalyticsDisabled internal fun IArgs.initUsageStatistics() { - analyticsReport.configure(project) - initializeStatisticsClient( - disableUsageStatistics || isGoogleAnalyticsDisabled(flank.common.userHome), + Mixpanel.configure( + projectName = project, + blockUsageStatistics = disableUsageStatistics, AndroidArgs::class, IosArgs::class, IArgs::class diff --git a/test_runner/src/main/kotlin/ftl/analytics/SendConfiguration.kt b/test_runner/src/main/kotlin/ftl/analytics/SendConfiguration.kt index d00c48c2e1..a706483659 100644 --- a/test_runner/src/main/kotlin/ftl/analytics/SendConfiguration.kt +++ b/test_runner/src/main/kotlin/ftl/analytics/SendConfiguration.kt @@ -1,17 +1,11 @@ package ftl.analytics -import com.google.common.annotations.VisibleForTesting -import flank.tool.analytics.mixpanel.FIREBASE -import flank.tool.analytics.mixpanel.FLANK_VERSION -import flank.tool.analytics.mixpanel.SESSION_ID -import flank.tool.analytics.mixpanel.TEST_PLATFORM -import flank.tool.analytics.mixpanel.add -import flank.tool.analytics.mixpanel.analyticsReport -import flank.tool.analytics.mixpanel.filterSensitiveValues -import flank.tool.analytics.mixpanel.objectToMap -import flank.tool.analytics.mixpanel.removeNotNeededKeys -import flank.tool.analytics.mixpanel.schemaVersion -import flank.tool.analytics.mixpanel.sessionId +import flank.tool.analytics.mixpanel.Mixpanel +import flank.tool.analytics.mixpanel.Mixpanel.CONFIGURATION +import flank.tool.analytics.mixpanel.Mixpanel.DEVICE_TYPES +import flank.tool.analytics.mixpanel.Mixpanel.add +import flank.tool.analytics.mixpanel.Mixpanel.removeSensitiveValues +import flank.tool.analytics.mixpanel.toMap import ftl.args.AndroidArgs import ftl.args.IArgs import ftl.args.IosArgs @@ -20,36 +14,23 @@ import ftl.environment.VIRTUAL_DEVICE import ftl.util.readVersion fun AndroidArgs.reportConfiguration() { + initUsageStatistics() addCommonData() - .add("configuration", createEventMap()) - .add("device_types", devices.map { if (it.isVirtual) VIRTUAL_DEVICE else PHYSICAL_DEVICE }.distinct()) + add(CONFIGURATION, removeSensitiveValues(toMap() + commonArgs.toMap())) + add(DEVICE_TYPES, devices.map { if (it.isVirtual) VIRTUAL_DEVICE else PHYSICAL_DEVICE }.distinct()) } -internal fun AndroidArgs.createEventMap() = - toArgsMap().plus(commonArgs.toArgsMap()).removeNotNeededKeys().filterSensitiveValues() - fun IosArgs.reportConfiguration() { + initUsageStatistics() addCommonData() - .add("configuration", createEventMap()) - .add("device_types", listOf(PHYSICAL_DEVICE)) + add(CONFIGURATION, removeSensitiveValues(toMap() + commonArgs.toMap())) + add(DEVICE_TYPES, listOf(PHYSICAL_DEVICE)) } fun IArgs.addCommonData() = let { - initUsageStatistics() - analyticsReport - .add("schema_version", schemaVersion) - .add(FLANK_VERSION, readVersion()) - .add(SESSION_ID, sessionId) - .add(TEST_PLATFORM, FIREBASE) - .add("project_id", project) + add(Mixpanel.SCHEMA_VERSION, Mixpanel.schemaVersion) + add(Mixpanel.FLANK_VERSION, readVersion()) + add(Mixpanel.SESSION_ID, Mixpanel.sessionId) + add(Mixpanel.TEST_PLATFORM, Mixpanel.Platform.FIREBASE) + add(Mixpanel.PROJECT_ID, project) } - -private fun IosArgs.createEventMap() = - toArgsMap().plus(commonArgs.toArgsMap()).removeNotNeededKeys().filterSensitiveValues() - -private fun IArgs.toArgsMap() = objectToMap().filterNonCommonArgs() - -@VisibleForTesting -internal fun Map.filterNonCommonArgs() = filter { it.key != COMMON_ARGS } - -private const val COMMON_ARGS = "commonArgs" diff --git a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt index 571c445552..6a9da98ffa 100644 --- a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt @@ -1,5 +1,6 @@ package ftl.args +import com.fasterxml.jackson.annotation.JsonIgnore import flank.tool.analytics.AnonymizeInStatistics import flank.tool.analytics.IgnoreInStatistics import ftl.api.ShardChunks @@ -10,6 +11,7 @@ import ftl.run.model.AndroidTestShards import java.nio.file.Paths data class AndroidArgs( + @get:JsonIgnore val commonArgs: CommonArgs, @property:AnonymizeInStatistics diff --git a/test_runner/src/main/kotlin/ftl/args/IosArgs.kt b/test_runner/src/main/kotlin/ftl/args/IosArgs.kt index 14fd195d55..e4ef41ef9d 100644 --- a/test_runner/src/main/kotlin/ftl/args/IosArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/IosArgs.kt @@ -12,6 +12,7 @@ import ftl.run.exception.FlankConfigurationError import java.nio.file.Paths data class IosArgs( + @get:JsonIgnore val commonArgs: CommonArgs, @property:AnonymizeInStatistics diff --git a/test_runner/src/main/kotlin/ftl/ios/xctest/XcTestData.kt b/test_runner/src/main/kotlin/ftl/ios/xctest/XcTestData.kt index fdbd4a4841..89b313fe05 100644 --- a/test_runner/src/main/kotlin/ftl/ios/xctest/XcTestData.kt +++ b/test_runner/src/main/kotlin/ftl/ios/xctest/XcTestData.kt @@ -1,9 +1,7 @@ package ftl.ios.xctest import com.dd.plist.NSDictionary -import flank.tool.analytics.mixpanel.APP_ID -import flank.tool.analytics.mixpanel.add -import flank.tool.analytics.mixpanel.analyticsReport +import flank.tool.analytics.mixpanel.Mixpanel import ftl.args.ArgsHelper.calculateShards import ftl.args.IosArgs import ftl.args.isXcTest @@ -61,7 +59,7 @@ private fun IosArgs.calculateXcTest(): XcTestRunData { ) } -private fun IosArgs.reportBundleId() = analyticsReport.add(APP_ID, getBundleId()) +private fun IosArgs.reportBundleId() = Mixpanel.add(Mixpanel.APP_ID, getBundleId()) private inline fun createCustomSharding(shardingJsonPath: String) = fromJson(Paths.get(shardingJsonPath).toFile().readText()) diff --git a/test_runner/src/main/kotlin/ftl/mock/MockServer.kt b/test_runner/src/main/kotlin/ftl/mock/MockServer.kt index b74f3c78f4..de6a0c4064 100644 --- a/test_runner/src/main/kotlin/ftl/mock/MockServer.kt +++ b/test_runner/src/main/kotlin/ftl/mock/MockServer.kt @@ -1,6 +1,9 @@ package ftl.mock +import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.readValue import com.google.api.services.toolresults.model.AppStartTime import com.google.api.services.toolresults.model.CPUInfo @@ -41,7 +44,6 @@ import com.google.testing.model.TestExecution import com.google.testing.model.TestMatrix import com.google.testing.model.ToolResultsExecution import com.google.testing.model.ToolResultsStep -import flank.tool.analytics.mixpanel.objectToMap import ftl.client.google.run.toClientInfoDetailList import ftl.config.FtlConstants import ftl.config.FtlConstants.JSON_FACTORY @@ -405,6 +407,10 @@ object MockServer { .setClientInfoDetails(allClientDetails.toClientInfoDetailList()) } + private fun Any.objectToMap(): Map = objectMapper.convertValue(this, object : TypeReference>() {}) + + private val objectMapper by lazy { jsonMapper { addModule(kotlinModule()) } } + fun start() { if (isStarted) return val loggingEnabled = LogbackLogger.Root.isEnabled diff --git a/test_runner/src/main/kotlin/ftl/reports/CostReport.kt b/test_runner/src/main/kotlin/ftl/reports/CostReport.kt index f3ff267f03..3d8648fba3 100644 --- a/test_runner/src/main/kotlin/ftl/reports/CostReport.kt +++ b/test_runner/src/main/kotlin/ftl/reports/CostReport.kt @@ -1,8 +1,7 @@ package ftl.reports import flank.common.println -import flank.tool.analytics.mixpanel.add -import flank.tool.analytics.mixpanel.analyticsReport +import flank.tool.analytics.mixpanel.Mixpanel import ftl.api.JUnitTest import ftl.args.IArgs import ftl.config.FtlConstants.indent @@ -32,7 +31,7 @@ object CostReport : IReport { val physicalCost = calculatePhysicalCost(totalBillablePhysicalMinutes.toBigDecimal()) val total = calculateTotalCost(virtualCost, physicalCost) - analyticsReport.add( + Mixpanel.add( "cost", mapOf( "virtual" to virtualCost, @@ -41,7 +40,7 @@ object CostReport : IReport { ) ) - analyticsReport.add( + Mixpanel.add( "test_duration", mapOf( "virtual" to totalBillableVirtualMinutes, diff --git a/test_runner/src/main/kotlin/ftl/run/NewTestRun.kt b/test_runner/src/main/kotlin/ftl/run/NewTestRun.kt index da5d92a811..57d9971a99 100644 --- a/test_runner/src/main/kotlin/ftl/run/NewTestRun.kt +++ b/test_runner/src/main/kotlin/ftl/run/NewTestRun.kt @@ -1,8 +1,6 @@ package ftl.run -import flank.tool.analytics.mixpanel.add -import flank.tool.analytics.mixpanel.analyticsReport -import flank.tool.analytics.mixpanel.send +import flank.tool.analytics.mixpanel.Mixpanel import ftl.api.TestMatrix import ftl.args.AndroidArgs import ftl.args.IArgs @@ -47,12 +45,14 @@ suspend fun IArgs.newTestRun() = withTimeoutOrNull(parsedTimeout) { matrixMap.printMatricesWebLinks(project) outputReport.log(matrixMap) matrixMap.reportTestResults() - analyticsReport.send() + Mixpanel.send(FIREBASE_TEST_LAB_RUN) matrixMap.validate(ignoreFailedTests) addStepTime("Generating reports", duration) } } +private const val FIREBASE_TEST_LAB_RUN = "firebase test lab run" + private fun MatrixMap.reportTestResults() { val outcomes = map.flatMap { it.value.axes } val testsSummary = outcomes.map { it.suiteOverview }.fold(TestMatrix.SuiteOverview()) { result, overview -> @@ -66,12 +66,12 @@ private fun MatrixMap.reportTestResults() { overheadTime = result.overheadTime + overview.overheadTime ) } - analyticsReport.add( + Mixpanel.add( "shards_count", map.values.flatMap { it.testExecutions }.maxOf { testExecution -> testExecution.shardIndex ?: 0 } + 1 ) - analyticsReport.add("outcome", outcomes) - analyticsReport.add( + Mixpanel.add("outcome", outcomes) + Mixpanel.add( "tests", mapOf( "total" to testsSummary.total, diff --git a/test_runner/src/main/kotlin/ftl/run/common/SaveSessionId.kt b/test_runner/src/main/kotlin/ftl/run/common/SaveSessionId.kt index 2ae513acd2..e045b8cace 100644 --- a/test_runner/src/main/kotlin/ftl/run/common/SaveSessionId.kt +++ b/test_runner/src/main/kotlin/ftl/run/common/SaveSessionId.kt @@ -1,9 +1,9 @@ package ftl.run.common -import flank.tool.analytics.mixpanel.sessionId +import flank.tool.analytics.mixpanel.Mixpanel import ftl.args.IArgs import java.nio.file.Paths const val SESSION_ID_FILE = "session_id.txt" -fun IArgs.saveSessionId() = Paths.get(localResultDir, SESSION_ID_FILE).toFile().writeText(sessionId) +fun IArgs.saveSessionId() = Paths.get(localResultDir, SESSION_ID_FILE).toFile().writeText(Mixpanel.sessionId) diff --git a/test_runner/src/main/kotlin/ftl/run/platform/RunAndroidTests.kt b/test_runner/src/main/kotlin/ftl/run/platform/RunAndroidTests.kt index a928312d35..9cef1cde40 100644 --- a/test_runner/src/main/kotlin/ftl/run/platform/RunAndroidTests.kt +++ b/test_runner/src/main/kotlin/ftl/run/platform/RunAndroidTests.kt @@ -2,9 +2,7 @@ package ftl.run.platform import flank.common.join import flank.common.logLn -import flank.tool.analytics.mixpanel.APP_ID -import flank.tool.analytics.mixpanel.add -import flank.tool.analytics.mixpanel.analyticsReport +import flank.tool.analytics.mixpanel.Mixpanel import ftl.api.RemoteStorage import ftl.api.TestMatrixAndroid import ftl.api.executeTestMatrixAndroid @@ -105,4 +103,4 @@ private fun AndroidTestContext.reportPackageName() = when (this) { is GameLoopContext -> getAndroidAppDetails(app.remote) }.sendPackageName() -private fun String.sendPackageName() = analyticsReport.add(APP_ID, this) +private fun String.sendPackageName() = Mixpanel.add(Mixpanel.APP_ID, this) diff --git a/test_runner/src/main/kotlin/ftl/util/CrashReporter.kt b/test_runner/src/main/kotlin/ftl/util/CrashReporter.kt index a3d9956499..a26a960e9a 100644 --- a/test_runner/src/main/kotlin/ftl/util/CrashReporter.kt +++ b/test_runner/src/main/kotlin/ftl/util/CrashReporter.kt @@ -1,8 +1,7 @@ package ftl.util import flank.common.config.isTest -import flank.tool.analytics.mixpanel.SESSION_ID -import flank.tool.analytics.mixpanel.sessionId +import flank.tool.analytics.mixpanel.Mixpanel import io.sentry.Sentry import io.sentry.SentryLevel import java.io.File @@ -53,7 +52,7 @@ private fun initializeCrashReportWrapper() { it.release = readRevision() } setCrashReportTag( - SESSION_ID to sessionId, + Mixpanel.SESSION_ID to Mixpanel.sessionId, OS_NAME to System.getProperty("os.name"), FLANK_VERSION to readVersion(), FLANK_REVISION to readRevision() diff --git a/test_runner/src/main/kotlin/ftl/util/Utils.kt b/test_runner/src/main/kotlin/ftl/util/Utils.kt index 81f8382864..41b94f9cbb 100644 --- a/test_runner/src/main/kotlin/ftl/util/Utils.kt +++ b/test_runner/src/main/kotlin/ftl/util/Utils.kt @@ -4,7 +4,7 @@ package ftl.util import com.fasterxml.jackson.annotation.JsonProperty import flank.common.logLn -import flank.tool.analytics.mixpanel.sessionId +import flank.tool.analytics.mixpanel.Mixpanel import ftl.run.exception.FlankGeneralError import java.io.File import java.io.InputStream @@ -74,7 +74,7 @@ private fun getResource(name: String): InputStream { fun printVersionInfo() { logLn("version: ${readVersion()}") logLn("revision: ${readRevision()}") - logLn("session id: $sessionId") + logLn("session id: ${Mixpanel.sessionId}") logLn() } diff --git a/test_runner/src/test/kotlin/ftl/analytics/UsageStatisticsTest.kt b/test_runner/src/test/kotlin/ftl/analytics/UsageStatisticsTest.kt index 29ffda5686..153d9be876 100644 --- a/test_runner/src/test/kotlin/ftl/analytics/UsageStatisticsTest.kt +++ b/test_runner/src/test/kotlin/ftl/analytics/UsageStatisticsTest.kt @@ -1,16 +1,19 @@ package ftl.analytics import com.google.common.truth.Truth.assertThat -import flank.tool.analytics.mixpanel.send +import flank.tool.analytics.mixpanel.Mixpanel +import flank.tool.analytics.mixpanel.toMap import ftl.args.AndroidArgs +import ftl.args.IosArgs import ftl.test.util.FlankTestRunner import ftl.util.readVersion import io.mockk.every +import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify -import org.json.JSONObject import org.junit.After +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -27,7 +30,7 @@ class UsageStatisticsTest { val default = AndroidArgs.default() val args = default.copy(environmentVariables = mapOf("testKey" to "testValue", "testKey2" to "testValue2")) args.initUsageStatistics() - val nonDefaultArgs = args.createEventMap() + val nonDefaultArgs = Mixpanel.removeSensitiveValues(args.toMap()) (nonDefaultArgs["environmentVariables"] as? Map<*, *>)?.let { environmentVariables -> assertThat(environmentVariables.count()).isEqualTo(2) assertThat(environmentVariables.values.all { it == "..." }).isTrue() @@ -38,25 +41,52 @@ class UsageStatisticsTest { @Test fun `should not run send configuration if unit tests`() { - mockkStatic(JSONObject::send) + mockkObject(Mixpanel) + + every { Mixpanel.send(any()) } AndroidArgs.default().reportConfiguration() - verify(inverse = true) { any().send() } + verify(inverse = true) { Mixpanel.send(any()) } } @Test fun `should not run send configuration if disable statistic param set`() { - mockkStatic(JSONObject::send) + mockkObject(Mixpanel) mockkStatic(::readVersion) every { readVersion() } returns "test" + every { Mixpanel.send(any()) } AndroidArgs.default().run { copy(commonArgs = commonArgs.copy(disableUsageStatistics = true)) .reportConfiguration() } - verify(inverse = true) { any().send() } + verify(inverse = true) { Mixpanel.send(any()) } + } + + @Test + fun `should ignore commonArgs from AndroidArgs`() { + // given + val args = AndroidArgs.default() + + // when + val data = args.toMap() + + // then + assertTrue(args::commonArgs.name !in data) + } + + @Test + fun `should ignore commonArgs from IosArgs`() { + // given + val args = IosArgs.default() + + // when + val data = args.toMap() + + // then + assertTrue(args::commonArgs.name !in data) } } diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/AnalyticsReport.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/AnalyticsReport.kt deleted file mode 100644 index c8e87b4a27..0000000000 --- a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/AnalyticsReport.kt +++ /dev/null @@ -1,25 +0,0 @@ -package flank.tool.analytics.mixpanel - -const val schemaVersion = "1.0" - -var analyticsReport = AnalyticsReport() - private set - -data class AnalyticsReport( - val projectName: String = "", - val data: AnalyticsData = mapOf() -) - -private typealias AnalyticsData = Map - -fun AnalyticsReport.configure(projectName: String) { - analyticsReport = copy(projectName = projectName) -} - -fun AnalyticsReport.add(key: String, reportNode: Any) = also { - if (blockSendUsageStatistics.not()) { - analyticsReport = copy(data = analyticsReport.data + (key to reportNode)) - } -} - -fun AnalyticsReport.send() = sendConfiguration(analyticsReport.projectName, data) diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/Mixpanel.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/Mixpanel.kt new file mode 100644 index 0000000000..1e6942b895 --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/Mixpanel.kt @@ -0,0 +1,55 @@ +package flank.tool.analytics.mixpanel + +import flank.tool.analytics.mixpanel.internal.addToReport +import flank.tool.analytics.mixpanel.internal.anonymizeSensitiveValues +import flank.tool.analytics.mixpanel.internal.configureReport +import flank.tool.analytics.mixpanel.internal.objectToMap +import flank.tool.analytics.mixpanel.internal.removeNotNeededKeys +import flank.tool.analytics.mixpanel.internal.sendReport +import java.util.UUID +import kotlin.reflect.KClass + +object Mixpanel { + const val SESSION_ID = "session.id" + const val PROJECT_ID = "project_id" + const val PROJECT_NAME = "name" + const val SCHEMA_VERSION = "schema_version" + const val FLANK_VERSION = "flank_version" + const val TEST_PLATFORM = "test_platform" + const val CONFIGURATION = "configuration" + const val DEVICE_TYPES = "device_types" + const val APP_ID = "app_id" + + object Platform { + const val FIREBASE = "firebase" + const val CORELLIUM = "corellium" + } + + const val schemaVersion = "1.0" + + val sessionId by lazy { UUID.randomUUID().toString() } + + fun configure( + projectName: String, + blockUsageStatistics: Boolean = false, + vararg statisticClasses: KClass<*> + ): Unit = configureReport( + projectName = projectName, + blockUsageStatistics = blockUsageStatistics, + statisticClasses = statisticClasses + ) + + fun removeSensitiveValues(map: ObjectMap): ObjectMap = map.removeNotNeededKeys().anonymizeSensitiveValues() + + fun add(key: String, reportNode: Any): Unit = addToReport(key, reportNode) + + fun add(map: ObjectMap): Unit = addToReport(map) + + fun add(vararg entries: Pair): Unit = addToReport(entries.toMap()) + + fun send(eventName: String): Unit = sendReport(eventName) +} + +fun Any.toMap(): ObjectMap = objectToMap() + +typealias ObjectMap = Map diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/SendConfiguration.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/SendConfiguration.kt deleted file mode 100644 index 5898090012..0000000000 --- a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/SendConfiguration.kt +++ /dev/null @@ -1,24 +0,0 @@ -package flank.tool.analytics.mixpanel - -import flank.common.toJSONObject - -private const val PROJECT_ID = "project_id" -private const val NAME_KEY = "name" - -fun sendConfiguration( - project: String, - events: Map, - eventName: String = FIREBASE_TEST_LAB_RUN -) = - project.takeUnless { blockSendUsageStatistics || project.isBlank() }?.run { - registerUser(project) - events - .toEvent(project, eventName) - .send() - } - -private fun registerUser(project: String) { - messageBuilder.set( - project, mapOf(PROJECT_ID to project, NAME_KEY to project).toJSONObject() - ).send() -} diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/StatisticDataFilters.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/StatisticDataFilters.kt deleted file mode 100644 index e36d044268..0000000000 --- a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/StatisticDataFilters.kt +++ /dev/null @@ -1,47 +0,0 @@ -package flank.tool.analytics.mixpanel - -import flank.tool.analytics.AnonymizeInStatistics -import flank.tool.analytics.IgnoreInStatistics -import kotlin.reflect.KClass - -internal val keysToRemove by lazy { - getClassesForStatisticsOrThrow().map(findMembersWithAnnotation(IgnoreInStatistics::class)).flatten() -} - -internal val keysToAnonymize by lazy { - getClassesForStatisticsOrThrow().map(findMembersWithAnnotation(AnonymizeInStatistics::class)).flatten() -} - -private fun getClassesForStatisticsOrThrow() = - (classesForStatistics ?: throw NullPointerException("Analytics client should be initialized first")) - -internal var classesForStatistics: List>? = null - -private fun findMembersWithAnnotation( - annotationType: KClass<*> -): KClass<*>.() -> List = { - members.filter { member -> - member.annotations.any { annotation -> annotation.annotationClass == annotationType } - }.map { - it.name - } -} - -fun Map.removeNotNeededKeys() = - filterNot { (key, _) -> - key in keysToRemove - } - -fun Map.filterSensitiveValues() = mapValues { it.anonymousSensitiveValues() } - -private fun Map.Entry.anonymousSensitiveValues() = - if (keysToAnonymize.contains(key)) value.toAnonymous() - else value - -private fun Any.toAnonymous(): Any = when (this) { - is Map<*, *> -> mapValues { ANONYMIZE_VALUE } - is List<*> -> "Count: $size" - else -> ANONYMIZE_VALUE -} - -private const val ANONYMIZE_VALUE = "..." diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/UsageStatisticsClient.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/UsageStatisticsClient.kt deleted file mode 100644 index f2330d3834..0000000000 --- a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/UsageStatisticsClient.kt +++ /dev/null @@ -1,41 +0,0 @@ -package flank.tool.analytics.mixpanel - -import com.fasterxml.jackson.module.kotlin.jsonMapper -import com.fasterxml.jackson.module.kotlin.kotlinModule -import com.mixpanel.mixpanelapi.MessageBuilder -import com.mixpanel.mixpanelapi.MixpanelAPI -import flank.common.toJSONObject -import org.json.JSONObject -import kotlin.reflect.KClass - -private const val MIXPANEL_API_TOKEN = "d9728b2c8e6ca9fd6de1fcd32dd8cdc2" - -internal var blockSendUsageStatistics: Boolean = false - -internal val messageBuilder by lazy { - MessageBuilder(MIXPANEL_API_TOKEN) -} - -internal val apiClient by lazy { - MixpanelAPI() -} - -internal val objectMapper by lazy { - jsonMapper { - addModule(kotlinModule()) - } -} - -fun initializeStatisticsClient(blockUsageStatistics: Boolean, vararg statisticClasses: KClass<*>) { - if (classesForStatistics != null) return - - blockSendUsageStatistics = blockUsageStatistics - classesForStatistics = statisticClasses.asList() -} - -fun JSONObject.send() = apiClient.sendMessage(this) - -fun Map.toEvent(projectId: String, eventName: String): JSONObject = - (this + Pair(SESSION_ID, sessionId)).run { - messageBuilder.event(projectId, eventName, toJSONObject()) - } diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/Util.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/Util.kt deleted file mode 100644 index ce63992703..0000000000 --- a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/Util.kt +++ /dev/null @@ -1,25 +0,0 @@ -package flank.tool.analytics.mixpanel - -import com.fasterxml.jackson.core.type.TypeReference -import java.util.UUID - -const val FIREBASE_TEST_LAB_RUN = "firebase test lab run" -const val APP_ID = "app_id" -const val DEVICE_TYPE = "device_type" -const val FLANK_VERSION = "flank_version" -const val FLANK_VERSION_PROPERTY = "version" -const val TEST_PLATFORM = "test_platform" - -const val FIREBASE = "firebase" -const val CORELLIUM = "corellium" - -const val ANDROID = "android" -const val IOS = "ios" - -const val SESSION_ID = "session.id" - -val sessionId by lazy { - UUID.randomUUID().toString() -} - -fun Any.objectToMap(): Map = objectMapper.convertValue(this, object : TypeReference>() {}) diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Add.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Add.kt new file mode 100644 index 0000000000..427cf7bb12 --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Add.kt @@ -0,0 +1,11 @@ +package flank.tool.analytics.mixpanel.internal + +import flank.tool.analytics.mixpanel.ObjectMap + +internal fun addToReport(key: String, reportNode: Any) { + Report.data[key] = reportNode +} + +internal fun addToReport(map: ObjectMap) { + Report.data += map +} diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Configure.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Configure.kt new file mode 100644 index 0000000000..58dee02e9e --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Configure.kt @@ -0,0 +1,29 @@ +package flank.tool.analytics.mixpanel.internal + +import flank.tool.analytics.AnonymizeInStatistics +import flank.tool.analytics.IgnoreInStatistics +import kotlin.reflect.KClass + +internal fun configureReport( + projectName: String, + blockUsageStatistics: Boolean, + statisticClasses: Array> +) { + Report.projectName = projectName + Report.blockSendUsageStatistics = blockUsageStatistics + Report.keysToRemove = statisticClasses getMembersWith IgnoreInStatistics::class + Report.keysToAnonymize = statisticClasses getMembersWith AnonymizeInStatistics::class +} + +private infix fun Array>.getMembersWith( + annotationType: KClass<*> +): Set = + flatMap { type -> + type.members.filter { member -> + member.annotations.any { annotation -> + annotation.annotationClass == annotationType + } + } + }.map { member -> + member.name + }.toSet() diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Filter.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Filter.kt new file mode 100644 index 0000000000..ca30ddd950 --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Filter.kt @@ -0,0 +1,23 @@ +package flank.tool.analytics.mixpanel.internal + +import flank.tool.analytics.mixpanel.ObjectMap + +internal fun ObjectMap.removeNotNeededKeys( + keysToRemove: Set = Report.keysToRemove +): ObjectMap = + if (keysToRemove.isEmpty()) this + else filterNot { (key, _) -> key in keysToRemove } + +internal fun ObjectMap.anonymizeSensitiveValues( + keysToAnonymize: Set = Report.keysToAnonymize, + anonymousValue: String = "...", +): ObjectMap = + if (keysToAnonymize.isEmpty()) this + else mapValues { (key, value) -> + when { + key !in keysToAnonymize -> value + value is Map<*, *> -> value.mapValues { anonymousValue } + value is List<*> -> "Count: ${value.size}" + else -> anonymousValue + } + } diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Mapper.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Mapper.kt new file mode 100644 index 0000000000..e7eeb3eb48 --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Mapper.kt @@ -0,0 +1,11 @@ +package flank.tool.analytics.mixpanel.internal + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule +import flank.tool.analytics.mixpanel.ObjectMap + +internal fun Any.objectToMap(): ObjectMap = + objectMapper.convertValue(this, object : TypeReference() {}) + +private val objectMapper by lazy { jsonMapper { addModule(kotlinModule()) } } diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Report.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Report.kt new file mode 100644 index 0000000000..65a19fdb6c --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Report.kt @@ -0,0 +1,12 @@ +package flank.tool.analytics.mixpanel.internal + +internal object Report { + // Configuration + var projectName: String = "" + var blockSendUsageStatistics: Boolean = false + var keysToRemove: Set = emptySet() + var keysToAnonymize: Set = emptySet() + + // Accumulated data + val data: MutableMap = mutableMapOf() +} diff --git a/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Send.kt b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Send.kt new file mode 100644 index 0000000000..b3143952b5 --- /dev/null +++ b/tool/analytics/mixpanel/src/main/kotlin/flank/tool/analytics/mixpanel/internal/Send.kt @@ -0,0 +1,49 @@ +package flank.tool.analytics.mixpanel.internal + +import com.mixpanel.mixpanelapi.MessageBuilder +import com.mixpanel.mixpanelapi.MixpanelAPI +import flank.tool.analytics.mixpanel.Mixpanel.PROJECT_ID +import flank.tool.analytics.mixpanel.Mixpanel.PROJECT_NAME +import flank.tool.analytics.mixpanel.Mixpanel.SESSION_ID +import flank.tool.analytics.mixpanel.Mixpanel.sessionId +import org.json.JSONObject + +internal fun sendReport( + eventName: String, +) { + !Report.blockSendUsageStatistics || return + Report.projectName.isNotBlank() || return + listOf( + createUser(Report.projectName), + createEvent(Report.projectName, eventName, Report.data), + ).forEach(apiClient::sendMessage) +} + +private fun createUser( + project: String +): JSONObject = + messageBuilder.set( + project, + JSONObject( + mapOf( + PROJECT_ID to project, + PROJECT_NAME to project + ) + ) + ) + +private fun createEvent( + projectId: String, + eventName: String, + data: Map +): JSONObject = + messageBuilder.event( + projectId, + eventName, + JSONObject(data + (SESSION_ID to sessionId)) + ) + +private val messageBuilder by lazy { MessageBuilder(MIXPANEL_API_TOKEN) } +private val apiClient by lazy { MixpanelAPI() } + +private const val MIXPANEL_API_TOKEN = "d9728b2c8e6ca9fd6de1fcd32dd8cdc2"