noibu-react-native 0.2.34-rc.1 → 0.2.34-rc.3

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/dist/constants.js CHANGED
@@ -24,7 +24,7 @@ const CONTENT_TYPE = 'content-type';
24
24
  * Gets the script id from the cookie object, returns default if cannot be found
25
25
  */
26
26
  function GET_SCRIPT_ID() {
27
- return "1.0.104-rn-sdk-0.2.34-rc.1" ;
27
+ return "1.0.104-rn-sdk-0.2.34-rc.3" ;
28
28
  }
29
29
  /**
30
30
  * Gets the max metro recon number
@@ -76,17 +76,23 @@ function globalInit(customerConfig) {
76
76
  const clickMonitor = ClickMonitor.getInstance();
77
77
  const pageMonitor = PageMonitor.getInstance();
78
78
  AppNavigationMonitor.getInstance().monitor();
79
- // Only initialize HTTP data collection and request monitoring if enabled
80
- if (ClientConfig.getInstance().enableHttpDataCollection) {
81
- HTTPDataBundler.getInstance();
82
- RequestMonitor.getInstance().monitor();
83
- }
84
- // monitoring calls
79
+ // monitoring calls - each wrapped independently so one failure doesn't affect others
85
80
  ErrorMonitor.getInstance().monitor();
86
81
  clickMonitor.monitor();
87
82
  keyboardInputMonitor.monitor();
88
83
  pageMonitor.monitor();
89
84
  SessionRecorder.getInstance().recordUserSession();
85
+ // Initialize HTTP data collection and request monitoring if enabled
86
+ // This is done after other monitors so failures here don't affect session recording
87
+ if (ClientConfig.getInstance().enableHttpDataCollection) {
88
+ try {
89
+ HTTPDataBundler.getInstance();
90
+ RequestMonitor.getInstance().monitor();
91
+ }
92
+ catch (error) {
93
+ ClientConfig.getInstance().postInternalError({ msg: `Error initializing HTTP data collection`, error }, false, Severity.ERROR);
94
+ }
95
+ }
90
96
  if (metroplexSocket.connectionPromise) {
91
97
  metroplexSocket.connectionPromise.catch((error) => ClientConfig.getInstance().postInternalError({ msg: `Error during metroplexSocket initial connection`, error }, false, Severity.ERROR));
92
98
  }
@@ -1,4 +1,4 @@
1
- import { asString, isStackTrace } from '../utils/function.js';
1
+ import { isStackTrace, asString } from '../utils/function.js';
2
2
  import { saveErrorToPagevisit } from '../pageVisit/pageVisitEventError.js';
3
3
  import { replace } from '../utils/object.js';
4
4
  import { Singleton } from './BaseMonitor.js';
@@ -3,14 +3,12 @@ import { Monitor, Singleton } from './BaseMonitor';
3
3
  export default class RequestMonitor extends Singleton implements Monitor {
4
4
  /** main method */
5
5
  monitor(): void;
6
- /**
7
- * Gets a response text Promise out of Response & headers. Returns reason if bundler decides to drop text.
8
- * Due to async nature of text() and async function sent to Promise.all,
9
- * our code race competes with client's code consuming the response.
10
- * So at this point engine somehow gets access to the original ReadableStream and marks it as locked,
11
- * thus client code may throw an error.
12
- */
13
- private static consumeResponseAndGetBodyText;
6
+ /** Decodes an ArrayBuffer into UTF-8 text. */
7
+ private static decodeArrayBufferToText;
8
+ /** Reads the response body once and returns both raw body and decoded text. */
9
+ private static readResponseBody;
10
+ /** Creates a fresh Response for the customer, preserving metadata. */
11
+ private static buildCustomerResponse;
14
12
  /** Generates new PVEventHTTP */
15
13
  private static getPvEventHttp;
16
14
  /** Gets headers from request or request options. Handles different scenarios where simple conversion is not possible */
