diff --git a/.gitignore b/.gitignore index d992a7f..df0f252 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ typings/ .env .idea -package-lock.json \ No newline at end of file +package-lock.json +.vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5885006..2c6ed57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +# 6.2.5 + +### New in this release +* fetch instrumentation for http request +* Adds configurable request header instrumentation to network events + The agent will now produce network event attributes for select header values if the headers are detected on the request. The header names to instrument are passed into the agent when started. +* Upgrading the native iOS agent to version 7.4.8. +* Upgrading the native Android agent to version 7.2.0. + + +# 6.2.4 + +### New in this release +* Upgraded native Android agent to v7.1.0 + # 6.2.3 ### New in this release diff --git a/README.md b/README.md index 03b9bdf..29e76e0 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,12 @@ By default, these configurations are already set to true on agent start. NewRelic.shutdown(); ``` +### [addHTTPHeadersTrackingFor(...)](https://docs.newrelic.com/docs/mobile-monitoring/new-relic-mobile/mobile-sdk/add-tracked-headers/) +> This API allows you to add any header field strings to a list that gets recorded as attributes with networking request events. After header fields have been added using this function, if the headers are in a network call they will be included in networking events in NR1. +```js + NewRelic.addHTTPHeadersTrackingFor(["Car"]); +``` + ### [httpRequestBodyCaptureEnabled](https://docs.newrelic.com/docs/mobile-monitoring/new-relic-mobile-android/android-sdk-api/android-agent-configuration-feature-flags/#ff-withHttpResponseBodyCaptureEnabled)(enabled: boolean) : void; > Enable or disable capture of HTTP response bodies for HTTP error traces, and MobileRequestError events. ```js diff --git a/package.json b/package.json index 9b25b66..ee7910c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "newrelic-cordova-plugin", - "version": "6.2.4", + "version": "6.2.5", "description": "New Relic Cordova Plugin for iOS and Android", "repo": "https://github.com/newrelic/newrelic-cordova-plugin/", "scripts": { diff --git a/plugin.xml b/plugin.xml index ce2741d..2af25cc 100644 --- a/plugin.xml +++ b/plugin.xml @@ -6,7 +6,7 @@ + id="newrelic-cordova-plugin" version="6.2.5"> NewRelic New Relic Cordova Plugin for iOS and Android New Relic @@ -18,7 +18,7 @@ - + @@ -26,8 +26,8 @@ - - + + @@ -71,7 +71,7 @@ - + @@ -81,7 +81,7 @@ - + diff --git a/src/android/NewRelicCordovaPlugin.java b/src/android/NewRelicCordovaPlugin.java index 656011c..e7ae6ec 100644 --- a/src/android/NewRelicCordovaPlugin.java +++ b/src/android/NewRelicCordovaPlugin.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2022-present New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: Apache-2.0 */ package com.newrelic.cordova.plugin; @@ -11,10 +11,12 @@ import com.newrelic.agent.android.Agent; import com.newrelic.agent.android.ApplicationFramework; +import com.newrelic.agent.android.HttpHeaders; import com.newrelic.agent.android.NewRelic; import com.newrelic.agent.android.analytics.AnalyticsAttribute; import com.newrelic.agent.android.distributedtracing.TraceContext; import com.newrelic.agent.android.distributedtracing.TraceHeader; +import com.newrelic.agent.android.distributedtracing.TracePayload; import com.newrelic.agent.android.harvest.DeviceInformation; import com.newrelic.agent.android.logging.AgentLog; import com.newrelic.agent.android.stats.StatsEngine; @@ -22,6 +24,7 @@ import com.newrelic.agent.android.util.NetworkFailure; import com.newrelic.agent.android.FeatureFlag; import com.newrelic.com.google.gson.Gson; +import com.newrelic.com.google.gson.JsonArray; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; @@ -33,6 +36,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -41,11 +45,11 @@ public class NewRelicCordovaPlugin extends CordovaPlugin { private final static String TAG = NewRelicCordovaPlugin.class.getSimpleName(); private final Pattern chromeStackTraceRegex = - Pattern.compile("^\\s*at (.*?) ?\\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\\/|[a-z]:\\\\|\\\\\\\\).*?)(?::(\\d+))?(?::(\\d+))?\\)?\\s*$", - Pattern.CASE_INSENSITIVE); + Pattern.compile("^\\s*at (.*?) ?\\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\\/|[a-z]:\\\\|\\\\\\\\).*?)(?::(\\d+))?(?::(\\d+))?\\)?\\s*$", + Pattern.CASE_INSENSITIVE); private final Pattern nodeStackTraceRegex = - Pattern.compile("^\\s*at (?:((?:\\[object object\\])?[^\\\\/]+(?: \\[as \\S+\\])?) )?\\(?(.*?):(\\d+)(?::(\\d+))?\\)?\\s*$", - Pattern.CASE_INSENSITIVE); + Pattern.compile("^\\s*at (?:((?:\\[object object\\])?[^\\\\/]+(?: \\[as \\S+\\])?) )?\\(?(.*?):(\\d+)(?::(\\d+))?\\)?\\s*$", + Pattern.CASE_INSENSITIVE); @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { @@ -62,16 +66,16 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) { final DeviceInformation devInfo = Agent.getDeviceInformation(); if (preferences.getString("crash_reporting_enabled", "true").equalsIgnoreCase("false")) { - NewRelic.disableFeature(FeatureFlag.CrashReporting); + NewRelic.disableFeature(FeatureFlag.CrashReporting); } if (preferences.getString("distributed_tracing_enabled", "true").equalsIgnoreCase("false")) { - NewRelic.disableFeature(FeatureFlag.DistributedTracing); + NewRelic.disableFeature(FeatureFlag.DistributedTracing); } if (preferences.getString("interaction_tracing_enabled", "true").equalsIgnoreCase("false")) { - NewRelic.disableFeature(FeatureFlag.InteractionTracing); + NewRelic.disableFeature(FeatureFlag.InteractionTracing); } if (preferences.getString("default_interactions_enabled", "true").equalsIgnoreCase("false")) { - NewRelic.disableFeature(FeatureFlag.DefaultInteractions); + NewRelic.disableFeature(FeatureFlag.DefaultInteractions); } if (preferences.getString("fedramp_enabled", "false").equalsIgnoreCase("true")) { NewRelic.enableFeature(FeatureFlag.FedRampEnabled); @@ -87,30 +91,30 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) { int logLevel = AgentLog.INFO; String configLogLevel = preferences.getString("loglevel", "INFO").toUpperCase(); if (strToLogLevel.containsKey(configLogLevel)) { - logLevel = strToLogLevel.get(configLogLevel); + logLevel = strToLogLevel.get(configLogLevel); } String collectorAddress = preferences.getString("collector_address", null); String crashCollectorAddress = preferences.getString("crash_collector_address", null); NewRelic newRelic = NewRelic.withApplicationToken(appToken) - .withApplicationFramework(ApplicationFramework.Cordova, pluginVersion) - .withLoggingEnabled(preferences.getString("logging_enabled", "true").toLowerCase().equals("true")) - .withLogLevel(logLevel); + .withApplicationFramework(ApplicationFramework.Cordova, pluginVersion) + .withLoggingEnabled(preferences.getString("logging_enabled", "true").toLowerCase().equals("true")) + .withLogLevel(logLevel); if (isEmptyConfigParameter(collectorAddress) && isEmptyConfigParameter(crashCollectorAddress)) { - newRelic.start(this.cordova.getActivity().getApplication()); + newRelic.start(this.cordova.getActivity().getApplication()); } else { - // Set missing collector addresses (if any) - if (collectorAddress == null) { - collectorAddress = "mobile-collector.newrelic.com"; - } - if (crashCollectorAddress == null) { - crashCollectorAddress = "mobile-crash.newrelic.com"; - } - newRelic.usingCollectorAddress(collectorAddress); - newRelic.usingCrashCollectorAddress(crashCollectorAddress); - newRelic.start(this.cordova.getActivity().getApplication()); + // Set missing collector addresses (if any) + if (collectorAddress == null) { + collectorAddress = "mobile-collector.newrelic.com"; + } + if (crashCollectorAddress == null) { + crashCollectorAddress = "mobile-crash.newrelic.com"; + } + newRelic.usingCollectorAddress(collectorAddress); + newRelic.usingCrashCollectorAddress(crashCollectorAddress); + newRelic.start(this.cordova.getActivity().getApplication()); } newRelic.start(this.cordova.getActivity().getApplication()); @@ -121,30 +125,30 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) { public boolean isEmptyConfigParameter(String parameter) { return parameter == null || parameter.isEmpty() || parameter.equals("x"); - } + } public StackTraceElement[] parseStackTrace(String stack) { String[] lines = stack.split("\n"); ArrayList stackTraceList = new ArrayList<>(); - + for (String line : lines) { - Matcher chromeMatcher = chromeStackTraceRegex.matcher(line); - Matcher nodeMatcher = nodeStackTraceRegex.matcher(line); - if (chromeMatcher.matches() || nodeMatcher.matches()) { - Matcher matcher = chromeMatcher.matches() ? chromeMatcher : nodeMatcher; - try { - String method = matcher.group(1) == null ? " " : matcher.group(1); - String file = matcher.group(2) == null ? " " : matcher.group(2); - int lineNumber = matcher.group(3) == null ? 1 : Integer.parseInt(matcher.group(3)); - stackTraceList.add(new StackTraceElement("", method, file, lineNumber)); - } catch (Exception e) { - NewRelic.recordHandledException(e); - return new StackTraceElement[0]; + Matcher chromeMatcher = chromeStackTraceRegex.matcher(line); + Matcher nodeMatcher = nodeStackTraceRegex.matcher(line); + if (chromeMatcher.matches() || nodeMatcher.matches()) { + Matcher matcher = chromeMatcher.matches() ? chromeMatcher : nodeMatcher; + try { + String method = matcher.group(1) == null ? " " : matcher.group(1); + String file = matcher.group(2) == null ? " " : matcher.group(2); + int lineNumber = matcher.group(3) == null ? 1 : Integer.parseInt(matcher.group(3)); + stackTraceList.add(new StackTraceElement("", method, file, lineNumber)); + } catch (Exception e) { + NewRelic.recordHandledException(e); + return new StackTraceElement[0]; + } } - } } return stackTraceList.toArray(new StackTraceElement[0]); - } + } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { @@ -187,15 +191,15 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo break; } case "recordLogs": { - if (preferences.getString("console_logs_enabled", "true").equalsIgnoreCase("true")) { - final String eventType = args.getString(0); - final String eventName = args.getString(1); - final JSONObject attributesASJson = args.getJSONObject(2); - final Map attributes = new Gson().fromJson(String.valueOf(attributesASJson), - Map.class); - NewRelic.recordCustomEvent(eventType, eventName, attributes); - break; - } + if (preferences.getString("console_logs_enabled", "true").equalsIgnoreCase("true")) { + final String eventType = args.getString(0); + final String eventName = args.getString(1); + final JSONObject attributesASJson = args.getJSONObject(2); + final Map attributes = new Gson().fromJson(String.valueOf(attributesASJson), + Map.class); + NewRelic.recordCustomEvent(eventType, eventName, attributes); + break; + } } case "setAttribute": { final String name = args.getString(0); @@ -228,7 +232,7 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo exceptionMap.put("isFatal", isFatal); if (attributesAsJson != null) { final Map attributes = new Gson().fromJson(String.valueOf(attributesAsJson), - Map.class); + Map.class); for (String key : attributes.keySet()) { exceptionMap.put(key, attributes.get(key)); } @@ -238,9 +242,9 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo } if(errorStack == null) { - NewRelic.recordBreadcrumb("JS Errors", exceptionMap); - StatsEngine.get().inc("Supportability/Mobile/Cordova/JSError"); - break; + NewRelic.recordBreadcrumb("JS Errors", exceptionMap); + StatsEngine.get().inc("Supportability/Mobile/Cordova/JSError"); + break; } StackTraceElement[] stackTraceElements = parseStackTrace(errorStack); @@ -259,24 +263,50 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo final int bytesSent = args.getInt(5); final int bytesReceived = args.getInt(6); final String body = args.getString(7); + final Object traceAttributes = args.get(9); + Map traceHeadersMap = new HashMap(); + if (traceAttributes instanceof JSONObject) { + traceHeadersMap = new Gson().fromJson(String.valueOf(traceAttributes), Map.class); + } + + JSONObject params = args.getJSONObject(8); + Map paramsMap = new HashMap<>(); + if (params != null) { + paramsMap = new Gson().fromJson(String.valueOf(params), Map.class); + } NewRelic.noticeHttpTransaction(url, method, status, startTime, endTime, bytesSent, bytesReceived, - body); + body,paramsMap,"",traceHeadersMap); break; } - case "noticeDistributedTrace": { + case "generateDistributedTracingHeaders": { cordova.getThreadPool().execute(new Runnable() { public void run() { try { + + JSONObject dtHeaders = new JSONObject(); TraceContext traceContext = NewRelic.noticeDistributedTrace(null); + TracePayload tracePayload = traceContext.getTracePayload(); - Map traceAttributes = new HashMap<>(traceContext.asTraceAttributes()); - for (TraceHeader header : traceContext.getHeaders()) { - traceAttributes.put(header.getHeaderName(), header.getHeaderValue()); - } + String headerName = tracePayload.getHeaderName(); + String headerValue = tracePayload.getHeaderValue(); + String spanId = tracePayload.getSpanId(); + String traceId = tracePayload.getTraceId(); + String parentId = traceContext.getParentId(); + String vendor = traceContext.getVendor(); + String accountId = traceContext.getAccountId(); + String applicationId = traceContext.getApplicationId(); - callbackContext.success(new JSONObject(traceAttributes)); + dtHeaders.put(headerName, headerValue); + dtHeaders.put(NRTraceConstants.TRACE_PARENT, "00-" + traceId + "-" + parentId + "-00"); + dtHeaders.put(NRTraceConstants.TRACE_STATE, vendor + "=0-2-" + accountId + "-" + applicationId + "-" + parentId + "----" + System.currentTimeMillis()); + dtHeaders.put(NRTraceConstants.TRACE_ID, traceId); + dtHeaders.put(NRTraceConstants.ID, spanId); + dtHeaders.put(NRTraceConstants.GUID, spanId); + + + callbackContext.success(dtHeaders); } catch (Exception e) { NewRelic.recordHandledException(e); @@ -418,7 +448,29 @@ public void run() { NewRelic.shutdown(); break; } - + case "addHTTPHeadersTrackingFor": { + final JSONArray headers = args.getJSONArray(0); + + List headerList = new ArrayList<>(); + if (headers != null) { + for (int i = 0; i < headers.length(); i++) { + headerList.add(headers.getString(i)); + } + } + NewRelic.addHTTPHeadersTrackingFor(headerList); + break; + } + case "getHTTPHeadersTrackingFor": { + JSONObject headers = new JSONObject(); + List arr = new ArrayList<>(HttpHeaders.getInstance().getHttpHeaders()); + JsonArray array = new JsonArray(); + for (int i = 0; i < arr.size(); i++) { + array.add(arr.get(i)); + } + headers.put("headersList", array); + callbackContext.success(headers); + } + } } catch (Exception e) { NewRelic.recordHandledException(e); @@ -429,4 +481,12 @@ public void run() { } + protected static final class NRTraceConstants { + public static final String TRACE_PARENT = "traceparent"; + public static final String TRACE_STATE = "tracestate"; + public static final String TRACE_ID = "trace.id"; + public static final String GUID = "guid"; + public static final String ID = "id"; + } + } diff --git a/src/ios/NewRelicCordovaPlugin.h b/src/ios/NewRelicCordovaPlugin.h index 3fc2454..506c509 100644 --- a/src/ios/NewRelicCordovaPlugin.h +++ b/src/ios/NewRelicCordovaPlugin.h @@ -59,4 +59,10 @@ - (void)shutdown:(CDVInvokedUrlCommand *) command; +- (void)addHTTPHeadersTrackingFor:(CDVInvokedUrlCommand *) command; + +- (void)getHTTPHeadersTrackingFor:(CDVInvokedUrlCommand *) command; + +- (void)generateDistributedTracingHeaders:(CDVInvokedUrlCommand *)command; + @end diff --git a/src/ios/NewRelicCordovaPlugin.m b/src/ios/NewRelicCordovaPlugin.m index 184f6f6..61614c1 100644 --- a/src/ios/NewRelicCordovaPlugin.m +++ b/src/ios/NewRelicCordovaPlugin.m @@ -389,4 +389,31 @@ - (void)shutdown:(CDVInvokedUrlCommand *)command { [NewRelic shutdown]; } +- (void)addHTTPHeadersTrackingFor:(CDVInvokedUrlCommand *) command{ + NSArray* headers = [command.arguments objectAtIndex:0]; + [NewRelic addHTTPHeaderTrackingFor:headers]; + +} + +- (void)getHTTPHeadersTrackingFor:(CDVInvokedUrlCommand *) command{ + + CDVPluginResult* pluginResult = nil; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary: @{@"headersList": @"[]"}]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + +} + +- (void)generateDistributedTracingHeaders:(CDVInvokedUrlCommand *)command { + + CDVPluginResult* pluginResult = nil; + + NSDictionary* headers = [NewRelic generateDistributedTracingHeaders]; + + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:headers]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + +} + @end diff --git a/types/newrelic.d.ts b/types/newrelic.d.ts index 46ee3b2..3c11a77 100644 --- a/types/newrelic.d.ts +++ b/types/newrelic.d.ts @@ -107,3 +107,6 @@ export function httpRequestBodyCaptureEnabled( cb: any, fail: any ): void; +export function addHTTPHeadersTrackingFor(name: string, cb: any, fail: any,headers:string[]): void; +export function getHTTPHeadersTrackingFor(name: string, cb: any, fail: any): Promise; +export function generateDistributedTracingHeaders(name: string, cb: any, fail: any): Promise; diff --git a/www/js/newrelic.js b/www/js/newrelic.js index 4bda4be..28bc1dd 100644 --- a/www/js/newrelic.js +++ b/www/js/newrelic.js @@ -1,556 +1,670 @@ -/* - * Copyright (c) 2022-present New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -var exec = require("cordova/exec"); - -var NewRelic = { - - /** - * - * @param {string} url The URL of the request. - * @param {string} method The HTTP method used, such as GET or POST. - * @param {number} status The statusCode of the HTTP response, such as 200 for OK. - * @param {number} startTime The start time of the request in milliseconds since the epoch. - * @param {number} endTime The end time of the request in milliseconds since the epoch. - * @param {number} bytesSent The number of bytes sent in the request. - * @param {number} bytesReceived The number of bytes received in the response. - * @param {string} body Optional. 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. +cordova.define("newrelic-cordova-plugin.NewRelic", function(require, exports, module) { + /* + * Copyright (c) 2022-present New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 */ - noticeHttpTransaction: function (url, method, status, startTime, endTime, bytesSent, bytesReceived, body, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "noticeHttpTransaction", [url, method, status, startTime, endTime, bytesSent, bytesReceived, body]); - }, - - noticeDistributedTrace: function (cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "noticeDistributedTrace"); - }, - /** - * Sets a custom user identifier value to associate mobile user - * @param {string} userId The user identifier string. - */ - setUserId: function (userId, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "setUserId", [userId]); - }, - - /** - * 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 {string} attributeName Name of the attribute. - * @param {number} value Value of the attribute. - */ - setAttribute: function (attributeName, value, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "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 {string} name Name of the attribute. - */ - removeAttribute: function (name, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "removeAttribute", [name]); - }, - - /** - * Creates and records a MobileBreadcrumb event. - * @param {string} eventName The name you want to give to a breadcrumb event. - * @param {Map} attributes A map that includes a list of attributes. - */ - recordBreadcrumb: function (eventName, attributes, cb, fail) { - const crumb = new BreadCrumb({ eventName, attributes }); - crumb.attributes.isValid(() => { - crumb.eventName.isValid(() => { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordBreadCrumb", [eventName, crumb.attributes.value]); + var exec = require("cordova/exec"); + + var NewRelic = { + + /** + * + * @param {string} url The URL of the request. + * @param {string} method The HTTP method used, such as GET or POST. + * @param {number} status The statusCode of the HTTP response, such as 200 for OK. + * @param {number} startTime The start time of the request in milliseconds since the epoch. + * @param {number} endTime The end time of the request in milliseconds since the epoch. + * @param {number} bytesSent The number of bytes sent in the request. + * @param {number} bytesReceived The number of bytes received in the response. + * @param {string} body Optional. 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: function (url, method, status, startTime, endTime, bytesSent, bytesReceived, body,params,traceAttributes, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "noticeHttpTransaction", [url, method, status, startTime, endTime, bytesSent, bytesReceived, body,params,traceAttributes]); + }, + + noticeDistributedTrace: function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "noticeDistributedTrace"); + }, + + /** + * Sets a custom user identifier value to associate mobile user + * @param {string} userId The user identifier string. + */ + setUserId: function (userId, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "setUserId", [userId]); + }, + + /** + * 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 {string} attributeName Name of the attribute. + * @param {number} value Value of the attribute. + */ + setAttribute: function (attributeName, value, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "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 {string} name Name of the attribute. + */ + removeAttribute: function (name, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "removeAttribute", [name]); + }, + + /** + * Creates and records a MobileBreadcrumb event. + * @param {string} eventName The name you want to give to a breadcrumb event. + * @param {Map} attributes A map that includes a list of attributes. + */ + recordBreadcrumb: function (eventName, attributes, cb, fail) { + const crumb = new BreadCrumb({ eventName, attributes }); + crumb.attributes.isValid(() => { + crumb.eventName.isValid(() => { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordBreadCrumb", [eventName, crumb.attributes.value]); + }); }); - }); - }, - - /** - * Creates and records a custom event, for use in New Relic Insights. - * The event includes a list of attributes, specified as a map. - * @param {string} eventType The type of event. - * @param {string} eventName The name of the event. - * @param {Map} attributes A map that includes a list of attributes. - */ - recordCustomEvent: function (eventType, eventName, attributes, cb, fail) { - const customEvent = new NewRelicEvent({ eventType, eventName, attributes }); - customEvent.attributes.isValid(() => { - if (customEvent.eventName.isValid() && customEvent.eventType.isValid()) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordCustomEvent", [eventType, eventName, customEvent.attributes.value]); - } else { - window.console.error("Invalid event name or type in recordCustomEvent"); - } - }); - }, - + }, + /** - * Creates and records log as custom Events, for use in New Relic Insights. - * The event includes a list of attributes, specified as a map. - * @param {string} eventType The type of event. - * @param {string} eventName The name of the event. - * @param {Map} attributes A map that includes a list of attributes. - */ - recordLogs: function (eventType, eventName, attributes, cb, fail) { + * Creates and records a custom event, for use in New Relic Insights. + * The event includes a list of attributes, specified as a map. + * @param {string} eventType The type of event. + * @param {string} eventName The name of the event. + * @param {Map} attributes A map that includes a list of attributes. + */ + recordCustomEvent: function (eventType, eventName, attributes, cb, fail) { const customEvent = new NewRelicEvent({ eventType, eventName, attributes }); customEvent.attributes.isValid(() => { if (customEvent.eventName.isValid() && customEvent.eventType.isValid()) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordLogs", [eventType, eventName, customEvent.attributes.value]); + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordCustomEvent", [eventType, eventName, customEvent.attributes.value]); } else { window.console.error("Invalid event name or type in recordCustomEvent"); } }); }, - - /** - * Track a method as an interaction. - * @param {string} actionName The name of the action. - * @param {function} cb A success callback function. - * @returns {Promise} A promise containing the interactionId. - */ - startInteraction: function (actionName, cb, fail) { - return new Promise(function (cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "startInteraction", [actionName]); - }); - }, - - /** - * End an interaction - * @param {string} interactionId The string ID for the interaction you want to end. This string is returned when you use startInteraction(). - */ - endInteraction: function (interactionId, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "endInteraction", [interactionId]); - }, - - sendConsole(type, args) { - const argsStr = JSON.stringify(args, getCircularReplacer()); - this.send('MobileJSConsole', { consoleType: type, args: argsStr }); - }, - - send(name, args) { - const nameStr = String(name); - const argsStr = {}; - Object.keys(args).forEach(key => { - argsStr[String(key)] = String(args[key]); - }); - - this.recordLogs("consoleEvents", name, args); - }, - - /** - * Records JavaScript errors for Cordova. - * @param {Error} err The error to record. - * @param {Map} attributes Optional attributes that will be appended to the handled exception event created in insights. - */ - recordError: function(err, attributes={}, cb, fail) { - let errorAttributes = attributes instanceof Map ? Object.fromEntries(attributes) : attributes; - if (attributes === null) { - errorAttributes = {}; - } - if (err) { - var error; - - if (err instanceof Error) { - error = err; - } - - if (typeof err === 'string') { - error = new Error(err || ''); + + /** + * Creates and records log as custom Events, for use in New Relic Insights. + * The event includes a list of attributes, specified as a map. + * @param {string} eventType The type of event. + * @param {string} eventName The name of the event. + * @param {Map} attributes A map that includes a list of attributes. + */ + recordLogs: function (eventType, eventName, attributes, cb, fail) { + const customEvent = new NewRelicEvent({ eventType, eventName, attributes }); + customEvent.attributes.isValid(() => { + if (customEvent.eventName.isValid() && customEvent.eventType.isValid()) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordLogs", [eventType, eventName, customEvent.attributes.value]); + } else { + window.console.error("Invalid event name or type in recordCustomEvent"); + } + }); + }, + + /** + * Track a method as an interaction. + * @param {string} actionName The name of the action. + * @param {function} cb A success callback function. + * @returns {Promise} A promise containing the interactionId. + */ + startInteraction: function (actionName, cb, fail) { + return new Promise(function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "startInteraction", [actionName]); + }); + }, + + /** + * End an interaction + * @param {string} interactionId The string ID for the interaction you want to end. This string is returned when you use startInteraction(). + */ + endInteraction: function (interactionId, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "endInteraction", [interactionId]); + }, + + sendConsole(type, args) { + const argsStr = JSON.stringify(args, getCircularReplacer()); + this.send('MobileJSConsole', { consoleType: type, args: argsStr }); + }, + + send(name, args) { + const nameStr = String(name); + const argsStr = {}; + Object.keys(args).forEach(key => { + argsStr[String(key)] = String(args[key]); + }); + + this.recordLogs("consoleEvents", name, args); + }, + + /** + * Records JavaScript errors for Cordova. + * @param {Error} err The error to record. + * @param {Map} attributes Optional attributes that will be appended to the handled exception event created in insights. + */ + recordError: function(err, attributes={}, cb, fail) { + let errorAttributes = attributes instanceof Map ? Object.fromEntries(attributes) : attributes; + if (attributes === null) { + errorAttributes = {}; } - - if(error !== undefined) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordError", [error.name, error.message, error.stack, false, errorAttributes]); + if (err) { + var error; + + if (err instanceof Error) { + error = err; + } + + if (typeof err === 'string') { + error = new Error(err || ''); + } + + if(error !== undefined) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordError", [error.name, error.message, error.stack, false, errorAttributes]); + } else { + window.console.warn('Undefined error in NewRelic.recordError'); + } + } else { - window.console.warn('Undefined error in NewRelic.recordError'); + window.console.warn('Error is required in NewRelic.recordError'); } - - } else { - window.console.warn('Error is required in NewRelic.recordError'); - } - }, - - /** - * Throws a demo run-time exception to test New Relic crash reporting. - * @param {string} message An optional argument attached to the exception. - */ - crashNow: function (message = '', cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "crashNow", [message]); - }, - - /** - * Returns the current session ID as a parameter to the successful callback function. - * This method is useful for consolidating monitoring of app data (not just New Relic data) based on a single session definition and identifier. - * @param {function} cb A success callback function. - * @returns {Promise} A promise containing the current session ID. - */ - currentSessionId: function (cb, fail) { - return new Promise(function (cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "currentSessionId"); - }); - }, - - /** - * 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 {string} name The name of the attribute. - * @param {number} value Optional argument that increments the attribute by this value. - */ - incrementAttribute: function(name, value=1, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "incrementAttribute", [name, value]); - }, - - /** - * 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 {string} url The URL of the request. - * @param {string} httpMethod The HTTP method used, such as GET or POST. - * @param {number} startTime The start time of the request in milliseconds since the epoch. - * @param {number} endTime The end time of the request in milliseconds since the epoch. - * @param {string} failure The name of the network failure. Possible values are 'Unknown', 'BadURL', 'TimedOut', 'CannotConnectToHost', 'DNSLookupFailed', 'BadServerResponse', 'SecureConnectionFailed'. - */ - noticeNetworkFailure: function (url, httpMethod, startTime, endTime, failure, cb, fail) { - const failureNames = new Set(['Unknown', 'BadURL', 'TimedOut', 'CannotConnectToHost', 'DNSLookupFailed', 'BadServerResponse', 'SecureConnectionFailed']); - if (!failureNames.has(failure)) { - window.console.error("NewRelic.noticeNetworkFailure: Network failure name has to be one of: 'Unknown', 'BadURL', 'TimedOut', 'CannotConnectToHost', 'DNSLookupFailed', 'BadServerResponse', 'SecureConnectionFailed'"); - return; - } - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "noticeNetworkFailure", [url, httpMethod, startTime, endTime, failure]); - }, - - /** - * - * @param {string} name The name for the custom metric. - * @param {string} category The metric category name. - * @param {number} value Optional. The value of the metric. Value should be a non-zero positive number. - * @param {string} countUnit Optional (but requires value and valueUnit to be set). Unit of measurement for the metric count. Supported values are 'PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', or 'OPERATIONS'. - * @param {string} valueUnit Optional (but requires value and countUnit to be set). Unit of measurement for the metric value. Supported values are 'PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', or 'OPERATIONS'. - */ - recordMetric: function (name, category, value = -1, countUnit = null, valueUnit = null, cb, fail) { - const metricUnits = new Set(['PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', 'OPERATIONS']); - if (value < 0) { - if (countUnit !== null || valueUnit !== null) { - window.console.error('NewRelic.recordMetric: value must be set in recordMetric if countUnit and valueUnit are set'); + }, + + /** + * Throws a demo run-time exception to test New Relic crash reporting. + * @param {string} message An optional argument attached to the exception. + */ + crashNow: function (message = '', cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "crashNow", [message]); + }, + + /** + * Returns the current session ID as a parameter to the successful callback function. + * This method is useful for consolidating monitoring of app data (not just New Relic data) based on a single session definition and identifier. + * @param {function} cb A success callback function. + * @returns {Promise} A promise containing the current session ID. + */ + currentSessionId: function (cb, fail) { + return new Promise(function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "currentSessionId"); + }); + }, + + /** + * 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 {string} name The name of the attribute. + * @param {number} value Optional argument that increments the attribute by this value. + */ + incrementAttribute: function(name, value=1, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "incrementAttribute", [name, value]); + }, + + /** + * 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 {string} url The URL of the request. + * @param {string} httpMethod The HTTP method used, such as GET or POST. + * @param {number} startTime The start time of the request in milliseconds since the epoch. + * @param {number} endTime The end time of the request in milliseconds since the epoch. + * @param {string} failure The name of the network failure. Possible values are 'Unknown', 'BadURL', 'TimedOut', 'CannotConnectToHost', 'DNSLookupFailed', 'BadServerResponse', 'SecureConnectionFailed'. + */ + noticeNetworkFailure: function (url, httpMethod, startTime, endTime, failure, cb, fail) { + const failureNames = new Set(['Unknown', 'BadURL', 'TimedOut', 'CannotConnectToHost', 'DNSLookupFailed', 'BadServerResponse', 'SecureConnectionFailed']); + if (!failureNames.has(failure)) { + window.console.error("NewRelic.noticeNetworkFailure: Network failure name has to be one of: 'Unknown', 'BadURL', 'TimedOut', 'CannotConnectToHost', 'DNSLookupFailed', 'BadServerResponse', 'SecureConnectionFailed'"); return; } - } else { - if ((countUnit !== null && valueUnit == null) || (countUnit == null && valueUnit !== null)) { - window.console.error('NewRelic.recordMetric: countUnit and valueUnit in recordMetric must both be null or set'); - return; - } else if (countUnit !== null && valueUnit !== null) { - if (!metricUnits.has(countUnit) || !metricUnits.has(valueUnit)) { - window.console.error("NewRelic.recordMetric: countUnit or valueUnit in recordMetric has to be one of 'PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', 'OPERATIONS'"); + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "noticeNetworkFailure", [url, httpMethod, startTime, endTime, failure]); + }, + + /** + * + * @param {string} name The name for the custom metric. + * @param {string} category The metric category name. + * @param {number} value Optional. The value of the metric. Value should be a non-zero positive number. + * @param {string} countUnit Optional (but requires value and valueUnit to be set). Unit of measurement for the metric count. Supported values are 'PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', or 'OPERATIONS'. + * @param {string} valueUnit Optional (but requires value and countUnit to be set). Unit of measurement for the metric value. Supported values are 'PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', or 'OPERATIONS'. + */ + recordMetric: function (name, category, value = -1, countUnit = null, valueUnit = null, cb, fail) { + const metricUnits = new Set(['PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', 'OPERATIONS']); + if (value < 0) { + if (countUnit !== null || valueUnit !== null) { + window.console.error('NewRelic.recordMetric: value must be set in recordMetric if countUnit and valueUnit are set'); return; } - } - } - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordMetric", [name, category, value, countUnit, valueUnit]); - }, - - /** - * Removes all attributes from the session.. - */ - removeAllAttributes: function (cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "removeAllAttributes"); - }, - - /** - * 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 {number} maxBufferTimeInSeconds The maximum time (in seconds) that the agent should store events in memory. - */ - setMaxEventBufferTime: function (maxBufferTimeInSeconds, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "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 {number} maxPoolSize The maximum number of events per harvest cycle. - */ - setMaxEventPoolSize: function (maxPoolSize, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "setMaxEventPoolSize", [maxPoolSize]); - }, - - /** - * FOR ANDROID ONLY. - * Enable or disable collection of event data. - * @param {boolean} enabled Boolean value for enabling analytics events. - */ - analyticsEventEnabled: function (enabled, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "analyticsEventEnabled", [enabled]); - }, - - /** - * Enable or disable reporting sucessful HTTP request to the MobileRequest event type. - * @param {boolean} enabled Boolean value for enable successful HTTP requests. - */ - networkRequestEnabled: function (enabled, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "networkRequestEnabled", [enabled]); - }, - - /** - * Enable or disable reporting network and HTTP request errors to the MobileRequestError event type. - * @param {boolean} enabled Boolean value for enabling network request errors. - */ - networkErrorRequestEnabled: function (enabled, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "networkErrorRequestEnabled", [enabled]); - }, - - /** - * Enable or disable capture of HTTP response bodies for HTTP error traces, and MobileRequestError events. - * @param {boolean} enabled Boolean value for enabling HTTP response bodies. - */ - httpRequestBodyCaptureEnabled: function (enabled, cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "httpRequestBodyCaptureEnabled", [enabled]); - }, - - /** - * Shut down the agent within the current application lifecycle during runtime. - * Once the agent has shut down, it cannot be restarted within the current application lifecycle. - */ - shutdown: function (cb, fail) { - cordova.exec(cb, fail, "NewRelicCordovaPlugin", "shutdown"); - }, - -} - -networkRequest = {}; -var originalXhrOpen = XMLHttpRequest.prototype.open; -var originalXHRSend = XMLHttpRequest.prototype.send; -window.XMLHttpRequest.prototype.open = function (method, url) { - // Keep track of the method and url - // start time is tracked by the `send` method - - // eslint-disable-next-line prefer-rest-params - - networkRequest.url = url; - networkRequest.method = method; - networkRequest.bytesSent = 0; - networkRequest.startTime = Date.now(); - return originalXhrOpen.apply(this, arguments) - -} - - - - -window.XMLHttpRequest.prototype.send = function (data) { - - console.log(data); - - if (this.addEventListener) { - this.addEventListener( - 'readystatechange', async () => { - - if (this.readyState === this.HEADERS_RECEIVED) { - const contentTypeString = this.getResponseHeader('Content-Type'); - - - if (this.getAllResponseHeaders()) { - const responseHeaders = this.getAllResponseHeaders().split('\r\n'); - const responseHeadersDictionary = {}; - responseHeaders.forEach(element => { - const key = element.split(':')[0]; - const value = element.split(':')[1]; - responseHeadersDictionary[key] = value; - }); - - } - } - if (this.readyState === this.DONE) { - networkRequest.endTime = Date.now(); - networkRequest.status = this.status; - if(this.responseText !== undefined) { - networkRequest.bytesreceived = this.responseText.length; - networkRequest.body = this.responseText; - } else { - networkRequest.bytesreceived = 0; - networkRequest.body = ""; + } else { + if ((countUnit !== null && valueUnit == null) || (countUnit == null && valueUnit !== null)) { + window.console.error('NewRelic.recordMetric: countUnit and valueUnit in recordMetric must both be null or set'); + return; + } else if (countUnit !== null && valueUnit !== null) { + if (!metricUnits.has(countUnit) || !metricUnits.has(valueUnit)) { + window.console.error("NewRelic.recordMetric: countUnit or valueUnit in recordMetric has to be one of 'PERCENT', 'BYTES', 'SECONDS', 'BYTES_PER_SECOND', 'OPERATIONS'"); + return; } - - - NewRelic.noticeHttpTransaction(networkRequest.url, networkRequest.method, networkRequest.status, networkRequest.startTime, networkRequest.endTime, networkRequest.bytesSent, networkRequest.bytesreceived, networkRequest.body); - - - } - }, - false - ); - - } - console.log(Date.now()); - return originalXHRSend.apply(this, arguments); -} - -window.addEventListener("error", (event) => { - console.log(event); - if (cordova.platformId == "android") { - const err = new Error(); - err.name = event.message; - err.message = (event.error) ? event.error.message : ''; - err.stack = (event.error) ? event.error.stack : ''; - NewRelic.recordError(err); - } else if (cordova.platformId == "iOS") { - const err = new Error(); - err.name = event.message; - err.message = ''; - err.stack = ''; - NewRelic.recordError(err); - } -}); - -try { - window.addEventListener('unhandledrejection', (e) => { - const err = new Error(`${e.reason}`) - NewRelic.recordError(err); - }) -} catch (err) { - // do nothing -- addEventListener is not supported -} - - -const defaultLog = window.console.log; -const defaultWarn = window.console.warn; -const defaultError = window.console.error; - -console.log = function () { - NewRelic.sendConsole('log', arguments); - defaultLog.apply(console, arguments); -}; -console.warn = function () { - NewRelic.sendConsole('warn', arguments); - defaultWarn.apply(console, arguments); -}; -console.error = function () { - NewRelic.sendConsole('error', arguments); - defaultError.apply(console, arguments); -}; - -class Utils { - static isObject(value) { - return value instanceof Object && !(value instanceof Array); - } - - static isString(value) { - return typeof value === 'string' || value instanceof String; + } + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "recordMetric", [name, category, value, countUnit, valueUnit]); + }, + + /** + * Removes all attributes from the session.. + */ + removeAllAttributes: function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "removeAllAttributes"); + }, + + /** + * 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 {number} maxBufferTimeInSeconds The maximum time (in seconds) that the agent should store events in memory. + */ + setMaxEventBufferTime: function (maxBufferTimeInSeconds, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "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 {number} maxPoolSize The maximum number of events per harvest cycle. + */ + setMaxEventPoolSize: function (maxPoolSize, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "setMaxEventPoolSize", [maxPoolSize]); + }, + + /** + * FOR ANDROID ONLY. + * Enable or disable collection of event data. + * @param {boolean} enabled Boolean value for enabling analytics events. + */ + analyticsEventEnabled: function (enabled, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "analyticsEventEnabled", [enabled]); + }, + + /** + * Enable or disable reporting sucessful HTTP request to the MobileRequest event type. + * @param {boolean} enabled Boolean value for enable successful HTTP requests. + */ + networkRequestEnabled: function (enabled, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "networkRequestEnabled", [enabled]); + }, + + /** + * Enable or disable reporting network and HTTP request errors to the MobileRequestError event type. + * @param {boolean} enabled Boolean value for enabling network request errors. + */ + networkErrorRequestEnabled: function (enabled, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "networkErrorRequestEnabled", [enabled]); + }, + + /** + * Enable or disable capture of HTTP response bodies for HTTP error traces, and MobileRequestError events. + * @param {boolean} enabled Boolean value for enabling HTTP response bodies. + */ + httpRequestBodyCaptureEnabled: function (enabled, cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "httpRequestBodyCaptureEnabled", [enabled]); + }, + + /** + * Shut down the agent within the current application lifecycle during runtime. + * Once the agent has shut down, it cannot be restarted within the current application lifecycle. + */ + shutdown: function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "shutdown"); + }, + + addHTTPHeadersTrackingFor: function (headers,cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "addHTTPHeadersTrackingFor",[headers]); + }, + + getHTTPHeadersTrackingFor: function (cb, fail) { + + return new Promise(function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "getHTTPHeadersTrackingFor"); + }); + }, + + generateDistributedTracingHeaders: function (cb, fail) { + + return new Promise(function (cb, fail) { + cordova.exec(cb, fail, "NewRelicCordovaPlugin", "generateDistributedTracingHeaders"); + }); + }, + } + + networkRequest = {}; + var originalXhrOpen = XMLHttpRequest.prototype.open; + var originalXHRSend = XMLHttpRequest.prototype.send; - static isBool(value) { - return typeof value === 'boolean' || value instanceof Boolean; + window.XMLHttpRequest.prototype.open = function (method, url) { + // Keep track of the method and url + // start time is tracked by the `send` method + + // eslint-disable-next-line prefer-rest-params + + networkRequest.url = url; + networkRequest.method = method; + networkRequest.bytesSent = 0; + networkRequest.startTime = Date.now(); + return originalXhrOpen.apply(this, arguments) + } - static isNumber(value) { - return !Number.isNaN(parseFloat(value)) && Number.isFinite(value); - } - static notEmptyString(value) { - return value && value.length !== 0; + window.XMLHttpRequest.prototype.send = function (data) { + + console.log(data); + + if (this.addEventListener) { + this.addEventListener( + 'readystatechange', async () => { + + if (this.readyState === this.HEADERS_RECEIVED) { + const contentTypeString = this.getResponseHeader('Content-Type'); + + + if (this.getAllResponseHeaders()) { + const responseHeaders = this.getAllResponseHeaders().split('\r\n'); + const responseHeadersDictionary = {}; + responseHeaders.forEach(element => { + const key = element.split(':')[0]; + const value = element.split(':')[1]; + responseHeadersDictionary[key] = value; + }); + + } + } + if (this.readyState === this.DONE) { + networkRequest.endTime = Date.now(); + networkRequest.status = this.status; + + const type = this.responseType; + if (type === "arraybuffer") { + networkRequest.bytesreceived = this.response.byteLength; + } else if (type === "blob") { + networkRequest.bytesreceived = this.response.size; + } else if (type === "text" || type === "" || type === undefined) { + networkRequest.bytesreceived = this.responseText.length; + networkRequest.body = this.responseText; + } else { + // unsupported response type + networkRequest.bytesreceived = 0; + networkRequest.body = ""; + } + + NewRelic.noticeHttpTransaction(networkRequest.url, networkRequest.method, networkRequest.status, networkRequest.startTime, networkRequest.endTime, networkRequest.bytesSent, networkRequest.bytesreceived, networkRequest.body,networkRequest.params); + } + }, + false + ); + + } + console.log(Date.now()); + return originalXHRSend.apply(this, arguments); } - - static hasValidAttributes(attributes) { - return Utils.isObject(attributes) && attributes !== null; + + window.addEventListener("error", (event) => { + console.log(event); + if (cordova.platformId == "android") { + const err = new Error(); + err.name = event.message; + err.message = (event.error) ? event.error.message : ''; + err.stack = (event.error) ? event.error.stack : ''; + NewRelic.recordError(err); + } else if (cordova.platformId == "iOS") { + const err = new Error(); + err.name = event.message; + err.message = ''; + err.stack = ''; + NewRelic.recordError(err); + } + }); + + try { + window.addEventListener('unhandledrejection', (e) => { + const err = new Error(`${e.reason}`) + NewRelic.recordError(err); + }) + } catch (err) { + // do nothing -- addEventListener is not supported } -} - -class Validator { - constructor() { - this.isString = 'isString'; - - this.isBool = 'isBool'; - - this.isNumber = 'isNumber'; - - this.isObject = 'isObject'; - - this.notEmptyString = 'notEmptyString'; - - this.hasValidAttributes = 'hasValidAttributes'; - - this.validate = (value, rules, msg) => rules.every((rule) => { - const isValid = Utils[rule](value); - if (!isValid) { - window.console.error(msg); + + const oldFetch = window.fetch; + + window.fetch = function fetch() { + var _arguments = arguments; + var urlOrRequest = arguments[0]; + var options = arguments[1]; + + return NewRelic.getHTTPHeadersTrackingFor().then((trackingHeadersList)=>{ + console.log(trackingHeadersList); + return NewRelic.generateDistributedTracingHeaders().then((headers) => { + console.log(headers); + networkRequest.startTime = Date.now(); + if (urlOrRequest && typeof urlOrRequest === 'object') { + networkRequest.url = urlOrRequest.url; + + if (options && 'method' in options) { + networkRequest. method = options.method; + } else if (urlOrRequest && 'method' in urlOrRequest) { + networkRequest.method = urlOrRequest.method; + } + } else { + networkRequest.url = urlOrRequest; + + if (options && 'method' in options) { + networkRequest.method = options.method; + } + } + + if(options && 'headers' in options) { + options.headers['newrelic'] = headers['newrelic']; + options.headers['traceparent'] = headers['traceparent']; + options.headers['tracestate'] = headers['tracestate']; + networkRequest.params = {}; + JSON.parse(trackingHeadersList["headersList"]).forEach((e) => { + if(options.headers[e] !== undefined) { + networkRequest.params[e] = options.headers[e]; + } - return Utils[rule](value); + }); + } else { + options = {headers:{}}; + options.headers['newrelic'] = headers['newrelic']; + options.headers['traceparent'] = headers['traceparent']; + options.headers['tracestate'] = headers['tracestate']; + _arguments[1] = options; + } + + if(options && 'body' in options) { + networkRequest.bytesSent = options.body.length; + } else { + networkRequest.bytesSent = 0; + } + + if (networkRequest.method === undefined || networkRequest.method === "" ) { + networkRequest.method = 'GET'; + } + return new Promise(function (resolve, reject) { + // pass through to native fetch + oldFetch.apply(void 0, _arguments).then(function(response) { + handleFetchSuccess(response.clone(), networkRequest.method, networkRequest.url,networkRequest.startTime,headers,networkRequest.params); + resolve(response) + })["catch"](function (error) { + NewRelic.recordError(error); + reject(error); + }); }); - } -} - -class Rule { - constructor(value, rules = [], message) { - this.value = value; - this.rules = rules; - this.message = message; - this.validator = new Validator(); - } - - isValid(isValid = val => val, failedValidation = val => val) { - const hasValidValues = this.validator.validate(this.value, this.rules, this.message); - - if (hasValidValues) { - isValid(); - } else { - failedValidation(hasValidValues, this.message); - } + }); + + }); + + }; + - return hasValidValues; + function handleFetchSuccess(response, method, url, startTime,headers,params) { + response.text().then((v)=>{ + NewRelic.noticeHttpTransaction( + url, + method, + response.status, + startTime, + Date.now(), + networkRequest.bytesSent, + v.length, + v, + params, + headers + ); + + }); } -}; - -const BreadCrumb = class CustomEvent { - constructor({ eventName, attributes }) { - let validator = new Validator(); - this.eventName = new Rule(eventName, - [validator.isString, validator.notEmptyString], - `eventName '${eventName}' is not a string.`); - this.attributes = new Rule( attributes instanceof Map ? Object.fromEntries(attributes):attributes, - [validator.isObject, validator.hasValidAttributes], - `attributes '${attributes}' are not valid.`); + + const defaultLog = window.console.log; + const defaultWarn = window.console.warn; + const defaultError = window.console.error; + + console.log = function () { + NewRelic.sendConsole('log', arguments); + defaultLog.apply(console, arguments); + }; + console.warn = function () { + NewRelic.sendConsole('warn', arguments); + defaultWarn.apply(console, arguments); + }; + console.error = function () { + NewRelic.sendConsole('error', arguments); + defaultError.apply(console, arguments); + }; + + class Utils { + static isObject(value) { + return value instanceof Object && !(value instanceof Array); + } + + static isString(value) { + return typeof value === 'string' || value instanceof String; + } + + static isBool(value) { + return typeof value === 'boolean' || value instanceof Boolean; + } + + static isNumber(value) { + return !Number.isNaN(parseFloat(value)) && Number.isFinite(value); + } + + static notEmptyString(value) { + return value && value.length !== 0; + } + + static hasValidAttributes(attributes) { + return Utils.isObject(attributes) && attributes !== null; + } } -}; - -const NewRelicEvent = class CustomEvent { - constructor({ eventName = '', attributes, eventType }) { - let validator = new Validator(); - this.eventType = new Rule(eventType, - [validator.isString, validator.notEmptyString], - `eventType '${eventType}' is not a string`); - - this.eventName = new Rule(eventName, - [validator.isString], - `eventName '${eventName}' is not a string`); - - this.attributes = new Rule( attributes instanceof Map ? Object.fromEntries(attributes):attributes, - [validator.isObject, validator.hasValidAttributes], - `attributes '${attributes}' are not valid.`); + + class Validator { + constructor() { + this.isString = 'isString'; + + this.isBool = 'isBool'; + + this.isNumber = 'isNumber'; + + this.isObject = 'isObject'; + + this.notEmptyString = 'notEmptyString'; + + this.hasValidAttributes = 'hasValidAttributes'; + + this.validate = (value, rules, msg) => rules.every((rule) => { + const isValid = Utils[rule](value); + if (!isValid) { + window.console.error(msg); + } + return Utils[rule](value); + }); + } } -}; - -/** - * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value - * Any copyright is dedicated to the Public Domain: https://creativecommons.org/publicdomain/zero/1.0/ - */ -const getCircularReplacer = () => { - const seen = new WeakSet(); - return (key, value) => { - if (typeof value === "object" && value !== null) { - if (seen.has(value)) { - return; + + class Rule { + constructor(value, rules = [], message) { + this.value = value; + this.rules = rules; + this.message = message; + this.validator = new Validator(); + } + + isValid(isValid = val => val, failedValidation = val => val) { + const hasValidValues = this.validator.validate(this.value, this.rules, this.message); + + if (hasValidValues) { + isValid(); + } else { + failedValidation(hasValidValues, this.message); + } + + return hasValidValues; } - seen.add(value); - } - return value; }; -}; - -module.exports = NewRelic; + + const BreadCrumb = class CustomEvent { + constructor({ eventName, attributes }) { + let validator = new Validator(); + this.eventName = new Rule(eventName, + [validator.isString, validator.notEmptyString], + `eventName '${eventName}' is not a string.`); + this.attributes = new Rule( attributes instanceof Map ? Object.fromEntries(attributes):attributes, + [validator.isObject, validator.hasValidAttributes], + `attributes '${attributes}' are not valid.`); + } + }; + + const NewRelicEvent = class CustomEvent { + constructor({ eventName = '', attributes, eventType }) { + let validator = new Validator(); + this.eventType = new Rule(eventType, + [validator.isString, validator.notEmptyString], + `eventType '${eventType}' is not a string`); + + this.eventName = new Rule(eventName, + [validator.isString], + `eventName '${eventName}' is not a string`); + + this.attributes = new Rule( attributes instanceof Map ? Object.fromEntries(attributes):attributes, + [validator.isObject, validator.hasValidAttributes], + `attributes '${attributes}' are not valid.`); + } + }; + + /** + * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value + * Any copyright is dedicated to the Public Domain: https://creativecommons.org/publicdomain/zero/1.0/ + */ + const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key, value) => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; + }; + + module.exports = NewRelic; + + }); +