-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathRaygunNativeBridgeModule.java
More file actions
425 lines (365 loc) · 17 KB
/
RaygunNativeBridgeModule.java
File metadata and controls
425 lines (365 loc) · 17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
package com.raygun.react;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.raygun.raygun4android.CrashReportingOnBeforeSend;
import com.raygun.raygun4android.RaygunClient;
import com.raygun.raygun4android.messages.crashreporting.RaygunBreadcrumbLevel;
import com.raygun.raygun4android.messages.crashreporting.RaygunBreadcrumbMessage;
import com.raygun.raygun4android.messages.crashreporting.RaygunErrorMessage;
import com.raygun.raygun4android.messages.crashreporting.RaygunMessage;
import com.raygun.raygun4android.messages.shared.RaygunUserInfo;
import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import timber.log.Timber;
import static android.provider.Settings.Secure.getString;
public class RaygunNativeBridgeModule extends ReactContextBaseJavaModule implements Application.ActivityLifecycleCallbacks {
//#region---GLOBAL CONSTANTS----------------------------------------------------------------------
// ReactNative Context, a connection the the React Code.
private static ReactApplicationContext reactContext;
// Are the NativeBridge features initialized
private boolean realUserMonitoringInitialized = false;
private boolean crashReportingInitialized = false;
//The activity in the foreground of the application that the user is currently interacting with.
private static WeakReference<Activity> currentActivity;
// Session state change events
public static final String ON_SESSION_RESUME = "ON_SESSION_RESUME";
public static final String ON_SESSION_PAUSE = "ON_SESSION_PAUSE";
public static final String ON_SESSION_END = "ON_SESSION_END";
//Activity state change events
public static final String ON_VIEW_LOADING = "ON_VIEW_LOADING";
public static final String ON_VIEW_LOADED = "ON_VIEW_LOADED";
private static final String DEVICE_ID = "DEVICE_ID";
//#endregion--------------------------------------------------------------------------------------
//#region---CONSTRUCTION METHODS------------------------------------------------------------------
/**
* Constructor for the RaygunNativeBridgeModule class that links ReactNative to Java and stores
* the start time to monitor the applications startup time.
*
* @param context - ReactApplicationContext, parsed to the RaygunNativeBridgePackage's
* createNativeModules method. An instance that links Java and ReactNative in a
* wrapper.
* @param startedAt - Long, time when the application started.
* @see RaygunNativeBridgePackage
*/
public RaygunNativeBridgeModule(ReactApplicationContext context, long startedAt) {
super(context);
reactContext = context;
}
/**
* Returns a string that will be used to identify this bridge in ReactNative with the code: const
* { stringReturnedByThisMethod } = NativeModules;
*
* @return - String value that will identify this bridge module.
*/
@Override
public @NotNull String getName() {
return "RaygunNativeBridge";
}
/**
* Initialize the Bridge (and the Raygun4Android client by proxy) with the same options given to
* the Raygun4ReactNative client init method.
*/
@ReactMethod
public void initCrashReportingNativeSupport(String apiKey, String version, String customCrashReportingEndpoint) {
if (crashReportingInitialized) {
Timber.i("ReactNativeBridge crash reporting already initialized");
return;
}
RaygunClient.INSTANCE.init(reactContext, apiKey, version);
RaygunClient.INSTANCE.enableCrashReporting();
RaygunClient.INSTANCE.setOnBeforeSend(new OnBeforeSendHandler());
RaygunClient.INSTANCE.setCustomCrashReportingEndpoint(customCrashReportingEndpoint);
crashReportingInitialized = true;
}
/**
* This method is used by the RealUserMonitor to instantiate the LifecycleEventListeners. This is
* not included with the 'init' method, as the RealUserMonitor needs this code to be run
* independently. However, if RUM is not enabled, then this would provide some unnecessary
* performance downgrades, hence why it is only instantiated if RUM is enabled.
*/
@ReactMethod
public void initRealUserMonitoringNativeSupport() {
//Cant initialise bridge rum twice
if (realUserMonitoringInitialized) return;
if (reactContext != null && reactContext.getCurrentActivity() != null) {
//Store the current activity to differentiate session changes
currentActivity = new WeakReference<>(reactContext.getCurrentActivity());
//Attach the activity listening logic to the Application
reactContext.getCurrentActivity().getApplication().registerActivityLifecycleCallbacks(this);
} else Log.e("Raygun", "This react application has no active activity");
realUserMonitoringInitialized = true;
}
@ReactMethod
public void addListener(String eventName) {
// Dummy method to silence NativeEventEmitter warnings in React Native.
// No implementation required.
}
@ReactMethod
public void removeListeners(Integer count) {
// Dummy method to silence NativeEventEmitter warnings in React Native.
// No implementation required.
}
/**
* This class is designed to implement CrashReportingOnBeforeSend interface from the
* Raygun4Android SDK. Before Sending some CrashReport, this handler will ensure that the crash
* didn't occur in the ReactNative space, else the ReactNative provider will have picked up on
* that (else we will have two of the same CrashReport going through).
*/
private static class OnBeforeSendHandler implements CrashReportingOnBeforeSend {
// Prevent the JS side error from being processed again as it propagate to the native side
@Override
public RaygunMessage onBeforeSend(RaygunMessage raygunMessage) {
RaygunErrorMessage error = raygunMessage.getDetails().getError();
String message = error != null ? error.getMessage() : null;
if (message != null && message.contains("JavascriptException")) {
return null;
}
return raygunMessage;
}
}
//#endregion--------------------------------------------------------------------------------------
//#region---Life Cycle Methods -----------------------------------------------------------------
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
//If there is no current activity then this one will become the current activity
if (currentActivity == null) {
currentActivity = new WeakReference<>(activity);
}
//Pass off a view loading event to the react side
WritableMap payload = Arguments.createMap();
payload.putString("viewname", activity.getClass().getSimpleName());
payload.putString("time", System.currentTimeMillis() + "");
this.sendJSEvent(ON_VIEW_LOADING, payload);
}
@Override
public void onActivityStarted(Activity activity) {
WritableMap payload = Arguments.createMap();
long time = System.currentTimeMillis();
payload.putString("viewname", activity.getClass().getSimpleName());
payload.putString("time", time + "");
this.sendJSEvent(ON_VIEW_LOADED, payload);
}
@Override
public void onActivityResumed(Activity activity) {
//If the activity that recently paused is returning to the foreground then the whole
// application has resumed, therefore update the session
if (currentActivity != null && currentActivity.get() == activity) {
this.sendJSEvent(ON_SESSION_RESUME, null);
}
else {
//If any other activity resumes that means that it is taking over from the current activity
currentActivity = new WeakReference<>(activity);
}
}
@Override
public void onActivityPaused(Activity activity) {
//If the current activity is pausing then the session is paused
if (currentActivity != null && currentActivity.get() == activity) {
this.sendJSEvent(ON_SESSION_PAUSE, null);
}
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
this.sendJSEvent(ON_SESSION_END, null);
currentActivity = null;
}
}
/**
* Emits an event to the ReactContext.
*
* @param eventType - Should be one of the KEY values, START, RESUME, PAUSE, DESTROY.
* @param payload - A WritableMap of information to be parsed with this event's occurence.
*/
private void sendJSEvent(String eventType, @Nullable WritableMap payload) {
if (reactContext == null || !reactContext.hasActiveCatalystInstance()) {
Log.w("Raygun", "Unable to send JS event for " + eventType + " due to an inactive React context.");
return;
}
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventType, payload);
}
//#endregion--------------------------------------------------------------------------------------
//#region---INFORMATION GATHERING METHODS---------------------------------------------------------
/**
* Collects all the environment information about this device and returns it to the promise in the
* form of a WritableMap.
*
* @param promise - Resolves with a WriteableMap of all the information about the system.
*/
@ReactMethod
public void getEnvironmentInfo(Promise promise) {
WritableMap map = Arguments.createMap();
map.putString("Architecture", Build.CPU_ABI);
map.putString("DeviceName", Build.MODEL);
map.putString("Brand", Build.BRAND);
map.putString("Board", Build.BOARD);
map.putString("DeviceCode", Build.DEVICE);
try {
String currentOrientation;
int orientation = reactContext.getResources().getConfiguration().orientation;
if (orientation == 1) {
currentOrientation = "Portrait";
} else if (orientation == 2) {
currentOrientation = "Landscape";
} else if (orientation == 3) {
currentOrientation = "Square";
} else {
currentOrientation = "Undefined";
}
DisplayMetrics metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay().getMetrics(metrics);
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
ActivityManager am = (ActivityManager) reactContext
.getSystemService(Context.ACTIVITY_SERVICE);
am.getMemoryInfo(mi);
map.putInt("ProcessorCount", Runtime.getRuntime().availableProcessors());
map.putString("OSVersion", Build.VERSION.RELEASE);
map.putString("OSSDKVersion", Integer.toString(Build.VERSION.SDK_INT));
map.putInt("WindowsBoundWidth", metrics.widthPixels);
map.putInt("WindowsBoundHeight", metrics.heightPixels);
map.putString("CurrentOrientation", currentOrientation);
map
.putString("Locale", reactContext.getResources().getConfiguration().locale.toString());
map.putDouble("AvailablePhysicalMemory", mi.availMem / 0x100000);
} catch (Exception e) {
Timber.e(e, "Retrieve Environment Info Error");
}
promise.resolve(map);
}
/**
* Collects the constant values used in this SDK. Such as the OS_Version, and Platform. Along with
* key values for events that can occur with RUM enabled.
*
* @return - A map of constant objects and a key value to access them.
*/
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
constants.put(ON_SESSION_END, ON_SESSION_END);
constants.put(ON_SESSION_PAUSE, ON_SESSION_PAUSE);
constants.put(ON_SESSION_RESUME, ON_SESSION_RESUME);
constants.put(ON_VIEW_LOADING, ON_VIEW_LOADING);
constants.put(ON_VIEW_LOADED, ON_VIEW_LOADED);
constants.put(DEVICE_ID, getUniqueIdSync());
constants.put("osVersion", Build.VERSION.RELEASE);
constants.put("platform", Build.MODEL);
return constants;
}
/**
* Gets the Device ID. This method is synchronous as the returning value needs to be immediately
* available.
*
* @return - String, value of the device id.
*/
@SuppressLint("HardwareIds")
@ReactMethod(isBlockingSynchronousMethod = true)
public String getUniqueIdSync() {
return getString(getReactApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
}
//#endregion--------------------------------------------------------------------------------------
//#region---UPDATE NATIVE TO MATCH REACT METHODS--------------------------------------------------
/**
* Set the User in the Raygun4Android SDK to ensure all reports are consistent between the SDKs.
*
* @param userObj - ReadableMap that is represented by the 'User' type.
*/
@ReactMethod
public void setUser(ReadableMap userObj) {
RaygunUserInfo user = RaygunUserInfo.create(
userObj.getString("identifier"),
userObj.getString("firstName"),
userObj.getString("fullName"),
userObj.getString("email"));
RaygunClient.INSTANCE.setUser(user);
}
/**
* Sets the tags in the Raygun4Android SDK to ensure all reports are consistent between the SDKs.
*
* @param tags - A ReadableArray of strings.
*/
@ReactMethod
public void setTags(ReadableArray tags) {
List<String> tagList = new ArrayList<>();
for (int i = 0; i < tags.size(); i++) {
tagList.add(tags.getString(i));
}
RaygunClient.INSTANCE.setTags(tagList);
}
/**
* Set the CustomData in the Raygun4Android SDK to ensure all reports are consistent between
* SDKs.
*
* @param customData - ReadableMap that is represented by the 'CustomData' type.
*/
@ReactMethod
public void setCustomData(ReadableMap customData) {
RaygunClient.INSTANCE.setCustomData(customData.toHashMap());
}
/**
* Set the Breadcrumb in the Raygun4Android SDK to ensure all reports are consistent between
* SDKs.
*
* @param breadcrumb - ReadableMap that is represented by the 'Breadcrumb' type.
*/
@ReactMethod
public void recordBreadcrumb(ReadableMap breadcrumb) {
String message = breadcrumb.getString("message");
String category = breadcrumb.getString("category");
ReadableMap customData = breadcrumb.getMap("customData");
String level = breadcrumb.getString("level");
RaygunBreadcrumbLevel breadcrumbLvl = level.equalsIgnoreCase("debug")
? RaygunBreadcrumbLevel.DEBUG
: level.equalsIgnoreCase("info")
? RaygunBreadcrumbLevel.INFO
: level.equalsIgnoreCase("warning")
? RaygunBreadcrumbLevel.WARNING
: level.equalsIgnoreCase("error")
? RaygunBreadcrumbLevel.ERROR
: RaygunBreadcrumbLevel.INFO;
RaygunBreadcrumbMessage breadcrumbMessage = new RaygunBreadcrumbMessage
.Builder(message)
.category(category)
.level(breadcrumbLvl)
.customData(customData.toHashMap())
.build();
RaygunClient.INSTANCE.recordBreadcrumb(breadcrumbMessage);
}
/**
* Clear the breadcrumbs.
*/
@ReactMethod
public void clearBreadcrumbs() {
RaygunClient.INSTANCE.clearBreadcrumbs();
}
//#endregion--------------------------------------------------------------------------------------
}