├── .babelrc ├── .eslintrc ├── .gitignore ├── .lintstagedrc ├── .travis.yml ├── LICENSE ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── pleak │ └── PleakDeviceInfo │ ├── PleakDeviceInfo.java │ └── PleakDeviceInfoModule.java ├── assets └── logo.png ├── ios └── PleakDeviceInfo │ ├── PleakDeviceInfo.h │ ├── PleakDeviceInfo.m │ └── PleakDeviceInfo.xcodeproj │ └── project.pbxproj ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── Pleak.js ├── Pleak.test.js ├── PleakBatchPublisher.js ├── PleakBatchPublisher.test.js ├── PleakContext.js ├── PleakContext.test.js ├── index.js └── utils │ ├── constants.js │ ├── deviceUtils.js │ ├── index.js │ ├── index.test.js │ ├── pleakUtils.js │ └── pleakUtils.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": false }]], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "external-helpers", 6 | "transform-class-properties" 7 | ], 8 | "env": { 9 | "test": { 10 | "presets": ["env"], 11 | "plugins": ["transform-runtime", "transform-class-properties", "transform-es2015-modules-commonjs"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "jest/globals": true 6 | }, 7 | "parser": "babel-eslint", 8 | "extends": ["airbnb", "prettier"], 9 | "plugins": ["prettier", "jest"], 10 | "rules": { 11 | "prettier/prettier": [2, { "singleQuote": true, "trailingComma": "es5" }], 12 | "no-console": [2, { "allow": ["info", "warn", "error"] }], 13 | "import/prefer-default-export": 0, 14 | "import/no-unresolved": 0, 15 | "no-param-reassign": 0, 16 | "no-unused-expressions": ["error", { "allowTernary": true }], 17 | "no-nested-ternary": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | yarn-error.log 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | **/xcuserdata/ 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | android/.gradle 24 | 25 | **/.DS_Store 26 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "*.js": ["eslint --fix", "git add"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | node_js: 4 | - 9 5 | 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | 11 | before_script: 12 | - yarn 13 | 14 | script: 15 | - yarn lint 16 | - yarn test 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, Pleak 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

@pleak/react-perf-monitor

4 |

Performance monitoring for React and React Native apps with Pleak.

