noibu-react-native 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/dist/api/clientConfig.js +416 -0
- package/dist/api/helpCode.js +106 -0
- package/dist/api/inputManager.js +233 -0
- package/dist/api/metroplexSocket.js +882 -0
- package/dist/api/storedMetrics.js +201 -0
- package/dist/api/storedPageVisit.js +235 -0
- package/dist/const_matchers.js +260 -0
- package/dist/constants.d.ts +264 -0
- package/dist/constants.js +528 -0
- package/dist/entry/index.d.ts +8 -0
- package/dist/entry/index.js +15 -0
- package/dist/entry/init.js +91 -0
- package/dist/monitors/clickMonitor.js +284 -0
- package/dist/monitors/elementMonitor.js +174 -0
- package/dist/monitors/errorMonitor.js +295 -0
- package/dist/monitors/gqlErrorValidator.js +306 -0
- package/dist/monitors/httpDataBundler.js +665 -0
- package/dist/monitors/inputMonitor.js +130 -0
- package/dist/monitors/keyboardInputMonitor.js +67 -0
- package/dist/monitors/locationChangeMonitor.js +30 -0
- package/dist/monitors/pageMonitor.js +119 -0
- package/dist/monitors/requestMonitor.js +679 -0
- package/dist/pageVisit/pageVisit.js +172 -0
- package/dist/pageVisit/pageVisitEventError/pageVisitEventError.js +313 -0
- package/dist/pageVisit/pageVisitEventHTTP/pageVisitEventHTTP.js +115 -0
- package/dist/pageVisit/userStep/userStep.js +20 -0
- package/dist/react/ErrorBoundary.d.ts +72 -0
- package/dist/react/ErrorBoundary.js +102 -0
- package/dist/storage/localStorageProvider.js +23 -0
- package/dist/storage/rnStorageProvider.js +62 -0
- package/dist/storage/sessionStorageProvider.js +23 -0
- package/dist/storage/storage.js +119 -0
- package/dist/storage/storageProvider.js +83 -0
- package/dist/utils/date.js +62 -0
- package/dist/utils/eventlistener.js +67 -0
- package/dist/utils/function.js +398 -0
- package/dist/utils/object.js +144 -0
- package/dist/utils/performance.js +21 -0
- package/package.json +57 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import 'react-native/Libraries/Network/fetch';
|
|
2
|
+
import { saveErrorToPagevisit } from '../pageVisit/pageVisitEventError/pageVisitEventError.js';
|
|
3
|
+
import { PageVisitEventHTTP, isHttpCodeFailure } from '../pageVisit/pageVisitEventHTTP/pageVisitEventHTTP.js';
|
|
4
|
+
import { propWriteableOrMadeWriteable, replace } from '../utils/object.js';
|
|
5
|
+
import 'stacktrace-parser';
|
|
6
|
+
import 'react-native-device-info';
|
|
7
|
+
import { PV_SEQ_ATT_NAME, XML_HTTP_REQUEST_ERROR_TYPE, GQL_ERROR_TYPE, SEVERITY_ERROR, SEVERITY_WARN, RESPONSE_ERROR_TYPE, HTTP_METHOD_ATT_NAME, HTTP_RESP_CODE_ATT_NAME, URL_ATT_NAME, HTTP_RESP_TIME_ATT_NAME, HTTP_RESP_LENGTH_ATT_NAME, FETCH_EXCEPTION_ERROR_TYPE } from '../constants.js';
|
|
8
|
+
import { addSafeEventListener } from '../utils/eventlistener.js';
|
|
9
|
+
import { HTTPDataBundler } from './httpDataBundler.js';
|
|
10
|
+
import ClientConfig from '../api/clientConfig.js';
|
|
11
|
+
import GqlErrorValidator from './gqlErrorValidator.js';
|
|
12
|
+
|
|
13
|
+
/** @module RequestMonitor */
|
|
14
|
+
// todo no idea why this works but it fixes fetch replacement
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Takes an XHR object, method, url, response time, and payload, and returns the parameters to pass
|
|
18
|
+
* the PageVisitEventHTTP constructor.
|
|
19
|
+
* @param {object} xhrObj
|
|
20
|
+
* @param {string} method
|
|
21
|
+
* @param {string} url
|
|
22
|
+
* @param {number} respTime
|
|
23
|
+
* @param {object} gqlError
|
|
24
|
+
* @param {*} rawPayload
|
|
25
|
+
* @returns HTTP event and data
|
|
26
|
+
*/
|
|
27
|
+
function _buildHttpEventDataObjectsForXHR(
|
|
28
|
+
xhrObj,
|
|
29
|
+
method,
|
|
30
|
+
url,
|
|
31
|
+
respTime,
|
|
32
|
+
gqlError,
|
|
33
|
+
rawPayload = null,
|
|
34
|
+
) {
|
|
35
|
+
const httpEvent = {
|
|
36
|
+
[HTTP_METHOD_ATT_NAME]: method,
|
|
37
|
+
[HTTP_RESP_CODE_ATT_NAME]: xhrObj.status,
|
|
38
|
+
[URL_ATT_NAME]: url,
|
|
39
|
+
[HTTP_RESP_TIME_ATT_NAME]: respTime,
|
|
40
|
+
};
|
|
41
|
+
let httpData = null;
|
|
42
|
+
const isGqlError = gqlError != null;
|
|
43
|
+
const isError = isHttpCodeFailure(xhrObj.status) || isGqlError;
|
|
44
|
+
if (isError && HTTPDataBundler.getInstance().shouldContinueForURL(url)) {
|
|
45
|
+
const responseHeaders = HTTPDataBundler.headersMapFromString(
|
|
46
|
+
xhrObj.getAllResponseHeaders(),
|
|
47
|
+
);
|
|
48
|
+
// add response payload length, if any
|
|
49
|
+
const contentLength =
|
|
50
|
+
HTTPDataBundler.getInstance().contentLength(responseHeaders);
|
|
51
|
+
if (contentLength > 0) {
|
|
52
|
+
httpEvent[HTTP_RESP_LENGTH_ATT_NAME] = contentLength;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let responseBody = '';
|
|
56
|
+
if (HTTPDataBundler.getInstance().shouldCollectPayloadForURL(url)) {
|
|
57
|
+
// Set descriptive body if content is wrong type or too long.
|
|
58
|
+
const droppedResponseText =
|
|
59
|
+
HTTPDataBundler.getInstance().dropPayloadIfNecessaryFromHeaders(
|
|
60
|
+
responseHeaders,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
responseBody =
|
|
64
|
+
droppedResponseText ||
|
|
65
|
+
HTTPDataBundler.responseStringFromXHRResponseType(xhrObj);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
httpData = HTTPDataBundler.getInstance().bundleHTTPData(
|
|
69
|
+
url,
|
|
70
|
+
xhrObj.noibuRequestHeaders,
|
|
71
|
+
rawPayload,
|
|
72
|
+
responseHeaders,
|
|
73
|
+
responseBody,
|
|
74
|
+
method,
|
|
75
|
+
isError,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return [httpEvent, httpData];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Called in the fetch() wrapper function, this specifically gets called after the promises which
|
|
83
|
+
* return the request and response body complete.
|
|
84
|
+
* @param {object} request a Request object
|
|
85
|
+
* @param {object} response a Response object
|
|
86
|
+
* @param {object} options the options parameter from the original fetch() call
|
|
87
|
+
* @param {string[]} bodyStrings an array of string HTTP bodies from the wrapped fetch() function.
|
|
88
|
+
* [0] is the response body, [1] is an optional response body.
|
|
89
|
+
* @param {string} url the URL of the request
|
|
90
|
+
* @param {string} method the method used in the fetch call
|
|
91
|
+
* @param {boolean} isError is an error
|
|
92
|
+
* @returns the return from the bundleHTTPData() call
|
|
93
|
+
*/
|
|
94
|
+
function _handleFetchOnPromiseCompletion(
|
|
95
|
+
request,
|
|
96
|
+
response,
|
|
97
|
+
options,
|
|
98
|
+
bodyStrings,
|
|
99
|
+
url,
|
|
100
|
+
method,
|
|
101
|
+
isError,
|
|
102
|
+
) {
|
|
103
|
+
let reqBody; //
|
|
104
|
+
let reqHeaders = new Map();
|
|
105
|
+
let respHeaders = new Map();
|
|
106
|
+
if (bodyStrings[1]) {
|
|
107
|
+
// Get reqBody from request obj if it exists
|
|
108
|
+
// I don't like this notation, but the linter enforces it.
|
|
109
|
+
// This is the same as reqBody = values[1]
|
|
110
|
+
[, reqBody] = bodyStrings;
|
|
111
|
+
|
|
112
|
+
// Get reqHeaders from request obj
|
|
113
|
+
if (request.headers) {
|
|
114
|
+
reqHeaders = HTTPDataBundler.headersMapFromIterable(
|
|
115
|
+
request.headers.entries(),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Get reqBody from options if it hasn't been defined above.
|
|
120
|
+
// Double-equals null check on reqBody to get null or undefined but not an empty string
|
|
121
|
+
if (reqBody == null && !!options && !!options.body) reqBody = options.body;
|
|
122
|
+
|
|
123
|
+
// Get reqHeaders from options.headers
|
|
124
|
+
if (reqHeaders.size < 1 && !!options && !!options.headers) {
|
|
125
|
+
// Two cases here: headers will either be a simple object (key/value), or
|
|
126
|
+
// a Headers type object.
|
|
127
|
+
|
|
128
|
+
// Case 1: headers has a .entries() property, it is a Header object
|
|
129
|
+
if (options.headers instanceof Headers) {
|
|
130
|
+
reqHeaders = HTTPDataBundler.headersMapFromIterable(
|
|
131
|
+
options.headers.entries(),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
// Case 2: headers is a simple object, same logic as above but we call it as
|
|
135
|
+
// Object.entries()
|
|
136
|
+
else {
|
|
137
|
+
reqHeaders = HTTPDataBundler.headersMapFromIterable(
|
|
138
|
+
Object.entries(options.headers),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// response.headers will always be a Headers object.
|
|
144
|
+
if (!!response && !!response.headers) {
|
|
145
|
+
respHeaders = HTTPDataBundler.headersMapFromIterable(
|
|
146
|
+
response.headers.entries(),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return HTTPDataBundler.getInstance().bundleHTTPData(
|
|
151
|
+
url,
|
|
152
|
+
reqHeaders,
|
|
153
|
+
reqBody,
|
|
154
|
+
respHeaders,
|
|
155
|
+
bodyStrings[0],
|
|
156
|
+
method,
|
|
157
|
+
isError,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns the parameters to pass the PageVisitEventHTTP constructor. Specific to the fetch wrapper.
|
|
163
|
+
* @param {object} request a Request object
|
|
164
|
+
* @param {object} response a Response object cloned() from the original
|
|
165
|
+
* @param {string} method the method used in the fetch call
|
|
166
|
+
* @param {string} url the URL of the request
|
|
167
|
+
* @param {object} options the options parameter from the original fetch() call
|
|
168
|
+
* @param {number} respTime the time between the fetch() call and the the resolution of the
|
|
169
|
+
* original implementation's return
|
|
170
|
+
* @param {object} gqlError a GraphQL error object
|
|
171
|
+
* @param {Promise<string>} requestTextPromise
|
|
172
|
+
* @returns an array in the form [httpEvent, httpData]
|
|
173
|
+
*/
|
|
174
|
+
async function _buildHttpEventDataObjectsForFetch(
|
|
175
|
+
request,
|
|
176
|
+
response,
|
|
177
|
+
method,
|
|
178
|
+
url,
|
|
179
|
+
options,
|
|
180
|
+
respTime,
|
|
181
|
+
requestTextPromise,
|
|
182
|
+
gqlError,
|
|
183
|
+
) {
|
|
184
|
+
const httpEvent = {
|
|
185
|
+
[HTTP_METHOD_ATT_NAME]: method,
|
|
186
|
+
[HTTP_RESP_CODE_ATT_NAME]: response.status,
|
|
187
|
+
[URL_ATT_NAME]: url,
|
|
188
|
+
[HTTP_RESP_TIME_ATT_NAME]: respTime,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let httpData = null;
|
|
192
|
+
const isGqlError = gqlError != null;
|
|
193
|
+
const isError = isHttpCodeFailure(response.status) || isGqlError;
|
|
194
|
+
// Only get the bodies of the request and response on error and if the URL is one we care about.
|
|
195
|
+
if (isError && HTTPDataBundler.getInstance().shouldContinueForURL(url)) {
|
|
196
|
+
// add response payload length, if any
|
|
197
|
+
if (response && response.headers) {
|
|
198
|
+
const contentLength = HTTPDataBundler.getInstance().contentLength(
|
|
199
|
+
response.headers,
|
|
200
|
+
);
|
|
201
|
+
if (contentLength > 0) {
|
|
202
|
+
httpEvent[HTTP_RESP_LENGTH_ATT_NAME] = contentLength;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let responseText = '';
|
|
207
|
+
if (
|
|
208
|
+
HTTPDataBundler.getInstance().shouldCollectPayloadForURL(url) &&
|
|
209
|
+
!!response &&
|
|
210
|
+
response instanceof Response
|
|
211
|
+
) {
|
|
212
|
+
const droppedResponseText =
|
|
213
|
+
HTTPDataBundler.getInstance().dropPayloadIfNecessaryFromHeaders(
|
|
214
|
+
response.headers,
|
|
215
|
+
);
|
|
216
|
+
responseText = droppedResponseText || response.clone().text();
|
|
217
|
+
}
|
|
218
|
+
const responseTextPromise = Promise.resolve(responseText);
|
|
219
|
+
|
|
220
|
+
// Array to pass to Promise.all(). We conditionally include requestTextPromise as a
|
|
221
|
+
// second element if it was defined earlier.
|
|
222
|
+
const promises = [
|
|
223
|
+
responseTextPromise,
|
|
224
|
+
...(requestTextPromise ? [requestTextPromise] : []),
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
// Wait on the promises for the response and the request (if applicable) to resolve,
|
|
228
|
+
// then call our handler
|
|
229
|
+
await Promise.all(promises)
|
|
230
|
+
.then(data =>
|
|
231
|
+
_handleFetchOnPromiseCompletion(
|
|
232
|
+
request,
|
|
233
|
+
response,
|
|
234
|
+
options,
|
|
235
|
+
data,
|
|
236
|
+
url,
|
|
237
|
+
method,
|
|
238
|
+
isError,
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
.then(httpDataReturn => {
|
|
242
|
+
httpData = httpDataReturn;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// This return will await the _handleFetchOnPromiseCompletion() call if necessary,
|
|
246
|
+
// but won't if not. (See if above).
|
|
247
|
+
return [httpEvent, httpData];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Wraps the send prototype of the xmlhttp object
|
|
252
|
+
* @param {object} proto window.XMLHTTPRequest's prototype
|
|
253
|
+
*/
|
|
254
|
+
function wrapXMLHTTPSend(proto) {
|
|
255
|
+
replace(
|
|
256
|
+
proto,
|
|
257
|
+
'send',
|
|
258
|
+
originalFunction =>
|
|
259
|
+
// We set nbuWrapper to the handler function so if this returns an error
|
|
260
|
+
// the trace message will start with nbuWrapper which would allow us to
|
|
261
|
+
// detect that this is a wrapped function and not a NoibuJS error
|
|
262
|
+
function nbuXMLHTTPSendWrapper(data) {
|
|
263
|
+
// We wrap all of our logic in a try block to guarantee that we will complete the original
|
|
264
|
+
// implementation if we throw an error in our logic.
|
|
265
|
+
try {
|
|
266
|
+
let method;
|
|
267
|
+
if (this.noibuHttpMethod) {
|
|
268
|
+
method = this.noibuHttpMethod;
|
|
269
|
+
} else {
|
|
270
|
+
// This is very hacky but we do not really have an option as
|
|
271
|
+
// we do not have access to either the url nor the method when
|
|
272
|
+
// overiding the send method
|
|
273
|
+
method = data ? 'POST' : 'GET';
|
|
274
|
+
}
|
|
275
|
+
// not using safePerformanceNow as there is a 70% hit with using it.
|
|
276
|
+
// We don't want every request to get impacted by this.
|
|
277
|
+
const rCreatedAt = new Date();
|
|
278
|
+
// on loadend we catch the event and
|
|
279
|
+
// make sure it was correct
|
|
280
|
+
addSafeEventListener(this, 'loadend', async () => {
|
|
281
|
+
const rEndedAt = new Date();
|
|
282
|
+
const respTime = Math.abs(rEndedAt - rCreatedAt);
|
|
283
|
+
// Prefer the URL from the open() call if it is defined.
|
|
284
|
+
const url = this.noibuHttpUrl || this.responseURL;
|
|
285
|
+
const gqlError = await GqlErrorValidator.fromXhr(url, this);
|
|
286
|
+
|
|
287
|
+
const [httpEvent, httpData] = _buildHttpEventDataObjectsForXHR(
|
|
288
|
+
this,
|
|
289
|
+
method,
|
|
290
|
+
url,
|
|
291
|
+
respTime,
|
|
292
|
+
gqlError,
|
|
293
|
+
data,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Save HTTP Info
|
|
297
|
+
let pageVisitEventHTTP = new PageVisitEventHTTP(
|
|
298
|
+
httpEvent,
|
|
299
|
+
httpData,
|
|
300
|
+
);
|
|
301
|
+
pageVisitEventHTTP.saveHTTPEvent();
|
|
302
|
+
|
|
303
|
+
const seq =
|
|
304
|
+
pageVisitEventHTTP.httpData &&
|
|
305
|
+
pageVisitEventHTTP.httpData[PV_SEQ_ATT_NAME];
|
|
306
|
+
if (isHttpCodeFailure(this.status)) {
|
|
307
|
+
saveErrorToPagevisit(XML_HTTP_REQUEST_ERROR_TYPE, this, seq);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (gqlError) {
|
|
311
|
+
gqlError.forEach(error =>
|
|
312
|
+
saveErrorToPagevisit(GQL_ERROR_TYPE, error, seq),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// helping gc
|
|
317
|
+
pageVisitEventHTTP = null;
|
|
318
|
+
}); // End of loadend callback, return to synchronous code below
|
|
319
|
+
} catch (e) {
|
|
320
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
321
|
+
`Error in XHR.send() wrapper: ${e}`,
|
|
322
|
+
false,
|
|
323
|
+
SEVERITY_ERROR,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return originalFunction.call(this, data);
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Wraps the open prototype of the xmlhttp object
|
|
333
|
+
* @param {object} proto window.XMLHTTPRequest's prototype
|
|
334
|
+
* @param {boolean} shouldHandleLoadend Whether the wrapped XHR.open() function should be
|
|
335
|
+
* responsible for listening to 'loadend' events. (Used in the case where the XHR.send()
|
|
336
|
+
* function on the XHR prototype cannot be modified.)
|
|
337
|
+
*/
|
|
338
|
+
function wrapXMLHTTPOpen(proto, shouldHandleLoadend) {
|
|
339
|
+
replace(
|
|
340
|
+
proto,
|
|
341
|
+
'open',
|
|
342
|
+
originalFunction =>
|
|
343
|
+
// We set nbuWrapper to the handler function so if this returns an error
|
|
344
|
+
// the trace message will start with nbuWrapper which would allow us to
|
|
345
|
+
// detect that this is a wrapped function and not a NoibuJS error
|
|
346
|
+
function nbuXMLHTTPOpenWrapper(
|
|
347
|
+
method,
|
|
348
|
+
url,
|
|
349
|
+
async = true,
|
|
350
|
+
user = null,
|
|
351
|
+
password = null,
|
|
352
|
+
) {
|
|
353
|
+
// We wrap all of our logic in a try block to guarantee that we will complete the original
|
|
354
|
+
// implementation if we throw an error in our logic.
|
|
355
|
+
try {
|
|
356
|
+
// We wrap assignment of the .noibu[...] variables in a try/catch, as we're assigning
|
|
357
|
+
// properties to a built-in object type, and have no guarantee of success.
|
|
358
|
+
try {
|
|
359
|
+
this.noibuHttpMethod = method;
|
|
360
|
+
this.noibuHttpUrl = url;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
363
|
+
`Unable to set custom properties on XHR object: ${error}`,
|
|
364
|
+
false,
|
|
365
|
+
SEVERITY_WARN,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (shouldHandleLoadend) {
|
|
370
|
+
// on loadend we catch the event and
|
|
371
|
+
// make sure it was correct
|
|
372
|
+
const rCreatedAt = new Date();
|
|
373
|
+
addSafeEventListener(this, 'loadend', async () => {
|
|
374
|
+
const rEndedAt = new Date();
|
|
375
|
+
const respTime = Math.abs(rEndedAt - rCreatedAt);
|
|
376
|
+
const gqlError = await GqlErrorValidator.fromXhr(url, this);
|
|
377
|
+
|
|
378
|
+
const [httpEvent, httpData] = _buildHttpEventDataObjectsForXHR(
|
|
379
|
+
this,
|
|
380
|
+
method,
|
|
381
|
+
url,
|
|
382
|
+
respTime,
|
|
383
|
+
gqlError,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
let pageVisitEventHTTP = new PageVisitEventHTTP(
|
|
387
|
+
httpEvent,
|
|
388
|
+
httpData,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Save HTTP Info
|
|
392
|
+
pageVisitEventHTTP.saveHTTPEvent();
|
|
393
|
+
const seq =
|
|
394
|
+
pageVisitEventHTTP.httpData &&
|
|
395
|
+
pageVisitEventHTTP.httpData[PV_SEQ_ATT_NAME];
|
|
396
|
+
if (isHttpCodeFailure(this.status)) {
|
|
397
|
+
saveErrorToPagevisit(XML_HTTP_REQUEST_ERROR_TYPE, this, seq);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (gqlError) {
|
|
401
|
+
gqlError.forEach(error =>
|
|
402
|
+
saveErrorToPagevisit(GQL_ERROR_TYPE, error, seq),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// helping gc
|
|
407
|
+
pageVisitEventHTTP = null;
|
|
408
|
+
}); // End of loadend callback, return to synchronous code below
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
412
|
+
`Error in XHR.open() wrapper: ${e}`,
|
|
413
|
+
false,
|
|
414
|
+
SEVERITY_ERROR,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return originalFunction.call(this, method, url, async, user, password);
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Replaces the native XMLHTTPRequest.setHeader() function. We use this to build an array of
|
|
424
|
+
* request headers as these are not stored by the XMLHTTPRequest object.
|
|
425
|
+
* @param {object} proto window.XMLHTTPRequest's prototype
|
|
426
|
+
*/
|
|
427
|
+
function wrapXMLHTTPSetRequestHeader(proto) {
|
|
428
|
+
replace(
|
|
429
|
+
proto,
|
|
430
|
+
'setRequestHeader',
|
|
431
|
+
originalFunction =>
|
|
432
|
+
function nbuXMLHTTPSetRequestHeaderWrapper(header, value) {
|
|
433
|
+
// We wrap all of our logic in a try block to guarantee that we will complete the original
|
|
434
|
+
// implementation if we throw an error in our logic.
|
|
435
|
+
try {
|
|
436
|
+
if (
|
|
437
|
+
!this.noibuRequestHeaders ||
|
|
438
|
+
!(this.noibuRequestHeaders instanceof Map)
|
|
439
|
+
) {
|
|
440
|
+
this.noibuRequestHeaders = new Map();
|
|
441
|
+
}
|
|
442
|
+
// Ensure it's a string. This also converts null to "null"
|
|
443
|
+
const stringValue = typeof value === 'string' ? value : String(value);
|
|
444
|
+
this.noibuRequestHeaders.set(header.toLowerCase(), stringValue);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
447
|
+
`Error in XHR.setRequestHeader() wrapper: ${error}`,
|
|
448
|
+
false,
|
|
449
|
+
SEVERITY_ERROR,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return originalFunction.call(this, header, value);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Handles fetch failure
|
|
459
|
+
* @param {} err
|
|
460
|
+
* @param {} url
|
|
461
|
+
*/
|
|
462
|
+
function handleFetchFailure(err, url) {
|
|
463
|
+
if (!err) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// we do not need this error if we cannot extract both the message and stack
|
|
468
|
+
if (!err.message || !err.stack) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// making sure we have a url
|
|
473
|
+
const checkedURL = !url ? '' : url;
|
|
474
|
+
const errorFromFetch = new Error();
|
|
475
|
+
errorFromFetch.stack = err.stack;
|
|
476
|
+
const urlMessage = checkedURL.trim() === '' ? '' : ` on url ${checkedURL}`;
|
|
477
|
+
errorFromFetch.message = `${err.message}${urlMessage}`;
|
|
478
|
+
const errorPayload = {
|
|
479
|
+
error: errorFromFetch,
|
|
480
|
+
};
|
|
481
|
+
saveErrorToPagevisit(FETCH_EXCEPTION_ERROR_TYPE, errorPayload);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Setting up the wrapper around fetch functions to try and
|
|
486
|
+
* catch http errors
|
|
487
|
+
*/
|
|
488
|
+
function setupGlobalFetchWrapper() {
|
|
489
|
+
const proto = global;
|
|
490
|
+
|
|
491
|
+
// Ensure we're able to replace the fetch() function
|
|
492
|
+
if (!propWriteableOrMadeWriteable(proto, 'fetch')) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
replace(
|
|
497
|
+
proto,
|
|
498
|
+
'fetch',
|
|
499
|
+
originalFunction =>
|
|
500
|
+
// We set nbuWrapper to the handler function so if this returns an error
|
|
501
|
+
// the trace message will start with nbuWrapper which would allow us to
|
|
502
|
+
// detect that this is a wrapped function and not a NoibuJS error
|
|
503
|
+
function nbuGlobalFetchWrapper(resource, options) {
|
|
504
|
+
let method;
|
|
505
|
+
let url;
|
|
506
|
+
|
|
507
|
+
// These two variables will only be defined if the resource param is a request object.
|
|
508
|
+
// request, if defined, will be a clone of the request object
|
|
509
|
+
let request;
|
|
510
|
+
// requestTextPromise, if defined, will be a promise obtained from request that resolves
|
|
511
|
+
// to the response body as text
|
|
512
|
+
let requestTextPromise;
|
|
513
|
+
|
|
514
|
+
// We wrap everything in try/catch to ensure the original function is called even if we throw
|
|
515
|
+
// an unexpected error.
|
|
516
|
+
try {
|
|
517
|
+
// There is no valid reason for fetch() to be called without a resource but we
|
|
518
|
+
// see it on a bunch of websites. It seems to work the same as empty string
|
|
519
|
+
if (!resource) {
|
|
520
|
+
method = 'GET';
|
|
521
|
+
url = '';
|
|
522
|
+
} else {
|
|
523
|
+
if (resource.method) {
|
|
524
|
+
method = resource.method;
|
|
525
|
+
url = resource.url;
|
|
526
|
+
} else {
|
|
527
|
+
// If options or options.method is missing, fallback to GET.
|
|
528
|
+
method = options && options.method ? options.method : 'GET';
|
|
529
|
+
url = resource.toString() ? resource.toString() : '';
|
|
530
|
+
}
|
|
531
|
+
// resource is defined
|
|
532
|
+
// Two possibilities: resource could be either any object with a .toString() function,
|
|
533
|
+
// or it could be a Request object. If it's a Request, we'll have to clone it, and read
|
|
534
|
+
// its body asynchronously. We check for the existence of a .method property on the
|
|
535
|
+
// resource to check whether it is a Request object.
|
|
536
|
+
if (
|
|
537
|
+
HTTPDataBundler.getInstance().shouldCollectPayloadForURL(url) &&
|
|
538
|
+
resource.clone &&
|
|
539
|
+
resource.text
|
|
540
|
+
) {
|
|
541
|
+
// We clone the request object, as its body can only be read once per instance.
|
|
542
|
+
// It's important that we do this before the original function is called, as it
|
|
543
|
+
// cannot be cloned afterwards.
|
|
544
|
+
request = resource.clone();
|
|
545
|
+
requestTextPromise = request.text();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} catch (e) {
|
|
549
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
550
|
+
`Error in fetch() wrapper: ${e}`,
|
|
551
|
+
false,
|
|
552
|
+
SEVERITY_ERROR,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Break out of the try wrapper momentarily to make the original call
|
|
557
|
+
|
|
558
|
+
// Can't put this inside finally as const needs to be defined outside it.
|
|
559
|
+
const promiseFromOriginalFetch = originalFunction.call(
|
|
560
|
+
this,
|
|
561
|
+
resource,
|
|
562
|
+
options,
|
|
563
|
+
);
|
|
564
|
+
// not using safePerformanceNow as there is a 75% hit with using it.
|
|
565
|
+
// We don't want every request to get impacted by this.
|
|
566
|
+
const rCreatedAt = new Date();
|
|
567
|
+
promiseFromOriginalFetch
|
|
568
|
+
.then(async response => {
|
|
569
|
+
// We wrap the callback in try in order to only catch real fetch failures in the promise's
|
|
570
|
+
// .catch() call
|
|
571
|
+
try {
|
|
572
|
+
const graphqlResponse = response.clone();
|
|
573
|
+
const httpEventDataResponse = response.clone();
|
|
574
|
+
const pageVisitErrorResponse = response.clone();
|
|
575
|
+
|
|
576
|
+
const gqlError = await GqlErrorValidator.fromFetch(
|
|
577
|
+
url,
|
|
578
|
+
options,
|
|
579
|
+
request,
|
|
580
|
+
graphqlResponse,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const rEndedAt = new Date();
|
|
584
|
+
const respTime = Math.abs(rEndedAt - rCreatedAt);
|
|
585
|
+
const [httpEvent, httpData] =
|
|
586
|
+
await _buildHttpEventDataObjectsForFetch(
|
|
587
|
+
request,
|
|
588
|
+
httpEventDataResponse,
|
|
589
|
+
method,
|
|
590
|
+
url,
|
|
591
|
+
options,
|
|
592
|
+
respTime,
|
|
593
|
+
requestTextPromise,
|
|
594
|
+
gqlError,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
let pageVisitEventHTTP = new PageVisitEventHTTP(
|
|
598
|
+
httpEvent,
|
|
599
|
+
httpData,
|
|
600
|
+
);
|
|
601
|
+
pageVisitEventHTTP.saveHTTPEvent();
|
|
602
|
+
|
|
603
|
+
const seq =
|
|
604
|
+
pageVisitEventHTTP.httpData &&
|
|
605
|
+
pageVisitEventHTTP.httpData[PV_SEQ_ATT_NAME];
|
|
606
|
+
|
|
607
|
+
if (isHttpCodeFailure(response.status)) {
|
|
608
|
+
saveErrorToPagevisit(
|
|
609
|
+
RESPONSE_ERROR_TYPE,
|
|
610
|
+
pageVisitErrorResponse,
|
|
611
|
+
seq,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (gqlError) {
|
|
616
|
+
gqlError.forEach(error =>
|
|
617
|
+
saveErrorToPagevisit(GQL_ERROR_TYPE, error, seq),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// helping gc
|
|
622
|
+
pageVisitEventHTTP = null;
|
|
623
|
+
} catch (e) {
|
|
624
|
+
// Catch errors with our logic in the .then() callback above
|
|
625
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
626
|
+
`Error in custom fetch() callback: ${e}`,
|
|
627
|
+
false,
|
|
628
|
+
SEVERITY_ERROR,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
})
|
|
632
|
+
.catch(err => {
|
|
633
|
+
// Catch errors in the call to the original fetch() implementation
|
|
634
|
+
handleFetchFailure(err, url);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Can't put this inside finally as linter does not allow return statements inside finally.
|
|
638
|
+
return promiseFromOriginalFetch;
|
|
639
|
+
},
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Setting up the wrapper around send functions to try and
|
|
645
|
+
* catch http errors
|
|
646
|
+
*/
|
|
647
|
+
function setupGlobalXMLHttpWrapper() {
|
|
648
|
+
const XMLHttp = window.XMLHttpRequest;
|
|
649
|
+
// The event target needs to have a prototype and have the open function
|
|
650
|
+
const proto = XMLHttp && XMLHttp.prototype;
|
|
651
|
+
|
|
652
|
+
const canWriteOpen = propWriteableOrMadeWriteable(proto, 'open');
|
|
653
|
+
const canWriteSend = propWriteableOrMadeWriteable(proto, 'send');
|
|
654
|
+
const canWriteSetHeader = propWriteableOrMadeWriteable(
|
|
655
|
+
proto,
|
|
656
|
+
'setRequestHeader',
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
if (canWriteOpen) {
|
|
660
|
+
// If we can't wrap the XHR.send() function, we need the wrapped .open() function to
|
|
661
|
+
// set the listener on 'loadend' events.
|
|
662
|
+
wrapXMLHTTPOpen(proto, !canWriteSend);
|
|
663
|
+
}
|
|
664
|
+
if (canWriteSend) {
|
|
665
|
+
wrapXMLHTTPSend(proto);
|
|
666
|
+
}
|
|
667
|
+
if (canWriteSetHeader) {
|
|
668
|
+
wrapXMLHTTPSetRequestHeader(proto);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Monitors all requests
|
|
673
|
+
*/
|
|
674
|
+
function monitorRequests() {
|
|
675
|
+
setupGlobalXMLHttpWrapper();
|
|
676
|
+
setupGlobalFetchWrapper();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export { handleFetchFailure, monitorRequests };
|