noibu-react-native 0.2.2 → 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.
- package/README.md +1 -1
- package/dist/api/clientConfig.js +225 -217
- package/dist/api/helpCode.js +61 -87
- package/dist/api/metroplexSocket.js +460 -463
- package/dist/api/storedPageVisit.js +150 -208
- package/dist/constants.js +10 -2
- package/dist/entry/init.js +65 -63
- package/dist/monitors/{appNavigationMonitor.js → AppNavigationMonitor.js} +12 -22
- package/dist/monitors/ClickMonitor.js +198 -0
- package/dist/monitors/ErrorMonitor.js +206 -0
- package/dist/monitors/KeyboardInputMonitor.js +60 -0
- package/dist/monitors/PageMonitor.js +98 -0
- package/dist/monitors/RequestMonitor.js +390 -0
- package/dist/monitors/http-tools/GqlErrorValidator.js +259 -0
- package/dist/monitors/http-tools/HTTPDataBundler.js +458 -0
- package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
- package/dist/pageVisit/EventDebouncer.js +99 -0
- package/dist/pageVisit/pageVisitEventError.js +2 -2
- package/dist/pageVisit/pageVisitEventHTTP.js +79 -93
- package/dist/react/ErrorBoundary.js +18 -15
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
- package/dist/sessionRecorder/sessionRecorder.js +152 -151
- package/dist/{api → src/api}/clientConfig.d.ts +2 -2
- package/dist/{api → src/api}/helpCode.d.ts +10 -16
- package/dist/{api → src/api}/metroplexSocket.d.ts +48 -67
- package/dist/{api → src/api}/storedPageVisit.d.ts +12 -21
- package/dist/{constants.d.ts → src/constants.d.ts} +45 -0
- package/dist/{entry → src/entry}/init.d.ts +1 -1
- package/dist/src/monitors/AppNavigationMonitor.d.ts +18 -0
- package/dist/src/monitors/ClickMonitor.d.ts +31 -0
- package/dist/src/monitors/ErrorMonitor.d.ts +63 -0
- package/dist/{monitors/keyboardInputMonitor.d.ts → src/monitors/KeyboardInputMonitor.d.ts} +7 -4
- package/dist/{monitors/pageMonitor.d.ts → src/monitors/PageMonitor.d.ts} +6 -8
- package/dist/src/monitors/RequestMonitor.d.ts +94 -0
- package/dist/src/monitors/http-tools/GqlErrorValidator.d.ts +59 -0
- package/dist/src/monitors/http-tools/HTTPDataBundler.d.ts +112 -0
- package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +3 -2
- package/dist/src/pageVisit/EventDebouncer.d.ts +24 -0
- package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +1 -1
- package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
- package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
- package/dist/{storage → src/storage}/rnStorageProvider.d.ts +1 -1
- package/dist/{storage → src/storage}/storage.d.ts +2 -2
- package/dist/{storage → src/storage}/storageProvider.d.ts +3 -3
- package/dist/{utils → src/utils}/function.d.ts +27 -7
- package/dist/{utils → src/utils}/object.d.ts +11 -8
- package/dist/src/utils/piiRedactor.d.ts +11 -0
- package/dist/src/utils/polyfills.d.ts +4 -0
- package/dist/storage/rnStorageProvider.js +7 -4
- package/dist/storage/storage.js +43 -35
- package/dist/storage/storageProvider.js +23 -19
- package/dist/types/Config.d.ts +24 -20
- package/dist/types/Metroplex.types.d.ts +73 -0
- package/dist/types/Monitor.d.ts +11 -0
- package/dist/types/Monitor.js +19 -0
- package/dist/types/PageVisit.types.d.ts +8 -0
- package/dist/types/PageVisitErrors.types.d.ts +114 -0
- package/dist/types/PageVisitEvents.types.d.ts +91 -0
- package/dist/types/PageVisitMetrics.types.d.ts +27 -0
- package/dist/types/Storage.d.ts +1 -1
- package/dist/types/StoredPageVisit.types.d.ts +4 -47
- package/dist/types/WrappedObjects.d.ts +6 -0
- package/dist/utils/function.js +110 -77
- package/dist/utils/object.js +59 -6
- package/dist/utils/piiRedactor.js +98 -0
- package/dist/utils/polyfills.js +24 -0
- package/package.json +8 -8
- package/dist/monitors/appNavigationMonitor.d.ts +0 -22
- package/dist/monitors/clickMonitor.d.ts +0 -44
- package/dist/monitors/clickMonitor.js +0 -251
- package/dist/monitors/errorMonitor.d.ts +0 -28
- package/dist/monitors/errorMonitor.js +0 -180
- package/dist/monitors/gqlErrorValidator.d.ts +0 -82
- package/dist/monitors/gqlErrorValidator.js +0 -306
- package/dist/monitors/httpDataBundler.d.ts +0 -161
- package/dist/monitors/httpDataBundler.js +0 -725
- package/dist/monitors/inputMonitor.d.ts +0 -34
- package/dist/monitors/inputMonitor.js +0 -138
- package/dist/monitors/keyboardInputMonitor.js +0 -66
- package/dist/monitors/pageMonitor.js +0 -122
- package/dist/monitors/requestMonitor.d.ts +0 -10
- package/dist/monitors/requestMonitor.js +0 -401
- package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
- package/dist/types/PageVisit.d.ts +0 -22
- package/dist/types/ReactNative.d.ts +0 -4
- package/dist/types/globals.d.ts +0 -45
- /package/dist/{api → src/api}/inputManager.d.ts +0 -0
- /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
- /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
- /package/dist/{entry → src/entry}/index.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
- /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
- /package/dist/{utils → src/utils}/date.d.ts +0 -0
- /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
- /package/dist/{utils → src/utils}/log.d.ts +0 -0
- /package/dist/{utils → src/utils}/performance.d.ts +0 -0
- /package/dist/{utils → src/utils}/stacktrace-parser.d.ts +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { __awaiter } from 'tslib';
|
|
2
|
+
import { HUMAN_READABLE_CONTENT_TYPE_REGEX, DEFAULT_WEBSITE_SUBDOMAIN_PATTERN, SEVERITY, HTTP_BODY_NULL_STRING, HTTP_DATA_REQ_HEADERS_ATT_NAME, HTTP_DATA_PAYLOAD_ATT_NAME, HTTP_DATA_RESP_HEADERS_ATT_NAME, HTTP_DATA_RESP_PAYLOAD_ATT_NAME, HTTP_BODY_DROPPED_LENGTH_MSG, HTTP_BODY_DROPPED_TYPE_MSG, CONTENT_TYPE, CONTENT_LENGTH, BLOCKED_HTTP_HEADER_KEYS, PII_REDACTION_REPLACEMENT_STRING, MAX_HTTP_DATA_PAYLOAD_LENGTH, MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH } from '../../constants.js';
|
|
3
|
+
import ClientConfig from '../../api/clientConfig.js';
|
|
4
|
+
import StoredMetrics from '../../api/storedMetrics.js';
|
|
5
|
+
import { safeFromEntries, safeEntries } from '../../utils/object.js';
|
|
6
|
+
import { safeTrim, stringifyJSON, isString } from '../../utils/function.js';
|
|
7
|
+
import { removePII } from '../../utils/piiRedactor.js';
|
|
8
|
+
import { Singleton } from '../../types/Monitor.js';
|
|
9
|
+
|
|
10
|
+
/** Bundles HTTP payloads and headers */
|
|
11
|
+
class HTTPDataBundler extends Singleton {
|
|
12
|
+
/**
|
|
13
|
+
* Creates an instance of the ClickMonitor instance
|
|
14
|
+
*/
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
// compile regex only once
|
|
18
|
+
this.contentTypeReadableRegex = new RegExp(HUMAN_READABLE_CONTENT_TYPE_REGEX, 'i');
|
|
19
|
+
// pull out the domain hostname
|
|
20
|
+
const initialURL = ClientConfig.getInstance().globalUrl;
|
|
21
|
+
this.initialURLPartsReversed = [];
|
|
22
|
+
this.hostname = '';
|
|
23
|
+
if (initialURL && initialURL.length > 0) {
|
|
24
|
+
try {
|
|
25
|
+
// Store URL parts to class variable
|
|
26
|
+
this.hostname = new URL(initialURL).hostname;
|
|
27
|
+
this.initialURLPartsReversed = this.hostname.split('.');
|
|
28
|
+
// Remove www. etc.
|
|
29
|
+
// eslint-disable-next-line no-unused-expressions
|
|
30
|
+
DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(this.initialURLPartsReversed[0]) && this.initialURLPartsReversed.shift();
|
|
31
|
+
// Reverse array
|
|
32
|
+
this.initialURLPartsReversed.reverse();
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for initial URL: ${e}`, false, SEVERITY.warn);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
this.httpDataCollectionEnabled =
|
|
39
|
+
!!ClientConfig.getInstance().enableHttpDataCollection;
|
|
40
|
+
// compile the relative and full HTTP URL regexes
|
|
41
|
+
const allowedURLs = ClientConfig.getInstance().listOfUrlsToCollectHttpDataFrom;
|
|
42
|
+
this.httpDataAllowedAbsoluteRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, true);
|
|
43
|
+
this.httpDataAllowedRelativeRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, false);
|
|
44
|
+
// track unique requests and only capture each once
|
|
45
|
+
// TODO: disabled for beta. NOI-4253
|
|
46
|
+
// this.uniqueRequests = new Set();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Builds the HTTP payload allowed regexes for full and relative URLs
|
|
50
|
+
* @param allowedURLs A list of allowed URLs
|
|
51
|
+
* @param absolute Use only absolute URLs if true, use only relative URL if false
|
|
52
|
+
* @returns a regex of allowed URLs
|
|
53
|
+
*/
|
|
54
|
+
static buildAllowedRegex(allowedURLs, absolute) {
|
|
55
|
+
if (!allowedURLs || !Array.isArray(allowedURLs))
|
|
56
|
+
return null;
|
|
57
|
+
const allowedURLsFiltered = allowedURLs
|
|
58
|
+
.map(url => safeTrim(url).toLowerCase())
|
|
59
|
+
.filter(url => {
|
|
60
|
+
const isAbsolute = HTTPDataBundler.isAbsoluteURL(url);
|
|
61
|
+
if (absolute) {
|
|
62
|
+
return url && isAbsolute;
|
|
63
|
+
}
|
|
64
|
+
return url && !isAbsolute;
|
|
65
|
+
});
|
|
66
|
+
if (allowedURLsFiltered.length > 0) {
|
|
67
|
+
return new RegExp(allowedURLsFiltered.join('|'));
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Takes an iterator and returns a map of strings representing headers.
|
|
73
|
+
* param {object} headersIterable any iterable object
|
|
74
|
+
* returns a map of strings (as expected by metroplex) representing HTTP
|
|
75
|
+
* request or response headers
|
|
76
|
+
*/
|
|
77
|
+
static headersMapFromIterable(headersIterable) {
|
|
78
|
+
const headerMap = new Map();
|
|
79
|
+
for (const header of headersIterable) {
|
|
80
|
+
// Ensure we're working with strings
|
|
81
|
+
// This will also automatically convert null to "null"
|
|
82
|
+
if (typeof header[0] !== 'string')
|
|
83
|
+
header[0] = String(header[0]);
|
|
84
|
+
if (typeof header[1] !== 'string')
|
|
85
|
+
header[1] = String(header[1]);
|
|
86
|
+
headerMap.set(header[0].toLowerCase(), header[1]);
|
|
87
|
+
}
|
|
88
|
+
return headerMap;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Takes a string of headers with 'name: value' and returns
|
|
92
|
+
* a map of strings representing headers.
|
|
93
|
+
* headersString is all the headers in one string
|
|
94
|
+
* returns a map of strings (as expected by metroplex) representing HTTP
|
|
95
|
+
* request or response headers
|
|
96
|
+
*/
|
|
97
|
+
static headersMapFromString(headersString) {
|
|
98
|
+
const headerMap = new Map();
|
|
99
|
+
if (!headersString || typeof headersString !== 'string')
|
|
100
|
+
return headerMap;
|
|
101
|
+
const headerStringArray = headersString.split('\r\n').filter(Boolean);
|
|
102
|
+
headerStringArray.forEach(function (headerString) {
|
|
103
|
+
const split = headerString.split(': ');
|
|
104
|
+
if (split.length === 2 && split[0].length > 0 && split[1].length > 0) {
|
|
105
|
+
headerMap.set(split[0].toLowerCase(), split[1]);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return headerMap;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* For an XHR object, checks the responseType property and handles the response or
|
|
112
|
+
* responseText property accordingly to return a string representation of the response.
|
|
113
|
+
* @returns a string representation of the response, or null if this fails.
|
|
114
|
+
*/
|
|
115
|
+
static getResponseStringFromXHR(xhr) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
var _a;
|
|
118
|
+
// This should not happen, as this function is called from a method on xhr.
|
|
119
|
+
if (!xhr)
|
|
120
|
+
return null;
|
|
121
|
+
if (xhr.responseType === '' || xhr.responseType === 'text') {
|
|
122
|
+
return xhr.responseText;
|
|
123
|
+
}
|
|
124
|
+
// If the XHR object exists but the response is null, return null as a string.
|
|
125
|
+
if (!xhr.response)
|
|
126
|
+
return HTTP_BODY_NULL_STRING;
|
|
127
|
+
if ((_a = xhr.response.documentElement) === null || _a === void 0 ? void 0 : _a.innerHTML) {
|
|
128
|
+
return xhr.response.documentElement.innerHTML;
|
|
129
|
+
}
|
|
130
|
+
if (typeof xhr.response.text === 'function') {
|
|
131
|
+
return yield xhr.response.text();
|
|
132
|
+
}
|
|
133
|
+
if (xhr.responseType === 'json') {
|
|
134
|
+
try {
|
|
135
|
+
const text = stringifyJSON(xhr.response);
|
|
136
|
+
if (text === '{}')
|
|
137
|
+
return null; // stringifyJSON returns {} of it fails to serialize a property
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify JSON response: ${e}`, false, SEVERITY.warn);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Builds an HTTP Data bundle
|
|
150
|
+
*/
|
|
151
|
+
bundleHTTPData(url, requestHeaders, rawRequestPayload, responseHeaders, rawResponsePayload, method, isError) {
|
|
152
|
+
if (!this.isValidRequest(method)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
// stringify payload if correct type and not too large
|
|
156
|
+
let requestPayload = '';
|
|
157
|
+
let responsePayload = '';
|
|
158
|
+
if (this.shouldCollectPayloadForURL(url)) {
|
|
159
|
+
requestPayload =
|
|
160
|
+
this.getReasonPayloadIsDropped(requestHeaders, isError) ||
|
|
161
|
+
this.stringFromRequestBody(rawRequestPayload, requestHeaders);
|
|
162
|
+
responsePayload =
|
|
163
|
+
this.getReasonPayloadIsDropped(responseHeaders, isError) ||
|
|
164
|
+
this.stringFromRequestBody(rawResponsePayload, responseHeaders);
|
|
165
|
+
}
|
|
166
|
+
// don't bundle if there is no data
|
|
167
|
+
const safeRequestHeaders = requestHeaders || new Map();
|
|
168
|
+
const safeRequestPayload = requestPayload || '';
|
|
169
|
+
const safeResponseHeaders = responseHeaders || new Map();
|
|
170
|
+
const safeResponsePayload = responsePayload || '';
|
|
171
|
+
if (safeRequestHeaders.size === 0 &&
|
|
172
|
+
!safeRequestPayload &&
|
|
173
|
+
safeResponseHeaders.size === 0 &&
|
|
174
|
+
!safeResponsePayload) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// Ensure payloads do not exceed the maximum size and redact PII
|
|
178
|
+
const requestPayloadUpdated = this.restrictPayload(safeRequestPayload, url, isError);
|
|
179
|
+
// Ensure payloads do not exceed the maximum size and redact PII
|
|
180
|
+
const responsePayloadUpdated = this.restrictPayload(safeResponsePayload, url, isError);
|
|
181
|
+
// Redact PII.
|
|
182
|
+
const cleanRequestHeaders = safeFromEntries(this.removePIIHeaders(requestHeaders));
|
|
183
|
+
const cleanResponseHeaders = safeFromEntries(this.removePIIHeaders(responseHeaders));
|
|
184
|
+
return {
|
|
185
|
+
[HTTP_DATA_REQ_HEADERS_ATT_NAME]: cleanRequestHeaders,
|
|
186
|
+
[HTTP_DATA_PAYLOAD_ATT_NAME]: requestPayloadUpdated,
|
|
187
|
+
[HTTP_DATA_RESP_HEADERS_ATT_NAME]: cleanResponseHeaders,
|
|
188
|
+
[HTTP_DATA_RESP_PAYLOAD_ATT_NAME]: responsePayloadUpdated,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Validates a request based on the URL and method. When enabled, will handle
|
|
193
|
+
* de-duping the requests
|
|
194
|
+
*/
|
|
195
|
+
isValidRequest(method) {
|
|
196
|
+
return (this.httpDataCollectionEnabled && method && typeof method === 'string');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Checks two things: that the URL is either on the same domain (or an address relative to the
|
|
200
|
+
* current domain), and also checks that the config http_data_collection flag is enabled.
|
|
201
|
+
* @param {string} url
|
|
202
|
+
* @returns boolean indicating whether the URL passed is either relative (in which case it is
|
|
203
|
+
* inherently on the current domain) or matches the current domain.
|
|
204
|
+
*/
|
|
205
|
+
shouldContinueForURL(url) {
|
|
206
|
+
if (!this.httpDataCollectionEnabled)
|
|
207
|
+
return false;
|
|
208
|
+
if (!url || typeof url !== 'string' || !this.initialURLPartsReversed) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
// URL is absolute; either "http://example.com" or "//example.com"
|
|
212
|
+
if (HTTPDataBundler.isAbsoluteURL(url)) {
|
|
213
|
+
// Only capture requests on the same domain or in the allowed list
|
|
214
|
+
if (!this.shouldCollectPayloadForURL(url)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
} // end `if` (if URL is relative, it is on the same domain.)
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Determines if the URL is absolute or relative
|
|
222
|
+
* returns boolean indicating whether the URL passed is either absolute or relative
|
|
223
|
+
*/
|
|
224
|
+
static isAbsoluteURL(url) {
|
|
225
|
+
if (!url || typeof url !== 'string') {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return url.indexOf('://') > 0 || url.indexOf('//') === 0;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Checks whether HTTP payloads can be collected on this URL
|
|
232
|
+
* returns boolean indicating whether HTTP payloads can be collected on this URL
|
|
233
|
+
*/
|
|
234
|
+
shouldCollectPayloadForURL(url) {
|
|
235
|
+
var _a, _b;
|
|
236
|
+
if (!url || typeof url !== 'string') {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
// check if in the full URL allowed list
|
|
240
|
+
const isAllowedAsAbsolute = (_a = this.httpDataAllowedAbsoluteRegex) === null || _a === void 0 ? void 0 : _a.test(url.toLowerCase());
|
|
241
|
+
const isRelative = !HTTPDataBundler.isAbsoluteURL(url);
|
|
242
|
+
// check if in the relative URL allowed list or if it is on the same domain
|
|
243
|
+
const isAllowedAsRelative =
|
|
244
|
+
// if this is a relative URL (isURLSameDomain will fail) OR a full URL on domain
|
|
245
|
+
isRelative && ((_b = this.httpDataAllowedRelativeRegex) === null || _b === void 0 ? void 0 : _b.test(url.toLowerCase()));
|
|
246
|
+
return isAllowedAsAbsolute || isAllowedAsRelative;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Double checks content length if we couldn't read the headers, and redacts PII
|
|
250
|
+
* returns the restricted payload
|
|
251
|
+
*/
|
|
252
|
+
restrictPayload(payload, url, isError) {
|
|
253
|
+
// if no payload, too large, payload already dropped,
|
|
254
|
+
// or payloads not allowed for this URL: nothing to do
|
|
255
|
+
if (!payload || !this.shouldCollectPayloadForURL(url)) {
|
|
256
|
+
return HTTP_BODY_NULL_STRING;
|
|
257
|
+
}
|
|
258
|
+
if (typeof payload !== 'string') {
|
|
259
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient({
|
|
260
|
+
msg: `restrictPayload received non string payload`,
|
|
261
|
+
payloadType: typeof payload,
|
|
262
|
+
}, false, SEVERITY.error);
|
|
263
|
+
return HTTP_BODY_NULL_STRING;
|
|
264
|
+
}
|
|
265
|
+
if (payload === HTTP_BODY_NULL_STRING ||
|
|
266
|
+
(!!payload.startsWith &&
|
|
267
|
+
(payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
|
|
268
|
+
payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG))) ||
|
|
269
|
+
(!!payload.indexOf &&
|
|
270
|
+
(payload.indexOf(HTTP_BODY_DROPPED_LENGTH_MSG) === 0 ||
|
|
271
|
+
payload.indexOf(HTTP_BODY_DROPPED_TYPE_MSG) === 0))) {
|
|
272
|
+
return payload;
|
|
273
|
+
}
|
|
274
|
+
let restrictSize = isError
|
|
275
|
+
? MAX_HTTP_DATA_PAYLOAD_LENGTH
|
|
276
|
+
: MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
|
|
277
|
+
// TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
|
|
278
|
+
if (this.hostname === 'www.holtrenfrew.com') {
|
|
279
|
+
restrictSize = 1.5 * 1024 * 1024;
|
|
280
|
+
}
|
|
281
|
+
if (payload.length > restrictSize) {
|
|
282
|
+
StoredMetrics.getInstance().addHttpDataDropByLength();
|
|
283
|
+
return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${payload.length}`;
|
|
284
|
+
}
|
|
285
|
+
// payload is good!
|
|
286
|
+
StoredMetrics.getInstance().addHttpDataPayloadCount();
|
|
287
|
+
return removePII(payload);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Returns true if the content-length header is of acceptable size.
|
|
291
|
+
* Too big gets rejected
|
|
292
|
+
* If the headers are not found, check actual content for length
|
|
293
|
+
* @param {Headers} headers
|
|
294
|
+
* @returns boolean true if acceptable to collect
|
|
295
|
+
*/
|
|
296
|
+
contentLengthAcceptable(headers, isError) {
|
|
297
|
+
// TODO This entire check should move into the parent's parent so there is a single place
|
|
298
|
+
// that restricts paylods and unterstands these 2 values
|
|
299
|
+
let restrictSize = isError
|
|
300
|
+
? MAX_HTTP_DATA_PAYLOAD_LENGTH
|
|
301
|
+
: MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
|
|
302
|
+
// TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
|
|
303
|
+
if (this.hostname === 'www.holtrenfrew.com') {
|
|
304
|
+
restrictSize = 1.5 * 1024 * 1024;
|
|
305
|
+
}
|
|
306
|
+
return this.contentLength(headers) <= restrictSize;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Returns true if the content type according to the headers is valid for collection.
|
|
310
|
+
* Also returns assumed true if content type is not stated in headers.
|
|
311
|
+
* @param {Map} headersMap
|
|
312
|
+
* @returns boolean true if acceptable to collect
|
|
313
|
+
*/
|
|
314
|
+
contentTypeAcceptable(headersMap) {
|
|
315
|
+
// check content type
|
|
316
|
+
const contentType = headersMap.get(CONTENT_TYPE);
|
|
317
|
+
return !(contentType &&
|
|
318
|
+
!this.contentTypeReadableRegex.test(contentType.toLowerCase()));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Returns a descriptive string if we have to drop payload based on the length
|
|
322
|
+
* or type listed in the headers passed. Returns an empty string otherwise.
|
|
323
|
+
*/
|
|
324
|
+
getReasonPayloadIsDropped(headers, isError) {
|
|
325
|
+
if (!(headers === null || headers === void 0 ? void 0 : headers.get)) {
|
|
326
|
+
return '';
|
|
327
|
+
}
|
|
328
|
+
const bundler = HTTPDataBundler.getInstance();
|
|
329
|
+
if (!bundler.contentTypeAcceptable(headers)) {
|
|
330
|
+
// not a readable request, drop the content
|
|
331
|
+
StoredMetrics.getInstance().addHttpDataDropByType();
|
|
332
|
+
return `${HTTP_BODY_DROPPED_TYPE_MSG} Payload type: ${headers.get(CONTENT_TYPE)}`;
|
|
333
|
+
}
|
|
334
|
+
if (!bundler.contentLengthAcceptable(headers, isError)) {
|
|
335
|
+
StoredMetrics.getInstance().addHttpDataDropByLength();
|
|
336
|
+
return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${bundler.contentLength(headers)}`;
|
|
337
|
+
}
|
|
338
|
+
return '';
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Returns content length from the headers, if available.
|
|
342
|
+
* If headers are not found or length is not a number, return -1
|
|
343
|
+
*/
|
|
344
|
+
contentLength(headersObject) {
|
|
345
|
+
if (!(headersObject === null || headersObject === void 0 ? void 0 : headersObject.get)) {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
// get content length
|
|
349
|
+
let contentLength = 0;
|
|
350
|
+
const contentValueString = headersObject.get(CONTENT_LENGTH);
|
|
351
|
+
if (!contentValueString) {
|
|
352
|
+
// no content length header found
|
|
353
|
+
return -1;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
contentLength = parseInt(contentValueString, 10);
|
|
357
|
+
if (Number.isNaN(contentLength)) {
|
|
358
|
+
// bad content length header
|
|
359
|
+
return -1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
// if we can't convert the value to a number, confirm after reading payload
|
|
364
|
+
return -1;
|
|
365
|
+
}
|
|
366
|
+
return contentLength;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Accepts a value that could be any type used as a request payload,
|
|
370
|
+
* and returns a string representation, or null if this fails.
|
|
371
|
+
*/
|
|
372
|
+
stringFromRequestBody(value, requestHeaders) {
|
|
373
|
+
/* eslint-disable no-param-reassign */
|
|
374
|
+
// Nullish check to ensure we don't send 'null' or 'undefined' as a string
|
|
375
|
+
if (value == null)
|
|
376
|
+
return null;
|
|
377
|
+
try {
|
|
378
|
+
if (isString(value) && requestHeaders instanceof Map) {
|
|
379
|
+
const contentType = requestHeaders.get(CONTENT_TYPE);
|
|
380
|
+
if (contentType &&
|
|
381
|
+
contentType
|
|
382
|
+
.toLowerCase()
|
|
383
|
+
.includes('application/x-www-form-urlencoded')) {
|
|
384
|
+
value = new URLSearchParams(value.toString());
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
// Nothing--try next try block
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
// If the payload is a FormData object, turn it into an object.
|
|
393
|
+
if (value instanceof FormData || value instanceof URLSearchParams) {
|
|
394
|
+
value = safeEntries(value).reduce((obj, [key, val]) => {
|
|
395
|
+
obj[key] = this.stringFromRequestBody(val);
|
|
396
|
+
return obj;
|
|
397
|
+
}, {});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
// Nothing--try next try block
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
// Case where value has a .toString() method
|
|
405
|
+
const returnVal = value.toString();
|
|
406
|
+
// Catch case where .toString() is on an object, we don't want that --
|
|
407
|
+
// should be stringified below.
|
|
408
|
+
if (!returnVal.includes('[object'))
|
|
409
|
+
return returnVal;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
// Nothing--try next try block
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
// Case where value is a Document type (HTML/XML)
|
|
416
|
+
return value.documentElement.innerHTML;
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
// Nothing--try next try block
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
// Case where value is any other object type
|
|
423
|
+
return stringifyJSON(value);
|
|
424
|
+
}
|
|
425
|
+
catch (e) {
|
|
426
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify request body: ${e}`, false, SEVERITY.warn);
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Removes possible PII from headers.
|
|
432
|
+
* @param {Map} dirtyHeaders a Map of HTTP response or request headers
|
|
433
|
+
* @returns {Map|null} a map of headers with PII redacted
|
|
434
|
+
*/
|
|
435
|
+
removePIIHeaders(dirtyHeaders) {
|
|
436
|
+
// In order to be fail-safe, let's return null if we won't be able to remove PII headers.
|
|
437
|
+
if (!(dirtyHeaders instanceof Map)) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
if (!dirtyHeaders.size) {
|
|
441
|
+
return dirtyHeaders;
|
|
442
|
+
}
|
|
443
|
+
const cleanHeaders = new Map(dirtyHeaders);
|
|
444
|
+
cleanHeaders.forEach((headerVal, headerKey, map) => {
|
|
445
|
+
// If the key is known PII, redact
|
|
446
|
+
if (BLOCKED_HTTP_HEADER_KEYS.includes(headerKey.toLowerCase())) {
|
|
447
|
+
map.set(headerKey, PII_REDACTION_REPLACEMENT_STRING);
|
|
448
|
+
// If not, run the header's value through the string redaction function
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
map.set(headerKey, removePII(headerVal));
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
return cleanHeaders;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export { HTTPDataBundler };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { PageVisit } from './pageVisit.js';
|
|
2
|
+
import { APP_NAVIGATION_EVENT_TYPE, PAGE_EVENT_TYPE, MAX_TIME_FOR_UNSENT_DATA_MILLIS, ERROR_EVENT_TYPE, HTTP_EVENT_TYPE, KEYBOARD_EVENT_TYPE, SEVERITY } from '../constants.js';
|
|
3
|
+
import { timestampWrapper } from '../utils/date.js';
|
|
4
|
+
import { addSafeEventListener } from '../utils/eventlistener.js';
|
|
5
|
+
import ClientConfig from '../api/clientConfig.js';
|
|
6
|
+
import { Singleton } from '../types/Monitor.js';
|
|
7
|
+
|
|
8
|
+
/** @module EventDebouncer */
|
|
9
|
+
/**
|
|
10
|
+
* Singleton class responsible for debouncing all events
|
|
11
|
+
* that are registered
|
|
12
|
+
*/
|
|
13
|
+
class EventDebouncer extends Singleton {
|
|
14
|
+
/**
|
|
15
|
+
* Creates an instance of EventDebouncer
|
|
16
|
+
*/
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.eventsToDebounce = {
|
|
20
|
+
[APP_NAVIGATION_EVENT_TYPE]: {
|
|
21
|
+
timeout: null,
|
|
22
|
+
events: [],
|
|
23
|
+
debouncePeriod: 0,
|
|
24
|
+
eventName: APP_NAVIGATION_EVENT_TYPE,
|
|
25
|
+
},
|
|
26
|
+
[PAGE_EVENT_TYPE]: {
|
|
27
|
+
timeout: null,
|
|
28
|
+
events: [],
|
|
29
|
+
debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
|
|
30
|
+
eventName: PAGE_EVENT_TYPE,
|
|
31
|
+
},
|
|
32
|
+
[ERROR_EVENT_TYPE]: {
|
|
33
|
+
timeout: null,
|
|
34
|
+
events: [],
|
|
35
|
+
debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
|
|
36
|
+
eventName: ERROR_EVENT_TYPE,
|
|
37
|
+
},
|
|
38
|
+
[HTTP_EVENT_TYPE]: {
|
|
39
|
+
timeout: null,
|
|
40
|
+
events: [],
|
|
41
|
+
debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
|
|
42
|
+
eventName: HTTP_EVENT_TYPE,
|
|
43
|
+
},
|
|
44
|
+
[KEYBOARD_EVENT_TYPE]: {
|
|
45
|
+
timeout: null,
|
|
46
|
+
events: [],
|
|
47
|
+
debouncePeriod: MAX_TIME_FOR_UNSENT_DATA_MILLIS,
|
|
48
|
+
eventName: KEYBOARD_EVENT_TYPE,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
this._setupUnloadHandler();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates an event object with the event and the time it was added then pushes
|
|
55
|
+
* that event object to the queue of events waiting to be debounced.
|
|
56
|
+
*/
|
|
57
|
+
addEvent(event, type, occurredAt) {
|
|
58
|
+
if (!(type in this.eventsToDebounce)) {
|
|
59
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(new Error(`Type: ${type} is not in eventsToDebounce`), false, SEVERITY.error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!occurredAt) {
|
|
63
|
+
occurredAt = Date.now();
|
|
64
|
+
}
|
|
65
|
+
this.eventsToDebounce[type].events.push({
|
|
66
|
+
event,
|
|
67
|
+
occurredAt: new Date(timestampWrapper(occurredAt)).toISOString(),
|
|
68
|
+
});
|
|
69
|
+
this._debouncePvEvents(type);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Adds the events from the object to the page visit and sets up a timer
|
|
73
|
+
* to send the events if no more are received without the timeout
|
|
74
|
+
*/
|
|
75
|
+
_debouncePvEvents(type) {
|
|
76
|
+
/**
|
|
77
|
+
* Debounce function to be executed once the debounce period is completed
|
|
78
|
+
*/
|
|
79
|
+
const later = () => {
|
|
80
|
+
this.eventsToDebounce[type].timeout = null;
|
|
81
|
+
PageVisit.getInstance().addPageVisitEvents(this.eventsToDebounce[type].events, this.eventsToDebounce[type].eventName);
|
|
82
|
+
this.eventsToDebounce[type].events = [];
|
|
83
|
+
};
|
|
84
|
+
if (this.eventsToDebounce[type].timeout !== null) {
|
|
85
|
+
clearTimeout(this.eventsToDebounce[type].timeout);
|
|
86
|
+
}
|
|
87
|
+
this.eventsToDebounce[type].timeout = setTimeout(later, this.eventsToDebounce[type].debouncePeriod);
|
|
88
|
+
}
|
|
89
|
+
/** Sets up the page hide handler to try to push remaining events in the queues */
|
|
90
|
+
_setupUnloadHandler() {
|
|
91
|
+
addSafeEventListener(window, 'pagehide', () => {
|
|
92
|
+
Object.values(this.eventsToDebounce).forEach(eventObject => {
|
|
93
|
+
PageVisit.getInstance().addPageVisitEvents(eventObject.events, eventObject.eventName);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { EventDebouncer };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isValidURL, getJSStack, stringifyJSON, getMaxSubstringAllowed, asString } from '../utils/function.js';
|
|
2
2
|
import { EVENT_ERROR_TYPE, URL_ATT_NAME, BLOCKLISTED_DOMAINS, ERROR_EVENT_TYPE, ERROR_EVENT_ERROR_TYPE, CUSTOM_ERROR_EVENT_TYPE, ERROR_EVENT_UNHANDLED_REJECTION_TYPE, ERROR_LOG_EVENT_ERROR_TYPE, FETCH_EXCEPTION_ERROR_TYPE, WRAPPED_EXCEPTION_ERROR_TYPE, GQL_ERROR_TYPE, RESPONSE_ERROR_TYPE, XML_HTTP_REQUEST_ERROR_TYPE, ERROR_SOURCE_ATT_NAME, TYPE_ATT_NAME, JS_EVENT_TYPE, JS_ERROR_ATT_NAME, JS_STACK_FRAMES_ATT_NAME, JS_STACK_FILE_ATT_NAME, JS_STACK_METHOD_ATT_NAME, SEVERITY, JS_STACK_MESSAGE_ATT_NAME, HTTP_EVENT_TYPE, NOIBU_INPUT_URLS, HTTP_CODE_ATT_NAME, PV_SEQ_ATT_NAME, GQL_EVENT_TYPE, GQL_ERROR_ATT_NAME } from '../constants.js';
|
|
3
3
|
import ClientConfig from '../api/clientConfig.js';
|
|
4
|
-
import { InputMonitor } from '../monitors/inputMonitor.js';
|
|
5
4
|
import StoredMetrics from '../api/storedMetrics.js';
|
|
5
|
+
import { EventDebouncer } from './EventDebouncer.js';
|
|
6
6
|
|
|
7
7
|
/** @module PageVisitEventError */
|
|
8
8
|
|
|
@@ -314,7 +314,7 @@ function saveErrorToPagevisit(type, payload, httpDataSeqNum) {
|
|
|
314
314
|
// we register an error event
|
|
315
315
|
StoredMetrics.getInstance().addError();
|
|
316
316
|
// debounce event
|
|
317
|
-
|
|
317
|
+
EventDebouncer.getInstance().addEvent(pvError, ERROR_EVENT_TYPE);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
export { getOnURL, isErrorCollectedByNoibu, saveErrorToPagevisit };
|