Add Mixpanel analytics for Android
This is mostly copied and modified from the Realm Java repo.
This commit is contained in:
parent
6f90a3a6e8
commit
b7c46b701c
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import fi.iki.elonen.NanoHTTPD;
|
||||||
|
|
||||||
public class RealmReactModule extends ReactContextBaseJavaModule {
|
public class RealmReactModule extends ReactContextBaseJavaModule {
|
||||||
private static final int DEFAULT_PORT = 8082;
|
private static final int DEFAULT_PORT = 8082;
|
||||||
|
private static boolean sentAnalytics = false;
|
||||||
|
|
||||||
private AndroidWebServer webServer;
|
private AndroidWebServer webServer;
|
||||||
private Handler handler = new Handler(Looper.getMainLooper());
|
private Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
@ -38,6 +39,14 @@ public class RealmReactModule extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultRealmFileDirectory(fileDir);
|
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
|
@Override
|
||||||
|
|
Loading…
Reference in New Issue