analytics.gradle
8.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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