/*
* 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 com.amazonaws.mobileconnectors.pinpoint.internal.core.PinpointContext;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.system.AndroidAppDetails;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.system.AndroidDeviceDetails;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.JSONBuilder;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.JSONSerializable;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.SDKInfo;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.StringUtil;
import com.amazonaws.mobileconnectors.pinpoint.internal.event.ClientContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Represents the any useful action you wish to record within your application
* <p>
* The example below demonstrates how to record events.
* </p>
*
* <pre class="prettyprint">
* // get the event client from your amazon PinpointManager instance
* EventClient analyticsClient = mobileAnalyticsManager.getAnalyticsClient();
*
* // create and record the view event
* Event level1CompleteEvent = analyticsClient.createEvent("level1Complete");
* analyticsClient.recordEvent(level1CompleteEvent);
*
* // record if the user bought an upgrade (conversion)
* if (userBoughtUpgrade) {
* Event level1UserBoughtUpgradeEvent = analyticsClient.createEvent("level1UserBoughtUpgrade");
* analyticsClient.recordEvent(level1UserBoughtUpgradeEvent);
* }
* </pre>
*/
public class AnalyticsEvent implements JSONSerializable {
private static final Log log =
LogFactory.getLog(AnalyticsEvent.class);
private final String eventId;
private final String eventType;
private final String sdkName;
private final String sdkVersion;
private final PinpointSession session;
private final Map<String, String> attributes = new ConcurrentHashMap<String, String>();
private final Map<String, Double> metrics = new ConcurrentHashMap<String, Double>();
private final Long timestamp;
private final String uniqueId;
private final AndroidAppDetails appDetails;
private final AndroidDeviceDetails deviceDetails;
static final int MAX_EVENT_ATTRIBUTE_METRIC_KEY_LENGTH = 50;
static final int MAX_EVENT_ATTRIBUTE_VALUE_LENGTH = 1000;
static final int MAX_NUM_OF_METRICS_AND_ATTRIBUTES = 50;
private final AtomicInteger currentNumOfAttributesAndMetrics = new AtomicInteger(0);
public static AnalyticsEvent createFromEvent(PinpointContext context, String sessionId,
long timestamp, AnalyticsEvent copyEvent) {
return new AnalyticsEvent(copyEvent.getEventId(), copyEvent.getEventType(), copyEvent.getAllAttributes(),
copyEvent.getAllMetrics(),
context.getSDKInfo(), sessionId, copyEvent.getSession().getSessionStart(),
copyEvent.getSession().getSessionStop(),
copyEvent.getSession().getSessionDuration(), timestamp, context.getUniqueId(),
context.getSystem()
.getAppDetails(), context.getSystem().getDeviceDetails());
}
public static AnalyticsEvent newInstance(PinpointContext context, String sessionId,
Long sessionStart, Long sessionEnd, Long duration, long timestamp,
final String eventType) {
return new AnalyticsEvent(eventType, null, null, context.getSDKInfo(), sessionId,
sessionStart, sessionEnd, duration, timestamp, context.getUniqueId(),
context.getSystem().getAppDetails(), context.getSystem().getDeviceDetails());
}
public static AnalyticsEvent newInstance(final String eventId, final String eventType,
final Map<String, String> attributes, final Map<String, Double> metrics,
final SDKInfo sdkInfo, String sessionId, Long sessionStart, Long sessionStop,
Long sessionDuration, long timestamp, String uniqueId, AndroidAppDetails appDetails,
AndroidDeviceDetails deviceDetails) {
return new AnalyticsEvent(eventId, eventType, attributes, metrics, sdkInfo, sessionId, sessionStart,
sessionStop, sessionDuration, timestamp, uniqueId, appDetails,
deviceDetails);
}
AnalyticsEvent(final String eventType, final Map<String, String> attributes, final Map<String, Double> metrics,
final SDKInfo sdkInfo, String sessionId, long sessionStart, Long sessionEnd, Long sessionDuration,
long timestamp, String uniqueId, AndroidAppDetails appDetails, AndroidDeviceDetails deviceDetails) {
this(UUID.randomUUID().toString(), eventType, attributes, metrics, sdkInfo, sessionId,
sessionStart, sessionEnd, sessionDuration, timestamp, uniqueId, appDetails, deviceDetails);
}
private AnalyticsEvent(final String eventId, final String eventType, final Map<String, String> attributes, final Map<String, Double> metrics,
final SDKInfo sdkInfo, String sessionId, long sessionStart, Long sessionEnd, Long sessionDuration,
long timestamp, String uniqueId, AndroidAppDetails appDetails, AndroidDeviceDetails deviceDetails) {
this.eventId = eventId;
this.sdkName = sdkInfo.getName();
this.sdkVersion = sdkInfo.getVersion();
this.session = new PinpointSession(sessionId, sessionStart, sessionEnd, sessionDuration);
this.timestamp = timestamp;
this.uniqueId = uniqueId;
this.eventType = eventType;
this.appDetails = appDetails;
this.deviceDetails = deviceDetails;
if (null != attributes) {
for (Entry<String, String> kvp : attributes.entrySet()) {
this.addAttribute(kvp.getKey(), kvp.getValue());
}
}
if (null != metrics) {
for (Entry<String, Double> kvp : metrics.entrySet()) {
this.addMetric(kvp.getKey(), kvp.getValue());
}
}
}
public String getEventId(){
return this.eventId;
}
/**
* Adds an attribute to this {@link AnalyticsEvent} with the specified key.
* Only 40 attributes/metrics are allowed to be added to an Event. If 40
* attribute/metrics already exist on this Event, the call may be ignored.
*
* @param name The name of the attribute. The name will be truncated if it
* exceeds 50 characters.
* @param value The value of the attribute. The value will be truncated if
* it exceeds 200 characters.
*/
public void addAttribute(String name, String value) {
if (null == name) {
return;
}
if (null != value) {
if (currentNumOfAttributesAndMetrics.get() < MAX_NUM_OF_METRICS_AND_ATTRIBUTES) {
attributes.put(this.processAttributeMetricKey(name), processAttributeValue(value));
currentNumOfAttributesAndMetrics.incrementAndGet();
} else {
log.warn("Max number of attributes/metrics reached(" + MAX_NUM_OF_METRICS_AND_ATTRIBUTES +
"). The attribute key " + name + " has been ignored.");
}
} else {
attributes.remove(name);
}
}
/**
* Determines if this {@link AnalyticsEvent} contains a specific attribute
*
* @param attributeName The name of the attribute
* @return true if this {@link AnalyticsEvent} has an attribute with the
* specified name, false otherwise
*/
public boolean hasAttribute(String attributeName) {
if (attributeName == null) {
return false;
}
return attributes.containsKey(attributeName);
}
/**
* Adds a metric to this {@link AnalyticsEvent} with the specified key. Only
* 40 attributes/metrics are allowed to be added to an Event. If 50
* attribute/metrics already exist on this Event, the call may be ignored.
*
* @param name The name of the metric. The name will be truncated if it
* exceeds 50 characters.
* @param value The value of the metric.
*/
public void addMetric(String name, Double value) {
if (null == name) {
return;
}
if (null != value) {
if (currentNumOfAttributesAndMetrics.get() < MAX_NUM_OF_METRICS_AND_ATTRIBUTES) {
metrics.put(this.processAttributeMetricKey(name), value);
currentNumOfAttributesAndMetrics.incrementAndGet();
} else {
log.warn("Max number of attributes/metrics reached(" + MAX_NUM_OF_METRICS_AND_ATTRIBUTES +
"). The metric key " + name + " has been ignored.");
}
} else {
metrics.remove(name);
}
}
/**
* Determines if this {@link AnalyticsEvent} contains a specific metric.
*
* @param metricName The name of the metric
* @return true if this {@link AnalyticsEvent} has a metric with the
* specified name, false otherwise
*/
public boolean hasMetric(String metricName) {
if (metricName == null) {
return false;
}
return metrics.containsKey(metricName);
}
/**
* Returns the name/type of this {@link AnalyticsEvent}
*
* @return the name/type of this {@link AnalyticsEvent}
*/
public String getEventType() {
return this.eventType;
}
/**
* Returns the value of the attribute with the specified name.
*
* @param name The name of the attribute to return
* @return The attribute with the specified name, or null if attribute does
* not exist
*/
public String getAttribute(String name) {
if (name == null) {
return null;
}
return attributes.get(name);
}
/**
* Returns the value of the metric with the specified name.
*
* @param name The name of the metric to return
* @return The metric with the specified name, or null if metric does not
* exist
*/
public Double getMetric(String name) {
if (name == null) {
return null;
}
return metrics.get(name);
}
public PinpointSession getSession(){
return session;
}
public Long getEventTimestamp() {
return timestamp;
}
public String getUniqueId() {
return uniqueId;
}
public String getSdkName() {
return sdkName;
}
public String getSdkVersion() {
return sdkVersion;
}
/**
* Adds an attribute to this {@link AnalyticsEvent} with the specified key.
* Only 40 attributes/metrics are allowed to be added to an
* {@link AnalyticsEvent}. If 40 attribute/metrics already exist on this
* {@link AnalyticsEvent}, the call may be ignored.
*
* @param name The name of the attribute. The name will be truncated if it
* exceeds 50 characters.
* @param value The value of the attribute. The value will be truncated if
* it exceeds 200 characters.
* @return The same {@link AnalyticsEvent} instance is returned to allow for
* method chaining.
*/
public AnalyticsEvent withAttribute(String name, String value) {
addAttribute(name, value);
return this;
}
/**
* Adds a metric to this {@link AnalyticsEvent} with the specified key. Only
* 40 attributes/metrics are allowed to be added to an
* {@link AnalyticsEvent}. If 40 attribute/metrics already exist on this
* {@link AnalyticsEvent}, the call may be ignored.
*
* @param name The name of the metric. The name will be truncated if it
* exceeds 50 characters.
* @param value The value of the metric.
* @return The same {@link AnalyticsEvent} instance is returned to allow for
* method chaining.
*/
public AnalyticsEvent withMetric(String name, Double value) {
addMetric(name, value);
return this;
}
/**
* Returns a map of all attributes contained within this
* {@link AnalyticsEvent}
*
* @return a map of all attributes, where the attribute names are the keys
* and the attribute values are the values
*/
public Map<String, String> getAllAttributes() {
return Collections.unmodifiableMap(attributes);
}
/**
* Returns a map of all metrics contained within this {@link AnalyticsEvent}
*
* @return a map of all metrics, where the metric names are the keys and the
* metric values are the values
*/
public Map<String, Double> getAllMetrics() {
return Collections.unmodifiableMap(metrics);
}
@Override
public String toString() {
JSONObject json = toJSONObject();
try {
return json.toString(4);
} catch (JSONException e) {
return json.toString();
}
}
@Override
public JSONObject toJSONObject() {
Locale locale = this.deviceDetails.locale();
String localeString = locale != null ? locale.toString() : "UNKNOWN";
JSONBuilder builder = new JSONBuilder(this);
// ****************************************************
// ==================System Attributes=================
// ****************************************************
builder.withAttribute("event_id", getEventId());
builder.withAttribute("event_type", getEventType());
builder.withAttribute("unique_id", getUniqueId());
builder.withAttribute("timestamp", getEventTimestamp());
// ****************************************************
// ==============Device Details Attributes=============
// ****************************************************
builder.withAttribute("platform", this.deviceDetails.platform());
builder.withAttribute("platform_version", this.deviceDetails.platformVersion());
builder.withAttribute("make", this.deviceDetails.manufacturer());
builder.withAttribute("model", this.deviceDetails.model());
builder.withAttribute("locale", localeString);
builder.withAttribute("carrier", this.deviceDetails.carrier());
// ****************************************************
// ==============Session Attributes=============
// ****************************************************
JSONObject sessionObject = new JSONObject();
try {
sessionObject.put("id", session.getSessionId());
if (session.getSessionStart() != null) {
sessionObject.put("startTimestamp", session.getSessionStart());
}
if (session.getSessionStop() != null) {
sessionObject.put("stopTimestamp", session.getSessionStop());
}
if (session.getSessionDuration() != null) {
sessionObject.put("duration", session.getSessionDuration().longValue());
}
} catch (JSONException e) {
log.error("Error serializing session information", e);
}
builder.withAttribute("session", sessionObject);
// ****************************************************
// ====SDK Details Attributes -- Prefix with 'sdk_'====
// ****************************************************
builder.withAttribute("sdk_version", this.sdkVersion);
builder.withAttribute("sdk_name", this.sdkName);
// ****************************************************
// Application Details Attributes -- Prefix with 'app_'
// ****************************************************
builder.withAttribute("app_version_name", this.appDetails.versionName());
builder.withAttribute("app_version_code", this.appDetails.versionCode());
builder.withAttribute("app_package_name", this.appDetails.packageName());
builder.withAttribute("app_title", this.appDetails.getAppTitle());
builder.withAttribute(ClientContext.APP_ID_KEY, this.appDetails.getAppId());
JSONObject attributesJson = new JSONObject();
for (Entry<String, String> entry : getAllAttributes().entrySet()) {
try {
attributesJson.put(entry.getKey(), entry.getValue());
} catch (JSONException e) {
}
}
JSONObject metricsJson = new JSONObject();
for (Entry<String, Double> entry : getAllMetrics().entrySet()) {
try {
metricsJson.put(entry.getKey(), entry.getValue());
} catch (JSONException e) {
log.error("error serializing metric. key:'" + entry.getKey() + "', value: "
+ entry.getValue().toString(), e);
}
}
// If there are any attributes put then add the attributes to the
// structure
if (attributesJson.length() > 0) {
builder.withAttribute("attributes", attributesJson);
}
// If there are any metrics put then add the attributes to the structure
if (metricsJson.length() > 0) {
builder.withAttribute("metrics", metricsJson);
}
return builder.toJSONObject();
}
public ClientContext createClientContext(String networkType) {
ClientContext.ClientContextBuilder builder = new ClientContext.ClientContextBuilder();
builder.withAppPackageName(appDetails.packageName())
.withAppVersionCode(appDetails.versionCode())
.withAppVersionName(appDetails.versionName())
.withLocale(deviceDetails.locale().toString())
.withMake(deviceDetails.manufacturer()).withModel(deviceDetails.model())
.withPlatformVersion(deviceDetails.platformVersion())
.withUniqueId(uniqueId)
.withAppTitle(appDetails.getAppTitle()).withNetworkType(networkType)
.withCarrier(deviceDetails.carrier())
.withAppId(appDetails.getAppId());
return builder.build();
}
private static String processAttributeMetricKey(String key) {
String trimmedKey = StringUtil
.clipString(key, MAX_EVENT_ATTRIBUTE_METRIC_KEY_LENGTH, false);
if (trimmedKey.length() < key.length()) {
log.warn("The attribute key has been trimmed to a length of "
+ MAX_EVENT_ATTRIBUTE_METRIC_KEY_LENGTH + " characters");
}
return trimmedKey;
}
private static String processAttributeValue(String value) {
String trimmedValue = StringUtil.clipString(value, MAX_EVENT_ATTRIBUTE_VALUE_LENGTH, false);
if (trimmedValue.length() < value.length()) {
log.warn("The attribute value has been trimmed to a length of "
+ MAX_EVENT_ATTRIBUTE_VALUE_LENGTH + " characters");
}
return trimmedValue;
}
public static JSONObject translateFromEvent(AnalyticsEvent source) {
if (null == source) {
log.warn("The Event provided was null");
return new JSONObject();
}
JSONObject json = source.toJSONObject();
if (json.has("class")) {
json.remove("class");
}
if (json.has("hashCode")) {
json.remove("hashCode");
}
return json;
}
public static AnalyticsEvent translateToEvent(JSONObject source) throws JSONException {
Map<String, String> attributes = new HashMap<String, String>();
Map<String, Double> metrics = new HashMap<String, Double>();
AndroidAppDetails appDetails = new AndroidAppDetails(source.optString("app_package_name"),
source.optString("app_version_code"), source.optString("app_version_name"),
source.optString("app_title"), source.optString(ClientContext.APP_ID_KEY));
SDKInfo sdkInfo = new SDKInfo(source.optString("sdk_version"), source.optString("sdk_name"));
AndroidDeviceDetails deviceDetails = new AndroidDeviceDetails(source.optString("carrier"));
String eventId = source.getString("event_id");
String eventType = source.getString("event_type");
Long timestamp = source.getLong("timestamp");
String uniqueId = source.getString("unique_id");
String sessionId = "";
Long sessionStart = null;
Long sessionStop = null;
Long sessionDuration = null;
JSONObject sessionJSON = source.getJSONObject("session");
if (sessionJSON != null) {
sessionId = sessionJSON.getString("id");
sessionStart = sessionJSON.getLong("startTimestamp");
sessionStop = sessionJSON.optLong("stopTimestamp");
sessionDuration = sessionJSON.optLong("duration");
}
JSONObject attributesJSON = source.optJSONObject("attributes");
if (attributesJSON != null) {
Iterator<String> keysIterator = attributesJSON.keys();
String key;
while (keysIterator.hasNext()) {
key = keysIterator.next();
attributes.put(key, attributesJSON.optString(key));
}
}
JSONObject metricsJSON = source.optJSONObject("metrics");
if (metricsJSON != null) {
Iterator<String> keysIterator = metricsJSON.keys();
String key;
while (keysIterator.hasNext()) {
key = keysIterator.next();
try {
metrics.put(key, metricsJSON.getDouble(key));
} catch (JSONException e) {
log.error("Failed to convert metric back to double from JSON value", e);
}
}
}
return AnalyticsEvent.newInstance(eventId, eventType, attributes, metrics, sdkInfo, sessionId,
sessionStart, sessionStop, sessionDuration, timestamp, uniqueId, appDetails,
deviceDetails);
}
}