All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.segment.analytics.android.integrations.google.analytics.GoogleAnalyticsIntegration Maven / Gradle / Ivy

The newest version!
package com.segment.analytics.android.integrations.google.analytics;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;

import android.net.Uri;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.ecommerce.ProductAction;
import com.segment.analytics.Analytics;
import com.segment.analytics.Properties;
import com.segment.analytics.AnalyticsContext;
import com.segment.analytics.Properties.Product;
import com.segment.analytics.ValueMap;
import com.segment.analytics.integrations.BasePayload;
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 com.segment.analytics.internal.Utils;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static com.segment.analytics.internal.Utils.hasPermission;
import static com.segment.analytics.internal.Utils.isNullOrEmpty;
import static java.util.regex.Pattern.CASE_INSENSITIVE;

/**
 * Google Analytics is the most popular analytics tool for the web because it's free and sports a
 * wide range of features. It's especially good at measuring traffic sources and ad campaigns.
 *
 * @see Google Analytics
 * @see Google Analytics
 * Integration
 * @see Google
 * Analyitcs Android SDK
 */
public class GoogleAnalyticsIntegration
    extends Integration {
  public static final Factory FACTORY = new Factory() {
    @Override public Integration create(ValueMap settings, Analytics analytics) {
      Logger logger = analytics.logger(GOOGLE_ANALYTICS_KEY);
      if (!hasPermission(analytics.getApplication(), Manifest.permission.ACCESS_NETWORK_STATE)) {
        logger.debug("ACCESS_NETWORK_STATE is required for Google Analytics.");
        return null;
      }
      String mobileTrackingId = settings.getString("mobileTrackingId");
      if (isNullOrEmpty(mobileTrackingId)) {
        logger.debug("mobileTrackingId is required for Google Analytics.");
        return null;
      }

      Context context = analytics.getApplication();
      // We already check for permissions before initializing the GA instance
      @SuppressLint("MissingPermission")
      com.google.android.gms.analytics.GoogleAnalytics ga =
          com.google.android.gms.analytics.GoogleAnalytics.getInstance(context);

      GoogleAnalytics googleAnalytics = new DefaultGoogleAnalytics(ga);
      return new GoogleAnalyticsIntegration(context, googleAnalytics, settings, logger);
    }

    @Override public String key() {
      return GOOGLE_ANALYTICS_KEY;
    }
  };
  private static final ValueMap EMPTY = new ValueMap(Collections.emptyMap());
  private static final String DEFAULT_CATEGORY = "All";
  private static final String ENHANCED_ECOMMERCE = "EnhancedEcommerce";
  static final Pattern COMPLETED_ORDER_PATTERN =
      Pattern.compile("completed *order|"
              + "order *completed", CASE_INSENSITIVE);
  static final Pattern PRODUCT_EVENT_NAME_PATTERN =
      Pattern.compile("(((viewed)|(added)|(removed)) *product *.*)|"
              + "(product *.* ((viewed)|(added)|(removed)))", CASE_INSENSITIVE);
  static final Pattern PRODUCT_ADDED =
      Pattern.compile("((added) *product *.*|product *.* (added))", CASE_INSENSITIVE);
  static final Pattern PRODUCT_REMOVED =
      Pattern.compile("((removed) *product *.*|product(removed))", CASE_INSENSITIVE);
  private static final String GOOGLE_ANALYTICS_KEY = "Google Analytics";
  private static final String DIMENSION_PREFIX = "dimension";
  private static final String DIMENSION_PREFIX_KEY = "&cd";
  private static final String METRIC_PREFIX = "metric";
  private static final String METRIC_PREFIX_KEY = "&cm";
  private static final String USER_ID_KEY = "&uid";
  private static final String QUANTITY_KEY = "quantity";
  private static final String LABEL_KEY = "label";

  final Tracker tracker;
  final GoogleAnalytics googleAnalytics;
  final Logger logger;
  // Mutable for testing.
  boolean sendUserId;
  ValueMap customDimensions;
  ValueMap customMetrics;

  GoogleAnalyticsIntegration(Context context, GoogleAnalytics googleAnalytics, ValueMap settings,
      Logger logger) {
    this.googleAnalytics = googleAnalytics;
    this.logger = logger;

    String mobileTrackingId = settings.getString("mobileTrackingId");
    tracker = googleAnalytics.newTracker(mobileTrackingId);
    logger.verbose("GoogleAnalytics.getInstance(context).newTracker(%s);", mobileTrackingId);

    boolean anonymizeIp = settings.getBoolean("anonymizeIp", false);
    tracker.setAnonymizeIp(anonymizeIp);
    logger.verbose("tracker.setAnonymizeIp(%s);", anonymizeIp);

    boolean reportUncaughtExceptions = settings.getBoolean("reportUncaughtExceptions", false);
    if (reportUncaughtExceptions) {
      tracker.setUncaughtExceptionReporter(context);
      logger.verbose("Thread.setDefaultUncaughtExceptionHandler(new ExceptionReporter(...));");
    }

    sendUserId = settings.getBoolean("sendUserId", false);
    customDimensions = settings.getValueMap("dimensions");
    if (isNullOrEmpty(customDimensions)) customDimensions = EMPTY;
    customMetrics = settings.getValueMap("metrics");
    if (isNullOrEmpty(customMetrics)) customMetrics = EMPTY;
  }

  @Override public void onActivityStarted(Activity activity) {
    super.onActivityStarted(activity);
    googleAnalytics.reportActivityStart(activity);
    logger.verbose("GoogleAnalytics.getInstance(context).reportActivityStart(activity);");
  }

  @Override public void onActivityStopped(Activity activity) {
    super.onActivityStopped(activity);
    googleAnalytics.reportActivityStop(activity);
    logger.verbose("GoogleAnalytics.getInstance(context).reportActivityStop(activity);");
  }

  @Override public void screen(ScreenPayload screen) {
    Properties properties = screen.properties();
    String screenName = screen.event();

    sendProductEvent(screenName, screen.category(), properties);

    tracker.setScreenName(screenName);
    logger.verbose("tracker.setScreenName(%s);", screenName);

    ScreenViewHitBuilder hitBuilder = new ScreenViewHitBuilder();
    attachCustomDimensionsAndMetrics(hitBuilder, properties);
    attachCampaignData(hitBuilder, screen);

    Map hit = hitBuilder.build();
    tracker.send(hit);
    logger.verbose("tracker.send(%s);", hit);
  }

  @Override public void identify(IdentifyPayload identify) {
    if (sendUserId) {
      String userId = identify.userId();
      tracker.set(USER_ID_KEY, userId);
      logger.verbose("tracker.set(%s, %s);", USER_ID_KEY, userId);
    }

    // Set traits, custom dimensions, and custom metrics on the shared tracker.
    for (Map.Entry entry : identify.traits().entrySet()) {
      String trait = entry.getKey();
      if (customDimensions.containsKey(trait)) {
        String dimension =
            customDimensions.getString(trait).replace(DIMENSION_PREFIX, DIMENSION_PREFIX_KEY);
        String value = String.valueOf(entry.getValue());
        tracker.set(dimension, value);
        logger.verbose("tracker.set(%s, %s);", dimension, value);
      }
      if (customMetrics.containsKey(trait)) {
        String metric = customMetrics.getString(trait).replace(METRIC_PREFIX, METRIC_PREFIX_KEY);
        String value = String.valueOf(entry.getValue());
        tracker.set(metric, value);
        logger.verbose("tracker.set(%s, %s);", metric, value);
      }
    }
  }

  @Override public void track(TrackPayload track) {
    Properties properties = track.properties();
    String event = track.event();
    String category = properties.category();

    sendProductEvent(event, category, properties);

    if (COMPLETED_ORDER_PATTERN.matcher(event).matches()) {

      EventHitBuilder eventHitBuilder = new EventHitBuilder();

      List products = properties.products();
      if (!isNullOrEmpty(products)) {
        for (int i = 0; i < products.size(); i++) {
          Product product = products.get(i);

          com.google.android.gms.analytics.ecommerce.Product newProduct =
            new com.google.android.gms.analytics.ecommerce.Product()
                .setId(product.id())
                .setName(product.name())
                .setPrice(product.price())
                .setQuantity(product.getInt(QUANTITY_KEY, 0));

          eventHitBuilder.addProduct(newProduct);
        }
      }

      ProductAction productAction = new ProductAction(ProductAction.ACTION_PURCHASE)
          .setTransactionId(properties.orderId())
          .setTransactionRevenue(properties.total())
          .setTransactionTax(properties.tax())
          .setTransactionShipping(properties.shipping());

      eventHitBuilder.setProductAction(productAction);

      eventHitBuilder = addEcommerceEventCategory(eventHitBuilder, properties);

      attachCustomDimensionsAndMetrics(eventHitBuilder, properties);
      Map transaction = eventHitBuilder.build();
      tracker.send(transaction);
      logger.verbose("tracker.send(%s);", transaction);
    }

    String label = properties.getString(LABEL_KEY);
    EventHitBuilder eventHitBuilder = new EventHitBuilder();
    eventHitBuilder.setAction(event)
        .setCategory(isNullOrEmpty(category) ? DEFAULT_CATEGORY : category)
        .setLabel(label)
        .setValue((int) properties.value());

    attachCustomDimensionsAndMetrics(eventHitBuilder, properties);
    attachCampaignData(eventHitBuilder, track);

    if (event.equals("Deep Link Opened")) {
      eventHitBuilder.setCampaignParamsFromUrl(track.properties().getString("url"));
    }

    Map eventHit = eventHitBuilder.build();
    tracker.send(eventHit);
    logger.verbose("tracker.send(%s);", eventHit);
  }

  /**
   * HitBuilder declares setCustomDimension and setCustomMetric, but it is a protected class, so
   * attachCustomDimensionsAndMetrics can't accept it as a parameter. Write our own wrapper that
   * exposes the required methods.
   */
  interface CustomHitBuilder {
    CustomHitBuilder setCustomDimension(int index, String dimension);

    CustomHitBuilder setCustomMetric(int index, float metric);

    CustomHitBuilder setCampaignParamsFromUrl(String url);
  }

  static class EventHitBuilder extends HitBuilders.EventBuilder implements CustomHitBuilder {
    @Override public EventHitBuilder setCustomDimension(int index, String dimension) {
      super.setCustomDimension(index, dimension);
      return this;
    }

    @Override public EventHitBuilder setCustomMetric(int index, float metric) {
      super.setCustomMetric(index, metric);
      return this;
    }

    @Override public EventHitBuilder setCampaignParamsFromUrl(String url) {
      super.setCampaignParamsFromUrl(url);
      return this;
    }
  }

  static class ScreenViewHitBuilder extends HitBuilders.ScreenViewBuilder
      implements CustomHitBuilder {
    @Override public ScreenViewHitBuilder setCustomDimension(int index, String dimension) {
      super.setCustomDimension(index, dimension);
      return this;
    }

    @Override public ScreenViewHitBuilder setCustomMetric(int index, float metric) {
      super.setCustomMetric(index, metric);
      return this;
    }

    @Override public ScreenViewHitBuilder setCampaignParamsFromUrl(String url) {
      super.setCampaignParamsFromUrl(url);
      return this;
    }
  }

  /** Set custom dimensions and metrics on the hit. */
  void attachCustomDimensionsAndMetrics(CustomHitBuilder hitBuilder, Properties properties) {
    for (Map.Entry entry : properties.entrySet()) {
      String property = entry.getKey();
      if (customDimensions.containsKey(property)) {
        int dimension =
            extractNumber(customDimensions.getString(property), DIMENSION_PREFIX.length());
        hitBuilder.setCustomDimension(dimension, String.valueOf(entry.getValue()));
      }
      if (customMetrics.containsKey(property)) {
        int metric = extractNumber(customMetrics.getString(property), METRIC_PREFIX.length());
        hitBuilder.setCustomMetric(metric, Utils.coerceToFloat(entry.getValue(), 0));
      }
    }
  }

  /**
   * Set custom dimensions and metrics on the product.
   *
   * This implementation is the same as {@link #attachCustomDimensionsAndMetrics}.
   *
   * It is copied over as there is no common interface between {@link HitBuilders}
   * and {@link com.google.android.gms.analytics.ecommerce.Product}.
   */
  void attachCustomDimensionsAndMetricsOnProduct(
      com.google.android.gms.analytics.ecommerce.Product product, Properties properties) {
    for (Map.Entry entry : properties.entrySet()) {
      String property = entry.getKey();
      if (customDimensions.containsKey(property)) {
        int dimension =
            extractNumber(customDimensions.getString(property), DIMENSION_PREFIX.length());
        product.setCustomDimension(dimension, String.valueOf(entry.getValue()));
      }
      if (customMetrics.containsKey(property)) {
        int metric = extractNumber(customMetrics.getString(property), METRIC_PREFIX.length());
        product.setCustomMetric(metric, (int) Utils.coerceToFloat(entry.getValue(), 0));
      }
    }
  }

  /** Set campaign data when present. */
  void attachCampaignData(CustomHitBuilder hitBuilder, BasePayload payload) {
    AnalyticsContext.Campaign campaign = payload.context().campaign();
    if (isNullOrEmpty(campaign)) {
      return;
    }

    
    String url = new Uri.Builder().appendQueryParameter("utm_content", campaign.content())
        .appendQueryParameter("utm_source", campaign.source())
        .appendQueryParameter("utm_medium", campaign.medium())
        .appendQueryParameter("utm_campaign", campaign.name())
        .build()
        .toString();

    hitBuilder.setCampaignParamsFromUrl(url);
  }

  // e.g. extractNumber("dimension3", 8) returns 3
  // e.g. extractNumber("dimension9", 8) returns 9
  private static int extractNumber(String text, int start) {
    if (isNullOrEmpty(text)) {
      return 0;
    }
    return Integer.parseInt(text.substring(start, text.length()));
  }

  @Override public void flush() {
    googleAnalytics.dispatchLocalHits();
    logger.verbose("GoogleAnalytics.getInstance(context).dispatchLocalHits();");
  }

  /** Send a product event. */
  void sendProductEvent(String event, String category, Properties eventProperties) {
    if (!PRODUCT_EVENT_NAME_PATTERN.matcher(event).matches()) {
      return;
    }

    List products = eventProperties.products();
    if (isNullOrEmpty(products)) {
      products = Collections.singletonList(new Product(eventProperties.productId(), eventProperties.sku(), eventProperties.price()));
    }

    for (Product productProperties : products) {
      Properties properties = new Properties();
      properties.putAll(eventProperties);
      properties.putAll(productProperties);

      com.google.android.gms.analytics.ecommerce.Product product =
        new com.google.android.gms.analytics.ecommerce.Product()
            .setId(properties.productId() )
            .setName(properties.name())
            .setCategory(isNullOrEmpty(category) ? DEFAULT_CATEGORY : category)
            .setPrice(properties.price())
            .setQuantity(properties.getInt(QUANTITY_KEY, 0));

      attachCustomDimensionsAndMetricsOnProduct(product, properties);

      // initialize variables with default values
      String action = ProductAction.ACTION_DETAIL;
      String eventAction = "Product Viewed";

      if (PRODUCT_ADDED.matcher(event).matches()) {
        action = ProductAction.ACTION_ADD;
        eventAction = "Product Added";
      }

      if (PRODUCT_REMOVED.matcher(event).matches()) {
        action = ProductAction.ACTION_REMOVE;
        eventAction = "Product Removed";
      }

      ProductAction productAction = new ProductAction(action);

      EventHitBuilder eventHitBuilder = new EventHitBuilder();
      eventHitBuilder.addProduct(product)
          .setProductAction(productAction)
          .setAction(eventAction);
      eventHitBuilder = addEcommerceEventCategory(eventHitBuilder, properties);
      attachCustomDimensionsAndMetrics(eventHitBuilder, properties);

      Map productEvent = eventHitBuilder.build();
      tracker.send(productEvent);
      logger.verbose("tracker.send(%s);", productEvent);
    }
  }

  @Override public com.google.android.gms.analytics.Tracker getUnderlyingInstance() {
    return tracker.delegate();
  }

  public EventHitBuilder addEcommerceEventCategory(EventHitBuilder eventHitBuilder,
                                                   Properties properties) {
    if (!isNullOrEmpty(properties.category())) {
      eventHitBuilder.setCategory(properties.category());
    } else {
      eventHitBuilder.setCategory(ENHANCED_ECOMMERCE);
    }
    return eventHitBuilder;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy