RealmAnalytics.mm
9.4 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
235
236
237
238
239
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
// Asynchronously submits build information to Realm if running in an iOS
// simulator or on OS X if a debugger is attached. Does nothing if running on an
// iOS / watchOS device or if a debugger is *not* attached.
//
// To be clear: this does *not* run when your app is in production or on
// your end-user’s devices; it will only run in the simulator or when a debugger
// is attached.
//
// Why are we doing this? In short, 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 language you use, what iOS
// versions you target, etc. Having this info will help 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 Swift app
// 10 times would report 10 times more than a single Objective-C developer that
// only builds once, 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. If you truly, absolutely
// feel compelled to not send this data back to Realm, then you can set an env
// variable named REALM_DISABLE_ANALYTICS. Since Realm is free we believe
// letting these analytics run is a small price to pay for the product & support
// we give you.
//
// Currently the following information is reported:
// - What kind of JavaScript framework is being used (e.g. React Native).
// - What kind of JavaScript VM is being used (e.g. JavaScriptCore or V8).
// - What version of Realm is being used, and from which language (obj-c or Swift).
// - What version of OS X it's running on (in case Xcode aggressively drops
// support for older versions again, we need to know what we need to support).
// - The minimum iOS/OS X version that the application is targeting (again, to
// help us decide what versions we need to support).
// - An anonymous MAC address and bundle ID to aggregate the other information on.
// - What version of Swift is being used (if applicable).
#import "RealmAnalytics.h"
#if TARGET_IPHONE_SIMULATOR || !TARGET_OS_IPHONE
#import <CommonCrypto/CommonDigest.h>
#import <sys/socket.h>
#import <sys/sysctl.h>
#import <net/if.h>
#import <net/if_dl.h>
#import <array>
// This symbol is defined by the Apple Generic versioning system when building this project.
// It confusingly looks like this: @(#)PROGRAM:RealmJS PROJECT:RealmJS-0.0.1
extern "C" const char RealmJSVersionString[];
// Wrapper for sysctl() that handles the memory management stuff
static auto RLMSysCtl(int *mib, u_int mibSize, size_t *bufferSize) {
std::unique_ptr<void, decltype(&free)> buffer(nullptr, &free);
int ret = sysctl(mib, mibSize, nullptr, bufferSize, nullptr, 0);
if (ret != 0) {
return buffer;
}
buffer.reset(malloc(*bufferSize));
if (!buffer) {
return buffer;
}
ret = sysctl(mib, mibSize, buffer.get(), bufferSize, nullptr, 0);
if (ret != 0) {
buffer.reset();
}
return buffer;
}
// Get the version of OS X we're running on (even in the simulator this gives
// the OS X version and not the simulated iOS version)
static NSString *RLMOSVersion() {
std::array<int, 2> mib = {CTL_KERN, KERN_OSRELEASE};
size_t bufferSize;
auto buffer = RLMSysCtl(&mib[0], mib.size(), &bufferSize);
if (!buffer) {
return nil;
}
return [[NSString alloc] initWithBytesNoCopy:buffer.release()
length:bufferSize - 1
encoding:NSUTF8StringEncoding
freeWhenDone:YES];
}
// Hash the data in the given buffer and convert it to a hex-format string
static NSString *RLMHashData(const void *bytes, size_t length) {
unsigned char buffer[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(bytes, static_cast<CC_LONG>(length), buffer);
char formatted[CC_SHA256_DIGEST_LENGTH * 2 + 1];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i) {
sprintf(formatted + i * 2, "%02x", buffer[i]);
}
return [[NSString alloc] initWithBytes:formatted
length:CC_SHA256_DIGEST_LENGTH * 2
encoding:NSUTF8StringEncoding];
}
// Returns the hash of the MAC address of the first network adaptor since the
// vendorIdentifier isn't constant between iOS simulators.
static NSString *RLMMACAddress() {
int en0 = static_cast<int>(if_nametoindex("en0"));
if (!en0) {
return nil;
}
std::array<int, 6> mib = {CTL_NET, PF_ROUTE, 0, AF_LINK, NET_RT_IFLIST, en0};
size_t bufferSize;
auto buffer = RLMSysCtl(&mib[0], mib.size(), &bufferSize);
if (!buffer) {
return nil;
}
// sockaddr_dl struct is immediately after the if_msghdr struct in the buffer
auto sockaddr = reinterpret_cast<sockaddr_dl *>(static_cast<if_msghdr *>(buffer.get()) + 1);
auto mac = reinterpret_cast<const unsigned char *>(sockaddr->sdl_data + sockaddr->sdl_nlen);
return RLMHashData(mac, 6);
}
static bool RLMIsDebuggerAttached() {
int name[] = {
CTL_KERN,
KERN_PROC,
KERN_PROC_PID,
getpid()
};
struct kinfo_proc info;
size_t info_size = sizeof(info);
if (sysctl(name, sizeof(name) / sizeof(name[0]), &info, &info_size, NULL, 0) == -1) {
NSLog(@"sysctl() failed: %s", strerror(errno));
return false;
}
return (info.kp_proc.p_flag & P_TRACED) != 0;
}
static NSDictionary *RLMAnalyticsPayload() {
static NSString * const kUnknownString = @"unknown";
NSBundle *appBundle = NSBundle.mainBundle;
NSString *hashedBundleID = appBundle.bundleIdentifier;
NSString *hashedMACAddress = RLMMACAddress();
// Main bundle isn't always the one of interest (e.g. when running tests
// it's xctest rather than the app's bundle), so look for one with a bundle ID
if (!hashedBundleID) {
for (NSBundle *bundle in NSBundle.allBundles) {
if ((hashedBundleID = bundle.bundleIdentifier)) {
appBundle = bundle;
break;
}
}
}
// If we found a bundle ID anywhere, hash it as it could contain sensitive
// information (e.g. the name of an unnanounced product)
if (hashedBundleID) {
NSData *data = [hashedBundleID dataUsingEncoding:NSUTF8StringEncoding];
hashedBundleID = RLMHashData(data.bytes, data.length);
}
return @{
@"event": @"Run",
@"properties": @{
// MixPanel properties
@"token": @"ce0fac19508f6c8f20066d345d360fd0",
// Anonymous identifiers to deduplicate events
@"distinct_id": hashedMACAddress ?: kUnknownString,
@"Anonymized MAC Address": hashedMACAddress ?: kUnknownString,
@"Anonymized Bundle ID": hashedBundleID ?: kUnknownString,
// Which version of Realm is being used
@"Binding": @"js",
@"Language": @"js",
@"Framework": @"react-native",
@"Virtual Machine": @"jsc",
@"Realm Version": [[@(RealmJSVersionString) componentsSeparatedByString:@"-"] lastObject] ?: kUnknownString,
#if TARGET_OS_IPHONE
@"Target OS Type": @"ios",
#else
@"Target OS Type": @"osx",
#endif
// Current OS version the app is targetting
@"Target OS Version": [[NSProcessInfo processInfo] operatingSystemVersionString],
// Minimum OS version the app is targetting
@"Target OS Minimum Version": appBundle.infoDictionary[@"MinimumOSVersion"] ?: kUnknownString,
// Host OS version being built on
@"Host OS Type": @"osx",
@"Host OS Version": RLMOSVersion() ?: kUnknownString,
}
};
}
void RLMSendAnalytics() {
if (getenv("REALM_DISABLE_ANALYTICS") || !RLMIsDebuggerAttached()) {
return;
}
NSData *payload = [NSJSONSerialization dataWithJSONObject:RLMAnalyticsPayload() options:0 error:nil];
NSString *url = [NSString stringWithFormat:@"https://api.mixpanel.com/track/?data=%@&ip=1", [payload base64EncodedStringWithOptions:0]];
// No error handling or anything because logging errors annoyed people for no
// real benefit, and it's not clear what else we could do
[[NSURLSession.sharedSession dataTaskWithURL:[NSURL URLWithString:url]] resume];
}
#else
void RLMSendAnalytics() {}
#endif