/* * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.amazonaws.mobileconnectors.pinpoint.analytics; import static com.amazonaws.mobileconnectors.pinpoint.internal.core.util.Preconditions.checkNotNull; import com.amazonaws.mobileconnectors.pinpoint.PinpointConfiguration; import com.amazonaws.mobileconnectors.pinpoint.internal.core.PinpointContext; import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.JSONBuilder; import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.JSONSerializable; import com.amazonaws.mobileconnectors.pinpoint.internal.event.EventRecorder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; /** * AnalyticsClient is the entry point into the Amazon Pinpoint SDK where * {@link AnalyticsEvent} objects are created, recorded, and submitted to the * Amazon Pinpoint Service. <h3>Recording Events</h3> Example: * * <pre class="prettyprint"> * // get the event client from your PinpointManager instance * AnalyticsClient analyticsClient = pinpointManager.getAnalyticsClient(); * * // create and record an event * Event level1CompleteEvent = analyticsClient.createEvent("level1Complete"); * analyticsClient.recordEvent(level1CompleteEvent); * * </pre> * * <h3>Submitting Events</h3> * <p> * The example below demonstrates how to submit events to the Amazon Pinpoint * Service. You have direct control over when events are submitted in * your app. Events are submitted in a background thread. * </p> * Example: * * <pre class="prettyprint"> * // submit events to the website * AnalyticsClient analyticsClient = pinpointManager.getAnalyticsClient(); * analyticsClient.submitEvents(); * </pre> * * Amazon recommends that you call <code>submitEvents</code> once you stop the session. * <p> * Note: This client will store at most 5MiB of event data locally. Past that * events will be dropped. When events successfully submitted, they are removed * from the local database. * </p> */ public class AnalyticsClient implements JSONSerializable { private static final Log log = LogFactory.getLog(AnalyticsClient.class); private static final String ANALYTICS_ENABLED = "isAnalyticsEnabled"; private static final int MAX_EVENT_TYPE_LENGTH = 50; private final PinpointContext context; private final Map<String, String> globalAttributes = new ConcurrentHashMap<String, String>(); private final Map<String, Double> globalMetrics = new ConcurrentHashMap<String, Double>(); private Map<String, String> campaignAttributes = new ConcurrentHashMap<String, String>(); private final Map<String, Map<String, String>> eventTypeAttributes = new ConcurrentHashMap<String, Map<String, String>>(); private final Map<String, Map<String, Double>> eventTypeMetrics = new ConcurrentHashMap<String, Map<String, Double>>(); private String sessionId; private long sessionStartTime; private EventRecorder eventRecorder; public AnalyticsClient(final PinpointContext context) { checkNotNull(context, "A valid pinpointContext must be provided"); this.context = context; eventRecorder = EventRecorder.newInstance(context); } /** * Create an event with the specified eventType. The eventType is a * developer defined String that can be used to distinguish between * different scenarios within an application. Note: You can have at most * 1,500 different eventTypes per app. * * @param eventType the type of event to create * @return an Event with the specified eventType */ public AnalyticsEvent createEvent(String eventType) throws IllegalArgumentException { if (eventType == null) { log.error("Null eventType provided to addGlobalAttribute"); throw new IllegalArgumentException("The eventType passed into create event was null"); } if (eventType.length() > MAX_EVENT_TYPE_LENGTH) { log.error("The event type is too long, the max event type length is " + MAX_EVENT_TYPE_LENGTH + " characters"); throw new IllegalArgumentException("The eventType passed into create event was too long"); } AnalyticsEvent event = createEvent(eventType, sessionStartTime, null, null); for (final Entry<String, String> attr : globalAttributes.entrySet()) { event.addAttribute(attr.getKey(), attr.getValue()); } if (eventTypeAttributes.containsKey(event.getEventType())) { for (final Entry<String, String> attr : eventTypeAttributes.get( event.getEventType()).entrySet()) { event.addAttribute(attr.getKey(), attr.getValue()); } } for (final Entry<String, Double> metric : globalMetrics.entrySet()) { event.addMetric(metric.getKey(), metric.getValue()); } if (eventTypeMetrics.containsKey(event.getEventType())) { for (final Entry<String, Double> metric : eventTypeMetrics.get( event.getEventType()).entrySet()) { event.addMetric(metric.getKey(), metric.getValue()); } } return event; } protected AnalyticsEvent createEvent(String eventType, long sessionStart, Long sessionEnd, Long sessionDuration) { return AnalyticsEvent.newInstance(context, sessionId, sessionStart, sessionEnd, sessionDuration, System.currentTimeMillis(), eventType); } /** * Record the specified event to the local filestore Please note if the * amount of data stored events takes up EXCEEDS 5MiB further recordings * will be dropped * * @param event The event to persist */ public void recordEvent(AnalyticsEvent event) { if (event == null) { log.info("The provided event was null"); return; } AnalyticsEvent recordEvent = AnalyticsEvent.createFromEvent(context, sessionId, System.currentTimeMillis(), event); eventRecorder.recordEvent(recordEvent); } /** * Submit all recorded events. If a submission occurred in the last minute, * this request is ignored. If the device is off line, this is a no-op. See * {@link PinpointConfiguration} * for customizing which Internet connection the SDK can submit on. */ public void submitEvents() { log.info("Submitting events"); eventRecorder.submitEvents(); } /** * Adds the specified attribute to all subsequently created events Note: The * maximum allowed attributes and metrics on a single event is 40. Attempts * to add more may be ignored * * @param attributeName the name of the attribute to add * @param attributeValue the value of the attribute */ public void addGlobalAttribute(String attributeName, String attributeValue) { if (attributeName == null) { log.info("Null attribute name provided to addGlobalAttribute"); return; } if (attributeValue == null) { log.debug("Null attribute value provided to addGlobalAttribute. attribute name:" + attributeName); return; } globalAttributes.put(attributeName, attributeValue); } /** * Adds the specified attribute to all subsequently created events with the * specified event type Note: The maximum allowed attributes and metrics on * a single event is 40. Attempts to add more may be ignored * * @param eventType the type of events to add the attribute to * @param attributeName the name of the attribute to add * @param attributeValue the value of the attribute */ public void addGlobalAttribute(String eventType, String attributeName, String attributeValue) { if (eventType == null) { log.warn("Null eventType provided to addGlobalAttribute"); return; } if (attributeName == null) { log.warn("Null attribute name provided to addGlobalAttribute. eventType:" + eventType); return; } if (attributeValue == null) { log.warn("Null value provided to addGlobalAttribute. eventType:" + eventType + ", attributeName:" + attributeName); return; } Map<String, String> eventAttrs = eventTypeAttributes.get(eventType); if (eventAttrs == null) { eventAttrs = new ConcurrentHashMap<String, String>(); eventTypeAttributes.put(eventType, eventAttrs); } eventAttrs.put(attributeName, attributeValue); } /** * Adds the specified metric to all subsequently created events Note: The * maximum allowed attributes and metrics on a single event is 40. Attempts * to add more may be ignored * * @param metricName the name of the metric to add * @param metricValue the value of the metric */ public void addGlobalMetric(String metricName, Double metricValue) { if (metricName == null) { log.warn("Null metric name provided to addGlobalMetric"); return; } if (metricValue == null) { log.warn("Null metric value provided to addGlobalMetric. metric name:" + metricName); return; } globalMetrics.put(metricName, metricValue); } /** * Adds the specified metric to all subsequently created events with the * specified event type Note: The maximum allowed attributes and metrics on * a single event is 40. Attempts to add more may be ignored * * @param eventType the type of events to add the metric to * @param metricName the name of the metric to add * @param metricValue the value of the metric */ public void addGlobalMetric(String eventType, String metricName, Double metricValue) { if (eventType == null) { log.warn("Null eventType provided to addGlobalMetric"); return; } if (metricName == null) { log.warn("Null metric name provided to addGlobalMetric. eventType:" + eventType); return; } if (metricValue == null) { log.warn("Null metric value provided to addGlobalMetric. eventType:" + eventType + ", metric name:" + metricName); return; } Map<String, Double> eventMetrics = eventTypeMetrics.get(eventType); if (eventMetrics == null) { eventMetrics = new ConcurrentHashMap<String, Double>(); eventTypeMetrics.put(eventType, eventMetrics); } eventMetrics.put(metricName, metricValue); } /** * Removes the specified attribute. All subsequently created events will no * longer have this global attribute. * * @param attributeName the name of the attribute to remove */ public void removeGlobalAttribute(String attributeName) { if (attributeName == null) { log.warn("Null attribute name provided to removeGlobalAttribute"); return; } globalAttributes.remove(attributeName); } /** * Removes the specified attribute. All subsequently created events with the * specified event type will no longer have this global attribute. * * @param eventType the type of events to remove the attribute from * @param attributeName the name of the attribute to remove */ public void removeGlobalAttribute(String eventType, String attributeName) { if (eventType == null) { log.warn("Null eventType provided to removeGlobalAttribute"); return; } if (attributeName == null) { log.warn("Null attribute name provided to removeGlobalAttribute"); return; } final Map<String, String> eventAttrs = eventTypeAttributes.get(eventType); if (eventAttrs != null) { eventAttrs.remove(attributeName); } } /** * Removes the specified metric. All subsequently created events will no * longer have this global metric. * * @param metricName the name of the metric to remove */ public void removeGlobalMetric(String metricName) { if (metricName == null) { log.warn("Null metric name provided to removeGlobalMetric"); return; } globalMetrics.remove(metricName); } /** * Removes the specified metric. All subsequently created events with the * specified event type will no longer have this global metric. * * @param eventType the type of events to remove the metric from * @param metricName the name of the metric to remove */ public void removeGlobalMetric(String eventType, String metricName) { if (eventType == null) { log.warn("Null eventType provided to removeGlobalMetric"); return; } if (metricName == null) { log.warn("Null metric name provided to removeGlobalMetric"); return; } final Map<String, Double> eventMetrics = eventTypeMetrics.get(eventType); if (eventMetrics != null) { eventMetrics.remove(metricName); } } /** * Adds the specified campaign attributes to events to track Campaign Analytic * * You should not use this method as it will be called by the NotificationManager when the app is opened * from a push notification. * * @param campaign the map with campaign attributes of the campaign received */ public void setCampaignAttributes(Map<String, String> campaign) { if (campaign == null) { log.warn("Null campaign attributes provided to setCampaignAttributes"); return; } campaignAttributes = campaign; } /** * Clears campaign attributes * * You should not use this method as it will be called by the NotificationManager when the app is opened * from a push notification. * */ public void clearCampaignAttributes() { for (String key : campaignAttributes.keySet()) { this.removeGlobalAttribute(key); } campaignAttributes.clear(); } public String toString() { JSONObject json = toJSONObject(); try { return json.toString(4); } catch (JSONException e) { return json.toString(); } } public JSONObject toJSONObject() { JSONArray observersJSON = new JSONArray(); JSONArray globalAttrs = new JSONArray(); if (null != globalAttributes) { for (Entry<String, String> entry : globalAttributes.entrySet()) { try { JSONObject attr = new JSONObject(); attr.put(entry.getKey(), entry.getValue()); globalAttrs.put(attr); } catch (JSONException e) { log.error("Error parsing Global Attributes: " + e.toString()); } } } JSONArray globalMets = new JSONArray(); if (null != globalMetrics) { for (Entry<String, Double> entry : globalMetrics.entrySet()) { try { JSONObject attr = new JSONObject(); attr.put(entry.getKey(), entry.getValue()); globalMets.put(attr); } catch (JSONException e) { log.error("Error parsing Global Metrics: " + e.toString()); } } } JSONObject eventTypesAttributesJson = new JSONObject(); if (null != eventTypeAttributes) { for (Entry<String, Map<String, String>> entry : eventTypeAttributes.entrySet()) { JSONArray eventTypeAttrs = new JSONArray(); try { for (Entry<String, String> attrEntry : entry.getValue().entrySet()) { JSONObject attr = new JSONObject(); attr.put(attrEntry.getKey(), attrEntry.getValue()); eventTypeAttrs.put(attr); } eventTypesAttributesJson.put(entry.getKey(), eventTypeAttrs); } catch (JSONException e) { log.error("Error parsing Event Type Attributes: " + e.toString()); } } } JSONObject eventTypesMetricsJson = new JSONObject(); if (null != eventTypeMetrics) { for (Entry<String, Map<String, Double>> entry : eventTypeMetrics.entrySet()) { JSONArray eventTypeMets = new JSONArray(); try { for (Entry<String, Double> attrEntry : entry.getValue().entrySet()) { JSONObject attr = new JSONObject(); attr.put(attrEntry.getKey(), attrEntry.getValue()); eventTypeMets.put(attr); } eventTypesMetricsJson.put(entry.getKey(), eventTypeMets); } catch (JSONException e) { log.error("Error parsing Event Type Metrics: " + e.toString()); } } } return new JSONBuilder(this).withAttribute("uniqueId", context.getUniqueId()) .withAttribute("observers", observersJSON) .withAttribute("globalAttributes", globalAttrs) .withAttribute("globalMetrics", globalMets) .withAttribute("eventTypeAttributes", eventTypesAttributesJson) .withAttribute("eventTypeMetrics", eventTypesMetricsJson) .toJSONObject(); } public void closeDB() { eventRecorder.closeDB(); } public List<JSONObject> getAllEvents() { return eventRecorder.getAllEvents(); } public void setSessionId(String sessionId) { this.sessionId = sessionId; } public void setSessionStartTime(long sessionStartTime) { this.sessionStartTime = sessionStartTime; } public String getSessionId() { return sessionId; } public long getSessionStartTime() { return sessionStartTime; } void setEventRecorder(EventRecorder eventRecorder){ this.eventRecorder = eventRecorder; } }