5 |
6 | 7 | # Table of contents 8 | 9 | * [Getting Started](#getting-started) 10 | * [Installation](#installation) 11 | * [React Native](#react-native) 12 | * [Initializing](#initializing) 13 | * [Options](#options) 14 | * [Required](#required) 15 | * [`uri`](#uri) 16 | * [Optional](#optional) 17 | * [`debug`](#debug) 18 | * [`publish`](#publish) 19 | * [`interval`](#interval) 20 | * [`environment`](#environment) 21 | * [Usage](#usage) 22 | 23 | # Getting started 24 | 25 | ## Installation 26 | 27 | ``` 28 | # With npm 29 | npm install @pleak/react-perf-monitor 30 | 31 | # With yarn 32 | yarn add @pleak/react-perf-monitor 33 | ``` 34 | 35 | ### React Native 36 | 37 | If you're using this package with a React Native app, you must link native dependencies to your project with [react-native-cli](https://www.npmjs.com/package/react-native-cli). 38 | 39 | ``` 40 | react-native link 41 | ``` 42 | 43 | This command will automatically find native dependencies and link them to your project. 44 | 45 | ## Initializing 46 | 47 | We recommend you to initialize the lib in a separate file and then import it when you need it. 48 | 49 | ```js 50 | import { Pleak } from '@pleak/react-perf-monitor'; 51 | 52 | const pleak = new Pleak({ 53 | uri: 'YOUR_PLEAK_DSN', 54 | }); 55 | 56 | export default pleak; 57 | ``` 58 | 59 | ### Options 60 | 61 | #### **Required** 62 | 63 | #### `uri` 64 | 65 | Your Pleak DSN, required to publish to Pleak. The structure of your DSN should look like this: 66 | 67 | ``` 68 | https://{publicKey}@{host}/{appId} 69 | ``` 70 | 71 | #### **Optional** 72 | 73 | #### `debug` 74 | 75 | _Defaults to false_ 76 | 77 | If true, informations about events and publishing will be logged in console. 78 | 79 | #### `publish` 80 | 81 | _Defaults to true_ 82 | 83 | If true, collected events will be published on Pleak. 84 | 85 | #### `interval` 86 | 87 | _Defaults to 5000_ 88 | 89 | Events are not published one by one, they are stored and published in batch at an interval in milliseconds defined by this option. 90 | 91 | #### `environment` 92 | 93 | _Defaults to `process.env.NODE_ENV`_ 94 | 95 | Define tracked environment of your app in Pleak. 96 | 97 | # Usage 98 | 99 | Once you installed and initialized the lib you can use it to monitor your React components like so: 100 | 101 | ```js 102 | import React, { Component } from 'react'; 103 | import pleak from '../pleak'; // Import the Pleak instance you defined earlier 104 | 105 | class MyComponent extends Component { 106 | state = { user: null }; 107 | 108 | constructor(props) { 109 | super(props); 110 | 111 | // Capture your component's performance 112 | pleak.captureComponentPerfs(this, { 113 | // Optional. Use the excludes option to avoid collecting events on specific methods 114 | excludes: ['render'], 115 | }); 116 | } 117 | 118 | componentDidMount() { 119 | /* Optional. 120 | This allows you to attach a context to any event triggered by this method */ 121 | pleak.setContext({ 122 | time: Date.now(), 123 | }); 124 | 125 | this.loadData(); 126 | } 127 | 128 | loadData = async () => { 129 | const res = await fetch('https://jsonplaceholder.typicode.com/users/1'); 130 | const user = await res.json(); 131 | 132 | /* Optional. 133 | This allows you to attach a context to all events, 134 | from the moment when this method is triggered (overwritable) */ 135 | pleak.setGlobalContext({ 136 | user, 137 | }); 138 | 139 | this.setState({ 140 | user, 141 | }); 142 | }; 143 | 144 | render() { 145 | const { user } = this.state; 146 | 147 | return
Hello, {user ? user.name : 'world'}!
; 148 | } 149 | } 150 | ``` 151 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | def DEFAULT_COMPILE_SDK_VERSION = 23 4 | def DEFAULT_BUILD_TOOLS_VERSION = "25.0.2" 5 | def DEFAULT_TARGET_SDK_VERSION = 22 6 | def DEFAULT_GOOGLE_PLAY_SERVICES_VERSION = "+" 7 | 8 | android { 9 | compileSdkVersion project.hasProperty('compileSdkVersion') ? project.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION 10 | buildToolsVersion project.hasProperty('buildToolsVersion') ? project.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION 11 | 12 | defaultConfig { 13 | minSdkVersion 16 14 | targetSdkVersion project.hasProperty('targetSdkVersion') ? project.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION 15 | versionCode 2 16 | versionName "1.1" 17 | ndk { 18 | abiFilters "armeabi-v7a", "x86" 19 | } 20 | } 21 | lintOptions { 22 | warning 'InvalidPackage' 23 | } 24 | } 25 | 26 | dependencies { 27 | def googlePlayServicesVersion = project.hasProperty('googlePlayServicesVersion') ? project.googlePlayServicesVersion : DEFAULT_GOOGLE_PLAY_SERVICES_VERSION 28 | 29 | compile 'com.facebook.react:react-native:+' 30 | compile "com.google.android.gms:play-services-gcm:$googlePlayServicesVersion" 31 | } 32 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/java/com/pleak/PleakDeviceInfo/PleakDeviceInfo.java: -------------------------------------------------------------------------------- 1 | package com.pleak.PleakDeviceInfo; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.JavaScriptModule; 5 | import com.facebook.react.bridge.NativeModule; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.uimanager.ViewManager; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class PleakDeviceInfo implements ReactPackage { 14 | 15 | @Override 16 | public List createNativeModules(ReactApplicationContext reactContext) { 17 | List modules = new ArrayList<>(); 18 | 19 | modules.add(new PleakDeviceInfoModule(reactContext)); 20 | 21 | return modules; 22 | } 23 | 24 | public List> createJSModules() { 25 | return Collections.emptyList(); 26 | } 27 | 28 | @Override 29 | public List createViewManagers(ReactApplicationContext reactContext) { 30 | return Collections.emptyList(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/src/main/java/com/pleak/PleakDeviceInfo/PleakDeviceInfoModule.java: -------------------------------------------------------------------------------- 1 | package com.pleak.PleakDeviceInfo; 2 | 3 | import android.content.pm.PackageInfo; 4 | import android.content.pm.PackageManager; 5 | import android.provider.Settings.Secure; 6 | import android.os.Build; 7 | import android.webkit.WebSettings; 8 | 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import javax.annotation.Nullable; 16 | 17 | public class PleakDeviceInfoModule extends ReactContextBaseJavaModule { 18 | ReactApplicationContext reactContext; 19 | 20 | public PleakDeviceInfoModule(ReactApplicationContext reactContext) { 21 | super(reactContext); 22 | 23 | this.reactContext = reactContext; 24 | } 25 | 26 | @Override 27 | public String getName() { 28 | return "PleakDeviceInfo"; 29 | } 30 | 31 | public String getDeviceUniqueId() { 32 | return Secure.getString(this.reactContext.getContentResolver(), Secure.ANDROID_ID); 33 | } 34 | 35 | @Override 36 | public @Nullable Map getConstants() { 37 | HashMap constants = new HashMap(); 38 | 39 | PackageManager packageManager = this.reactContext.getPackageManager(); 40 | String packageName = this.reactContext.getPackageName(); 41 | 42 | constants.put("bundleId", packageName); 43 | constants.put("appVersion", "not available"); 44 | 45 | try { 46 | PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); 47 | constants.put("appVersion", packageInfo.versionName); 48 | } catch (PackageManager.NameNotFoundException e) { 49 | e.printStackTrace(); 50 | } 51 | 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 53 | try { 54 | constants.put("userAgent", WebSettings.getDefaultUserAgent(this.reactContext)); 55 | } catch (RuntimeException e) { 56 | constants.put("userAgent", System.getProperty("http.agent")); 57 | } 58 | } 59 | 60 | constants.put("brand", Build.BRAND); 61 | constants.put("model", Build.MODEL); 62 | constants.put("deviceUniqueId", this.getDeviceUniqueId()); 63 | constants.put("systemName", "Android"); 64 | constants.put("systemVersion", Build.VERSION.RELEASE); 65 | 66 | return constants; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleak/pleak-react-perf-monitor/a57bd4d85c152246fea7c6faca1887a355e72b20/assets/logo.png -------------------------------------------------------------------------------- /ios/PleakDeviceInfo/PleakDeviceInfo.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | 7 | @interface PleakDeviceInfo : NSObject 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /ios/PleakDeviceInfo/PleakDeviceInfo.m: -------------------------------------------------------------------------------- 1 | #import "PleakDeviceInfo.h" 2 | 3 | @implementation PleakDeviceInfo 4 | 5 | RCT_EXPORT_MODULE() 6 | 7 | + (BOOL) requiresMainQueueSetup { 8 | return YES; 9 | } 10 | 11 | - (NSString *) userAgent 12 | { 13 | if (TARGET_OS_TV) { 14 | return @"OS TV, user agent not available"; 15 | } else { 16 | UIWebView* webView = [[UIWebView alloc] initWithFrame:CGRectZero]; 17 | return [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; 18 | } 19 | } 20 | 21 | - (NSString *) deviceId 22 | { 23 | struct utsname systemInfo; 24 | 25 | uname(&systemInfo); 26 | 27 | NSString* deviceId = [NSString stringWithCString:systemInfo.machine 28 | encoding:NSUTF8StringEncoding]; 29 | 30 | if ([deviceId isEqualToString:@"i386"] || [deviceId isEqualToString:@"x86_64"] ) { 31 | deviceId = [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")]; 32 | } 33 | 34 | return deviceId; 35 | } 36 | 37 | - (NSString *) model 38 | { 39 | static NSDictionary* modelsById = nil; 40 | 41 | if (!modelsById) { 42 | modelsById = @{ 43 | @"iPod1,1": @"iPod Touch", // (Original) 44 | @"iPod2,1": @"iPod Touch", // (Second Generation) 45 | @"iPod3,1": @"iPod Touch", // (Third Generation) 46 | @"iPod4,1": @"iPod Touch", // (Fourth Generation) 47 | @"iPod5,1": @"iPod Touch", // (Fifth Generation) 48 | @"iPod7,1": @"iPod Touch", // (Sixth Generation) 49 | @"iPhone1,1": @"iPhone", // (Original) 50 | @"iPhone1,2": @"iPhone 3G", // (3G) 51 | @"iPhone2,1": @"iPhone 3GS", // (3GS) 52 | @"iPad1,1": @"iPad", // (Original) 53 | @"iPad2,1": @"iPad 2", 54 | @"iPad2,2": @"iPad 2", 55 | @"iPad2,3": @"iPad 2", 56 | @"iPad2,4": @"iPad 2", 57 | @"iPad3,1": @"iPad", // (3rd Generation) 58 | @"iPad3,2": @"iPad", // (3rd Generation) 59 | @"iPad3,3": @"iPad", // (3rd Generation) 60 | @"iPhone3,1": @"iPhone 4", // (GSM) 61 | @"iPhone3,2": @"iPhone 4", // iPhone 4 62 | @"iPhone3,3": @"iPhone 4", // (CDMA/Verizon/Sprint) 63 | @"iPhone4,1": @"iPhone 4S", 64 | @"iPhone5,1": @"iPhone 5", // (model A1428, AT&T/Canada) 65 | @"iPhone5,2": @"iPhone 5", // (model A1429, everything else) 66 | @"iPad3,4": @"iPad", // (4th Generation) 67 | @"iPad3,5": @"iPad", // (4th Generation) 68 | @"iPad3,6": @"iPad", // (4th Generation) 69 | @"iPad2,5": @"iPad Mini", // (Original) 70 | @"iPad2,6": @"iPad Mini", // (Original) 71 | @"iPad2,7": @"iPad Mini", // (Original) 72 | @"iPhone5,3": @"iPhone 5c", // (model A1456, A1532 | GSM) 73 | @"iPhone5,4": @"iPhone 5c", // (model A1507, A1516, A1526 (China), A1529 | Global) 74 | @"iPhone6,1": @"iPhone 5s", // (model A1433, A1533 | GSM) 75 | @"iPhone6,2": @"iPhone 5s", // (model A1457, A1518, A1528 (China), A1530 | Global) 76 | @"iPhone7,1": @"iPhone 6 Plus", 77 | @"iPhone7,2": @"iPhone 6", 78 | @"iPhone8,1": @"iPhone 6s", 79 | @"iPhone8,2": @"iPhone 6s Plus", 80 | @"iPhone8,4": @"iPhone SE", 81 | @"iPhone9,1": @"iPhone 7", // (model A1660 | CDMA) 82 | @"iPhone9,3": @"iPhone 7", // (model A1778 | Global) 83 | @"iPhone9,2": @"iPhone 7 Plus", // (model A1661 | CDMA) 84 | @"iPhone9,4": @"iPhone 7 Plus", // (model A1784 | Global) 85 | @"iPhone10,3": @"iPhone X", // (model A1865, A1902) 86 | @"iPhone10,6": @"iPhone X", // (model A1901) 87 | @"iPhone10,1": @"iPhone 8", // (model A1863, A1906, A1907) 88 | @"iPhone10,4": @"iPhone 8", // (model A1905) 89 | @"iPhone10,2": @"iPhone 8 Plus", // (model A1864, A1898, A1899) 90 | @"iPhone10,5": @"iPhone 8 Plus", // (model A1897) 91 | @"iPad4,1": @"iPad Air", // 5th Generation iPad (iPad Air) - Wifi 92 | @"iPad4,2": @"iPad Air", // 5th Generation iPad (iPad Air) - Cellular 93 | @"iPad4,3": @"iPad Air", // 5th Generation iPad (iPad Air) 94 | @"iPad4,4": @"iPad Mini 2", // (2nd Generation iPad Mini - Wifi) 95 | @"iPad4,5": @"iPad Mini 2", // (2nd Generation iPad Mini - Cellular) 96 | @"iPad4,6": @"iPad Mini 2", // (2nd Generation iPad Mini) 97 | @"iPad4,7": @"iPad Mini 3", // (3rd Generation iPad Mini) 98 | @"iPad4,8": @"iPad Mini 3", // (3rd Generation iPad Mini) 99 | @"iPad4,9": @"iPad Mini 3", // (3rd Generation iPad Mini) 100 | @"iPad5,1": @"iPad Mini 4", // (4th Generation iPad Mini) 101 | @"iPad5,2": @"iPad Mini 4", // (4th Generation iPad Mini) 102 | @"iPad5,3": @"iPad Air 2", // 6th Generation iPad (iPad Air 2) 103 | @"iPad5,4": @"iPad Air 2", // 6th Generation iPad (iPad Air 2) 104 | @"iPad6,3": @"iPad Pro 9.7-inch", // iPad Pro 9.7-inch 105 | @"iPad6,4": @"iPad Pro 9.7-inch", // iPad Pro 9.7-inch 106 | @"iPad6,7": @"iPad Pro 12.9-inch", // iPad Pro 12.9-inch 107 | @"iPad6,8": @"iPad Pro 12.9-inch", // iPad Pro 12.9-inch 108 | @"iPad7,1": @"iPad Pro 12.9-inch", // 2nd Generation iPad Pro 12.5-inch - Wifi 109 | @"iPad7,2": @"iPad Pro 12.9-inch", // 2nd Generation iPad Pro 12.5-inch - Cellular 110 | @"iPad7,3": @"iPad Pro 10.5-inch", // iPad Pro 10.5-inch - Wifi 111 | @"iPad7,4": @"iPad Pro 10.5-inch", // iPad Pro 10.5-inch - Cellular 112 | @"AppleTV2,1": @"Apple TV", // Apple TV (2nd Generation) 113 | @"AppleTV3,1": @"Apple TV", // Apple TV (3rd Generation) 114 | @"AppleTV3,2": @"Apple TV", // Apple TV (3rd Generation - Rev A) 115 | @"AppleTV5,3": @"Apple TV", // Apple TV (4th Generation) 116 | @"AppleTV6,2": @"Apple TV 4K", // Apple TV 4K 117 | }; 118 | } 119 | 120 | NSString* model = [modelsById objectForKey:self.deviceId]; 121 | 122 | if (!model) { 123 | if ([self.deviceId rangeOfString:@"iPod"].location != NSNotFound) { 124 | model = @"iPod Touch"; 125 | } else if ([self.deviceId rangeOfString:@"iPad"].location != NSNotFound) { 126 | model = @"iPad"; 127 | } else if ([self.deviceId rangeOfString:@"iPhone"].location != NSNotFound) { 128 | model = @"iPhone"; 129 | } else if ([self.deviceId rangeOfString:@"AppleTV"].location != NSNotFound) { 130 | model = @"Apple TV"; 131 | } 132 | } 133 | 134 | return model; 135 | } 136 | 137 | - (NSString *) bundleId 138 | { 139 | return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; 140 | } 141 | 142 | - (NSString *) appVersion 143 | { 144 | return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 145 | } 146 | 147 | - (NSString *) systemName 148 | { 149 | return self.currentDevice.systemName; 150 | } 151 | 152 | - (NSString *) systemVersion 153 | { 154 | return self.currentDevice.systemVersion; 155 | } 156 | 157 | - (NSString *) deviceUniqueId 158 | { 159 | return [[self.currentDevice identifierForVendor]UUIDString]; 160 | } 161 | 162 | - (UIDevice *) currentDevice 163 | { 164 | return [UIDevice currentDevice]; 165 | } 166 | 167 | - (NSDictionary *) constantsToExport 168 | { 169 | return @{ 170 | @"userAgent": self.userAgent ?: [NSNull null], 171 | @"brand": @"Apple", 172 | @"model": self.model ?: [NSNull null], 173 | @"deviceUniqueId": self.deviceUniqueId ?: [NSNull null], 174 | @"bundleId": self.bundleId ?: [NSNull null], 175 | @"appVersion": self.appVersion ?: [NSNull null], 176 | @"systemName": self.systemName, 177 | @"systemVersion": self.systemVersion, 178 | }; 179 | } 180 | 181 | @end 182 | -------------------------------------------------------------------------------- /ios/PleakDeviceInfo/PleakDeviceInfo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13BE3DEE1AC21097009241FE /* PleakDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* PleakDeviceInfo.m */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = "include/$(PRODUCT_NAME)"; 18 | dstSubfolderSpec = 16; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 0; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 134814201AA4EA6300B7C361 /* libPleakDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPleakDeviceInfo.a; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 13BE3DEC1AC21097009241FE /* PleakDeviceInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PleakDeviceInfo.h; sourceTree = ""; }; 28 | 13BE3DED1AC21097009241FE /* PleakDeviceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PleakDeviceInfo.m; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 134814211AA4EA7D00B7C361 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | 134814201AA4EA6300B7C361 /* libPleakDeviceInfo.a */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | 58B511D21A9E6C8500147676 = { 51 | isa = PBXGroup; 52 | children = ( 53 | 13BE3DEC1AC21097009241FE /* PleakDeviceInfo.h */, 54 | 13BE3DED1AC21097009241FE /* PleakDeviceInfo.m */, 55 | 134814211AA4EA7D00B7C361 /* Products */, 56 | ); 57 | indentWidth = 2; 58 | sourceTree = ""; 59 | tabWidth = 2; 60 | usesTabs = 0; 61 | }; 62 | /* End PBXGroup section */ 63 | 64 | /* Begin PBXNativeTarget section */ 65 | 58B511DA1A9E6C8500147676 /* PleakDeviceInfo */ = { 66 | isa = PBXNativeTarget; 67 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "PleakDeviceInfo" */; 68 | buildPhases = ( 69 | 58B511D71A9E6C8500147676 /* Sources */, 70 | 58B511D81A9E6C8500147676 /* Frameworks */, 71 | 58B511D91A9E6C8500147676 /* CopyFiles */, 72 | ); 73 | buildRules = ( 74 | ); 75 | dependencies = ( 76 | ); 77 | name = PleakDeviceInfo; 78 | productName = RCTDataManager; 79 | productReference = 134814201AA4EA6300B7C361 /* libPleakDeviceInfo.a */; 80 | productType = "com.apple.product-type.library.static"; 81 | }; 82 | /* End PBXNativeTarget section */ 83 | 84 | /* Begin PBXProject section */ 85 | 58B511D31A9E6C8500147676 /* Project object */ = { 86 | isa = PBXProject; 87 | attributes = { 88 | LastUpgradeCheck = 0610; 89 | ORGANIZATIONNAME = Facebook; 90 | TargetAttributes = { 91 | 58B511DA1A9E6C8500147676 = { 92 | CreatedOnToolsVersion = 6.1.1; 93 | }; 94 | }; 95 | }; 96 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "PleakDeviceInfo" */; 97 | compatibilityVersion = "Xcode 3.2"; 98 | developmentRegion = English; 99 | hasScannedForEncodings = 0; 100 | knownRegions = ( 101 | en, 102 | ); 103 | mainGroup = 58B511D21A9E6C8500147676; 104 | productRefGroup = 58B511D21A9E6C8500147676; 105 | projectDirPath = ""; 106 | projectRoot = ""; 107 | targets = ( 108 | 58B511DA1A9E6C8500147676 /* PleakDeviceInfo */, 109 | ); 110 | }; 111 | /* End PBXProject section */ 112 | 113 | /* Begin PBXSourcesBuildPhase section */ 114 | 58B511D71A9E6C8500147676 /* Sources */ = { 115 | isa = PBXSourcesBuildPhase; 116 | buildActionMask = 2147483647; 117 | files = ( 118 | 13BE3DEE1AC21097009241FE /* PleakDeviceInfo.m in Sources */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | /* End PBXSourcesBuildPhase section */ 123 | 124 | /* Begin XCBuildConfiguration section */ 125 | 58B511ED1A9E6C8500147676 /* Debug */ = { 126 | isa = XCBuildConfiguration; 127 | buildSettings = { 128 | ALWAYS_SEARCH_USER_PATHS = NO; 129 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 130 | CLANG_CXX_LIBRARY = "libc++"; 131 | CLANG_ENABLE_MODULES = YES; 132 | CLANG_ENABLE_OBJC_ARC = YES; 133 | CLANG_WARN_BOOL_CONVERSION = YES; 134 | CLANG_WARN_CONSTANT_CONVERSION = YES; 135 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 136 | CLANG_WARN_EMPTY_BODY = YES; 137 | CLANG_WARN_ENUM_CONVERSION = YES; 138 | CLANG_WARN_INT_CONVERSION = YES; 139 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 140 | CLANG_WARN_UNREACHABLE_CODE = YES; 141 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 142 | COPY_PHASE_STRIP = NO; 143 | ENABLE_STRICT_OBJC_MSGSEND = YES; 144 | GCC_C_LANGUAGE_STANDARD = gnu99; 145 | GCC_DYNAMIC_NO_PIC = NO; 146 | GCC_OPTIMIZATION_LEVEL = 0; 147 | GCC_PREPROCESSOR_DEFINITIONS = ( 148 | "DEBUG=1", 149 | "$(inherited)", 150 | ); 151 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 152 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 153 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 154 | GCC_WARN_UNDECLARED_SELECTOR = YES; 155 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 156 | GCC_WARN_UNUSED_FUNCTION = YES; 157 | GCC_WARN_UNUSED_VARIABLE = YES; 158 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 159 | MTL_ENABLE_DEBUG_INFO = YES; 160 | ONLY_ACTIVE_ARCH = YES; 161 | SDKROOT = iphoneos; 162 | }; 163 | name = Debug; 164 | }; 165 | 58B511EE1A9E6C8500147676 /* Release */ = { 166 | isa = XCBuildConfiguration; 167 | buildSettings = { 168 | ALWAYS_SEARCH_USER_PATHS = NO; 169 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 170 | CLANG_CXX_LIBRARY = "libc++"; 171 | CLANG_ENABLE_MODULES = YES; 172 | CLANG_ENABLE_OBJC_ARC = YES; 173 | CLANG_WARN_BOOL_CONVERSION = YES; 174 | CLANG_WARN_CONSTANT_CONVERSION = YES; 175 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 176 | CLANG_WARN_EMPTY_BODY = YES; 177 | CLANG_WARN_ENUM_CONVERSION = YES; 178 | CLANG_WARN_INT_CONVERSION = YES; 179 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 180 | CLANG_WARN_UNREACHABLE_CODE = YES; 181 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 182 | COPY_PHASE_STRIP = YES; 183 | ENABLE_NS_ASSERTIONS = NO; 184 | ENABLE_STRICT_OBJC_MSGSEND = YES; 185 | GCC_C_LANGUAGE_STANDARD = gnu99; 186 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 187 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 188 | GCC_WARN_UNDECLARED_SELECTOR = YES; 189 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 190 | GCC_WARN_UNUSED_FUNCTION = YES; 191 | GCC_WARN_UNUSED_VARIABLE = YES; 192 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 193 | MTL_ENABLE_DEBUG_INFO = NO; 194 | SDKROOT = iphoneos; 195 | VALIDATE_PRODUCT = YES; 196 | }; 197 | name = Release; 198 | }; 199 | 58B511F01A9E6C8500147676 /* Debug */ = { 200 | isa = XCBuildConfiguration; 201 | buildSettings = { 202 | HEADER_SEARCH_PATHS = ( 203 | "$(inherited)", 204 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 205 | "$(SRCROOT)/../../React/**", 206 | "$(SRCROOT)/../../node_modules/react-native/React/**", 207 | ); 208 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 209 | OTHER_LDFLAGS = "-ObjC"; 210 | PRODUCT_NAME = PleakDeviceInfo; 211 | SKIP_INSTALL = YES; 212 | }; 213 | name = Debug; 214 | }; 215 | 58B511F11A9E6C8500147676 /* Release */ = { 216 | isa = XCBuildConfiguration; 217 | buildSettings = { 218 | HEADER_SEARCH_PATHS = ( 219 | "$(inherited)", 220 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 221 | "$(SRCROOT)/../../React/**", 222 | ); 223 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 224 | OTHER_LDFLAGS = "-ObjC"; 225 | PRODUCT_NAME = PleakDeviceInfo; 226 | SKIP_INSTALL = YES; 227 | }; 228 | name = Release; 229 | }; 230 | /* End XCBuildConfiguration section */ 231 | 232 | /* Begin XCConfigurationList section */ 233 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "PleakDeviceInfo" */ = { 234 | isa = XCConfigurationList; 235 | buildConfigurations = ( 236 | 58B511ED1A9E6C8500147676 /* Debug */, 237 | 58B511EE1A9E6C8500147676 /* Release */, 238 | ); 239 | defaultConfigurationIsVisible = 0; 240 | defaultConfigurationName = Release; 241 | }; 242 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "PleakDeviceInfo" */ = { 243 | isa = XCConfigurationList; 244 | buildConfigurations = ( 245 | 58B511F01A9E6C8500147676 /* Debug */, 246 | 58B511F11A9E6C8500147676 /* Release */, 247 | ); 248 | defaultConfigurationIsVisible = 0; 249 | defaultConfigurationName = Release; 250 | }; 251 | /* End XCConfigurationList section */ 252 | }; 253 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 254 | } 255 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: ['/node_modules/'], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pleak/react-perf-monitor", 3 | "description": 4 | "Performance monitoring for React and React Native apps with Pleak.", 5 | "version": "0.1.4", 6 | "license": "BSD-2-Clause", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "build": "rollup -c", 10 | "watch": "rollup -c -w", 11 | "prepare": "npm run build", 12 | "lint": "eslint --ext .js ./src", 13 | "lint:fix": "eslint --fix --ext .js ./src", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "precommit": "lint-staged" 17 | }, 18 | "dependencies": { 19 | "cross-fetch": "^2.2.1", 20 | "fbjs": "^0.8.16", 21 | "uuid": "^3.2.1" 22 | }, 23 | "devDependencies": { 24 | "babel-core": "^6.26.0", 25 | "babel-eslint": "^8.2.3", 26 | "babel-plugin-external-helpers": "^6.22.0", 27 | "babel-plugin-transform-class-properties": "^6.24.1", 28 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 29 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 30 | "babel-plugin-transform-runtime": "^6.23.0", 31 | "babel-preset-env": "^1.6.1", 32 | "eslint": "^4.19.1", 33 | "eslint-config-airbnb": "^16.1.0", 34 | "eslint-config-prettier": "^2.9.0", 35 | "eslint-plugin-import": "^2.11.0", 36 | "eslint-plugin-jest": "^21.15.0", 37 | "eslint-plugin-jsx-a11y": "^6.0.3", 38 | "eslint-plugin-prettier": "^2.6.0", 39 | "eslint-plugin-react": "^7.7.0", 40 | "husky": "^0.14.3", 41 | "jest": "^22.4.3", 42 | "lint-staged": "^7.0.4", 43 | "prettier": "^1.12.0", 44 | "rollup": "^0.57.1", 45 | "rollup-plugin-babel": "^3.0.3", 46 | "rollup-plugin-node-resolve": "^3.3.0", 47 | "rollup-watch": "^4.3.1" 48 | }, 49 | "files": ["src", "lib", "ios", "android"], 50 | "keywords": ["react", "react-native", "performance"] 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: { 7 | file: 'lib/index.js', 8 | format: 'cjs', 9 | }, 10 | external: [ 11 | 'fbjs/lib/performanceNow', 12 | 'uuid/v4', 13 | 'cross-fetch', 14 | 'react-native', 15 | 'react-native-web', 16 | ], 17 | plugins: [ 18 | babel({ 19 | exclude: 'node_modules/**', 20 | }), 21 | resolve(), 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /src/Pleak.js: -------------------------------------------------------------------------------- 1 | import performanceNow from 'fbjs/lib/performanceNow'; 2 | import uuid from 'uuid/v4'; 3 | import { isNotAvoidedProperty, isPropertyValid } from './utils'; 4 | import { 5 | measureTiming, 6 | getMethodType, 7 | parsePleakUri, 8 | } from './utils/pleakUtils'; 9 | import { PleakContext } from './PleakContext'; 10 | import { PleakBatchPublisher } from './PleakBatchPublisher'; 11 | import { getSystemPayload } from './utils/deviceUtils'; 12 | 13 | export class Pleak { 14 | constructor({ 15 | uri, 16 | debug = false, 17 | publish = true, 18 | interval = 5000, 19 | environment = process.env.NODE_ENV, 20 | } = {}) { 21 | this.debug = debug; 22 | this.environment = environment; 23 | 24 | this.system = getSystemPayload(); 25 | 26 | this.context = new PleakContext(); 27 | this.batchPublisher = new PleakBatchPublisher({ 28 | parsedUrl: parsePleakUri(uri), 29 | debug, 30 | publish, 31 | interval, 32 | }); 33 | 34 | this.batchPublisher.run(); 35 | } 36 | 37 | setContext = context => this.context.setContext(context); 38 | 39 | setGlobalContext = context => this.context.setGlobalContext(context); 40 | 41 | createPayload = ({ component, method, timing, context, timestamp }) => ({ 42 | informations: { 43 | uuid: uuid(), 44 | component, 45 | method, 46 | timestamp, 47 | environment: this.environment, 48 | type: getMethodType(method), 49 | }, 50 | system: this.system, 51 | metrics: { timing }, 52 | context, 53 | }); 54 | 55 | processResult = ({ 56 | result, 57 | component, 58 | method, 59 | timing, 60 | context, 61 | timestamp, 62 | }) => { 63 | const payload = this.createPayload({ 64 | component, 65 | method, 66 | timing, 67 | context, 68 | timestamp, 69 | }); 70 | 71 | this.batchPublisher.pushPayload(payload); 72 | 73 | if (this.debug) console.info('[PLEAK] Event', payload); 74 | 75 | return result; 76 | }; 77 | 78 | captureComponentPerfs = ( 79 | instance, 80 | { identifier, excludes = ['constructor'] } = {} 81 | ) => { 82 | const component = 83 | identifier || 84 | instance.constructor.displayName || 85 | instance.constructor.name || 86 | 'Component'; 87 | 88 | const properties = [ 89 | ...Object.getOwnPropertyNames(instance).filter(isNotAvoidedProperty), 90 | ...Object.getOwnPropertyNames(Object.getPrototypeOf(instance)), 91 | ]; 92 | 93 | properties.filter(isPropertyValid(instance, excludes)).forEach(method => { 94 | const fn = instance[method]; 95 | 96 | instance[method] = (...args) => { 97 | const timestamp = Date.now(); 98 | const start = performanceNow(); 99 | const result = fn.call(instance, ...args); 100 | 101 | const context = this.context.getContextPayload(); 102 | this.context.resetContext(); 103 | 104 | if (result && result.then) { 105 | return result.then(res => 106 | this.processResult({ 107 | result: res, 108 | component, 109 | method, 110 | timing: measureTiming(start), 111 | context, 112 | timestamp, 113 | }) 114 | ); 115 | } 116 | 117 | return this.processResult({ 118 | result, 119 | component, 120 | method, 121 | timing: measureTiming(start), 122 | context, 123 | timestamp, 124 | }); 125 | }; 126 | }); 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /src/Pleak.test.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4'; 2 | import performanceNow from 'fbjs/lib/performanceNow'; 3 | import { Pleak } from './Pleak'; 4 | import { getSystemPayload } from './utils/deviceUtils'; 5 | 6 | jest.mock('uuid/v4'); 7 | 8 | jest.mock('fbjs/lib/performanceNow'); 9 | 10 | jest.mock('./utils/deviceUtils.js', () => ({ 11 | getSystemPayload: jest.fn(), 12 | })); 13 | 14 | jest.mock('./PleakBatchPublisher.js', () => ({ 15 | PleakBatchPublisher: class { 16 | run = () => jest.fn(); 17 | }, 18 | })); 19 | 20 | describe('Pleak', () => { 21 | const systemPayload = { 22 | userAgent: 'USER_AGENT', 23 | brand: 'DEVICE_BRAND', 24 | model: 'DEVICE_MODEL', 25 | uniqueId: 'DEVICE_UNIQUE_ID', 26 | appId: 'APP_ID', 27 | appVersion: 'APP_VERSION', 28 | systemName: 'SYSTEM_NAME', 29 | systemVersion: 'SYSTEM_VERSION', 30 | }; 31 | 32 | performanceNow.mockReturnValue(1000); 33 | getSystemPayload.mockReturnValue(systemPayload); 34 | uuid.mockReturnValue('this-should-be-an-uuid'); 35 | 36 | const pleak = new Pleak({ 37 | uri: 'https://this-is-a-public-key@getpleak.io/thisisanappid', 38 | environment: 'test', 39 | }); 40 | 41 | beforeEach(() => { 42 | pleak.context.resetContext(); 43 | pleak.context.resetGlobalContext(); 44 | }); 45 | 46 | describe('createPayload', () => { 47 | it('should return a payload', () => { 48 | expect( 49 | pleak.createPayload({ 50 | component: 'App', 51 | method: 'componentDidMount', 52 | timing: '12.20000', 53 | context: { user: 'Bob' }, 54 | timestamp: 123456789, 55 | }) 56 | ).toEqual({ 57 | informations: { 58 | uuid: 'this-should-be-an-uuid', 59 | component: 'App', 60 | method: 'componentDidMount', 61 | timestamp: 123456789, 62 | environment: 'test', 63 | type: 'LIFECYCLE', 64 | }, 65 | system: systemPayload, 66 | metrics: { timing: '12.20000' }, 67 | context: { user: 'Bob' }, 68 | }); 69 | }); 70 | }); 71 | 72 | describe('setContext', () => { 73 | it('should set the context of the instance', () => { 74 | const fakeContext = { 75 | user: 'Bob', 76 | }; 77 | 78 | const contextSpy = jest.spyOn(pleak.context, 'setContext'); 79 | 80 | pleak.setContext(fakeContext); 81 | expect(contextSpy).toHaveBeenCalledWith(fakeContext); 82 | expect(pleak.context.getContext()).toEqual(fakeContext); 83 | }); 84 | }); 85 | 86 | describe('setGlobalContext', () => { 87 | it('should set the global context of the instance', () => { 88 | const fakeGlobalContext = { 89 | locale: 'en', 90 | }; 91 | 92 | const contextSpy = jest.spyOn(pleak.context, 'setGlobalContext'); 93 | 94 | pleak.setGlobalContext(fakeGlobalContext); 95 | expect(contextSpy).toHaveBeenCalledWith(fakeGlobalContext); 96 | expect(pleak.context.getGlobalContext()).toEqual(fakeGlobalContext); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/PleakBatchPublisher.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | 3 | export class PleakBatchPublisher { 4 | constructor({ 5 | parsedUrl = {}, 6 | debug = false, 7 | publish = true, 8 | interval = 5000, 9 | } = {}) { 10 | this.parsedUrl = parsedUrl; 11 | this.debug = debug; 12 | this.publish = publish; 13 | this.interval = interval; 14 | 15 | const { protocol, host, appId, publicKey } = this.parsedUrl; 16 | this.url = `${protocol}://${host}/collect/${appId}`; 17 | this.publicKey = publicKey; 18 | 19 | this.batchedPayloads = []; 20 | } 21 | 22 | pushPayload = payload => { 23 | this.batchedPayloads = [...this.batchedPayloads, payload]; 24 | }; 25 | 26 | clearPayloads = () => { 27 | this.batchedPayloads = []; 28 | }; 29 | 30 | publishEvents = () => { 31 | if (this.batchedPayloads.length > 0 && this.publish) { 32 | if (this.debug) { 33 | console.info('[PLEAK] Publishing events', this.batchedPayloads); 34 | } 35 | 36 | fetch(this.url, { 37 | method: 'POST', 38 | headers: { 39 | 'content-type': 'application/json', 40 | authorization: this.publicKey, 41 | }, 42 | body: JSON.stringify({ 43 | events: this.batchedPayloads, 44 | }), 45 | }); 46 | 47 | this.clearPayloads(); 48 | } 49 | }; 50 | 51 | run = () => { 52 | this.batchInterval = setInterval(this.publishEvents, this.interval); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/PleakBatchPublisher.test.js: -------------------------------------------------------------------------------- 1 | import { PleakBatchPublisher } from './PleakBatchPublisher'; 2 | 3 | jest.mock('cross-fetch'); 4 | jest.useFakeTimers(); 5 | 6 | describe('PleakBatchPublisher', () => { 7 | describe('payloads management', () => { 8 | const pleakBatchPublisher = new PleakBatchPublisher(); 9 | 10 | beforeEach(() => { 11 | pleakBatchPublisher.batchedPayloads = []; 12 | }); 13 | 14 | it('should be able to push payloads', () => { 15 | pleakBatchPublisher.pushPayload({ test: 'payloadTest' }); 16 | 17 | expect(pleakBatchPublisher.batchedPayloads).toEqual([ 18 | { test: 'payloadTest' }, 19 | ]); 20 | }); 21 | 22 | it('should be able to clear payloads', () => { 23 | pleakBatchPublisher.pushPayload({ test: 'payloadTest' }); 24 | pleakBatchPublisher.clearPayloads(); 25 | 26 | expect(pleakBatchPublisher.batchedPayloads).toEqual([]); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/PleakContext.js: -------------------------------------------------------------------------------- 1 | import { isObject } from './utils'; 2 | 3 | const BASE_CONTEXT = {}; 4 | 5 | const PLEAK_CONTEXT = Symbol('PLEAK_CONTEXT'); 6 | const PLEAK_GLOBAL_CONTEXT = Symbol('PLEAK_GLOBAL_CONTEXT'); 7 | 8 | export class PleakContext { 9 | constructor() { 10 | this[PLEAK_CONTEXT] = BASE_CONTEXT; 11 | this[PLEAK_GLOBAL_CONTEXT] = BASE_CONTEXT; 12 | } 13 | 14 | getContext = () => ({ 15 | ...this[PLEAK_CONTEXT], 16 | }); 17 | 18 | getGlobalContext = () => ({ 19 | ...this[PLEAK_GLOBAL_CONTEXT], 20 | }); 21 | 22 | getContextPayload = () => ({ 23 | ...this.getContext(), 24 | ...this.getGlobalContext(), 25 | }); 26 | 27 | resetContext = () => { 28 | this[PLEAK_CONTEXT] = BASE_CONTEXT; 29 | }; 30 | 31 | resetGlobalContext = () => { 32 | this[PLEAK_GLOBAL_CONTEXT] = BASE_CONTEXT; 33 | }; 34 | 35 | setContext = context => { 36 | isObject(context) 37 | ? (this[PLEAK_CONTEXT] = context) 38 | : console.error('[PLEAK] Context should be an object'); 39 | }; 40 | 41 | setGlobalContext = context => { 42 | isObject(context) 43 | ? (this[PLEAK_GLOBAL_CONTEXT] = context) 44 | : console.error('[PLEAK] Global context should be an object'); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/PleakContext.test.js: -------------------------------------------------------------------------------- 1 | import { PleakContext } from './PleakContext'; 2 | 3 | describe('PleakContext', () => { 4 | const context = new PleakContext(); 5 | 6 | beforeEach(() => { 7 | context.resetContext(); 8 | context.resetGlobalContext(); 9 | }); 10 | 11 | it('should have its context and globalContext be a base context', () => { 12 | expect(context.getContext()).toEqual({}); 13 | expect(context.getGlobalContext()).toEqual({}); 14 | }); 15 | 16 | it('should be possible to set context and global context and get a context payload', () => { 17 | context.setContext({ user: 'Bob' }); 18 | expect(context.getContext()).toEqual({ user: 'Bob' }); 19 | expect(context.getGlobalContext()).toEqual({}); 20 | expect(context.getContextPayload()).toEqual({ user: 'Bob' }); 21 | 22 | context.setGlobalContext({ locale: 'en' }); 23 | 24 | expect(context.getContext()).toEqual({ user: 'Bob' }); 25 | expect(context.getGlobalContext()).toEqual({ locale: 'en' }); 26 | expect(context.getContextPayload()).toEqual({ user: 'Bob', locale: 'en' }); 27 | }); 28 | 29 | it('should be possible to reset context and global context', () => { 30 | context.setContext({ user: 'Bob' }); 31 | context.resetContext(); 32 | expect(context.getContext()).toEqual({}); 33 | expect(context.getContextPayload()).toEqual({}); 34 | 35 | context.setGlobalContext({ locale: 'en' }); 36 | context.resetGlobalContext(); 37 | expect(context.getGlobalContext()).toEqual({}); 38 | expect(context.getContextPayload()).toEqual({}); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { Pleak } from './Pleak'; 2 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const AVOIDED_PROPERTIES = [ 2 | 'props', 3 | 'refs', 4 | 'context', 5 | 'updater', 6 | 'state', 7 | ]; 8 | 9 | export const METHOD_TYPES = { 10 | COMPONENT_METHOD: 'COMPONENT_METHOD', 11 | RENDER: 'RENDER', 12 | LIFECYCLE: 'LIFECYCLE', 13 | }; 14 | 15 | export const LIFECYCLES = [ 16 | 'componentWillMount', 17 | 'UNSAFE_componentWillMount', 18 | 'componentDidMount', 19 | 'componentDidUpdate', 20 | 'componentWillUpdate', 21 | 'UNSAFE_componentWillUpdate', 22 | 'componentWillReceiveProps', 23 | 'UNSAFE_componentWillReceiveProps', 24 | 'shouldComponentUpdate', 25 | 'getSnapshotBeforeUpdate', 26 | 'componentWillUnmount', 27 | 'componentDidCatch', 28 | ]; 29 | export const RENDER_METHOD = 'render'; 30 | -------------------------------------------------------------------------------- /src/utils/deviceUtils.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4'; 2 | 3 | let PleakDeviceInfo; 4 | 5 | const isWeb = () => 6 | window.navigator !== undefined && window.navigator.product !== 'ReactNative'; 7 | 8 | if (!isWeb()) { 9 | try { 10 | // eslint-disable-next-line global-require 11 | const { NativeModules } = require('react-native'); 12 | 13 | // eslint-disable-next-line prefer-destructuring 14 | PleakDeviceInfo = NativeModules.PleakDeviceInfo; 15 | } catch (err) { 16 | throw err; 17 | } 18 | } 19 | 20 | const getPleakCookieId = () => { 21 | const pleakCookie = document.cookie.replace( 22 | /(?:(?:^|.*;\s*)_pleak\s*=\s*([^;]*).*$)|^.*$/, 23 | '$1' 24 | ); 25 | if (pleakCookie.length > 0) { 26 | return pleakCookie; 27 | } 28 | const cookie = uuid(); 29 | document.cookie = `_pleak=${cookie}`; 30 | return cookie; 31 | }; 32 | 33 | const USER_AGENT = isWeb() 34 | ? window.navigator.userAgent 35 | : PleakDeviceInfo.userAgent; 36 | const DEVICE_MODEL = isWeb() ? undefined : PleakDeviceInfo.model; 37 | const DEVICE_BRAND = isWeb() ? undefined : PleakDeviceInfo.brand; 38 | const DEVICE_UNIQUE_ID = isWeb() 39 | ? getPleakCookieId() 40 | : PleakDeviceInfo.deviceUniqueId; 41 | const APP_ID = isWeb() ? window.location.hostname : PleakDeviceInfo.bundleId; 42 | const APP_VERSION = isWeb() ? undefined : PleakDeviceInfo.appVersion; 43 | const SYSTEM_NAME = isWeb() ? undefined : PleakDeviceInfo.systemName; 44 | const SYSTEM_VERSION = isWeb() ? undefined : PleakDeviceInfo.systemVersion; 45 | 46 | export const getSystemPayload = () => ({ 47 | userAgent: USER_AGENT, 48 | brand: DEVICE_BRAND, 49 | model: DEVICE_MODEL, 50 | uniqueId: DEVICE_UNIQUE_ID, 51 | appIdentifierUrl: APP_ID, 52 | appVersion: APP_VERSION, 53 | systemName: SYSTEM_NAME, 54 | systemVersion: SYSTEM_VERSION, 55 | }); 56 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { AVOIDED_PROPERTIES } from './constants'; 2 | 3 | export const isNotAvoidedProperty = property => 4 | !AVOIDED_PROPERTIES.includes(property); 5 | 6 | export const isPropertyValid = (instance, excludes = []) => property => 7 | typeof instance[property] === 'function' && !excludes.includes(property); 8 | 9 | export const isObject = obj => typeof obj === 'object' && obj !== null; 10 | -------------------------------------------------------------------------------- /src/utils/index.test.js: -------------------------------------------------------------------------------- 1 | import { isNotAvoidedProperty, isPropertyValid, isObject } from './'; 2 | 3 | describe('utils', () => { 4 | describe('isNotAvoidedProperty', () => { 5 | it('should return true if property is not an avoided property', () => { 6 | expect(isNotAvoidedProperty('componentDidMount')).toEqual(true); 7 | }); 8 | 9 | it('should return false if property is an avoided property', () => { 10 | expect(isNotAvoidedProperty('props')).toEqual(false); 11 | }); 12 | }); 13 | 14 | describe('isPropertyValid', () => { 15 | const instance = new class TestingClass { 16 | constructor() { 17 | this.testingProperty = 'test'; 18 | } 19 | 20 | testingMethod() { 21 | return this.testingProperty; 22 | } 23 | }(); 24 | 25 | it('should return false if property is not a function', () => { 26 | expect(isPropertyValid(instance)('testingProperty')).toEqual(false); 27 | }); 28 | 29 | it('should return false if property is a function but is excluded', () => { 30 | expect( 31 | isPropertyValid(instance, ['testingMethod'])('testingMethod') 32 | ).toEqual(false); 33 | }); 34 | 35 | it('should return true if property is a function and is not excluded', () => { 36 | expect(isPropertyValid(instance)('testingMethod')).toEqual(true); 37 | }); 38 | }); 39 | 40 | describe('isObject', () => { 41 | it('should return false if parameter is not an object', () => { 42 | expect(isObject('')).toEqual(false); 43 | expect(isObject(0)).toEqual(false); 44 | expect(isObject(null)).toEqual(false); 45 | expect(isObject(undefined)).toEqual(false); 46 | expect(isObject(() => {})).toEqual(false); 47 | }); 48 | 49 | it('should return true if parameter is an object', () => { 50 | expect(isObject({})).toEqual(true); 51 | expect(isObject([])).toEqual(true); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/utils/pleakUtils.js: -------------------------------------------------------------------------------- 1 | import performanceNow from 'fbjs/lib/performanceNow'; 2 | import { RENDER_METHOD, METHOD_TYPES, LIFECYCLES } from './constants'; 3 | 4 | export const measureTiming = start => (performanceNow() - start).toFixed(5); 5 | 6 | export const getMethodType = method => 7 | method === RENDER_METHOD 8 | ? METHOD_TYPES.RENDER 9 | : LIFECYCLES.includes(method) 10 | ? METHOD_TYPES.LIFECYCLE 11 | : METHOD_TYPES.COMPONENT_METHOD; 12 | 13 | export const parsePleakUri = uri => { 14 | if (typeof uri !== 'string') throw new Error('Invalid uri'); 15 | 16 | const match = uri.match( 17 | /^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/ 18 | ); 19 | 20 | const protocol = match[2]; 21 | const [publicKey, host] = match[4].split('@'); 22 | const appId = match[5].replace('/', ''); 23 | 24 | return { 25 | protocol, 26 | publicKey, 27 | host, 28 | appId, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils/pleakUtils.test.js: -------------------------------------------------------------------------------- 1 | import { measureTiming, getMethodType } from './pleakUtils'; 2 | import { METHOD_TYPES } from './constants'; 3 | 4 | jest.mock('fbjs/lib/performanceNow'); 5 | const performanceNow = require('fbjs/lib/performanceNow'); 6 | 7 | describe('pleakUtils', () => { 8 | describe('measureTiming', () => { 9 | performanceNow.mockReturnValue(1000); 10 | it('should return a time measure with a string representing a number in fixed-point notation', () => { 11 | const measure = measureTiming(567.2); 12 | 13 | expect(performanceNow).toHaveBeenCalled(); 14 | expect(measure).toEqual('432.80000'); 15 | }); 16 | }); 17 | 18 | describe('getMethodType', () => { 19 | it('should return the right method type', () => { 20 | expect(getMethodType('render')).toEqual(METHOD_TYPES.RENDER); 21 | expect(getMethodType('componentWillMount')).toEqual( 22 | METHOD_TYPES.LIFECYCLE 23 | ); 24 | expect(getMethodType('UNSAFE_componentWillMount')).toEqual( 25 | METHOD_TYPES.LIFECYCLE 26 | ); 27 | expect(getMethodType('componentDidMount')).toEqual( 28 | METHOD_TYPES.LIFECYCLE 29 | ); 30 | expect(getMethodType('componentDidUpdate')).toEqual( 31 | METHOD_TYPES.LIFECYCLE 32 | ); 33 | expect(getMethodType('componentWillUpdate')).toEqual( 34 | METHOD_TYPES.LIFECYCLE 35 | ); 36 | expect(getMethodType('UNSAFE_componentWillUpdate')).toEqual( 37 | METHOD_TYPES.LIFECYCLE 38 | ); 39 | expect(getMethodType('componentWillReceiveProps')).toEqual( 40 | METHOD_TYPES.LIFECYCLE 41 | ); 42 | expect(getMethodType('UNSAFE_componentWillReceiveProps')).toEqual( 43 | METHOD_TYPES.LIFECYCLE 44 | ); 45 | expect(getMethodType('shouldComponentUpdate')).toEqual( 46 | METHOD_TYPES.LIFECYCLE 47 | ); 48 | expect(getMethodType('getSnapshotBeforeUpdate')).toEqual( 49 | METHOD_TYPES.LIFECYCLE 50 | ); 51 | expect(getMethodType('componentWillUnmount')).toEqual( 52 | METHOD_TYPES.LIFECYCLE 53 | ); 54 | expect(getMethodType('componentDidCatch')).toEqual( 55 | METHOD_TYPES.LIFECYCLE 56 | ); 57 | expect(getMethodType('handleClick')).toEqual( 58 | METHOD_TYPES.COMPONENT_METHOD 59 | ); 60 | }); 61 | }); 62 | }); 63 | --------------------------------------------------------------------------------