All files beaconProxy.ts

0% Statements 0/51
0% Branches 0/24
0% Functions 0/5
0% Lines 0/51

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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105                                                                                                                                                                                                                 
import NetworkMessage from './networkMessage'
import { RequestState, INetworkMessage, RequestResponseData } from './types';
import { genStringBody, getURL } from './utils'
 
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
const getContentType = (data?: BodyInit) => {
  if (data instanceof Blob) {
    return data.type
  }
  if (data instanceof FormData) {
    return 'multipart/form-data'
  }
  if (data instanceof URLSearchParams) {
    return 'application/x-www-form-urlencoded;charset=UTF-8'
  }
  return 'text/plain;charset=UTF-8'
}
 
export class BeaconProxyHandler<T extends typeof navigator.sendBeacon> implements ProxyHandler<T> {
  constructor(
    private readonly ignoredHeaders: boolean | string[],
    private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
    private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
    private readonly sendMessage: (item: INetworkMessage) => void,
    private readonly isServiceUrl: (url: string) => boolean,
  ) {}
 
  public apply(target: T, thisArg: T, argsList: any[]) {
    const urlString: string = argsList[0]
    const data: BodyInit = argsList[1]
    const item = new NetworkMessage(this.ignoredHeaders, this.setSessionTokenHeader, this.sanitize)
    if (this.isServiceUrl(urlString)) {
      return target.apply(thisArg, argsList)
    }
    const url = getURL(urlString)
    item.method = 'POST'
    item.url = urlString
    item.name = (url.pathname.split('/').pop() || '') + url.search
    item.requestType = 'beacon'
    item.requestHeader = { 'Content-Type': getContentType(data) }
    item.status = 0
    item.statusText = 'Pending'
 
    if (url.search && url.searchParams) {
      item.getData = {}
      for (const [key, value] of url.searchParams) {
        item.getData[key] = value
      }
    }
    item.requestData = genStringBody(data)
 
    if (!item.startTime) {
      item.startTime = performance.now()
    }
 
    const isSuccess = target.apply(thisArg, argsList)
    if (isSuccess) {
      item.endTime = performance.now()
      item.duration = item.endTime - (item.startTime || item.endTime)
      item.status = 0
      item.statusText = 'Sent'
      item.readyState = 4
    } else {
      item.status = 500
      item.statusText = 'Unknown'
    }
 
    const msg = item.getMessage()
    if (msg) {
      this.sendMessage(msg)
    }
    return isSuccess
  }
}
 
export default class BeaconProxy {
  public static origSendBeacon = window?.navigator?.sendBeacon
 
  public static hasSendBeacon() {
    return !!BeaconProxy.origSendBeacon
  }
 
  public static create(
    ignoredHeaders: boolean | string[],
    setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
    sanitize: (data: RequestResponseData) => RequestResponseData | null,
    sendMessage: (item: INetworkMessage) => void,
    isServiceUrl: (url: string) => boolean,
  ) {
    if (!BeaconProxy.hasSendBeacon()) {
      return undefined
    }
    return new Proxy(
      BeaconProxy.origSendBeacon,
      new BeaconProxyHandler(
        ignoredHeaders,
        setSessionTokenHeader,
        sanitize,
        sendMessage,
        isServiceUrl,
      ),
    )
  }
}