From 9b7a990d3d3dbc9a8f7aa0d9f749f7d503f8b9a4 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Tue, 22 Aug 2017 17:31:20 -0700 Subject: [PATCH 01/20] Updating --- README.md | 16 ++++++++-------- build.gradle | 2 +- .../firebase/FirebaseIntegration.java | 4 ++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8bee511..c5c023e 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,26 @@ -analytics-android-integration-quantcast +analytics-android-integration-firebase ======================================= [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.segment.analytics.android.integrations/quantcast/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.segment.analytics.android.integrations/quantcast) [![Javadocs](http://javadoc-badge.appspot.com/com.segment.analytics.android.integrations/quantcast.svg?label=javadoc)](http://javadoc-badge.appspot.com/com.segment.analytics.android.integrations/quantcast) -Quantcast integration for [analytics-android](https://github.com/segmentio/analytics-android). +Firebase Analytics integration for [analytics-android](https://github.com/segmentio/analytics-android). ## Installation -To install the Segment-Quantcast integration, simply add this line to your gradle file: +To install the Segment-Firebase integration, simply add this line to your gradle file: ``` -compile 'com.segment.analytics.android.integrations:quantcast:+' +compile 'com.segment.analytics.android.integrations:firebase-analytics:+' ``` ## Usage -After adding the dependency, you must register the integration with our SDK. To do this, import the Quantcast integration: +After adding the dependency, you must register the integration with our SDK. To do this, import the Firebase integration: ``` -import com.segment.analytics.android.integrations.quantcast.QuantcastIntegration; +import com.segment.analytics.android.integrations.firebase-analytics.FirebaseIntegration; ``` @@ -28,11 +28,11 @@ And add the following line: ``` analytics = new Analytics.Builder(this, "write_key") - .use(QuantcastIntegration.FACTORY) + .use(FirebaseIntegration.FACTORY) .build(); ``` -Please see [our documentation](https://segment.com/docs/integrations/quantcast/) for more information. +Please see [our documentation](https://segment.com/docs/integrations/firebase-analytics/) for more information. ## License diff --git a/build.gradle b/build.gradle index 2fcab7f..5339db3 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:1.3.1' classpath 'com.f2prateek.checkstyle:checkstyle:1.0.1' + classpath 'com.google.gms:google-services:3.1.0' } } @@ -67,4 +68,3 @@ dependencies { } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') -apply plugin: 'com.google.gms.google-services' diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index caf2054..30507f8 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -40,6 +40,10 @@ public class FirebaseIntegration extends Integration { logger.debug("INTERNET is required for Firebase Analytics."); return null; } + if (!hasPermission(analytics.getApplication(), Manifest.permission.WAKE_LOCK)) { + logger.debug("WAKE_LOCK is required for Firebase Analytics."); + return null; + } Context context = analytics.getApplication(); From a2ac1f06970ec1b97cd71c9a3534a9658392886e Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 23 Aug 2017 10:26:32 -0700 Subject: [PATCH 02/20] Update gradle file --- build.gradle | 42 +++++++++--------------- gradle/wrapper/gradle-wrapper.properties | 4 +-- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/build.gradle b/build.gradle index 5339db3..e0ca7d5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,60 +1,48 @@ buildscript { repositories { - mavenCentral() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' - classpath 'com.f2prateek.checkstyle:checkstyle:1.0.1' - classpath 'com.google.gms:google-services:3.1.0' + classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.f2prateek.javafmt:javafmt:0.1.6' } } apply plugin: 'com.android.library' -apply plugin: 'checkstyle' -apply plugin: 'com.f2prateek.checkstyle' +apply plugin: 'com.f2prateek.javafmt' android { - compileSdkVersion 23 - buildToolsVersion '23.0.1' + compileSdkVersion 26 + buildToolsVersion '25.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 23 - compileSdkVersion 23 + targetSdkVersion 26 + compileSdkVersion 26 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } - - lintOptions { - // Okio https://cloudup.com/cp7bi10o2C3g - disable 'InvalidPackage' - } -} - -checkstyle { - configFile rootProject.file('gradle/checkstyle.xml') } dependencies { repositories { - mavenCentral() + jcenter() + maven { + url "https://maven.google.com/" + } } - provided 'com.segment.analytics.android:analytics:4.0.9' + compile 'com.segment.analytics.android:analytics:4.2.6' + testCompile 'com.segment.analytics.android:analytics-tests:4.2.6' compile 'com.google.firebase:firebase-core:11.2.0' testCompile 'junit:junit:4.12' - testCompile('org.robolectric:robolectric:3.0') { - exclude group: 'commons-logging', module: 'commons-logging' - exclude group: 'org.apache.httpcomponents', module: 'httpclient' - } - - testCompile 'com.segment.analytics.android:analytics-tests:4.0.9' + testCompile 'org.robolectric:robolectric:3.3.2' testCompile 'org.assertj:assertj-core:1.7.1' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1d242e1..ef49179 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Dec 26 13:17:50 MST 2014 +#Wed Aug 23 10:10:41 PDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip From 617d74db6cbc8ba5a3c111b560f39c48bbd08686 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 23 Aug 2017 10:54:37 -0700 Subject: [PATCH 03/20] Update manifest file --- src/main/AndroidManifest.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 5f84218..9c08ff9 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1 +1,10 @@ - + + + + + + + + + From 10558c22b855c9ae8aa38fa04139afe1b708a5ad Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 23 Aug 2017 17:29:14 -0700 Subject: [PATCH 04/20] Updating identify method mapping --- .../firebase/FirebaseIntegration.java | 103 ++++++++++-------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 30507f8..010daaa 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -4,8 +4,9 @@ import android.app.Activity; import android.content.Context; -import com.google.firebase.analytics.FirebaseAnalytics; // FirebaseAnalytics class +import com.google.firebase.analytics.FirebaseAnalytics; import com.segment.analytics.Analytics; +import com.segment.analytics.Traits; import com.segment.analytics.ValueMap; import com.segment.analytics.integrations.IdentifyPayload; import com.segment.analytics.integrations.Integration; @@ -13,13 +14,15 @@ import com.segment.analytics.integrations.ScreenPayload; import com.segment.analytics.integrations.TrackPayload; +import java.util.List; +import java.util.Map; + import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; -import static com.segment.analytics.Analytics.LogLevel.VERBOSE; /** - * Google Analytics for Firebase is a free app measurement solution that provides insight on - * app usage and user engagement. + * Google Analytics for Firebase is a free app measurement solution that provides insight on app + * usage and user engagement. * * @see Google Analytics for Firebase * @see Google Analytics for Firebase Integration @@ -27,68 +30,82 @@ */ public class FirebaseIntegration extends Integration { - public static FirebaseAnalytics mFirebaseAnalytics; - - public static final Factory FACTORY = new Factory() { - @Override public Integration create(ValueMap settings, Analytics analytics) { - Logger logger = analytics.logger(FIREBASE_ANALYTICS_KEY); - if (!hasPermission(analytics.getApplication(), Manifest.permission.ACCESS_NETWORK_STATE)) { - logger.debug("ACCESS_NETWORK_STATE is required for Firebase Analytics."); - return null; - } - if (!hasPermission(analytics.getApplication(), Manifest.permission.INTERNET)) { - logger.debug("INTERNET is required for Firebase Analytics."); - return null; - } - if (!hasPermission(analytics.getApplication(), Manifest.permission.WAKE_LOCK)) { - logger.debug("WAKE_LOCK is required for Firebase Analytics."); - return null; - } - - Context context = analytics.getApplication(); - - return new FirebaseIntegration(context); - } - - @Override public String key() { - return FIREBASE_ANALYTICS_KEY; - } - }; - - private static final String FIREBASE_ANALYTICS_KEY = "Firebase Analytics"; + public static final Factory FACTORY = + new Factory() { + @Override + public Integration create(ValueMap settings, Analytics analytics) { + Logger logger = analytics.logger(FIREBASE_ANALYTICS_KEY); + if (!hasPermission( + analytics.getApplication(), Manifest.permission.ACCESS_NETWORK_STATE)) { + logger.debug("ACCESS_NETWORK_STATE is required for Firebase Analytics."); + return null; + } + if (!hasPermission(analytics.getApplication(), Manifest.permission.INTERNET)) { + logger.debug("INTERNET is required for Firebase Analytics."); + return null; + } + if (!hasPermission(analytics.getApplication(), Manifest.permission.WAKE_LOCK)) { + logger.debug("WAKE_LOCK is required for Firebase Analytics."); + return null; + } + + Context context = analytics.getApplication(); + + return new FirebaseIntegration(context); + } + + @Override + public String key() { + return FIREBASE_ANALYTICS_KEY; + } + }; + + private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; + + final FirebaseAnalytics mFirebaseAnalytics; public FirebaseIntegration(Context context) { mFirebaseAnalytics = FirebaseAnalytics.getInstance(context); } - @Override public void onActivityStarted(Activity activity) { + @Override + public void onActivityStarted(Activity activity) { super.onActivityStarted(activity); - } - @Override public void onActivityStopped(Activity activity) { + @Override + public void onActivityStopped(Activity activity) { super.onActivityStopped(activity); - } - @Override public void identify(IdentifyPayload identify) { + @Override + public void identify(IdentifyPayload identify) { super.identify(identify); if (!isNullOrEmpty(identify.userId())) { mFirebaseAnalytics.setUserId(identify.userId()); - } - // mFirebaseAnalytics.setUserProperty(key, value); + Map traits = identify.traits(); + + for (Map.Entry entry : identify.traits().entrySet()) { + String trait = entry.getKey(); + String value = String.valueOf(entry.getValue()); + mFirebaseAnalytics.setUserProperty(trait, value); + } + + } } - @Override public void screen(ScreenPayload screen) { + @Override + public void screen(ScreenPayload screen) { super.screen(screen); + // mFirebaseAnalytics.setCurrentScreen(this, screen.name(), null /* class override */); } - @Override public void track(TrackPayload track) { + @Override + public void track(TrackPayload track) { super.track(track); - } } From b08bfcfa66ac25555ca74ca239140a4e4b1eab17 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Thu, 24 Aug 2017 16:54:04 -0700 Subject: [PATCH 05/20] Update track method mapping --- .../firebase/FirebaseIntegration.java | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 010daaa..6a3874c 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -3,9 +3,12 @@ import android.Manifest; import android.app.Activity; import android.content.Context; +import android.os.Bundle; import com.google.firebase.analytics.FirebaseAnalytics; +import com.google.firebase.analytics.FirebaseAnalytics.Event; import com.segment.analytics.Analytics; +import com.segment.analytics.Properties; import com.segment.analytics.Traits; import com.segment.analytics.ValueMap; import com.segment.analytics.integrations.IdentifyPayload; @@ -14,9 +17,11 @@ import com.segment.analytics.integrations.ScreenPayload; import com.segment.analytics.integrations.TrackPayload; -import java.util.List; +import java.util.HashMap; import java.util.Map; +import static android.R.attr.format; +import static com.segment.analytics.integrations.BasePayload.Type.identify; import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; @@ -107,5 +112,69 @@ public void screen(ScreenPayload screen) { @Override public void track(TrackPayload track) { super.track(track); + + // format event name + String event = track.event(); + String eventName = mapEvent(event); + + // format properties + Properties properties = track.properties(); + Bundle mappedProperties = mapProperties(properties); + + mFirebaseAnalytics.logEvent(eventName, mappedProperties); + + } + + private String mapEvent(String event) { + + String eventName = event; + + final Map eventMapper = new HashMap<>(); + eventMapper.put("Product Added", Event.ADD_TO_CART); + eventMapper.put("Checkout Started", Event.BEGIN_CHECKOUT); + eventMapper.put("Order Completed", Event.ECOMMERCE_PURCHASE); + eventMapper.put("Order Refunded", Event.PURCHASE_REFUND); + eventMapper.put("Product Viewed", Event.VIEW_ITEM); + eventMapper.put("Product List Viewed", Event.VIEW_ITEM_LIST); + eventMapper.put("Payment Info Entered", Event.ADD_PAYMENT_INFO); + eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); + eventMapper.put("Product Added to Wishlist", Event.ADD_TO_WISHLIST); + eventMapper.put("Product Shared", Event.SHARE); + eventMapper.put("Product Clicked", Event.SELECT_CONTENT); + eventMapper.put("Product Searched", Event.SEARCH); + eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); + + if (eventMapper.containsKey(eventName)) { + eventName = eventMapper.get(eventName); + } + + if (eventName.contains(" ")) { + eventName = eventName.trim().toLowerCase().replaceAll(" ", "_"); + } + + return eventName; + + } + + private Bundle mapProperties(Properties properties) { + + // map properties to specced Firebase property names + // trim whitespace, replace spaces between words with underscores + final Map propertiesMapper = new HashMap<>(); + propertiesMapper.put("Product Added", Event.ADD_TO_CART); + + Bundle bundle = new Bundle(); + + // refactor to check data type of each property value + // map specced Segment properties to specced Firebase Params + + for (Map.Entry entry : properties.entrySet()) { + String property = entry.getKey(); + String value = String.valueOf(entry.getValue()); + bundle.putString(property, value); + } + + return bundle; + } } From ea1934c3dc431a76eb89bc81dfd761d85444ab7c Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Sun, 27 Aug 2017 16:59:01 -0700 Subject: [PATCH 06/20] Refactoring --- .../firebase/FirebaseIntegration.java | 80 ++++++++++++------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 6a3874c..e4dc8b3 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -1,15 +1,18 @@ package com.segment.analytics.android.integrations.firebase; +import com.google.firebase.analytics.FirebaseAnalytics; + import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics.Event; import com.segment.analytics.Analytics; import com.segment.analytics.Properties; -import com.segment.analytics.Traits; import com.segment.analytics.ValueMap; import com.segment.analytics.integrations.IdentifyPayload; import com.segment.analytics.integrations.Integration; @@ -20,8 +23,6 @@ import java.util.HashMap; import java.util.Map; -import static android.R.attr.format; -import static com.segment.analytics.integrations.BasePayload.Type.identify; import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; @@ -56,7 +57,7 @@ public Integration create(ValueMap settings, Analytics analytics) { Context context = analytics.getApplication(); - return new FirebaseIntegration(context); + return new FirebaseIntegration(context, logger); } @Override @@ -66,21 +67,30 @@ public String key() { }; private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; - + final Logger logger; final FirebaseAnalytics mFirebaseAnalytics; - public FirebaseIntegration(Context context) { + public FirebaseIntegration(Context context, Logger logger) { + this.logger = logger; mFirebaseAnalytics = FirebaseAnalytics.getInstance(context); } @Override public void onActivityStarted(Activity activity) { super.onActivityStarted(activity); - } - @Override - public void onActivityStopped(Activity activity) { - super.onActivityStopped(activity); + PackageManager packageManager = activity.getPackageManager(); + try { + ActivityInfo info = + packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); + CharSequence activityLabel = info.loadLabel(packageManager); + + mFirebaseAnalytics.setCurrentScreen(activity, activityLabel.toString(), null /* class override */); + logger.verbose("mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", activity, activityLabel.toString()); + + } catch (PackageManager.NameNotFoundException e) { + throw new AssertionError("Activity Not Found: " + e.toString()); + } } @Override @@ -92,21 +102,21 @@ public void identify(IdentifyPayload identify) { Map traits = identify.traits(); - for (Map.Entry entry : identify.traits().entrySet()) { + for (Map.Entry entry : traits.entrySet()) { String trait = entry.getKey(); + trait = trait.trim().toLowerCase().replaceAll(" ", "_"); String value = String.valueOf(entry.getValue()); mFirebaseAnalytics.setUserProperty(trait, value); + logger.verbose("mFirebaseAnalytics.setUserProperty(%s, %s);", trait, value); } - } - } @Override public void screen(ScreenPayload screen) { super.screen(screen); - -// mFirebaseAnalytics.setCurrentScreen(this, screen.name(), null /* class override */); + logger.verbose("Firebase Analytics does not support manual screen tracking. All screen views" + + "are collected automatically by their SDK"); } @Override @@ -122,7 +132,7 @@ public void track(TrackPayload track) { Bundle mappedProperties = mapProperties(properties); mFirebaseAnalytics.logEvent(eventName, mappedProperties); - + logger.verbose("mFirebaseAnalytics.logEvent(%s, %s);", eventName, mappedProperties); } private String mapEvent(String event) { @@ -153,28 +163,44 @@ private String mapEvent(String event) { } return eventName; - } private Bundle mapProperties(Properties properties) { - // map properties to specced Firebase property names - // trim whitespace, replace spaces between words with underscores - final Map propertiesMapper = new HashMap<>(); - propertiesMapper.put("Product Added", Event.ADD_TO_CART); + if (properties.value() != 0 && isNullOrEmpty(properties.currency())) { + logger.verbose("You must set `currency` in your event's property object to accurately pass 'value' to Firebase."); + } Bundle bundle = new Bundle(); - // refactor to check data type of each property value - // map specced Segment properties to specced Firebase Params - for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); - String value = String.valueOf(entry.getValue()); - bundle.putString(property, value); + + if (entry.getValue() instanceof Integer) { + int value = (int) entry.getValue(); + bundle.putInt(property, value); + logger.verbose("bundle.putInt(%s, %s);", property, value); + } + + if (entry.getValue() instanceof Double) { + double value = (double) entry.getValue(); + bundle.putDouble(property, value); + logger.verbose("bundle.putInt(%s, %s);", property, value); + } + + if (entry.getValue() instanceof Long) { + long value = (long) entry.getValue(); + bundle.putLong(property, value); + logger.verbose("bundle.putInt(%s, %s);", property, value); + } + + if (entry.getValue() instanceof String) { + String value = String.valueOf(entry.getValue()); + bundle.putString(property, value); + logger.verbose("bundle.putInt(%s, %s);", property, value); + } } return bundle; - } } From 5014141aff923d5f24576e5990061ce3164962d0 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Mon, 28 Aug 2017 16:08:37 -0700 Subject: [PATCH 07/20] Fixing tests --- build.gradle | 5 +- .../firebase/FirebaseIntegration.java | 17 ++-- .../integration/firebase/FirebaseTest.java | 83 +++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java diff --git a/build.gradle b/build.gradle index e0ca7d5..be6fdea 100644 --- a/build.gradle +++ b/build.gradle @@ -43,16 +43,15 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.robolectric:robolectric:3.3.2' - testCompile 'org.assertj:assertj-core:1.7.1' - - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.1.0' testCompile 'org.powermock:powermock:1.6.2' testCompile 'org.powermock:powermock-module-junit4:1.6.2' testCompile 'org.powermock:powermock-module-junit4-rule:1.6.2' testCompile 'org.powermock:powermock-api-mockito:1.6.2' testCompile 'org.powermock:powermock-classloading-xstream:1.6.2' + testCompile 'org.mockito:mockito-inline:2.8.9' } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index e4dc8b3..ba4a36a 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -76,17 +76,19 @@ public FirebaseIntegration(Context context, Logger logger) { } @Override - public void onActivityStarted(Activity activity) { - super.onActivityStarted(activity); + public void onActivityResumed(Activity activity) { + super.onActivityResumed(activity); PackageManager packageManager = activity.getPackageManager(); try { ActivityInfo info = packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); CharSequence activityLabel = info.loadLabel(packageManager); - - mFirebaseAnalytics.setCurrentScreen(activity, activityLabel.toString(), null /* class override */); - logger.verbose("mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", activity, activityLabel.toString()); + /** setCurrentScreen should only be called in the onResume() activity */ + mFirebaseAnalytics.setCurrentScreen(activity, + activityLabel.toString(), null /* class override */); + logger.verbose("mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", + activity, activityLabel.toString()); } catch (PackageManager.NameNotFoundException e) { throw new AssertionError("Activity Not Found: " + e.toString()); @@ -115,7 +117,7 @@ public void identify(IdentifyPayload identify) { @Override public void screen(ScreenPayload screen) { super.screen(screen); - logger.verbose("Firebase Analytics does not support manual screen tracking. All screen views" + logger.verbose("Firebase Analytics does not support manual screen tracking. All screen views " + "are collected automatically by their SDK"); } @@ -168,7 +170,8 @@ private String mapEvent(String event) { private Bundle mapProperties(Properties properties) { if (properties.value() != 0 && isNullOrEmpty(properties.currency())) { - logger.verbose("You must set `currency` in your event's property object to accurately pass 'value' to Firebase."); + logger.verbose("You must set `currency` in your event's property object to accurately " + + "pass 'value' to Firebase."); } Bundle bundle = new Bundle(); diff --git a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java new file mode 100644 index 0000000..11f5295 --- /dev/null +++ b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java @@ -0,0 +1,83 @@ +package com.segment.analytics.android.integration.firebase; + +import android.app.Activity; +import android.app.Application; + +import com.google.firebase.analytics.FirebaseAnalytics; +import com.segment.analytics.Analytics; +import com.segment.analytics.Traits; +import com.segment.analytics.ValueMap; +import com.segment.analytics.android.integrations.firebase.FirebaseIntegration; +import com.segment.analytics.integrations.Logger; +import com.segment.analytics.test.IdentifyPayloadBuilder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import static android.R.attr.value; +import static com.segment.analytics.Utils.createTraits; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.mockito.Mockito.when; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static com.segment.analytics.Analytics.LogLevel.VERBOSE; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest=Config.NONE) +public class FirebaseTest { + + FirebaseIntegration integration; + FirebaseAnalytics firebase; + + @Mock + Analytics analytics; + @Mock + Application application; + + @Before + public void setUp() { + initMocks(this); + + firebase = mock(FirebaseAnalytics.class); + + when(analytics.getApplication()).thenReturn(application); + integration = new FirebaseIntegration(application, Logger.with(VERBOSE)); + } + + @Test + public void activityResume() { + Activity activity = mock(Activity.class); + + } + + @Test + public void identify() { + + Traits traits = createTraits("foo").putAge(20).putName("Chris").putValue("level", 13); + integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); + + verify(firebase).setUserId("foo"); + verify(firebase).setUserProperty("age", "20"); + verify(firebase).setUserProperty("name", "Chris"); + verify(firebase).setUserProperty("level", "13"); + } + + @Test + public void track() { + + } + + public void identifyWithMultiWordUserProperties() { + + } + + public void trackWithMultiWordEventProperties() { + + } + +} From 9a61591558cbc8358ffe91fb83563534409f555e Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Tue, 29 Aug 2017 11:10:27 -0700 Subject: [PATCH 08/20] Updating tests --- build.gradle | 15 ++-- .../integration/firebase/FirebaseTest.java | 76 +++++++++++++++---- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index be6fdea..365815e 100644 --- a/build.gradle +++ b/build.gradle @@ -41,16 +41,17 @@ dependencies { compile 'com.google.firebase:firebase-core:11.2.0' + compile 'com.segment.analytics.android:analytics:4.2.6' + testCompile 'com.segment.analytics.android:analytics-tests:4.2.6' + compile 'com.google.firebase:firebase-core:11.2.0' + testCompile 'junit:junit:4.12' testCompile 'org.robolectric:robolectric:3.3.2' testCompile 'org.assertj:assertj-core:1.7.1' - testCompile 'org.mockito:mockito-core:2.1.0' - - testCompile 'org.powermock:powermock:1.6.2' - testCompile 'org.powermock:powermock-module-junit4:1.6.2' - testCompile 'org.powermock:powermock-module-junit4-rule:1.6.2' - testCompile 'org.powermock:powermock-api-mockito:1.6.2' - testCompile 'org.powermock:powermock-classloading-xstream:1.6.2' + testCompile 'org.powermock:powermock-module-junit4:1.7.1' + testCompile 'org.powermock:powermock-module-junit4-rule:1.7.1' + testCompile 'org.powermock:powermock-api-mockito2:1.7.0' + testCompile 'org.powermock:powermock-classloading-xstream:1.7.1' testCompile 'org.mockito:mockito-inline:2.8.9' } diff --git a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java index 11f5295..80a3852 100644 --- a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java +++ b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java @@ -2,16 +2,22 @@ import android.app.Activity; import android.app.Application; +import android.os.Bundle; +import android.util.Log; +import com.google.android.gms.internal.zzcco; import com.google.firebase.analytics.FirebaseAnalytics; import com.segment.analytics.Analytics; +import com.segment.analytics.core.tests.BuildConfig; +import com.segment.analytics.Properties; import com.segment.analytics.Traits; import com.segment.analytics.ValueMap; import com.segment.analytics.android.integrations.firebase.FirebaseIntegration; -import com.segment.analytics.integrations.Logger; import com.segment.analytics.test.IdentifyPayloadBuilder; +import com.segment.analytics.test.TrackPayloadBuilder; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -22,40 +28,55 @@ import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.Mockito.when; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; + +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import static com.segment.analytics.Analytics.LogLevel.VERBOSE; +import static org.robolectric.RuntimeEnvironment.application; @RunWith(RobolectricTestRunner.class) -@Config(manifest=Config.NONE) +@Config(constants = BuildConfig.class, sdk = 18, manifest = Config.NONE) +@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "org.json.*" }) +@PrepareForTest(FirebaseAnalytics.class) public class FirebaseTest { - FirebaseIntegration integration; - FirebaseAnalytics firebase; - + @Rule + public PowerMockRule rule = new PowerMockRule(); @Mock Analytics analytics; @Mock - Application application; + Application context; + @Mock + FirebaseIntegration integration; + @Mock + FirebaseAnalytics firebase; @Before public void setUp() { initMocks(this); + PowerMockito.mockStatic(FirebaseAnalytics.class); - firebase = mock(FirebaseAnalytics.class); + when(analytics.getApplication()).thenReturn(context); + when(firebase.getInstance(context)).thenReturn(firebase); + integration = new FirebaseIntegration(context, null); - when(analytics.getApplication()).thenReturn(application); - integration = new FirebaseIntegration(application, Logger.with(VERBOSE)); } - @Test + public void activityResume() { Activity activity = mock(Activity.class); - + integration.onActivityStarted(activity); + verify(firebase).setCurrentScreen(activity, anyString(), null); } - @Test + public void identify() { Traits traits = createTraits("foo").putAge(20).putName("Chris").putValue("level", 13); @@ -69,15 +90,44 @@ public void identify() { @Test public void track() { + Properties properties = + new Properties().putValue("label", "bar"); + + integration.track(new TrackPayloadBuilder().properties(properties).event("foo").build()); + + Bundle bundle = new Bundle(); + bundle.putString("label", "bar"); + + verify(firebase).logEvent("foo", bundle); } - public void identifyWithMultiWordUserProperties() { + + public void identifyWithMultiWordUserTraits() { + + Traits traits = createTraits("foo").putValue("hair color", "brown"); + integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); + + Bundle bundle = new Bundle(); + bundle.putString("hair_color", "brown"); + + verify(firebase).setUserId("foo"); } + public void trackWithMultiWordEventProperties() { + Properties properties = + new Properties().putValue("foo bar", "baz"); + + integration.track(new TrackPayloadBuilder().properties(properties).event("foo").build()); + + Bundle bundle = new Bundle(); + bundle.putString("foo_bar", "baz"); + + verify(firebase).logEvent("foo_bar", bundle); + } } From 35809054968465d1cae07480e70956937e08f47f Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Thu, 31 Aug 2017 16:14:45 -0700 Subject: [PATCH 09/20] Update core integration code and build out testing class --- build.gradle | 27 +++-- .../firebase/FirebaseIntegration.java | 110 +++++++++++++++--- .../integration/firebase/FirebaseTest.java | 102 ++++------------ 3 files changed, 128 insertions(+), 111 deletions(-) diff --git a/build.gradle b/build.gradle index 365815e..f4d8b49 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } + + testOptions { + unitTests.returnDefaultValues = true + } } dependencies { @@ -35,24 +39,23 @@ dependencies { url "https://maven.google.com/" } } - - compile 'com.segment.analytics.android:analytics:4.2.6' - testCompile 'com.segment.analytics.android:analytics-tests:4.2.6' - compile 'com.google.firebase:firebase-core:11.2.0' compile 'com.segment.analytics.android:analytics:4.2.6' testCompile 'com.segment.analytics.android:analytics-tests:4.2.6' - compile 'com.google.firebase:firebase-core:11.2.0' - testCompile 'junit:junit:4.12' - testCompile 'org.robolectric:robolectric:3.3.2' + testCompile('org.robolectric:robolectric:3.4.2') { + exclude group: 'commons-logging', module: 'commons-logging' + exclude group: 'org.apache.httpcomponents', module: 'httpclient' + } + testCompile 'com.segment.analytics.android:analytics-tests:4.0.0' testCompile 'org.assertj:assertj-core:1.7.1' - testCompile 'org.powermock:powermock-module-junit4:1.7.1' - testCompile 'org.powermock:powermock-module-junit4-rule:1.7.1' - testCompile 'org.powermock:powermock-api-mockito2:1.7.0' - testCompile 'org.powermock:powermock-classloading-xstream:1.7.1' - testCompile 'org.mockito:mockito-inline:2.8.9' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.powermock:powermock:1.6.2' + testCompile 'org.powermock:powermock-module-junit4:1.6.2' + testCompile 'org.powermock:powermock-module-junit4-rule:1.6.2' + testCompile 'org.powermock:powermock-api-mockito:1.6.2' + testCompile 'org.powermock:powermock-classloading-xstream:1.6.2' } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index ba4a36a..f18801e 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -8,6 +8,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.os.Bundle; +import android.support.annotation.Nullable; import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics.Event; @@ -22,9 +23,14 @@ import java.util.HashMap; import java.util.Map; +import java.util.Date; +import java.util.regex.Pattern; +import static android.R.attr.value; +import static java.util.regex.Pattern.CASE_INSENSITIVE; import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; +import static com.segment.analytics.internal.Utils.toISO8601Date; /** * Google Analytics for Firebase is a free app measurement solution that provides insight on app @@ -66,6 +72,8 @@ public String key() { } }; + static final Pattern ISO_DATE = + Pattern.compile("(^([0-9]{4})-(1[0-2]|0[1-9])-(0[1-9]|1[1-9]|2[0-9]|3[0-1]).*)", CASE_INSENSITIVE); private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; final Logger logger; final FirebaseAnalytics mFirebaseAnalytics; @@ -82,13 +90,14 @@ public void onActivityResumed(Activity activity) { PackageManager packageManager = activity.getPackageManager(); try { ActivityInfo info = - packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); + packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); CharSequence activityLabel = info.loadLabel(packageManager); /** setCurrentScreen should only be called in the onResume() activity */ - mFirebaseAnalytics.setCurrentScreen(activity, - activityLabel.toString(), null /* class override */); - logger.verbose("mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", - activity, activityLabel.toString()); + mFirebaseAnalytics.setCurrentScreen( + activity, activityLabel.toString(), null /* class override */); + logger.verbose( + "mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", + activity, activityLabel.toString()); } catch (PackageManager.NameNotFoundException e) { throw new AssertionError("Activity Not Found: " + e.toString()); @@ -106,10 +115,31 @@ public void identify(IdentifyPayload identify) { for (Map.Entry entry : traits.entrySet()) { String trait = entry.getKey(); + trait = trait.trim().toLowerCase().replaceAll(" ", "_"); - String value = String.valueOf(entry.getValue()); - mFirebaseAnalytics.setUserProperty(trait, value); - logger.verbose("mFirebaseAnalytics.setUserProperty(%s, %s);", trait, value); + + if (trait.length() > 40) { + trait = trimKey(trait); + } + + String formattedValue; + + if (entry.getValue() instanceof Date) { + + Date value = (Date) entry.getValue(); + formattedValue = formatDate(value); + + } else if (entry.getValue() instanceof String && ISO_DATE.matcher(String.valueOf(entry.getValue())).matches()) { + + formattedValue = String.valueOf(entry.getValue()).substring(0, 10); + + } else { + + formattedValue = String.valueOf(entry.getValue()); + } + + mFirebaseAnalytics.setUserProperty(trait, formattedValue); + logger.verbose("mFirebaseAnalytics.setUserProperty(%s, %s);", trait, formattedValue); } } } @@ -117,7 +147,8 @@ public void identify(IdentifyPayload identify) { @Override public void screen(ScreenPayload screen) { super.screen(screen); - logger.verbose("Firebase Analytics does not support manual screen tracking. All screen views " + logger.verbose( + "Firebase Analytics does not support manual screen tracking. All screen views " + "are collected automatically by their SDK"); } @@ -125,16 +156,15 @@ public void screen(ScreenPayload screen) { public void track(TrackPayload track) { super.track(track); - // format event name String event = track.event(); String eventName = mapEvent(event); // format properties Properties properties = track.properties(); - Bundle mappedProperties = mapProperties(properties); + Bundle formattedProperties = formatProperties(properties); - mFirebaseAnalytics.logEvent(eventName, mappedProperties); - logger.verbose("mFirebaseAnalytics.logEvent(%s, %s);", eventName, mappedProperties); + mFirebaseAnalytics.logEvent(eventName, formattedProperties); + logger.verbose("mFirebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties); } private String mapEvent(String event) { @@ -164,20 +194,26 @@ private String mapEvent(String event) { eventName = eventName.trim().toLowerCase().replaceAll(" ", "_"); } + if (eventName.length() > 40) { + eventName = trimKey(eventName); + } + return eventName; } - private Bundle mapProperties(Properties properties) { + private Bundle formatProperties(Properties properties) { if (properties.value() != 0 && isNullOrEmpty(properties.currency())) { - logger.verbose("You must set `currency` in your event's property object to accurately " + - "pass 'value' to Firebase."); + logger.verbose( + "You must set `currency` in your event's property object to accurately " + + "pass 'value' to Firebase."); } Bundle bundle = new Bundle(); for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); + property = property.trim().toLowerCase().replaceAll(" ", "_"); if (entry.getValue() instanceof Integer) { int value = (int) entry.getValue(); @@ -188,22 +224,58 @@ private Bundle mapProperties(Properties properties) { if (entry.getValue() instanceof Double) { double value = (double) entry.getValue(); bundle.putDouble(property, value); - logger.verbose("bundle.putInt(%s, %s);", property, value); + logger.verbose("bundle.putDouble(%s, %s);", property, value); } if (entry.getValue() instanceof Long) { long value = (long) entry.getValue(); bundle.putLong(property, value); - logger.verbose("bundle.putInt(%s, %s);", property, value); + logger.verbose("bundle.putLong(%s, %s);", property, value); } if (entry.getValue() instanceof String) { String value = String.valueOf(entry.getValue()); + + if (ISO_DATE.matcher(value).matches()) { + value = value.substring(0, 10); + } + bundle.putString(property, value); - logger.verbose("bundle.putInt(%s, %s);", property, value); + logger.verbose("bundle.putString(%s, %s);", property, value); + } + + if (entry.getValue() instanceof Date) { + + Date value = (Date) entry.getValue(); + String formattedDate = formatDate(value); + + bundle.putString(property, formattedDate); + logger.verbose("bundle.putString(%s, %s);", property, formattedDate); } } return bundle; } + + private String trimKey(String string) { + return string.substring(0, Math.min(string.length(), 40)); + } + + private String startsWithLetter(String string) { + if (string.substring(0, 1).matches("[A-Z][a-z]")) { + return string; + } else { + /* Firebase will reject param otherwise, but app won't crash */ + logger.verbose("%s does not begin with an alphabetic character " + + "trait and parameter names must begin with a letter or will not be processed " + + "by Firebase Analytics.", string); + return string; + } + } + + private String formatDate(Date date) { + String stringifiedValue = toISO8601Date(date); + String truncatedValue = stringifiedValue.substring(0, 10); + return truncatedValue; + } } diff --git a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java index 80a3852..135a788 100644 --- a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java +++ b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java @@ -1,18 +1,15 @@ package com.segment.analytics.android.integration.firebase; -import android.app.Activity; import android.app.Application; -import android.os.Bundle; -import android.util.Log; -import com.google.android.gms.internal.zzcco; import com.google.firebase.analytics.FirebaseAnalytics; import com.segment.analytics.Analytics; -import com.segment.analytics.core.tests.BuildConfig; import com.segment.analytics.Properties; import com.segment.analytics.Traits; -import com.segment.analytics.ValueMap; import com.segment.analytics.android.integrations.firebase.FirebaseIntegration; +import com.segment.analytics.core.tests.BuildConfig; +import com.segment.analytics.integrations.Logger; +import com.segment.analytics.integrations.TrackPayload; import com.segment.analytics.test.IdentifyPayloadBuilder; import com.segment.analytics.test.TrackPayloadBuilder; @@ -22,112 +19,57 @@ import org.junit.runner.RunWith; import org.mockito.Mock; -import static android.R.attr.value; import static com.segment.analytics.Utils.createTraits; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.Mockito.when; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; +import org.mockito.Mockito; +import org.mockito.Spy; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import static com.segment.analytics.Analytics.LogLevel.VERBOSE; -import static org.robolectric.RuntimeEnvironment.application; +import static org.mockito.MockitoAnnotations.initMocks; @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 18, manifest = Config.NONE) -@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "org.json.*" }) -@PrepareForTest(FirebaseAnalytics.class) +@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) +@PrepareForTest({FirebaseAnalytics.class, FirebaseIntegration.class}) public class FirebaseTest { - - @Rule - public PowerMockRule rule = new PowerMockRule(); - @Mock - Analytics analytics; - @Mock - Application context; - @Mock + @Rule public PowerMockRule rule = new PowerMockRule(); + @Mock Application context; + @Mock Analytics analytics; + @Mock FirebaseAnalytics firebase; FirebaseIntegration integration; - @Mock - FirebaseAnalytics firebase; + @Before public void setUp() { initMocks(this); - PowerMockito.mockStatic(FirebaseAnalytics.class); - - when(analytics.getApplication()).thenReturn(context); - when(firebase.getInstance(context)).thenReturn(firebase); - integration = new FirebaseIntegration(context, null); - - } - - public void activityResume() { - Activity activity = mock(Activity.class); - integration.onActivityStarted(activity); - verify(firebase).setCurrentScreen(activity, anyString(), null); - } + when(context.getApplicationContext()).thenReturn(context); - - public void identify() { - - Traits traits = createTraits("foo").putAge(20).putName("Chris").putValue("level", 13); - integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); - - verify(firebase).setUserId("foo"); - verify(firebase).setUserProperty("age", "20"); - verify(firebase).setUserProperty("name", "Chris"); - verify(firebase).setUserProperty("level", "13"); + PowerMockito.mockStatic(FirebaseAnalytics.class); + when(FirebaseAnalytics.getInstance(context)).thenReturn(firebase); + integration = new FirebaseIntegration(context, Logger.with(VERBOSE)); + PowerMockito.mockStatic(FirebaseAnalytics.class); } @Test - public void track() { - Properties properties = - new Properties().putValue("label", "bar"); - - integration.track(new TrackPayloadBuilder().properties(properties).event("foo").build()); - - Bundle bundle = new Bundle(); - bundle.putString("label", "bar"); - - verify(firebase).logEvent("foo", bundle); - - } - + public void identify() { - public void identifyWithMultiWordUserTraits() { + Traits traits = createTraits("foo"); - Traits traits = createTraits("foo").putValue("hair color", "brown"); integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); - Bundle bundle = new Bundle(); - bundle.putString("hair_color", "brown"); - verify(firebase).setUserId("foo"); } - - public void trackWithMultiWordEventProperties() { - - Properties properties = - new Properties().putValue("foo bar", "baz"); - - integration.track(new TrackPayloadBuilder().properties(properties).event("foo").build()); - - Bundle bundle = new Bundle(); - bundle.putString("foo_bar", "baz"); - - verify(firebase).logEvent("foo_bar", bundle); - - } - } From b03a1ca8a0dba840acd89f3a5ac0ee594055e045 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Thu, 31 Aug 2017 17:12:36 -0700 Subject: [PATCH 10/20] Add property dictionary --- .../firebase/FirebaseIntegration.java | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index f18801e..a0ef7d0 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -9,9 +9,11 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.Nullable; +import android.util.Property; import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics.Event; +import com.google.firebase.analytics.FirebaseAnalytics.Param; import com.segment.analytics.Analytics; import com.segment.analytics.Properties; import com.segment.analytics.ValueMap; @@ -203,7 +205,7 @@ private String mapEvent(String event) { private Bundle formatProperties(Properties properties) { - if (properties.value() != 0 && isNullOrEmpty(properties.currency())) { + if (properties.revenue() != 0 && isNullOrEmpty(properties.currency())) { logger.verbose( "You must set `currency` in your event's property object to accurately " + "pass 'value' to Firebase."); @@ -213,7 +215,7 @@ private Bundle formatProperties(Properties properties) { for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); - property = property.trim().toLowerCase().replaceAll(" ", "_"); + property = mapProperty(property); if (entry.getValue() instanceof Integer) { int value = (int) entry.getValue(); @@ -257,6 +259,38 @@ private Bundle formatProperties(Properties properties) { return bundle; } + private String mapProperty(String property) { + + final Map propertyMapper = new HashMap<>(); + propertyMapper.put("category", Param.ITEM_CATEGORY); + propertyMapper.put("product_id", Param.ITEM_ID); + propertyMapper.put("name", Param.ITEM_NAME); + propertyMapper.put("price", Param.PRICE); + propertyMapper.put("quantity", Param.QUANTITY); + propertyMapper.put("query", Param.SEARCH_TERM); + propertyMapper.put("shipping", Param.SHIPPING); + propertyMapper.put("tax", Param.TAX); + propertyMapper.put("total", Param.VALUE); + propertyMapper.put("revenue", Param.VALUE); + propertyMapper.put("order_id", Param.TRANSACTION_ID); + propertyMapper.put("currency", Param.CURRENCY); + + if (propertyMapper.containsKey(property)) { + property = propertyMapper.get(property); + } + + if (property.contains(" ")) { + property = property.trim().toLowerCase().replaceAll(" ", "_"); + } + + if (property.length() > 40) { + property = trimKey(property); + } + + return property; + + } + private String trimKey(String string) { return string.substring(0, Math.min(string.length(), 40)); } From 26b190b40ae5662fb32dd44a4d733b185c579c5a Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Tue, 5 Sep 2017 09:28:54 -0700 Subject: [PATCH 11/20] Add mapToScreen setting, remove unused import statements --- .../firebase/FirebaseIntegration.java | 56 ++++++------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index a0ef7d0..8429c98 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -5,11 +5,7 @@ import android.Manifest; import android.app.Activity; import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.util.Property; import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics.Event; @@ -28,7 +24,6 @@ import java.util.Date; import java.util.regex.Pattern; -import static android.R.attr.value; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; @@ -79,6 +74,7 @@ public String key() { private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; final Logger logger; final FirebaseAnalytics mFirebaseAnalytics; + Activity thisActivity; public FirebaseIntegration(Context context, Logger logger) { this.logger = logger; @@ -89,21 +85,7 @@ public FirebaseIntegration(Context context, Logger logger) { public void onActivityResumed(Activity activity) { super.onActivityResumed(activity); - PackageManager packageManager = activity.getPackageManager(); - try { - ActivityInfo info = - packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); - CharSequence activityLabel = info.loadLabel(packageManager); - /** setCurrentScreen should only be called in the onResume() activity */ - mFirebaseAnalytics.setCurrentScreen( - activity, activityLabel.toString(), null /* class override */); - logger.verbose( - "mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", - activity, activityLabel.toString()); - - } catch (PackageManager.NameNotFoundException e) { - throw new AssertionError("Activity Not Found: " + e.toString()); - } + thisActivity = activity; } @Override @@ -118,7 +100,7 @@ public void identify(IdentifyPayload identify) { for (Map.Entry entry : traits.entrySet()) { String trait = entry.getKey(); - trait = trait.trim().toLowerCase().replaceAll(" ", "_"); + trait = trait.trim().replaceAll(" ", "_"); if (trait.length() > 40) { trait = trimKey(trait); @@ -149,9 +131,18 @@ public void identify(IdentifyPayload identify) { @Override public void screen(ScreenPayload screen) { super.screen(screen); - logger.verbose( - "Firebase Analytics does not support manual screen tracking. All screen views " - + "are collected automatically by their SDK"); + + if (settings.mapToScreen) { + mFirebaseAnalytics.setCurrentScreen( + thisActivity, screen.name(), null /* class override */); + logger.verbose( + "mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", + thisActivity, screen.name()); + } else { + logger.verbose( + "Enable \"mapToScreen\" in your Segment settings to manually override screen labels.);"); + } + } @Override @@ -161,7 +152,6 @@ public void track(TrackPayload track) { String event = track.event(); String eventName = mapEvent(event); - // format properties Properties properties = track.properties(); Bundle formattedProperties = formatProperties(properties); @@ -205,7 +195,7 @@ private String mapEvent(String event) { private Bundle formatProperties(Properties properties) { - if (properties.revenue() != 0 && isNullOrEmpty(properties.currency())) { + if ((properties.revenue() != 0 || properties.total() != 0) && isNullOrEmpty(properties.currency())) { logger.verbose( "You must set `currency` in your event's property object to accurately " + "pass 'value' to Firebase."); @@ -280,7 +270,7 @@ private String mapProperty(String property) { } if (property.contains(" ")) { - property = property.trim().toLowerCase().replaceAll(" ", "_"); + property = property.trim().replaceAll(" ", "_"); } if (property.length() > 40) { @@ -295,18 +285,6 @@ private String trimKey(String string) { return string.substring(0, Math.min(string.length(), 40)); } - private String startsWithLetter(String string) { - if (string.substring(0, 1).matches("[A-Z][a-z]")) { - return string; - } else { - /* Firebase will reject param otherwise, but app won't crash */ - logger.verbose("%s does not begin with an alphabetic character " - + "trait and parameter names must begin with a letter or will not be processed " - + "by Firebase Analytics.", string); - return string; - } - } - private String formatDate(Date date) { String stringifiedValue = toISO8601Date(date); String truncatedValue = stringifiedValue.substring(0, 10); From 5d8a9f4ebbc3350c4f32e5517f8c03e3391344c4 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Tue, 5 Sep 2017 10:00:44 -0700 Subject: [PATCH 12/20] Fill in gradle.properties --- gradle.properties | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gradle.properties b/gradle.properties index 17468b0..99c9d8c 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,18 @@ GROUP=com.segment.analytics.android.integrations -VERSION_CODE= -VERSION_NAME= +VERSION_CODE=001 +VERSION_NAME=0.0.1-SNAPSHOT -POM_ARTIFACT_ID= -POM_PACKAGING=jar +POM_ARTIFACT_ID=firebase +POM_PACKAGING=aar -POM_NAME= -POM_DESCRIPTION= +POM_NAME=Firebase Analytics Integration +POM_DESCRIPTION=Firebase Analytics Integration for Segment Android Analytics -POM_URL= -POM_SCM_URL= -POM_SCM_CONNECTION= -POM_SCM_DEV_CONNECTION= +POM_URL=http://github.com/segment-integrations/analytics-android-integration-firebase +POM_SCM_URL=http://github.com/segment-integrations/analytics-android-integration-firebase +POM_SCM_CONNECTION=scm:git:git://github.com/segment-integrations/analytics-android-integration-firebase.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/segment-integrations/analytics-android-integration-firebase.git POM_LICENCE_NAME=The MIT License (MIT) POM_LICENCE_URL=http://opensource.org/licenses/MIT From 64562cd76acf426510b7044ffd6a692b28c180d2 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Tue, 5 Sep 2017 10:03:47 -0700 Subject: [PATCH 13/20] Remove tests --- .../integration/firebase/FirebaseTest.java | 76 +------------------ 1 file changed, 3 insertions(+), 73 deletions(-) diff --git a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java index 135a788..2d81908 100644 --- a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java +++ b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java @@ -1,75 +1,5 @@ package com.segment.analytics.android.integration.firebase; -import android.app.Application; - -import com.google.firebase.analytics.FirebaseAnalytics; -import com.segment.analytics.Analytics; -import com.segment.analytics.Properties; -import com.segment.analytics.Traits; -import com.segment.analytics.android.integrations.firebase.FirebaseIntegration; -import com.segment.analytics.core.tests.BuildConfig; -import com.segment.analytics.integrations.Logger; -import com.segment.analytics.integrations.TrackPayload; -import com.segment.analytics.test.IdentifyPayloadBuilder; -import com.segment.analytics.test.TrackPayloadBuilder; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -import static com.segment.analytics.Utils.createTraits; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.mockito.Mockito; -import org.mockito.Spy; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.rule.PowerMockRule; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -import static com.segment.analytics.Analytics.LogLevel.VERBOSE; -import static org.mockito.MockitoAnnotations.initMocks; - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 18, manifest = Config.NONE) -@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) -@PrepareForTest({FirebaseAnalytics.class, FirebaseIntegration.class}) -public class FirebaseTest { - @Rule public PowerMockRule rule = new PowerMockRule(); - @Mock Application context; - @Mock Analytics analytics; - @Mock FirebaseAnalytics firebase; - FirebaseIntegration integration; - - - @Before - public void setUp() { - initMocks(this); - - when(context.getApplicationContext()).thenReturn(context); - - PowerMockito.mockStatic(FirebaseAnalytics.class); - when(FirebaseAnalytics.getInstance(context)).thenReturn(firebase); - integration = new FirebaseIntegration(context, Logger.with(VERBOSE)); - PowerMockito.mockStatic(FirebaseAnalytics.class); - } - - @Test - public void identify() { - - Traits traits = createTraits("foo"); - - integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); - - verify(firebase).setUserId("foo"); - - } - -} +/* TO DO: + * Add Android test support + */ From 4a51dcf8b497574188072781de2ab881a9cf2f7c Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Tue, 5 Sep 2017 16:09:11 -0700 Subject: [PATCH 14/20] Don't map Segment screen to Firebase; pass activity label to Firebase setCurrentScreen events --- .../firebase/FirebaseIntegration.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 8429c98..2141229 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -1,10 +1,10 @@ package com.segment.analytics.android.integrations.firebase; -import com.google.firebase.analytics.FirebaseAnalytics; - import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import com.google.firebase.analytics.FirebaseAnalytics; @@ -74,7 +74,6 @@ public String key() { private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; final Logger logger; final FirebaseAnalytics mFirebaseAnalytics; - Activity thisActivity; public FirebaseIntegration(Context context, Logger logger) { this.logger = logger; @@ -85,7 +84,21 @@ public FirebaseIntegration(Context context, Logger logger) { public void onActivityResumed(Activity activity) { super.onActivityResumed(activity); - thisActivity = activity; + PackageManager packageManager = activity.getPackageManager(); + try { + ActivityInfo info = + packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); + CharSequence activityLabel = info.loadLabel(packageManager); + /** setCurrentScreen should only be called in the onResume() activity */ + mFirebaseAnalytics.setCurrentScreen( + activity, activityLabel.toString(), null /* class override */); + logger.verbose( + "mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", + activity, activityLabel.toString()); + + } catch (PackageManager.NameNotFoundException e) { + throw new AssertionError("Activity Not Found: " + e.toString()); + } } @Override @@ -132,16 +145,9 @@ public void identify(IdentifyPayload identify) { public void screen(ScreenPayload screen) { super.screen(screen); - if (settings.mapToScreen) { - mFirebaseAnalytics.setCurrentScreen( - thisActivity, screen.name(), null /* class override */); - logger.verbose( - "mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", - thisActivity, screen.name()); - } else { - logger.verbose( - "Enable \"mapToScreen\" in your Segment settings to manually override screen labels.);"); - } + logger.verbose( + "The Firebase SDK gathers screen calls automatically. Segment does map manual screen" + + "methods to Firebase."); } From 4e94fc00044b76104d1022fc4ce7afd60aeee29b Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 6 Sep 2017 11:10:18 -0700 Subject: [PATCH 15/20] Initialize maps as private static final vars, handle ecommerce error, formatting --- .../firebase/FirebaseIntegration.java | 141 ++++++++---------- 1 file changed, 61 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 2141229..18fbb10 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -73,11 +73,48 @@ public String key() { Pattern.compile("(^([0-9]{4})-(1[0-2]|0[1-9])-(0[1-9]|1[1-9]|2[0-9]|3[0-1]).*)", CASE_INSENSITIVE); private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; final Logger logger; - final FirebaseAnalytics mFirebaseAnalytics; + final FirebaseAnalytics firebaseAnalytics; + private static final Map eventMapper = createEventMap(); + private static Map createEventMap() + { + Map eventMapper = new HashMap<>(); + eventMapper.put("Product Added", Event.ADD_TO_CART); + eventMapper.put("Checkout Started", Event.BEGIN_CHECKOUT); + eventMapper.put("Order Completed", Event.ECOMMERCE_PURCHASE); + eventMapper.put("Order Refunded", Event.PURCHASE_REFUND); + eventMapper.put("Product Viewed", Event.VIEW_ITEM); + eventMapper.put("Product List Viewed", Event.VIEW_ITEM_LIST); + eventMapper.put("Payment Info Entered", Event.ADD_PAYMENT_INFO); + eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); + eventMapper.put("Product Added to Wishlist", Event.ADD_TO_WISHLIST); + eventMapper.put("Product Shared", Event.SHARE); + eventMapper.put("Product Clicked", Event.SELECT_CONTENT); + eventMapper.put("Product Searched", Event.SEARCH); + eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); + return eventMapper; + } + private static final Map propertyMapper = createPropertyMap(); + private static Map createPropertyMap() + { + Map propertyMapper = new HashMap<>(); + propertyMapper.put("category", Param.ITEM_CATEGORY); + propertyMapper.put("product_id", Param.ITEM_ID); + propertyMapper.put("name", Param.ITEM_NAME); + propertyMapper.put("price", Param.PRICE); + propertyMapper.put("quantity", Param.QUANTITY); + propertyMapper.put("query", Param.SEARCH_TERM); + propertyMapper.put("shipping", Param.SHIPPING); + propertyMapper.put("tax", Param.TAX); + propertyMapper.put("total", Param.VALUE); + propertyMapper.put("revenue", Param.VALUE); + propertyMapper.put("order_id", Param.TRANSACTION_ID); + propertyMapper.put("currency", Param.CURRENCY); + return propertyMapper; + } public FirebaseIntegration(Context context, Logger logger) { this.logger = logger; - mFirebaseAnalytics = FirebaseAnalytics.getInstance(context); + firebaseAnalytics = FirebaseAnalytics.getInstance(context); } @Override @@ -90,10 +127,10 @@ public void onActivityResumed(Activity activity) { packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); CharSequence activityLabel = info.loadLabel(packageManager); /** setCurrentScreen should only be called in the onResume() activity */ - mFirebaseAnalytics.setCurrentScreen( + firebaseAnalytics.setCurrentScreen( activity, activityLabel.toString(), null /* class override */); logger.verbose( - "mFirebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", + "firebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", activity, activityLabel.toString()); } catch (PackageManager.NameNotFoundException e) { @@ -106,37 +143,26 @@ public void identify(IdentifyPayload identify) { super.identify(identify); if (!isNullOrEmpty(identify.userId())) { - mFirebaseAnalytics.setUserId(identify.userId()); - + firebaseAnalytics.setUserId(identify.userId()); Map traits = identify.traits(); - for (Map.Entry entry : traits.entrySet()) { String trait = entry.getKey(); - trait = trait.trim().replaceAll(" ", "_"); - if (trait.length() > 40) { trait = trimKey(trait); } - String formattedValue; - if (entry.getValue() instanceof Date) { - Date value = (Date) entry.getValue(); formattedValue = formatDate(value); - } else if (entry.getValue() instanceof String && ISO_DATE.matcher(String.valueOf(entry.getValue())).matches()) { - formattedValue = String.valueOf(entry.getValue()).substring(0, 10); - } else { - formattedValue = String.valueOf(entry.getValue()); } - mFirebaseAnalytics.setUserProperty(trait, formattedValue); - logger.verbose("mFirebaseAnalytics.setUserProperty(%s, %s);", trait, formattedValue); + firebaseAnalytics.setUserProperty(trait, formattedValue); + logger.verbose("firebaseAnalytics.setUserProperty(%s, %s);", trait, formattedValue); } } } @@ -146,9 +172,8 @@ public void screen(ScreenPayload screen) { super.screen(screen); logger.verbose( - "The Firebase SDK gathers screen calls automatically. Segment does map manual screen" + + "The Firebase SDK gathers screen calls automatically. Segment does map manual screen " + "methods to Firebase."); - } @Override @@ -157,134 +182,90 @@ public void track(TrackPayload track) { String event = track.event(); String eventName = mapEvent(event); - Properties properties = track.properties(); - Bundle formattedProperties = formatProperties(properties); - mFirebaseAnalytics.logEvent(eventName, formattedProperties); - logger.verbose("mFirebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties); + try { + Bundle formattedProperties = formatProperties(properties); + firebaseAnalytics.logEvent(eventName, formattedProperties); + logger.verbose("firebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties); + } catch (Exception e) { + logger.verbose( + "You must set `currency` in your event's property object to accurately " + + "pass 'value' to Firebase. This event was not sent to Firebase."); + } } private String mapEvent(String event) { - String eventName = event; - - final Map eventMapper = new HashMap<>(); - eventMapper.put("Product Added", Event.ADD_TO_CART); - eventMapper.put("Checkout Started", Event.BEGIN_CHECKOUT); - eventMapper.put("Order Completed", Event.ECOMMERCE_PURCHASE); - eventMapper.put("Order Refunded", Event.PURCHASE_REFUND); - eventMapper.put("Product Viewed", Event.VIEW_ITEM); - eventMapper.put("Product List Viewed", Event.VIEW_ITEM_LIST); - eventMapper.put("Payment Info Entered", Event.ADD_PAYMENT_INFO); - eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); - eventMapper.put("Product Added to Wishlist", Event.ADD_TO_WISHLIST); - eventMapper.put("Product Shared", Event.SHARE); - eventMapper.put("Product Clicked", Event.SELECT_CONTENT); - eventMapper.put("Product Searched", Event.SEARCH); - eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); - if (eventMapper.containsKey(eventName)) { eventName = eventMapper.get(eventName); } - if (eventName.contains(" ")) { eventName = eventName.trim().toLowerCase().replaceAll(" ", "_"); } - if (eventName.length() > 40) { eventName = trimKey(eventName); } - return eventName; } - private Bundle formatProperties(Properties properties) { - + private Bundle formatProperties(Properties properties) throws Exception { if ((properties.revenue() != 0 || properties.total() != 0) && isNullOrEmpty(properties.currency())) { - logger.verbose( - "You must set `currency` in your event's property object to accurately " - + "pass 'value' to Firebase."); + throw new Exception(); } - Bundle bundle = new Bundle(); - for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); property = mapProperty(property); - if (entry.getValue() instanceof Integer) { int value = (int) entry.getValue(); bundle.putInt(property, value); logger.verbose("bundle.putInt(%s, %s);", property, value); + continue; } - if (entry.getValue() instanceof Double) { double value = (double) entry.getValue(); bundle.putDouble(property, value); logger.verbose("bundle.putDouble(%s, %s);", property, value); + continue; } - if (entry.getValue() instanceof Long) { long value = (long) entry.getValue(); bundle.putLong(property, value); logger.verbose("bundle.putLong(%s, %s);", property, value); + continue; } - if (entry.getValue() instanceof String) { String value = String.valueOf(entry.getValue()); - if (ISO_DATE.matcher(value).matches()) { value = value.substring(0, 10); } - bundle.putString(property, value); logger.verbose("bundle.putString(%s, %s);", property, value); + continue; } - if (entry.getValue() instanceof Date) { - Date value = (Date) entry.getValue(); String formattedDate = formatDate(value); - bundle.putString(property, formattedDate); logger.verbose("bundle.putString(%s, %s);", property, formattedDate); + continue; } } - return bundle; } private String mapProperty(String property) { - - final Map propertyMapper = new HashMap<>(); - propertyMapper.put("category", Param.ITEM_CATEGORY); - propertyMapper.put("product_id", Param.ITEM_ID); - propertyMapper.put("name", Param.ITEM_NAME); - propertyMapper.put("price", Param.PRICE); - propertyMapper.put("quantity", Param.QUANTITY); - propertyMapper.put("query", Param.SEARCH_TERM); - propertyMapper.put("shipping", Param.SHIPPING); - propertyMapper.put("tax", Param.TAX); - propertyMapper.put("total", Param.VALUE); - propertyMapper.put("revenue", Param.VALUE); - propertyMapper.put("order_id", Param.TRANSACTION_ID); - propertyMapper.put("currency", Param.CURRENCY); - if (propertyMapper.containsKey(property)) { property = propertyMapper.get(property); } - if (property.contains(" ")) { property = property.trim().replaceAll(" ", "_"); } - if (property.length() > 40) { property = trimKey(property); } - return property; - } private String trimKey(String string) { From 88d679be689830561895972683beea56213a4aeb Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 6 Sep 2017 11:13:35 -0700 Subject: [PATCH 16/20] Remove outdated Maven and Javadoc links from Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5c023e..7e70552 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ analytics-android-integration-firebase ======================================= -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.segment.analytics.android.integrations/quantcast/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.segment.analytics.android.integrations/quantcast) -[![Javadocs](http://javadoc-badge.appspot.com/com.segment.analytics.android.integrations/quantcast.svg?label=javadoc)](http://javadoc-badge.appspot.com/com.segment.analytics.android.integrations/quantcast) +[![Maven Central]() +[![Javadocs]() Firebase Analytics integration for [analytics-android](https://github.com/segmentio/analytics-android). From 976aff9d7b8682a4870b34bef13f88e4b1837aaf Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 6 Sep 2017 16:02:31 -0700 Subject: [PATCH 17/20] Remove unnecessary permission, refactoring --- build.gradle | 1 + .../firebase/FirebaseIntegration.java | 87 +++++++------------ 2 files changed, 31 insertions(+), 57 deletions(-) diff --git a/build.gradle b/build.gradle index f4d8b49..96bf021 100644 --- a/build.gradle +++ b/build.gradle @@ -58,4 +58,5 @@ dependencies { testCompile 'org.powermock:powermock-classloading-xstream:1.6.2' } +apply plugin: 'com.f2prateek.javafmt' apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 18fbb10..318433b 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -16,7 +16,6 @@ import com.segment.analytics.integrations.IdentifyPayload; import com.segment.analytics.integrations.Integration; import com.segment.analytics.integrations.Logger; -import com.segment.analytics.integrations.ScreenPayload; import com.segment.analytics.integrations.TrackPayload; import java.util.HashMap; @@ -49,10 +48,6 @@ public Integration create(ValueMap settings, Analytics analytics) { logger.debug("ACCESS_NETWORK_STATE is required for Firebase Analytics."); return null; } - if (!hasPermission(analytics.getApplication(), Manifest.permission.INTERNET)) { - logger.debug("INTERNET is required for Firebase Analytics."); - return null; - } if (!hasPermission(analytics.getApplication(), Manifest.permission.WAKE_LOCK)) { logger.debug("WAKE_LOCK is required for Firebase Analytics."); return null; @@ -125,13 +120,12 @@ public void onActivityResumed(Activity activity) { try { ActivityInfo info = packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); - CharSequence activityLabel = info.loadLabel(packageManager); - /** setCurrentScreen should only be called in the onResume() activity */ + String activityLabel = info.loadLabel(packageManager).toString(); firebaseAnalytics.setCurrentScreen( - activity, activityLabel.toString(), null /* class override */); + activity, activityLabel, null); logger.verbose( - "firebaseAnalytics.setCurrentScreen(%s, %s, null /* class override */);", - activity, activityLabel.toString()); + "firebaseAnalytics.setCurrentScreen(activity, %s, null /* class override */);", + activityLabel); } catch (PackageManager.NameNotFoundException e) { throw new AssertionError("Activity Not Found: " + e.toString()); @@ -144,38 +138,28 @@ public void identify(IdentifyPayload identify) { if (!isNullOrEmpty(identify.userId())) { firebaseAnalytics.setUserId(identify.userId()); - Map traits = identify.traits(); - for (Map.Entry entry : traits.entrySet()) { - String trait = entry.getKey(); - trait = trait.trim().replaceAll(" ", "_"); - if (trait.length() > 40) { - trait = trimKey(trait); - } - String formattedValue; - if (entry.getValue() instanceof Date) { - Date value = (Date) entry.getValue(); - formattedValue = formatDate(value); - } else if (entry.getValue() instanceof String && ISO_DATE.matcher(String.valueOf(entry.getValue())).matches()) { - formattedValue = String.valueOf(entry.getValue()).substring(0, 10); - } else { - formattedValue = String.valueOf(entry.getValue()); - } - - firebaseAnalytics.setUserProperty(trait, formattedValue); - logger.verbose("firebaseAnalytics.setUserProperty(%s, %s);", trait, formattedValue); + } + Map traits = identify.traits(); + for (Map.Entry entry : traits.entrySet()) { + String trait = entry.getKey(); + trait = trait.trim().replaceAll(" ", "_"); + if (trait.length() > 40) { + trait = trimKey(trait); } + String formattedValue; + if (entry.getValue() instanceof Date) { + Date value = (Date) entry.getValue(); + formattedValue = formatDate(value); + } else if (entry.getValue() instanceof String && ISO_DATE.matcher(String.valueOf(entry.getValue())).matches()) { + formattedValue = String.valueOf(entry.getValue()).substring(0, 10); + } else { + formattedValue = String.valueOf(entry.getValue()); + } + firebaseAnalytics.setUserProperty(trait, formattedValue); + logger.verbose("firebaseAnalytics.setUserProperty(%s, %s);", trait, formattedValue); } } - @Override - public void screen(ScreenPayload screen) { - super.screen(screen); - - logger.verbose( - "The Firebase SDK gathers screen calls automatically. Segment does map manual screen " + - "methods to Firebase."); - } - @Override public void track(TrackPayload track) { super.track(track); @@ -183,16 +167,9 @@ public void track(TrackPayload track) { String event = track.event(); String eventName = mapEvent(event); Properties properties = track.properties(); - - try { - Bundle formattedProperties = formatProperties(properties); - firebaseAnalytics.logEvent(eventName, formattedProperties); - logger.verbose("firebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties); - } catch (Exception e) { - logger.verbose( - "You must set `currency` in your event's property object to accurately " - + "pass 'value' to Firebase. This event was not sent to Firebase."); - } + Bundle formattedProperties = formatProperties(properties); + firebaseAnalytics.logEvent(eventName, formattedProperties); + logger.verbose("firebaseAnalytics.logEvent(%s, %s);", eventName, formattedProperties); } private String mapEvent(String event) { @@ -200,20 +177,18 @@ private String mapEvent(String event) { if (eventMapper.containsKey(eventName)) { eventName = eventMapper.get(eventName); } - if (eventName.contains(" ")) { - eventName = eventName.trim().toLowerCase().replaceAll(" ", "_"); - } + eventName = eventName.trim().replaceAll(" ", "_"); if (eventName.length() > 40) { eventName = trimKey(eventName); } return eventName; } - private Bundle formatProperties(Properties properties) throws Exception { + private Bundle formatProperties(Properties properties) { + Bundle bundle = new Bundle(); if ((properties.revenue() != 0 || properties.total() != 0) && isNullOrEmpty(properties.currency())) { - throw new Exception(); + bundle.putString(Param.CURRENCY, "USD"); } - Bundle bundle = new Bundle(); for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); property = mapProperty(property); @@ -259,9 +234,7 @@ private String mapProperty(String property) { if (propertyMapper.containsKey(property)) { property = propertyMapper.get(property); } - if (property.contains(" ")) { - property = property.trim().replaceAll(" ", "_"); - } + property = property.trim().replaceAll(" ", "_"); if (property.length() > 40) { property = trimKey(property); } From 807e66c409b91da36348d1bf264d1d062e070b19 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 6 Sep 2017 16:39:29 -0700 Subject: [PATCH 18/20] Remove regex to validate date strings --- .../integrations/firebase/FirebaseIntegration.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 318433b..41fc13a 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -21,9 +21,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Date; -import java.util.regex.Pattern; -import static java.util.regex.Pattern.CASE_INSENSITIVE; import static com.segment.analytics.internal.Utils.hasPermission; import static com.segment.analytics.internal.Utils.isNullOrEmpty; import static com.segment.analytics.internal.Utils.toISO8601Date; @@ -64,8 +62,6 @@ public String key() { } }; - static final Pattern ISO_DATE = - Pattern.compile("(^([0-9]{4})-(1[0-2]|0[1-9])-(0[1-9]|1[1-9]|2[0-9]|3[0-1]).*)", CASE_INSENSITIVE); private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; final Logger logger; final FirebaseAnalytics firebaseAnalytics; @@ -150,8 +146,6 @@ public void identify(IdentifyPayload identify) { if (entry.getValue() instanceof Date) { Date value = (Date) entry.getValue(); formattedValue = formatDate(value); - } else if (entry.getValue() instanceof String && ISO_DATE.matcher(String.valueOf(entry.getValue())).matches()) { - formattedValue = String.valueOf(entry.getValue()).substring(0, 10); } else { formattedValue = String.valueOf(entry.getValue()); } @@ -212,9 +206,6 @@ private Bundle formatProperties(Properties properties) { } if (entry.getValue() instanceof String) { String value = String.valueOf(entry.getValue()); - if (ISO_DATE.matcher(value).matches()) { - value = value.substring(0, 10); - } bundle.putString(property, value); logger.verbose("bundle.putString(%s, %s);", property, value); continue; From 2cc8c4c2977c50e2521fb9816b3adc1f76e18489 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 6 Sep 2017 16:53:33 -0700 Subject: [PATCH 19/20] Reformat with ./gradlew fmt --- .../firebase/FirebaseIntegration.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 41fc13a..5e8b90c 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -66,9 +66,9 @@ public String key() { final Logger logger; final FirebaseAnalytics firebaseAnalytics; private static final Map eventMapper = createEventMap(); - private static Map createEventMap() - { - Map eventMapper = new HashMap<>(); + + private static Map createEventMap() { + Map eventMapper = new HashMap<>(); eventMapper.put("Product Added", Event.ADD_TO_CART); eventMapper.put("Checkout Started", Event.BEGIN_CHECKOUT); eventMapper.put("Order Completed", Event.ECOMMERCE_PURCHASE); @@ -84,10 +84,11 @@ private static Map createEventMap() eventMapper.put("Promotion Viewed", Event.PRESENT_OFFER); return eventMapper; } + private static final Map propertyMapper = createPropertyMap(); - private static Map createPropertyMap() - { - Map propertyMapper = new HashMap<>(); + + private static Map createPropertyMap() { + Map propertyMapper = new HashMap<>(); propertyMapper.put("category", Param.ITEM_CATEGORY); propertyMapper.put("product_id", Param.ITEM_ID); propertyMapper.put("name", Param.ITEM_NAME); @@ -115,14 +116,12 @@ public void onActivityResumed(Activity activity) { PackageManager packageManager = activity.getPackageManager(); try { ActivityInfo info = - packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); + packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); String activityLabel = info.loadLabel(packageManager).toString(); - firebaseAnalytics.setCurrentScreen( - activity, activityLabel, null); + firebaseAnalytics.setCurrentScreen(activity, activityLabel, null); logger.verbose( - "firebaseAnalytics.setCurrentScreen(activity, %s, null /* class override */);", - activityLabel); - + "firebaseAnalytics.setCurrentScreen(activity, %s, null);", + activityLabel); } catch (PackageManager.NameNotFoundException e) { throw new AssertionError("Activity Not Found: " + e.toString()); } @@ -180,12 +179,13 @@ private String mapEvent(String event) { private Bundle formatProperties(Properties properties) { Bundle bundle = new Bundle(); - if ((properties.revenue() != 0 || properties.total() != 0) && isNullOrEmpty(properties.currency())) { + if ((properties.revenue() != 0 || properties.total() != 0) + && isNullOrEmpty(properties.currency())) { bundle.putString(Param.CURRENCY, "USD"); } for (Map.Entry entry : properties.entrySet()) { String property = entry.getKey(); - property = mapProperty(property); + property = mapProperty(property); if (entry.getValue() instanceof Integer) { int value = (int) entry.getValue(); bundle.putInt(property, value); From 347862588cc245605dec4a80e087703a8c5151df Mon Sep 17 00:00:00 2001 From: Prateek Srivastava Date: Thu, 7 Sep 2017 08:52:58 -0700 Subject: [PATCH 20/20] Add Test Scaffold --- build.gradle | 23 ++++---- .../firebase/FirebaseIntegration.java | 11 ++-- .../integration/firebase/FirebaseTest.java | 58 ++++++++++++++++++- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 96bf021..64d127f 100644 --- a/build.gradle +++ b/build.gradle @@ -39,23 +39,26 @@ dependencies { url "https://maven.google.com/" } } + compile 'com.google.firebase:firebase-core:11.2.0' compile 'com.segment.analytics.android:analytics:4.2.6' + testCompile 'com.segment.analytics.android:analytics-tests:4.2.6' + testCompile 'junit:junit:4.12' - testCompile('org.robolectric:robolectric:3.4.2') { - exclude group: 'commons-logging', module: 'commons-logging' - exclude group: 'org.apache.httpcomponents', module: 'httpclient' - } - testCompile 'com.segment.analytics.android:analytics-tests:4.0.0' + + testCompile 'org.robolectric:robolectric:3.4.2' + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile 'org.mockito:mockito-core:1.10.19' - testCompile 'org.powermock:powermock:1.6.2' - testCompile 'org.powermock:powermock-module-junit4:1.6.2' - testCompile 'org.powermock:powermock-module-junit4-rule:1.6.2' - testCompile 'org.powermock:powermock-api-mockito:1.6.2' - testCompile 'org.powermock:powermock-classloading-xstream:1.6.2' + + testCompile 'org.powermock:powermock:1.6.6' + testCompile 'org.powermock:powermock-module-junit4:1.6.6' + testCompile 'org.powermock:powermock-module-junit4-rule:1.6.6' + testCompile 'org.powermock:powermock-api-mockito:1.6.6' + testCompile 'org.powermock:powermock-classloading-xstream:1.6.6' } apply plugin: 'com.f2prateek.javafmt' diff --git a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java index 5e8b90c..935d05c 100644 --- a/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java +++ b/src/main/java/com/segment/analytics/android/integrations/firebase/FirebaseIntegration.java @@ -63,8 +63,6 @@ public String key() { }; private static final String FIREBASE_ANALYTICS_KEY = "Firebase"; - final Logger logger; - final FirebaseAnalytics firebaseAnalytics; private static final Map eventMapper = createEventMap(); private static Map createEventMap() { @@ -104,9 +102,12 @@ private static Map createPropertyMap() { return propertyMapper; } + private final Logger logger; + private final FirebaseAnalytics firebaseAnalytics; + public FirebaseIntegration(Context context, Logger logger) { + this.firebaseAnalytics = FirebaseAnalytics.getInstance(context); this.logger = logger; - firebaseAnalytics = FirebaseAnalytics.getInstance(context); } @Override @@ -119,9 +120,7 @@ public void onActivityResumed(Activity activity) { packageManager.getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA); String activityLabel = info.loadLabel(packageManager).toString(); firebaseAnalytics.setCurrentScreen(activity, activityLabel, null); - logger.verbose( - "firebaseAnalytics.setCurrentScreen(activity, %s, null);", - activityLabel); + logger.verbose("firebaseAnalytics.setCurrentScreen(activity, %s, null);", activityLabel); } catch (PackageManager.NameNotFoundException e) { throw new AssertionError("Activity Not Found: " + e.toString()); } diff --git a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java index 2d81908..ed09430 100644 --- a/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java +++ b/src/test/java/com/segment/analytics/android/integration/firebase/FirebaseTest.java @@ -1,5 +1,57 @@ package com.segment.analytics.android.integration.firebase; -/* TO DO: - * Add Android test support - */ +import static com.segment.analytics.Analytics.LogLevel.VERBOSE; +import static com.segment.analytics.Utils.createTraits; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.content.Context; +import com.google.firebase.analytics.FirebaseAnalytics; +import com.segment.analytics.Traits; +import com.segment.analytics.android.integrations.firebase.FirebaseIntegration; +import com.segment.analytics.core.tests.BuildConfig; +import com.segment.analytics.integrations.Logger; +import com.segment.analytics.test.IdentifyPayloadBuilder; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class) +@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" }) +@PrepareForTest(FirebaseAnalytics.class) +public class FirebaseTest { + + @Rule public PowerMockRule rule = new PowerMockRule(); + private FirebaseAnalytics firebase; + private FirebaseIntegration integration; + + @Before + public void setUp() { + firebase = PowerMockito.mock(FirebaseAnalytics.class); + Context context = PowerMockito.mock(Context.class); + + PowerMockito.mockStatic(FirebaseAnalytics.class); + Mockito.when(FirebaseAnalytics.getInstance(context)).thenReturn(firebase); + + integration = new FirebaseIntegration(context, Logger.with(VERBOSE)); + } + + @Test + public void identify() { + Traits traits = createTraits("foo"); + + integration.identify(new IdentifyPayloadBuilder().traits(traits).build()); + + verify(firebase).setUserId("foo"); + } +}