All files / scripts/instrumentor/patcher/xhr open.ts

86.95% Statements 20/23
100% Branches 5/5
100% Functions 4/4
86.36% Lines 19/22

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 78 79 80 81 82 83 84 85 86                                20x   20x               19x   19x   1x         18x 18x                             18x 18x   18x       2x 2x     18x         18x             18x       18x 18x 2x 2x        
import { PatcherContext } from '../context'
import { FingerprintContextSymbol, XHRFingerprintMetadata, XHRContext, XHRWithFingerprintContext } from './types'
import { collectSignalsForProtectedUrl } from '../signalsInjection'
import { createPatcherRequest } from './patcherRequest'
import { logger } from '../../../shared/logger'
 
/**
 * Creates a patched version of the `XMLHttpRequest.prototype.open` method to capture request metadata,
 * apply signal handling, and provide additional context for fingerprinting.
 *
 * @param {PatcherContext} ctx - The context object used for configuring and managing the patching process
 *                              and interacting with signal handling mechanisms.
 * @return {function} Returns a new function that wraps the original `XMLHttpRequest.open` method and includes
 *                    additional behavior for metadata collection and signal injection.
 */
export function createPatchedOpen(ctx: PatcherContext): typeof XMLHttpRequest.prototype.open {
  const originalOpen = XMLHttpRequest.prototype.open
 
  return function patchedOpen(
    this: XHRWithFingerprintContext,
    method: string,
    url: string,
    async: boolean = true,
    username?: string | null,
    password?: string | null
  ) {
    const callOpen = () => originalOpen.call(this, method, url, async, username, password)
 
    if (!async) {
      // Sync requests are not supported for now
      return callOpen()
    }
 
    let metadata: XHRFingerprintMetadata
 
    try {
      metadata = {
        method: method?.toUpperCase?.(),
        // Resolve relative URLs against the current location
        url: new URL(url, location.origin).toString(),
      }
    } catch (e) {
      // If URL cannot be resolved (very unlikely)
      logger.warn('Failed to resolve XHR URL for patching:', e)
 
      metadata = {
        method: method?.toUpperCase?.(),
        url,
      }
    }
 
    try {
      const request = createPatcherRequest(this, metadata)
      // Start gathering signals as soon as possible.
      const signalsCollectionPromise = collectSignalsForProtectedUrl({
        request,
        ctx,
      }).catch((error) => {
        logger.error('Error injecting signals:', error)
        return undefined
      })
 
      const nextFingerprintContext: XHRContext = {
        preservedWithCredentials: this[FingerprintContextSymbol]?.preservedWithCredentials,
        signalsCollectionPromise,
        request,
      }
      Object.assign(this, {
        [FingerprintContextSymbol]: nextFingerprintContext,
      })
    } catch (e) {
      logger.error('Error setting XHR fingerprint context:', e)
    }
 
    callOpen()
 
    // Restore the original withCredentials setting. This can only be changed before the initial send
    // or after the XHR instance is reinitialized by calling open after a send.
    const fingerprintContext = this[FingerprintContextSymbol]
    if (fingerprintContext?.preservedWithCredentials !== undefined) {
      this.withCredentials = fingerprintContext.preservedWithCredentials
      fingerprintContext.preservedWithCredentials = undefined
    }
  }
}