├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/BusyApache20.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/All_Unit_Tests.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------