Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sensorsdata.analytics.javasdk.SensorsAnalytics Maven / Gradle / Ivy
package com.sensorsdata.analytics.javasdk;
import com.sensorsdata.analytics.javasdk.exceptions.DebugModeException;
import com.sensorsdata.analytics.javasdk.exceptions.InvalidArgumentException;
import com.sensorsdata.analytics.javasdk.util.Base64Coder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
/**
* Sensors Analytics SDK
*/
public class SensorsAnalytics {
private boolean enableTimeFree = false;
public boolean isEnableTimeFree() {
return enableTimeFree;
}
public void setEnableTimeFree(boolean enableTimeFree) {
this.enableTimeFree = enableTimeFree;
}
public interface Consumer {
void send(Map message);
void flush();
void close();
}
public static class DebugConsumer implements Consumer {
public DebugConsumer(final String serverUrl, final boolean writeData) {
String debugUrl = null;
try {
// 将 URI Path 替换成 Debug 模式的 '/debug'
URIBuilder builder = new URIBuilder(new URI(serverUrl));
String[] urlPathes = builder.getPath().split("/");
urlPathes[urlPathes.length - 1] = "debug";
builder.setPath(strJoin(urlPathes, "/"));
debugUrl = builder.build().toURL().toString();
} catch (URISyntaxException e) {
throw new DebugModeException(e);
} catch (MalformedURLException e) {
throw new DebugModeException(e);
}
Map headers = new HashMap();
if (!writeData) {
headers.put("Dry-Run", "true");
}
this.httpConsumer = new HttpConsumer(debugUrl, headers);
this.jsonMapper = getJsonObjectMapper();
}
@Override public void send(Map message) {
// XXX: HttpConsumer 只处理了 Message List 的发送?
List> messageList = new ArrayList>();
messageList.add(message);
String sendingData = null;
try {
sendingData = jsonMapper.writeValueAsString(messageList);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize data.", e);
}
System.out
.println("==========================================================================");
try {
synchronized (httpConsumer) {
httpConsumer.consume(sendingData);
}
System.out.println(String.format("valid message: %s", sendingData));
} catch (IOException e) {
throw new DebugModeException("Failed to send message with DebugConsumer.", e);
} catch (HttpConsumer.HttpConsumerException e) {
System.out.println(String.format("invalid message: %s", e.getSendingData()));
System.out.println(String.format("http status code: %d", e.getHttpStatusCode()));
System.out.println(String.format("http content: %s", e.getHttpContent()));
throw new DebugModeException(e);
}
}
@Override public void flush() {
// do NOTHING
}
@Override public void close() {
httpConsumer.close();
}
final HttpConsumer httpConsumer;
final ObjectMapper jsonMapper;
}
public static class BatchConsumer implements Consumer {
public BatchConsumer(final String serverUrl) {
this(serverUrl, MAX_FLUSH_BULK_SIZE);
}
public BatchConsumer(final String serverUrl, final int bulkSize) {
this(serverUrl, bulkSize, false);
}
public BatchConsumer(final String serverUrl, final int bulkSize, final boolean throwException) {
this.messageList = new LinkedList>();
this.httpConsumer = new HttpConsumer(serverUrl, null);
this.jsonMapper = getJsonObjectMapper();
this.bulkSize = Math.min(MAX_FLUSH_BULK_SIZE, bulkSize);
this.throwException = throwException;
}
@Override public void send(Map message) {
synchronized (messageList) {
messageList.add(message);
if (messageList.size() >= bulkSize) {
flush();
}
}
}
@Override public void flush() {
synchronized (messageList) {
while (!messageList.isEmpty()) {
String sendingData = null;
List> sendList =
messageList.subList(0, Math.min(bulkSize, messageList.size()));
try {
sendingData = jsonMapper.writeValueAsString(sendList);
} catch (JsonProcessingException e) {
sendList.clear();
if (throwException) {
throw new RuntimeException("Failed to serialize data.", e);
}
continue;
}
try {
this.httpConsumer.consume(sendingData);
sendList.clear();
} catch (Exception e) {
if (throwException) {
throw new RuntimeException("Failed to dump message with BatchConsumer.", e);
}
return;
}
}
}
}
@Override public void close() {
flush();
httpConsumer.close();
}
private static final int MAX_FLUSH_BULK_SIZE = 50;
private final List> messageList;
private final HttpConsumer httpConsumer;
private final ObjectMapper jsonMapper;
private final int bulkSize;
private final boolean throwException;
}
@Deprecated
public interface AsyncBatchConsumerCallback {
void onFlushTask(Future task);
}
/**
* @deprecated Async模式下,开发者需要仔细处理缓存中的数据,如由于异步发送不及时导致缓存队列过大、程序停止时缓
* 存队列清空等问题。因此我们建议开发者使用 LoggingConsumer 结合 LogAgent 工具导入数据。
*/
@Deprecated
public static class AsyncBatchConsumer implements Consumer {
public AsyncBatchConsumer(final String serverUrl, final int bulkSize,
final ThreadPoolExecutor executor, final AsyncBatchConsumerCallback callback) {
this.messageList = new ArrayList>();
this.httpConsumer = new HttpConsumer(serverUrl, null);
this.jsonMapper = getJsonObjectMapper();
this.bulkSize = Math.min(MAX_FLUSH_BULK_SIZE, bulkSize);
this.executor = executor;
this.callback = callback;
}
@Override public void send(Map message) {
synchronized (messageList) {
messageList.add(message);
if (messageList.size() >= bulkSize) {
flush();
}
}
}
@Override public void flush() {
synchronized (messageList) {
try {
final String sendingData = jsonMapper.writeValueAsString(messageList);
final Future task = executor.submit(new Callable() {
@Override public Boolean call() throws Exception {
int reties = 0;
while (reties < 5) {
try {
httpConsumer.consume(sendingData);
break;
} catch (IOException e) {
// XXX: 发生错误时,默认1秒后才重试
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
reties = reties + 1;
} catch (HttpConsumer.HttpConsumerException e) {
// XXX: 发生错误时,默认1秒后才重试
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
reties = reties + 1;
}
}
return reties < 5;
}
});
if (callback != null) {
callback.onFlushTask(task);
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
} finally {
messageList.clear();
}
}
}
@Override public void close() {
flush();
httpConsumer.close();
executor.shutdown();
try {
executor.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static final int MAX_FLUSH_BULK_SIZE = 50;
private final List> messageList;
private final HttpConsumer httpConsumer;
private final ObjectMapper jsonMapper;
private final ThreadPoolExecutor executor;
private final AsyncBatchConsumerCallback callback;
private final int bulkSize;
}
public static class ConsoleConsumer implements Consumer {
public ConsoleConsumer(final Writer writer) {
this.jsonMapper = getJsonObjectMapper();
this.writer = writer;
}
@Override public void send(Map message) {
try {
synchronized (writer) {
writer.write(jsonMapper.writeValueAsString(message));
writer.write("\n");
}
} catch (IOException e) {
throw new RuntimeException("Failed to dump message with ConsoleConsumer.", e);
}
}
@Override public void flush() {
synchronized (writer) {
try {
writer.flush();
} catch (IOException e) {
throw new RuntimeException("Failed to flush with ConsoleConsumer.", e);
}
}
}
@Override public void close() {
flush();
}
private final ObjectMapper jsonMapper;
private final Writer writer;
}
@Deprecated public static class LoggingConsumer extends InnerLoggingConsumer {
public LoggingConsumer(final String filenamePrefix) throws IOException {
this(filenamePrefix, 8192);
}
public LoggingConsumer(final String filenamePrefix, int bufferSize) throws IOException {
super(new LoggingFileWriterFactory() {
@Override public LoggingFileWriter getFileWriter(String fileName, String scheduleFileName)
throws FileNotFoundException {
return new LoggingConsumer.InnerLoggingFileWriter(fileName, scheduleFileName);
}
@Override public void closeFileWriter(LoggingFileWriter writer) {
writer.close();
}
}, filenamePrefix, bufferSize);
}
static class InnerLoggingFileWriter implements LoggingFileWriter {
private final String fileName;
private File outputFile;
private FileOutputStream outputStream;
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
InnerLoggingFileWriter(final String fileName, final String scheduleFileName) throws
FileNotFoundException {
this.outputFile = new File(fileName);
this.fileName = scheduleFileName;
if (this.outputFile.exists()) {
String realFileName = fileName + "." + simpleDateFormat.format(this.outputFile
.lastModified());
if (!realFileName.equals(this.fileName)) {
File target = new File(realFileName);
int count = 0;
while (target.exists()) {
count += 1;
target = new File(realFileName + "." + count);
}
if (!this.outputFile.renameTo(target)) {
throw new RuntimeException(
"Failed to rename [" + this.outputFile.getName() + "] to [" +
target.getName() + "]");
}
this.outputFile = new File(fileName);
}
}
this.outputStream = new FileOutputStream(this.outputFile, true);
}
public void close() {
try {
outputStream.close();
} catch (Exception e) {
throw new RuntimeException("fail to close output stream.", e);
}
}
public boolean isValid(final String fileName) {
return this.fileName.equals(fileName);
}
public boolean write(final StringBuilder sb) {
FileLock lock = null;
try {
final FileChannel channel = outputStream.getChannel();
lock = channel.lock(0, Long.MAX_VALUE, false);
outputStream.write(sb.toString().getBytes("UTF-8"));
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (lock != null) {
try {
lock.release();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return true;
}
}
}
public static class ConcurrentLoggingConsumer extends InnerLoggingConsumer {
public ConcurrentLoggingConsumer(final String filenamePrefix) throws IOException {
this(filenamePrefix, null);
}
public ConcurrentLoggingConsumer(final String filenamePrefix, int bufferSize) throws IOException {
this(filenamePrefix, null, bufferSize);
}
public ConcurrentLoggingConsumer(final String filenamePrefix, final String lockFileName) throws IOException {
this(filenamePrefix, lockFileName, 8192);
}
public ConcurrentLoggingConsumer(
String filenamePrefix,
String lockFileName,
int bufferSize) throws IOException {
super(new InnerLoggingFileWriterFactory(lockFileName), filenamePrefix, bufferSize);
}
static class InnerLoggingFileWriterFactory implements LoggingFileWriterFactory {
private String lockFileName;
InnerLoggingFileWriterFactory(String lockFileName) {
this.lockFileName = lockFileName;
}
@Override public LoggingFileWriter getFileWriter(String fileName, String scheduleFileName)
throws FileNotFoundException {
return InnerLoggingFileWriter.getInstance(scheduleFileName, lockFileName);
}
@Override public void closeFileWriter(LoggingFileWriter writer) {
ConcurrentLoggingConsumer.InnerLoggingFileWriter
.removeInstance((ConcurrentLoggingConsumer.InnerLoggingFileWriter) writer);
}
}
static class InnerLoggingFileWriter implements LoggingFileWriter {
private final String fileName;
private final FileOutputStream outputStream;
private final FileOutputStream lockStream;
private int refCount;
private static final Map instances;
static {
instances = new HashMap();
}
static InnerLoggingFileWriter getInstance(final String fileName, final String lockFileName) throws FileNotFoundException {
synchronized (instances) {
if (!instances.containsKey(fileName)) {
instances.put(fileName, new InnerLoggingFileWriter(fileName, lockFileName));
}
InnerLoggingFileWriter writer = instances.get(fileName);
writer.refCount = writer.refCount + 1;
return writer;
}
}
static void removeInstance(final InnerLoggingFileWriter writer) {
synchronized (instances) {
writer.refCount = writer.refCount - 1;
if (writer.refCount == 0) {
writer.close();
instances.remove(writer.fileName);
}
}
}
private InnerLoggingFileWriter(final String fileName, final String lockFileName) throws FileNotFoundException {
this.outputStream = new FileOutputStream(fileName, true);
if (lockFileName != null) {
this.lockStream = new FileOutputStream(lockFileName, true);
} else {
this.lockStream = this.outputStream;
}
this.fileName = fileName;
this.refCount = 0;
}
public void close() {
try {
outputStream.close();
} catch (Exception e) {
throw new RuntimeException("fail to close output stream.", e);
}
}
public boolean isValid(final String fileName) {
return this.fileName.equals(fileName);
}
public boolean write(final StringBuilder sb) {
synchronized (this.lockStream) {
FileLock lock = null;
try {
final FileChannel channel = lockStream.getChannel();
lock = channel.lock(0, Long.MAX_VALUE, false);
outputStream.write(sb.toString().getBytes("UTF-8"));
} catch (Exception e) {
throw new RuntimeException("fail to write file.", e);
} finally {
if (lock != null) {
try {
lock.release();
} catch (IOException e) {
throw new RuntimeException("fail to release file lock.", e);
}
}
}
}
return true;
}
}
}
interface LoggingFileWriter {
boolean isValid(final String fileName);
boolean write(final StringBuilder sb);
void close();
}
interface LoggingFileWriterFactory {
LoggingFileWriter getFileWriter(final String fileName, final String scheduleFileName)
throws FileNotFoundException;
void closeFileWriter(LoggingFileWriter writer);
}
static class InnerLoggingConsumer implements Consumer {
private static final int BUFFER_LIMITATION = 1 * 1024 * 1024 * 1024; // 1G
private final ObjectMapper jsonMapper;
private final String filenamePrefix;
private final StringBuilder messageBuffer;
private final int bufferSize;
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private final LoggingFileWriterFactory fileWriterFactory;
private LoggingFileWriter fileWriter;
public InnerLoggingConsumer(
LoggingFileWriterFactory fileWriterFactory,
String filenamePrefix,
int bufferSize) throws IOException {
this.fileWriterFactory = fileWriterFactory;
this.filenamePrefix = filenamePrefix;
this.jsonMapper = getJsonObjectMapper();
this.messageBuffer = new StringBuilder(bufferSize);
this.bufferSize = bufferSize;
}
@Override public synchronized void send(Map message) {
if (messageBuffer.length() < BUFFER_LIMITATION) {
try {
messageBuffer.append(jsonMapper.writeValueAsString(message));
messageBuffer.append("\n");
} catch (JsonProcessingException e) {
throw new RuntimeException("fail to process json", e);
}
} else {
throw new RuntimeException("logging buffer exceeded the allowed limitation.");
}
if (messageBuffer.length() >= bufferSize) {
flush();
}
}
private String constructFileName(Date now) {
return filenamePrefix + "." + simpleDateFormat.format(now);
}
@Override public synchronized void flush() {
if (messageBuffer.length() == 0) {
return;
}
String filename = constructFileName(new Date());
if (fileWriter != null && !fileWriter.isValid(filename)) {
this.fileWriterFactory.closeFileWriter(fileWriter);
fileWriter = null;
}
if (fileWriter == null) {
try {
fileWriter = this.fileWriterFactory.getFileWriter(filenamePrefix, filename);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
if (fileWriter.write(messageBuffer)) {
messageBuffer.setLength(0);
}
}
@Override public synchronized void close() {
flush();
if (fileWriter != null) {
this.fileWriterFactory.closeFileWriter(fileWriter);
fileWriter = null;
}
}
}
public SensorsAnalytics(final Consumer consumer) {
this.consumer = consumer;
this.superProperties = new ConcurrentHashMap();
clearSuperProperties();
}
/**
* 设置每个事件都带有的一些公共属性
*
* 当track的Properties,superProperties和SDK自动生成的automaticProperties有相同的key时,遵循如下的优先级:
* track.properties 高于 superProperties 高于 automaticProperties
*
* 另外,当这个接口被多次调用时,是用新传入的数据去merge先前的数据
*
* 例如,在调用接口前,dict是 {"a":1, "b": "bbb"},传入的dict是 {"b": 123, "c": "asd"},则merge后
* 的结果是 {"a":1, "b": 123, "c": "asd"}
*
* @param superPropertiesMap 一个或多个公共属性
*/
public void registerSuperProperties(Map superPropertiesMap) {
for (Map.Entry item : superPropertiesMap.entrySet()) {
this.superProperties.put(item.getKey(), item.getValue());
}
}
/**
* 清除公共属性
*/
public void clearSuperProperties() {
this.superProperties.clear();
this.superProperties.put("$lib", "Java");
this.superProperties.put("$lib_version", SDK_VERSION);
}
/**
* 记录一个没有任何属性的事件
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param eventName 事件名称
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void track(String distinctId, boolean isLoginId, String eventName)
throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "track", eventName, null);
}
/**
* 记录一个拥有一个或多个属性的事件。属性取值可接受类型为{@link Number}, {@link String}, {@link Date}和
* {@link List};
* 若属性包含 $time 字段,则它会覆盖事件的默认时间属性,该字段只接受{@link Date}类型;
* 若属性包含 $project 字段,则它会指定事件导入的项目;
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param eventName 事件名称
* @param properties 事件的属性
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void track(String distinctId, boolean isLoginId, String eventName, Map properties)
throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "track", eventName, properties);
}
/**
* 记录用户注册事件
*
* 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:
* http://www.sensorsdata.cn/manual/track_signup.html
* 并在必要时联系我们的技术支持人员。
*
* @param loginId 登录 ID
* @param anonymousId 匿名 ID
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void trackSignUp(String loginId, String anonymousId)
throws InvalidArgumentException {
addEvent(loginId, false, anonymousId, "track_signup", "$SignUp", null);
}
/**
* 记录用户注册事件
*
* 这个接口是一个较为复杂的功能,请在使用前先阅读相关说明:
* http://www.sensorsdata.cn/manual/track_signup.html
* 并在必要时联系我们的技术支持人员。
*
* 属性取值可接受类型为{@link Number}, {@link String}, {@link Date}和{@link List};
* 若属性包含 $time 字段,它会覆盖事件的默认时间属性,该字段只接受{@link Date}类型;
* 若属性包含 $project 字段,则它会指定事件导入的项目;
*
* @param loginId 登录 ID
* @param anonymousId 匿名 ID
* @param properties 事件的属性
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void trackSignUp(String loginId, String anonymousId,
Map properties) throws InvalidArgumentException {
addEvent(loginId, false, anonymousId, "track_signup", "$SignUp", properties);
}
/**
* 设置用户的属性。属性取值可接受类型为{@link Number}, {@link String}, {@link Date}和{@link List};
*
* 如果要设置的properties的key,之前在这个用户的profile中已经存在,则覆盖,否则,新创建
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param properties 用户的属性
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileSet(String distinctId, boolean isLoginId, Map properties)
throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "profile_set", null, properties);
}
/**
* 设置用户的属性。这个接口只能设置单个key对应的内容,同样,如果已经存在,则覆盖,否则,新创建
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param property 属性名称
* @param value 属性的值
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileSet(String distinctId, boolean isLoginId, String property, Object value)
throws InvalidArgumentException {
Map properties = new HashMap();
properties.put(property, value);
addEvent(distinctId, isLoginId, null, "profile_set", null, properties);
}
/**
* 首次设置用户的属性。
* 属性取值可接受类型为{@link Number}, {@link String}, {@link Date}和{@link List};
*
* 与profileSet接口不同的是:
* 如果要设置的properties的key,在这个用户的profile中已经存在,则不处理,否则,新创建
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param properties 用户的属性
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileSetOnce(String distinctId, boolean isLoginId, Map properties)
throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "profile_set_once", null, properties);
}
/**
* 首次设置用户的属性。这个接口只能设置单个key对应的内容。
* 与profileSet接口不同的是,如果key的内容之前已经存在,则不处理,否则,重新创建
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param property 属性名称
* @param value 属性的值
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileSetOnce(String distinctId, boolean isLoginId, String property, Object value)
throws InvalidArgumentException {
Map properties = new HashMap();
properties.put(property, value);
addEvent(distinctId, isLoginId, null, "profile_set_once", null, properties);
}
/**
* 为用户的一个或多个数值类型的属性累加一个数值,若该属性不存在,则创建它并设置默认值为0。属性取值只接受
* {@link Number}类型
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param properties 用户的属性
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileIncrement(String distinctId, boolean isLoginId, Map properties)
throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "profile_increment", null, properties);
}
/**
* 为用户的数值类型的属性累加一个数值,若该属性不存在,则创建它并设置默认值为0
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param property 属性名称
* @param value 属性的值
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileIncrement(String distinctId, boolean isLoginId, String property, long value)
throws InvalidArgumentException {
Map properties = new HashMap();
properties.put(property, value);
addEvent(distinctId, isLoginId, null, "profile_increment", null, properties);
}
/**
* 为用户的一个或多个数组类型的属性追加字符串,属性取值类型必须为 {@link java.util.List},且列表中元素的类型
* 必须为 {@link java.lang.String}
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param properties 用户的属性
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileAppend(String distinctId, boolean isLoginId, Map properties)
throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "profile_append", null, properties);
}
/**
* 为用户的数组类型的属性追加一个字符串
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param property 属性名称
* @param value 属性的值
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileAppend(String distinctId, boolean isLoginId, String property, String value)
throws InvalidArgumentException {
List values = new ArrayList();
values.add(value);
Map properties = new HashMap();
properties.put(property, values);
addEvent(distinctId, isLoginId, null, "profile_append", null, properties);
}
/**
* 删除用户某一个属性
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
* @param property 属性名称
*
* @throws InvalidArgumentException eventName 或 properties 不符合命名规范和类型规范时抛出该异常
*/
public void profileUnset(String distinctId, boolean isLoginId, String property)
throws InvalidArgumentException {
Map properties = new HashMap();
properties.put(property, true);
addEvent(distinctId, isLoginId, null, "profile_unset", null, properties);
}
/**
* 删除用户所有属性
*
* @param distinctId 用户 ID
* @param isLoginId 用户 ID 是否是登录 ID,false 表示该 ID 是一个匿名 ID
*
* @throws InvalidArgumentException distinctId 不符合命名规范时抛出该异常
*/
public void profileDelete(String distinctId, boolean isLoginId) throws InvalidArgumentException {
addEvent(distinctId, isLoginId, null, "profile_delete", null, new HashMap());
}
/**
* 设置 item
*
* @param itemType item 类型
* @param itemId item ID
* @param properties item 相关属性
* @throws InvalidArgumentException 取值不符合规范抛出该异常
*/
public void itemSet(String itemType, String itemId, Map properties)
throws InvalidArgumentException {
addItem(itemType, itemId, "item_set", properties);
}
/**
* 删除 item
*
* @param itemType item 类型
* @param itemId item ID
* @throws InvalidArgumentException 取值不符合规范抛出该异常
*/
public void itemDelete(String itemType, String itemId)
throws InvalidArgumentException {
addItem(itemType, itemId, "item_delete", null);
}
/**
* 立即发送缓存中的所有日志
*/
public void flush() {
this.consumer.flush();
}
/**
* 停止SensorsDataAPI所有线程,API停止前会清空所有本地数据
*/
public void shutdown() {
this.consumer.close();
}
private static class HttpConsumer implements Closeable {
static class HttpConsumerException extends Exception {
HttpConsumerException(String error, String sendingData, int httpStatusCode, String
httpContent) {
super(error);
this.sendingData = sendingData;
this.httpStatusCode = httpStatusCode;
this.httpContent = httpContent;
}
String getSendingData() {
return sendingData;
}
int getHttpStatusCode() {
return httpStatusCode;
}
String getHttpContent() {
return httpContent;
}
final String sendingData;
final int httpStatusCode;
final String httpContent;
}
HttpConsumer(String serverUrl, Map httpHeaders) {
this.serverUrl = serverUrl.trim();
this.httpHeaders = httpHeaders;
this.compressData = true;
}
synchronized void consume(final String data) throws IOException, HttpConsumerException {
HttpUriRequest request = getHttpRequest(data);
CloseableHttpResponse response = null;
if (httpClient == null) {
httpClient = HttpClients.custom().setUserAgent("SensorsAnalytics Java SDK " + SDK_VERSION).build();
}
try {
response = httpClient.execute(request);
int httpStatusCode = response.getStatusLine().getStatusCode();
if (httpStatusCode < 200 || httpStatusCode >= 300) {
String httpContent = new String(EntityUtils.toByteArray(response.getEntity()), "UTF-8");
throw new HttpConsumerException(
String.format("Unexpected response %d from Sensors Analytics: %s", httpStatusCode, httpContent), data,
httpStatusCode, httpContent);
}
} finally {
if (response != null) {
response.close();
}
}
}
HttpUriRequest getHttpRequest(final String data) throws IOException {
HttpPost httpPost = new HttpPost(this.serverUrl);
httpPost.setEntity(getHttpEntry(data));
if (this.httpHeaders != null) {
for (Map.Entry entry : this.httpHeaders.entrySet()) {
httpPost.addHeader(entry.getKey(), entry.getValue());
}
}
return httpPost;
}
UrlEncodedFormEntity getHttpEntry(final String data) throws IOException {
byte[] bytes = data.getBytes(Charset.forName("UTF-8"));
List nameValuePairs = new ArrayList();
if (compressData) {
ByteArrayOutputStream os = new ByteArrayOutputStream(bytes.length);
GZIPOutputStream gos = new GZIPOutputStream(os);
gos.write(bytes);
gos.close();
byte[] compressed = os.toByteArray();
os.close();
nameValuePairs.add(new BasicNameValuePair("gzip", "1"));
nameValuePairs.add(new BasicNameValuePair("data_list", new String(Base64Coder.encode
(compressed))));
} else {
nameValuePairs.add(new BasicNameValuePair("gzip", "0"));
nameValuePairs.add(new BasicNameValuePair("data_list", new String(Base64Coder.encode
(bytes))));
}
return new UrlEncodedFormEntity(nameValuePairs);
}
@Override public synchronized void close() {
try {
if (httpClient != null) {
httpClient.close();
httpClient = null;
}
} catch (IOException ignored) {
// do nothing
}
}
CloseableHttpClient httpClient;
final String serverUrl;
final Map httpHeaders;
final boolean compressData;
}
private void addEvent(String distinctId, boolean isLoginId, String originDistinceId,
String actionType, String eventName, Map properties)
throws InvalidArgumentException {
assertKey("Distinct Id", distinctId);
assertProperties(actionType, properties);
if (actionType.equals("track")) {
assertKeyWithRegex("Event Name", eventName);
} else if (actionType.equals("track_signup")) {
assertKey("Original Distinct Id", originDistinceId);
}
// Event time
long time = System.currentTimeMillis();
if (properties != null && properties.containsKey("$time")) {
Date eventTime = (Date) properties.get("$time");
properties.remove("$time");
time = eventTime.getTime();
}
String eventProject = null;
if (properties != null && properties.containsKey("$project")) {
eventProject = (String) properties.get("$project");
properties.remove("$project");
}
Map eventProperties = new HashMap();
if (actionType.equals("track") || actionType.equals("track_signup")) {
eventProperties.putAll(superProperties);
}
if (properties != null) {
eventProperties.putAll(properties);
}
if (isLoginId) {
eventProperties.put("$is_login_id", true);
}
Map libProperties = getLibProperties();
Map event = new HashMap();
event.put("type", actionType);
event.put("time", time);
event.put("distinct_id", distinctId);
event.put("properties", eventProperties);
event.put("lib", libProperties);
if (eventProject != null) {
event.put("project", eventProject);
}
if (enableTimeFree) {
event.put("time_free", true);
}
if (actionType.equals("track")) {
event.put("event", eventName);
} else if (actionType.equals("track_signup")) {
event.put("event", eventName);
event.put("original_id", originDistinceId);
}
this.consumer.send(event);
}
private void addItem(String itemType, String itemId, String actionType,
Map properties) throws InvalidArgumentException {
assertKeyWithRegex("Item Type", itemType);
assertKey("Item Id", itemId);
assertProperties(actionType, properties);
String eventProject = null;
if (properties != null && properties.containsKey("$project")) {
eventProject = (String) properties.get("$project");
properties.remove("$project");
}
Map eventProperties = new HashMap();
if (properties != null) {
eventProperties.putAll(properties);
}
Map libProperties = getLibProperties();
Map record = new HashMap();
record.put("type", actionType);
record.put("time", System.currentTimeMillis());
record.put("properties", eventProperties);
record.put("lib", libProperties);
if (eventProject != null) {
record.put("project", eventProject);
}
record.put("item_type", itemType);
record.put("item_id", itemId);
this.consumer.send(record);
}
private Map getLibProperties() {
Map libProperties = new HashMap();
libProperties.put("$lib", "Java");
libProperties.put("$lib_version", SDK_VERSION);
libProperties.put("$lib_method", "code");
if (this.superProperties.containsKey("$app_version")) {
libProperties.put("$app_version", (String) this.superProperties.get("$app_version"));
}
StackTraceElement[] trace = (new Exception()).getStackTrace();
if (trace.length > 3) {
StackTraceElement traceElement = trace[3];
libProperties.put("$lib_detail", String.format("%s##%s##%s##%s", traceElement.getClassName(),
traceElement.getMethodName(), traceElement.getFileName(), traceElement.getLineNumber()));
}
return libProperties;
}
private void assertKey(String type, String key) throws InvalidArgumentException {
if (key == null || key.length() < 1) {
throw new InvalidArgumentException("The " + type + " is empty.");
}
if (key.length() > 255) {
throw new InvalidArgumentException("The " + type + " is too long, max length is 255.");
}
}
private void assertKeyWithRegex(String type, String key) throws InvalidArgumentException {
assertKey(type, key);
if (!(KEY_PATTERN.matcher(key).matches())) {
throw new InvalidArgumentException("The " + type + "'" + key + "' is invalid.");
}
}
private void assertProperties(String eventType, Map properties)
throws InvalidArgumentException {
if (null == properties) {
return;
}
for (Map.Entry property : properties.entrySet()) {
if (property.getKey().equals("$is_login_id")) {
if (!(property.getValue() instanceof Boolean)) {
throw new InvalidArgumentException("The property value of '$is_login_id' should be "
+ "Boolean.");
}
continue;
}
assertKeyWithRegex("property", property.getKey());
if (!(property.getValue() instanceof Number) && !(property.getValue() instanceof Date) && !
(property.getValue() instanceof String) && !(property.getValue() instanceof Boolean) &&
!(property.getValue() instanceof List>)) {
throw new InvalidArgumentException("The property '" + property.getKey() + "' should be a basic type: "
+ "Number, String, Date, Boolean, List.");
}
if (property.getKey().equals("$time") && !(property.getValue() instanceof Date)) {
throw new InvalidArgumentException(
"The property '$time' should be a java.util.Date.");
}
// List 类型的属性值,List 元素必须为 String 类型
if (property.getValue() instanceof List>) {
for (final ListIterator it = ((List)property.getValue()).listIterator
(); it.hasNext();) {
Object element = it.next();
if (!(element instanceof String)) {
throw new InvalidArgumentException("The property '" + property.getKey() + "' should be a list of String.");
}
if (((String) element).length() > 8192) {
it.set(((String) element).substring(0, 8192));
}
}
}
// String 类型的属性值,长度不能超过 8192
if (property.getValue() instanceof String) {
String value = (String) property.getValue();
if (value.length() > 8192) {
property.setValue(value.substring(0, 8192));
}
}
if (eventType.equals("profile_increment")) {
if (!(property.getValue() instanceof Number)) {
throw new InvalidArgumentException("The property value of PROFILE_INCREMENT should be a "
+ "Number.");
}
} else if (eventType.equals("profile_append")) {
if (!(property.getValue() instanceof List>)) {
throw new InvalidArgumentException("The property value of PROFILE_INCREMENT should be a "
+ "List.");
}
}
}
}
private static String strJoin(String[] arr, String sep) {
StringBuilder sbStr = new StringBuilder();
for (int i = 0, il = arr.length; i < il; i++) {
if (i > 0)
sbStr.append(sep);
sbStr.append(arr[i]);
}
return sbStr.toString();
}
private static ObjectMapper getJsonObjectMapper() {
ObjectMapper jsonObjectMapper = new ObjectMapper();
// 容忍json中出现未知的列
jsonObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 兼容java中的驼峰的字段名命名
jsonObjectMapper.setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
jsonObjectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
return jsonObjectMapper;
}
private static final String SDK_VERSION = "3.1.11";
private static final Pattern KEY_PATTERN = Pattern.compile(
"^((?!^distinct_id$|^original_id$|^time$|^properties$|^id$|^first_id$|^second_id$|^users$|^events$|^event$|^user_id$|^date$|^datetime$)[a-zA-Z_$][a-zA-Z\\d_$]{0,99})$",
Pattern.CASE_INSENSITIVE);
private final Consumer consumer;
private final Map superProperties;
}