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.
Files changed (40) hide show
  1. package/README.md +155 -0
  2. package/dist/api/clientConfig.js +416 -0
  3. package/dist/api/helpCode.js +106 -0
  4. package/dist/api/inputManager.js +233 -0
  5. package/dist/api/metroplexSocket.js +882 -0
  6. package/dist/api/storedMetrics.js +201 -0
  7. package/dist/api/storedPageVisit.js +235 -0
  8. package/dist/const_matchers.js +260 -0
  9. package/dist/constants.d.ts +264 -0
  10. package/dist/constants.js +528 -0
  11. package/dist/entry/index.d.ts +8 -0
  12. package/dist/entry/index.js +15 -0
  13. package/dist/entry/init.js +91 -0
  14. package/dist/monitors/clickMonitor.js +284 -0
  15. package/dist/monitors/elementMonitor.js +174 -0
  16. package/dist/monitors/errorMonitor.js +295 -0
  17. package/dist/monitors/gqlErrorValidator.js +306 -0
  18. package/dist/monitors/httpDataBundler.js +665 -0
  19. package/dist/monitors/inputMonitor.js +130 -0
  20. package/dist/monitors/keyboardInputMonitor.js +67 -0
  21. package/dist/monitors/locationChangeMonitor.js +30 -0
  22. package/dist/monitors/pageMonitor.js +119 -0
  23. package/dist/monitors/requestMonitor.js +679 -0
  24. package/dist/pageVisit/pageVisit.js +172 -0
  25. package/dist/pageVisit/pageVisitEventError/pageVisitEventError.js +313 -0
  26. package/dist/pageVisit/pageVisitEventHTTP/pageVisitEventHTTP.js +115 -0
  27. package/dist/pageVisit/userStep/userStep.js +20 -0
  28. package/dist/react/ErrorBoundary.d.ts +72 -0
  29. package/dist/react/ErrorBoundary.js +102 -0
  30. package/dist/storage/localStorageProvider.js +23 -0
  31. package/dist/storage/rnStorageProvider.js +62 -0
  32. package/dist/storage/sessionStorageProvider.js +23 -0
  33. package/dist/storage/storage.js +119 -0
  34. package/dist/storage/storageProvider.js +83 -0
  35. package/dist/utils/date.js +62 -0
  36. package/dist/utils/eventlistener.js +67 -0
  37. package/dist/utils/function.js +398 -0
  38. package/dist/utils/object.js +144 -0
  39. package/dist/utils/performance.js +21 -0
  40. package/package.json +57 -0