@@ -24,10 +22,8 @@ export default class RequestMonitor extends Singleton implements Monitor {
24
22
  private static getRequestBody;
25
23
  /** Returns the parameters to pass the HttpEventManager constructor. Specific to the fetch wrapper. */
26
24
  private static getHttpDataFromFetch;
27
- /** Clones Response / Request or returns original object if cloning is not possible */
28
- private static cloneResponse;
29
25
  /** Handles fetch response safely. Reports HttpEventManager and error to Pagevisit if needed. */
30
- private static safeHandleFetchResponse;
26
+ private static handleFetchResponse;
31
27
  /** Safe function to get method and url from fetch args */
32
28
  private static getMethodUrlFromFetchArgs;
33
29
  /**
@@ -5,7 +5,6 @@ import { safeEntries, propWriteableOrMadeWriteable, replace } from '../utils/obj
5
5
  import ClientConfig from '../api/ClientConfig.js';
6
6
  import { noibuLog } from '../utils/log.js';
7
7
  import { tryGetStackTrace, safeTrim } from '../utils/function.js';
8
- import { promiseAll } from '../utils/polyfills.js';
9
8
  import { addSafeEventListener } from '../utils/eventlistener.js';
10
9
  import { HTTPDataBundler } from './http-tools/HTTPDataBundler.js';
11
10
  import GqlErrorValidator from './http-tools/GqlErrorValidator.js';
@@ -23,24 +22,100 @@ class RequestMonitor extends Singleton {
23
22
  RequestMonitor.setupGlobalXMLHttpWrapper();
24
23
  noibuLog('monitorRequests ended');
25
24
  }
26
- /**
27
- * Gets a response text Promise out of Response & headers. Returns reason if bundler decides to drop text.
28
- * Due to async nature of text() and async function sent to Promise.all,
29
- * our code race competes with client's code consuming the response.
30
- * So at this point engine somehow gets access to the original ReadableStream and marks it as locked,
31
- * thus client code may throw an error.
32
- */
33
- static consumeResponseAndGetBodyText(response) {
25
+ /** Decodes an ArrayBuffer into UTF-8 text. */
26
+ static decodeArrayBufferToText(buffer) {
27
+ try {
28
+ if (typeof TextDecoder !== 'undefined') {
29
+ return new TextDecoder('utf-8').decode(buffer);
30
+ }
31
+ }
32
+ catch (_a) {
33
+ // ignore and fall back to manual decode
34
+ }
35
+ const bytes = new Uint8Array(buffer);
36
+ const chunkSize = 0x8000;
37
+ let result = '';
38
+ for (let i = 0; i < bytes.length; i += chunkSize) {
39
+ const chunk = bytes.subarray(i, i + chunkSize);
40
+ result += String.fromCharCode.apply(null, Array.from(chunk));
41
+ }
42
+ return result;
43
+ }
44
+ /** Reads the response body once and returns both raw body and decoded text. */
45
+ static readResponseBody(response) {
34
46
  return __awaiter(this, void 0, void 0, function* () {
35
47
  if (!(response instanceof Response)) {
36
- return '';
48
+ return null;
49
+ }
50
+ const tryRead = (target) => __awaiter(this, void 0, void 0, function* () {
51
+ try {
52
+ const buffer = yield target.arrayBuffer();
53
+ return { body: buffer, text: RequestMonitor.decodeArrayBufferToText(buffer) };
54
+ }
55
+ catch (_a) {
56
+ // Fall back to text() if arrayBuffer() is unsupported or fails
57
+ try {
58
+ const text = yield target.text();
59
+ return { body: text, text };
60
+ }
61
+ catch (_b) {
62
+ return null;
63
+ }
64
+ }
65
+ });
66
+ // Prefer reading from a clone to avoid mutating the original Response.
67
+ try {
68
+ if (!response.bodyUsed && typeof response.clone === 'function') {
69
+ const clone = response.clone();
70
+ const clonedResult = yield tryRead(clone);
71
+ if (clonedResult) {
72
+ return clonedResult;
73
+ }
74
+ }
75
+ }
76
+ catch (_a) {
77
+ // ignore and fall back to reading the original response
37
78
  }
38
79
  if (response.bodyUsed) {
39
- return BODY_USED_ERROR;
80
+ return null;
40
81
  }
41
- return response.text();
82
+ return tryRead(response);
42
83
  });
43
84
  }
85
+ /** Creates a fresh Response for the customer, preserving metadata. */
86
+ static buildCustomerResponse(ogResponse, body) {
87
+ try {
88
+ const headers = new Headers(ogResponse.headers);
89
+ const customerResponse = new Response(body, {
90
+ status: ogResponse.status,
91
+ statusText: ogResponse.statusText,
92
+ headers,
93
+ });
94
+ // Best-effort copy of read-only metadata (works in some polyfills).
95
+ try {
96
+ Object.defineProperty(customerResponse, 'url', { value: ogResponse.url, configurable: true });
97
+ }
98
+ catch (_a) {
99
+ // ignore
100
+ }
101
+ try {
102
+ Object.defineProperty(customerResponse, 'redirected', { value: ogResponse.redirected, configurable: true });
103
+ }
104
+ catch (_b) {
105
+ // ignore
106
+ }
107
+ try {
108
+ Object.defineProperty(customerResponse, 'type', { value: ogResponse.type, configurable: true });
109
+ }
110
+ catch (_c) {
111
+ // ignore
112
+ }
113
+ return customerResponse;
114
+ }
115
+ catch (_d) {
116
+ return ogResponse;
117
+ }
118
+ }
44
119
  /** Generates new PVEventHTTP */
45
120
  static getPvEventHttp(status, responseHeaders, method, url, responseTime) {
46
121
  const httpEvent = {
@@ -87,16 +162,9 @@ class RequestMonitor extends Singleton {
87
162
  return HTTPDataBundler.getInstance().bundleHTTPData(url, RequestMonitor.getRequestHeaders(request, options), RequestMonitor.getRequestBody(requestText, options), RequestMonitor.getResponseHeaders(response), responseText, method, isError);
88
163
  });
89
164
  }
90
- /** Clones Response / Request or returns original object if cloning is not possible */
91
- static cloneResponse(ogResponse) {
92
- if (!ogResponse.bodyUsed) {
93
- return ogResponse.clone();
94
- }
95
- return ogResponse; // body consumed already, no http response body for us
96
- }
97
165
  /** Handles fetch response safely. Reports HttpEventManager and error to Pagevisit if needed. */
98
- static safeHandleFetchResponse(startTime, url, method, options, request) {
99
- return (ogResponse) => __awaiter(this, void 0, void 0, function* () {
166
+ static handleFetchResponse(startTime, url, method, options, request, ogResponse, responseText, shouldCaptureBody, shouldCheckGql) {
167
+ return __awaiter(this, void 0, void 0, function* () {
100
168
  var _a;
101
169
  try {
102
170
  if (!ogResponse) {
@@ -104,31 +172,26 @@ class RequestMonitor extends Singleton {
104
172
  // logging to track the issue if it becomes widespread
105
173
  return ClientConfig.getInstance().postInternalError({ msg: 'No response object in fetch callback', url, method, options, request }, false, Severity.ERROR);
106
174
  }
107
- const clonedResponse = RequestMonitor.cloneResponse(ogResponse); // have to do it as early as possible
108
- const graphqlResponse = RequestMonitor.cloneResponse(clonedResponse); // and two times more for each consumption
109
- const httpEventDataResponse = RequestMonitor.cloneResponse(clonedResponse);
175
+ // Read metadata directly from ogResponse (sync, does not consume body)
110
176
  const respTime = Date.now() - startTime;
111
- const status = clonedResponse.status;
112
- const headers = clonedResponse.headers;
113
- const gqlError = yield GqlErrorValidator.fromFetch(url, options, request, graphqlResponse);
177
+ const status = ogResponse.status;
178
+ const headers = ogResponse.headers;
179
+ const gqlError = shouldCheckGql && responseText
180
+ ? yield GqlErrorValidator.fromFetchResponseText(url, options, request, status, responseText)
181
+ : null;
114
182
  const isHttpError = isHttpCodeFailure(status);
115
- // Only consume request/response bodies if HTTP data collection is enabled for this URL
116
- // This prevents bodyUsed from being set to true when customers don't want body capture
117
- const shouldCaptureBody = HTTPDataBundler.getInstance().shouldContinueForURL(url);
118
- const [maybeRequestText, responseText] = shouldCaptureBody
119
- ? yield promiseAll([
120
- Promise.resolve((_a = request === null || request === void 0 ? void 0 : request.text) === null || _a === void 0 ? void 0 : _a.call(request)),
121
- RequestMonitor.consumeResponseAndGetBodyText(httpEventDataResponse),
122
- ])
123
- : [undefined, undefined];
183
+ const maybeRequestText = shouldCaptureBody ? yield Promise.resolve((_a = request === null || request === void 0 ? void 0 : request.text) === null || _a === void 0 ? void 0 : _a.call(request)) : undefined;
184
+ const responseTextForHttp = shouldCaptureBody
185
+ ? responseText !== null && responseText !== void 0 ? responseText : BODY_USED_ERROR
186
+ : undefined;
124
187
  const httpEvent = RequestMonitor.getPvEventHttp(status, headers, method, url, respTime);
125
188
  const httpData = shouldCaptureBody
126
- ? yield RequestMonitor.getHttpDataFromFetch(request, httpEventDataResponse, method, url, options, isHttpError || !!gqlError, maybeRequestText, responseText)
189
+ ? yield RequestMonitor.getHttpDataFromFetch(request, ogResponse, method, url, options, isHttpError || !!gqlError, maybeRequestText, responseTextForHttp)
127
190
  : null;
128
191
  const seq = saveHTTPEvent(httpEvent, httpData, !!gqlError);
129
192
  if (isHttpError) {
130
193
  // this does not consume response body so no need to clone it
131
- saveErrorToPagevisit(Object.assign(Object.assign({}, clonedResponse), { type: PageVisitErrorSource.Response }), seq);
194
+ saveErrorToPagevisit(Object.assign(Object.assign({}, ogResponse), { type: PageVisitErrorSource.Response }), seq);
132
195
  }
133
196
  if (gqlError) {
134
197
  gqlError.forEach(error => saveErrorToPagevisit({
@@ -193,12 +256,33 @@ class RequestMonitor extends Singleton {
193
256
  }
194
257
  const startTime = Date.now();
195
258
  const promiseFromOriginalFetch = originalFunction.call(this, resource, options);
196
- promiseFromOriginalFetch
197
- .then(RequestMonitor.safeHandleFetchResponse(startTime, url, method, options, request))
198
- .catch(err => {
259
+ /**
260
+ * Return a promise that resolves with a response safe for the customer.
261
+ * When HTTP collection or GQL validation need the response body, we read it
262
+ * once and rebuild a fresh Response to avoid mutating the customer's bodyUsed.
263
+ */
264
+ const customerPromise = promiseFromOriginalFetch.then((ogResponse) => __awaiter(this, void 0, void 0, function* () {
265
+ const shouldCheckGql = GqlErrorValidator.shouldCheckRequest(url, options, request);
266
+ const shouldCaptureBody = HTTPDataBundler.getInstance().shouldContinueForURL(url);
267
+ const shouldReadBody = shouldCaptureBody || shouldCheckGql;
268
+ let customerResponse = ogResponse;
269
+ let responseText;
270
+ if (shouldReadBody) {
271
+ const buffered = yield RequestMonitor.readResponseBody(ogResponse);
272
+ if (buffered) {
273
+ responseText = buffered.text;
274
+ customerResponse = RequestMonitor.buildCustomerResponse(ogResponse, buffered.body);
275
+ }
276
+ }
277
+ RequestMonitor.handleFetchResponse(startTime, url, method, options, request, ogResponse, responseText, shouldCaptureBody, shouldCheckGql).catch(() => {
278
+ // errors are handled inside handleFetchResponse
279
+ });
280
+ return customerResponse;
281
+ }));
282
+ customerPromise.catch(err => {
199
283
  RequestMonitor.handleFetchFailure(err, url);
200
284
  });
201
- return promiseFromOriginalFetch;
285
+ return customerPromise;
202
286
  };
203
287
  });
204
288
  }
@@ -1,6 +1,10 @@
1
1
  import { GQLError } from 'noibu-metroplex-ts-bindings';
2
2
  /** Try detecting GraphQL errors from http response */
3
3
  export default class GqlErrorValidator {
4
+ /** Determines if a request should be checked for GQL errors. */
5
+ static shouldCheckRequest(url: string, options?: RequestInit, request?: Request): boolean;
6
+ /** Retrieves GQL error object based on response body text */
7
+ static fromFetchResponseText(url: string, options: RequestInit | undefined, request: Request | undefined, status: number, responseText: string | null | undefined): Promise<GQLError[] | null>;
4
8
  /** Retrieves GQL error object based on fetch request/response */
5
9
  static fromFetch(url: string, options: RequestInit | undefined, request: Request | undefined, response: Response): Promise<GQLError[] | null>;
6
10
  /** Retrieves GQL error object based on XHR object */
@@ -15,11 +15,43 @@ const MESSAGE_MAX_LENGTH = 1000;
15
15
  /* eslint-disable no-param-reassign */
16
16
  /** Try detecting GraphQL errors from http response */
17
17
  class GqlErrorValidator {
18
+ /** Determines if a request should be checked for GQL errors. */
19
+ static shouldCheckRequest(url, options, request) {
20
+ const contentType = this._getContentTypeFromFetchArguments(options, request);
21
+ return this._shouldHandleRequest(url, contentType);
22
+ }
23
+ /** Retrieves GQL error object based on response body text */
24
+ static fromFetchResponseText(url, options, request, status, responseText) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ try {
27
+ if (!responseText) {
28
+ return null;
29
+ }
30
+ const isResponseValid = status >= 200 && status <= 299;
31
+ if (!isResponseValid) {
32
+ return null;
33
+ }
34
+ const contentType = this._getContentTypeFromFetchArguments(options, request);
35
+ if (this._shouldHandleRequest(url, contentType)) {
36
+ const data = this._parseJsonSafely(responseText);
37
+ if (data) {
38
+ return this._validate(data, []);
39
+ }
40
+ }
41
+ }
42
+ catch (e) {
43
+ if (!this._isRequestAborted(options, request)) {
44
+ this._postError(e);
45
+ }
46
+ }
47
+ return null;
48
+ });
49
+ }
18
50
  /** Retrieves GQL error object based on fetch request/response */
19
51
  static fromFetch(url, options, request, response) {
20
52
  return __awaiter(this, void 0, void 0, function* () {
21
53
  try {
22
- const isResponseValid = isInstanceOf(response, Response) && response.ok;
54
+ const isResponseValid = isInstanceOf(response, Response) && response.ok && !response.bodyUsed;
23
55
  if (!isResponseValid) {
24
56
  return null;
25
57
  }
@@ -11,8 +11,6 @@ import { Severity, MetroplexMessageType } from 'noibu-metroplex-ts-bindings';
11
11
 
12
12
  // custom event name for posting metrics
13
13
  const POST_METRICS_EVENT_NAME = 'noibuPostMetrics';
14
- // the max amount of time to wait for user events until freezing rrweb mutation events
15
- const MAX_TIME_FOR_RECORDER_USER_EVENTS = 2000;
16
14
  // Maximum number of events in the RRWEB session recorder buffer
17
15
  // before sending to Metroplex
18
16
  const MAX_RECORDER_EVENT_BUFFER = 10;
@@ -105,22 +103,13 @@ class SessionRecorder extends Singleton {
105
103
  this.freeze();
106
104
  return;
107
105
  }
108
- // determine if timeout should be extended based on event/source type
109
- // event type 3 is an incremental snapshot
110
- // event data source 0 is a mutation (all other data sources are user events)
106
+ // Note: Automatic freeze timeout is disabled for React Native.
107
+ // The native SDK handles its own event throttling/batching.
108
+ // Freezing is still triggered by closeIfInactive() and didCutVideo checks above.
111
109
  if (this.pauseTimeout) {
112
- // received a user event, extend the timeout
113
110
  clearTimeout(this.pauseTimeout);
114
111
  this.freezingEvents = false;
115
112
  }
116
- this.pauseTimeout = setTimeout(() => {
117
- // stop recording page mutations after 2s of inactivity
118
- // otherwise sites with many mutations will hit max video size
119
- // in a short amount of time without any user events
120
- this.freezingEvents = true;
121
- // freezePage stops emitting events until the next user event is received
122
- this.freeze();
123
- }, MAX_TIME_FOR_RECORDER_USER_EVENTS);
124
113
  // Set the first recorded timestamp if it hasn't been set yet.
125
114
  // We usually only want this to be set once as the first recorded timestamp
126
115
  // should not change.
@@ -86,7 +86,6 @@ function subscribeToNativeEvent(callback) {
86
86
  try {
87
87
  const batch = yield NativeSessionRecorder.consumeEvents();
88
88
  if (Array.isArray(batch) && batch.length > 0) {
89
- noibuLog("Actual batch: ", batch);
90
89
  for (const message of batch) {
91
90
  try {
92
91
  const _a = JSON.parse(message), { data } = _a, rest = __rest(_a, ["data"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noibu-react-native",
3
- "version": "0.2.34-rc.1",
3
+ "version": "0.2.34-rc.3",
4
4
  "targetNjsVersion": "1.0.104",
5
5
  "description": "React-Native SDK for NoibuJS to collect errors in React-Native applications",
6
6
  "main": "dist/entry/index.js",
@@ -1,24 +0,0 @@
1
- var _a, _b;
2
- /**
3
- * In case Promise.all is not available, use this polyfill
4
- */
5
- const promiseAll = ((_b = (_a = Promise === null || Promise === void 0 ? void 0 : Promise.all) === null || _a === void 0 ? void 0 : _a.bind) === null || _b === void 0 ? void 0 : _b.call(_a, Promise)) ||
6
- ((values) => {
7
- return new Promise(function (resolve, reject) {
8
- const result = [];
9
- let total = 0;
10
- values.forEach((item, index) => {
11
- Promise.resolve(item)
12
- .then(res => {
13
- result[index] = res;
14
- total += 1;
15
- if (total === values.length) {
16
- resolve(result);
17
- }
18
- })
19
- .catch(reject);
20
- });
21
- });
22
- });
23
-
24
- export { promiseAll };