analytics.gradle 8.2 KB
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 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() {
          // MixPanel requires padded Base64 encoding
          def url = (ADDRESS_PREFIX + generateJson().bytes.encodeBase64().toString() + ADDRESS_SUFFIX).toURL()
          def connection = url.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 {

    /**
     * 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