@@ -0,0 +1,882 @@
1
+ import uuid from 'react-native-uuid';
2
+ import { getProperGlobalUrl, getMaxSubstringAllowed, getUserAgent, stringifyJSON, getUserLanguage } from '../utils/function.js';
3
+ import { addSafeEventListener } from '../utils/eventlistener.js';
4
+ import { GET_METROPLEX_BASE_SOCKET_URL, METROPLEX_FRAG_ROUTE, GET_METROPLEX_POST_URL, METROPLEX_RETRY_FREQUENCY, META_DATA_METROPLEX_TYPE, PAGE_VISIT_META_DATA_ATT_NAME, HTTP_DATA_METROPLEX_TYPE, PAGE_VISIT_HTTP_DATA_ATT_NAME, VIDEO_METROPLEX_TYPE, PAGE_VISIT_VID_FRAG_ATT_NAME, PV_METROPLEX_TYPE, PAGE_VISIT_PART_ATT_NAME, SEQ_NUM_ATT_NAME, WORK_REQUEST_ATT_NAME, PV_EVENTS_ATT_NAME, TYPE_ATT_NAME, USERSTEP_EVENT_TYPE, GET_MAX_METROPLEX_RECONNECTION_NUMBER, MAX_METROPLEX_CONNECTION_COUNT, GET_METROPLEX_CONSECUTIVE_CONNECTION_DELAY, END_AT_ATT_NAME, SEVERITY_ERROR, OK_SOCKET_MESSAGE, CLOSE_CONNECTION_FORCEFULLY, BLOCK_SOCKET_MESSAGE, STOP_STORING_PV_SOCKET_MESSAGE, STOP_STORING_VID_SOCKET_MESSAGE, MAX_BEACON_PAYLOAD_SIZE, PAGE_VISIT_INFORMATION_ATT_NAME, VIDEO_PART_COUNT_ATT_NAME, IS_LAST_ATT_NAME, SEVERITY_WARN, JS_ENV, BROWSER_ID_ATT_NAME, PV_ID_ATT_NAME, VER_ATT_NAME, CURRENT_PV_VERSION, PV_SEQ_ATT_NAME, ON_URL_ATT_NAME, REF_URL_ATT_NAME, STARTED_AT_ATT_NAME, CONN_COUNT_ATT_NAME, COLLECT_VER_ATT_NAME, CURRENT_NOIBUJS_VERSION, SCRIPT_ID_ATT_NAME, GET_SCRIPT_ID, SCRIPT_INSTANCE_ID_ATT_NAME, METROPLEX_SOCKET_INSTANCE_ID_ATT_NAME, SOCKET_INSTANCE_ID_ATT_NAME, LANG_ATT_NAME, HELP_CODE_ATT_NAME } from '../constants.js';
5
+ import ClientConfig from './clientConfig.js';
6
+ import StoredMetrics from './storedMetrics.js';
7
+ import StoredPageVisit from './storedPageVisit.js';
8
+ import { safePerformanceNow } from '../utils/performance.js';
9
+ import { isDateOverwritten } from '../utils/date.js';
10
+ import HelpCode from './helpCode.js';
11
+
12
+ /** @module MetroplexSocket */
13
+
14
+ /** Manages the socket to Metroplex */
15
+ class MetroplexSocket {
16
+ /**
17
+ * Creates an instance of metroplex
18
+ * @param {} scriptInstanceId id of script, to make sure only a single socket is open
19
+ */
20
+ constructor(scriptInstanceId) {
21
+ const socketBase = GET_METROPLEX_BASE_SOCKET_URL();
22
+
23
+ // this flag is set to true when we manually need to close the
24
+ // socket. It happens currently during the pagehide event and
25
+ // if we havent sent a message to our backend in some time.
26
+ this.forceClosed = false;
27
+ // the socket used to transmit all data to metroplex
28
+ this.socket = null;
29
+ // the unique instance id of the socket
30
+ this.socketInstanceId = null;
31
+
32
+ // previous message type sent to metroplex
33
+ this.previousMessageType = '';
34
+ // how many times we tried reconnecting to metroplex
35
+ this.currentConnectionAttempts = 0;
36
+ // how many successful connections to metroplex
37
+ this.connectionCount = 0;
38
+ // sessiont start time, used to calculate accurate end time
39
+ this.sessionStartTime = safePerformanceNow();
40
+ // promise that will resolve once the socket is connected and metroplex has acknowledged
41
+ this.connectionPromise = null;
42
+ // Whether or not we have sent the page visit information after connecting the socket
43
+ this.pageVisitInfoSent = false;
44
+ // socket connection url
45
+ this.connectionURL = `${socketBase}/${METROPLEX_FRAG_ROUTE}`;
46
+ // post endpoint for the same events we would send to the socket
47
+ this.postURL = GET_METROPLEX_POST_URL();
48
+ // sequence number of the message sent to metroplex
49
+ this.messageSequenceNum = 0;
50
+ // the latest received sequence number from metroplex
51
+ this.latestReceivedSeqNumber = -1;
52
+ // set to true only when we start running behind on messages in metroplex
53
+ this.isRetryLoopDisabled = false;
54
+ // messages that need to be resent to metroplex since they are lacking confirmation
55
+ this.retryMessageQueue = [];
56
+ // this map will hold all types that noibujs should not send to metroplex
57
+ this.metroplexTypeLock = {};
58
+ // setting initial URL at the start in order to guarentee that the
59
+ // current page visit has the real initial onURL. Fragments and SPA's
60
+ // can change the URL without reloading the page.
61
+ this.initialURL = getProperGlobalUrl();
62
+ this.initialReferingURL =
63
+ global.document && global.document.referrer
64
+ ? getMaxSubstringAllowed(global.document.referrer)
65
+ : '';
66
+ this.sessionTimestamp = new Date();
67
+
68
+ // latest time that we received a confirmation message from metroplex
69
+ this.latestReceivedSeqNumStoredTime = new Date();
70
+
71
+ // unique instance id of Metroplex Socket
72
+ this.instanceId = uuid.v4();
73
+
74
+ // unique script instance id
75
+ this.scriptInstanceId = scriptInstanceId;
76
+
77
+ // length of the session based on page visit events
78
+ this.sessionLength = 0;
79
+
80
+ // track socket closure codes for debug
81
+ this.socketCloseCodes = [];
82
+
83
+ // track socket open times
84
+ this.socketOpens = [];
85
+
86
+ // flag to indicate Metroplex has acknowledged at least once
87
+ this.ackedOnce = false;
88
+
89
+ // retry frequency in ms
90
+ this.metroRetryFrequencyMS = METROPLEX_RETRY_FREQUENCY;
91
+ }
92
+
93
+ /**
94
+ * gets the singleton instance
95
+ * @param {} [scriptInstanceId]
96
+ * @returns {MetroplexSocket}
97
+ */
98
+ static getInstance(scriptInstanceId) {
99
+ if (!this.instance) {
100
+ this.instance = new MetroplexSocket(scriptInstanceId);
101
+ this.instance.start();
102
+ }
103
+
104
+ return this.instance;
105
+ }
106
+
107
+ /** Starts off the socket connection */
108
+ start() {
109
+ // Connect the WS
110
+ this.connectSocket();
111
+ // Set up the offload events immediately
112
+ this._setupOffloadEvents();
113
+ }
114
+
115
+ /**
116
+ * Adds the seq num field to the given payload depending on whether its
117
+ * a page visit part or video frag
118
+ * @param {} type
119
+ * @param {} payload
120
+ */
121
+ _addSeqNumToPayload(type, payload) {
122
+ switch (type) {
123
+ case PV_METROPLEX_TYPE:
124
+ this._setSeqNumInPayloadAndIncrementSeqNum(
125
+ PAGE_VISIT_PART_ATT_NAME,
126
+ payload,
127
+ );
128
+ break;
129
+ case VIDEO_METROPLEX_TYPE:
130
+ this._setSeqNumInPayloadAndIncrementSeqNum(
131
+ PAGE_VISIT_VID_FRAG_ATT_NAME,
132
+ payload,
133
+ );
134
+ break;
135
+ case HTTP_DATA_METROPLEX_TYPE:
136
+ this._setSeqNumInPayloadAndIncrementSeqNum(
137
+ PAGE_VISIT_HTTP_DATA_ATT_NAME,
138
+ payload,
139
+ );
140
+ break;
141
+ case META_DATA_METROPLEX_TYPE:
142
+ this._setSeqNumInPayloadAndIncrementSeqNum(
143
+ PAGE_VISIT_META_DATA_ATT_NAME,
144
+ payload,
145
+ );
146
+ break;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * sets the seq num in the payload for the given key and increments the
152
+ * global seq number
153
+ * @param {} payloadKey
154
+ * @param {} payload
155
+ */
156
+ _setSeqNumInPayloadAndIncrementSeqNum(payloadKey, payload) {
157
+ // eslint-disable-next-line no-param-reassign
158
+ payload[payloadKey][SEQ_NUM_ATT_NAME] = this.messageSequenceNum;
159
+ this.messageSequenceNum += 1;
160
+ }
161
+
162
+ /**
163
+ * Immediately sends a message to Metroplex over the web socket
164
+ * Queues the message if the connection isn't open yet.
165
+ * @param {} type
166
+ * @param {} payload
167
+ * @returns {boolean} true if message was sent succefully, false otherwise
168
+ */
169
+ sendMessage(type, payload) {
170
+ // if we have a lock on this specific type, we dont send it
171
+ if (
172
+ type in this.metroplexTypeLock ||
173
+ ClientConfig.getInstance().isClientDisabled
174
+ ) {
175
+ return false;
176
+ }
177
+
178
+ const payloadData = payload;
179
+
180
+ if (type !== WORK_REQUEST_ATT_NAME) {
181
+ // Increasing the message sequence number for every message we send to metroplex
182
+ // and move the data to the retry queue.
183
+ this._addSeqNumToPayload(type, payloadData);
184
+
185
+ // push the message to the retry queue immediately in case socket isnt connected
186
+ this.retryMessageQueue.push({ payload: payloadData, type });
187
+ StoredPageVisit.getInstance().checkAndStoreRetryQueue(
188
+ this.retryMessageQueue,
189
+ this.getPageInformation(),
190
+ );
191
+ }
192
+
193
+ // send the socket message if we are connected and have sent page visit info
194
+ if (this.isConnected() && this.pageVisitInfoSent) {
195
+ // sending the data to metroplex
196
+ this._sendSocketMessage(payloadData);
197
+ }
198
+
199
+ this.previousMessageType = type;
200
+
201
+ // Only update the last message send if its a page visit with user action
202
+ // ensure this is done regardless of whether socket is connected or not,
203
+ // so that pagehide will post PV if user is active but socket is not
204
+ if (type === PV_METROPLEX_TYPE && payload[PAGE_VISIT_PART_ATT_NAME]) {
205
+ const events = payload[PAGE_VISIT_PART_ATT_NAME][PV_EVENTS_ATT_NAME]
206
+ ? payload[PAGE_VISIT_PART_ATT_NAME][PV_EVENTS_ATT_NAME]
207
+ : [];
208
+
209
+ this._updateLatestPvTimestamp(events);
210
+ }
211
+
212
+ return true;
213
+ }
214
+
215
+ /** Updates the latest pv message sent timestamp if events contain any user steps
216
+ * @param {} events
217
+ */
218
+ _updateLatestPvTimestamp(events) {
219
+ const userstepsEvents = events.filter(
220
+ ev => ev[TYPE_ATT_NAME] === USERSTEP_EVENT_TYPE,
221
+ );
222
+
223
+ if (userstepsEvents.length > 0) {
224
+ ClientConfig.getInstance().updateLastActiveTime(new Date());
225
+ }
226
+ }
227
+
228
+ /** returns true if the socket is either connecting or connected to metroplex */
229
+ isConnected() {
230
+ return this.socket !== null && this.socket.readyState === 1;
231
+ }
232
+
233
+ /** returns true if we are connecting to the socket */
234
+ isConnecting() {
235
+ return this.socket !== null && this.socket.readyState === 0;
236
+ }
237
+
238
+ /** close will close the socket opened for video frag transmission */
239
+ close() {
240
+ this.forceClosed = true;
241
+ // closing the video frag socket only if it was used in the past by
242
+ // the sessionRecorder
243
+ if (this.isConnected() || this.isConnecting()) {
244
+ this.socket.close(1000);
245
+ }
246
+ }
247
+
248
+ /** Connects the web socket to metroplex and calls callback upon successfully connecting
249
+ * @param {} callback
250
+ * @param {} forceOpen
251
+ */
252
+ async handleConnect(callback, forceOpen) {
253
+ // if we are connected or about to connect, we do not reset
254
+ // the socket information
255
+ if (!forceOpen && (this.isConnected() || this.isConnecting())) {
256
+ return;
257
+ }
258
+
259
+ this.currentConnectionAttempts += 1;
260
+ this.socket = new WebSocket(this.connectionURL, undefined, {
261
+ headers: {
262
+ 'User-Agent': await getUserAgent(), // must pass useragent explicitly
263
+ },
264
+ });
265
+ this.socketInstanceId = uuid.v4();
266
+
267
+ this.socket.onerror = () => {
268
+ // use for metrics
269
+ // there is no useful error message/description from this event
270
+ };
271
+
272
+ // once the socket closes
273
+ this.socket.onclose = event => {
274
+ // Reset this to prevent sending messages before the PVI is sent on reconnect
275
+ this.pageVisitInfoSent = false;
276
+
277
+ // we do not reconnect if the page is unloading
278
+ if (this.forceClosed) {
279
+ return;
280
+ }
281
+
282
+ // track socket closure codes and times
283
+ this.socketCloseCodes.push(
284
+ `${!isDateOverwritten() ? new Date().toISOString() : ''}:${event.code}`,
285
+ );
286
+
287
+ // we are already in the process of reconnecting
288
+ // we do not attempt to reconnect yet
289
+ if (this.isConnecting()) {
290
+ return;
291
+ }
292
+
293
+ // Clear the interval that resends unconfirmed msgs so we don't try send while the socket
294
+ // is and starting back up, after which it will be restarted
295
+ clearInterval(this.retryMetroplexInterval);
296
+
297
+ // if we tried reconnecting too many times we abandon
298
+ if (
299
+ this.currentConnectionAttempts >=
300
+ GET_MAX_METROPLEX_RECONNECTION_NUMBER()
301
+ ) {
302
+ // if we tried beyond the threshold, we block ourselves a short
303
+ // while while fix the issue
304
+ ClientConfig.getInstance().lockClientUntilNextPage(
305
+ 'Too many reconnection attempts, locking until next page',
306
+ );
307
+ return;
308
+ }
309
+
310
+ // if we tried reconnecting too many times we abandon
311
+ if (this.connectionCount >= MAX_METROPLEX_CONNECTION_COUNT) {
312
+ // if we tried beyond the threshold, we block ourselves a short
313
+ // while while fix the issue
314
+ ClientConfig.getInstance().lockClientUntilNextPage(
315
+ 'Too many connections, locking until next page',
316
+ );
317
+ return;
318
+ }
319
+
320
+ // try to reconnect on close but after a certain delay based off unsuccessful connections
321
+ setTimeout(() => {
322
+ this.handleConnect(callback, false);
323
+ }, this.currentConnectionAttempts ** 2 * GET_METROPLEX_CONSECUTIVE_CONNECTION_DELAY());
324
+ };
325
+ // everytime we get a message from the socket
326
+ this.socket.onmessage = event => {
327
+ this._onSocketMessage(event, callback);
328
+ };
329
+ // the moment we open the socket
330
+ this.socket.onopen = () => {
331
+ // track socket open codes and times
332
+ this.socketOpens.push(
333
+ `${!isDateOverwritten() ? new Date().toISOString() : ''}`,
334
+ );
335
+ this._onSocketOpen();
336
+ };
337
+ }
338
+
339
+ /**
340
+ * connectSocket will establish a websocket connection to the metroplex
341
+ * service
342
+ */
343
+ connectSocket() {
344
+ if (this.isConnected() || this.isConnecting()) {
345
+ // self resolving promise since we are already ready to send
346
+ // requests, if we did reach state 1, we already sent initial
347
+ // request, thus not sending it again.
348
+ return this.connectionPromise;
349
+ }
350
+ // we return a promisified socket resolver to be able to chain the
351
+ // opening of the socket
352
+ this.connectionPromise = new Promise(resolve => {
353
+ this.handleConnect(resolve, false);
354
+
355
+ // setting up retry reconnection hooks
356
+ // everytime we change make the page hidden this will trigger:
357
+ // tab change, app change, etc
358
+ addSafeEventListener(window, 'visibilitychange', () => {
359
+ // Allow socket to reopen even after forceClosed from the pagehide event
360
+ // Session should be allowed to continue if window becomes visible again
361
+ // However, don't open if client was disabled
362
+ if (ClientConfig.getInstance().isClientDisabled) {
363
+ return;
364
+ }
365
+
366
+ // we always force reconnection when going from hidden to visible
367
+ // since mobile browsers (particularly Safari) are known to silently
368
+ // drop socket connections while in the background
369
+ const visible = global.document.visibilityState === 'visible';
370
+ if (visible) {
371
+ // reset the forceClosed flag in case this happens after pagehide occurred
372
+ this.forceClosed = false;
373
+ if (this.isConnected() || this.isConnecting()) {
374
+ // close the current socket connection and ensure
375
+ // no automatic connection attempted on close
376
+ this.socket.onclose = () => {};
377
+ this.socket.close(1000);
378
+ }
379
+ this.handleConnect(resolve, visible);
380
+ }
381
+ });
382
+ });
383
+
384
+ return this.connectionPromise;
385
+ }
386
+
387
+ /** Calculates and sets the end_at field of the payload
388
+ * @param {} payload
389
+ * @param {} isPageVisit
390
+ */
391
+ addEndTimeToPayload(payload, isPageVisit) {
392
+ const delta = Math.ceil(safePerformanceNow() - this.sessionStartTime);
393
+ // update session length if this is a page visit event
394
+ if (isPageVisit) {
395
+ this.sessionLength = delta;
396
+ }
397
+
398
+ const endTime = new Date(
399
+ this.sessionTimestamp.getTime() + delta,
400
+ ).toISOString();
401
+
402
+ // Assigning this value here is much better than copying the whole object.
403
+ // eslint-disable-next-line no-param-reassign
404
+ payload[END_AT_ATT_NAME] = endTime;
405
+ }
406
+
407
+ /** open handler for socket */
408
+ _onSocketOpen() {
409
+ // if we are not connected, we do not send any messages
410
+ if (!this.isConnected() || ClientConfig.getInstance().isClientDisabled) {
411
+ return;
412
+ }
413
+
414
+ this._sendSocketMessage(this.getPageInformation());
415
+ // Set this to allow normal page visit and video frag messages to be sent
416
+ this.pageVisitInfoSent = true;
417
+ this.currentConnectionAttempts = 0;
418
+ // Resend the pagevisit or video message type before sending data so reset the prev type
419
+ this.previousMessageType = '';
420
+ // attempt to send the unconfirmed messages again
421
+ this._sendUnconfirmedMessages(false);
422
+ // setting up the socket retry mechanism
423
+ this.setupRetryMechanism();
424
+ // increasing the connection count everytime we connect
425
+ this.connectionCount += 1;
426
+ }
427
+
428
+ /** message handler for socket
429
+ * @param {} event
430
+ * @param {} metroAckCallback
431
+ */
432
+ _onSocketMessage(event, metroAckCallback) {
433
+ switch (event.data) {
434
+ case STOP_STORING_VID_SOCKET_MESSAGE:
435
+ // stopping vids from being sent
436
+ this.metroplexTypeLock[VIDEO_METROPLEX_TYPE] = true;
437
+ StoredMetrics.getInstance().setDidCutVideo();
438
+ break;
439
+ case STOP_STORING_PV_SOCKET_MESSAGE:
440
+ // stopping pv from being sent
441
+ this.metroplexTypeLock[PV_METROPLEX_TYPE] = true;
442
+ StoredMetrics.getInstance().setDidCutPv();
443
+ break;
444
+ case BLOCK_SOCKET_MESSAGE:
445
+ // we are disabling collect for 1 day if we get the
446
+ // block message from metroplex. We are probably on it
447
+ ClientConfig.getInstance().lockClient(1440, 'Metroplex blocked script');
448
+ this.close();
449
+ break;
450
+ case CLOSE_CONNECTION_FORCEFULLY:
451
+ this.close();
452
+ break;
453
+ case OK_SOCKET_MESSAGE:
454
+ // we do nothing on an OK
455
+ break;
456
+ default:
457
+ // we now need to check if we receive text that contains
458
+ // the text SEQ_NUM. if that is true, then its a seq num
459
+ // and we need to clear out the retry queue.
460
+ if (event.data.includes(SEQ_NUM_ATT_NAME)) {
461
+ const seqNumSplit = event.data.split(`${SEQ_NUM_ATT_NAME}:`);
462
+ if (seqNumSplit.length < 2) {
463
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
464
+ `Invalid message received from metroplex while clearing retry queue ${event.data}`,
465
+ false,
466
+ SEVERITY_ERROR,
467
+ );
468
+ break;
469
+ }
470
+ // the second element in the string split is the sequence number
471
+ const seqNum = parseInt(seqNumSplit[1], 10);
472
+
473
+ // ignore sequence num if this is just page info, it will always be -1 and we don't want
474
+ // to disable the retry loop on this
475
+ if (seqNum === -1) {
476
+ break;
477
+ }
478
+
479
+ // we disable the retry loop if we start running behind
480
+ if (seqNum <= this.latestReceivedSeqNumber) {
481
+ this.isRetryLoopDisabled = true;
482
+ } else {
483
+ this.isRetryLoopDisabled = false;
484
+
485
+ // setting the latest received seqNum
486
+ this.latestReceivedSeqNumber = seqNum;
487
+
488
+ // clear the retry queue when for this seqNum and below
489
+ this._clearRetryQueue(seqNum);
490
+ }
491
+
492
+ // Metroplex acknowledged at least once, so resolve the promise
493
+ if (!this.ackedOnce && metroAckCallback) {
494
+ this.ackedOnce = true;
495
+ metroAckCallback();
496
+ }
497
+ }
498
+
499
+ if (this._tryProcessHelpCodeResponse(event.data)) {
500
+ break;
501
+ }
502
+
503
+ break;
504
+ // still need to collect the id sent back from metroplex
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Returns true if the message's payload has the payload type given and has a sequence
510
+ * number higher than seqNum
511
+ * @param {} message
512
+ * @param {} payloadType
513
+ * @param {} seqNum
514
+ */
515
+ _messagePayloadHasLargerSeqNum(message, payloadType, seqNum) {
516
+ return (
517
+ message.payload[payloadType] &&
518
+ message.payload[payloadType][SEQ_NUM_ATT_NAME] &&
519
+ message.payload[payloadType][SEQ_NUM_ATT_NAME] > seqNum
520
+ );
521
+ }
522
+
523
+ /**
524
+ * removes messages from the retry queue that are smaller than the
525
+ * latest stored message in metroplex
526
+ * @param {} seqNum
527
+ */
528
+ _clearRetryQueue(seqNum) {
529
+ this.latestReceivedSeqNumStoredTime = new Date();
530
+ this.retryMessageQueue = this.retryMessageQueue.filter(
531
+ message =>
532
+ this._messagePayloadHasLargerSeqNum(
533
+ message,
534
+ PAGE_VISIT_PART_ATT_NAME,
535
+ seqNum,
536
+ ) ||
537
+ this._messagePayloadHasLargerSeqNum(
538
+ message,
539
+ PAGE_VISIT_VID_FRAG_ATT_NAME,
540
+ seqNum,
541
+ ),
542
+ );
543
+ }
544
+
545
+ /** will resend everything that is in the retry queue */
546
+ _sendUnconfirmedMessages(socketWasAlreadyOpen) {
547
+ // we only check once. If the socket is disconnected midway, the events would
548
+ // still be in the retry queue.
549
+ if (!this.isConnected() || ClientConfig.getInstance().isClientDisabled) {
550
+ return;
551
+ }
552
+
553
+ // We always send if this is on socket open
554
+ if (socketWasAlreadyOpen) {
555
+ // we do not send the messages to metroplex if we are falling behind on received
556
+ // sequence numbers
557
+ const someTimeAgo = new Date();
558
+ someTimeAgo.setMilliseconds(
559
+ someTimeAgo.getMilliseconds() - this.metroRetryFrequencyMS,
560
+ );
561
+
562
+ if (someTimeAgo < this.latestReceivedSeqNumStoredTime) {
563
+ return;
564
+ }
565
+
566
+ // we do not resend messages to metroplex if we are running behind
567
+ if (this.isRetryLoopDisabled) {
568
+ return;
569
+ }
570
+ }
571
+
572
+ // removing all the messages with a blocked type from the retry queue
573
+ this.retryMessageQueue = this.retryMessageQueue.filter(
574
+ message => !(message.type in this.metroplexTypeLock),
575
+ );
576
+
577
+ // we dont remove any items from this queue in this function
578
+ for (let i = 0; i < this.retryMessageQueue.length; i += 1) {
579
+ const { type, payload } = this.retryMessageQueue[i];
580
+
581
+ // sending the data to metroplex
582
+ if (!this._sendSocketMessage(payload)) {
583
+ break;
584
+ }
585
+
586
+ this.previousMessageType = type;
587
+ }
588
+ }
589
+
590
+ /** sets up the interval to empty the queue as we receive confirmation messages from metroplex */
591
+ setupRetryMechanism() {
592
+ this.retryMetroplexInterval = setInterval(() => {
593
+ this._sendUnconfirmedMessages(true);
594
+ }, METROPLEX_RETRY_FREQUENCY);
595
+ }
596
+
597
+ /** sets up events that will trigger the event queue to be emptied */
598
+ _setupOffloadEvents() {
599
+ // when the page hides we try getting all the events
600
+ // and empty the event and retry queue.
601
+ addSafeEventListener(window, 'pagehide', () => {
602
+ this._handleUnload();
603
+ });
604
+ }
605
+
606
+ /**
607
+ * will handle the final moments of a page being active. It
608
+ * will try to empty both the queues with beacons.
609
+ */
610
+ _handleUnload() {
611
+ // closing the socket since the page is unloading
612
+ this.close();
613
+
614
+ // if we disable the client within the pagevisit, we do not
615
+ // post anything during the unloading.
616
+ if (ClientConfig.getInstance().isClientDisabled) {
617
+ return;
618
+ }
619
+
620
+ // Don't send the final complete PV if the session has become inactive
621
+ // we already sent the PV when went inactive
622
+ if (ClientConfig.getInstance().isInactive()) {
623
+ return;
624
+ }
625
+
626
+ this.postFullPageVisit(MAX_BEACON_PAYLOAD_SIZE);
627
+ }
628
+
629
+ /**
630
+ * will post full page visit to metroplex. It
631
+ * will try to empty both the queues with beacons.
632
+ * @param {} maxMessageSize
633
+ */
634
+ postFullPageVisit(maxMessageSize) {
635
+ if (this.retryMessageQueue.length === 0) {
636
+ return;
637
+ }
638
+
639
+ // debug counters
640
+ const postInfo = [];
641
+ const numDropped = {
642
+ [VIDEO_METROPLEX_TYPE]: 0,
643
+ [PV_METROPLEX_TYPE]: 0,
644
+ };
645
+
646
+ let currentMsgPayloadSize = 0;
647
+ let currentCompletePv = {
648
+ [PAGE_VISIT_INFORMATION_ATT_NAME]: this.getPageInformation(),
649
+ [PAGE_VISIT_PART_ATT_NAME]: [],
650
+ [PAGE_VISIT_VID_FRAG_ATT_NAME]: [],
651
+ [PAGE_VISIT_HTTP_DATA_ATT_NAME]: [],
652
+ [VIDEO_PART_COUNT_ATT_NAME]: this.connectionCount,
653
+ };
654
+ currentCompletePv[PAGE_VISIT_INFORMATION_ATT_NAME][IS_LAST_ATT_NAME] = true;
655
+
656
+ this.retryMessageQueue.forEach(msg => {
657
+ // can't use two variables types in obj deconstruction
658
+ // eslint-disable-next-line prefer-const
659
+ let { type, payload } = msg;
660
+
661
+ // If the message is larger than the beacon limit then skip to the next message
662
+ const currentPayloadSize = new Blob([stringifyJSON(payload)]).size;
663
+ if (currentPayloadSize > maxMessageSize) {
664
+ // increment dropped count for this type
665
+ numDropped[type] += 1;
666
+ return;
667
+ }
668
+
669
+ currentMsgPayloadSize += currentPayloadSize;
670
+ if (currentMsgPayloadSize >= maxMessageSize) {
671
+ this.postMessage(currentCompletePv);
672
+ // add to post info
673
+ let postInfoMessage = `Vid: ${currentCompletePv[PAGE_VISIT_VID_FRAG_ATT_NAME].length}`;
674
+ postInfoMessage += ` PV: ${currentCompletePv[PAGE_VISIT_PART_ATT_NAME].length}`;
675
+ postInfoMessage += ` HTTP: ${currentCompletePv[PAGE_VISIT_HTTP_DATA_ATT_NAME].length},`;
676
+ postInfo.push(postInfoMessage);
677
+
678
+ // resetting currentCompletePv, since we need to keep adding
679
+ // events to a blank object to send to metroplex.
680
+ // retain the video part count so we don't overwrite anything
681
+ currentCompletePv = {
682
+ [PAGE_VISIT_INFORMATION_ATT_NAME]: this.getPageInformation(),
683
+ [PAGE_VISIT_PART_ATT_NAME]: [],
684
+ [PAGE_VISIT_VID_FRAG_ATT_NAME]: [],
685
+ [PAGE_VISIT_HTTP_DATA_ATT_NAME]: [],
686
+ [VIDEO_PART_COUNT_ATT_NAME]:
687
+ currentCompletePv[VIDEO_PART_COUNT_ATT_NAME],
688
+ };
689
+
690
+ currentCompletePv[PAGE_VISIT_INFORMATION_ATT_NAME][
691
+ IS_LAST_ATT_NAME
692
+ ] = true;
693
+
694
+ // resetting the message size based on what is being added
695
+ currentMsgPayloadSize = currentPayloadSize;
696
+ }
697
+
698
+ switch (type) {
699
+ case VIDEO_METROPLEX_TYPE:
700
+ currentCompletePv[PAGE_VISIT_VID_FRAG_ATT_NAME].push(
701
+ payload[PAGE_VISIT_VID_FRAG_ATT_NAME],
702
+ );
703
+ break;
704
+ case PV_METROPLEX_TYPE:
705
+ currentCompletePv[PAGE_VISIT_PART_ATT_NAME].push(
706
+ payload[PAGE_VISIT_PART_ATT_NAME],
707
+ );
708
+ break;
709
+ case HTTP_DATA_METROPLEX_TYPE:
710
+ currentCompletePv[PAGE_VISIT_HTTP_DATA_ATT_NAME].push(
711
+ payload[PAGE_VISIT_HTTP_DATA_ATT_NAME],
712
+ );
713
+ break;
714
+ }
715
+ });
716
+
717
+ this.postMessage(currentCompletePv);
718
+
719
+ // debug log if large retry message queue
720
+ if (this.retryMessageQueue.length > 100) {
721
+ let postInfoMessage = `Vid: ${currentCompletePv[PAGE_VISIT_VID_FRAG_ATT_NAME].length}`;
722
+ postInfoMessage += ` PV: ${currentCompletePv[PAGE_VISIT_PART_ATT_NAME].length}`;
723
+ postInfoMessage += ` HTTP: ${currentCompletePv[PAGE_VISIT_HTTP_DATA_ATT_NAME].length},`;
724
+ postInfo.push(postInfoMessage);
725
+
726
+ // we completed posted the full pv, send the confirmation debug logs
727
+ // build the debug message
728
+ let message = 'POST Full PV complete';
729
+
730
+ // POST counts
731
+ message += `, POSTs count: ${postInfo.length}`;
732
+ message += `, POSTs info: ${stringifyJSON(postInfo)}`;
733
+
734
+ // Initial retry message queue size
735
+ message += `, Retry message queue size: ${this.retryMessageQueue.length}`;
736
+
737
+ // Num drops
738
+ if (numDropped[VIDEO_METROPLEX_TYPE] > 0) {
739
+ message += `, Video parts dropped: ${numDropped[VIDEO_METROPLEX_TYPE]}`;
740
+ }
741
+ if (numDropped[PV_METROPLEX_TYPE] > 0) {
742
+ message += `, Page visit parts dropped: ${numDropped[PV_METROPLEX_TYPE]}`;
743
+ }
744
+ if (numDropped[HTTP_DATA_METROPLEX_TYPE] > 0) {
745
+ message += `, HTTP data parts dropped: ${numDropped[HTTP_DATA_METROPLEX_TYPE]}`;
746
+ }
747
+
748
+ // Sequence info
749
+ message += `, Sequence Info: Latest ${this.messageSequenceNum}`;
750
+ message += ` Ack'd ${this.latestReceivedSeqNumStoredTime} ${this.latestReceivedSeqNumber}`;
751
+
752
+ // if client was disabled (due to inactive or otherwise) enable briefly so the
753
+ // debug message gets through
754
+ const clientDisabled = ClientConfig.getInstance().isClientDisabled;
755
+ ClientConfig.getInstance().isClientDisabled = false;
756
+ ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
757
+ message,
758
+ clientDisabled,
759
+ SEVERITY_WARN,
760
+ );
761
+ }
762
+ }
763
+
764
+ /**
765
+ * will send a message to metroplex via a post request that will outlive the current page
766
+ * @param {} msg
767
+ */
768
+ postMessage(msg) {
769
+ const updatedMsg = msg;
770
+
771
+ // ensure a unique video part number each call
772
+ updatedMsg[VIDEO_PART_COUNT_ATT_NAME] += 1;
773
+
774
+ // we use the beacon api in testing since the version of chromium we are using
775
+ // does not support the keepalive flag
776
+ if (JS_ENV() === 'test') {
777
+ navigator.sendBeacon(this.postURL, stringifyJSON(updatedMsg));
778
+ // we only write to the pv route if the fetch api is enabled in the current browser
779
+ // and we need to fetch api to set the application/json header for metroplex
780
+ } else if (global.fetch) {
781
+ // we send the remainder elements
782
+ fetch(this.postURL, {
783
+ method: 'POST',
784
+ headers: {
785
+ 'content-type': 'application/json',
786
+ },
787
+ body: stringifyJSON(updatedMsg),
788
+ // keep alive outlives the current page, its the same as beacon
789
+ keepalive: true,
790
+ });
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Stringifies the payload into JSON, sends it to the back end if the session
796
+ * is active and returns true. If inactive the session and socket are closed
797
+ * and this method returns false.
798
+ * @param {} payload
799
+ */
800
+ _sendSocketMessage(payload) {
801
+ if (this.closeIfInactive()) {
802
+ return false;
803
+ }
804
+ this.socket.send(stringifyJSON(payload));
805
+ return true;
806
+ }
807
+
808
+ /**
809
+ * Closes the socket connection if the session is inactive. Returns true if the
810
+ * session is inactive
811
+ */
812
+ closeIfInactive() {
813
+ const inactive = ClientConfig.getInstance().isInactive();
814
+ // if we weren't already inactive, go to inactive state
815
+ if (inactive && !ClientConfig.getInstance().isClientDisabled) {
816
+ // lock the client for 1 minute, since the lock expires
817
+ // only on new page loads this will lock the client until
818
+ // the next page is loaded.
819
+ ClientConfig.getInstance().lockClientUntilNextPage(
820
+ 'Session is inactive, locking until next page',
821
+ );
822
+ this.close();
823
+
824
+ // post metrics now so we don't include events after going inactive
825
+ StoredMetrics.getInstance().postMetrics('inactive');
826
+
827
+ // post retry queue now so we don't include event after going inactive
828
+ this.postFullPageVisit(MAX_BEACON_PAYLOAD_SIZE);
829
+ }
830
+
831
+ return inactive;
832
+ }
833
+
834
+ /** will get page information, calling this will increase the connection count */
835
+ getPageInformation() {
836
+ const pvi = {
837
+ [BROWSER_ID_ATT_NAME]: ClientConfig.getInstance().browserId,
838
+ [PV_ID_ATT_NAME]: ClientConfig.getInstance().pageVisitId,
839
+ [VER_ATT_NAME]: CURRENT_PV_VERSION,
840
+ [PV_SEQ_ATT_NAME]: ClientConfig.getInstance().getPageVisitSeq(),
841
+ [ON_URL_ATT_NAME]: this.initialURL,
842
+ [REF_URL_ATT_NAME]: this.initialReferingURL,
843
+ [STARTED_AT_ATT_NAME]: this.sessionTimestamp.toISOString(),
844
+ [CONN_COUNT_ATT_NAME]: this.connectionCount,
845
+ [COLLECT_VER_ATT_NAME]: CURRENT_NOIBUJS_VERSION,
846
+ [IS_LAST_ATT_NAME]: false,
847
+ [SCRIPT_ID_ATT_NAME]: GET_SCRIPT_ID(),
848
+ [SCRIPT_INSTANCE_ID_ATT_NAME]: this.scriptInstanceId,
849
+ [METROPLEX_SOCKET_INSTANCE_ID_ATT_NAME]: this.instanceId,
850
+ [SOCKET_INSTANCE_ID_ATT_NAME]: this.socketInstanceId,
851
+ };
852
+
853
+ // only setting the user language if we can safely access it
854
+ const userLang = getUserLanguage();
855
+ if (userLang) {
856
+ pvi[LANG_ATT_NAME] = userLang;
857
+ }
858
+
859
+ return pvi;
860
+ }
861
+
862
+ /**
863
+ * Try to parse help code response and fire custom event
864
+ * @param {String} response
865
+ */
866
+ _tryProcessHelpCodeResponse(response) {
867
+ const prefix = `${HELP_CODE_ATT_NAME}:`;
868
+
869
+ if (typeof response !== 'string' || !response.startsWith(prefix)) {
870
+ return false;
871
+ }
872
+
873
+ const data = response.substring(prefix.length);
874
+ const success = /^\d{6}$/.test(data);
875
+
876
+ HelpCode.getInstance().receiveHelpCode({ detail: { success, data } });
877
+
878
+ return true;
879
+ }
880
+ }
881
+
882
+ export { MetroplexSocket as default };