Skip to content

Commit

Permalink
Merge pull request #168 from teogor/feature/core-analytics-module
Browse files Browse the repository at this point in the history
Feature: Analytics Integration for Enhanced Event Tracking
  • Loading branch information
teogor authored Oct 27, 2023
2 parents 807ab83 + 2279095 commit a75a53e
Show file tree
Hide file tree
Showing 33 changed files with 457 additions and 272 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import dev.teogor.ceres.framework.core.screen.showSettingsButton
import dev.teogor.ceres.framework.core.screen.toolbarTitle
import dev.teogor.ceres.framework.core.screen.toolbarTokens
import dev.teogor.ceres.monetisation.admob.formats.nativead.NativeAd
import dev.teogor.ceres.monetisation.ads.ExperimentalAdsControlApi
import dev.teogor.ceres.monetisation.messaging.ConsentManager
import dev.teogor.ceres.navigation.core.LocalNavigationParameters
import dev.teogor.ceres.navigation.core.utilities.toScreenName
Expand Down Expand Up @@ -113,7 +112,6 @@ fun handleOnboardingReset(): () -> Unit {
return resetOnboarding
}

@OptIn(ExperimentalAdsControlApi::class)
@Composable
private fun HomeScreen(
homeVM: HomeViewModel,
Expand Down
1 change: 1 addition & 0 deletions core/analytics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
97 changes: 97 additions & 0 deletions core/analytics/api/analytics.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
public abstract class dev/teogor/ceres/core/analytics/AnalyticsEvent {
public static final field $stable I
public abstract fun getParams ()Ljava/util/List;
public abstract fun getType ()Ldev/teogor/ceres/core/analytics/Types;
}

public final class dev/teogor/ceres/core/analytics/AnalyticsEvent$ScreenView : dev/teogor/ceres/core/analytics/AnalyticsEvent {
public static final field $stable I
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Ldev/teogor/ceres/core/analytics/AnalyticsEvent$ScreenView;
public static synthetic fun copy$default (Ldev/teogor/ceres/core/analytics/AnalyticsEvent$ScreenView;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ldev/teogor/ceres/core/analytics/AnalyticsEvent$ScreenView;
public fun equals (Ljava/lang/Object;)Z
public fun getParams ()Ljava/util/List;
public final fun getScreenClass ()Ljava/lang/String;
public final fun getScreenName ()Ljava/lang/String;
public fun getType ()Ldev/teogor/ceres/core/analytics/Types;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/teogor/ceres/core/analytics/AnalyticsEventKt {
public static final fun toLowercaseString (Ldev/teogor/ceres/core/analytics/ParamKeys;)Ljava/lang/String;
public static final fun toLowercaseString (Ldev/teogor/ceres/core/analytics/Types;)Ljava/lang/String;
}

public abstract interface class dev/teogor/ceres/core/analytics/AnalyticsHelper {
public abstract fun logEvent (Ldev/teogor/ceres/core/analytics/AnalyticsEvent;)V
}

public final class dev/teogor/ceres/core/analytics/AnalyticsHelperKt {
public static final fun getAnalyticsProvider ()Ldev/teogor/ceres/core/analytics/AnalyticsProvider;
public static final fun getLocalAnalyticsHelper ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}

public class dev/teogor/ceres/core/analytics/AnalyticsProvider {
public static final field $stable I
public fun <init> ()V
public final fun addAnalyticsHelper (Ldev/teogor/ceres/core/analytics/AnalyticsHelper;)Ldev/teogor/ceres/core/analytics/AnalyticsHelper;
public final fun getAnalyticsHelper (Ljava/lang/Class;)Ldev/teogor/ceres/core/analytics/AnalyticsHelper;
public final fun getAnalyticsHelpers ()Ljava/util/List;
}

public final class dev/teogor/ceres/core/analytics/AnalyticsProviderKt {
public static final fun get (Ldev/teogor/ceres/core/analytics/AnalyticsProvider;Lkotlin/reflect/KClass;)Ldev/teogor/ceres/core/analytics/AnalyticsHelper;
public static final fun plusAssign (Ldev/teogor/ceres/core/analytics/AnalyticsProvider;Ldev/teogor/ceres/core/analytics/AnalyticsHelper;)V
}

public final class dev/teogor/ceres/core/analytics/DefaultAnalyticsHelper : dev/teogor/ceres/core/analytics/AnalyticsHelper {
public static final field $stable I
public fun <init> ()V
public fun logEvent (Ldev/teogor/ceres/core/analytics/AnalyticsEvent;)V
}

public abstract interface annotation class dev/teogor/ceres/core/analytics/ExperimentalAnalyticsApi : java/lang/annotation/Annotation {
}

public final class dev/teogor/ceres/core/analytics/Param {
public static final field $stable I
public fun <init> (Ldev/teogor/ceres/core/analytics/ParamKeys;Ljava/lang/String;)V
public final fun component1 ()Ldev/teogor/ceres/core/analytics/ParamKeys;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ldev/teogor/ceres/core/analytics/ParamKeys;Ljava/lang/String;)Ldev/teogor/ceres/core/analytics/Param;
public static synthetic fun copy$default (Ldev/teogor/ceres/core/analytics/Param;Ldev/teogor/ceres/core/analytics/ParamKeys;Ljava/lang/String;ILjava/lang/Object;)Ldev/teogor/ceres/core/analytics/Param;
public fun equals (Ljava/lang/Object;)Z
public final fun getKey ()Ldev/teogor/ceres/core/analytics/ParamKeys;
public final fun getValue ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/teogor/ceres/core/analytics/ParamKeys : java/lang/Enum {
public static final field SCREEN_CLASS Ldev/teogor/ceres/core/analytics/ParamKeys;
public static final field SCREEN_NAME Ldev/teogor/ceres/core/analytics/ParamKeys;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Ldev/teogor/ceres/core/analytics/ParamKeys;
public static fun values ()[Ldev/teogor/ceres/core/analytics/ParamKeys;
}

public final class dev/teogor/ceres/core/analytics/StubAnalyticsHelper : dev/teogor/ceres/core/analytics/AnalyticsHelper {
public static final field $stable I
public fun <init> ()V
public fun logEvent (Ldev/teogor/ceres/core/analytics/AnalyticsEvent;)V
}

public final class dev/teogor/ceres/core/analytics/Types : java/lang/Enum {
public static final field SCREEN_VIEW Ldev/teogor/ceres/core/analytics/Types;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Ldev/teogor/ceres/core/analytics/Types;
public static fun values ()[Ldev/teogor/ceres/core/analytics/Types;
}

public final class dev/teogor/ceres/core/analytics/composables/UiEventsKt {
public static final fun TrackScreenViewEvent (Ljava/lang/String;Ljava/lang/String;Ldev/teogor/ceres/core/analytics/AnalyticsHelper;Landroidx/compose/runtime/Composer;II)V
}

37 changes: 37 additions & 0 deletions core/analytics/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("dev.teogor.ceres.android.library")
id("dev.teogor.ceres.android.library.compose")
id("dev.teogor.ceres.android.library.jacoco")
}

android {
namespace = "dev.teogor.ceres.core.analytics"
defaultConfig {
consumerProguardFiles("consumer-proguard-rules.pro")
}
}

dependencies {
api(libs.androidx.annotation)
api(libs.androidx.compose.runtime)
api(libs.androidx.compose.ui.tooling)
}

ceresLibrary {
name = "Ceres Core Analytics"
}
Empty file.
21 changes: 21 additions & 0 deletions core/analytics/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
17 changes: 17 additions & 0 deletions core/analytics/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 teogor (Teodor Grigor)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2023 teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.teogor.ceres.core.analytics

enum class Types {
SCREEN_VIEW, // (extras: SCREEN_NAME, SCREEN_CLASS)
}

fun Types.toLowercaseString(): String {
return this.name.lowercase()
}

enum class ParamKeys {
SCREEN_NAME,
SCREEN_CLASS,
}

fun ParamKeys.toLowercaseString(): String {
return this.name.lowercase()
}

sealed class AnalyticsEvent {
abstract val type: Types
abstract val params: List<Param>

data class ScreenView(
val screenName: String,
val screenClass: String,
) : AnalyticsEvent() {
override val type: Types
get() = Types.SCREEN_VIEW

override val params: List<Param>
get() = listOf(
Param(ParamKeys.SCREEN_NAME, screenName),
Param(ParamKeys.SCREEN_CLASS, screenClass),
)
}
}

/**
* A key-value pair used to supply extra context to an analytics event.
*
* @param key - the parameter key. Wherever possible use one of the standard `ParamKeys`,
* however, if no suitable key is available you can define your own as long as it is configured
* in your backend analytics system (for example, by creating a Firebase Analytics custom
* parameter).
*
* @param value - the parameter value.
*/
data class Param(val key: ParamKeys, val value: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2023 teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.teogor.ceres.core.analytics

import androidx.compose.runtime.compositionLocalOf

@OptIn(ExperimentalAnalyticsApi::class)
val analyticsProvider = AnalyticsProvider()

interface AnalyticsHelper {
fun logEvent(analyticsEvent: AnalyticsEvent)
}

val LocalAnalyticsHelper = compositionLocalOf<AnalyticsHelper> {
DefaultAnalyticsHelper()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2023 teogor (Teodor Grigor)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.teogor.ceres.core.analytics

import androidx.annotation.RestrictTo
import kotlin.reflect.KClass

@ExperimentalAnalyticsApi
open class AnalyticsProvider {
private val _analyticsHelpers: MutableList<AnalyticsHelper> = mutableListOf()

@get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val analyticsHelpers: List<AnalyticsHelper>
get() = _analyticsHelpers.toList()

fun <T : AnalyticsHelper> getAnalyticsHelper(helperClass: Class<T>): T {
return _analyticsHelpers
.filterIsInstance(helperClass)
.firstOrNull()
?: throw RuntimeException("AnalyticsHelper of type $helperClass not found.")
}

fun addAnalyticsHelper(helper: AnalyticsHelper): AnalyticsHelper {
val existingHelper = _analyticsHelpers.filterIsInstance(helper::class.java).firstOrNull()
if (existingHelper == null) {
_analyticsHelpers.add(helper)
}
return existingHelper ?: helper
}
}

@ExperimentalAnalyticsApi
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T : AnalyticsHelper> AnalyticsProvider.get(
clazz: KClass<T>,
): T = getAnalyticsHelper(clazz.java)

@ExperimentalAnalyticsApi
@Suppress("NOTHING_TO_INLINE")
inline operator fun AnalyticsProvider.plusAssign(helper: AnalyticsHelper) {
addAnalyticsHelper(helper)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
* limitations under the License.
*/

package dev.teogor.ceres.firebase.analytics
package dev.teogor.ceres.core.analytics

/**
* Interface for logging analytics events. See `FirebaseAnalyticsHelper` and
* `StubAnalyticsHelper` for implementations.
*/
interface AnalyticsHelper {
fun logEvent(event: AnalyticsEvent)
class DefaultAnalyticsHelper : AnalyticsHelper {

@ExperimentalAnalyticsApi
override fun logEvent(
analyticsEvent: AnalyticsEvent,
) {
analyticsProvider.analyticsHelpers.forEach {
it.logEvent(analyticsEvent)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
* limitations under the License.
*/

package dev.teogor.ceres.firebase.analytics
package dev.teogor.ceres.core.analytics

/**
* Implementation of AnalyticsHelper which does nothing. Useful for tests and previews.
*/
class NoOpAnalyticsHelper : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) = Unit
}
@RequiresOptIn(message = "Analytics is experimental. The API may be changed in the future.")
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalAnalyticsApi
Loading

0 comments on commit a75a53e

Please sign in to comment.