diff --git a/networkProxy/coverage/base.css b/networkProxy/coverage/base.css new file mode 100644 index 000000000..f418035b4 --- /dev/null +++ b/networkProxy/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/networkProxy/coverage/beaconProxy.ts.html b/networkProxy/coverage/beaconProxy.ts.html new file mode 100644 index 000000000..2e0301465 --- /dev/null +++ b/networkProxy/coverage/beaconProxy.ts.html @@ -0,0 +1,397 @@ + + + + +
++ 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,
+ ),
+ )
+ }
+}
+ |
+ 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 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | /**
+ * I took inspiration in few stack exchange posts
+ * and Tencent vConsole library (MIT)
+ * by wrapping the XMLHttpRequest object in a Proxy
+ * we can intercept the network requests
+ * in not-so-hacky way
+ * */
+import NetworkMessage from './networkMessage'
+import { RequestState, INetworkMessage, RequestResponseData } from './types';
+import { formatByteSize, genStringBody, getStringResponseByType, getURL } from './utils'
+
+export class ResponseProxyHandler<T extends Response> implements ProxyHandler<T> {
+ public resp: Response
+ public item: NetworkMessage
+
+ constructor(resp: T, item: NetworkMessage) {
+ this.resp = resp
+ this.item = item
+ this.mockReader()
+ }
+
+ public set(target: T, key: string, value: (args: any[]) => any) {
+ return Reflect.set(target, key, value)
+ }
+
+ public get(target: T, key: string) {
+ const value = Reflect.get(target, key)
+ switch (key) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'formData':
+ case 'json':
+ case 'text':
+ return () => {
+ this.item.responseType = <any>key.toLowerCase()
+ // @ts-ignore
+ return value.apply(target).then((resp: any) => {
+ this.item.response = getStringResponseByType(this.item.responseType, resp)
+ return resp
+ })
+ }
+ }
+ if (typeof value === 'function') {
+ return value.bind(target)
+ } else {
+ return value
+ }
+ }
+
+ protected mockReader() {
+ let readerReceivedValue: Uint8Array
+ if (!this.resp.body) {
+ // some browsers do not return `body` in some cases, like `OPTIONS` method
+ return
+ }
+ if (typeof this.resp.body.getReader !== 'function') {
+ return
+ }
+ const _getReader = this.resp.body.getReader
+ // @ts-ignore
+ this.resp.body.getReader = () => {
+ const reader = <ReturnType<typeof _getReader>>_getReader.apply(this.resp.body)
+
+ // when readyState is already 4,
+ // it's not a chunked stream, or it had already been read.
+ // so should not update status.
+ if (this.item.readyState === RequestState.DONE) {
+ return reader
+ }
+
+ const _read = reader.read
+ const _cancel = reader.cancel
+ this.item.responseType = 'arraybuffer'
+
+ // @ts-ignore
+ reader.read = () => {
+ return (<ReturnType<typeof _read>>_read.apply(reader)).then(
+ (result: ReadableStreamReadResult<Uint8Array>) => {
+ if (!readerReceivedValue) {
+ // @ts-ignore
+ readerReceivedValue = new Uint8Array(result.value)
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const newValue = new Uint8Array(readerReceivedValue.length + result.value!.length)
+ newValue.set(readerReceivedValue)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ newValue.set(result.value!, readerReceivedValue.length)
+ readerReceivedValue = newValue
+ }
+ this.item.endTime = performance.now()
+ this.item.duration = this.item.endTime - (this.item.startTime || this.item.endTime)
+ this.item.readyState = result.done ? 4 : 3
+ this.item.statusText = result.done ? String(this.item.status) : 'Loading'
+ this.item.responseSize = readerReceivedValue.length
+ this.item.responseSizeText = formatByteSize(this.item.responseSize)
+ if (result.done) {
+ this.item.response = getStringResponseByType(
+ this.item.responseType,
+ readerReceivedValue,
+ )
+ }
+ return result
+ },
+ )
+ }
+
+ reader.cancel = (...args) => {
+ this.item.cancelState = 2
+ this.item.statusText = 'Cancel'
+ this.item.endTime = performance.now()
+ this.item.duration = this.item.endTime - (this.item.startTime || this.item.endTime)
+ this.item.response = getStringResponseByType(this.item.responseType, readerReceivedValue)
+ return _cancel.apply(reader, args)
+ }
+ return reader
+ }
+ }
+}
+
+export class FetchProxyHandler<T extends typeof fetch> 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,
+ private readonly tokenUrlMatcher?: (url: string) => boolean,
+ ) {}
+
+ public apply(target: T, _: typeof window, argsList: [RequestInfo | URL, RequestInit]) {
+ const input = argsList[0]
+ const init = argsList[1]
+ if (
+ !input ||
+ // @ts-ignore
+ (typeof input !== 'string' && !input?.url)
+ ) {
+ return <ReturnType<T>>target.apply(window, argsList)
+ }
+
+ const isORUrl =
+ input instanceof URL || typeof input === 'string'
+ ? this.isServiceUrl(String(input))
+ : this.isServiceUrl(String(input.url))
+
+ if (isORUrl) {
+ return target.apply(window, argsList)
+ }
+
+ const item = new NetworkMessage(this.ignoredHeaders, this.setSessionTokenHeader, this.sanitize)
+ this.beforeFetch(item, input as RequestInfo, init)
+
+ this.setSessionTokenHeader((name, value) => {
+ if (this.tokenUrlMatcher !== undefined) {
+ if (!this.tokenUrlMatcher(item.url)) {
+ return
+ }
+ }
+ if (argsList[1] === undefined && argsList[0] instanceof Request) {
+ return argsList[0].headers.append(name, value)
+ } else {
+ if (!argsList[1]) argsList[1] = {}
+ if (argsList[1].headers === undefined) {
+ argsList[1] = { ...argsList[1], headers: {} }
+ }
+ if (argsList[1].headers instanceof Headers) {
+ argsList[1].headers.append(name, value)
+ } else if (Array.isArray(argsList[1].headers)) {
+ argsList[1].headers.push([name, value])
+ } else {
+ // @ts-ignore
+ argsList[1].headers[name] = value
+ }
+ }
+ })
+ return (<ReturnType<T>>target.apply(window, argsList))
+ .then(this.afterFetch(item))
+ .catch((e) => {
+ // mock finally
+ item.endTime = performance.now()
+ item.duration = item.endTime - (item.startTime || item.endTime)
+ throw e
+ })
+ }
+
+ protected beforeFetch(item: NetworkMessage, input: RequestInfo | string, init?: RequestInit) {
+ let url: URL,
+ method = 'GET',
+ requestHeader: HeadersInit = {}
+
+ // handle `input` content
+ if (typeof input === 'string') {
+ // when `input` is a string
+ method = init?.method || 'GET'
+ url = getURL(input)
+ requestHeader = init?.headers || {}
+ } else {
+ // when `input` is a `Request` object
+ method = input.method || 'GET'
+ url = getURL(input.url)
+ requestHeader = input.headers
+ }
+
+ item.method = <NetworkMessage['method']>method
+ item.requestType = 'fetch'
+ item.requestHeader = requestHeader
+ item.url = url.toString()
+ item.name = (url.pathname.split('/').pop() || '') + url.search
+ item.status = 0
+ item.statusText = 'Pending'
+ item.readyState = 1
+ if (!item.startTime) {
+ // UNSENT
+ item.startTime = performance.now()
+ }
+
+ if (Object.prototype.toString.call(requestHeader) === '[object Headers]') {
+ item.requestHeader = {}
+ for (const [key, value] of <Headers>requestHeader) {
+ item.requestHeader[key] = value
+ }
+ } else {
+ item.requestHeader = requestHeader
+ }
+
+ // save GET data
+ if (url.search && url.searchParams) {
+ item.getData = {}
+ for (const [key, value] of url.searchParams) {
+ item.getData[key] = value
+ }
+ }
+
+ // save POST data
+ if (init?.body) {
+ item.requestData = genStringBody(init.body)
+ }
+ }
+
+ protected afterFetch(item: NetworkMessage) {
+ return (resp: Response) => {
+ item.endTime = performance.now()
+ item.duration = item.endTime - (item.startTime || item.endTime)
+ item.status = resp.status
+ item.statusText = String(resp.status)
+
+ let isChunked = false
+ item.header = {}
+ for (const [key, value] of resp.headers) {
+ item.header[key] = value
+ isChunked = value.toLowerCase().indexOf('chunked') > -1 ? true : isChunked
+ }
+
+ if (isChunked) {
+ // when `transfer-encoding` is chunked, the response is a stream which is under loading,
+ // so the `readyState` should be 3 (Loading),
+ // and the response should NOT be `clone()` which will affect stream reading.
+ item.readyState = 3
+ } else {
+ // Otherwise, not chunked, the response is not a stream,
+ // so it's completed and can be cloned for `text()` calling.
+ item.readyState = 4
+
+ this.handleResponseBody(resp.clone(), item)
+ .then((responseValue: string | ArrayBuffer) => {
+ item.responseSize =
+ typeof responseValue === 'string' ? responseValue.length : responseValue.byteLength
+ item.responseSizeText = formatByteSize(item.responseSize)
+ item.response = getStringResponseByType(item.responseType, responseValue)
+
+ const msg = item.getMessage()
+ if (msg) {
+ this.sendMessage(msg)
+ }
+ })
+ .catch((e) => {
+ if (e.name !== 'AbortError') {
+ throw e
+ } else {
+ // ignore AbortError
+ }
+ })
+ }
+
+ return new Proxy(resp, new ResponseProxyHandler(resp, item))
+ }
+ }
+
+ protected handleResponseBody(resp: Response, item: NetworkMessage) {
+ // parse response body by Content-Type
+ const contentType = resp.headers.get('content-type')
+ if (contentType && contentType.includes('application/json')) {
+ item.responseType = 'json'
+ return resp.text()
+ } else if (
+ contentType &&
+ (contentType.includes('text/html') || contentType.includes('text/plain'))
+ ) {
+ item.responseType = 'text'
+ return resp.text()
+ } else {
+ item.responseType = 'arraybuffer'
+ return resp.arrayBuffer()
+ }
+ }
+}
+
+export default class FetchProxy {
+ 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,
+ tokenUrlMatcher?: (url: string) => boolean,
+ ) {
+ return new Proxy(
+ fetch,
+ new FetchProxyHandler(
+ ignoredHeaders,
+ setSessionTokenHeader,
+ sanitize,
+ sendMessage,
+ isServiceUrl,
+ tokenUrlMatcher,
+ ),
+ )
+ }
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| beaconProxy.ts | +
+
+ |
+ 0% | +0/51 | +0% | +0/24 | +0% | +0/5 | +0% | +0/51 | +
| fetchProxy.ts | +
+
+ |
+ 0% | +0/148 | +0% | +0/95 | +0% | +0/21 | +0% | +0/147 | +
| index.ts | +
+
+ |
+ 0% | +0/14 | +0% | +0/13 | +0% | +0/2 | +0% | +0/14 | +
| networkMessage.ts | +
+
+ |
+ 90.19% | +46/51 | +55.55% | +10/18 | +87.5% | +7/8 | +91.48% | +43/47 | +
| types.ts | +
+
+ |
+ 0% | +0/7 | +0% | +0/2 | +0% | +0/1 | +0% | +0/6 | +
| utils.ts | +
+
+ |
+ 81.9% | +86/105 | +60% | +60/100 | +100% | +9/9 | +81.9% | +86/105 | +
| xhrProxy.ts | +
+
+ |
+ 0% | +0/126 | +0% | +0/59 | +0% | +0/26 | +0% | +0/124 | +
+ 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 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + | import BeaconProxy from "./beaconProxy";
+import FetchProxy from "./fetchProxy";
+import XHRProxy from "./xhrProxy";
+import { INetworkMessage, RequestResponseData } from "./types";
+
+export {
+ BeaconProxy,
+ FetchProxy,
+ XHRProxy,
+ INetworkMessage,
+ RequestResponseData,
+};
+
+const getWarning = (api: string) => {
+ const str = `Openreplay: Can't find ${api} in global context.`;
+ console.warn(str);
+};
+
+/**
+ * Creates network proxies for XMLHttpRequest, fetch, and sendBeacon to intercept and monitor network requests and
+ * responses.
+ *
+ * @param {Window | typeof globalThis} context - The global context object (e.g., window or globalThis).
+ * @param {boolean | string[]} ignoredHeaders - Headers to ignore from requests. If `true`, all headers are ignored; if
+ * an array of strings, those header names are ignored.
+ * @param {(cb: (name: string, value: string) => void) => void} setSessionTokenHeader - Function to set a session token
+ * header; accepts a callback that sets the header name and value.
+ * @param {(data: RequestResponseData) => RequestResponseData | null} sanitize - Function to sanitize request and
+ * response data; should return sanitized data or `null` to ignore the data.
+ * @param {(message: INetworkMessage) => void} sendMessage - Function to send network messages for further processing
+ * or logging.
+ * @param {(url: string) => boolean} isServiceUrl - Function to determine if a URL is a service URL that should be
+ * ignored by the proxy.
+ * @param {Object} [modules] - Modules to apply the proxies to.
+ * @param {boolean} [modules.xhr=true] - Whether to proxy XMLHttpRequest.
+ * @param {boolean} [modules.fetch=true] - Whether to proxy the fetch API.
+ * @param {boolean} [modules.beacon=true] - Whether to proxy navigator.sendBeacon.
+ * @param {(url: string) => boolean} [tokenUrlMatcher] - Optional function; the session token header will only be
+ * applied to requests matching this function.
+ *
+ * @returns {void}
+ */
+export default function createNetworkProxy(
+ context: typeof globalThis,
+ ignoredHeaders: boolean | string[],
+ setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
+ sanitize: (data: RequestResponseData) => RequestResponseData | null,
+ sendMessage: (message: INetworkMessage) => void,
+ isServiceUrl: (url: string) => boolean,
+ modules: { xhr: boolean; fetch: boolean; beacon: boolean } = {
+ xhr: true,
+ fetch: true,
+ beacon: true,
+ },
+ tokenUrlMatcher?: (url: string) => boolean,
+): void {
+ if (modules.xhr) {
+ if (context.XMLHttpRequest) {
+ context.XMLHttpRequest = XHRProxy.create(
+ ignoredHeaders,
+ setSessionTokenHeader,
+ sanitize,
+ sendMessage,
+ isServiceUrl,
+ tokenUrlMatcher,
+ );
+ } else {
+ getWarning("XMLHttpRequest");
+ }
+ }
+ if (modules.fetch) {
+ if (context.fetch) {
+ context.fetch = FetchProxy.create(
+ ignoredHeaders,
+ setSessionTokenHeader,
+ sanitize,
+ sendMessage,
+ isServiceUrl,
+ tokenUrlMatcher,
+ );
+ } else {
+ getWarning("fetch");
+ }
+ }
+ if (modules.beacon) {
+ if (context?.navigator?.sendBeacon) {
+ context.navigator.sendBeacon = BeaconProxy.create(
+ ignoredHeaders,
+ setSessionTokenHeader,
+ sanitize,
+ sendMessage,
+ isServiceUrl,
+ );
+ }
+ }
+}
+ |
+ 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 | + + + + + + + + + + + +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x +4x + +4x +4x +4x +4x +4x +4x +4x + + +4x +4x +4x + + + +1x +1x + + + +1x + +1x + + + + + + + +1x + +1x +1x + + + + + +1x + + + + + + + + + + + + + +2x +2x +2x +1x + +2x + + +2x +2x +2x +1x + +2x + + + +9x +6x + +3x + + + + | import {
+ RequestResponseData,
+ INetworkMessage,
+ httpMethod,
+ RequestState,
+} from './types'
+/**
+ * I know we're not using most of the information from this class
+ * but it can be useful in the future if we will decide to display more stuff in our ui
+ * */
+
+export default class NetworkMessage {
+ id = ''
+ name?: string = ''
+ method: httpMethod = ''
+ url = ''
+ status = 0
+ statusText?: string = ''
+ cancelState?: 0 | 1 | 2 | 3 = 0
+ readyState?: RequestState = 0
+ header: { [key: string]: string } = {}
+ responseType: XMLHttpRequest['responseType'] = ''
+ requestType: 'xhr' | 'fetch' | 'ping' | 'custom' | 'beacon' | 'graphql' = 'xhr'
+ requestHeader: HeadersInit = {}
+ response: string
+ responseSize = 0 // bytes
+ responseSizeText = ''
+ startTime = 0
+ endTime = 0
+ duration = 0
+ getData: { [key: string]: string } = {}
+ requestData: string | null = null
+
+ constructor(
+ private readonly ignoredHeaders: boolean | string[] = [],
+ private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
+ private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
+ ) {}
+
+ getMessage(): INetworkMessage | null {
+ const { reqHs, resHs } = this.writeHeaders()
+ const request = {
+ headers: reqHs,
+ body: this.method === 'GET' ? JSON.stringify(this.getData) : this.requestData,
+ }
+ const response = { headers: resHs, body: this.response }
+
+ const messageInfo = this.sanitize({
+ url: this.url,
+ method: this.method,
+ status: this.status,
+ request,
+ response,
+ })
+
+ Iif (!messageInfo) return null;
+
+ const isGraphql = messageInfo.url.includes("/graphql");
+ Iif (isGraphql && messageInfo.response.body && typeof messageInfo.response.body === 'string') {
+ const isError = messageInfo.response.body.includes("errors");
+ messageInfo.status = isError ? 400 : 200;
+ this.requestType = 'graphql';
+ }
+
+ return {
+ requestType: this.requestType,
+ method: messageInfo.method as httpMethod,
+ url: messageInfo.url,
+ request: JSON.stringify(messageInfo.request),
+ response: JSON.stringify(messageInfo.response),
+ status: messageInfo.status,
+ startTime: this.startTime,
+ duration: this.duration,
+ responseSize: this.responseSize,
+ }
+ }
+
+ writeHeaders() {
+ const reqHs: Record<string, string> = {}
+ Object.entries(this.requestHeader).forEach(([key, value]) => {
+ if (this.isHeaderIgnored(key)) return
+ reqHs[key] = value
+ })
+ this.setSessionTokenHeader((name, value) => {
+ reqHs[name] = value
+ })
+ const resHs: Record<string, string> = {}
+ Object.entries(this.header).forEach(([key, value]) => {
+ if (this.isHeaderIgnored(key)) return
+ resHs[key] = value
+ })
+ return { reqHs, resHs }
+ }
+
+ isHeaderIgnored(key: string) {
+ if (Array.isArray(this.ignoredHeaders)) {
+ return this.ignoredHeaders.map((k) => k.toLowerCase()).includes(key.toLowerCase())
+ } else {
+ return this.ignoredHeaders
+ }
+ }
+}
+ |