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

com.segment.analytics.Analytics Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Segment, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.segment.analytics;

import android.Manifest;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.segment.analytics.internal.Utils;
import com.segment.analytics.internal.Utils.AnalyticsExecutorService;
import com.segment.analytics.internal.model.payloads.AliasPayload;
import com.segment.analytics.internal.model.payloads.BasePayload;
import com.segment.analytics.internal.model.payloads.GroupPayload;
import com.segment.analytics.internal.model.payloads.IdentifyPayload;
import com.segment.analytics.internal.model.payloads.ScreenPayload;
import com.segment.analytics.internal.model.payloads.TrackPayload;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import static com.segment.analytics.internal.Utils.debug;
import static com.segment.analytics.internal.Utils.getResourceString;
import static com.segment.analytics.internal.Utils.hasPermission;
import static com.segment.analytics.internal.Utils.isNullOrEmpty;

/**
 * The entry point into the Segment for Android SDK.
 * 

* The idea is simple: one pipeline for all your data. Segment is the single hub to collect, * translate and route your data with the flip of a switch. *

* Analytics for Android will automatically batch events, queue them to disk, and upload it * periodically to Segment for you. It will also look up your project's settings (that you've * configured in the web interface), specifically looking up settings for bundled integrations, and * then initialize them for you on the user's phone, and mapping our standardized events to formats * they can all understand. You only need to instrument Segment once, then flip a switch to install * new tools. *

* This class is the main entry point into the client API. Use {@link * #with(android.content.Context)} for the global singleton instance or construct your own instance * with {@link Builder}. * * @see Segment */ public class Analytics { static final Handler HANDLER = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { throw new AssertionError("Unknown handler message received: " + msg.what); } }; static final String WRITE_KEY_RESOURCE_IDENTIFIER = "analytics_write_key"; static final List INSTANCES = new ArrayList<>(1); volatile static Analytics singleton = null; private static final Properties EMPTY_PROPERTIES = new Properties(); private final Application application; private final ExecutorService networkExecutor; private final IntegrationManager integrationManager; private final Stats stats; private final Options defaultOptions; private final Traits.Cache traitsCache; private final AnalyticsContext analyticsContext; private final LogLevel logLevel; private final String tag; volatile boolean shutdown; /** * Return a reference to the global default {@link Analytics} instance. *

* This instance is automatically initialized with defaults that are suitable to most * implementations. *

* If these settings do not meet the requirements of your application, you can override defaults * in {@code analytics.xml}, or you can construct your own instance with full control over the * configuration by using {@link Builder}. *

* By default, events are uploaded every 30 seconds, or every 20 events (whichever occurs first), * and debugging is disabled. */ public static Analytics with(Context context) { if (singleton == null) { if (context == null) { throw new IllegalArgumentException("Context must not be null."); } synchronized (Analytics.class) { if (singleton == null) { String writeKey = getResourceString(context, WRITE_KEY_RESOURCE_IDENTIFIER); Builder builder = new Builder(context, writeKey); try { String packageName = context.getPackageName(); int flags = context.getPackageManager().getApplicationInfo(packageName, 0).flags; boolean debugging = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; if (debugging) { builder.logLevel(LogLevel.INFO); } } catch (PackageManager.NameNotFoundException ignored) { } singleton = builder.build(); } } } return singleton; } /** * Set the global instance returned from {@link #with}. *

* This method must be called before any calls to {@link #with} and may only be called once. */ public static void setSingletonInstance(Analytics analytics) { synchronized (Analytics.class) { if (singleton != null) { throw new IllegalStateException("Singleton instance already exists."); } singleton = analytics; } } Analytics(Application application, ExecutorService networkExecutor, IntegrationManager.Factory integrationManagerFactory, Stats stats, Traits.Cache traitsCache, AnalyticsContext analyticsContext, Options defaultOptions, LogLevel logLevel, String tag) { this.application = application; this.networkExecutor = networkExecutor; this.stats = stats; this.traitsCache = traitsCache; this.analyticsContext = analyticsContext; this.defaultOptions = defaultOptions; this.logLevel = logLevel; this.tag = tag; // This needs to be last so that the analytics instance members are assigned first this.integrationManager = integrationManagerFactory.create(this); } // Analytics API /** * Identify lets you tie one of your users and their actions to a recognizable {@code userId}. It * also lets you record {@code traits} about the user, like their email, name, account type, etc. * This method will simply set the userId for the current user. * * @see #identify(String, Traits, Options) */ public void identify(String userId) { identify(userId, null, null); } /** * Identify lets you tie one of your users and their actions to a recognizable {@code userId}. It * also lets you record {@code traits} about the user, like their email, name, account type, etc. * This method will simply add the given traits to the user profile. * * @see #identify(String, Traits, Options) */ public void identify(Traits traits) { identify(null, traits, null); } /** * Identify lets you tie one of your users and their actions to a recognizable {@code userId}. It * also lets you record {@code traits} about the user, like their email, name, account type, etc. *

* Traits and userId will be automatically cached and available on future sessions for the same * user. To update a trait on the server, call identify with the same user id (or null). You can * also use {@link #identify(Traits)} for this purpose. * * @param userId Unique identifier which you recognize a user by in your own database. If this * is null or empty, any previous id we have (could be the anonymous id) will be * used. * @param newTraits Traits about the user * @param options To configure the call * @throws IllegalArgumentException if both {@code userId} and {@code newTraits} are not provided * @see Identify Documentation */ public void identify(String userId, Traits newTraits, Options options) { if (isNullOrEmpty(userId) && isNullOrEmpty(newTraits)) { throw new IllegalArgumentException("Either userId or some traits must be provided."); } Traits traits = traitsCache.get(); if (!isNullOrEmpty(userId)) { traits.putUserId(userId); } if (!isNullOrEmpty(newTraits)) { traits.putAll(newTraits); } traitsCache.set(traits); // Save the new traits analyticsContext.setTraits(traits); // Update the references if (options == null) { options = defaultOptions; } IdentifyPayload payload = new IdentifyPayload(analyticsContext, options, traitsCache.get()); submit(payload); } /** * The group method lets you associate a user with a group. It also lets you record custom traits * about the group, like industry or number of employees. * * @see #group(String, Traits, Options) */ public void group(String groupId) { group(groupId, null, null); } /** * The group method lets you associate a user with a group. It also lets you record custom traits * about the group, like industry or number of employees. *

* If you've called {@link #identify(String, Traits, Options)} before, this will automatically * remember the userId. If not, it will fall back to use the anonymousId instead. * * @param groupId Unique identifier which you recognize a group by in your own database. Must not * be null or empty. * @param options To configure the call * @throws IllegalArgumentException if groupId is null or an empty string * @see Group Documentation */ public void group(String groupId, Traits groupTraits, Options options) { if (isNullOrEmpty(groupId)) { throw new IllegalArgumentException("groupId must not be null or empty."); } if (groupTraits == null) { groupTraits = new Traits(); } if (options == null) { options = defaultOptions; } GroupPayload payload = new GroupPayload(analyticsContext, options, groupId, groupTraits); submit(payload); } /** * The track method is how you record any actions your users perform. Each action is known by a * name, like 'Purchased a T-Shirt'. You can also record properties specific to those actions. * For example a 'Purchased a Shirt' event might have properties like revenue or size. * * @see #track(String, Properties, Options) */ public void track(String event) { track(event, null, null); } /** * The track method is how you record any actions your users perform. Each action is known by a * name, like 'Purchased a T-Shirt'. You can also record properties specific to those actions. * For example a 'Purchased a Shirt' event might have properties like revenue or size. * * @see #track(String, Properties, Options) */ public void track(String event, Properties properties) { track(event, properties, null); } /** * The track method is how you record any actions your users perform. Each action is known by a * name, like 'Purchased a T-Shirt'. You can also record properties specific to those actions. * For example a 'Purchased a Shirt' event might have properties like revenue or size. * * @param event Name of the event. Must not be null or empty. * @param properties {@link Properties} to add extra information to this call * @param options To configure the call * @throws IllegalArgumentException if event name is null or an empty string * @see Track Documentation */ public void track(String event, Properties properties, Options options) { if (isNullOrEmpty(event)) { throw new IllegalArgumentException("event must not be null or empty."); } if (properties == null) { properties = EMPTY_PROPERTIES; } if (options == null) { options = defaultOptions; } TrackPayload payload = new TrackPayload(analyticsContext, options, event, properties); submit(payload); } /** @see #screen(String, String, Properties, Options) */ public void screen(String category, String name) { screen(category, name, null, null); } /** @see #screen(String, String, Properties, Options) */ public void screen(String category, String name, Properties properties) { screen(category, name, properties, null); } /** * The screen methods let your record whenever a user sees a screen of your mobile app, and * attach * a name, category or properties to the screen. *

* Either category or name must be provided. * * @param category A category to describe the screen * @param name A name for the screen * @param properties {@link Properties} to add extra information to this call * @param options To configure the call * @see Screen Documentation */ public void screen(String category, String name, Properties properties, Options options) { if (isNullOrEmpty(category) && isNullOrEmpty(name)) { throw new IllegalArgumentException("either category or name must be provided."); } if (properties == null) { properties = EMPTY_PROPERTIES; } if (options == null) { options = defaultOptions; } ScreenPayload payload = new ScreenPayload(analyticsContext, options, category, name, properties); submit(payload); } /** * The alias method is used to merge two user identities, effectively connecting two sets of user * data as one. This is an advanced method, but it is required to manage user identities * successfully in some of our integrations. * * @see #alias(String, Options) */ public void alias(String newId) { alias(newId, null); } /** * The alias method is used to merge two user identities, effectively connecting two sets of user * data as one. This is an advanced method, but it is required to manage user identities * successfully in some of our integrations. *

* * Usage: *

 
   *   analytics.track("user did something");
   *   analytics.alias(newId);
   *   analytics.identify(newId);
   *  
* * @param newId The new ID you want to alias the existing ID to. The existing ID will be either * the previousId if you have called identify, or the anonymous ID. * @param options To configure the call * @throws IllegalArgumentException if newId is null or empty * @see Alias Documentation */ public void alias(String newId, Options options) { if (isNullOrEmpty(newId)) { throw new IllegalArgumentException("newId must not be null or empty."); } if (options == null) { options = defaultOptions; } AliasPayload payload = new AliasPayload(analyticsContext, options, newId); submit(payload); } void submit(BasePayload payload) { if (shutdown) { throw new IllegalStateException("Cannot enqueue messages after client is shutdown."); } if (logLevel.log()) { debug("Created payload %s.", payload); } integrationManager.dispatchEnqueue(payload); } /** * Asynchronously flushes all messages in the queue to the server, and tells bundled integrations * to do the same. */ public void flush() { if (shutdown) { throw new IllegalStateException("Cannot enqueue messages after client is shutdown."); } integrationManager.dispatchFlush(); } /** Get the {@link AnalyticsContext} used by this instance. */ @SuppressWarnings("UnusedDeclaration") public AnalyticsContext getAnalyticsContext() { return analyticsContext; } /** Creates a {@link StatsSnapshot} of the current stats for this instance. */ public StatsSnapshot getSnapshot() { return stats.createSnapshot(); } /** Return the {@link Application} used to create this instance. */ public Application getApplication() { return application; } /** Return the {@link LogLevel} for this instance. */ public LogLevel getLogLevel() { return logLevel; } /** * Logs out the current user by clearing any information, including traits and user id. * * @deprecated Use {@link #reset()} instead */ @Deprecated public void logout() { reset(); } /** * Resets the analytics client by clearing any stored information about the user. Events queued * on disk are not cleared, and will be uploaded at a later time. */ public void reset() { traitsCache.delete(); traitsCache.set(Traits.create()); analyticsContext.setTraits(traitsCache.get()); integrationManager.dispatchReset(); } /** Stops this instance from accepting further requests. */ public void shutdown() { if (this == singleton) { throw new UnsupportedOperationException("Default singleton instance cannot be shutdown."); } if (shutdown) { return; } integrationManager.shutdown(); if (networkExecutor instanceof AnalyticsExecutorService) { networkExecutor.shutdown(); } stats.shutdown(); shutdown = true; synchronized (INSTANCES) { INSTANCES.remove(tag); } } /** * Register to be notified when a bundled integration is ready. *

* In most cases, integrations would have already been initialized, and the callback will be * invoked fairly quickly. However there may be a latency the first time the app is launched, and * we don't have settings for bundled integrations yet. This is compounded if the user is offline * on the first run. *

* You can only register for one callback per integration at a time, and passing in a {@code * callback} will remove the previous callback for that integration. *

* The callback is invoked on the same thread we call integrations on, so if you want to update * the UI, make sure you move off to the main thread. *

* Usage: *

 
   *   analytics.onIntegrationReady(BundledIntegration.AMPLITUDE, new Callback() {
   *     {@literal @}Override public void onIntegrationReady(Object instance) {
   *       Amplitude.enableLocationListening();
   *     }
   *   });
   *   analytics.onIntegrationReady(BundledIntegration.MIXPANEL, new Callback() {
   *     {@literal @}Override public void onIntegrationReady(Object instance) {
   *       ((MixpanelAPI) instance).clearSuperProperties();
   *     }
   *   })*
   *  
*/ public void onIntegrationReady(BundledIntegration bundledIntegration, Callback callback) { if (bundledIntegration == null) { throw new IllegalArgumentException("bundledIntegration cannot be null."); } if (integrationManager == null) { throw new IllegalStateException("Enable bundled integrations to register for this callback."); } integrationManager.dispatchRegisterCallback(bundledIntegration.key, callback); } public enum BundledIntegration { AMPLITUDE("Amplitude"), APPS_FLYER("AppsFlyer"), APPTIMIZE("Apptimize"), BUGSNAG("Bugsnag"), COUNTLY("Countly"), CRITTERCISM("Crittercism"), FLURRY("Flurry"), GOOGLE_ANALYTICS("Google Analytics"), KAHUNA("Kahuna"), LEANPLUM("Leanplum"), LOCALYTICS("Localytics"), MIXPANEL("Mixpanel"), QUANTCAST("Quantcast"), TAPLYTICS("Taplytics"), TAPSTREAM("Tapstream"), UXCAM("UXCam"); /** The key that identifies this integration in our API. */ final String key; BundledIntegration(String key) { this.key = key; } } /** Controls the level of logging. */ public enum LogLevel { /** No logging. */ NONE, /** Log exceptions and events through the Segment SDK only. */ BASIC, /** * Log exceptions, events through the Segment SDK, and enable logging for bundled integrations. */ INFO, /** * Log exceptions, events through the SDK, and enable verbose logging for bundled integrations * that support it. */ VERBOSE; public boolean log() { return this != NONE; } } /** * A callback interface that is invoked when the Analytics client initializes bundled * integrations. */ public interface Callback { /** * This method will be invoked once for each callback. * * @param instance The underlying instance that has been initialized with the settings from * Segment. */ void onReady(Object instance); } /** Fluent API for creating {@link Analytics} instances. */ public static class Builder { private final Application application; private String writeKey; private boolean collectDeviceID = Utils.DEFAULT_COLLECT_DEVICE_ID; private int flushQueueSize = Utils.DEFAULT_FLUSH_QUEUE_SIZE; private long flushIntervalInMillis = Utils.DEFAULT_FLUSH_INTERVAL; private Options defaultOptions; private String tag; private LogLevel logLevel; private ExecutorService networkExecutor; private ConnectionFactory connectionFactory; /** Start building a new {@link Analytics} instance. */ public Builder(Context context, String writeKey) { if (context == null) { throw new IllegalArgumentException("Context must not be null."); } if (!hasPermission(context, Manifest.permission.INTERNET)) { throw new IllegalArgumentException("INTERNET permission is required."); } application = (Application) context.getApplicationContext(); if (application == null) { throw new IllegalArgumentException("Application context must not be null."); } if (isNullOrEmpty(writeKey)) { throw new IllegalArgumentException("writeKey must not be null or empty."); } this.writeKey = writeKey; } /** * Set the queue size at which the client should flush events. The client will automatically * flush events to Segment when the queue reaches {@code flushQueueSize}. * * @throws IllegalArgumentException if the flushQueueSize is less than or equal to zero. */ public Builder flushQueueSize(int flushQueueSize) { if (flushQueueSize <= 0) { throw new IllegalArgumentException("flushQueueSize must be greater than or equal to zero."); } // 250 is a reasonably high number to trigger queue size flushes. // The queue may go over this size (as much as 1000), but you should flush much before then. if (flushQueueSize > 250) { throw new IllegalArgumentException("flushQueueSize must be less than or equal to 250."); } this.flushQueueSize = flushQueueSize; return this; } /** * Set the interval at which the client should flush events. The client will automatically * flush events to Segment every {@code flushInterval} duration, regardless of {@code * flushQueueSize}. * * @throws IllegalArgumentException if the flushInterval is less than or equal to zero. */ public Builder flushInterval(long flushInterval, TimeUnit timeUnit) { if (timeUnit == null) { throw new IllegalArgumentException("timeUnit must not be null."); } if (flushInterval <= 0) { throw new IllegalArgumentException("flushInterval must be greater than zero."); } this.flushIntervalInMillis = timeUnit.toMillis(flushInterval); return this; } /** * Enable or disable collection of {@link android.provider.Settings.Secure#ANDROID_ID}, * {@link android.os.Build#SERIAL} or the Telephony Identifier retreived via * {@link TelephonyManager#getDeviceId()} as available. * * Collection of the device identifier is enabled by default. */ public Builder collectDeviceId(boolean collect) { this.collectDeviceID = collect; return this; } /** * Set some default options for all calls. This will only be used to figure out which * integrations should be enabled or not for actions by default. * * @see {@link Options} */ public Builder defaultOptions(Options defaultOptions) { if (defaultOptions == null) { throw new IllegalArgumentException("defaultOptions must not be null."); } // Make a defensive copy this.defaultOptions = new Options(); for (Map.Entry entry : defaultOptions.integrations().entrySet()) { if (entry.getValue() instanceof Boolean) { this.defaultOptions.setIntegration(entry.getKey(), (Boolean) entry.getValue()); } else { // A value is provided for an integration, and it is not a boolean. Assume it is enabled. this.defaultOptions.setIntegration(entry.getKey(), true); } } return this; } /** * Set a tag for this instance. The tag is used to generate keys for caching. *

* By default the writeKey is used. You may want to specify an alternative one, if you want * the instances with the same writeKey to share different caches (you probably do). * * @throws IllegalArgumentException if the tag is null or empty. */ public Builder tag(String tag) { if (isNullOrEmpty(tag)) { throw new IllegalArgumentException("tag must not be null or empty."); } this.tag = tag; return this; } /** Set a {@link LogLevel} for this instance. */ public Builder logLevel(LogLevel logLevel) { if (logLevel == null) { throw new IllegalArgumentException("LogLevel must not be null."); } this.logLevel = logLevel; return this; } /** @deprecated As of {@code 3.0.1}, this method does nothing. */ @Deprecated public Builder disableBundledIntegrations() { return this; } /** * Specify the executor service for making network calls in the background. *

* Note: Calling {@link Analytics#shutdown()} will not shutdown supplied executors. *

* Use it with care! http://bit.ly/1JVlA2e */ public Builder networkExecutor(ExecutorService networkExecutor) { if (networkExecutor == null) { throw new IllegalArgumentException("Executor service must not be null."); } this.networkExecutor = networkExecutor; return this; } /** * Specify the connection factory for customizing how connections are created. *

* This is a beta API, and might be changed in the future. * Use it with care! http://bit.ly/1JVlA2e */ public Builder connectionFactory(ConnectionFactory connectionFactory) { if (connectionFactory == null) { throw new IllegalArgumentException("ConnectionFactory must not be null."); } this.connectionFactory = connectionFactory; return this; } /** Create a {@link Analytics} client. */ public Analytics build() { if (defaultOptions == null) { defaultOptions = new Options(); } if (logLevel == null) { logLevel = LogLevel.NONE; } if (isNullOrEmpty(tag)) { tag = writeKey; } if (networkExecutor == null) { networkExecutor = new AnalyticsExecutorService(); } if (connectionFactory == null) { connectionFactory = new ConnectionFactory(); } final Stats stats = new Stats(); final Cartographer cartographer = Cartographer.INSTANCE; final Client client = new Client(application, writeKey, connectionFactory); IntegrationManager.Factory integrationManagerFactory = new IntegrationManager.Factory() { @Override public IntegrationManager create(Analytics analytics) { return IntegrationManager.create(analytics, cartographer, client, networkExecutor, stats, tag, flushIntervalInMillis, flushQueueSize); } }; Traits.Cache traitsCache = new Traits.Cache(application, cartographer, tag); if (!traitsCache.isSet() || traitsCache.get() == null) { Traits traits = Traits.create(); traitsCache.set(traits); } AnalyticsContext analyticsContext = AnalyticsContext.create(application, traitsCache.get(), collectDeviceID); analyticsContext.attachAdvertisingId(application); synchronized (INSTANCES) { if (INSTANCES.contains(tag)) { throw new IllegalStateException("Duplicate analytics client created with tag: " + tag + ". If you want to use multiple Analytics clients, use a different writeKey " + "or set a tag via the builder during construction."); } INSTANCES.add(tag); } if (logLevel.log()) { debug("Creating analytics client for project with writeKey:%s and tag:%s.", writeKey, tag); } return new Analytics(application, networkExecutor, integrationManagerFactory, stats, traitsCache, analyticsContext, defaultOptions, logLevel, tag); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy