All files / src/scripts/instrumentor/patcher/xhr send.ts

92.3% Statements 24/26
100% Branches 6/6
100% Functions 8/8
91.66% Lines 22/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77                          19x   19x 15x   15x 15x   15x 14x   14x     14x   14x     14x     14x       1x                             14x 14x 14x   14x 10x   10x 7x               14x 14x          
import { PatcherContext } from '../context'
import { XHRWithFingerprintContext, FingerprintContextSymbol } from './types'
import { AGENT_DATA_HEADER } from '../../../../shared/const'
import { logger } from '../../../shared/logger'
 
/**
 * Creates a patched version of the `send` method for `XMLHttpRequest` instances.
 * This allows for the injection and handling of signals in requests based on the provided context.
 *
 * @param {PatcherContext} ctx - The context object containing configurations and methods for signal processing.
 * @return {function} A patched `send` method for `XMLHttpRequest` instances.
 */
export function createPatchedSend(ctx: PatcherContext): typeof XMLHttpRequest.prototype.send {
  const originalSend = XMLHttpRequest.prototype.send
 
  return function patchedSend(this: XHRWithFingerprintContext, body?: Document | XMLHttpRequestBodyInit | null) {
    const sendRequest = () => originalSend.call(this, body)
 
    const fingerprintContext = this[FingerprintContextSymbol]
    const handleSignalsInjectionPromise = fingerprintContext?.handleSignalsInjectionPromise
 
    if (handleSignalsInjectionPromise) {
      let didInjectSignals = false
 
      prepareResponseHandling(this, ctx, () => didInjectSignals)
 
      // Signals' promise is present only in async requests. In that case, we can await the signal promise before sending the request
      handleSignalsInjectionPromise
        .then((didInject) => {
          didInjectSignals = didInject
        })
        .finally(() => {
          sendRequest()
        })
 
      return
    }
 
    // Sync requests are not supported for now
    return sendRequest()
  }
}
 
/**
 * Prepares the handling of the response for the specified XMLHttpRequest by attaching
 * logic to manage agent data and context processing after the request is completed.
 *
 * @param {XMLHttpRequest} request - The XMLHttpRequest object for which response handling is prepared.
 * @param {PatcherContext} ctx - The context used for processing agent data after the request is completed.
 * @param {function(): boolean} didInjectSignals - A function that determines if signals were injected
 *        and influences whether agent data is processed.
 */
function prepareResponseHandling(request: XMLHttpRequest, ctx: PatcherContext, didInjectSignals: () => boolean) {
  // Helper to process agent data after response, only once
  const processAgentData = () => {
    try {
      request.removeEventListener?.('loadend', processAgentData)
 
      if (didInjectSignals()) {
        const agentData = request.getResponseHeader(AGENT_DATA_HEADER)
 
        if (agentData) {
          ctx.processAgentData(agentData)
        }
      }
    } catch (e) {
      logger.error('Error processing XHR agent data:', e)
    }
  }
 
  try {
    request.addEventListener('loadend', processAgentData)
  } catch {
    logger.error('Failed to add event listener for XHR agent data processing')
  }
}