Add Mixpanel analytics for Android

This is mostly copied and modified from the Realm Java repo.
This commit is contained in:
Scott Kyle 2016-02-16 22:34:43 -08:00
parent 6f90a3a6e8
commit b7c46b701c
2 changed files with 218 additions and 0 deletions

View File

@ -0,0 +1,209 @@
/* Copyright 2016 Realm Inc - All Rights Reserved
* Proprietary and Confidential
*/
package io.realm.react;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.util.Base64;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.Set;
// Asynchronously submits build information to Realm when the annotation
// processor is running
//
// To be clear: this does *not* run when your app is in production or on
// your end-user's devices; it will only run when you build your app from source.
//
// Why are we doing this? Because it helps us build a better product for you.
// None of the data personally identifies you, your employer or your app, but it
// *will* help us understand what Realm version you use, what host OS you use,
// etc. Having this info will help with prioritizing our time, adding new
// features and deprecating old features. Collecting an anonymized bundle &
// anonymized MAC is the only way for us to count actual usage of the other
// metrics accurately. If we don't have a way to deduplicate the info reported,
// it will be useless, as a single developer building their app on Windows ten
// times would report 10 times more than a single developer that only builds
// once from Mac OS X, making the data all but useless. No one likes sharing
// data unless it's necessary, we get it, and we've debated adding this for a
// long long time. Since Realm is a free product without an email signup, we
// feel this is a necessary step so we can collect relevant data to build a
// better product for you.
//
// Currently the following information is reported:
// - What version of Realm is being used
// - What OS you are running on
// - An anonymized MAC address and bundle ID to aggregate the other information on.
public class RealmAnalytics {
private static RealmAnalytics instance;
private static final int READ_TIMEOUT = 2000;
private static final int CONNECT_TIMEOUT = 4000;
private static final String ADDRESS_PREFIX = "https://api.mixpanel.com/track/?data=";
private static final String ADDRESS_SUFFIX = "&ip=1";
private static final String TOKEN = "ce0fac19508f6c8f20066d345d360fd0";
private static final String EVENT_NAME = "Run";
private static final String JSON_TEMPLATE
= "{\n"
+ " \"event\": \"%EVENT%\",\n"
+ " \"properties\": {\n"
+ " \"token\": \"%TOKEN%\",\n"
+ " \"distinct_id\": \"%USER_ID%\",\n"
+ " \"Anonymized MAC Address\": \"%USER_ID%\",\n"
+ " \"Anonymized Bundle ID\": \"%APP_ID%\",\n"
+ " \"Binding\": \"react-native\",\n"
+ " \"Language\": \"js\",\n"
+ " \"Realm Version\": \"%REALM_VERSION%\",\n"
+ " \"Host OS Type\": \"%OS_TYPE%\",\n"
+ " \"Host OS Version\": \"%OS_VERSION%\",\n"
+ " \"Target OS Type\": \"android\"\n"
+ " }\n"
+ "}";
private ApplicationInfo applicationInfo;
private RealmAnalytics(ApplicationInfo applicationInfo) {
this.applicationInfo = applicationInfo;
}
public static RealmAnalytics getInstance(ApplicationInfo applicationInfo) {
if (instance == null) {
instance = new RealmAnalytics(applicationInfo);
}
return instance;
}
public static boolean shouldExecute() {
return System.getenv("REALM_DISABLE_ANALYTICS") == null
&& (isRunningOnGenymotion() || isRunningOnStockEmulator());
}
private void send() {
try {
URL url = getUrl();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
connection.getResponseCode();
} catch (Exception ignored) {
}
}
public void execute() {
Thread backgroundThread = new Thread(new Runnable() {
@Override
public void run() {
send();
}
});
backgroundThread.start();
try {
backgroundThread.join(CONNECT_TIMEOUT + READ_TIMEOUT);
} catch (InterruptedException ignored) {
// We ignore this exception on purpose not to break the build system if this class fails
} catch (IllegalArgumentException ignored) {
// We ignore this exception on purpose not to break the build system if this class fails
}
}
private URL getUrl() throws
MalformedURLException,
SocketException,
NoSuchAlgorithmException,
UnsupportedEncodingException {
return new URL(ADDRESS_PREFIX + base64Encode(generateJson()) + ADDRESS_SUFFIX);
}
private String generateJson() throws SocketException, NoSuchAlgorithmException {
return JSON_TEMPLATE
.replaceAll("%EVENT%", EVENT_NAME)
.replaceAll("%TOKEN%", TOKEN)
.replaceAll("%USER_ID%", getAnonymousUserId())
.replaceAll("%APP_ID%", getAnonymousAppId())
.replaceAll("%REALM_VERSION%", Version.VERSION)
.replaceAll("%OS_TYPE%", System.getProperty("os.name"))
.replaceAll("%OS_VERSION%", System.getProperty("os.version"));
}
private static boolean isRunningOnGenymotion() {
return Build.FINGERPRINT.contains("vbox");
}
private static boolean isRunningOnStockEmulator() {
return Build.FINGERPRINT.contains("generic");
}
/**
* Compute an anonymous user id from the hashed MAC address of the first network interface
* @return the anonymous user id
* @throws NoSuchAlgorithmException
* @throws SocketException
*/
private static String getAnonymousUserId() throws NoSuchAlgorithmException, SocketException {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
if (!networkInterfaces.hasMoreElements()) {
throw new IllegalStateException("No network interfaces detected");
}
NetworkInterface networkInterface = networkInterfaces.nextElement();
byte[] hardwareAddress = networkInterface.getHardwareAddress(); // Normally this is the MAC address
return hexStringify(sha256Hash(hardwareAddress));
}
/**
* Compute an anonymous app/library id from the packages containing RealmObject classes
* @return the anonymous app/library id
* @throws NoSuchAlgorithmException
*/
private String getAnonymousAppId() throws NoSuchAlgorithmException {
byte[] packagesBytes = applicationInfo.packageName.getBytes();
return hexStringify(sha256Hash(packagesBytes));
}
/**
* Encode the given string with Base64
* @param data the string to encode
* @return the encoded string
* @throws UnsupportedEncodingException
*/
private static String base64Encode(String data) throws UnsupportedEncodingException {
return Base64.encodeToString(data.getBytes("UTF-8"), Base64.DEFAULT);
}
/**
* Compute the SHA-256 hash of the given byte array
* @param data the byte array to hash
* @return the hashed byte array
* @throws NoSuchAlgorithmException
*/
private static byte[] sha256Hash(byte[] data) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
return messageDigest.digest(data);
}
/**
* Convert a byte array to its hex-string
* @param data the byte array to convert
* @return the hex-string of the byte array
*/
private static String hexStringify(byte[] data) {
StringBuilder stringBuilder = new StringBuilder();
for (byte singleByte : data) {
stringBuilder.append(Integer.toString((singleByte & 0xff) + 0x100, 16).substring(1));
}
return stringBuilder.toString();
}
}

View File

@ -19,6 +19,7 @@ import fi.iki.elonen.NanoHTTPD;
public class RealmReactModule extends ReactContextBaseJavaModule {
private static final int DEFAULT_PORT = 8082;
private static boolean sentAnalytics = false;
private AndroidWebServer webServer;
private Handler handler = new Handler(Looper.getMainLooper());
@ -38,6 +39,14 @@ public class RealmReactModule extends ReactContextBaseJavaModule {
}
setDefaultRealmFileDirectory(fileDir);
// Attempt to send analytics info only once, and only if allowed to do so.
if (!sentAnalytics && RealmAnalytics.shouldExecute()) {
sentAnalytics = true;
RealmAnalytics analytics = RealmAnalytics.getInstance(reactContext.getApplicationInfo());
analytics.execute();
}
}
@Override