├── .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 |
--------------------------------------------------------------------------------