├── .gitignore ├── .idea ├── .name ├── artifacts │ └── AnalyticsKit_Android_jar.xml ├── codeStyleSettings.xml ├── compiler.xml ├── copyright │ ├── BusyApache20.xml │ └── profiles_settings.xml ├── runConfigurations │ └── All_Unit_Tests.xml └── vcs.xml ├── LICENSE ├── README.md ├── analyticskit ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── analyticskit_android │ │ ├── AnalyticsEvent.kt │ │ ├── AnalyticsKit.kt │ │ ├── AnalyticsKitProvider.kt │ │ ├── CommonEvents.kt │ │ ├── ContentViewEvent.kt │ │ └── ErrorEvent.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── analyticskit_android │ ├── AnalyticsEventTest.kt │ ├── AnalyticsKitTest.kt │ ├── ContentViewEventTest.kt │ ├── ErrorEventTest.kt │ └── MockProvider.kt ├── build.gradle ├── firebase-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── firebase_provider │ │ └── FirebaseProvider.kt │ └── test │ ├── kotlin │ └── com │ │ └── busybusy │ │ └── firebase_provider │ │ └── FirebaseProviderTest.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── flurry-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── flurry_provider │ │ └── FlurryProvider.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── flurry_provider │ └── FlurryProviderTest.kt ├── google-analytics-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── google_analytics_provider │ │ └── GoogleAnalyticsProvider.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── google_analytics_provider │ └── GoogleAnalyticsProviderTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── graylog-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── graylog_provider │ │ ├── EventJsonizer.kt │ │ ├── GraylogProvider.kt │ │ ├── GraylogResponse.kt │ │ └── GraylogResponseListener.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── graylog_provider │ ├── EventJsonizerTest.kt │ ├── GraylogProviderFailedCallTest.kt │ └── GraylogProviderTest.kt ├── intercom-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── intercom_provider │ │ └── IntercomProvider.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── intercom_provider │ └── IntercomProviderTest.kt ├── kissmetrics-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── com │ │ └── busybusy │ │ └── analyticskit │ │ └── kissmetrics_provider │ │ └── KissMetricsProvider.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── analyticskit │ └── kissmetrics_provider │ └── KissMetricsProviderTest.kt ├── mixpanel-provider ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── mixpanel_provider │ │ └── MixpanelProvider.kt │ └── test │ └── kotlin │ └── com │ └── busybusy │ └── mixpanel_provider │ └── MixpanelProviderTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | 18 | # Build outputs 19 | build/ 20 | out/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # Idea project items 38 | .idea/ 39 | # Allow sharing of test run configurations through VCS 40 | !.idea/runConfigurations 41 | AnalyticsKit-Android.iml 42 | projectFilesBackup 43 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | AnalyticsKit-Android -------------------------------------------------------------------------------- /.idea/artifacts/AnalyticsKit_Android_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/out/artifacts/AnalyticsKit_Android_jar 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 177 | 179 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/BusyApache20.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/runConfigurations/All_Unit_Tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnalyticsKit-Android 2 | Analytics framework for Android 3 | 4 | ## Installation 5 | Add the JitPack repository to the end of your root build.gradle: 6 | ```groovy 7 | allprojects { 8 | repositories { 9 | ... 10 | maven { url "https://jitpack.io" } 11 | } 12 | } 13 | ``` 14 | 15 | In your module's build.gradle file, add the dependency: 16 | ```groovy 17 | dependencies { 18 | implementation "com.github.busybusy.AnalyticsKit-Android:analyticskit:0.10.0" 19 | ... 20 | } 21 | ``` 22 | 23 | You can include the implemented providers you want by adding them to the same dependency block: 24 | ```groovy 25 | dependencies { 26 | ... 27 | implementation "com.github.busybusy.AnalyticsKit-Android:mixpanel-provider:0.10.0" 28 | } 29 | ``` 30 | 31 | ## Usage 32 | In your Application's onCreate() method, register your provider SDKs as normal with your API keys. 33 | Then initialize AnalyticsKit-Android to work with those providers. 34 | 35 | ```kotlin 36 | AnalyticsKit.getInstance() 37 | .registerProvider(MixpanelProvider(MixpanelAPI.getInstance(this, MIXPANEL_TOKEN))) 38 | ``` 39 | 40 | Send events where appropriate in your application code. 41 | 42 | ```kotlin 43 | AnalyticsEvent("Your Event Name") 44 | .putAttribute("key", "value") 45 | .send() 46 | ``` 47 | 48 | The framework provides a ```ContentViewEvent``` to facilitate capturing content views: 49 | ```kotlin 50 | ContentViewEvent() 51 | .putAttribute("screen_name", "Dashboard") 52 | .putAttribute("category", "navigation") 53 | .send() 54 | ``` 55 | 56 | ### Event Priorities 57 | By default, AnalyticsEvent objects have priority 0. However, you can 58 | set any integer priority on your events. It is up to you to decide on your priority scheme 59 | and provider filtering. 60 | ```kotlin 61 | AnalyticsEvent("Readme Read Event") 62 | .putAttribute("read", true) 63 | .setPriority(7) 64 | .send() 65 | ``` 66 | 67 | By default, providers will log all events regardless of priority. If desired, you can 68 | configure providers with a ```PriorityFilter``` so that only events that pass the 69 | ```PriorityFilter```'s shouldLog() filter function will be logged by that provider. 70 | In the following example, only AnalyticsEvent objects with priority less than 10 will be 71 | logged by the Mixpanel provider: 72 | ```kotlin 73 | myMixpanelApi = MixpanelAPI.getInstance(this, "YOUR MIXPANEL API TOKEN") 74 | val filter = PriorityFilter { priorityLevel -> priorityLevel < 10 } 75 | val filteringProvider = MixpanelProvider(mixpanelApi = myMixpanelApi, priorityFilter = filter) 76 | AnalyticsKit.getInstance().registerProvider(filteringProvider) 77 | ``` 78 | 79 | ## License 80 | 81 | Licensed under the Apache License, Version 2.0 (the "License"); 82 | you may not use this file except in compliance with the License. 83 | You may obtain a copy of the License at 84 | 85 | http://www.apache.org/licenses/LICENSE-2.0 86 | 87 | Unless required by applicable law or agreed to in writing, software 88 | distributed under the License is distributed on an "AS IS" BASIS, 89 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 90 | See the License for the specific language governing permissions and 91 | limitations under the License. 92 | -------------------------------------------------------------------------------- /analyticskit/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | analyticskit.iml -------------------------------------------------------------------------------- /analyticskit/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.analyticskit_android' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | } 30 | buildTypes { 31 | release { 32 | debuggable false 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | debug { 37 | debuggable true 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility = 17 45 | targetCompatibility = 17 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | test.java.srcDirs += 'src/test/kotlin' 51 | } 52 | } 53 | 54 | dependencies { 55 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 56 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 57 | 58 | testImplementation "junit:junit:$rootProject.ext.junit_version" 59 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 60 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 61 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 62 | } 63 | 64 | afterEvaluate { 65 | publishing { 66 | publications { 67 | releaseAnalyticsKit(MavenPublication) { // Creates a Maven publication called "releaseAnalyticsKit". 68 | from components.release // Applies the component for the release build variant. 69 | 70 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 71 | artifactId = 'analyticskit' 72 | version = "$VERSION_NAME" 73 | } 74 | } 75 | } 76 | } 77 | 78 | task sourcesJar(type: Jar) { 79 | archiveClassifier.set("sources") 80 | from android.sourceSets.main.java.srcDirs 81 | } 82 | 83 | task javadoc(type: Javadoc) { 84 | failOnError false 85 | source = android.sourceSets.main.java.sourceFiles 86 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 87 | } 88 | 89 | task javadocJar(type: Jar, dependsOn: javadoc) { 90 | archiveClassifier.set("javadoc") 91 | from javadoc.destinationDir 92 | } 93 | 94 | artifacts { 95 | archives sourcesJar 96 | archives javadocJar 97 | } 98 | -------------------------------------------------------------------------------- /analyticskit/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Sources/android_sdk_mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /analyticskit/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /analyticskit/src/main/kotlin/analyticskit_android/AnalyticsEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import androidx.annotation.NonNull 19 | import java.io.Serializable 20 | 21 | /** 22 | * Defines information that is needed to distribute the event to the registered analytics providers. 23 | * 24 | * @author John Hunt on 3/5/16. 25 | */ 26 | open class AnalyticsEvent( 27 | @NonNull val name: String, 28 | var attributes: MutableMap? = null, 29 | var timed: Boolean = false, 30 | var priority: Int = 0, 31 | ) : Serializable { 32 | 33 | /** 34 | * Access the event name. 35 | * 36 | * @return the name of the custom event 37 | */ 38 | fun name(): String { 39 | return name 40 | } 41 | 42 | /** 43 | * Adds an attribute to the event. 44 | * 45 | * @param attributeName the name of the attribute (should be unique) 46 | * @param value the [Object] to associate with the name given 47 | * @return the [AnalyticsEvent] instance 48 | */ 49 | fun putAttribute(attributeName: String, value: Any): AnalyticsEvent { 50 | // guard clause - make sure the dictionary is initialized 51 | if (attributes == null) { 52 | attributes = LinkedHashMap() 53 | } 54 | attributes!![attributeName] = value 55 | return this 56 | } 57 | 58 | /** 59 | * Gets the priority of this event. 60 | * 61 | * @return the priority of the event. Returns `0` when [.setPriority] has not been called. 62 | */ 63 | @JvmName("getEventPriority") 64 | fun getPriority(): Int = priority 65 | 66 | /** 67 | * Sets the priority of the event. The event defaults to `0` when this method is not called. 68 | * 69 | * 70 | * **Note:** It is up to the developer to define what priority scheme to use (if any). 71 | * 72 | * @param priorityLevel the priority the event should use 73 | * @return the [AnalyticsEvent] instance (for builder-style convenience) 74 | */ 75 | fun setPriority(priorityLevel: Int): AnalyticsEvent { 76 | this.priority = priorityLevel 77 | return this 78 | } 79 | 80 | /** 81 | * Access a single attribute of this event. 82 | * 83 | * @param name the name the of the attribute to retrieve 84 | * @return the value associated with the given attribute name. 85 | * Returns `null` if the attribute has not been set. 86 | */ 87 | fun getAttribute(name: String): Any? { 88 | return if (attributes == null) null else attributes!![name] 89 | } 90 | 91 | /** 92 | * Access the attributes of this event. 93 | * @return A non-empty map of attributes set on this event. 94 | * Returns `null` if no attributes have been added to the event. 95 | */ 96 | @JvmName("getEventAttributes") 97 | fun getAttributes(): Map? = attributes 98 | 99 | /** 100 | * Indicates if this event is a timed event. 101 | * 102 | * @return `true` if the event has been set to be a timed event. Returns `false` otherwise. 103 | */ 104 | fun isTimed(): Boolean = timed 105 | 106 | /** 107 | * Sets whether this event should capture timing. 108 | * 109 | * @param timed `true` to set the event to track the time 110 | * @return the [AnalyticsEvent] instance 111 | */ 112 | fun setTimed(timed: Boolean): AnalyticsEvent { 113 | this.timed = timed 114 | return this 115 | } 116 | 117 | /** 118 | * Sends the event out to the registered/specified providers. 119 | * This is a convenience method that wraps [AnalyticsKit.logEvent] 120 | */ 121 | fun send(): AnalyticsEvent { 122 | AnalyticsKit.getInstance().logEvent(this) 123 | return this 124 | } 125 | 126 | override fun equals(other: Any?): Boolean { 127 | if (this === other) return true 128 | if (other !is AnalyticsEvent) return false 129 | 130 | if (name != other.name) return false 131 | if (attributes != other.attributes) return false 132 | if (timed != other.timed) return false 133 | if (priority != other.priority) return false 134 | 135 | return true 136 | } 137 | 138 | override fun hashCode(): Int { 139 | var result = name.hashCode() 140 | result = 31 * result + (attributes?.hashCode() ?: 0) 141 | result = 31 * result + timed.hashCode() 142 | result = 31 * result + priority 143 | return result 144 | } 145 | 146 | override fun toString(): String { 147 | return "AnalyticsEvent(name='$name', attributes=$attributes, timed=$timed, priorityLevel=$priority)" 148 | } 149 | 150 | companion object { 151 | @JvmStatic 152 | private val serialVersionUID: Long = 1 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /analyticskit/src/main/kotlin/analyticskit_android/AnalyticsKit.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | /** 19 | * Presents an interface for logging Analytics events across multiple analytics providers. 20 | * 21 | * @author John Hunt on 3/4/16. 22 | */ 23 | object AnalyticsKit { 24 | val providers: MutableSet = mutableSetOf() 25 | val timedEvents: MutableMap by lazy { mutableMapOf() } 26 | 27 | /** 28 | * Returns the AnalyticsKit singleton. 29 | * 30 | * @return the singleton instance 31 | */ 32 | fun getInstance(): AnalyticsKit = this 33 | 34 | /** 35 | * Registers an `AnalyticsKitProvider` instance to receive future events. 36 | * 37 | * @param provider the `AnalyticsKitProvider` to notify on future calls to [AnalyticsKit.logEvent]. 38 | * @return the `AnalyticsKit` instance so multiple calls to `registerProvider(AnalyticsKitProvider)` can be chained. 39 | */ 40 | fun registerProvider(provider: AnalyticsKitProvider): AnalyticsKit { 41 | providers.add(provider) 42 | return this 43 | } 44 | 45 | /** 46 | * Sends the given event to all registered analytics providers (OR just to select providers if the event has been set to restrict the providers). 47 | * 48 | * @param event the event to capture with analytics tools 49 | */ 50 | @Throws(IllegalStateException::class) 51 | fun logEvent(event: AnalyticsEvent) { 52 | if (event.isTimed()) { 53 | timedEvents[event.name()] = event 54 | } 55 | // No else needed: no need to worry about hanging on to this event 56 | 57 | for (provider in providers) { 58 | if (provider.getPriorityFilter().shouldLog(event.getPriority())) { 59 | provider.sendEvent(event) 60 | } 61 | // No else needed: the provider doesn't care about logging events of the specified priority 62 | } 63 | } 64 | 65 | /** 66 | * Marks the end of a timed event. 67 | * 68 | * @param eventName the unique name of the event that has finished 69 | */ 70 | @Throws(java.lang.IllegalStateException::class) 71 | fun endTimedEvent(eventName: String) { 72 | val timedEvent = timedEvents.remove(eventName) 73 | when { 74 | timedEvent != null -> for (provider in providers) { 75 | if (provider.getPriorityFilter().shouldLog(timedEvent.getPriority())) { 76 | provider.endTimedEvent(timedEvent) 77 | } 78 | // No else needed: the provider doesn't care about logging events of the specified priority 79 | } 80 | else -> error("Attempted ending an event that was never started (or was previously ended): $eventName") 81 | } 82 | } 83 | 84 | /** 85 | * Marks the end of a timed event. 86 | * 87 | * @param event the event that has finished 88 | */ 89 | fun endTimedEvent(event: AnalyticsEvent) = endTimedEvent(event.name()) 90 | } 91 | -------------------------------------------------------------------------------- /analyticskit/src/main/kotlin/analyticskit_android/AnalyticsKitProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import androidx.annotation.NonNull 19 | 20 | /** 21 | * Defines the interface for provider plugins to be used with AnalyticsKit-Android. 22 | * 23 | * 24 | * Note: in your provider implementation, make sure the underlying provider SDK calls are 25 | * executed asynchronously. Otherwise, you will have network operations running on the main thread. 26 | * 27 | * @author John Hunt on 3/5/16. 28 | */ 29 | interface AnalyticsKitProvider { 30 | /** 31 | * Returns the filter used to restrict events by priority. 32 | * 33 | * @return the [PriorityFilter] instance the provider is using to determine if an event of a given priority should be logged 34 | */ 35 | @NonNull 36 | fun getPriorityFilter() : PriorityFilter 37 | 38 | /** 39 | * Sends the event using provider-specific code. 40 | * 41 | * @param event an instantiated event 42 | */ 43 | fun sendEvent(event: AnalyticsEvent) 44 | 45 | /** 46 | * End the timed event. 47 | * 48 | * @param timedEvent the event which has finished 49 | */ 50 | fun endTimedEvent(timedEvent: AnalyticsEvent) 51 | 52 | /** 53 | * Defines the 'callback' interface providers will use to determine 54 | * how to handle events of various priorities. 55 | */ 56 | fun interface PriorityFilter { 57 | /** 58 | * Determines if a provider should log an event with a given priority 59 | * 60 | * @param priorityLevel the priority value from an [AnalyticsEvent] object 61 | * (Generally [AnalyticsEvent.getPriority]) 62 | * @return `true` if the event should be logged by the provider. 63 | * Returns `false` otherwise. 64 | */ 65 | fun shouldLog(priorityLevel: Int): Boolean 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /analyticskit/src/main/kotlin/analyticskit_android/CommonEvents.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.busybusy.analyticskit_android 18 | 19 | /** 20 | * Defines constants used for common [AnalyticsEvent] objects. 21 | * The goal is to facilitate provider implementation of these types of events. 22 | * 23 | * @author John Hunt on 3/16/16. 24 | */ 25 | interface CommonEvents { 26 | companion object { 27 | const val CONTENT_VIEW: String = "Content View" 28 | const val ERROR: String = "Error" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /analyticskit/src/main/kotlin/analyticskit_android/ContentViewEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | /** 19 | * Defines information that is needed to distribute a "Content View" event to the registered analytics providers. 20 | * 21 | * @param contentName The name/title of the content that is viewed 22 | * 23 | * @author John Hunt on 3/16/16. 24 | */ 25 | class ContentViewEvent(contentName: String) : AnalyticsEvent(CommonEvents.CONTENT_VIEW) { 26 | companion object { 27 | const val CONTENT_NAME = "contentName" 28 | } 29 | 30 | init { 31 | putAttribute(CONTENT_NAME, contentName) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /analyticskit/src/main/kotlin/analyticskit_android/ErrorEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | /** 19 | * Defines information that is needed to distribute an "Error" event to the registered analytics providers. 20 | * 21 | * @param eventName the name of the `ErrorEvent` 22 | * 23 | * @author John Hunt on 4/6/16. 24 | */ 25 | class ErrorEvent(eventName: String = CommonEvents.ERROR) : AnalyticsEvent(eventName) { 26 | val ERROR_MESSAGE = "error_message" 27 | val EXCEPTION_OBJECT = "exception_object" 28 | val ERROR_OBJECT = "error_object" 29 | 30 | /** 31 | * Sets an error message on the `ErrorEvent`. 32 | * 33 | * @param errorMessage the message to set 34 | * @return the `ErrorEvent` instance (for builder-style convenience) 35 | */ 36 | fun setMessage(errorMessage: String): ErrorEvent { 37 | putAttribute(ERROR_MESSAGE, errorMessage) 38 | return this 39 | } 40 | 41 | /** 42 | * Access the error message. 43 | * 44 | * @return the error message set on this event. Returns `null` if the message was not set. 45 | */ 46 | fun message(): String? = attributes?.get(ERROR_MESSAGE)?.toString() 47 | 48 | /** 49 | * Sets an `Exception` object to associate with this event. 50 | * 51 | * @param exception the Exception object to store 52 | * @return the `ErrorEvent` instance (for builder-style convenience) 53 | */ 54 | fun setException(exception: Exception): ErrorEvent { 55 | putAttribute(EXCEPTION_OBJECT, exception) 56 | return this 57 | } 58 | 59 | /** 60 | * Access the [Exception] object. 61 | * 62 | * @return the `Exception` set on this event. Returns `null` if the exception was not set. 63 | */ 64 | fun exception(): Exception? = attributes?.get(EXCEPTION_OBJECT) as? Exception 65 | 66 | /** 67 | * Sets an `Error` object to associate with this event. 68 | * 69 | * @param error the Error object to store 70 | * @return the `ErrorEvent` instance (for builder-style convenience) 71 | */ 72 | fun setError(error: Error): ErrorEvent { 73 | putAttribute(ERROR_OBJECT, error) 74 | return this 75 | } 76 | 77 | /** 78 | * Access the [Error] object. 79 | * 80 | * @return the `Error` set on this event. Returns `null` if the error was not set. 81 | */ 82 | fun error(): Error? = attributes?.get(ERROR_OBJECT) as? Error 83 | } 84 | -------------------------------------------------------------------------------- /analyticskit/src/test/kotlin/com/busybusy/analyticskit_android/AnalyticsEventTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.junit.Test 20 | 21 | /** 22 | * Tests the [AnalyticsEvent] class. 23 | * 24 | * @author John Hunt on 3/5/16. 25 | */ 26 | class AnalyticsEventTest { 27 | @Test 28 | fun testBuilder_withAttributes() { 29 | val name = "Yeah, this should work" 30 | val animalText = "The quick brown fox jumps over the lazy dog" 31 | val event = AnalyticsEvent(name) 32 | .putAttribute("the answer", 42) 33 | .putAttribute("animal text", animalText) 34 | assertThat(event.name()).isEqualTo(name) 35 | assertThat(event.attributes).isNotNull 36 | assertThat(event.attributes!!.keys).containsOnly("the answer", "animal text") 37 | assertThat(event.attributes!!.values).containsOnly(42, animalText) 38 | } 39 | 40 | @Test 41 | fun testBuilder_noAttributes() { 42 | val name = "Yeah, this should work" 43 | val event = AnalyticsEvent(name) 44 | assertThat(event.name()).isEqualTo(name) 45 | assertThat(event.attributes).isNull() 46 | } 47 | 48 | @Test 49 | fun testBuilder_specifyPriority() { 50 | val name = "Priority 7 event" 51 | val event = AnalyticsEvent(name) 52 | .setPriority(7) 53 | assertThat(event.name()).isEqualTo(name) 54 | assertThat(event.attributes).isNull() 55 | assertThat(event.priority).isEqualTo(7) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /analyticskit/src/test/kotlin/com/busybusy/analyticskit_android/AnalyticsKitTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 19 | import org.assertj.core.api.Assertions.assertThat 20 | import org.junit.Test 21 | 22 | /** 23 | * Tests the [AnalyticsKit] class. 24 | * 25 | * @author John Hunt on 3/7/16. 26 | */ 27 | class AnalyticsKitTest { 28 | @Test 29 | fun testGetInstance() { 30 | val analyticsKit = AnalyticsKit.getInstance() 31 | assertThat(analyticsKit).isNotNull 32 | assertThat(analyticsKit).isEqualTo(AnalyticsKit.getInstance()) 33 | } 34 | 35 | @Test 36 | fun testRegisterProvider() { 37 | val provider: AnalyticsKitProvider = object : AnalyticsKitProvider { 38 | override fun getPriorityFilter(): PriorityFilter = PriorityFilter { true } 39 | override fun sendEvent(event: AnalyticsEvent) {} // do nothing 40 | override fun endTimedEvent(timedEvent: AnalyticsEvent) {} // do nothing 41 | } 42 | AnalyticsKit.getInstance().registerProvider(provider) 43 | assertThat(AnalyticsKit.getInstance().providers).contains(provider) 44 | } 45 | 46 | @Test 47 | fun testPriorityFiltering_multiple() { 48 | val flurryProvider = MockProvider().setPriorityUpperBound(0) 49 | val customProviderOne = MockProvider().setPriorityUpperBound(3) 50 | val customProviderTwo = MockProvider().setPriorityUpperBound(5) 51 | AnalyticsKit.getInstance() 52 | .registerProvider(flurryProvider) 53 | .registerProvider(customProviderOne) 54 | .registerProvider(customProviderTwo) 55 | val eventName1 = "Custom Providers only" 56 | val customOneAndTwo = AnalyticsEvent(eventName1) 57 | .putAttribute("hello", "world") 58 | .setPriority(2) 59 | .send() 60 | assertThat(flurryProvider.sentEvents).isEmpty() 61 | assertThat(customProviderOne.sentEvents.keys).containsExactly(eventName1) 62 | assertThat(customProviderOne.sentEvents.values).containsExactly(customOneAndTwo) 63 | assertThat(customProviderTwo.sentEvents.keys).containsExactly(eventName1) 64 | assertThat(customProviderTwo.sentEvents.values).containsExactly(customOneAndTwo) 65 | } 66 | 67 | @Test 68 | fun testPriorityFiltering_none() { 69 | val flurryProvider = MockProvider() 70 | val customProviderOne = MockProvider() 71 | val customProviderTwo = MockProvider() 72 | AnalyticsKit.getInstance() 73 | .registerProvider(flurryProvider) 74 | .registerProvider(customProviderOne) 75 | .registerProvider(customProviderTwo) 76 | val eventName1 = "Flurry and Custom 1 only" 77 | AnalyticsEvent(eventName1) 78 | .putAttribute("hello", "world") 79 | .setPriority(1) 80 | .send() 81 | assertThat(flurryProvider.sentEvents).isEmpty() 82 | assertThat(customProviderOne.sentEvents).isEmpty() 83 | assertThat(customProviderTwo.sentEvents).isEmpty() 84 | } 85 | 86 | @Test 87 | fun testTimedEvent() { 88 | val flurryProvider = MockProvider() 89 | AnalyticsKit.getInstance().registerProvider(flurryProvider) 90 | val eventName1 = "Hello event" 91 | val flurryEvent = AnalyticsEvent(eventName1) 92 | .putAttribute("hello", "world") 93 | .setTimed(true) 94 | .send() 95 | assertThat(flurryProvider.sentEvents.keys).containsExactly(eventName1) 96 | assertThat(flurryProvider.sentEvents.values).containsExactly(flurryEvent) 97 | try { 98 | Thread.sleep(150, 0) 99 | } catch (e: InterruptedException) { 100 | e.printStackTrace() 101 | } 102 | AnalyticsKit.getInstance().endTimedEvent(flurryEvent) 103 | assertThat(flurryEvent.getAttribute(MockProvider.EVENT_DURATION)).isNotNull 104 | val duration: Long = flurryEvent.getAttribute(MockProvider.EVENT_DURATION) as Long 105 | assertThat(duration).isGreaterThanOrEqualTo(150L) 106 | } 107 | 108 | @Test 109 | fun testTimedEvent_manyProviders() { 110 | val flurryProvider = MockProvider() 111 | val customProviderOne = MockProvider() 112 | val customProviderTwo = MockProvider() 113 | AnalyticsKit.getInstance() 114 | .registerProvider(flurryProvider) 115 | .registerProvider(customProviderOne) 116 | .registerProvider(customProviderTwo) 117 | val eventName1 = "Flurry only event" 118 | val flurryEvent = AnalyticsEvent(eventName1) 119 | .putAttribute("hello", "world") 120 | .setTimed(true) 121 | .send() 122 | assertThat(flurryProvider.sentEvents.keys).containsExactly(eventName1) 123 | assertThat(flurryProvider.sentEvents.values).containsExactly(flurryEvent) 124 | assertThat(customProviderOne.sentEvents.keys).containsExactly(eventName1) 125 | assertThat(customProviderOne.sentEvents.values).containsExactly(flurryEvent) 126 | assertThat(customProviderTwo.sentEvents.keys).containsExactly(eventName1) 127 | assertThat(customProviderTwo.sentEvents.values).containsExactly(flurryEvent) 128 | try { 129 | Thread.sleep(150, 0) 130 | } catch (e: InterruptedException) { 131 | e.printStackTrace() 132 | } 133 | AnalyticsKit.getInstance().endTimedEvent(flurryEvent) 134 | assertThat(flurryEvent.getAttribute(MockProvider.EVENT_DURATION)).isNotNull 135 | val duration = flurryEvent.getAttribute(MockProvider.EVENT_DURATION) as Long 136 | assertThat(duration).isGreaterThanOrEqualTo(150L) 137 | } 138 | 139 | @Test 140 | @Throws(Exception::class) 141 | fun test_endTimeEvent_willThrow() { 142 | val flurryProvider = MockProvider() 143 | AnalyticsKit.getInstance() 144 | .registerProvider(flurryProvider) 145 | val eventName1 = "throwEvent" 146 | val flurryEvent = AnalyticsEvent(eventName1) 147 | .putAttribute("hello", "world") 148 | .send() 149 | var didThrow = false 150 | try { 151 | AnalyticsKit.getInstance().endTimedEvent(flurryEvent) 152 | } catch (e: IllegalStateException) { 153 | didThrow = true 154 | } 155 | assertThat(didThrow).isEqualTo(true) 156 | } 157 | 158 | @Test 159 | @Throws(Exception::class) 160 | fun test_endTimeEvent_willThrow_innerCase() { 161 | val flurryProvider = MockProvider() 162 | AnalyticsKit.getInstance() 163 | .registerProvider(flurryProvider) 164 | AnalyticsEvent("normalEvent") 165 | .putAttribute("hello", "world") 166 | .setTimed(true) 167 | .send() 168 | val eventName2 = "throwEvent" 169 | val throwEvent = AnalyticsEvent(eventName2) 170 | .putAttribute("hello", "world") 171 | .send() 172 | var didThrow = false 173 | try { 174 | AnalyticsKit.getInstance().endTimedEvent(throwEvent) 175 | } catch (e: IllegalStateException) { 176 | didThrow = true 177 | } 178 | assertThat(didThrow).isEqualTo(true) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /analyticskit/src/test/kotlin/com/busybusy/analyticskit_android/ContentViewEventTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.junit.Test 20 | 21 | /** 22 | * Tests the [ContentViewEvent] class. 23 | * 24 | * @author John Hunt on 3/16/16. 25 | */ 26 | class ContentViewEventTest { 27 | @Test 28 | fun testConstructor() { 29 | val event: AnalyticsEvent = ContentViewEvent("JUnit Test") 30 | assertThat(event.name()).isEqualTo(CommonEvents.CONTENT_VIEW) 31 | assertThat(event.getAttribute(ContentViewEvent.CONTENT_NAME)).isEqualTo("JUnit Test") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /analyticskit/src/test/kotlin/com/busybusy/analyticskit_android/ErrorEventTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.junit.Test 20 | 21 | /** 22 | * Tests the [ErrorEvent] class. 23 | * 24 | * @author John Hunt on 4/6/16. 25 | */ 26 | class ErrorEventTest { 27 | @Test 28 | fun testConstructor() { 29 | var event: AnalyticsEvent = ErrorEvent() 30 | assertThat(event.name()).isEqualTo(CommonEvents.ERROR) 31 | event = ErrorEvent("Custom Error Name") 32 | assertThat(event.name()).isEqualTo("Custom Error Name") 33 | assertThat(event.error()).isNull() 34 | } 35 | 36 | @Test 37 | fun testSetAndGetMessage() { 38 | val message = "Something went wrong" 39 | val event = ErrorEvent() 40 | assertThat(event.message()).isNull() 41 | event.setMessage(message) 42 | assertThat(event.message()).isEqualTo(message) 43 | } 44 | 45 | @Test 46 | fun testSetAndGetException() { 47 | val exception: Exception = StringIndexOutOfBoundsException() 48 | val event = ErrorEvent() 49 | assertThat(event.exception()).isNull() 50 | event.setException(exception) 51 | assertThat(event.exception()).isEqualTo(exception) 52 | } 53 | 54 | @Test 55 | fun testSetAndGetError() { 56 | val error: Error = UnknownError() 57 | val event = ErrorEvent() 58 | assertThat(event.error()).isNull() 59 | event.setError(error) 60 | assertThat(event.error()).isEqualTo(error) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /analyticskit/src/test/kotlin/com/busybusy/analyticskit_android/MockProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.analyticskit_android 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 19 | 20 | /** 21 | * Provides an implementation of the [AnalyticsKitProvider] interface that facilitates testing. 22 | * 23 | * @author John Hunt on 3/8/16. 24 | */ 25 | class MockProvider : AnalyticsKitProvider { 26 | var sentEvents: MutableMap = mutableMapOf() 27 | var eventTimes: MutableMap = mutableMapOf() 28 | var priorityLevel = 0 29 | var myPriorityFilter: PriorityFilter = PriorityFilter { priorityLevel -> 30 | priorityLevel <= this@MockProvider.priorityLevel 31 | } 32 | 33 | fun setPriorityUpperBound(priorityLevel: Int): MockProvider { 34 | this.priorityLevel = priorityLevel 35 | return this 36 | } 37 | 38 | override fun getPriorityFilter(): PriorityFilter = myPriorityFilter 39 | 40 | override fun sendEvent(event: AnalyticsEvent) { 41 | sentEvents[event.name()] = event 42 | if (event.isTimed()) { 43 | val startTime = System.currentTimeMillis() 44 | eventTimes[event.name()] = startTime 45 | } 46 | } 47 | 48 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 49 | val endTime = System.currentTimeMillis() 50 | val startTime = eventTimes.remove(timedEvent.name()) 51 | if (startTime != null) { 52 | timedEvent.putAttribute(EVENT_DURATION, endTime - startTime) 53 | } 54 | } 55 | 56 | companion object { 57 | const val EVENT_DURATION = "event_duration" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018, 2020-2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | buildscript { 18 | ext { 19 | kotlin_version = '1.9.10' 20 | } 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | dependencies { 26 | classpath 'com.android.tools.build:gradle:8.0.2' 27 | classpath 'com.google.gms:google-services:4.3.15' 28 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10" 29 | } 30 | } 31 | 32 | plugins { 33 | id 'maven-publish' 34 | } 35 | 36 | allprojects { 37 | repositories { 38 | google() 39 | mavenCentral() 40 | maven { url "https://jitpack.io" } 41 | } 42 | } 43 | 44 | ext { 45 | /** Android Library/Application Config values **/ 46 | compileSdkVersion = 33 47 | minSdkVersion = 16 48 | targetSdkVersion = 33 49 | buildToolsVersion = "33.0.2" 50 | versionCode = 12 51 | versionName = "$VERSION_NAME" 52 | 53 | // dependency versions 54 | annotationsVersion = "1.6.0" 55 | assertjVersion = "3.24.2" 56 | kotlinVersion = "1.9.10" 57 | mockitoVersion = "4.6.1" 58 | mockitoKotlinVersion = "2.2.0" 59 | 60 | analytics_kit = ':analyticskit' 61 | 62 | /** Unit Testing Dependency Versions **/ 63 | junit_version = "4.13.2" 64 | } 65 | 66 | task clean(type: Delete) { 67 | delete rootProject.buildDir 68 | } 69 | -------------------------------------------------------------------------------- /firebase-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | firebase-provider.iml -------------------------------------------------------------------------------- /firebase-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018, 2020-2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.firebase_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | 30 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 31 | 32 | } 33 | 34 | buildTypes { 35 | release { 36 | debuggable false 37 | minifyEnabled false 38 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 39 | } 40 | debug { 41 | debuggable true 42 | minifyEnabled false 43 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 44 | } 45 | } 46 | 47 | compileOptions { 48 | sourceCompatibility = 17 49 | targetCompatibility = 17 50 | } 51 | 52 | sourceSets { 53 | main.java.srcDirs += 'src/main/kotlin' 54 | test.java.srcDirs += 'src/test/kotlin' 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation project(path: rootProject.ext.analytics_kit) 60 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 61 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 62 | compileOnly 'com.google.firebase:firebase-analytics:21.1.0' 63 | 64 | testImplementation 'com.google.firebase:firebase-analytics:21.1.0' 65 | testImplementation "junit:junit:$rootProject.ext.junit_version" 66 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 67 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 68 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 69 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$rootProject.ext.mockitoKotlinVersion" 70 | testImplementation "org.mockito:mockito-core:$rootProject.ext.mockitoVersion" 71 | testImplementation 'org.robolectric:robolectric:4.10.3' 72 | } 73 | 74 | afterEvaluate { 75 | publishing { 76 | publications { 77 | releaseFirebase(MavenPublication) { // Creates a Maven publication called "releaseFirebase". 78 | from components.release // Applies the component for the release build variant. 79 | 80 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 81 | artifactId = 'firebase-provider' 82 | version = "$VERSION_NAME" 83 | } 84 | } 85 | } 86 | } 87 | 88 | task sourcesJar(type: Jar) { 89 | archiveClassifier.set("sources") 90 | from android.sourceSets.main.java.srcDirs 91 | } 92 | 93 | task javadoc(type: Javadoc) { 94 | failOnError false 95 | source = android.sourceSets.main.java.sourceFiles 96 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 97 | } 98 | 99 | task javadocJar(type: Jar, dependsOn: javadoc) { 100 | archiveClassifier.set("javadoc") 101 | from javadoc.destinationDir 102 | } 103 | 104 | artifacts { 105 | archives sourcesJar 106 | archives javadocJar 107 | } 108 | -------------------------------------------------------------------------------- /firebase-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /firebase-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /firebase-provider/src/main/kotlin/firebase_provider/FirebaseProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.firebase_provider 17 | 18 | import com.google.firebase.analytics.FirebaseAnalytics 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 21 | import com.busybusy.analyticskit_android.AnalyticsEvent 22 | import android.os.Bundle 23 | import java.io.Serializable 24 | import java.lang.IllegalStateException 25 | import java.text.DecimalFormat 26 | 27 | /** 28 | * Provider that facilitates reporting events to Firebase Analytics. 29 | * We recommend using the [FirebaseAnalytics.Param] names for attributes as much as possible due to the restriction on the number of 30 | * custom parameters in Firebase. 31 | * 32 | * @see [Log Events](https://firebase.google.com/docs/analytics/android/events) 33 | * @see [Custom-parameter reporting](https://support.google.com/firebase/answer/7397304?hl=en&ref_topic=6317489) 34 | * 35 | * @property firebaseAnalytics the initialized [FirebaseAnalytics] instance associated with the application 36 | * @property priorityFilter the [PriorityFilter] to use when evaluating if an event should be sent to this provider's platform. 37 | * By default, this provider will log all events regardless of priority. 38 | */ 39 | class FirebaseProvider( 40 | private val firebaseAnalytics: FirebaseAnalytics, 41 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 42 | ) : AnalyticsKitProvider { 43 | 44 | private val timedEvents: MutableMap by lazy { mutableMapOf() } 45 | private val eventTimes: MutableMap by lazy { mutableMapOf() } 46 | 47 | /** 48 | * Returns the filter used to restrict events by priority. 49 | * 50 | * @return the [PriorityFilter] instance the provider is using to determine if an 51 | * event of a given priority should be logged 52 | */ 53 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 54 | 55 | /** 56 | * Sends the event using provider-specific code. 57 | * 58 | * @param event an instantiated event 59 | */ 60 | override fun sendEvent(event: AnalyticsEvent) { 61 | if (event.isTimed()) { // Hang onto the event until it is done 62 | eventTimes[event.name()] = System.currentTimeMillis() 63 | timedEvents[event.name()] = event 64 | } else { // Send the event through the Firebase Analytics API 65 | logFirebaseAnalyticsEvent(event) 66 | } 67 | } 68 | 69 | /** 70 | * End the timed event. 71 | * 72 | * @param timedEvent the event which has finished 73 | * @throws IllegalStateException 74 | */ 75 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 76 | val endTime = System.currentTimeMillis() 77 | val startTime = eventTimes.remove(timedEvent.name()) 78 | val finishedEvent = timedEvents.remove(timedEvent.name()) 79 | if (startTime != null && finishedEvent != null) { 80 | val durationSeconds = ((endTime - startTime) / 1000).toDouble() 81 | val df = DecimalFormat("#.###") 82 | finishedEvent.putAttribute(FirebaseAnalytics.Param.VALUE, df.format(durationSeconds)) 83 | logFirebaseAnalyticsEvent(finishedEvent) 84 | } else { 85 | error("Attempted ending an event that was never started (or was previously ended): ${timedEvent.name()}") 86 | } 87 | } 88 | 89 | private fun logFirebaseAnalyticsEvent(event: AnalyticsEvent) { 90 | var parameterBundle: Bundle? = null 91 | val attributes = event.attributes 92 | if (attributes != null && attributes.isNotEmpty()) { 93 | parameterBundle = Bundle() 94 | for (key in attributes.keys) { 95 | parameterBundle.putSerializable(key, getCheckAndCast(attributes, key)) 96 | } 97 | } 98 | firebaseAnalytics.logEvent(event.name(), parameterBundle) 99 | } 100 | 101 | @Suppress("UNCHECKED_CAST") 102 | private fun getCheckAndCast( 103 | map: Map, 104 | key: String, 105 | ): ObjectType { 106 | val result = map[key] as Serializable 107 | return result as ObjectType 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /firebase-provider/src/test/kotlin/com/busybusy/firebase_provider/FirebaseProviderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018, 2020-2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.firebase_provider 17 | 18 | import android.os.Bundle 19 | import com.busybusy.analyticskit_android.AnalyticsEvent 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 21 | import com.busybusy.analyticskit_android.CommonEvents 22 | import com.busybusy.analyticskit_android.ContentViewEvent 23 | import com.busybusy.analyticskit_android.ErrorEvent 24 | import com.google.firebase.analytics.FirebaseAnalytics 25 | import com.nhaarman.mockitokotlin2.* 26 | import org.assertj.core.api.Assertions.assertThat 27 | import org.junit.Before 28 | import org.junit.Test 29 | import org.junit.runner.RunWith 30 | import org.robolectric.RobolectricTestRunner 31 | import org.robolectric.annotation.Config 32 | import java.util.* 33 | 34 | /** 35 | * Tests the [FirebaseProvider] class 36 | * 37 | * @author John Hunt on 3/21/16. 38 | */ 39 | @RunWith(RobolectricTestRunner::class) 40 | @Config(sdk = [28], manifest=Config.NONE) 41 | class FirebaseProviderTest { 42 | private val firebaseAnalytics: FirebaseAnalytics = mock() 43 | private val provider: FirebaseProvider = FirebaseProvider(firebaseAnalytics) 44 | private var sendCalled = false 45 | private var testEventName: String? = null 46 | private var testBundle: Bundle? = null 47 | 48 | @Before 49 | fun setup() { 50 | // Mock behavior for when FirebaseAnalytics logEvent() is called 51 | doAnswer { invocation -> 52 | val args = invocation.arguments 53 | testEventName = args[0] as String 54 | testBundle = args[1] as Bundle 55 | sendCalled = true 56 | null 57 | }.whenever(firebaseAnalytics).logEvent(any(), any()) 58 | 59 | doAnswer { invocation -> 60 | val args = invocation.arguments 61 | testEventName = args[0] as String 62 | testBundle = args[1] as Bundle? 63 | sendCalled = true 64 | null 65 | }.whenever(firebaseAnalytics).logEvent(any(), isNull()) 66 | 67 | sendCalled = false 68 | testEventName = null 69 | testBundle = null 70 | } 71 | 72 | @Test 73 | fun testSetAndGetPriorityFilter() { 74 | val filter = PriorityFilter { false } 75 | val filteringProvider = FirebaseProvider(firebaseAnalytics, filter) 76 | assertThat(filteringProvider.getPriorityFilter()).isEqualTo(filter) 77 | } 78 | 79 | @Test 80 | fun test_priorityFiltering_default() { 81 | val event = AnalyticsEvent("A Firebase Event") 82 | .setPriority(10) 83 | .send() 84 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 85 | event.setPriority(-9) 86 | .send() 87 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 88 | } 89 | 90 | @Test 91 | fun test_priorityFiltering_custom() { 92 | val filteringProvider = FirebaseProvider(firebaseAnalytics) { 93 | priorityLevel -> priorityLevel < 10 94 | } 95 | val event = AnalyticsEvent("Priority 10 Event") 96 | .setPriority(10) 97 | .send() 98 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(false) 99 | event.setPriority(9) 100 | .send() 101 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 102 | } 103 | 104 | @Test 105 | fun testSendEvent_unTimed_noParams() { 106 | val event = AnalyticsEvent("Firebase Analytics Test Run") 107 | provider.sendEvent(event) 108 | assertThat(sendCalled).isEqualTo(true) 109 | assertThat(testEventName).isEqualTo("Firebase Analytics Test Run") 110 | assertThat(testBundle).isNull() 111 | } 112 | 113 | @Test 114 | fun testSendEvent_unTimed_withParams() { 115 | val event = AnalyticsEvent("Firebase Analytics Event With Params Run") 116 | .putAttribute("some_param", "yes") 117 | .putAttribute("another_param", "yes again") 118 | provider.sendEvent(event) 119 | assertThat(sendCalled).isEqualTo(true) 120 | assertThat(testBundle).isNotNull 121 | assertThat(testBundle!!.getString("some_param")).isEqualTo("yes") 122 | assertThat(testBundle!!.getString("another_param")).isEqualTo("yes again") 123 | } 124 | 125 | @Test 126 | fun testSendEvent_unTimed_withTypedParams() { 127 | val intArray = intArrayOf(0, 1, 2) 128 | val event = AnalyticsEvent("Firebase Analytics Event With Typed Params Run") 129 | .putAttribute("int_param", 1) 130 | .putAttribute("long_param", 32L) 131 | .putAttribute("string_param", "a string") 132 | .putAttribute("double_param", 3.1415926) 133 | .putAttribute("float_param", 0.0f) 134 | .putAttribute("int_array_param", intArray) 135 | provider.sendEvent(event) 136 | assertThat(sendCalled).isEqualTo(true) 137 | assertThat(testEventName).isEqualTo("Firebase Analytics Event With Typed Params Run") 138 | assertThat(testBundle).isNotNull 139 | assertThat(testBundle!!.getInt("int_param")).isEqualTo(1) 140 | assertThat(testBundle!!.getLong("long_param")).isEqualTo(32L) 141 | assertThat(testBundle!!.getString("string_param")).isEqualTo("a string") 142 | assertThat(testBundle!!.getDouble("double_param")).isEqualTo(3.1415926) 143 | assertThat(testBundle!!.getFloat("float_param")).isEqualTo(0.0f) 144 | assertThat(testBundle!!.getIntArray("int_array_param")).isEqualTo(intArray) 145 | } 146 | 147 | @Test 148 | fun testLogContentViewEvent() { 149 | val event = ContentViewEvent("Test page 7") 150 | provider.sendEvent(event) 151 | assertThat(sendCalled).isEqualTo(true) 152 | assertThat(testEventName).isEqualTo(CommonEvents.CONTENT_VIEW) 153 | assertThat(testBundle).isNotNull 154 | assertThat(testBundle!!.getString(ContentViewEvent.CONTENT_NAME)).isEqualTo("Test page 7") 155 | } 156 | 157 | @Test 158 | fun testLogErrorEvent() { 159 | val event = ErrorEvent() 160 | .setMessage("something bad happened") 161 | .setException(EmptyStackException()) 162 | provider.sendEvent(event) 163 | assertThat(sendCalled).isEqualTo(true) 164 | assertThat(testEventName).isEqualTo(CommonEvents.ERROR) 165 | assertThat(testBundle).isNotNull 166 | assertThat(testBundle!!.getString("error_message")).isEqualTo("something bad happened") 167 | assertThat(testBundle!!["exception_object"]).isInstanceOf(EmptyStackException::class.java) 168 | } 169 | 170 | @Test 171 | fun testSendEvent_timed_noParams() { 172 | val event = AnalyticsEvent("Firebase Analytics Timed Event") 173 | .setTimed(true) 174 | provider.sendEvent(event) 175 | assertThat(sendCalled).isEqualTo(false) 176 | } 177 | 178 | @Test 179 | fun testSendEvent_timed_withParams() { 180 | val event = AnalyticsEvent("Firebase Analytics Timed Event With Parameters") 181 | .setTimed(true) 182 | .putAttribute("some_param", "yes") 183 | .putAttribute("another_param", "yes again") 184 | provider.sendEvent(event) 185 | assertThat(sendCalled).isEqualTo(false) 186 | assertThat(testBundle).isNull() 187 | } 188 | 189 | @Test 190 | fun testEndTimedEvent_Valid() { 191 | val event = AnalyticsEvent("Firebase Analytics Timed Event With Parameters") 192 | .setTimed(true) 193 | .putAttribute("some_param", "yes") 194 | .putAttribute("another_param", "yes again") 195 | provider.sendEvent(event) 196 | assertThat(sendCalled).isEqualTo(false) 197 | provider.endTimedEvent(event) 198 | 199 | // Verify Firebase Analytics framework is called 200 | assertThat(sendCalled).isEqualTo(true) 201 | assertThat(testBundle).isNotNull 202 | assertThat(testBundle!!.size()).isEqualTo(3) 203 | assertThat(testBundle!!.getString("some_param")).isEqualTo("yes") 204 | assertThat(testBundle!!.getString("another_param")).isEqualTo("yes again") 205 | assertThat(testBundle!!.getString(FirebaseAnalytics.Param.VALUE)).isNotNull 206 | } 207 | 208 | @Test 209 | fun test_endTimedEvent_WillThrow() { 210 | var didThrow = false 211 | val event = AnalyticsEvent("Firebase Analytics Timed Event With Parameters") 212 | .setTimed(true) 213 | try { 214 | provider.endTimedEvent(event) // attempting to end a timed event that was not started should throw an exception 215 | } catch (e: IllegalStateException) { 216 | didThrow = true 217 | } 218 | assertThat(didThrow).isEqualTo(true) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /firebase-provider/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /flurry-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | flurry-provider.iml -------------------------------------------------------------------------------- /flurry-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018, 2020-2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.flurry_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | } 30 | buildTypes { 31 | release { 32 | debuggable false 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | debug { 37 | debuggable true 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility = 17 45 | targetCompatibility = 17 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | test.java.srcDirs += 'src/test/kotlin' 51 | } 52 | 53 | testOptions { 54 | unitTests.returnDefaultValues = true 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation project(path: rootProject.ext.analytics_kit) 60 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 61 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 62 | compileOnly 'com.flurry.android:analytics:13.3.0' 63 | 64 | testImplementation 'com.flurry.android:analytics:13.3.0' 65 | testImplementation "junit:junit:$rootProject.ext.junit_version" 66 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 67 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 68 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 69 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$rootProject.ext.mockitoKotlinVersion" 70 | testImplementation "org.mockito:mockito-inline:$rootProject.ext.mockitoVersion" 71 | } 72 | 73 | afterEvaluate { 74 | publishing { 75 | publications { 76 | releaseFlurry(MavenPublication) { // Creates a Maven publication called "releaseFlurry". 77 | from components.release // Applies the component for the release build variant. 78 | 79 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 80 | artifactId = 'flurry-provider' 81 | version = "$VERSION_NAME" 82 | } 83 | } 84 | } 85 | } 86 | 87 | task sourcesJar(type: Jar) { 88 | archiveClassifier.set("sources") 89 | from android.sourceSets.main.java.srcDirs 90 | } 91 | 92 | task javadoc(type: Javadoc) { 93 | failOnError false 94 | source = android.sourceSets.main.java.sourceFiles 95 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 96 | } 97 | 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | archiveClassifier.set("javadoc") 100 | from javadoc.destinationDir 101 | } 102 | 103 | artifacts { 104 | archives sourcesJar 105 | archives javadocJar 106 | } 107 | -------------------------------------------------------------------------------- /flurry-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Sources/android_sdk_mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /flurry-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /flurry-provider/src/main/kotlin/flurry_provider/FlurryProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.flurry_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 20 | import com.busybusy.analyticskit_android.AnalyticsEvent 21 | import com.flurry.android.FlurryAgent 22 | 23 | /** 24 | * Implements Flurry as a provider to use with [com.busybusy.analyticskit_android.AnalyticsKit] 25 | * 26 | * 27 | * **Important**: It is a violation of Flurry’s TOS to record personally identifiable information such as a user’s UDID, 28 | * email address, and so on using Flurry. If you have a user login that you wish to associate with your session and 29 | * event data, you should use the SetUserID function. If you do choose to record a user id of any type within a parameter, 30 | * you must anonymize the data using a hashing function such as MD5 or SHA256 prior to calling the method. 31 | * 32 | * 33 | * Refer to the Flurry documentation here: 34 | * [Flurry Documentation](https://developer.yahoo.com/flurry/docs/analytics/gettingstarted/events/android/) 35 | * 36 | * @author John Hunt on 3/21/16. 37 | */ 38 | class FlurryProvider( 39 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 40 | ) : AnalyticsKitProvider { 41 | 42 | /** 43 | * Returns the filter used to restrict events by priority. 44 | * 45 | * @return the [PriorityFilter] instance the provider is using to determine if an 46 | * event of a given priority should be logged 47 | */ 48 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 49 | 50 | /** 51 | * Sends the event using provider-specific code. 52 | * 53 | * @param event an instantiated event 54 | */ 55 | @Throws(IllegalStateException::class) 56 | override fun sendEvent(event: AnalyticsEvent) { 57 | val eventParams: Map? = stringifyParameters(event.attributes) 58 | if (event.isTimed()) { 59 | // start the Flurry SDK event timer for the event 60 | when (eventParams) { 61 | null -> FlurryAgent.logEvent(event.name(), true) 62 | else -> FlurryAgent.logEvent(event.name(), eventParams, true) 63 | } 64 | } else { 65 | when (eventParams) { 66 | null -> FlurryAgent.logEvent(event.name()) 67 | else -> FlurryAgent.logEvent(event.name(), eventParams) 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * End the timed event. 74 | * 75 | * @param timedEvent the event which has finished 76 | */ 77 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 78 | FlurryAgent.endTimedEvent(timedEvent.name()) 79 | } 80 | 81 | /** 82 | * Converts a `Map` to `Map` 83 | * @param attributes the map of attributes attached to the event 84 | * @return the String map of parameters. Returns `null` if no parameters are attached to the event. 85 | */ 86 | @Throws(IllegalStateException::class) 87 | fun stringifyParameters(attributes: Map?): Map? { 88 | check((attributes?.size ?: 0) <= ATTRIBUTE_LIMIT) { 89 | "Flurry events are limited to $ATTRIBUTE_LIMIT attributes" 90 | } 91 | 92 | return attributes?.map { (key, value) -> key to value.toString() } 93 | ?.takeIf { it.isNotEmpty() } 94 | ?.toMap() 95 | } 96 | } 97 | 98 | internal const val ATTRIBUTE_LIMIT = 10 99 | -------------------------------------------------------------------------------- /flurry-provider/src/test/kotlin/com/busybusy/flurry_provider/FlurryProviderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.flurry_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.flurry.android.FlurryAgent 21 | import org.assertj.core.api.Assertions.assertThat 22 | import org.assertj.core.api.Assertions.entry 23 | import org.junit.Test 24 | import org.mockito.Mockito.mockStatic 25 | 26 | /** 27 | * Tests the [FlurryProvider] class 28 | * 29 | * @author John Hunt on 3/21/16. 30 | */ 31 | class FlurryProviderTest { 32 | private val provider = FlurryProvider() 33 | 34 | @Test 35 | fun testSetAndGetPriorityFilter() { 36 | val filter = PriorityFilter { false } 37 | val filteringProvider = FlurryProvider(priorityFilter = filter) 38 | assertThat(filteringProvider.getPriorityFilter()).isEqualTo(filter) 39 | } 40 | 41 | @Test 42 | fun test_priorityFiltering_default() { 43 | val event = AnalyticsEvent("Forecast: Event Flurries") 44 | .setPriority(10) 45 | .send() 46 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 47 | event.setPriority(-9) 48 | .send() 49 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 50 | } 51 | 52 | @Test 53 | fun test_priorityFiltering_custom() { 54 | val filter = PriorityFilter { priorityLevel -> priorityLevel < 10 } 55 | val filteringProvider = FlurryProvider(priorityFilter = filter) 56 | val event = AnalyticsEvent("Forecast: Event Flurries") 57 | .setPriority(10) 58 | .send() 59 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(false) 60 | event.setPriority(9) 61 | .send() 62 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 63 | } 64 | 65 | @Test 66 | fun testStringifyParameters_noParams() { 67 | val flurryParams = provider.stringifyParameters(null) 68 | assertThat(flurryParams).isNull() 69 | } 70 | 71 | @Test 72 | fun testStringifyParameters_validParams() { 73 | val eventParams = mutableMapOf("favorite_color" to "Blue", "favorite_number" to 42) 74 | val flurryParams = provider.stringifyParameters(eventParams) 75 | assertThat(flurryParams).isNotNull 76 | assertThat(flurryParams).containsExactly(entry("favorite_color", "Blue"), entry("favorite_number", "42")) 77 | } 78 | 79 | @Test 80 | fun testStringifyParameters_willThrow() { 81 | val attributeMap = mutableMapOf() 82 | for (count in 0..ATTRIBUTE_LIMIT + 1) { 83 | attributeMap[count.toString()] = "placeholder" 84 | } 85 | var exceptionMessage: String? = "" 86 | try { 87 | provider.stringifyParameters(attributeMap) 88 | } catch (e: IllegalStateException) { 89 | exceptionMessage = e.message 90 | } 91 | assertThat(exceptionMessage).isEqualTo("Flurry events are limited to $ATTRIBUTE_LIMIT attributes") 92 | } 93 | 94 | @Test 95 | fun testSendEvent_unTimed_noParams() { 96 | val event = AnalyticsEvent("Flurry Test Run") 97 | mockStatic(FlurryAgent::class.java).use { 98 | provider.sendEvent(event) 99 | it.verify { FlurryAgent.logEvent("Flurry Test Run") } 100 | } 101 | } 102 | 103 | @Test 104 | fun testSendEvent_unTimed_withParams() { 105 | val event = AnalyticsEvent("Flurry Event With Params Run") 106 | .putAttribute("some_param", "yes") 107 | .putAttribute("another_param", "yes again") 108 | mockStatic(FlurryAgent::class.java).use { 109 | provider.sendEvent(event) 110 | it.verify { 111 | FlurryAgent.logEvent( 112 | "Flurry Event With Params Run", 113 | mapOf("some_param" to "yes", "another_param" to "yes again"), 114 | ) 115 | } 116 | } 117 | } 118 | 119 | @Test 120 | fun testSendEvent_timed_noParams() { 121 | val event = AnalyticsEvent("Flurry Timed Event") 122 | .setTimed(true) 123 | mockStatic(FlurryAgent::class.java).use { 124 | provider.sendEvent(event) 125 | it.verify { FlurryAgent.logEvent("Flurry Timed Event", true) } 126 | } 127 | } 128 | 129 | @Test 130 | fun testSendEvent_timed_withParams() { 131 | val event = AnalyticsEvent("Flurry Timed Event With Parameters") 132 | .setTimed(true) 133 | .putAttribute("some_param", "yes") 134 | .putAttribute("another_param", "yes again") 135 | mockStatic(FlurryAgent::class.java).use { 136 | provider.sendEvent(event) 137 | it.verify { 138 | FlurryAgent.logEvent( 139 | "Flurry Timed Event With Parameters", 140 | mapOf("some_param" to "yes", "another_param" to "yes again"), 141 | true, 142 | ) 143 | } 144 | } 145 | } 146 | 147 | @Test 148 | fun testEndTimedEvent() { 149 | val event = AnalyticsEvent("End timed event") 150 | mockStatic(FlurryAgent::class.java).use { 151 | provider.endTimedEvent(event) 152 | it.verify { FlurryAgent.endTimedEvent("End timed event") } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /google-analytics-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | google-analytics-provider.iml -------------------------------------------------------------------------------- /google-analytics-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2018 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.google_analytics_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | } 30 | buildTypes { 31 | release { 32 | debuggable false 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | debug { 37 | debuggable true 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility = 17 45 | targetCompatibility = 17 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | test.java.srcDirs += 'src/test/kotlin' 51 | } 52 | } 53 | 54 | dependencies { 55 | implementation project(path: rootProject.ext.analytics_kit) 56 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 57 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 58 | compileOnly 'com.google.android.gms:play-services-analytics:17.0.0' 59 | 60 | testImplementation 'com.google.android.gms:play-services-analytics:17.0.0' 61 | testImplementation "junit:junit:$rootProject.ext.junit_version" 62 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 63 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 64 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 65 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$rootProject.ext.mockitoKotlinVersion" 66 | } 67 | 68 | afterEvaluate { 69 | publishing { 70 | publications { 71 | releaseGoogle(MavenPublication) { // Creates a Maven publication called "releaseGoogle". 72 | from components.release // Applies the component for the release build variant. 73 | 74 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 75 | artifactId = 'google-analytics-provider' 76 | version = "$VERSION_NAME" 77 | } 78 | } 79 | } 80 | } 81 | 82 | task sourcesJar(type: Jar) { 83 | archiveClassifier.set("sources") 84 | from android.sourceSets.main.java.srcDirs 85 | } 86 | 87 | task javadoc(type: Javadoc) { 88 | failOnError false 89 | source = android.sourceSets.main.java.sourceFiles 90 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 91 | } 92 | 93 | task javadocJar(type: Jar, dependsOn: javadoc) { 94 | archiveClassifier.set("javadoc") 95 | from javadoc.destinationDir 96 | } 97 | 98 | artifacts { 99 | archives sourcesJar 100 | archives javadocJar 101 | } 102 | -------------------------------------------------------------------------------- /google-analytics-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Sources/android_sdk_mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /google-analytics-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /google-analytics-provider/src/main/kotlin/google_analytics_provider/GoogleAnalyticsProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.google_analytics_provider 17 | 18 | import androidx.annotation.Nullable 19 | import com.busybusy.analyticskit_android.AnalyticsEvent 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 21 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 22 | import com.busybusy.analyticskit_android.ContentViewEvent 23 | import com.busybusy.analyticskit_android.ErrorEvent 24 | import com.google.android.gms.analytics.HitBuilders 25 | import com.google.android.gms.analytics.HitBuilders.ExceptionBuilder 26 | import com.google.android.gms.analytics.HitBuilders.ScreenViewBuilder 27 | import com.google.android.gms.analytics.HitBuilders.TimingBuilder 28 | import com.google.android.gms.analytics.Tracker 29 | 30 | /** 31 | * Implements Google Analytics as a provider to use with [com.busybusy.analyticskit_android.AnalyticsKit] 32 | * @property tracker the initialized `Tracker` instance associated with the application 33 | * @property priorityFilter the `PriorityFilter` to use when evaluating events, defaults to 34 | * logging all events regardless of priority 35 | * 36 | * @author John Hunt on 5/4/16. 37 | */ 38 | class GoogleAnalyticsProvider( 39 | private val tracker: Tracker, 40 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 41 | ) : AnalyticsKitProvider { 42 | 43 | private val timedEvents: MutableMap by lazy { mutableMapOf() } 44 | private val eventTimes: MutableMap by lazy { mutableMapOf() } 45 | 46 | /** 47 | * Returns the filter used to restrict events by priority. 48 | * 49 | * @return the [PriorityFilter] instance the provider is using to determine if an 50 | * event of a given priority should be logged 51 | */ 52 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 53 | 54 | /** 55 | * Sends the event using provider-specific code. 56 | * 57 | * @param event an instantiated event 58 | */ 59 | override fun sendEvent(event: AnalyticsEvent) { 60 | if (event.isTimed()) { // Hang onto it until it is done 61 | eventTimes[event.name()] = System.currentTimeMillis() 62 | timedEvents[event.name()] = event 63 | } else { // Send the event through the Google Analytics API 64 | logGoogleAnalyticsEvent(event) 65 | } 66 | } 67 | 68 | /** 69 | * End the timed event. 70 | * 71 | * @param timedEvent the event which has finished 72 | */ 73 | @Throws(IllegalStateException::class) 74 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 75 | val endTime = System.currentTimeMillis() 76 | val startTime = eventTimes.remove(timedEvent.name()) 77 | val finishedEvent = timedEvents.remove(timedEvent.name()) 78 | if (startTime != null && finishedEvent != null) { 79 | val timingBuilder = TimingBuilder() 80 | timingBuilder.setLabel(finishedEvent.name()) 81 | .setCategory("Timed Events") 82 | .setValue(endTime - startTime) 83 | // add any custom attributes already set on the event 84 | timingBuilder.setAll(stringifyAttributesMap(finishedEvent.attributes)) 85 | tracker.send(timingBuilder.build()) 86 | } else { 87 | error("Attempted ending an event that was never started (or was previously ended): ${timedEvent.name()}") 88 | } 89 | } 90 | 91 | private fun logGoogleAnalyticsEvent(event: AnalyticsEvent) { 92 | when (event) { 93 | is ContentViewEvent -> { 94 | val screenViewBuilder = ScreenViewBuilder() 95 | // add any custom attributes already set on the event 96 | screenViewBuilder.setAll(stringifyAttributesMap(event.attributes)) 97 | synchronized(tracker) { // Set the screen name and send a screen view. 98 | tracker.setScreenName( 99 | event.getAttribute(ContentViewEvent.CONTENT_NAME).toString() 100 | ) 101 | tracker.send(screenViewBuilder.build()) 102 | } 103 | } 104 | is ErrorEvent -> { // Build and send exception. 105 | val exceptionBuilder = ExceptionBuilder() 106 | .setDescription(event.message()) 107 | .setFatal(false) 108 | 109 | // Add any custom attributes that are attached to the event 110 | exceptionBuilder.setAll(stringifyAttributesMap(event.attributes)) 111 | tracker.send(exceptionBuilder.build()) 112 | } 113 | else -> { // Build and send an Event. 114 | val eventBuilder = HitBuilders.EventBuilder() 115 | .setCategory("User Event") 116 | .setAction(event.name()) 117 | 118 | // Add any custom attributes that are attached to the event 119 | eventBuilder.setAll(stringifyAttributesMap(event.attributes)) 120 | tracker.send(eventBuilder.build()) 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Converts a `Map` to a `Map` (nullable) 127 | * 128 | * @param attributes the map of attributes attached to the event 129 | * @return the String map of parameters. Returns `null` if no parameters are attached to the event. 130 | */ 131 | @Nullable 132 | fun stringifyAttributesMap(attributes: Map?): Map? { 133 | return attributes?.map { (key, value) -> key to value.toString() } 134 | ?.takeIf { it.isNotEmpty() } 135 | ?.toMap() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /google-analytics-provider/src/test/kotlin/com/busybusy/google_analytics_provider/GoogleAnalyticsProviderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.google_analytics_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.busybusy.analyticskit_android.ContentViewEvent 21 | import com.busybusy.analyticskit_android.ErrorEvent 22 | import com.google.android.gms.analytics.Tracker 23 | import org.assertj.core.api.Assertions.assertThat 24 | import org.assertj.core.api.Assertions.entry 25 | import org.junit.Before 26 | import org.junit.Test 27 | import org.mockito.ArgumentMatchers.anyMap 28 | import org.mockito.Mockito 29 | import java.util.* 30 | import kotlin.collections.HashMap 31 | import kotlin.properties.Delegates.notNull 32 | 33 | /** 34 | * Tests the [GoogleAnalyticsProvider] class 35 | * 36 | * @author John Hunt on 3/21/16. 37 | */ 38 | class GoogleAnalyticsProviderTest { 39 | private lateinit var tracker: Tracker 40 | private lateinit var provider: GoogleAnalyticsProvider 41 | private lateinit var testEventPropertiesMap: Map 42 | private var sendCalled: Boolean by notNull() 43 | 44 | @Suppress("UNCHECKED_CAST") 45 | @Before 46 | fun setup() { 47 | // Mock behavior for when Tracker.send(Map) is called 48 | tracker = Mockito.mock(Tracker::class.java) 49 | provider = GoogleAnalyticsProvider(tracker) 50 | sendCalled = false 51 | testEventPropertiesMap = mutableMapOf() 52 | Mockito.doAnswer { invocation -> 53 | val args = invocation.arguments 54 | testEventPropertiesMap = args[0] as Map 55 | sendCalled = true 56 | null 57 | }.`when`(tracker).send(anyMap()) 58 | } 59 | 60 | @Test 61 | fun testSetAndGetPriorityFilter() { 62 | val filter = PriorityFilter { false } 63 | val filteringProvider = GoogleAnalyticsProvider(tracker, filter) 64 | assertThat(filteringProvider.getPriorityFilter()).isEqualTo(filter) 65 | } 66 | 67 | @Test 68 | fun test_priorityFiltering_default() { 69 | val event = AnalyticsEvent("Forecast: Event Flurries") 70 | .setPriority(10) 71 | .send() 72 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 73 | event.setPriority(-9) 74 | .send() 75 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 76 | } 77 | 78 | @Test 79 | fun test_priorityFiltering_custom() { 80 | val filter = PriorityFilter { priorityLevel -> priorityLevel < 10 } 81 | val filteringProvider = GoogleAnalyticsProvider(tracker, filter) 82 | val event = AnalyticsEvent("Forecast: Event Flurries") 83 | .setPriority(10) 84 | .send() 85 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(false) 86 | event.setPriority(9) 87 | .send() 88 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 89 | } 90 | 91 | @Test 92 | fun testStringifyAttributesMap_noParams() { 93 | val stringAttributes = provider.stringifyAttributesMap(null) 94 | assertThat(stringAttributes).isNull() 95 | } 96 | 97 | @Test 98 | fun testStringifyAttributesMap_validParams() { 99 | val eventParams = HashMap() 100 | eventParams["favorite_color"] = "Blue" 101 | eventParams["favorite_number"] = 42 102 | val stringAttributes = provider.stringifyAttributesMap(eventParams) 103 | assertThat(stringAttributes).isNotNull 104 | assertThat(stringAttributes!!.containsKey("favorite_color")) 105 | assertThat(stringAttributes["favorite_color"]).isEqualTo("Blue") 106 | assertThat(stringAttributes.containsKey("favorite_number")) 107 | assertThat(stringAttributes["favorite_number"]).isEqualTo("42") 108 | } 109 | 110 | @Test 111 | fun testSendEvent_unTimed_noParams() { 112 | val event = AnalyticsEvent("Google Analytics Test Run") 113 | provider.sendEvent(event) 114 | assertThat(sendCalled).isEqualTo(true) 115 | assertThat(testEventPropertiesMap).isNotNull 116 | assertThat(testEventPropertiesMap.size).isEqualTo(3) 117 | assertThat(testEventPropertiesMap).containsExactly( 118 | entry("&ea", "Google Analytics Test Run"), 119 | entry("&ec", "User Event"), 120 | entry("&t", "event") 121 | ) 122 | } 123 | 124 | @Test 125 | fun testSendEvent_unTimed_withParams() { 126 | val event = AnalyticsEvent("Google Analytics Event With Params Run") 127 | .putAttribute("some_param", "yes") 128 | .putAttribute("another_param", "yes again") 129 | provider.sendEvent(event) 130 | assertThat(sendCalled).isEqualTo(true) 131 | assertThat(testEventPropertiesMap).isNotNull 132 | assertThat(testEventPropertiesMap.size).isEqualTo(5) 133 | assertThat(testEventPropertiesMap).containsExactlyInAnyOrderEntriesOf( 134 | mutableMapOf( 135 | "&ea" to "Google Analytics Event With Params Run", 136 | "&ec" to "User Event", 137 | "&t" to "event", 138 | "some_param" to "yes", 139 | "another_param" to "yes again" 140 | ) 141 | ) 142 | } 143 | 144 | @Test 145 | fun testLogContentViewEvent() { 146 | val event = ContentViewEvent("Test page 7") 147 | provider.sendEvent(event) 148 | assertThat(sendCalled).isEqualTo(true) 149 | assertThat(testEventPropertiesMap).isNotNull 150 | assertThat(testEventPropertiesMap.size).isEqualTo(2) 151 | assertThat(testEventPropertiesMap).containsExactly( 152 | entry("&t", "screenview"), 153 | entry("contentName", "Test page 7") 154 | ) 155 | } 156 | 157 | @Test 158 | fun testLogErrorEvent() { 159 | val event = ErrorEvent() 160 | .setMessage("something bad happened") 161 | .setException(EmptyStackException()) 162 | provider.sendEvent(event) 163 | assertThat(sendCalled).isEqualTo(true) 164 | assertThat(testEventPropertiesMap).isNotNull 165 | assertThat(testEventPropertiesMap.size).isEqualTo(5) 166 | assertThat(testEventPropertiesMap).containsExactlyInAnyOrderEntriesOf( 167 | mutableMapOf( 168 | "&exd" to "something bad happened", 169 | "&t" to "exception", 170 | "&exf" to "0", 171 | "error_message" to "something bad happened", 172 | "exception_object" to "java.util.EmptyStackException" 173 | ) 174 | ) 175 | } 176 | 177 | @Test 178 | fun testSendEvent_timed_noParams() { 179 | val event = AnalyticsEvent("Google Analytics Timed Event") 180 | .setTimed(true) 181 | provider.sendEvent(event) 182 | assertThat(sendCalled).isEqualTo(false) 183 | } 184 | 185 | @Test 186 | fun testSendEvent_timed_withParams() { 187 | val event = AnalyticsEvent("Google Analytics Timed Event With Parameters") 188 | .setTimed(true) 189 | .putAttribute("some_param", "yes") 190 | .putAttribute("another_param", "yes again") 191 | provider.sendEvent(event) 192 | assertThat(sendCalled).isEqualTo(false) 193 | } 194 | 195 | @Test 196 | fun testEndTimedEvent_Valid() { 197 | val event = AnalyticsEvent("Google Analytics Timed Event With Parameters") 198 | .setTimed(true) 199 | .putAttribute("some_param", "yes") 200 | .putAttribute("another_param", "yes again") 201 | provider.sendEvent(event) 202 | assertThat(sendCalled).isEqualTo(false) 203 | try { 204 | Thread.sleep(50) 205 | } catch (e: InterruptedException) { 206 | // don't do anything, this is just a test that needs some delay 207 | } 208 | provider.endTimedEvent(event) 209 | 210 | assertThat(sendCalled).isEqualTo(true) 211 | assertThat(testEventPropertiesMap).isNotNull 212 | assertThat(testEventPropertiesMap.size).isEqualTo(6) 213 | val timeString = testEventPropertiesMap["&utt"] 214 | assertThat(testEventPropertiesMap).containsExactlyInAnyOrderEntriesOf( 215 | mutableMapOf( 216 | "&utl" to "Google Analytics Timed Event With Parameters", 217 | "&utc" to "Timed Events", 218 | "&t" to "timing", 219 | "&utt" to timeString, 220 | "some_param" to "yes", 221 | "another_param" to "yes again" 222 | ) 223 | ) 224 | 225 | val elapsedTime = java.lang.Long.valueOf(timeString!!) 226 | assertThat(elapsedTime).isGreaterThanOrEqualTo(50) 227 | } 228 | 229 | @Test 230 | fun test_endTimedEvent_WillThrow() { 231 | var didThrow = false 232 | val event = AnalyticsEvent("Google Analytics Timed Event With Parameters") 233 | .setTimed(true) 234 | try { 235 | provider.endTimedEvent(event) // attempting to end a timed event that was not started should throw an exception 236 | } catch (e: IllegalStateException) { 237 | didThrow = true 238 | } 239 | assertThat(didThrow).isEqualTo(true) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | android.useAndroidX=true 20 | 21 | VERSION_NAME=1.0.0 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alignops/AnalyticsKit-Android/c6997e689cd777502a691fc61d3ff8c391f626d3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /graylog-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | graylog-provider.iml -------------------------------------------------------------------------------- /graylog-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018, 2020-2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.graylog_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | 30 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 31 | 32 | } 33 | buildTypes { 34 | release { 35 | debuggable false 36 | minifyEnabled false 37 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 38 | } 39 | debug { 40 | debuggable true 41 | minifyEnabled false 42 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 43 | } 44 | } 45 | 46 | compileOptions { 47 | sourceCompatibility = 17 48 | targetCompatibility = 17 49 | } 50 | 51 | sourceSets { 52 | main.java.srcDirs += 'src/main/kotlin' 53 | test.java.srcDirs += 'src/test/kotlin' 54 | } 55 | } 56 | 57 | dependencies { 58 | implementation project(path: rootProject.ext.analytics_kit) 59 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 60 | compileOnly 'com.squareup.okhttp3:okhttp:4.9.3' 61 | 62 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 63 | 64 | testImplementation "junit:junit:$rootProject.ext.junit_version" 65 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 66 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 67 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 68 | testImplementation 'org.robolectric:robolectric:4.10.3' 69 | testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1' 70 | testImplementation "org.mockito:mockito-core:$rootProject.ext.mockitoVersion" 71 | } 72 | 73 | afterEvaluate { 74 | publishing { 75 | publications { 76 | releaseGraylog(MavenPublication) { // Creates a Maven publication called "releaseGraylog". 77 | from components.release // Applies the component for the release build variant. 78 | 79 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 80 | artifactId = 'graylog-provider' 81 | version = "$VERSION_NAME" 82 | } 83 | } 84 | } 85 | } 86 | 87 | task sourcesJar(type: Jar) { 88 | archiveClassifier.set("sources") 89 | from android.sourceSets.main.java.srcDirs 90 | } 91 | 92 | task javadoc(type: Javadoc) { 93 | failOnError false 94 | source = android.sourceSets.main.java.sourceFiles 95 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 96 | } 97 | 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | archiveClassifier.set("javadoc") 100 | from javadoc.destinationDir 101 | } 102 | 103 | artifacts { 104 | archives sourcesJar 105 | archives javadocJar 106 | } 107 | -------------------------------------------------------------------------------- /graylog-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Sources/android_sdk_mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /graylog-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /graylog-provider/src/main/kotlin/graylog_provider/EventJsonizer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import java.util.UUID 20 | 21 | /** 22 | * Turns an [AnalyticsEvent] into a JSON String. 23 | * 24 | * @author John Hunt on 6/29/17. 25 | */ 26 | class EventJsonizer internal constructor(gelfSpecVersion: String, host: String) { 27 | private val HOST: String 28 | private val GELF_SPEC_VERSION: String 29 | 30 | init { 31 | GELF_SPEC_VERSION = getSafeSizeString(gelfSpecVersion) 32 | HOST = getSafeSizeString(host) 33 | } 34 | 35 | /** 36 | * Builds a JSON string from an [AnalyticsEvent]. 37 | * 38 | * @param event the event to serialize to JSON 39 | * @return the JSON string representation of the given event 40 | */ 41 | @Suppress("UNCHECKED_CAST") 42 | @Throws(UnsupportedOperationException::class) 43 | fun getJsonBody(event: AnalyticsEvent): String { 44 | val eventAttributes = if (event.attributes != null) event.attributes else mutableMapOf() 45 | val attributes: Set = when { 46 | event.attributes != null -> event.attributes!!.keys 47 | else -> mutableSetOf() 48 | } 49 | 50 | // guard clause: Libraries SHOULD NOT allow to send id as additional field (_id). 51 | if (attributes.contains("id")) { 52 | throw UnsupportedOperationException("id is NOT allowed as an additional field according to the GELF spec!") 53 | } 54 | 55 | return buildString { 56 | append("{") 57 | putGraylogSpecFields(this, event.name(), attributes, requireNotNull(eventAttributes)) 58 | 59 | for (attribute in attributes.filterNot { 60 | // these fields have already been handled above 61 | it.equals("version", ignoreCase = true) || 62 | it.equals("host", ignoreCase = true) || 63 | it.equals("short_message", ignoreCase = true) || 64 | it.equals("full_message", ignoreCase = true) || 65 | it.equals("timestamp", ignoreCase = true) || 66 | it.equals("level", ignoreCase = true) 67 | }) { 68 | // Graylog omits fields whose name have white space, replace with '_' 69 | val jsonAttribute = attribute.replace("\\s".toRegex(), "_") 70 | 71 | append(", \"${underscorePrefix(jsonAttribute)}") 72 | when (val attributeValue = eventAttributes[attribute]) { 73 | is String -> append(jsonAttribute).append("\": \"") 74 | .append(getSafeSizeString(attributeValue)).append("\"") 75 | is List<*> -> append(jsonAttribute).append("\": ") 76 | .append(getJsonFromListRecursive(attributeValue)) 77 | is Map<*, *> -> append(jsonAttribute).append("\": ") 78 | .append(getJsonFromMapRecursive(attributeValue as Map)) 79 | is Number, is Boolean -> append(jsonAttribute).append("\": ") 80 | .append(attributeValue) 81 | is Exception, is Error -> append(jsonAttribute).append("\": \"") 82 | .append(attributeValue.toString()).append("\"") 83 | is UUID -> append(jsonAttribute).append("\": \"") 84 | .append(attributeValue.toString()).append("\"") 85 | else -> { 86 | throw UnsupportedOperationException("Unsupported type for GELF message: " + attributeValue!!.javaClass.simpleName) 87 | } 88 | } 89 | } 90 | append("}") 91 | } 92 | } 93 | 94 | private fun putGraylogSpecFields( 95 | builder: StringBuilder, 96 | eventName: String, 97 | attributes: Set, 98 | eventAttributes: Map, 99 | ) = builder.apply { 100 | append("\"version\": \"$GELF_SPEC_VERSION").append("\", ") 101 | append("\"host\": \"$HOST").append("\", ") 102 | append("\"short_message\": \"").append(getSafeSizeString(eventName)).append("\", ") 103 | 104 | if (attributes.contains("full_message")) { 105 | val fullMessage = eventAttributes["full_message"].toString() 106 | append("\"full_message\": \"").append(getSafeSizeString(fullMessage)).append("\", ") 107 | } 108 | // No else needed: this is an optional long message (may contain a backtrace) 109 | 110 | if (attributes.contains("timestamp")) { // user-provided timestamp 111 | append("\"timestamp\": ") 112 | .append(getSafeSizeString(eventAttributes["timestamp"].toString())).append(", ") 113 | } else { 114 | append("\"timestamp\": ").append(System.currentTimeMillis() / 1000.0).append(", ") 115 | } 116 | 117 | if (attributes.contains("level")) { // user-provided syslog level 118 | append("\"level\": ").append(getSafeSizeString(eventAttributes["level"].toString())) 119 | } else { 120 | append("\"level\": 6") // default to Informational syslog level 121 | } 122 | } 123 | 124 | /** 125 | * Ensures that user-provided fields conform to the Graylog spec (prefixed by an underscore) 126 | */ 127 | private fun underscorePrefix(fieldName: String): String = 128 | if (fieldName.startsWith("_")) "" else "_" 129 | 130 | @Suppress("UNCHECKED_CAST") 131 | internal fun getJsonFromMapRecursive(attributeMap: Map): String = buildString { 132 | append("{") 133 | for (innerAttribute in attributeMap.keys) { 134 | when (val innerAttributeValue = attributeMap[innerAttribute]) { 135 | is Map<*, *> -> { // recursive case 136 | val innerMap = attributeMap[innerAttribute] as Map 137 | append("\"").append(innerAttribute).append("\": ") 138 | .append(getJsonFromMapRecursive(innerMap)).append(", ") 139 | } 140 | is List<*> -> { // recursive case 141 | append("\"").append(innerAttribute).append("\": ") 142 | .append(getJsonFromListRecursive(innerAttributeValue)).append(", ") 143 | } 144 | is String -> { // gotta use escape quotes for JSON strings 145 | append("\"").append(innerAttribute).append("\": \"") 146 | .append(getSafeSizeString(innerAttributeValue)).append("\", ") 147 | } 148 | is Number, is Boolean -> { 149 | append("\"").append(innerAttribute).append("\": ") 150 | .append(innerAttributeValue).append(", ") 151 | } 152 | is UUID -> { 153 | append("\"").append(innerAttribute).append("\": \"") 154 | .append(innerAttributeValue.toString()).append("\", ") 155 | } 156 | else -> throw UnsupportedOperationException( 157 | "Unsupported type for GELF message: " + innerAttributeValue!!.javaClass.simpleName 158 | ) 159 | } 160 | } 161 | if (this.toString().length > 1) { 162 | deleteCharAt(this.toString().length - 1) 163 | deleteCharAt(this.toString().length - 1) 164 | } 165 | // No else needed: the map was empty, no need to trim the last comma and space 166 | append("}") 167 | } 168 | 169 | @Suppress("UNCHECKED_CAST") 170 | internal fun getJsonFromListRecursive(attributeList: List<*>): String = buildString { 171 | append("[") 172 | for (element in attributeList) { 173 | when (element) { 174 | is Map<*, *> -> { // recursive case 175 | append(getJsonFromMapRecursive(element as Map)).append(", ") 176 | } 177 | is List<*> -> { // recursive case 178 | append(getJsonFromListRecursive(element)).append(", ") 179 | } 180 | is String -> append("\"").append(getSafeSizeString(element)).append("\", ") 181 | is Number, is Boolean -> append(element).append(", ") 182 | is UUID -> append("\"").append(element.toString()).append("\", ") 183 | else -> throw UnsupportedOperationException( 184 | "Unsupported type for GELF message: " + element?.javaClass?.simpleName 185 | ) 186 | } 187 | } 188 | if (this.toString().length > 1) { 189 | deleteCharAt(this.toString().length - 1) 190 | deleteCharAt(this.toString().length - 1) 191 | } 192 | // No else needed: the list was empty, no need to trim the last comma and space 193 | append("]") 194 | } 195 | 196 | private fun getSafeSizeString(input: String): String = 197 | if (input.length <= MAX_FIELD_LENGTH) { 198 | input 199 | } else { 200 | var safeSizeValue = input.substring(0, input.length.coerceAtMost(MAX_FIELD_LENGTH)) 201 | //correctly process UTF-16 surrogate pairs 202 | if (safeSizeValue.length > MAX_FIELD_LENGTH) { 203 | val correctedMaxWidth = 204 | if (Character.isLowSurrogate(safeSizeValue[MAX_FIELD_LENGTH])) { 205 | MAX_FIELD_LENGTH - 1 206 | } else { 207 | MAX_FIELD_LENGTH 208 | } 209 | safeSizeValue = safeSizeValue.substring( 210 | 0, 211 | safeSizeValue.length.coerceAtMost(correctedMaxWidth) 212 | ) 213 | } 214 | safeSizeValue 215 | } 216 | } 217 | 218 | private const val MAX_FIELD_LENGTH = 32_000 219 | -------------------------------------------------------------------------------- /graylog-provider/src/main/kotlin/graylog_provider/GraylogProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 21 | import okhttp3.Call 22 | import okhttp3.Callback 23 | import okhttp3.MediaType 24 | import okhttp3.MediaType.Companion.toMediaType 25 | import okhttp3.OkHttpClient 26 | import okhttp3.Request 27 | import okhttp3.RequestBody 28 | import okhttp3.RequestBody.Companion.toRequestBody 29 | import okhttp3.Response 30 | import java.io.IOException 31 | import java.text.DecimalFormat 32 | 33 | /** 34 | * Initializes a new {@code GraylogProvider} object. 35 | * 36 | * @param client an initialized {@link OkHttpClient} instance 37 | * @param graylogInputUrl the URL of the Graylog HTTP input to use. Example: {@code http://graylog.example.org:12202/gelf} 38 | * @param graylogHostName the name of the host application that is sending events 39 | * @property priorityFilter the {@code PriorityFilter} to use when evaluating events 40 | * 41 | * @author John Hunt on 6/28/17. 42 | */ 43 | class GraylogProvider constructor( 44 | private val client: OkHttpClient, 45 | private val graylogInputUrl: String, 46 | graylogHostName: String, 47 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 48 | ) : AnalyticsKitProvider { 49 | private val GELF_SPEC_VERSION = "1.1" 50 | private val jsonizer: EventJsonizer = EventJsonizer(GELF_SPEC_VERSION, graylogHostName) 51 | private val timedEvents: MutableMap by lazy { mutableMapOf() } 52 | private val eventTimes: MutableMap by lazy { mutableMapOf() } 53 | var callbackListener: GraylogResponseListener? = null 54 | 55 | /** 56 | * Specifies the [GraylogResponseListener] that should listen for callbacks. 57 | * 58 | * @param callbackListener the instance that should be notified on each Graylog response 59 | * @return the `GraylogProvider` instance (for builder-style convenience) 60 | */ 61 | fun setCallbackHandler(callbackListener: GraylogResponseListener): GraylogProvider { 62 | this.callbackListener = callbackListener 63 | return this 64 | } 65 | 66 | /** 67 | * Returns the filter used to restrict events by priority. 68 | * 69 | * @return the [PriorityFilter] instance the provider is using to determine if an 70 | * event of a given priority should be logged 71 | */ 72 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 73 | 74 | /** 75 | * Sends the event using provider-specific code. 76 | * 77 | * @param event an instantiated event 78 | */ 79 | @Throws(IllegalStateException::class) 80 | override fun sendEvent(event: AnalyticsEvent) { 81 | if (event.isTimed()) { // Hang onto it until it is done 82 | eventTimes[event.name()] = System.currentTimeMillis() 83 | timedEvents[event.name()] = event 84 | } else { // Send the event to the Graylog input 85 | logFromJson(event.name(), event.hashCode(), jsonizer.getJsonBody(event)) 86 | } 87 | } 88 | 89 | /** 90 | * End the timed event. 91 | * 92 | * @param timedEvent the event which has finished 93 | */ 94 | @Throws(IllegalStateException::class) 95 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 96 | val endTime = System.currentTimeMillis() 97 | val startTime = eventTimes.remove(timedEvent.name()) 98 | val finishedEvent = timedEvents.remove(timedEvent.name()) 99 | if (startTime != null && finishedEvent != null) { 100 | val durationSeconds = ((endTime - startTime) / 1_000).toDouble() 101 | val df = DecimalFormat("#.###") 102 | finishedEvent.putAttribute("event_duration", df.format(durationSeconds)) 103 | logFromJson( 104 | finishedEvent.name(), 105 | finishedEvent.hashCode(), 106 | jsonizer.getJsonBody(finishedEvent) 107 | ) 108 | } else { 109 | error("Attempted ending an event that was never started (or was previously ended): ${timedEvent.name()}") 110 | } 111 | } 112 | 113 | /** 114 | * Logs a JSON payload to your Graylog instance. 115 | * 116 | * @param eventName the result of calling [AnalyticsEvent.name] 117 | * @param eventHashCode the result of calling [AnalyticsEvent.hashCode] 118 | * @param json the payload to send to the Graylog input 119 | */ 120 | private fun logFromJson(eventName: String, eventHashCode: Int, json: String) { 121 | val jsonMediaType: MediaType = "application/json; charset=utf-8".toMediaType() 122 | val body: RequestBody = json.toRequestBody(jsonMediaType) 123 | val request: Request = Request.Builder() 124 | .url(graylogInputUrl) 125 | .post(body) 126 | .build() 127 | 128 | // Prevent the old NetworkOnMainThreadException by using async calls 129 | client.newCall(request).enqueue(object : Callback { 130 | override fun onFailure(call: Call, e: IOException) { 131 | providerCallback(GraylogResponse(420, 132 | "An error occurred communicating with the Graylog server", 133 | eventName, 134 | eventHashCode, 135 | json)) 136 | } 137 | 138 | @Throws(IOException::class) 139 | override fun onResponse(call: Call, response: Response) { 140 | providerCallback( 141 | GraylogResponse( 142 | response.code, 143 | response.message, 144 | eventName, 145 | eventHashCode, 146 | json 147 | ) 148 | ) 149 | } 150 | 151 | fun providerCallback(graylogResponse: GraylogResponse) = 152 | callbackListener?.onGraylogResponse(graylogResponse) 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /graylog-provider/src/main/kotlin/graylog_provider/GraylogResponse.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | /** 19 | * Data class containing the HTTP response code, HTTP response message, and the serialized 20 | * event JSON data. 21 | * @property code the HTTP response code 22 | * @property message the HTTP response message 23 | * @property eventName the result of calling [AnalyticsEvent.name] 24 | * @property eventHashCode the result of calling [AnalyticsEvent.hashCode] 25 | * @property jsonPayload the JSON payload that was sent via HTTP to your Graylog input 26 | * 27 | * @author John Hunt on 6/28/17. 28 | */ 29 | data class GraylogResponse( 30 | val code: Int, 31 | val message: String, 32 | val eventName: String, 33 | val eventHashCode: Int, 34 | val jsonPayload: String 35 | ) 36 | -------------------------------------------------------------------------------- /graylog-provider/src/main/kotlin/graylog_provider/GraylogResponseListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | import androidx.annotation.NonNull 19 | 20 | /** 21 | * Defines the contract for callbacks fired by the [GraylogProvider] instance. 22 | * 23 | * @author John Hunt on 6/28/17. 24 | */ 25 | interface GraylogResponseListener { 26 | /** 27 | * This method gets called after an [com.busybusy.analyticskit_android.AnalyticsEvent] is sent to a Graylog server. 28 | * 29 | * @param response the Response object describing a result of an HTTP call to a Graylog server and the event that was sent 30 | */ 31 | fun onGraylogResponse(@NonNull response: GraylogResponse) 32 | } 33 | -------------------------------------------------------------------------------- /graylog-provider/src/test/kotlin/com/busybusy/graylog_provider/EventJsonizerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import org.assertj.core.api.Assertions.assertThat 20 | import org.junit.Test 21 | import org.junit.runner.RunWith 22 | import org.robolectric.RobolectricTestRunner 23 | import org.robolectric.annotation.Config 24 | import java.util.UUID 25 | 26 | /** 27 | * Tests the [EventJsonizer] class. 28 | * 29 | * @author John Hunt on 6/29/17. 30 | */ 31 | @RunWith(RobolectricTestRunner::class) 32 | @Config(sdk = [23], manifest = Config.NONE) 33 | class EventJsonizerTest { 34 | val VERSION = "1.1" 35 | val HOST = "unit-test-android" 36 | var jsonizer = EventJsonizer(gelfSpecVersion = VERSION, host = HOST) 37 | 38 | @Test 39 | fun jsonBody_defaultFields() { 40 | val emails = mutableListOf("john.jacob@unittest.me", "john.jacob@unittest.us") 41 | val mapField = mutableMapOf("first_name" to "John", "last_name" to "Jacob", "emails" to emails) 42 | val event = AnalyticsEvent("test_event") 43 | .putAttribute("test_attribute_1", 100) 44 | .putAttribute("test_attribute_2", "200") 45 | .putAttribute("member_info", mapField) 46 | val now = System.currentTimeMillis() / 1000.0 // possibly flaky, shame on me 47 | val json = jsonizer.getJsonBody(event) 48 | assertThat(json).isEqualTo("""{"version": "$VERSION", "host": "$HOST", "short_message": "${event.name()}", 49 | | "timestamp": $now, "level": 6, "_test_attribute_1": 100, "_test_attribute_2": "200", "_member_info": 50 | | {"first_name": "John", "last_name": "Jacob", "emails": ["john.jacob@unittest.me", "john.jacob@unittest.us"]}}""" 51 | .trimMargin().replace("\n", "")) 52 | } 53 | 54 | @Test 55 | fun jsonBody_customFields() { 56 | val emails = mutableMapOf("personal" to "john.jacob@unittest.me", "work" to "john.jacob@unittest.us") 57 | val mapField = mutableMapOf("first_name" to "John", "last_name" to "Jacob", "emails" to emails) 58 | val timestamp = (System.currentTimeMillis() - 5000) / 1000.0 59 | val event = AnalyticsEvent("test_event") 60 | .putAttribute("level", 5) 61 | .putAttribute("full_message", "This is a test of the JSON conversion") 62 | .putAttribute("timestamp", timestamp) 63 | .putAttribute("test_attribute_1", 100) 64 | .putAttribute("test_attribute_2", "200") 65 | .putAttribute("member_info", mapField) 66 | val json = jsonizer.getJsonBody(event) 67 | assertThat(json).isEqualTo("""{"version": "$VERSION", "host": "$HOST", "short_message": "${event.name()}" 68 | |, "full_message": "This is a test of the JSON conversion", "timestamp": $timestamp, "level": 5, "_test_attribute_1": 100, 69 | | "_test_attribute_2": "200", "_member_info": {"first_name": "John", "last_name": "Jacob", "emails": {"personal": 70 | | "john.jacob@unittest.me", "work": "john.jacob@unittest.us"}}}""".trimMargin().replace("\n", "")) 71 | } 72 | 73 | @Test 74 | fun jsonBody_WithBooleans() { 75 | val timestamp = (System.currentTimeMillis() - 5000) / 1000.0 76 | val event = AnalyticsEvent("test_event") 77 | .putAttribute("level", 5) 78 | .putAttribute("full_message", "This is a test of the JSON conversion") 79 | .putAttribute("timestamp", timestamp) 80 | .putAttribute("test_attribute_1", 100) 81 | .putAttribute("test_attribute_2", "200") 82 | .putAttribute("can_pass_booleans", true) 83 | val json = jsonizer.getJsonBody(event) 84 | assertThat(json).isEqualTo("""{"version": "$VERSION", "host": "$HOST", "short_message": "${event.name()}" 85 | |, "full_message": "This is a test of the JSON conversion", "timestamp": $timestamp, 86 | |"level": 5, "_test_attribute_1": 100, "_test_attribute_2": "200", 87 | |"_can_pass_booleans": true}""".trimMargin().replace("\n", "")) 88 | } 89 | 90 | @Test 91 | fun jsonBody_WithUuid() { 92 | val timestamp = (System.currentTimeMillis() - 5000) / 1000.0 93 | val uuidString = "34d70df0-d36e-458f-b93f-d2bff7e8feac" 94 | val uuid = UUID.fromString(uuidString) 95 | val event = AnalyticsEvent("test_event") 96 | .putAttribute("level", 5) 97 | .putAttribute("full_message", "UUID conversion test") 98 | .putAttribute("timestamp", timestamp) 99 | .putAttribute("test_uuid_attribute", uuid) 100 | val json = jsonizer.getJsonBody(event) 101 | assertThat(json).isEqualTo("""{"version": "$VERSION", "host": "$HOST", "short_message": "${event.name()}" 102 | |, "full_message": "UUID conversion test", "timestamp": $timestamp, "level": 5, 103 | |"_test_uuid_attribute": "$uuidString"}""".trimMargin().replace("\n", "")) 104 | } 105 | 106 | @Test 107 | fun jsonBody_WithUuidInMap() { 108 | val uuid1 = UUID.randomUUID() 109 | val uuid2 = UUID.randomUUID() 110 | val uuids = mutableMapOf("id1" to uuid1, "id2" to uuid2) 111 | val timestamp = (System.currentTimeMillis() - 5000) / 1000.0 112 | 113 | val event = AnalyticsEvent("test_event") 114 | .putAttribute("level", 5) 115 | .putAttribute("full_message", "UUID conversion test") 116 | .putAttribute("timestamp", timestamp) 117 | .putAttribute("test_uuid_attribute", uuids) 118 | val json = jsonizer.getJsonBody(event) 119 | assertThat(json).isEqualTo("""{"version": "$VERSION", "host": "$HOST", "short_message": "${event.name()}" 120 | |, "full_message": "UUID conversion test", "timestamp": $timestamp, "level": 5, 121 | |"_test_uuid_attribute": {"id1": "$uuid1", "id2": "$uuid2"}}""".trimMargin().replace("\n", "")) 122 | } 123 | 124 | @Test 125 | fun jsonBody_WithUuidInList() { 126 | val uuid1 = UUID.randomUUID() 127 | val uuid2 = UUID.randomUUID() 128 | val uuidList = mutableListOf(uuid1, uuid2) 129 | val timestamp = (System.currentTimeMillis() - 5000) / 1000.0 130 | 131 | val event = AnalyticsEvent("test_event") 132 | .putAttribute("level", 5) 133 | .putAttribute("full_message", "UUID conversion test") 134 | .putAttribute("timestamp", timestamp) 135 | .putAttribute("test_uuid_attribute", uuidList) 136 | val json = jsonizer.getJsonBody(event) 137 | assertThat(json).isEqualTo("""{"version": "$VERSION", "host": "$HOST", "short_message": "${event.name()}" 138 | |, "full_message": "UUID conversion test", "timestamp": $timestamp, "level": 5, 139 | |"_test_uuid_attribute": ["$uuid1", "$uuid2"]}""".trimMargin().replace("\n", "")) 140 | } 141 | 142 | @Test 143 | fun jsonFromMapRecursive_emptyMap() { 144 | val map = mutableMapOf() 145 | val json = jsonizer.getJsonFromMapRecursive(map) 146 | assertThat(json).isEqualTo("{}") 147 | } 148 | 149 | @Test 150 | fun jsonFromMapRecursive_flatMapStructure() { 151 | val map = mutableMapOf("one" to "abc", "two" to 123, "three" to 321.123, "four" to true) 152 | val json = jsonizer.getJsonFromMapRecursive(map) 153 | assertThat(json).isEqualTo("""{"one": "abc", "two": 123, "three": 321.123, "four": true}""") 154 | } 155 | 156 | @Test 157 | fun jsonFromMapRecursive_recursiveMapStructure() { 158 | val levelTwoMap = mutableMapOf("a value" to 123, "l3" to mutableMapOf()) 159 | val map = mutableMapOf("l2" to levelTwoMap) 160 | val json = jsonizer.getJsonFromMapRecursive(map) 161 | assertThat(json).isEqualTo("""{"l2": {"a value": 123, "l3": {}}}""") 162 | } 163 | 164 | @Test 165 | fun jsonFromMapRecursive_mapOfLists() { 166 | val levelTwoMap = mutableMapOf("a value" to 123, "l3" to mutableMapOf()) 167 | val map = mutableMapOf("l2" to levelTwoMap) 168 | val json = jsonizer.getJsonFromMapRecursive(map) 169 | assertThat(json).isEqualTo("""{"l2": {"a value": 123, "l3": {}}}""") 170 | } 171 | 172 | @Test 173 | fun jsonFromListRecursive() { 174 | val levelThreeListOne = mutableListOf(31) 175 | val levelThreeListTwo = mutableListOf(32) 176 | val innerListOne = mutableListOf(levelThreeListOne) 177 | val innerListTwo = mutableListOf(levelThreeListTwo) 178 | val innerListThree = mutableListOf(true) 179 | val topLevelList = mutableListOf(innerListOne, innerListTwo, innerListThree) 180 | val json = jsonizer.getJsonFromListRecursive(topLevelList) 181 | assertThat(json).isEqualTo("[[[31]], [[32]], [true]]") 182 | } 183 | 184 | @Test 185 | fun jsonFromListRecursive_listOfMaps() { 186 | val levelTwoMap = mutableMapOf("a value" to 123, "l3" to mutableMapOf()) 187 | val map = mutableMapOf("l2" to levelTwoMap) 188 | val listMapList = mutableListOf(42, 24) 189 | val anotherMap = mutableMapOf("second" to "value", "second_num" to 31, "list_map_list" to listMapList) 190 | val list = mutableListOf(map, anotherMap) 191 | val json = jsonizer.getJsonFromListRecursive(list) 192 | assertThat(json).isEqualTo("""[{"l2": {"a value": 123, "l3": {}}}, {"second": "value", "second_num": 31, "list_map_list": [42, 24]}]""") 193 | 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /graylog-provider/src/test/kotlin/com/busybusy/graylog_provider/GraylogProviderFailedCallTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import okhttp3.OkHttpClient 20 | import okhttp3.mockwebserver.MockResponse 21 | import okhttp3.mockwebserver.MockWebServer 22 | import org.assertj.core.api.Assertions.assertThat 23 | import org.junit.After 24 | import org.junit.Before 25 | import org.junit.Test 26 | import java.io.IOException 27 | import java.util.concurrent.CountDownLatch 28 | import java.util.concurrent.TimeUnit 29 | 30 | /** 31 | * @author John Hunt on 6/29/17. 32 | */ 33 | class GraylogProviderFailedCallTest { 34 | private lateinit var provider: GraylogProvider 35 | private val mockServer = MockWebServer() 36 | private val httpClient = OkHttpClient.Builder().build() 37 | private lateinit var callbackListener: GraylogResponseListener 38 | private var testEventHashCode = 0 39 | private var logEventCalled = false 40 | private var loggedEventName: String? = null 41 | private var httpResponseCode = 0 42 | private var httpStatusMessage: String? = null 43 | private lateinit var lock: CountDownLatch 44 | 45 | @Before 46 | fun setup() { 47 | lock = CountDownLatch(1) 48 | callbackListener = object : GraylogResponseListener { 49 | override fun onGraylogResponse(response: GraylogResponse) { 50 | testEventHashCode = response.eventHashCode 51 | loggedEventName = response.eventName 52 | logEventCalled = true 53 | httpResponseCode = response.code 54 | httpStatusMessage = response.message 55 | lock.countDown() 56 | } 57 | 58 | } 59 | mockServer.enqueue(MockResponse().setResponseCode(420).setStatus("An error occurred communicating with the Graylog server")) 60 | try { 61 | mockServer.start() 62 | } catch (e: IOException) { 63 | e.printStackTrace() 64 | } 65 | provider = GraylogProvider(httpClient, "http://" + mockServer.hostName + ":" + mockServer.port, "unit-test-android") 66 | provider.setCallbackHandler(callbackListener) 67 | logEventCalled = false 68 | testEventHashCode = -1 69 | loggedEventName = null 70 | httpResponseCode = -1 71 | httpStatusMessage = "Not Sent" 72 | } 73 | 74 | @After 75 | fun tearDown() { 76 | try { 77 | mockServer.shutdown() 78 | } catch (e: IOException) { 79 | e.printStackTrace() 80 | } 81 | } 82 | 83 | @Test 84 | @Throws(InterruptedException::class) 85 | fun testSendEvent_unTimed_withParams() { 86 | val event = AnalyticsEvent("Graylog Event With Params Run") 87 | .putAttribute("some_param", "yes") 88 | .putAttribute("another_param", "yes again") 89 | provider.sendEvent(event) 90 | lock.await(50, TimeUnit.MILLISECONDS) 91 | assertThat(loggedEventName).isEqualTo("Graylog Event With Params Run") 92 | assertThat(logEventCalled).isEqualTo(true) 93 | assertThat(testEventHashCode).isEqualTo(event.hashCode()) 94 | assertThat(httpResponseCode).isEqualTo(420) 95 | assertThat(httpStatusMessage).isEqualTo("An error occurred communicating with the Graylog server") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /graylog-provider/src/test/kotlin/com/busybusy/graylog_provider/GraylogProviderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.graylog_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.busybusy.analyticskit_android.CommonEvents 21 | import com.busybusy.analyticskit_android.ContentViewEvent 22 | import com.busybusy.analyticskit_android.ErrorEvent 23 | import okhttp3.OkHttpClient 24 | import okhttp3.mockwebserver.MockResponse 25 | import okhttp3.mockwebserver.MockWebServer 26 | import org.assertj.core.api.Assertions.assertThat 27 | import org.junit.Before 28 | import org.junit.Test 29 | import java.io.IOException 30 | import java.util.* 31 | import java.util.concurrent.CountDownLatch 32 | import java.util.concurrent.TimeUnit 33 | 34 | /** 35 | * Tests the [GraylogProvider] class. 36 | * 37 | * @author John Hunt on 6/28/17. 38 | */ 39 | class GraylogProviderTest { 40 | private lateinit var provider: GraylogProvider 41 | private val mockServer = MockWebServer() 42 | private val httpClient = OkHttpClient.Builder().build() 43 | private lateinit var callbackListener: GraylogResponseListener 44 | private var testEventHashCode = 0 45 | private var logEventCalled = false 46 | private var loggedEventName: String? = null 47 | private lateinit var lock: CountDownLatch 48 | 49 | @Before 50 | fun setup() { 51 | lock = CountDownLatch(1) 52 | callbackListener = object : GraylogResponseListener { 53 | override fun onGraylogResponse(response: GraylogResponse) { 54 | logEventCalled = true 55 | loggedEventName = response.eventName 56 | testEventHashCode = response.eventHashCode 57 | lock.countDown() 58 | } 59 | 60 | } 61 | mockServer.enqueue(MockResponse().setResponseCode(202).setStatus("Accepted")) 62 | try { 63 | mockServer.start() 64 | } catch (e: IOException) { 65 | e.printStackTrace() 66 | } 67 | provider = GraylogProvider( 68 | client = httpClient, 69 | graylogInputUrl = "http://${mockServer.hostName}:${mockServer.port}", 70 | graylogHostName = "unit-test-android", 71 | ) 72 | provider.setCallbackHandler(callbackListener) 73 | logEventCalled = false 74 | testEventHashCode = -1 75 | loggedEventName = null 76 | } 77 | 78 | @Test 79 | fun testSetAndGetPriorityFilter() { 80 | val filter = PriorityFilter { false } 81 | val filteringProvider = GraylogProvider( 82 | client = httpClient, 83 | graylogInputUrl = "http://${mockServer.hostName}:${mockServer.port}", 84 | graylogHostName = "unit-test-android", 85 | priorityFilter = filter 86 | ) 87 | assertThat(filteringProvider.getPriorityFilter()).isEqualTo(filter) 88 | } 89 | 90 | @Test 91 | fun test_priorityFiltering_default() { 92 | val event = AnalyticsEvent("A Test Event") 93 | .setPriority(10) 94 | .send() 95 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 96 | event.setPriority(-9) 97 | .send() 98 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 99 | } 100 | 101 | @Test 102 | fun test_priorityFiltering_custom() { 103 | val filter = PriorityFilter { priorityLevel -> priorityLevel < 10 } 104 | val filteringProvider = GraylogProvider( 105 | client = httpClient, 106 | graylogInputUrl = "http://${mockServer.hostName}:${mockServer.port}", 107 | graylogHostName = "unit-test-android", 108 | priorityFilter = filter 109 | ) 110 | val event = AnalyticsEvent("A Test Event") 111 | .setPriority(10) 112 | .send() 113 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(false) 114 | event.setPriority(9) 115 | .send() 116 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 117 | } 118 | 119 | @Test 120 | @Throws(InterruptedException::class) 121 | fun testSendEvent_unTimed_noParams() { 122 | val event = AnalyticsEvent("Graylog Test Run No Params") 123 | provider.sendEvent(event) 124 | lock.await(50L, TimeUnit.MILLISECONDS) 125 | assertThat(logEventCalled).isEqualTo(true) 126 | assertThat(testEventHashCode).isEqualTo(event.hashCode()) 127 | assertThat(loggedEventName).isEqualTo("Graylog Test Run No Params") 128 | } 129 | 130 | @Test 131 | @Throws(InterruptedException::class) 132 | fun testSendEvent_unTimed_withParams() { 133 | val event = AnalyticsEvent("Graylog Event With Params Run") 134 | .putAttribute("some_param", "yes") 135 | .putAttribute("another_param", "yes again") 136 | provider.sendEvent(event) 137 | lock.await(50L, TimeUnit.MILLISECONDS) 138 | assertThat(loggedEventName).isEqualTo("Graylog Event With Params Run") 139 | assertThat(logEventCalled).isEqualTo(true) 140 | assertThat(testEventHashCode).isEqualTo(event.hashCode()) 141 | } 142 | 143 | @Test 144 | @Throws(InterruptedException::class) 145 | fun testLogContentViewEvent() { 146 | val event = ContentViewEvent("Test page 7") 147 | provider.sendEvent(event) 148 | lock.await(50L, TimeUnit.MILLISECONDS) 149 | assertThat(loggedEventName).isEqualTo(CommonEvents.CONTENT_VIEW) 150 | assertThat(logEventCalled).isEqualTo(true) 151 | assertThat(testEventHashCode).isEqualTo(event.hashCode()) 152 | } 153 | 154 | @Test 155 | @Throws(InterruptedException::class) 156 | fun testLogErrorEvent() { 157 | val event = ErrorEvent() 158 | .setMessage("something bad happened") 159 | .setException(EmptyStackException()) 160 | provider.sendEvent(event) 161 | lock.await(50L, TimeUnit.MILLISECONDS) 162 | assertThat(loggedEventName).isEqualTo(CommonEvents.ERROR) 163 | assertThat(logEventCalled).isEqualTo(true) 164 | assertThat(testEventHashCode).isEqualTo(event.hashCode()) 165 | } 166 | 167 | @Test 168 | fun testSendEvent_timed_noParams() { 169 | val event = AnalyticsEvent("Graylog Timed Event") 170 | .setTimed(true) 171 | provider.sendEvent(event) 172 | assertThat(logEventCalled).isEqualTo(false) 173 | } 174 | 175 | @Test 176 | fun testSendEvent_timed_withParams() { 177 | val event = AnalyticsEvent("Graylog Timed Event With Parameters") 178 | .setTimed(true) 179 | .putAttribute("some_param", "yes") 180 | .putAttribute("another_param", "yes again") 181 | provider.sendEvent(event) 182 | assertThat(logEventCalled).isEqualTo(false) 183 | } 184 | 185 | @Test 186 | @Throws(InterruptedException::class) 187 | fun testEndTimedEvent_Valid() { 188 | val event = AnalyticsEvent("Graylog Timed Event With Parameters") 189 | .setTimed(true) 190 | .putAttribute("some_param", "yes") 191 | .putAttribute("another_param", "yes again") 192 | provider.sendEvent(event) 193 | assertThat(logEventCalled).isEqualTo(false) 194 | provider.endTimedEvent(event) 195 | lock.await(50L, TimeUnit.MILLISECONDS) 196 | assertThat(loggedEventName).isEqualTo("Graylog Timed Event With Parameters") 197 | assertThat(logEventCalled).isEqualTo(true) 198 | assertThat(testEventHashCode).isEqualTo(event.hashCode()) 199 | } 200 | 201 | @Test 202 | fun test_endTimedEvent_WillThrow() { 203 | var didThrow = false 204 | val event = AnalyticsEvent("Graylog Timed Event With Parameters") 205 | .setTimed(true) 206 | try { 207 | provider.endTimedEvent(event) // attempting to end a timed event that was not started should throw an exception 208 | } catch (e: IllegalStateException) { 209 | didThrow = true 210 | } 211 | assertThat(didThrow).isEqualTo(true) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /intercom-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | intercom-provider.iml -------------------------------------------------------------------------------- /intercom-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.intercom_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | } 30 | buildTypes { 31 | release { 32 | debuggable false 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | debug { 37 | debuggable true 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility = 17 45 | targetCompatibility = 17 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | test.java.srcDirs += 'src/test/kotlin' 51 | } 52 | 53 | testOptions { 54 | unitTests.returnDefaultValues = true 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation project(path: rootProject.ext.analytics_kit) 60 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 61 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 62 | compileOnly 'io.intercom.android:intercom-sdk-base:12.0.0' 63 | 64 | testImplementation 'io.intercom.android:intercom-sdk-base:12.0.0' 65 | testImplementation "junit:junit:$rootProject.ext.junit_version" 66 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 67 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 68 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 69 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$rootProject.ext.mockitoKotlinVersion" 70 | testImplementation "org.mockito:mockito-inline:$rootProject.ext.mockitoVersion" 71 | } 72 | 73 | afterEvaluate { 74 | publishing { 75 | publications { 76 | releaseIntercom(MavenPublication) { // Creates a Maven publication called "releaseIntercom". 77 | from components.release // Applies the component for the release build variant. 78 | 79 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 80 | artifactId = 'intercom-provider' 81 | version = "$VERSION_NAME" 82 | } 83 | } 84 | } 85 | } 86 | 87 | task sourcesJar(type: Jar) { 88 | archiveClassifier.set("sources") 89 | from android.sourceSets.main.java.srcDirs 90 | } 91 | 92 | task javadoc(type: Javadoc) { 93 | failOnError false 94 | source = android.sourceSets.main.java.sourceFiles 95 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 96 | } 97 | 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | archiveClassifier.set("javadoc") 100 | from javadoc.destinationDir 101 | } 102 | 103 | artifacts { 104 | archives sourcesJar 105 | archives javadocJar 106 | } 107 | -------------------------------------------------------------------------------- /intercom-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Sources/android_sdk_mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /intercom-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /intercom-provider/src/main/kotlin/intercom_provider/IntercomProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.intercom_provider 17 | 18 | import io.intercom.android.sdk.Intercom 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 21 | import com.busybusy.analyticskit_android.AnalyticsEvent 22 | import java.text.DecimalFormat 23 | 24 | /** 25 | * Implements Intercom as a provider to use with [com.busybusy.analyticskit_android.AnalyticsKit] 26 | * 27 | * @property intercom your already-initialized [Intercom] instance. 28 | * Please call [Intercom.initialize] prior to setting up your IntercomProvider. 29 | * @property priorityFilter the `PriorityFilter` to use when evaluating events if an event of a given priority should be logged 30 | * @property quietMode `true` to silently take the first ten items of metadata in the event's attributes per Intercom limits. 31 | * ` false` to throw Exceptions when events contain more than the allowed limit of metadata items. 32 | * 33 | * @author John Hunt on 5/19/17. 34 | */ 35 | class IntercomProvider( 36 | private val intercom: Intercom, 37 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 38 | private val quietMode: Boolean = false, 39 | ) : AnalyticsKitProvider { 40 | 41 | private val timedEvents: MutableMap by lazy { mutableMapOf() } 42 | private val eventTimes: MutableMap by lazy { mutableMapOf() } 43 | 44 | /** 45 | * Returns the filter used to restrict events by priority. 46 | * 47 | * @return the [PriorityFilter] instance the provider is using to determine if an 48 | * event of a given priority should be logged 49 | */ 50 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 51 | 52 | /** 53 | * Sends the event using provider-specific code 54 | * @param event an instantiated event. **Note:** When sending timed events, be aware that this provider does not support concurrent timed events with the same name. 55 | * @see AnalyticsKitProvider 56 | */ 57 | override fun sendEvent(event: AnalyticsEvent) { 58 | when { 59 | event.isTimed() -> { 60 | // Hang on to the event until it is done 61 | eventTimes[event.name()] = System.currentTimeMillis() 62 | timedEvents[event.name()] = event 63 | } 64 | else -> logIntercomEvent(event) // Send the event through the Intercom SDK 65 | } 66 | } 67 | 68 | /** 69 | * End the timed event. 70 | * 71 | * @param timedEvent the event which has finished 72 | */ 73 | @Throws(IllegalStateException::class) 74 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 75 | val endTime = System.currentTimeMillis() 76 | val startTime = eventTimes.remove(timedEvent.name()) 77 | val finishedEvent = timedEvents.remove(timedEvent.name()) 78 | if (startTime != null && finishedEvent != null) { 79 | val durationSeconds = (endTime - startTime) / 1000.0 80 | val df = DecimalFormat("#.###") 81 | finishedEvent.putAttribute(DURATION_KEY, df.format(durationSeconds)) 82 | logIntercomEvent(finishedEvent) 83 | } else { 84 | error("Attempted ending an event that was never started (or was previously ended): ${timedEvent.name()}") 85 | } 86 | } 87 | 88 | @Throws(IllegalStateException::class) 89 | private fun logIntercomEvent(event: AnalyticsEvent) { 90 | val sanitizedAttributes: MutableMap? = 91 | if (event.attributes != null && event.attributes!!.keys.size > MAX_METADATA_ATTRIBUTES) { 92 | if (quietMode) { 93 | val keepAttributeNames = event.attributes!!.keys.take(MAX_METADATA_ATTRIBUTES) 94 | event.attributes!!.filter { 95 | keepAttributeNames.contains(it.key) 96 | }.toMutableMap() 97 | } else { 98 | error("Intercom does not support more than $MAX_METADATA_ATTRIBUTES" + 99 | " metadata fields. See https://www.intercom.com/help/en/articles/175-set-up-event-tracking-in-intercom.") 100 | } 101 | } else { 102 | event.attributes 103 | } 104 | 105 | Intercom.client().logEvent(event.name(), sanitizedAttributes) 106 | } 107 | } 108 | 109 | internal const val MAX_METADATA_ATTRIBUTES = 10 110 | internal const val DURATION_KEY = "event_duration" 111 | -------------------------------------------------------------------------------- /kissmetrics-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | kissmetrics-provider.iml -------------------------------------------------------------------------------- /kissmetrics-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.analyticskit.kissmetrics_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | } 30 | 31 | buildTypes { 32 | release { 33 | debuggable false 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | } 37 | debug { 38 | debuggable true 39 | minifyEnabled false 40 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 41 | } 42 | } 43 | 44 | compileOptions { 45 | sourceCompatibility = 17 46 | targetCompatibility = 17 47 | } 48 | 49 | sourceSets { 50 | main.java.srcDirs += 'src/main/kotlin' 51 | test.java.srcDirs += 'src/test/kotlin' 52 | } 53 | 54 | testOptions { 55 | unitTests.returnDefaultValues = true 56 | } 57 | } 58 | 59 | dependencies { 60 | implementation 'androidx.core:core-ktx:1.10.1' 61 | 62 | implementation project(path: rootProject.ext.analytics_kit) 63 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 64 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 65 | // compileOnly 'com.kissmetrics.sdk:KISSmetricsSDK:2.3.1' // Not working 66 | // workaround until KISSmetrics migrates their SDK off jcenter(); version 2.3.1 67 | compileOnly 'com.github.jahunt1:KISSmetrics-Android-SDK:gradle-wrapper-SNAPSHOT' 68 | 69 | // workaround until KISSmetrics migrates their SDK off jcenter(); version 2.3.1 70 | testImplementation 'com.github.jahunt1:KISSmetrics-Android-SDK:gradle-wrapper-SNAPSHOT' 71 | testImplementation "junit:junit:$rootProject.ext.junit_version" 72 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 73 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 74 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 75 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$rootProject.ext.mockitoKotlinVersion" 76 | testImplementation "org.mockito:mockito-inline:$rootProject.ext.mockitoVersion" 77 | } 78 | 79 | afterEvaluate { 80 | publishing { 81 | publications { 82 | releaseKissMetrics(MavenPublication) { // Creates a Maven publication called "releaseKissMetrics". 83 | from components.release // Applies the component for the release build variant. 84 | 85 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 86 | artifactId = 'kissmetrics-provider' 87 | version = "$VERSION_NAME" 88 | } 89 | } 90 | } 91 | } 92 | 93 | task sourcesJar(type: Jar) { 94 | archiveClassifier.set("sources") 95 | from android.sourceSets.main.java.srcDirs 96 | } 97 | 98 | task javadoc(type: Javadoc) { 99 | failOnError false 100 | source = android.sourceSets.main.java.sourceFiles 101 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 102 | } 103 | 104 | task javadocJar(type: Jar, dependsOn: javadoc) { 105 | archiveClassifier.set("javadoc") 106 | from javadoc.destinationDir 107 | } 108 | 109 | artifacts { 110 | archives sourcesJar 111 | archives javadocJar 112 | } 113 | -------------------------------------------------------------------------------- /kissmetrics-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /kissmetrics-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /kissmetrics-provider/src/main/kotlin/com/busybusy/analyticskit/kissmetrics_provider/KissMetricsProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 - 2023 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.busybusy.analyticskit.kissmetrics_provider 18 | 19 | import com.busybusy.analyticskit_android.AnalyticsEvent 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 21 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 22 | import com.kissmetrics.sdk.KISSmetricsAPI 23 | import com.kissmetrics.sdk.KISSmetricsAPI.RecordCondition 24 | import java.text.DecimalFormat 25 | 26 | /** 27 | * Provider that facilitates reporting events to KissMetrics. 28 | * *Note* The KissMetrics SDK supports sending a [RecordCondition]. To send a [RecordCondition] with this provider, 29 | * simply add it to the event's attribute map with "`record_condition`" as the key. From Kotlin code this can 30 | * also be accomplished by calling the `recordCondition(condition)` extension function on an [AnalyticsEvent] instance. 31 | * @see KissMetrics Android SDK documentation 32 | * 33 | * @constructor Initializes a new [KissMetricsProvider] object. 34 | * 35 | * @property kissMetrics your already-initialized [KISSmetricsAPI] instance. Please call 36 | * KISSmetricsAPI.sharedAPI(API_KEY, APPLICATION_CONTEXT) prior to setting up your [KissMetricsProvider]. 37 | * @property priorityFilter the [PriorityFilter] to use when evaluating events 38 | */ 39 | class KissMetricsProvider( 40 | private val kissMetrics: KISSmetricsAPI, 41 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 42 | ) : AnalyticsKitProvider { 43 | private val timedEvents: MutableMap by lazy { mutableMapOf() } 44 | private val eventTimes: MutableMap by lazy { mutableMapOf() } 45 | 46 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 47 | 48 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 49 | val endTime: Long = System.currentTimeMillis() 50 | val startTime: Long? = this.eventTimes.remove(timedEvent.name()) 51 | val finishedEvent: AnalyticsEvent? = timedEvents.remove(timedEvent.name()) 52 | 53 | if (startTime != null && finishedEvent != null) { 54 | val durationSeconds = (endTime - startTime) / 1000.0 55 | val df = DecimalFormat("#.###") 56 | finishedEvent.putAttribute(DURATION, df.format(durationSeconds)) 57 | logKissMetricsEvent(finishedEvent) 58 | } else { 59 | error("Attempted ending an event that was never started (or was previously ended): " + timedEvent.name()) 60 | } 61 | } 62 | 63 | override fun sendEvent(event: AnalyticsEvent) { 64 | if (event.isTimed()) { // Hang onto it until it is done 65 | eventTimes[event.name()] = System.currentTimeMillis() 66 | timedEvents[event.name()] = event 67 | } else { // Send the event through the KissMetrics SDK 68 | logKissMetricsEvent(event) 69 | } 70 | } 71 | 72 | private fun logKissMetricsEvent(event: AnalyticsEvent) { 73 | if (event.attributes != null && event.attributes?.isNotEmpty() == true) { 74 | val attributes: MutableMap = event.attributes as MutableMap 75 | if (attributes.containsKey(RECORD_CONDITION)) { 76 | val condition: RecordCondition = 77 | attributes.remove(RECORD_CONDITION) as RecordCondition 78 | if (attributes.isNotEmpty()) { // other attributes there we need to track 79 | val stringProperties = attributes.stringifyAttributes() 80 | KISSmetricsAPI.sharedAPI().record(event.name(), stringProperties, condition) 81 | } else { // just the condition to send 82 | KISSmetricsAPI.sharedAPI().record(event.name(), condition) 83 | } 84 | } else { // no condition to record, just attributes 85 | val stringProperties = attributes.stringifyAttributes() 86 | KISSmetricsAPI.sharedAPI().record(event.name(), stringProperties) 87 | } 88 | } else { // record event by name only 89 | KISSmetricsAPI.sharedAPI().record(event.name()) 90 | } 91 | } 92 | } 93 | 94 | internal const val DURATION = "event_duration" 95 | const val RECORD_CONDITION = "record_condition" 96 | 97 | /** 98 | * Converts an attributes Map to to Map to appease the KissMetrics API. 99 | */ 100 | fun Map.stringifyAttributes(): Map = when { 101 | this.isNotEmpty() -> this.map { (key, value) -> key to value.toString() }.toMap() 102 | else -> emptyMap() 103 | } 104 | 105 | 106 | fun AnalyticsEvent.recordCondition(condition: RecordCondition): AnalyticsEvent { 107 | this.putAttribute(RECORD_CONDITION, condition) 108 | return this 109 | } 110 | -------------------------------------------------------------------------------- /mixpanel-provider/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | mixpanel-provider.iml -------------------------------------------------------------------------------- /mixpanel-provider/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018, 2020-2023 busybusy, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id 'com.android.library' 19 | id 'kotlin-android' 20 | } 21 | 22 | android { 23 | namespace 'com.busybusy.mixpanel_provider' 24 | compileSdk rootProject.ext.compileSdkVersion 25 | 26 | defaultConfig { 27 | minSdkVersion rootProject.ext.minSdkVersion 28 | targetSdkVersion rootProject.ext.targetSdkVersion 29 | } 30 | buildTypes { 31 | release { 32 | debuggable false 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | debug { 37 | debuggable true 38 | minifyEnabled false 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility = 17 45 | targetCompatibility = 17 46 | } 47 | 48 | sourceSets { 49 | main.java.srcDirs += 'src/main/kotlin' 50 | test.java.srcDirs += 'src/test/kotlin' 51 | } 52 | 53 | testOptions { 54 | unitTests.returnDefaultValues = true 55 | } 56 | } 57 | 58 | dependencies { 59 | implementation project(path: rootProject.ext.analytics_kit) 60 | api "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 61 | api "androidx.annotation:annotation:$rootProject.ext.annotationsVersion" 62 | implementation 'com.mixpanel.android:mixpanel-android:6.5.2' 63 | compileOnly 'com.google.android.gms:play-services-gcm:17.0.0' 64 | 65 | testImplementation 'com.mixpanel.android:mixpanel-android:6.5.2' 66 | testImplementation "junit:junit:$rootProject.ext.junit_version" 67 | testImplementation "org.assertj:assertj-core:$rootProject.ext.assertjVersion" 68 | testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.ext.kotlinVersion" 69 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$rootProject.ext.kotlinVersion" 70 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$rootProject.ext.mockitoKotlinVersion" 71 | } 72 | 73 | afterEvaluate { 74 | publishing { 75 | publications { 76 | releaseMixpanel(MavenPublication) { // Creates a Maven publication called "releaseMixpanel". 77 | from components.release // Applies the component for the release build variant. 78 | 79 | groupId = 'com.github.busybusy.AnalyticsKit-Android' 80 | artifactId = 'mixpanel-provider' 81 | version = "$VERSION_NAME" 82 | } 83 | } 84 | } 85 | } 86 | 87 | task sourcesJar(type: Jar) { 88 | archiveClassifier.set("sources") 89 | from android.sourceSets.main.java.srcDirs 90 | } 91 | 92 | task javadoc(type: Javadoc) { 93 | failOnError false 94 | source = android.sourceSets.main.java.sourceFiles 95 | setClasspath(getClasspath() + project.files(android.getBootClasspath().join(File.pathSeparator))) 96 | } 97 | 98 | task javadocJar(type: Jar, dependsOn: javadoc) { 99 | archiveClassifier.set("javadoc") 100 | from javadoc.destinationDir 101 | } 102 | 103 | artifacts { 104 | archives sourcesJar 105 | archives javadocJar 106 | } 107 | -------------------------------------------------------------------------------- /mixpanel-provider/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Sources/android_sdk_mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /mixpanel-provider/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /mixpanel-provider/src/main/kotlin/mixpanel_provider/MixpanelProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.mixpanel_provider 17 | 18 | import com.mixpanel.android.mpmetrics.MixpanelAPI 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.busybusy.analyticskit_android.AnalyticsKitProvider 21 | import com.busybusy.analyticskit_android.AnalyticsEvent 22 | 23 | /** 24 | * Implements Mixpanel as a provider to use with [com.busybusy.analyticskit_android.AnalyticsKit] 25 | * 26 | * @property mixpanelApi the initialized [MixpanelAPI] instance associated with the application. 27 | * Just send `MixpanelAPI.getInstance(context, MIXPANEL_TOKEN)` 28 | * @property priorityFilter the [PriorityFilter] to use when evaluating if an event should be sent to this provider's platform. 29 | * By default, this provider will log all events regardless of priority. 30 | * 31 | * @author John Hunt on 3/16/16. 32 | */ 33 | class MixpanelProvider( 34 | private val mixpanelApi: MixpanelAPI, 35 | private val priorityFilter: PriorityFilter = PriorityFilter { true }, 36 | ) : AnalyticsKitProvider { 37 | 38 | /** 39 | * Returns the filter used to restrict events by priority. 40 | * 41 | * @return the {@link PriorityFilter} instance the provider is using to determine if an event of a given priority should be logged 42 | */ 43 | override fun getPriorityFilter(): PriorityFilter = priorityFilter 44 | 45 | /** 46 | * Sends the event using provider-specific code. 47 | * 48 | * @param event an instantiated event 49 | */ 50 | override fun sendEvent(event: AnalyticsEvent) = when { 51 | event.isTimed() -> mixpanelApi.timeEvent(event.name()) 52 | else -> mixpanelApi.trackMap(event.name(), event.attributes) 53 | } 54 | 55 | /** 56 | * End the timed event. 57 | * 58 | * @param timedEvent the event which has finished 59 | */ 60 | override fun endTimedEvent(timedEvent: AnalyticsEvent) { 61 | mixpanelApi.trackMap(timedEvent.name(), timedEvent.attributes) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /mixpanel-provider/src/test/kotlin/com/busybusy/mixpanel_provider/MixpanelProviderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 - 2022 busybusy, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.busybusy.mixpanel_provider 17 | 18 | import com.busybusy.analyticskit_android.AnalyticsEvent 19 | import com.busybusy.analyticskit_android.AnalyticsKitProvider.PriorityFilter 20 | import com.mixpanel.android.mpmetrics.MixpanelAPI 21 | import com.nhaarman.mockitokotlin2.doAnswer 22 | import com.nhaarman.mockitokotlin2.mock 23 | import org.assertj.core.api.Assertions.assertThat 24 | import org.assertj.core.api.Assertions.entry 25 | import org.junit.Before 26 | import org.junit.Test 27 | import org.mockito.ArgumentMatchers.anyMap 28 | import org.mockito.ArgumentMatchers.anyString 29 | 30 | /** 31 | * Tests the [MixpanelProvider] class 32 | * 33 | * @author John Hunt on 3/16/16. 34 | */ 35 | class MixpanelProviderTest { 36 | private val mockMixpanelAPI: MixpanelAPI = mock() 37 | private val provider = MixpanelProvider(mockMixpanelAPI) 38 | 39 | private lateinit var testEventName: String 40 | private lateinit var testEventPropertiesMap: Map 41 | private var trackMapCalled = false 42 | private var timeEventCalled = false 43 | 44 | private val timedEvent = AnalyticsEvent("Timed Event") 45 | .setTimed(true) 46 | .putAttribute("timed_attribute1", "timed_test1") 47 | private val untimedEvent = AnalyticsEvent("Untimed Event") 48 | .putAttribute("attribute1", "test1") 49 | .putAttribute("attribute2", "test2") 50 | 51 | @Suppress("UNCHECKED_CAST") 52 | @Before 53 | fun setup() { 54 | testEventPropertiesMap = mutableMapOf() 55 | trackMapCalled = false 56 | timeEventCalled = false 57 | 58 | // Mock behavior for when MixpanelAPI.trackMap(String, Map) is called 59 | doAnswer { invocation -> 60 | val args = invocation.arguments 61 | testEventName = args[0] as String 62 | testEventPropertiesMap = args[1] as Map 63 | trackMapCalled = true 64 | null 65 | }.`when`(mockMixpanelAPI).trackMap(anyString(), anyMap()) 66 | 67 | // Mock behavior for when MixpanelAPI.timeEvent(String) is called 68 | doAnswer { invocation -> 69 | val args = invocation.arguments 70 | testEventName = args[0] as String 71 | timeEventCalled = true 72 | null 73 | }.`when`(mockMixpanelAPI).timeEvent(anyString()) 74 | } 75 | 76 | @Test 77 | fun testGetAndSetPriorityFilter() { 78 | val filter = PriorityFilter { false } 79 | val filteringProvider = MixpanelProvider(mixpanelApi = mockMixpanelAPI, priorityFilter = filter) 80 | assertThat(filteringProvider.getPriorityFilter()).isEqualTo(filter) 81 | } 82 | 83 | @Test 84 | fun test_priorityFiltering_default() { 85 | val event = AnalyticsEvent("Let's test event priorities") 86 | .setPriority(10) 87 | .send() 88 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 89 | event.setPriority(-9) 90 | .send() 91 | assertThat(provider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 92 | } 93 | 94 | @Test 95 | fun test_priorityFiltering_custom() { 96 | val filter = PriorityFilter { priorityLevel -> priorityLevel < 10 } 97 | val filteringProvider = MixpanelProvider(mixpanelApi = mockMixpanelAPI, priorityFilter = filter) 98 | val event = AnalyticsEvent("Let's test event priorities again") 99 | .setPriority(10) 100 | .send() 101 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(false) 102 | event.setPriority(9) 103 | .send() 104 | assertThat(filteringProvider.getPriorityFilter().shouldLog(event.priority)).isEqualTo(true) 105 | } 106 | 107 | @Suppress("UsePropertyAccessSyntax") 108 | @Test 109 | fun testSendEvent_notTimed() { 110 | provider.sendEvent(untimedEvent) 111 | assertThat(trackMapCalled).isEqualTo(true) 112 | assertThat(timeEventCalled).isEqualTo(false) 113 | assertThat(testEventName).isNotNull() 114 | assertThat(testEventName).isEqualTo("Untimed Event") 115 | assertThat(testEventPropertiesMap).containsAllEntriesOf(mutableMapOf("attribute1" to "test1", "attribute2" to "test2")) 116 | } 117 | 118 | @Test 119 | fun testSendEvent_timed() { 120 | provider.sendEvent(timedEvent) 121 | assertThat(timeEventCalled).isEqualTo(true) 122 | assertThat(trackMapCalled).isEqualTo(false) 123 | assertThat(testEventName).isEqualTo("Timed Event") 124 | assertThat(testEventPropertiesMap).isEmpty() 125 | } 126 | 127 | @Test 128 | fun testEndTimedEvent() { 129 | provider.endTimedEvent(timedEvent) 130 | assertThat(timeEventCalled).isEqualTo(false) 131 | assertThat(trackMapCalled).isEqualTo(true) 132 | assertThat(testEventName).isEqualTo("Timed Event") 133 | assertThat(testEventPropertiesMap).containsExactly(entry("timed_attribute1", "timed_test1")) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':analyticskit', ':mixpanel-provider', ':flurry-provider', ':google-analytics-provider', ':intercom-provider', ':kissmetrics-provider', ':graylog-provider', ':firebase-provider' 2 | --------------------------------------------------------------------------------