245 lines
8.6 KiB
Plaintext
245 lines
8.6 KiB
Plaintext
|
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.NoSuchAlgorithmException;
|
||
|
import java.util.Enumeration;
|
||
|
import java.util.Set;
|
||
|
import org.gradle.api.DefaultTask
|
||
|
import org.gradle.api.tasks.TaskAction
|
||
|
|
||
|
import java.io.File;
|
||
|
import java.io.FileNotFoundException;
|
||
|
import java.io.IOException;
|
||
|
import java.io.InputStream;
|
||
|
import java.net.NetworkInterface;
|
||
|
import java.net.SocketException;
|
||
|
import java.security.NoSuchAlgorithmException;
|
||
|
import java.util.Scanner;
|
||
|
|
||
|
import javax.xml.bind.DatatypeConverter;
|
||
|
import java.io.UnsupportedEncodingException;
|
||
|
import java.security.MessageDigest;
|
||
|
import java.security.NoSuchAlgorithmException;
|
||
|
|
||
|
|
||
|
// Submits build information to Realm when assembling the app
|
||
|
//
|
||
|
// 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.
|
||
|
|
||
|
class SendAnalyticsTask extends DefaultTask {
|
||
|
String applicationId = 'UNKNOWN'
|
||
|
String version = 'UNKNOWN'
|
||
|
|
||
|
@TaskAction
|
||
|
def sendAnalytics() {
|
||
|
try {
|
||
|
def env = System.getenv()
|
||
|
def disableAnalytics= env['REALM_DISABLE_ANALYTICS']
|
||
|
if (disableAnalytics == null || disableAnalytics != "true") {
|
||
|
send()
|
||
|
}
|
||
|
} catch(all) {}
|
||
|
}
|
||
|
|
||
|
//TODO replace with properties
|
||
|
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 = '''
|
||
|
{
|
||
|
"event": "%EVENT%",
|
||
|
"properties": {
|
||
|
"token": "%TOKEN%,
|
||
|
"distinct_id": "%USER_ID%",
|
||
|
"Anonymized MAC Address": "%USER_ID%",
|
||
|
"Anonymized Bundle ID": "%APP_ID%",
|
||
|
"Binding": "js",
|
||
|
"Language": "js",
|
||
|
"Framework": "react-native",
|
||
|
"Virtual Machine": "jsc",
|
||
|
"Realm Version": "%REALM_VERSION%",
|
||
|
"Host OS Type": "%OS_TYPE%",
|
||
|
"Host OS Version": "%OS_VERSION%",
|
||
|
"Target OS Type": "android"
|
||
|
}
|
||
|
}'''
|
||
|
|
||
|
void send() {
|
||
|
def connection = (ADDRESS_PREFIX + Utils.base64Encode(generateJson()) + ADDRESS_SUFFIX)
|
||
|
.toURL().openConnection()
|
||
|
connection.setConnectTimeout(CONNECT_TIMEOUT);
|
||
|
connection.setReadTimeout(READ_TIMEOUT);
|
||
|
connection.setRequestMethod("GET")
|
||
|
connection.connect()
|
||
|
connection.getResponseCode()
|
||
|
}
|
||
|
|
||
|
private String generateJson() {
|
||
|
JSON_TEMPLATE
|
||
|
.replaceAll("%EVENT%", EVENT_NAME)
|
||
|
.replaceAll("%TOKEN%", TOKEN)
|
||
|
.replaceAll("%USER_ID%", ComputerIdentifierGenerator.get())
|
||
|
.replaceAll("%APP_ID%", getAnonymousAppId())
|
||
|
.replaceAll("%REALM_VERSION%", version)
|
||
|
.replaceAll("%OS_TYPE%", System.getProperty("os.name"))
|
||
|
.replaceAll("%OS_VERSION%", System.getProperty("os.version"))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Computes an anonymous app/library id from the packages containing RealmObject classes
|
||
|
* @return the anonymous app/library id
|
||
|
* @throws NoSuchAlgorithmException
|
||
|
*/
|
||
|
private String getAnonymousAppId() {
|
||
|
byte[] packagesBytes = applicationId?.getBytes()
|
||
|
Utils.hexStringify(Utils.sha256Hash(packagesBytes))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ComputerIdentifierGenerator {
|
||
|
private static final String UNKNOWN = "unknown";
|
||
|
|
||
|
private static String OS = System.getProperty("os.name").toLowerCase()
|
||
|
|
||
|
public static String get() {
|
||
|
if (isWindows()) {
|
||
|
return getWindowsIdentifier()
|
||
|
} else if (isMac()) {
|
||
|
return getMacOsIdentifier()
|
||
|
} else if (isLinux()) {
|
||
|
return getLinuxMacAddress()
|
||
|
} else {
|
||
|
return UNKNOWN
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean isWindows() {
|
||
|
OS.contains("win")
|
||
|
}
|
||
|
|
||
|
private static boolean isMac() {
|
||
|
OS.contains("mac")
|
||
|
}
|
||
|
|
||
|
private static boolean isLinux() {
|
||
|
OS.contains("inux")
|
||
|
}
|
||
|
|
||
|
private static String getLinuxMacAddress() {
|
||
|
File machineId = new File("/var/lib/dbus/machine-id")
|
||
|
if (!machineId.exists()) {
|
||
|
machineId = new File("/etc/machine-id")
|
||
|
}
|
||
|
if (!machineId.exists()) {
|
||
|
return UNKNOWN
|
||
|
}
|
||
|
|
||
|
Scanner scanner = null
|
||
|
try {
|
||
|
scanner = new Scanner(machineId)
|
||
|
String id = scanner.useDelimiter("\\A").next()
|
||
|
return Utils.hexStringify(Utils.sha256Hash(id.getBytes()))
|
||
|
} finally {
|
||
|
if (scanner != null) {
|
||
|
scanner.close()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static String getMacOsIdentifier() {
|
||
|
NetworkInterface networkInterface = NetworkInterface.getByName("en0")
|
||
|
byte[] hardwareAddress = networkInterface.getHardwareAddress()
|
||
|
Utils.hexStringify(Utils.sha256Hash(hardwareAddress))
|
||
|
}
|
||
|
|
||
|
private static String getWindowsIdentifier() {
|
||
|
Runtime runtime = Runtime.getRuntime()
|
||
|
Process process = runtime.exec(["wmic", "csproduct", "get", "UUID"])
|
||
|
|
||
|
String result = null
|
||
|
InputStream is = process.getInputStream()
|
||
|
Scanner sc = new Scanner(process.getInputStream())
|
||
|
try {
|
||
|
while (sc.hasNext()) {
|
||
|
String next = sc.next()
|
||
|
if (next.contains("UUID")) {
|
||
|
result = sc.next().trim()
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
} finally {
|
||
|
is.close()
|
||
|
}
|
||
|
|
||
|
result==null?UNKNOWN:Utils.hexStringify(Utils.sha256Hash(result.getBytes()))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Utils {
|
||
|
|
||
|
/**
|
||
|
* Encode the given string with Base64
|
||
|
* @param data the string to encode
|
||
|
* @return the encoded string
|
||
|
* @throws UnsupportedEncodingException
|
||
|
*/
|
||
|
public static String base64Encode(String data) throws UnsupportedEncodingException {
|
||
|
return DatatypeConverter.printBase64Binary(data.getBytes("UTF-8"));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Compute the SHA-256 hash of the given byte array
|
||
|
* @param data the byte array to hash
|
||
|
* @return the hashed byte array
|
||
|
* @throws NoSuchAlgorithmException
|
||
|
*/
|
||
|
public 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
|
||
|
*/
|
||
|
public 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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// see https://discuss.gradle.org/t/build-gradle-cant-find-task-class-defined-in-an-apply-from-external-script/5836/2
|
||
|
ext.SendAnalyticsTask = SendAnalyticsTask
|