noibu-react-native 0.2.3 → 0.2.4

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.
Files changed (66) hide show
  1. package/dist/api/helpCode.js +61 -87
  2. package/dist/api/metroplexSocket.js +72 -65
  3. package/dist/api/storedPageVisit.js +150 -208
  4. package/dist/constants.js +3 -7
  5. package/dist/entry/init.js +11 -11
  6. package/dist/monitors/{appNavigationMonitor.js → AppNavigationMonitor.js} +10 -19
  7. package/dist/monitors/ClickMonitor.js +198 -0
  8. package/dist/monitors/ErrorMonitor.js +206 -0
  9. package/dist/monitors/KeyboardInputMonitor.js +60 -0
  10. package/dist/monitors/PageMonitor.js +98 -0
  11. package/dist/monitors/RequestMonitor.js +390 -0
  12. package/dist/monitors/http-tools/GqlErrorValidator.js +259 -0
  13. package/dist/monitors/{httpDataBundler.js → http-tools/HTTPDataBundler.js} +23 -102
  14. package/dist/pageVisit/{eventDebouncer.js → EventDebouncer.js} +36 -47
  15. package/dist/pageVisit/pageVisitEventError.js +3 -3
  16. package/dist/pageVisit/pageVisitEventHTTP.js +5 -4
  17. package/dist/sessionRecorder/sessionRecorder.js +1 -1
  18. package/dist/src/api/clientConfig.d.ts +1 -1
  19. package/dist/src/api/helpCode.d.ts +10 -16
  20. package/dist/src/api/metroplexSocket.d.ts +52 -71
  21. package/dist/src/api/storedPageVisit.d.ts +12 -21
  22. package/dist/src/constants.d.ts +1 -0
  23. package/dist/src/monitors/AppNavigationMonitor.d.ts +18 -0
  24. package/dist/src/monitors/ClickMonitor.d.ts +31 -0
  25. package/dist/src/monitors/ErrorMonitor.d.ts +63 -0
  26. package/dist/src/monitors/{keyboardInputMonitor.d.ts → KeyboardInputMonitor.d.ts} +7 -4
  27. package/dist/src/monitors/{pageMonitor.d.ts → PageMonitor.d.ts} +6 -8
  28. package/dist/src/monitors/RequestMonitor.d.ts +94 -0
  29. package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +59 -0
  30. package/dist/src/monitors/{httpDataBundler.d.ts → http-tools/HTTPDataBundler.d.ts} +13 -28
  31. package/dist/src/monitors/integrations/react-native-navigation-integration.d.ts +3 -2
  32. package/dist/src/pageVisit/{eventDebouncer.d.ts → EventDebouncer.d.ts} +3 -10
  33. package/dist/src/pageVisit/pageVisit.d.ts +1 -1
  34. package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +3 -3
  35. package/dist/src/storage/rnStorageProvider.d.ts +1 -1
  36. package/dist/src/storage/storage.d.ts +1 -1
  37. package/dist/src/storage/storageProvider.d.ts +2 -2
  38. package/dist/src/utils/function.d.ts +4 -5
  39. package/dist/src/utils/object.d.ts +3 -5
  40. package/dist/src/utils/polyfills.d.ts +1 -4
  41. package/dist/types/Metroplex.types.d.ts +73 -0
  42. package/dist/types/Monitor.d.ts +11 -0
  43. package/dist/types/Monitor.js +19 -0
  44. package/dist/types/PageVisit.types.d.ts +2 -145
  45. package/dist/types/PageVisitErrors.types.d.ts +114 -0
  46. package/dist/types/PageVisitEvents.types.d.ts +91 -0
  47. package/dist/types/Storage.d.ts +1 -1
  48. package/dist/types/StoredPageVisit.types.d.ts +4 -45
  49. package/dist/utils/function.js +0 -1
  50. package/dist/utils/object.js +1 -0
  51. package/package.json +4 -3
  52. package/dist/monitors/clickMonitor.js +0 -258
  53. package/dist/monitors/errorMonitor.js +0 -202
  54. package/dist/monitors/gqlErrorValidator.js +0 -306
  55. package/dist/monitors/inputMonitor.js +0 -138
  56. package/dist/monitors/keyboardInputMonitor.js +0 -66
  57. package/dist/monitors/pageMonitor.js +0 -122
  58. package/dist/monitors/requestMonitor.js +0 -386
  59. package/dist/src/monitors/appNavigationMonitor.d.ts +0 -22
  60. package/dist/src/monitors/clickMonitor.d.ts +0 -44
  61. package/dist/src/monitors/errorMonitor.d.ts +0 -28
  62. package/dist/src/monitors/gqlErrorValidator.d.ts +0 -82
  63. package/dist/src/monitors/inputMonitor.d.ts +0 -34
  64. package/dist/src/monitors/requestMonitor.d.ts +0 -10
  65. package/dist/types/RRWeb.d.ts +0 -48
  66. package/dist/types/ReactNative.d.ts +0 -4
@@ -0,0 +1,390 @@
1
+ import { __awaiter } from 'tslib';
2
+ import 'react-native/Libraries/Network/fetch';
3
+ import { saveErrorToPagevisit } from '../pageVisit/pageVisitEventError.js';
4
+ import { isHttpCodeFailure, PageVisitEventHTTP } from '../pageVisit/pageVisitEventHTTP.js';
5
+ import { safeEntries, propWriteableOrMadeWriteable, replace } from '../utils/object.js';
6
+ import { BODY_USED_ERROR, HTTP_RESP_LENGTH_ATT_NAME, SEVERITY, PV_SEQ_ATT_NAME, RESPONSE_ERROR_TYPE, GQL_ERROR_TYPE, XML_HTTP_REQUEST_ERROR_TYPE, FETCH_EXCEPTION_ERROR_TYPE, HTTP_METHOD_ATT_NAME, HTTP_RESP_CODE_ATT_NAME, URL_ATT_NAME, HTTP_RESP_TIME_ATT_NAME } from '../constants.js';
7
+ import ClientConfig from '../api/clientConfig.js';
8
+ import { noibuLog } from '../utils/log.js';
9
+ import { tryGetStackTrace, safeTrim } from '../utils/function.js';
10
+ import { promiseAll } from '../utils/polyfills.js';
11
+ import { addSafeEventListener } from '../utils/eventlistener.js';
12
+ import { HTTPDataBundler } from './http-tools/HTTPDataBundler.js';
13
+ import GqlErrorValidator from './http-tools/GqlErrorValidator.js';
14
+ import { Singleton } from '../types/Monitor.js';
15
+
16
+ /**
17
+ * Monitors all requests
18
+ */
19
+ class RequestMonitor extends Singleton {
20
+ /** main method */
21
+ monitor() {
22
+ noibuLog('monitorRequests started');
23
+ RequestMonitor.setupGlobalFetchWrapper(global);
24
+ RequestMonitor.setupGlobalXMLHttpWrapper();
25
+ noibuLog('monitorRequests ended');
26
+ }
27
+ /**
28
+ * Gets a response text Promise out of Response & headers. Returns reason if bundler decides to drop text.
29
+ * Due to async nature of text() and async function sent to Promise.all,
30
+ * our code race competes with client's code consuming the response.
31
+ * So at this point engine somehow gets access to the original ReadableStream and marks it as locked,
32
+ * thus client code may throw an error.
33
+ */
34
+ static consumeResponseAndGetBodyText(response) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ if (!(response instanceof Response)) {
37
+ return '';
38
+ }
39
+ if (response.bodyUsed) {
40
+ return BODY_USED_ERROR;
41
+ }
42
+ return response.text();
43
+ });
44
+ }
45
+ /**
46
+ * Generates new PVEventHTTP
47
+ */
48
+ static getPvEventHttp(status, responseHeaders, method, url, responseTime) {
49
+ const httpEvent = {
50
+ [HTTP_METHOD_ATT_NAME]: method,
51
+ [HTTP_RESP_CODE_ATT_NAME]: status,
52
+ [URL_ATT_NAME]: url,
53
+ [HTTP_RESP_TIME_ATT_NAME]: responseTime,
54
+ };
55
+ const bundler = HTTPDataBundler.getInstance();
56
+ // add response payload length, if any
57
+ const contentLength = bundler.contentLength(responseHeaders);
58
+ if (contentLength > 0 && bundler.shouldContinueForURL(url)) {
59
+ httpEvent[HTTP_RESP_LENGTH_ATT_NAME] = contentLength;
60
+ }
61
+ return httpEvent;
62
+ }
63
+ /**
64
+ * Gets headers from request or request options. Handles different scenarios where simple conversion is not possible
65
+ */
66
+ static getRequestHeaders(request, options) {
67
+ const headers = safeEntries((request === null || request === void 0 ? void 0 : request.headers) || (options === null || options === void 0 ? void 0 : options.headers));
68
+ return HTTPDataBundler.headersMapFromIterable(headers);
69
+ }
70
+ /**
71
+ * Gets response headers.
72
+ * It is always a Headers object.
73
+ */
74
+ static getResponseHeaders(response) {
75
+ const headers = safeEntries(response === null || response === void 0 ? void 0 : response.headers);
76
+ return HTTPDataBundler.headersMapFromIterable(headers);
77
+ }
78
+ /**
79
+ * Gets request body from body strings or Request options
80
+ */
81
+ static getRequestBody(requestText, options) {
82
+ if (typeof requestText === 'string') {
83
+ return requestText;
84
+ }
85
+ // it's fine if it's falsy or an empty string, we catch that in HTTPDataBundler.getInstance().bundleHTTPData
86
+ return options === null || options === void 0 ? void 0 : options.body;
87
+ }
88
+ /**
89
+ * Returns the parameters to pass the PageVisitEventHTTP constructor. Specific to the fetch wrapper.
90
+ */
91
+ static getHttpDataFromFetch(request, response, method, url, options, isError, requestText, responseText) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ if (!HTTPDataBundler.getInstance().shouldContinueForURL(url)) {
94
+ return null;
95
+ }
96
+ return HTTPDataBundler.getInstance().bundleHTTPData(url, RequestMonitor.getRequestHeaders(request, options), RequestMonitor.getRequestBody(requestText, options), RequestMonitor.getResponseHeaders(response), responseText, method, isError);
97
+ });
98
+ }
99
+ /** Clones Response / Request or returns original object if cloning is not possible */
100
+ static cloneResponse(ogResponse) {
101
+ if (!ogResponse.bodyUsed) {
102
+ return ogResponse.clone();
103
+ }
104
+ return ogResponse; // body consumed already, no http response body for us
105
+ }
106
+ /**
107
+ * Handles fetch response safely. Reports PageVisitEventHTTP and error to Pagevisit if needed.
108
+ */
109
+ static safeHandleFetchResponse(startTime, url, method, options, request) {
110
+ return (ogResponse) => __awaiter(this, void 0, void 0, function* () {
111
+ var _a, _b;
112
+ try {
113
+ if (!ogResponse) {
114
+ // no idea how, but this happens sometimes, esp if other libs override fetch
115
+ // logging to track the issue if it becomes widespread
116
+ return ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error in custom fetch() callback: no response received`, false, SEVERITY.error);
117
+ }
118
+ const clonedResponse = RequestMonitor.cloneResponse(ogResponse); // have to do it as early as possible
119
+ const graphqlResponse = RequestMonitor.cloneResponse(clonedResponse); // and two times more for each consumption
120
+ const httpEventDataResponse = RequestMonitor.cloneResponse(clonedResponse);
121
+ const respTime = Date.now() - startTime;
122
+ const status = clonedResponse.status;
123
+ const headers = clonedResponse.headers;
124
+ const gqlError = yield GqlErrorValidator.fromFetch(url, options, request, graphqlResponse);
125
+ const isHttpError = isHttpCodeFailure(status);
126
+ const [maybeRequestText, responseText] = yield promiseAll([
127
+ Promise.resolve((_a = request === null || request === void 0 ? void 0 : request.text) === null || _a === void 0 ? void 0 : _a.call(request)),
128
+ RequestMonitor.consumeResponseAndGetBodyText(httpEventDataResponse),
129
+ ]);
130
+ const httpEvent = RequestMonitor.getPvEventHttp(status, headers, method, url, respTime);
131
+ const httpData = yield RequestMonitor.getHttpDataFromFetch(request, httpEventDataResponse, method, url, options, isHttpError || !!gqlError, maybeRequestText, responseText);
132
+ const pageVisitEventHTTP = new PageVisitEventHTTP(httpEvent, httpData, !!gqlError);
133
+ pageVisitEventHTTP.saveHTTPEvent();
134
+ const seq = (_b = pageVisitEventHTTP.httpData) === null || _b === void 0 ? void 0 : _b[PV_SEQ_ATT_NAME];
135
+ if (isHttpError) {
136
+ // this does not consume response body so no need to clone it
137
+ saveErrorToPagevisit(RESPONSE_ERROR_TYPE, clonedResponse, seq);
138
+ }
139
+ if (gqlError) {
140
+ gqlError.forEach(error => saveErrorToPagevisit(GQL_ERROR_TYPE, error, seq));
141
+ }
142
+ }
143
+ catch (e) {
144
+ if ((e === null || e === void 0 ? void 0 : e.name) === 'AbortError') {
145
+ // skip it, as it's somewhat expected
146
+ return;
147
+ }
148
+ const stack = tryGetStackTrace(e);
149
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error in custom fetch() callback: ${e}${stack}`, false, SEVERITY.error);
150
+ }
151
+ });
152
+ }
153
+ /**
154
+ * Safe function to get method and url from fetch args
155
+ */
156
+ static getMethodUrlFromFetchArgs(resource, options) {
157
+ const defaultResource = { method: 'GET', url: '' };
158
+ try {
159
+ // There is no valid reason for fetch() to be called without a resource, but we
160
+ // see it on a bunch of websites. It seems to work the same as empty string
161
+ if (!resource) {
162
+ return defaultResource;
163
+ }
164
+ if (resource instanceof Request && resource.method) {
165
+ return { method: resource.method, url: resource.url || '' };
166
+ }
167
+ return {
168
+ method: (options === null || options === void 0 ? void 0 : options.method) || 'GET',
169
+ url: typeof resource.toString === 'function' ? resource.toString() : '',
170
+ };
171
+ }
172
+ catch (e) {
173
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error in fetch() wrapper: ${e}`, false, SEVERITY.error);
174
+ }
175
+ return defaultResource;
176
+ }
177
+ /**
178
+ * Setting up the wrapper around fetch functions to try and
179
+ * catch http errors
180
+ */
181
+ static setupGlobalFetchWrapper(proto) {
182
+ if (!propWriteableOrMadeWriteable(proto, 'fetch')) {
183
+ return;
184
+ }
185
+ replace(proto, 'fetch', function (originalFunction) {
186
+ // please don't change the name of this function. we need this stack frame to be nbuWrapper
187
+ return function nbuWrapper(resource, options) {
188
+ let request;
189
+ const { url, method } = RequestMonitor.getMethodUrlFromFetchArgs(resource, options);
190
+ if (resource instanceof Request) {
191
+ /**
192
+ * We clone the request object, as its body can only be read once per instance.
193
+ * It's important that we do this before the original function is called, as it
194
+ * cannot be cloned afterward.
195
+ */
196
+ request = resource.clone();
197
+ }
198
+ const startTime = Date.now();
199
+ const promiseFromOriginalFetch = originalFunction.call(this, resource, options);
200
+ promiseFromOriginalFetch
201
+ .then(RequestMonitor.safeHandleFetchResponse(startTime, url, method, options, request))
202
+ .catch(err => {
203
+ RequestMonitor.handleFetchFailure(err, url);
204
+ });
205
+ return promiseFromOriginalFetch;
206
+ };
207
+ });
208
+ }
209
+ /**
210
+ * Does a check if data collection is enabled and combines it into HttpDataBundle
211
+ */
212
+ static getHttpDataFromXhr(xhrObj_1, method_1, url_1, isError_1) {
213
+ return __awaiter(this, arguments, void 0, function* (xhrObj, method, url, isError, requestPayload = null, responseText) {
214
+ if (!HTTPDataBundler.getInstance().shouldContinueForURL(url)) {
215
+ return null;
216
+ }
217
+ const responseHeaders = HTTPDataBundler.headersMapFromString(xhrObj.getAllResponseHeaders());
218
+ return HTTPDataBundler.getInstance().bundleHTTPData(url, xhrObj.noibuRequestHeaders, requestPayload, responseHeaders, responseText, method, isError);
219
+ });
220
+ }
221
+ /**
222
+ * on loadend we catch the event and
223
+ * make sure it was correct
224
+ */
225
+ static loadendHandler(xhr, startTime, url, method, requestPayload) {
226
+ return __awaiter(this, void 0, void 0, function* () {
227
+ var _a;
228
+ const respTime = Date.now() - startTime;
229
+ const gqlError = yield GqlErrorValidator.fromXhr(url, xhr);
230
+ const httpError = isHttpCodeFailure(xhr.status);
231
+ const responseText = yield HTTPDataBundler.getResponseStringFromXHR(xhr);
232
+ const httpEvent = RequestMonitor.getPvEventHttp(xhr.status, HTTPDataBundler.headersMapFromString(xhr.getAllResponseHeaders()), method, url, respTime);
233
+ const httpData = yield RequestMonitor.getHttpDataFromXhr(xhr, method, url, httpError || !!gqlError, requestPayload, responseText);
234
+ const pageVisitEventHTTP = new PageVisitEventHTTP(httpEvent, httpData, !!gqlError);
235
+ // Save HTTP Info
236
+ pageVisitEventHTTP.saveHTTPEvent();
237
+ const seq = (_a = pageVisitEventHTTP.httpData) === null || _a === void 0 ? void 0 : _a[PV_SEQ_ATT_NAME];
238
+ if (httpError) {
239
+ saveErrorToPagevisit(XML_HTTP_REQUEST_ERROR_TYPE, xhr, seq);
240
+ }
241
+ if (gqlError) {
242
+ gqlError.forEach(error => saveErrorToPagevisit(GQL_ERROR_TYPE, error, seq));
243
+ }
244
+ });
245
+ }
246
+ /**
247
+ * Handles fetch failure
248
+ */
249
+ static handleFetchFailure(maybeErr, url) {
250
+ if (!maybeErr || typeof maybeErr !== 'object') {
251
+ return;
252
+ }
253
+ const err = maybeErr;
254
+ // we do not need this error if we cannot extract both the message and stack
255
+ if (!(err === null || err === void 0 ? void 0 : err.message) || !(err === null || err === void 0 ? void 0 : err.stack)) {
256
+ return;
257
+ }
258
+ const errorFromFetch = new Error();
259
+ errorFromFetch.stack = err.stack;
260
+ // making sure we have an url
261
+ const checkedURL = safeTrim(url);
262
+ const urlMessage = checkedURL ? ` on url ${checkedURL}` : '';
263
+ errorFromFetch.message = `${err.message}${urlMessage}`;
264
+ const errorPayload = {
265
+ error: errorFromFetch,
266
+ };
267
+ saveErrorToPagevisit(FETCH_EXCEPTION_ERROR_TYPE, errorPayload);
268
+ }
269
+ /** gets method from xhr */
270
+ static getMethodFromXHR(xhr, payload) {
271
+ if (xhr.noibuHttpMethod) {
272
+ return xhr.noibuHttpMethod;
273
+ }
274
+ // This is very hacky, but we do not really have an option as
275
+ // we do not have access to either the url nor the method when
276
+ // overriding the send method
277
+ return payload ? 'POST' : 'GET';
278
+ }
279
+ /**
280
+ * Wraps the send prototype of the xmlhttp object
281
+ * We set nbuWrapper to the handler function so if this returns an error
282
+ * the trace message will start with nbuWrapper which would allow us to
283
+ * detect that this is a wrapped function and not a NoibuJS error
284
+ */
285
+ static wrapXMLHTTPSend(proto) {
286
+ replace(proto, 'send', function (originalFunction) {
287
+ return function nbuWrapper(payload) {
288
+ try {
289
+ const method = RequestMonitor.getMethodFromXHR(this, payload);
290
+ const startTime = Date.now();
291
+ addSafeEventListener(this, 'loadend', () => RequestMonitor.loadendHandler(this, startTime, this.noibuHttpUrl || this.responseURL, method, payload));
292
+ }
293
+ catch (e) {
294
+ const stack = tryGetStackTrace(e);
295
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error in XHR.send() wrapper: ${e}${stack}`, false, SEVERITY.error);
296
+ }
297
+ return originalFunction.call(this, payload);
298
+ };
299
+ });
300
+ }
301
+ /**
302
+ * Wraps the open prototype of the xmlhttp object
303
+ * We set nbuWrapper to the handler function so if this returns an error
304
+ * the trace message will start with nbuWrapper which would allow us to
305
+ * detect that this is a wrapped function and not a NoibuJS error
306
+ */
307
+ static wrapXMLHTTPOpen(proto, shouldHandleLoadend) {
308
+ replace(proto, 'open', function (originalFunction) {
309
+ return function nbuWrapper(method, url, async = true, user = null, password = null) {
310
+ try {
311
+ /**
312
+ * We wrap assignment of the .noibu[...] variables in a try/catch, as we're assigning
313
+ * properties to a built-in object type, and have no guarantee of success.
314
+ */
315
+ try {
316
+ this.noibuHttpMethod = method;
317
+ this.noibuHttpUrl = url;
318
+ }
319
+ catch (error) {
320
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to set custom properties on XHR object: ${error}`, false, SEVERITY.warn);
321
+ }
322
+ if (shouldHandleLoadend) {
323
+ const startTime = Date.now();
324
+ addSafeEventListener(this, 'loadend', () => RequestMonitor.loadendHandler(this, startTime, url, method, null));
325
+ }
326
+ }
327
+ catch (e) {
328
+ const stack = tryGetStackTrace(e);
329
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error in XHR.open() wrapper: ${e}${stack}`, false, SEVERITY.error);
330
+ }
331
+ return originalFunction.call(this, method, url, async, user, password);
332
+ };
333
+ });
334
+ }
335
+ /**
336
+ * Replaces the native XMLHTTPRequest.setHeader() function. We use this to build an array of
337
+ * request headers as these are not stored by the XMLHTTPRequest object.
338
+ * @param {object} proto window.XMLHTTPRequest's prototype
339
+ */
340
+ static wrapXMLHTTPSetRequestHeader(proto) {
341
+ replace(proto, 'setRequestHeader', function (originalFunction) {
342
+ return function nbuWrapper(header, value) {
343
+ // We wrap all of our logic in a try block to guarantee that we will complete the original
344
+ // implementation if we throw an error in our logic.
345
+ try {
346
+ if (!(this.noibuRequestHeaders instanceof Map)) {
347
+ this.noibuRequestHeaders = new Map();
348
+ }
349
+ // Ensure it's a string. This also converts null to "null"
350
+ const stringValue = typeof value === 'string' ? value : String(value);
351
+ if (typeof header === 'string') {
352
+ this.noibuRequestHeaders.set(header.toLowerCase(), stringValue);
353
+ }
354
+ }
355
+ catch (error) {
356
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Error in XHR.setRequestHeader() wrapper: ${error}`, false, SEVERITY.error);
357
+ }
358
+ return originalFunction.call(this, header, value);
359
+ };
360
+ });
361
+ }
362
+ /**
363
+ * Setting up the wrapper around send functions to try and
364
+ * catch http errors
365
+ */
366
+ static setupGlobalXMLHttpWrapper() {
367
+ if (typeof XMLHttpRequest === 'undefined') {
368
+ return;
369
+ }
370
+ const XMLHttp = XMLHttpRequest;
371
+ // The event target needs to have a prototype and have the open function
372
+ const proto = XMLHttp && XMLHttp.prototype;
373
+ const canWriteOpen = propWriteableOrMadeWriteable(proto, 'open');
374
+ const canWriteSend = propWriteableOrMadeWriteable(proto, 'send');
375
+ const canWriteSetHeader = propWriteableOrMadeWriteable(proto, 'setRequestHeader');
376
+ if (canWriteOpen) {
377
+ // If we can't wrap the XHR.send() function, we need the wrapped .open() function to
378
+ // set the listener on 'loadend' events.
379
+ RequestMonitor.wrapXMLHTTPOpen(proto, !canWriteSend);
380
+ }
381
+ if (canWriteSend) {
382
+ RequestMonitor.wrapXMLHTTPSend(proto);
383
+ }
384
+ if (canWriteSetHeader) {
385
+ RequestMonitor.wrapXMLHTTPSetRequestHeader(proto);
386
+ }
387
+ }
388
+ }
389
+
390
+ export { RequestMonitor as default };
@@ -0,0 +1,259 @@
1
+ import { __awaiter } from 'tslib';
2
+ import { isInstanceOf, getMaxSubstringAllowed } from '../../utils/function.js';
3
+ import { CONTENT_TYPE, SEVERITY } from '../../constants.js';
4
+ import ClientConfig from '../../api/clientConfig.js';
5
+
6
+ const MESSAGE_ATT_NAME = 'message';
7
+ const EXTENSIONS_ATT_NAME = 'extensions';
8
+ const LOCATIONS_ATT_NAME = 'locations';
9
+ const PATH_ATT_NAME = 'path';
10
+ const LINE_ATT_NAME = 'line';
11
+ const COLUMN_ATT_NAME = 'column';
12
+ const MESSAGE_MAX_LENGTH = 1000;
13
+ /* eslint-disable no-restricted-syntax */
14
+ /* eslint-disable no-param-reassign */
15
+ /**
16
+ * Try detecting GraphQL errors from http response
17
+ */
18
+ class GqlErrorValidator {
19
+ /**
20
+ * Retrieves GQL error object based on fetch request/response
21
+ */
22
+ static fromFetch(url, options, request, response) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ try {
25
+ const isResponseValid = isInstanceOf(response, Response) && response.ok;
26
+ if (!isResponseValid) {
27
+ return null;
28
+ }
29
+ const contentType = this._getContentTypeFromFetchArguments(options, request);
30
+ if (this._shouldHandleRequest(url, contentType)) {
31
+ const data = yield response.json();
32
+ return this._validate(data, []);
33
+ }
34
+ }
35
+ catch (e) {
36
+ // can't read the response if request is aborted
37
+ // so just ignore it
38
+ if (!this._isRequestAborted(options, request)) {
39
+ this._postError(e);
40
+ }
41
+ }
42
+ return null;
43
+ });
44
+ }
45
+ /**
46
+ * Retrieves GQL error object based on XHR object
47
+ */
48
+ static fromXhr(url, xhr) {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ try {
51
+ const isResponseValid = isInstanceOf(xhr, XMLHttpRequest) &&
52
+ xhr.status >= 200 &&
53
+ xhr.status <= 299;
54
+ if (!isResponseValid) {
55
+ return null;
56
+ }
57
+ let contentType;
58
+ if (xhr.noibuRequestHeaders) {
59
+ contentType = xhr.noibuRequestHeaders.get(CONTENT_TYPE);
60
+ }
61
+ if (this._shouldHandleRequest(url, contentType)) {
62
+ let data = null;
63
+ if (xhr.responseType === 'blob') {
64
+ if (xhr.response.text) {
65
+ const content = yield xhr.response.text();
66
+ data = this._parseJsonSafely(content);
67
+ }
68
+ }
69
+ else if (xhr.responseType === 'json') {
70
+ data = xhr.response;
71
+ }
72
+ else {
73
+ const content = xhr.responseText;
74
+ data = this._parseJsonSafely(content);
75
+ }
76
+ if (data) {
77
+ return this._validate(data, []);
78
+ }
79
+ }
80
+ }
81
+ catch (e) {
82
+ this._postError(e);
83
+ }
84
+ return null;
85
+ });
86
+ }
87
+ /**
88
+ * Try safely parse a string and return null if fails
89
+ */
90
+ static _parseJsonSafely(content) {
91
+ try {
92
+ return JSON.parse(content);
93
+ }
94
+ catch (e) {
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * Try to get content type for fetch arguments
100
+ */
101
+ static _getContentTypeFromFetchArguments(options, request) {
102
+ let headers = null;
103
+ if (isInstanceOf(request, Request)) {
104
+ headers = request.headers;
105
+ }
106
+ else if (options && options.headers) {
107
+ headers = new Headers(options.headers);
108
+ }
109
+ let contentType = null;
110
+ if (headers) {
111
+ contentType = headers.get(CONTENT_TYPE);
112
+ }
113
+ return contentType;
114
+ }
115
+ /**
116
+ * Checks if request is aborted
117
+ * If it has been aborted we are not able to consume the response
118
+ */
119
+ static _isRequestAborted(options, request) {
120
+ if (request) {
121
+ return request.signal && request.signal.aborted;
122
+ }
123
+ if (options && isInstanceOf(options.signal, AbortSignal)) {
124
+ return options.signal.aborted;
125
+ }
126
+ return false;
127
+ }
128
+ /**
129
+ * Determines if request should be processed
130
+ */
131
+ static _shouldHandleRequest(url, contentType) {
132
+ if (contentType) {
133
+ contentType = contentType.toLowerCase();
134
+ }
135
+ let isGqlUrl = false;
136
+ if (url) {
137
+ if (isInstanceOf(url, URL)) {
138
+ url = url.toString();
139
+ }
140
+ isGqlUrl = url.toLowerCase().includes('graphql');
141
+ }
142
+ return ((contentType === 'application/json' && isGqlUrl) ||
143
+ contentType === 'application/graphql');
144
+ }
145
+ /**
146
+ * Sanitizes payload object
147
+ */
148
+ static _validate(data, validationIssues) {
149
+ if (!(typeof data === 'object' &&
150
+ data &&
151
+ Array.isArray(data.errors))) {
152
+ return null;
153
+ }
154
+ const errors = data.errors;
155
+ errors.forEach(error => {
156
+ if (typeof error !== 'object' || !error) {
157
+ return;
158
+ }
159
+ const properties = Object.keys(error);
160
+ for (const property of properties) {
161
+ switch (property) {
162
+ case MESSAGE_ATT_NAME:
163
+ this._validateMessage(error);
164
+ break;
165
+ case LOCATIONS_ATT_NAME:
166
+ this._validateLocations(error, validationIssues);
167
+ break;
168
+ case PATH_ATT_NAME:
169
+ this._validatePath(error, validationIssues);
170
+ break;
171
+ case EXTENSIONS_ATT_NAME:
172
+ this._validateExtensions(error);
173
+ break;
174
+ default:
175
+ delete error[property];
176
+ validationIssues.push(`unexpected error.${property}`);
177
+ break;
178
+ }
179
+ }
180
+ });
181
+ if (validationIssues.length > 0) {
182
+ this._postValidationIssues(validationIssues);
183
+ }
184
+ return errors;
185
+ }
186
+ /**
187
+ * Sanitizes message object
188
+ */
189
+ static _validateMessage(error) {
190
+ error[MESSAGE_ATT_NAME] = getMaxSubstringAllowed(error[MESSAGE_ATT_NAME], MESSAGE_MAX_LENGTH);
191
+ }
192
+ /**
193
+ * Sanitizes extensions object
194
+ * @param {any} error
195
+ */
196
+ static _validateExtensions(error) {
197
+ const json = JSON.stringify(error[EXTENSIONS_ATT_NAME]);
198
+ error[EXTENSIONS_ATT_NAME] = getMaxSubstringAllowed(json, MESSAGE_MAX_LENGTH);
199
+ }
200
+ /**
201
+ * Sanitizes locations object
202
+ */
203
+ static _validateLocations(error, validationIssues) {
204
+ const locations = error[LOCATIONS_ATT_NAME];
205
+ if (Array.isArray(locations)) {
206
+ for (const location of locations) {
207
+ const properties = Object.keys(location);
208
+ for (const property of properties) {
209
+ switch (property) {
210
+ case LINE_ATT_NAME:
211
+ case COLUMN_ATT_NAME:
212
+ if (!Number.isSafeInteger(location[property])) {
213
+ const value = location[property];
214
+ location[property] = 0;
215
+ validationIssues.push(`unexpected ${property} value '${value}'`);
216
+ }
217
+ break;
218
+ default:
219
+ delete location[property];
220
+ validationIssues.push(`unexpected error.location.${property}`);
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ else {
227
+ delete error[LOCATIONS_ATT_NAME];
228
+ validationIssues.push(`unexpected error.locations`);
229
+ }
230
+ }
231
+ /**
232
+ * Sanitizes path object
233
+ */
234
+ static _validatePath(error, validationIssues) {
235
+ const path = error[PATH_ATT_NAME];
236
+ if (Array.isArray(path)) {
237
+ error[PATH_ATT_NAME] = error[PATH_ATT_NAME].map(x => x.toString());
238
+ }
239
+ else {
240
+ delete error[PATH_ATT_NAME];
241
+ validationIssues.push(`unexpected error.path`);
242
+ }
243
+ }
244
+ /**
245
+ * Posts error
246
+ */
247
+ static _postError(message) {
248
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`GQL parse error: ${message}`, false, SEVERITY.error);
249
+ }
250
+ /**
251
+ * Posts issue found during object sanitization
252
+ */
253
+ static _postValidationIssues(validationIssues) {
254
+ const message = validationIssues.join(',');
255
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`GQL error validation warning: ${message}`, false, SEVERITY.error);
256
+ }
257
+ }
258
+
259
+ export { GqlErrorValidator as default };