newrelic-react-native-agent
Version:
A New Relic Mobile Agent for React Native
739 lines (643 loc) • 26.1 kB
JavaScript
/*
* Copyright (c) 2022-present New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import { Platform } from 'react-native';
import {
getUnhandledPromiseRejectionTracker,
setUnhandledPromiseRejectionTracker,
} from 'react-native-promise-rejection-utils';
import getCircularReplacer from './new-relic/circular-replacer';
import { LOG } from './new-relic/nr-logger';
import utils from './new-relic/nr-utils';
import NRMAModularAgentWrapper from './new-relic/nrma-modular-agent-wrapper';
import version from './new-relic/version';
/**
* New Relic
*/
class NewRelic {
constructor() {
this.JSAppVersion = '';
this.state = {
didAddErrorHandler: false,
didAddPromiseRejection: false,
didOverrideConsole: false,
isFirstScreen: true
};
this.lastScreen = '';
this.LOG = LOG;
this.NRMAModularAgentWrapper = new NRMAModularAgentWrapper();
this.agentVersion = version;
this.agentConfiguration = {
analyticsEventEnabled: true,
nativeCrashReportingEnabled: false,
crashReportingEnabled: true,
interactionTracingEnabled: false,
networkRequestEnabled: true,
networkErrorRequestEnabled: true,
httpResponseBodyCaptureEnabled: true,
loggingEnabled: true,
logLevel: this.LogLevel.WARN,
webViewInstrumentation: true,
collectorAddress: "",
crashCollectorAddress: "",
fedRampEnabled: false,
offlineStorageEnabled: true,
backgroundReportingEnabled: false,
newEventSystemEnabled: false,
distributedTracingEnabled: true,
};
}
LogLevel = {
// ERROR is least verbose and AUDIT is most verbose
ERROR: "ERROR",
WARN: "WARN",
INFO: "INFO",
VERBOSE: "VERBOSE",
AUDIT: "AUDIT",
DEBUG: "DEBUG"
};
NetworkFailure = {
Unknown: 'Unknown',
BadURL: 'BadURL',
TimedOut: 'TimedOut',
CannotConnectToHost: 'CannotConnectToHost',
DNSLookupFailed: 'DNSLookupFailed',
BadServerResponse: 'BadServerResponse',
SecureConnectionFailed: 'SecureConnectionFailed'
}
MetricUnit = {
PERCENT: 'PERCENT',
BYTES: 'BYTES',
SECONDS: 'SECONDS',
BYTES_PER_SECOND: 'BYTES_PER_SECOND',
OPERATIONS: 'OPERATIONS'
}
/**
* True if native agent is started. False if native agent is not started.
* @returns {boolean}
*/
isAgentStarted = () => NRMAModularAgentWrapper.isAgentStarted;
/**
* Navigation Route Listener
*/
/**
* Subscribe onNavigationStateChange Listener from React Navigation Version 4.x and lower
* Creates and records a MobileBreadcrumb for Current Screen
* @param {object} prevState
* @param {object} newState
* @param {object} action
*/
onNavigationStateChange = (prevState, newState, action) => {
var currentScreenName = this.getCurrentRouteName(newState);
var params = {
'screenName': currentScreenName
};
this.recordBreadcrumb('navigation', params);
}
/**
* @param {object} currentState
* @returns {string | null}
*/
getCurrentRouteName = (currentState) => {
if (!currentState) {
return null;
}
const route = currentState.routes[currentState.index];
if (route.routes) {
return this.getCurrentRouteName(route);
}
return route.routeName;
}
/**
* Subcribe componentDidAppearListener Listenr from React Native Navigation Package
* Creates and records a MobileBreadcrumb for Current Screen
* @param {object} event
*/
componentDidAppearListener = (event) => {
if (this.state.isFirstScreen) {
this.lastScreen = event.componentName;
this.state.isFirstScreen = false;
return;
}
if (this.lastScreen != event.componentName) {
var currentScreenName = event.componentName;
this.lastScreen = currentScreenName;
var params = {
'screenName': currentScreenName
};
this.recordBreadcrumb('navigation', params);
}
}
/**
* Subcribe OnStateChange Listenr from React Navigation Version 5.x and higer
* Creates and records a MobileBreadcrumb for Current Screen
* @param {object} state
*/
onStateChange = (state) => {
var currentScreenName = this.getCurrentScreen(state);
var params = {
'screenName': currentScreenName
};
this.recordBreadcrumb('navigation', params);
}
/**
* @param {object} state
* @returns {string}
*/
getCurrentScreen(state) {
if (!state.routes[state.index].state) {
return state.routes[state.index].name
}
return this.getCurrentScreen(state.routes[state.index].state);
}
/**
* Start the agent
* @param {string} appkey
* @param {object} customerConfiguration
*/
startAgent(appkey, customerConfiguration) {
this.LOG.verbose = true; // todo: should let this get set by a param
this.config = {...this.agentConfiguration, ...customerConfiguration};
this.NRMAModularAgentWrapper.startAgent(appkey, this.agentVersion, this.getReactNativeVersion(), this.config);
this.addNewRelicErrorHandler();
this.addNewRelicPromiseRejectionHandler();
this._overrideConsole();
//this.enableNetworkInteraction();
this.LOG.info('React Native agent started.');
this.LOG.info(`New Relic React Native agent version ${this.agentVersion}`);
this.setAttribute('ReactNativeAgentVersion', this.agentVersion);
this.setAttribute('JSEngine', global.HermesInternal ? "Hermes" : "JavaScriptCore");
}
getReactNativeVersion() {
var rnVersion = Platform.constants.reactNativeVersion;
return `${rnVersion.major}.${rnVersion.minor}.${rnVersion.patch}`
}
/**
* FOR ANDROID ONLY.
* Enable or disable collection of event data.
* @param enabled {boolean} Boolean value for enabling analytics events.
*/
analyticsEventEnabled(enabled) {
this.NRMAModularAgentWrapper.execute('analyticsEventEnabled', enabled);
}
/**
* Enable or disable reporting successful HTTP requests to the MobileRequest event type.
* @param enabled {boolean} Boolean value for enabling successful HTTP requests.
*/
networkRequestEnabled(enabled) {
this.NRMAModularAgentWrapper.execute('networkRequestEnabled', enabled);
}
/**
* Enable or disable reporting network and HTTP request errors to the MobileRequestError event type.
* @param enabled {boolean} Boolean value for enabling network request errors.
*/
networkErrorRequestEnabled(enabled) {
this.NRMAModularAgentWrapper.execute('networkErrorRequestEnabled', enabled);
}
/**
* Enable or disable capture of HTTP response bodies for HTTP error traces, and MobileRequestError events.
* @param enabled {boolean} Boolean value for enabling HTTP response bodies.
*/
httpResponseBodyCaptureEnabled(enabled) {
this.NRMAModularAgentWrapper.execute('httpResponseBodyCaptureEnabled', enabled);
}
/**
* Creates and records a MobileBreadcrumb event
* @param eventName {string} the name you want to give to the breadcrumb event.
* @param attributes {Map<string, any>} a map that includes a list of attributes.
*/
recordBreadcrumb(eventName, attributes) {
attributes = attributes instanceof Map ? Object.fromEntries(attributes) : attributes;
this.NRMAModularAgentWrapper.execute('recordBreadcrumb', eventName, attributes);
}
/**
* Creates and records a custom event, for use in New Relic Insights.
* The event includes a list of attributes, specified as a map.
* @param eventType {string} The type of event.
* @param eventName {string} Use this parameter to name the event.
* @param attributes {Map<string, any>} A map that includes a list of attributes.
*/
recordCustomEvent(eventType, eventName, attributes) {
attributes = attributes instanceof Map ? Object.fromEntries(attributes) : attributes;
this.NRMAModularAgentWrapper.execute('recordCustomEvent', eventType, eventName, attributes);
}
/**
* Throws a demo run-time exception to test New Relic crash reporting.
* @param message {string} Optional argument attached to the exception.
*/
crashNow(message = '') {
this.NRMAModularAgentWrapper.execute('crashNow', message);
}
/**
* Returns the current session ID.
* This method is useful for consolidating monitoring of app data (not just New Relic data) based on a single session definition and identifier.
* @return currentSessionId {Promise} A promise that returns the current session id.
*/
async currentSessionId() {
return await this.NRMAModularAgentWrapper.execute('currentSessionId');
}
/**
* Tracks network requests manually.
* You can use this method to record HTTP transactions, with an option to also send a response body.
* @param url {string} The URL of the request.
* @param httpMethod {string} The HTTP method used, such as GET or POST
* @param statusCode {number} The statusCode of the HTTP response, such as 200 for OK.
* @param startTime {number} The start time of the request in milliseconds since the epoch.
* @param endTime {number} The end time of the request in milliseconds since the epoch.
* @param bytesSent {number} The number of bytes sent in the request.
* @param bytesReceived {number} The number of bytes received in the response
* @param responseBody {string} The response body of the HTTP response. The response body will be truncated and included in an HTTP Error metric if the HTTP transaction is an error.
*/
noticeHttpTransaction(url, httpMethod, statusCode, startTime, endTime, bytesSent, bytesReceived, responseBody) {
this.NRMAModularAgentWrapper.execute('noticeHttpTransaction', url, httpMethod, statusCode, startTime, endTime, bytesSent, bytesReceived, responseBody);
}
/**
* Add Headers as Attributes in Http Requests, for use in New Relic Insights.
* The method includes a list of headers, specified as a map.
* @param headers {Map<string, any>} A map that includes a list of headers.
*/
addHTTPHeadersTrackingFor(headers) {
this.NRMAModularAgentWrapper.execute('addHTTPHeadersTrackingFor', headers);
}
/**
* Records network failures.
* If a network request fails, use this method to record details about the failure.
* In most cases, place this call inside exception handlers.
* @param url {string} The URL of the request.
* @param httpMethod {string} The HTTP method used, such as GET or POST.
* @param startTime {number} The start time of the request in milliseconds since the epoch.
* @param endTime {number} The end time of the request in milliseconds since the epoch.
* @param failure {string} Name of the network failure. Possible values are in NewRelic.NetworkFailure.
*/
noticeNetworkFailure(url, httpMethod, startTime, endTime, failure) {
this.NRMAModularAgentWrapper.execute('noticeNetworkFailure', url, httpMethod, startTime, endTime, failure);
}
/**
* Records custom metrics (arbitrary numerical data).
* @param name {string} The name for the custom metric.
* @param category {string} The metric category name.
* @param value {number} Optional. The value of the metric. Value should be a non-zero positive number.
* @param countUnit {string} Optional (but requires value and valueUnit to be set). Unit of measurement for the metric count. Supported values are in NewRelic.MetricUnit.
* @param valueUnit {string} Optional (but requires value and countUnit to be set). Unit of measurement for the metric value. Supported values are in NewRelic.MetricUnit.
*/
recordMetric(name, category, value = -1, countUnit = null, valueUnit = null) {
this.NRMAModularAgentWrapper.execute('recordMetric', name, category, value, countUnit, valueUnit);
}
/**
* Removes all attributes from the session.
*/
removeAllAttributes() {
this.NRMAModularAgentWrapper.execute('removeAllAttributes');
}
/**
* Records JavaScript errors for react-native.
* @param e {Error | string} A JavaScript error.
*/
async recordError(e) {
await this.recordError(e, false);
}
/**
* Records JavaScript errors for react-native.
* @param {Error | string} e
* @param {boolean} isFatal
*/
async recordError(e, isFatal) {
if (e) {
if (!this.JSAppVersion) {
this.LOG.error('unable to capture JS error. Make sure to call NewRelic.setJSAppVersion() at the start of your application.');
}
var error;
if (e instanceof Error) {
error = e;
}
if (typeof e === 'string') {
error = new Error(e || '');
}
if (error !== undefined) {
this.NRMAModularAgentWrapper.execute("recordHandledException", error, this.JSAppVersion, isFatal)
} else {
this.LOG.warn('undefined error name or message');
}
} else {
this.LOG.warn('error is required');
}
}
/***
* Sets the event harvest cycle length.
* Default is 600 seconds (10 minutes).
* Minimum value cannot be less than 60 seconds.
* Maximum value should not be greater than 600 seconds.
* @param maxBufferTimeInSeconds {number} The maximum time (in seconds) that the agent should store events in memory.
*/
setMaxEventBufferTime(maxBufferTimeInSeconds) {
this.NRMAModularAgentWrapper.execute('setMaxEventBufferTime', maxBufferTimeInSeconds);
}
/**
* Sets the maximum size of the event pool stored in memory until the next harvest cycle.
* When the pool size limit is reached, the agent will start sampling events, discarding some new and old, until the pool of events is sent in the next harvest cycle.
* Default is a maximum of 1000 events per event harvest cycle.
* @param maxSize {number} The maximum number of events per harvest cycle.
*/
setMaxEventPoolSize(maxSize) {
this.NRMAModularAgentWrapper.execute('setMaxEventPoolSize', maxSize);
}
/**
* Sets the maximum size of total data that can be stored for offline storage.By default, mobile monitoring can collect a maximum of 100 megaBytes of offline storage.
* When a data payload fails to send because the device doesn't have an internet connection, it can be stored in the file system until an internet connection has been made.
* After a typical harvest payload has been successfully sent, all offline data is sent to New Relic and cleared from storage.
* @param megaBytes {number} Maximum size in megaBytes that can be stored in the file system..
*/
setMaxOfflineStorageSize(megaBytes) {
this.NRMAModularAgentWrapper.execute('setMaxOfflineStorageSize', megaBytes);
}
/**
* Track a method as an interaction
* @param {string} actionName - The interaction's name
*/
async startInteraction(actionName) {
if (utils.notEmptyString(actionName)) {
return await this.NRMAModularAgentWrapper.startInteraction(actionName);
} else {
this.LOG.warn(`actionName is required`);
}
}
/**
* End an interaction
* Required. The string ID for the interaction you want to end.
* This string is returned when you use startInteraction().
* @param {string} interActionId
*/
endInteraction(interActionId) {
if (utils.notEmptyString(interActionId)) {
this.NRMAModularAgentWrapper.execute('endInteraction', interActionId);
} else {
this.LOG.warn(`interActionId is Required`);
}
}
/**
* ANDROID ONLY
* Name or rename an interaction
* @param {string} name
*/
setInteractionName(name) {
if (Platform.OS === 'android') {
this.NRMAModularAgentWrapper.execute('setInteractionName', name);
} else {
this.LOG.info(`setInterActionName is not supported by iOS Agent`);
}
}
/**
* Creates a custom attribute with a specified name and value.
* When called, it overwrites its previous value and type.
* The created attribute is shared by multiple Mobile event types.
* @param attributeName {string} Name of the attribute.
* @param value {string|number|boolean}
*/
setAttribute(attributeName, value) {
this.NRMAModularAgentWrapper.execute('setAttribute', attributeName, value);
}
/**
* Remove a custom attribute with a specified name and value.
* When called, it removes the attribute specified by the name string.
* The removed attribute is shared by multiple Mobile event types.
* @param attributeName {string} Name of the attribute.
*/
removeAttribute(attributeName) {
this.NRMAModularAgentWrapper.execute('removeAttribute', attributeName);
}
/**
* This function is used to log a message with a specific log level.
*
* @param {string} logLevel - The level of the log (e.g., ERROR, WARN, INFO, VERBOSE, AUDIT, DEBUG).
* @param {string} message - The message to be logged.
*/
log(logLevel, message) {
// Check if the message is empty
if (message === undefined || message.length === 0) {
// If the message is empty, log an error message and return
console.error("Log message is empty.");
return;
}
// Create a new Map to store attributes
var attributes = {};
// Set the message and log level as attributes
attributes["message"] = message;
attributes["level"] = logLevel;
// Log the attributes
this.logAttributes(attributes);
}
/**
* This function is used to log an error message.
*
* @param {string} message - The error message to be logged.
*/
logError(message) {
this.log(this.LogLevel.ERROR, message);
}
/**
* This function is used to log a debug message.
*
* @param {string} message - The debug message to be logged.
*/
logDebug(message) {
this.log(this.LogLevel.DEBUG, message);
}
/**
* This function is used to log an informational message.
*
* @param {string} message - The informational message to be logged.
*/
logInfo(message) {
this.log(this.LogLevel.INFO, message);
}
/**
* This function is used to log a verbose message.
*
* @param {string} message - The verbose message to be logged.
*/
logVerbose(message) {
this.log(this.LogLevel.VERBOSE, message);
}
/**
* This function is used to log a warning message.
*
* @param {string} message - The warning message to be logged.
*/
logWarn(message) {
this.log(this.LogLevel.WARN, message);
}
/**
* This function is used to log all attributes.
*
* @param {Error} error - The error object from which the message is extracted.
* @param {Record<string, string | number | boolean>} attributes - The attributes to be logged.
*/
logAll(error, attributes) {
// Create a new Map to store all attributes
let allAttributes = {};
// Set the error message as an attribute
allAttributes["message"] = error.message;
// Iterate over the attributes Map and add each attribute to allAttributes
for (const [key, value] of Object.entries(attributes)) {
allAttributes[key] = value;
}
// Log all attributes
this.logAttributes(allAttributes);
}
/**
* This function is used to log attributes.
*
* @param {Record<string, string | number | boolean>} attributes - The attributes to be logged.
*/
logAttributes(attributes) {
// Check if the attributes are empty
if (utils.isObjectEmpty(attributes)){
// If attributes are empty, log an error message and return
console.error("Attributes are empty.");
return;
}
this.NRMAModularAgentWrapper.execute('logAttributes', attributes);
}
/**
* Increments the count of an attribute with a specified name.
* When called, it overwrites its previous value and type each time.
* If attribute does not exist, it creates an attribute with a value of 1.
* The incremented attribute is shared by multiple Mobile event types.
* @param attributeName {string} Name of the Attribute.
* @param value {number} Optional argument that increments the attribute by this value.
*/
incrementAttribute(attributeName, value = 1) {
this.NRMAModularAgentWrapper.execute('incrementAttribute', attributeName, value);
}
/**
* Sets the js release version
* @param version {string}
*/
setJSAppVersion(version) {
if (utils.isString(version)) {
this.JSAppVersion = version;
this.NRMAModularAgentWrapper.execute('setJSAppVersion', version);
return;
}
this.LOG.error(`JSAppVersion '${version}' is not a string.`);
}
/**
* Sets a custom user identifier value to associate mobile user
* @param userId {string}
*/
setUserId(userId) {
if (utils.isString(userId)) {
this.NRMAModularAgentWrapper.execute('setUserId', userId);
} else {
this.LOG.error(`userId '${userId}' is not a string.`);
}
}
/**
* Shut down the agent within the current application lifecycle during runtime.
*/
shutdown() {
this.NRMAModularAgentWrapper.execute('shutdown');
}
/**
* Start recording session replay.
* Enables session replay functionality to capture and record user interactions.
*/
recordReplay() {
this.NRMAModularAgentWrapper.execute('recordReplay');
}
/**
* Pause recording session replay.
* Temporarily pauses the session replay recording functionality.
*/
pauseReplay() {
this.NRMAModularAgentWrapper.execute('pauseReplay');
}
/**
* @private
*/
addNewRelicErrorHandler() {
if (global && global.ErrorUtils && !this.state.didAddErrorHandler) {
const previousHandler = global.ErrorUtils.getGlobalHandler();
global.ErrorUtils.setGlobalHandler(async (error, isFatal) => {
if (!this.JSAppVersion) {
this.LOG.error('unable to capture JS error. Make sure to call NewRelic.setJSAppVersion() at the start of your application.');
}
await this.recordError(error, isFatal);
previousHandler(error, isFatal);
});
// prevent us from adding the error handler multiple times.
this.state.didAddErrorHandler = true;
} else if (!this.state.didAddErrorHandler) {
this.LOG.debug('failed to add New Relic error handler no error utils detected');
}
}
/**
* @private
*/
addNewRelicPromiseRejectionHandler() {
const prevTracker = getUnhandledPromiseRejectionTracker();
if (!this.state.didAddPromiseRejection) {
setUnhandledPromiseRejectionTracker(async (id, error) => {
if (error != undefined) {
await this.recordError(error);
} else {
this.recordBreadcrumb("Possible Unhandled Promise Rejection", {id: id})
}
if (prevTracker !== undefined) {
prevTracker(id, error)
}
});
this.state.didAddPromiseRejection = true;
}
}
/**
* @private
*/
_overrideConsole() {
if (!this.state.didOverrideConsole) {
const defaultLog = console.log;
const defaultWarn = console.warn;
const defaultError = console.error;
const defaultDebug = console.debug;
const self = this;
console.log = function () {
self.sendConsole('log', arguments);
defaultLog.apply(console, arguments);
};
console.warn = function () {
self.sendConsole('warn', arguments);
defaultWarn.apply(console, arguments);
};
console.error = function () {
self.sendConsole('error', arguments);
defaultError.apply(console, arguments);
};
console.debug = function () {
self.sendConsole('debug', arguments);
defaultDebug.apply(console, arguments);
}
this.state.didOverrideConsole = true;
}
}
/**
* @private
* @param {string} type
* @param {IArguments | Array<unknown>} args
*/
sendConsole(type, args) {
const argsStr = JSON.stringify(args, getCircularReplacer());
if (type === 'error') {
this.logError("[CONSOLE][ERROR]" + argsStr);
} else if (type === 'warn') {
this.logWarn("[CONSOLE][WARN]" + argsStr);
} else if (type === 'debug') {
this.logDebug("[CONSOLE][DEBUG]" + argsStr);
} else {
this.logInfo("[CONSOLE][LOG]" + argsStr);
}
}
}
const newRelic = new NewRelic();
export default newRelic;
export { NewRelicMask, NewRelicUnmask, NewRelicBlock } from './new-relic/replay/newrelic-mask-view';