From 5b2e7b37affe9da82f1d80e5c041edc4e6da00e7 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Tue, 16 Feb 2016 22:26:35 -0800 Subject: [PATCH 1/9] Add script to update version in package.json and iOS This uses agvtool to update the CURRENT_PROJECT_VERSION and DYLIB_CURRENT_VERSION project variables. The Info.plist files have been updated to propagate this value. --- RealmJS.xcodeproj/project.pbxproj | 18 +++++++++--------- package.json | 2 ++ scripts/set-version.sh | 20 ++++++++++++++++++++ src/ios/Info.plist | 2 +- tests/ios/Info.plist | 4 ++-- 5 files changed, 34 insertions(+), 12 deletions(-) create mode 100755 scripts/set-version.sh diff --git a/RealmJS.xcodeproj/project.pbxproj b/RealmJS.xcodeproj/project.pbxproj index 9f767b1f..0e266001 100644 --- a/RealmJS.xcodeproj/project.pbxproj +++ b/RealmJS.xcodeproj/project.pbxproj @@ -873,7 +873,7 @@ buildSettings = { DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0.0.1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -907,7 +907,7 @@ buildSettings = { DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0.0.1; DYLIB_INSTALL_NAME_BASE = "@rpath"; HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -936,7 +936,7 @@ buildSettings = { DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0.0.1; DYLIB_INSTALL_NAME_BASE = "@rpath"; HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -979,7 +979,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0.0.1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1034,7 +1034,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0.0.1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1073,7 +1073,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = NO; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0.0.1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = src/ios/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1094,7 +1094,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = NO; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0.0.1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = src/ios/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1168,7 +1168,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 0.0.1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -1210,7 +1210,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; DEFINES_MODULE = NO; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; + DYLIB_CURRENT_VERSION = 0.0.1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = src/ios/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/package.json b/package.json index 570e29d6..890c3f16 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "RealmJS.xcodeproj" ], "scripts": { + "get-version": "echo $npm_package_version", + "set-version": "scripts/set-version.sh", "jsdoc": "rm -rf docs/output && jsdoc -c docs/conf.json", "test": "scripts/test.sh", "prepublish": "scripts/prepublish.sh" diff --git a/scripts/set-version.sh b/scripts/set-version.sh new file mode 100755 index 00000000..ce2b97df --- /dev/null +++ b/scripts/set-version.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e +set -o pipefail + +VERSION="$1" + +cd "$(dirname "$0")/.." + +# Check that the version looks semver compliant. +if [[ ! $VERSION =~ ^[0-9].[0-9]{1,2}.[0-9]{1,2}$ ]]; then + echo "Invalid version number: $VERSION" >&2 + exit 1 +fi + +# Update the version in package.json +npm --no-git-tag-version version "$VERSION" + +# Update CURRENT_PROJECT_VERSION and DYLIB_CURRENT_VERSION in the Xcode project. +xcrun agvtool new-version "$VERSION" diff --git a/src/ios/Info.plist b/src/ios/Info.plist index d3de8eef..0e600e67 100644 --- a/src/ios/Info.plist +++ b/src/ios/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + $(CURRENT_PROJECT_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/tests/ios/Info.plist b/tests/ios/Info.plist index ba72822e..2cafdf8e 100644 --- a/tests/ios/Info.plist +++ b/tests/ios/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.0 + $(CURRENT_PROJECT_VERSION) CFBundleSignature ???? CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) From 18b0d4bf49afd6bb4dbe61c5743d63a19a2e72a7 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Tue, 16 Feb 2016 22:29:39 -0800 Subject: [PATCH 2/9] Add Gradle task to generate Version class This uses the version that's in the package.json file. --- react-native/android/build.gradle | 24 ++++++++++++------- react-native/android/gradle.properties | 1 - .../android/src/main/templates/Version.java | 5 ++++ 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 react-native/android/src/main/templates/Version.java diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle index f4d4e3ba..69c15ec9 100644 --- a/react-native/android/build.gradle +++ b/react-native/android/build.gradle @@ -27,14 +27,22 @@ import org.apache.tools.ant.filters.ReplaceTokens // We download various C++ open-source dependencies into downloads. // We then copy both the downloaded code and our custom makefiles and headers into third-party-ndk. // After that we build native code from src/main/jni with module path pointing at third-party-ndk. +ext.coreVersion = '0.95.6' +def currentVersion = "npm --silent run get-version".execute().text.trim() def downloadsDir = new File("$projectDir/downloads") def jscDownloadDir = new File("$projectDir/src/main/jni/jsc") def coreDownloadDir = new File("$projectDir/src/main/jni") -ext.coreVersion = '0.95.6' def publishDir = new File("$projectDir/../../android/") +task generateVersionClass(type: Copy) { + from 'src/main/templates/Version.java' + into 'build/generated-src/main/java/io/realm/react' + filter(ReplaceTokens, tokens: [version: currentVersion]) + outputs.upToDateWhen { false } +} + task createNativeDepsDirectories { downloadsDir.mkdirs() } @@ -159,18 +167,17 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 23 - versionCode 1 - versionName "1.0" } sourceSets.main { + java.srcDir "$buildDir/generated-src/main/java" jni.srcDirs = [] jniLibs.srcDir "$buildDir/realm-react-ndk/exported" res.srcDirs = ['src/main/res/devsupport', 'src/main/res/shell'] } tasks.withType(JavaCompile) { - compileTask -> compileTask.dependsOn packageReactNdkLibs + compileTask -> compileTask.dependsOn generateVersionClass, packageReactNdkLibs } clean.dependsOn cleanReactNdkLib @@ -180,14 +187,14 @@ android { } } -task publishAndroid(dependsOn: packageReactNdkLibs, type: Copy) { +task publishAndroid(dependsOn: [generateVersionClass, packageReactNdkLibs], type: Sync) { // Copy task can only have one top level into "$publishDir" // copy java source into ('/src/main') { - from "$projectDir/src/main" - exclude '**/jni/**' + from "$projectDir/src/main", "$buildDir/generated-src/main" + exclude '**/jni/**', '**/templates/**' } // add compiled shared object @@ -202,7 +209,6 @@ task publishAndroid(dependsOn: packageReactNdkLibs, type: Copy) { } // copy and rename template build.gradle - into ('/') { from "$projectDir/publish_android_template" rename { String fileName -> @@ -260,7 +266,7 @@ afterEvaluate { project -> archives androidSourcesJar } - version = VERSION_NAME + version = currentVersion group = GROUP signing { diff --git a/react-native/android/gradle.properties b/react-native/android/gradle.properties index 2d3cbaa5..645750a5 100644 --- a/react-native/android/gradle.properties +++ b/react-native/android/gradle.properties @@ -1,4 +1,3 @@ -VERSION_NAME=0.0.1-SNAPSHOT GROUP=io.realm.react POM_NAME=RealmReactAndroid diff --git a/react-native/android/src/main/templates/Version.java b/react-native/android/src/main/templates/Version.java new file mode 100644 index 00000000..d2d89794 --- /dev/null +++ b/react-native/android/src/main/templates/Version.java @@ -0,0 +1,5 @@ +package io.realm.react; + +public class Version { + public static final String VERSION = "@version@"; +} From 6f90a3a6e8687a5062c3856717f2516141aebf9b Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Tue, 16 Feb 2016 22:32:56 -0800 Subject: [PATCH 3/9] Add Mixpanel analytics for iOS This is mostly copied and then modified from the Realm Cocoa repo. --- RealmJS.xcodeproj/project.pbxproj | 8 ++ react-native/RealmAnalytics.h | 15 +++ react-native/RealmAnalytics.mm | 181 ++++++++++++++++++++++++++++++ react-native/RealmReact.mm | 9 ++ 4 files changed, 213 insertions(+) create mode 100644 react-native/RealmAnalytics.h create mode 100644 react-native/RealmAnalytics.mm diff --git a/RealmJS.xcodeproj/project.pbxproj b/RealmJS.xcodeproj/project.pbxproj index 0e266001..68f28e55 100644 --- a/RealmJS.xcodeproj/project.pbxproj +++ b/RealmJS.xcodeproj/project.pbxproj @@ -86,6 +86,8 @@ F63FF3311C16434400B3B8E0 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = F63FF3301C16434400B3B8E0 /* libz.tbd */; }; F68A278C1BC2722A0063D40A /* RJSModuleLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = F68A278B1BC2722A0063D40A /* RJSModuleLoader.m */; }; F6BB7DF21BF681BC00D0A69E /* base64.hpp in Headers */ = {isa = PBXBuildFile; fileRef = F6BB7DF01BF681BC00D0A69E /* base64.hpp */; }; + F6C74DF01C732CC500C9DDCD /* RealmAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = F6C74DEE1C732CC500C9DDCD /* RealmAnalytics.h */; }; + F6C74DF11C732CC500C9DDCD /* RealmAnalytics.mm in Sources */ = {isa = PBXBuildFile; fileRef = F6C74DEF1C732CC500C9DDCD /* RealmAnalytics.mm */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -247,6 +249,8 @@ F6BB7DEF1BF681BC00D0A69E /* base64.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = base64.cpp; sourceTree = ""; }; F6BB7DF01BF681BC00D0A69E /* base64.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = base64.hpp; sourceTree = ""; }; F6C3FBBC1BF680EC00E6FFD4 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; + F6C74DEE1C732CC500C9DDCD /* RealmAnalytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RealmAnalytics.h; path = "react-native/RealmAnalytics.h"; sourceTree = ""; }; + F6C74DEF1C732CC500C9DDCD /* RealmAnalytics.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RealmAnalytics.mm; path = "react-native/RealmAnalytics.mm"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -334,6 +338,8 @@ 022D59301C043340001E25FE /* ReactTests.xcodeproj */, 0270BCCF1B7D067300010E03 /* RealmReact.h */, 0270BCD01B7D067300010E03 /* RealmReact.mm */, + F6C74DEE1C732CC500C9DDCD /* RealmAnalytics.h */, + F6C74DEF1C732CC500C9DDCD /* RealmAnalytics.mm */, ); name = RealmReact; sourceTree = ""; @@ -550,6 +556,7 @@ buildActionMask = 2147483647; files = ( F636F6C81BCDB3570023F35C /* RealmReact.h in Headers */, + F6C74DF01C732CC500C9DDCD /* RealmAnalytics.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -770,6 +777,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F6C74DF11C732CC500C9DDCD /* RealmAnalytics.mm in Sources */, 0270BCD11B7D067300010E03 /* RealmReact.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/react-native/RealmAnalytics.h b/react-native/RealmAnalytics.h new file mode 100644 index 00000000..39587d0d --- /dev/null +++ b/react-native/RealmAnalytics.h @@ -0,0 +1,15 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +void RLMSendAnalytics(); + +#ifdef __cplusplus +} +#endif diff --git a/react-native/RealmAnalytics.mm b/react-native/RealmAnalytics.mm new file mode 100644 index 00000000..99b2e6f9 --- /dev/null +++ b/react-native/RealmAnalytics.mm @@ -0,0 +1,181 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +#import "RealmAnalytics.h" + +#if TARGET_IPHONE_SIMULATOR +#import +#import +#import +#import +#import +#import + +// This symbol is defined by the Apple Generic versioning system when building this project. +// It confusingly looks like this: @(#)PROGRAM:RealmReact PROJECT:RealmJS-0.0.1 +extern "C" const char RealmReactVersionString[]; + +// Wrapper for sysctl() that handles the memory management stuff +static auto RLMSysCtl(int *mib, u_int mibSize, size_t *bufferSize) { + std::unique_ptr 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 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(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(if_nametoindex("en0")); + if (!en0) { + return nil; + } + + std::array 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(static_cast(buffer.get()) + 1); + auto mac = reinterpret_cast(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": @"react-native", + @"Language": @"js", + @"Realm Version": [[@(RealmReactVersionString) componentsSeparatedByString:@"-"] lastObject] ?: kUnknownString, + @"Target OS Type": @"ios", + // 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 diff --git a/react-native/RealmReact.mm b/react-native/RealmReact.mm index 6715a88c..99db64f0 100644 --- a/react-native/RealmReact.mm +++ b/react-native/RealmReact.mm @@ -3,6 +3,7 @@ */ #import "RealmReact.h" +#import "RealmAnalytics.h" #import "RCTBridge.h" #import "js_init.h" @@ -79,6 +80,14 @@ extern "C" JSGlobalContextRef RealmReactGetJSGlobalContextForExecutor(id executo } } ++ (void)initialize { + if (self != [RealmReact class]) { + return; + } + + RLMSendAnalytics(); +} + + (NSString *)moduleName { return @"Realm"; } From b7c46b701c5d1878f11f27ce85c92f2bc5772742 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Tue, 16 Feb 2016 22:34:43 -0800 Subject: [PATCH 4/9] Add Mixpanel analytics for Android This is mostly copied and modified from the Realm Java repo. --- .../java/io/realm/react/RealmAnalytics.java | 209 ++++++++++++++++++ .../java/io/realm/react/RealmReactModule.java | 9 + 2 files changed, 218 insertions(+) create mode 100644 react-native/android/src/main/java/io/realm/react/RealmAnalytics.java diff --git a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java new file mode 100644 index 00000000..7f49bbb1 --- /dev/null +++ b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java @@ -0,0 +1,209 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +package io.realm.react; + +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.util.Base64; + +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.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Enumeration; +import java.util.Set; + +// Asynchronously submits build information to Realm when the annotation +// processor is running +// +// 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. +public class RealmAnalytics { + private static RealmAnalytics instance; + 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 + = "{\n" + + " \"event\": \"%EVENT%\",\n" + + " \"properties\": {\n" + + " \"token\": \"%TOKEN%\",\n" + + " \"distinct_id\": \"%USER_ID%\",\n" + + " \"Anonymized MAC Address\": \"%USER_ID%\",\n" + + " \"Anonymized Bundle ID\": \"%APP_ID%\",\n" + + " \"Binding\": \"react-native\",\n" + + " \"Language\": \"js\",\n" + + " \"Realm Version\": \"%REALM_VERSION%\",\n" + + " \"Host OS Type\": \"%OS_TYPE%\",\n" + + " \"Host OS Version\": \"%OS_VERSION%\",\n" + + " \"Target OS Type\": \"android\"\n" + + " }\n" + + "}"; + + private ApplicationInfo applicationInfo; + + private RealmAnalytics(ApplicationInfo applicationInfo) { + this.applicationInfo = applicationInfo; + } + + public static RealmAnalytics getInstance(ApplicationInfo applicationInfo) { + if (instance == null) { + instance = new RealmAnalytics(applicationInfo); + } + return instance; + } + + public static boolean shouldExecute() { + return System.getenv("REALM_DISABLE_ANALYTICS") == null + && (isRunningOnGenymotion() || isRunningOnStockEmulator()); + } + + private void send() { + try { + URL url = getUrl(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + connection.getResponseCode(); + } catch (Exception ignored) { + } + } + + public void execute() { + Thread backgroundThread = new Thread(new Runnable() { + @Override + public void run() { + send(); + } + }); + backgroundThread.start(); + try { + backgroundThread.join(CONNECT_TIMEOUT + READ_TIMEOUT); + } catch (InterruptedException ignored) { + // We ignore this exception on purpose not to break the build system if this class fails + } catch (IllegalArgumentException ignored) { + // We ignore this exception on purpose not to break the build system if this class fails + } + } + + private URL getUrl() throws + MalformedURLException, + SocketException, + NoSuchAlgorithmException, + UnsupportedEncodingException { + return new URL(ADDRESS_PREFIX + base64Encode(generateJson()) + ADDRESS_SUFFIX); + } + + private String generateJson() throws SocketException, NoSuchAlgorithmException { + return JSON_TEMPLATE + .replaceAll("%EVENT%", EVENT_NAME) + .replaceAll("%TOKEN%", TOKEN) + .replaceAll("%USER_ID%", getAnonymousUserId()) + .replaceAll("%APP_ID%", getAnonymousAppId()) + .replaceAll("%REALM_VERSION%", Version.VERSION) + .replaceAll("%OS_TYPE%", System.getProperty("os.name")) + .replaceAll("%OS_VERSION%", System.getProperty("os.version")); + } + + private static boolean isRunningOnGenymotion() { + return Build.FINGERPRINT.contains("vbox"); + } + + private static boolean isRunningOnStockEmulator() { + return Build.FINGERPRINT.contains("generic"); + } + + /** + * Compute an anonymous user id from the hashed MAC address of the first network interface + * @return the anonymous user id + * @throws NoSuchAlgorithmException + * @throws SocketException + */ + private static String getAnonymousUserId() throws NoSuchAlgorithmException, SocketException { + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + + if (!networkInterfaces.hasMoreElements()) { + throw new IllegalStateException("No network interfaces detected"); + } + + NetworkInterface networkInterface = networkInterfaces.nextElement(); + byte[] hardwareAddress = networkInterface.getHardwareAddress(); // Normally this is the MAC address + + return hexStringify(sha256Hash(hardwareAddress)); + } + + /** + * Compute an anonymous app/library id from the packages containing RealmObject classes + * @return the anonymous app/library id + * @throws NoSuchAlgorithmException + */ + private String getAnonymousAppId() throws NoSuchAlgorithmException { + byte[] packagesBytes = applicationInfo.packageName.getBytes(); + + return hexStringify(sha256Hash(packagesBytes)); + } + + /** + * Encode the given string with Base64 + * @param data the string to encode + * @return the encoded string + * @throws UnsupportedEncodingException + */ + private static String base64Encode(String data) throws UnsupportedEncodingException { + return Base64.encodeToString(data.getBytes("UTF-8"), Base64.DEFAULT); + } + + /** + * Compute the SHA-256 hash of the given byte array + * @param data the byte array to hash + * @return the hashed byte array + * @throws NoSuchAlgorithmException + */ + private 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 + */ + private 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(); + } +} \ No newline at end of file diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java index ffa9773f..37345b90 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java @@ -19,6 +19,7 @@ import fi.iki.elonen.NanoHTTPD; public class RealmReactModule extends ReactContextBaseJavaModule { private static final int DEFAULT_PORT = 8082; + private static boolean sentAnalytics = false; private AndroidWebServer webServer; private Handler handler = new Handler(Looper.getMainLooper()); @@ -38,6 +39,14 @@ public class RealmReactModule extends ReactContextBaseJavaModule { } setDefaultRealmFileDirectory(fileDir); + + // Attempt to send analytics info only once, and only if allowed to do so. + if (!sentAnalytics && RealmAnalytics.shouldExecute()) { + sentAnalytics = true; + + RealmAnalytics analytics = RealmAnalytics.getInstance(reactContext.getApplicationInfo()); + analytics.execute(); + } } @Override From 0c9d70905a1fd3ad0a014fdc2b854c9b4e00d2b6 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Wed, 17 Feb 2016 13:46:00 -0800 Subject: [PATCH 5/9] Adjust Mixpanel properties --- react-native/RealmAnalytics.mm | 9 +++++++-- .../src/main/java/io/realm/react/RealmAnalytics.java | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/react-native/RealmAnalytics.mm b/react-native/RealmAnalytics.mm index 99b2e6f9..f2688498 100644 --- a/react-native/RealmAnalytics.mm +++ b/react-native/RealmAnalytics.mm @@ -4,7 +4,7 @@ #import "RealmAnalytics.h" -#if TARGET_IPHONE_SIMULATOR +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_MAC #import #import #import @@ -145,10 +145,15 @@ static NSDictionary *RLMAnalyticsPayload() { @"Anonymized Bundle ID": hashedBundleID ?: kUnknownString, // Which version of Realm is being used - @"Binding": @"react-native", + @"Binding": @"js", @"Language": @"js", + @"Framework": @"react-native", @"Realm Version": [[@(RealmReactVersionString) componentsSeparatedByString:@"-"] lastObject] ?: kUnknownString, +#if TARGET_OS_MAC + @"Target OS Type": @"osx", +#else @"Target OS Type": @"ios", +#endif // Current OS version the app is targetting @"Target OS Version": [[NSProcessInfo processInfo] operatingSystemVersionString], // Minimum OS version the app is targetting diff --git a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java index 7f49bbb1..acf13c71 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java +++ b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java @@ -60,8 +60,9 @@ public class RealmAnalytics { + " \"distinct_id\": \"%USER_ID%\",\n" + " \"Anonymized MAC Address\": \"%USER_ID%\",\n" + " \"Anonymized Bundle ID\": \"%APP_ID%\",\n" - + " \"Binding\": \"react-native\",\n" + + " \"Binding\": \"js\",\n" + " \"Language\": \"js\",\n" + + " \"Framework\": \"react-native\"," + " \"Realm Version\": \"%REALM_VERSION%\",\n" + " \"Host OS Type\": \"%OS_TYPE%\",\n" + " \"Host OS Version\": \"%OS_VERSION%\",\n" From 6c644067fdd50bec2395aa17362d897dc2398a8b Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Wed, 17 Feb 2016 13:46:17 -0800 Subject: [PATCH 6/9] Add comments explaining analytics usage --- react-native/RealmAnalytics.h | 37 +++++++++++++++++++ react-native/RealmAnalytics.mm | 37 +++++++++++++++++++ .../java/io/realm/react/RealmAnalytics.java | 1 + 3 files changed, 75 insertions(+) diff --git a/react-native/RealmAnalytics.h b/react-native/RealmAnalytics.h index 39587d0d..22a2a860 100644 --- a/react-native/RealmAnalytics.h +++ b/react-native/RealmAnalytics.h @@ -2,6 +2,43 @@ * Proprietary and Confidential */ +// 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 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 #ifdef __cplusplus diff --git a/react-native/RealmAnalytics.mm b/react-native/RealmAnalytics.mm index f2688498..2494b5c6 100644 --- a/react-native/RealmAnalytics.mm +++ b/react-native/RealmAnalytics.mm @@ -2,6 +2,43 @@ * Proprietary and Confidential */ +// 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 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_MAC diff --git a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java index acf13c71..2cdbeb3f 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java +++ b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java @@ -41,6 +41,7 @@ import java.util.Set; // better product for you. // // Currently the following information is reported: +// - What kind of JavaScript framework is being used (e.g. React Native) // - 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. From f059d57bf446f7b932480d7636d56ef8a56c9340 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Wed, 17 Feb 2016 13:53:26 -0800 Subject: [PATCH 7/9] Add Mixpanel property for JS VM --- react-native/RealmAnalytics.h | 1 + react-native/RealmAnalytics.mm | 2 ++ .../android/src/main/java/io/realm/react/RealmAnalytics.java | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/react-native/RealmAnalytics.h b/react-native/RealmAnalytics.h index 22a2a860..ec796399 100644 --- a/react-native/RealmAnalytics.h +++ b/react-native/RealmAnalytics.h @@ -31,6 +31,7 @@ // // 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). diff --git a/react-native/RealmAnalytics.mm b/react-native/RealmAnalytics.mm index 2494b5c6..f4d91f19 100644 --- a/react-native/RealmAnalytics.mm +++ b/react-native/RealmAnalytics.mm @@ -31,6 +31,7 @@ // // 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). @@ -185,6 +186,7 @@ static NSDictionary *RLMAnalyticsPayload() { @"Binding": @"js", @"Language": @"js", @"Framework": @"react-native", + @"Virtual Machine": @"jsc", @"Realm Version": [[@(RealmReactVersionString) componentsSeparatedByString:@"-"] lastObject] ?: kUnknownString, #if TARGET_OS_MAC @"Target OS Type": @"osx", diff --git a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java index 2cdbeb3f..90150b28 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java +++ b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java @@ -42,6 +42,7 @@ import java.util.Set; // // 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 // - What OS you are running on // - An anonymized MAC address and bundle ID to aggregate the other information on. @@ -63,7 +64,8 @@ public class RealmAnalytics { + " \"Anonymized Bundle ID\": \"%APP_ID%\",\n" + " \"Binding\": \"js\",\n" + " \"Language\": \"js\",\n" - + " \"Framework\": \"react-native\"," + + " \"Framework\": \"react-native\",\n" + + " \"Virtual Machine\": \"jsc\",\n" + " \"Realm Version\": \"%REALM_VERSION%\",\n" + " \"Host OS Type\": \"%OS_TYPE%\",\n" + " \"Host OS Version\": \"%OS_VERSION%\",\n" From cac3d09bcbda6e11116a5eca5c88cf96a580104f Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Thu, 18 Feb 2016 14:09:11 -0800 Subject: [PATCH 8/9] Mistakenly thought TARGET_OS_MAC was useful --- react-native/RealmAnalytics.mm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/react-native/RealmAnalytics.mm b/react-native/RealmAnalytics.mm index f4d91f19..49d23699 100644 --- a/react-native/RealmAnalytics.mm +++ b/react-native/RealmAnalytics.mm @@ -42,7 +42,7 @@ #import "RealmAnalytics.h" -#if TARGET_IPHONE_SIMULATOR || TARGET_OS_MAC +#if TARGET_IPHONE_SIMULATOR || !TARGET_OS_IPHONE #import #import #import @@ -188,10 +188,10 @@ static NSDictionary *RLMAnalyticsPayload() { @"Framework": @"react-native", @"Virtual Machine": @"jsc", @"Realm Version": [[@(RealmReactVersionString) componentsSeparatedByString:@"-"] lastObject] ?: kUnknownString, -#if TARGET_OS_MAC - @"Target OS Type": @"osx", -#else +#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], From 9b55ac9a59a60c37da301c2fbe6c81a78a22e674 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Thu, 18 Feb 2016 14:55:28 -0800 Subject: [PATCH 9/9] Fix bug with Mixpanel on Android --- .../android/src/main/java/io/realm/react/RealmAnalytics.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java index 90150b28..2f906a5e 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java +++ b/react-native/android/src/main/java/io/realm/react/RealmAnalytics.java @@ -183,7 +183,7 @@ public class RealmAnalytics { * @throws UnsupportedEncodingException */ private static String base64Encode(String data) throws UnsupportedEncodingException { - return Base64.encodeToString(data.getBytes("UTF-8"), Base64.DEFAULT); + return Base64.encodeToString(data.getBytes("UTF-8"), Base64.NO_WRAP); } /**