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