diff --git a/tracker/tracker/CHANGELOG.md b/tracker/tracker/CHANGELOG.md index 3fb85a189..5b18ad276 100644 --- a/tracker/tracker/CHANGELOG.md +++ b/tracker/tracker/CHANGELOG.md @@ -2,6 +2,10 @@ - **[breaking]** added gzip compression to large messages +# 6.0.2 + +- fix network tracking for samedomain iframes created by js code + # 6.0.1 - fix webworker writer re-init request diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index bf2c328df..3084491da 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "6.0.1-beta.1", + "version": "6.0.2", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/observer/top_observer.ts b/tracker/tracker/src/main/app/observer/top_observer.ts index 6586694e2..46390fb02 100644 --- a/tracker/tracker/src/main/app/observer/top_observer.ts +++ b/tracker/tracker/src/main/app/observer/top_observer.ts @@ -94,7 +94,6 @@ export default class TopObserver extends Observer { //TODO: more explicit logic ) { this.contextsSet.add(currentWin) - Network(this.app, this.app.networkOptions, currentWin) //@ts-ignore https://github.com/microsoft/TypeScript/issues/41684 this.contextCallbacks.forEach((cb) => cb(currentWin)) } diff --git a/tracker/tracker/src/main/modules/network.ts b/tracker/tracker/src/main/modules/network.ts index 097648d54..de2ea9789 100644 --- a/tracker/tracker/src/main/modules/network.ts +++ b/tracker/tracker/src/main/modules/network.ts @@ -21,7 +21,7 @@ type FetchRequestBody = RequestInit['body'] // } // XHRResponse body: ArrayBuffer, a Blob, a Document, a JavaScript Object, or a string -// TODO: extract maximum of useful information from any type of Request/Responce bodies +// TODO: extract maximum of useful information from any type of Request/Response bodies // function objectifyBody(body: any): RequestBody { // if (body instanceof Blob) { // return { @@ -35,21 +35,11 @@ type FetchRequestBody = RequestInit['body'] // } // } -function checkCacheByPerformanceTimings(requestUrl: string) { - if (performance) { - const timings = performance.getEntriesByName(requestUrl)[0] - if (timings) { - // @ts-ignore - weird ts typings, please refer to https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming - return timings.transferSize === 0 || timings.responseStart - timings.requestStart < 10 - } - } - return false -} - interface RequestData { body: XHRRequestBody | FetchRequestBody headers: Record } + interface ResponseData { body: any headers: Record @@ -92,7 +82,7 @@ export interface Options { sanitizer?: Sanitizer } -export default function (app: App, opts: Partial = {}, customEnv?: Record) { +export default function (app: App, opts: Partial = {}) { const options: Options = Object.assign( { failuresOnly: false, @@ -110,6 +100,7 @@ export default function (app: App, opts: Partial = {}, customEnv?: Reco const stHeader = options.sessionTokenHeader === true ? 'X-OpenReplay-SessionToken' : options.sessionTokenHeader + function setSessionTokenHeader(setRequestHeader: (name: string, value: string) => void) { if (stHeader) { const sessionToken = app.getSessionToken() @@ -149,73 +140,142 @@ export default function (app: App, opts: Partial = {}, customEnv?: Reco return JSON.stringify(r) } - /* ====== Fetch ====== */ - const origFetch = customEnv - ? (customEnv.fetch.bind(customEnv) as WindowFetch) - : (window.fetch.bind(window) as WindowFetch) + const patchWindow = (context: typeof globalThis) => { + /* ====== Fetch ====== */ + const origFetch = context.fetch.bind(context) as WindowFetch - const trackFetch = (input: RequestInfo | URL, init: RequestInit = {}) => { - if (!(typeof input === 'string' || input instanceof URL) || app.isServiceURL(String(input))) { - return origFetch(input, init) - } - - setSessionTokenHeader(function (name, value) { - if (init.headers === undefined) { - init.headers = {} - } - if (init.headers instanceof Headers) { - init.headers.append(name, value) - } else if (Array.isArray(init.headers)) { - init.headers.push([name, value]) - } else { - init.headers[name] = value - } - }) - - const startTime = performance.now() - return origFetch(input, init).then((response) => { - const duration = performance.now() - startTime - if (options.failuresOnly && response.status < 400) { - return response + const trackFetch = (input: RequestInfo | URL, init: RequestInit = {}) => { + if (!(typeof input === 'string' || input instanceof URL) || app.isServiceURL(String(input))) { + return origFetch(input, init) } - const r = response.clone() - r.text() - .then((text) => { - const reqHs: Record = {} - const resHs: Record = {} - if (ignoreHeaders !== true) { - // request headers - const writeReqHeader = ([n, v]: [string, string]) => { - if (!isHIgnored(n)) { - reqHs[n] = v + setSessionTokenHeader(function (name, value) { + if (init.headers === undefined) { + init.headers = {} + } + if (init.headers instanceof Headers) { + init.headers.append(name, value) + } else if (Array.isArray(init.headers)) { + init.headers.push([name, value]) + } else { + init.headers[name] = value + } + }) + + const startTime = performance.now() + return origFetch(input, init).then((response) => { + const duration = performance.now() - startTime + if (options.failuresOnly && response.status < 400) { + return response + } + + const r = response.clone() + r.text() + .then((text) => { + const reqHs: Record = {} + const resHs: Record = {} + if (ignoreHeaders !== true) { + // request headers + const writeReqHeader = ([n, v]: [string, string]) => { + if (!isHIgnored(n)) { + reqHs[n] = v + } } + if (init.headers instanceof Headers) { + init.headers.forEach((v, n) => writeReqHeader([n, v])) + } else if (Array.isArray(init.headers)) { + init.headers.forEach(writeReqHeader) + } else if (typeof init.headers === 'object') { + Object.entries(init.headers).forEach(writeReqHeader) + } + // response headers + r.headers.forEach((v, n) => { + if (!isHIgnored(n)) resHs[n] = v + }) } - if (init.headers instanceof Headers) { - init.headers.forEach((v, n) => writeReqHeader([n, v])) - } else if (Array.isArray(init.headers)) { - init.headers.forEach(writeReqHeader) - } else if (typeof init.headers === 'object') { - Object.entries(init.headers).forEach(writeReqHeader) - } - // response headers - r.headers.forEach((v, n) => { - if (!isHIgnored(n)) resHs[n] = v - }) - } - const method = strMethod(init.method) + const method = strMethod(init.method) + const reqResInfo = sanitize({ + url: String(input), + method, + status: r.status, + request: { + headers: reqHs, + body: init.body, + }, + response: { + headers: resHs, + body: text, + }, + }) + if (!reqResInfo) { + return + } + + app.send( + NetworkRequest( + 'fetch', + method, + String(reqResInfo.url), + stringify(reqResInfo.request), + stringify(reqResInfo.response), + r.status, + startTime + getTimeOrigin(), + duration, + ), + ) + }) + .catch((e) => app.debug.error('Could not process Fetch response:', e)) + + return response + }) + } + context.fetch = trackFetch + + /* ====== <> ====== */ + + /* ====== XHR ====== */ + + const nativeOpen = context.XMLHttpRequest.prototype.open + + function trackXMLHttpReqOpen(this: XMLHttpRequest, initMethod: string, url: string | URL) { + const xhr = this + setSessionTokenHeader((name, value) => xhr.setRequestHeader(name, value)) + + let startTime = 0 + xhr.addEventListener('loadstart', (e) => { + startTime = e.timeStamp + }) + xhr.addEventListener( + 'load', + app.safe((e) => { + const { headers: reqHs, body: reqBody } = getXHRRequestDataObject(xhr) + const duration = startTime > 0 ? e.timeStamp - startTime : 0 + + const hString: string | null = ignoreHeaders ? '' : xhr.getAllResponseHeaders() // might be null (though only if no response received though) + const resHs = hString + ? hString + .split('\r\n') + .map((h) => h.split(':')) + .filter((entry) => !isHIgnored(entry[0])) + .reduce( + (hds, [name, value]) => ({ ...hds, [name]: value }), + {} as Record, + ) + : {} + + const method = strMethod(initMethod) const reqResInfo = sanitize({ - url: String(input), + url: String(url), method, - status: r.status, + status: xhr.status, request: { headers: reqHs, - body: init.body, + body: reqBody, }, response: { headers: resHs, - body: text, + body: xhr.response, }, }) if (!reqResInfo) { @@ -224,139 +284,57 @@ export default function (app: App, opts: Partial = {}, customEnv?: Reco app.send( NetworkRequest( - 'fetch', + 'xhr', method, String(reqResInfo.url), stringify(reqResInfo.request), stringify(reqResInfo.response), - r.status, + xhr.status, startTime + getTimeOrigin(), duration, ), ) - }) - .catch((e) => app.debug.error('Could not process Fetch response:', e)) + }), + ) - return response - }) - } - - if (customEnv) { - customEnv.fetch = trackFetch - } else { - window.fetch = trackFetch - } - /* ====== <> ====== */ - - /* ====== XHR ====== */ - - const nativeOpen = customEnv - ? customEnv.XMLHttpRequest.prototype.open - : XMLHttpRequest.prototype.open - - function trackXMLHttpReqOpen(initMethod: string, url: string | URL) { - // @ts-ignore ??? this -> XMLHttpRequest - const xhr = this as XMLHttpRequest - setSessionTokenHeader((name, value) => xhr.setRequestHeader(name, value)) - - let startTime = 0 - xhr.addEventListener('loadstart', (e) => { - startTime = e.timeStamp - }) - xhr.addEventListener( - 'load', - app.safe((e) => { - const { headers: reqHs, body: reqBody } = getXHRRequestDataObject(xhr) - const duration = startTime > 0 ? e.timeStamp - startTime : 0 - - const hString: string | null = ignoreHeaders ? '' : xhr.getAllResponseHeaders() // might be null (though only if no response received though) - const resHs = hString - ? hString - .split('\r\n') - .map((h) => h.split(':')) - .filter((entry) => !isHIgnored(entry[0])) - .reduce( - (hds, [name, value]) => ({ ...hds, [name]: value }), - {} as Record, - ) - : {} - - const method = strMethod(initMethod) - const reqResInfo = sanitize({ - url: String(url), - method, - status: xhr.status, - request: { - headers: reqHs, - body: reqBody, - }, - response: { - headers: resHs, - body: xhr.response, - }, - }) - if (!reqResInfo) { - return - } - - app.send( - NetworkRequest( - 'xhr', - method, - String(reqResInfo.url), - stringify(reqResInfo.request), - stringify(reqResInfo.response), - xhr.status, - startTime + getTimeOrigin(), - duration, - ), - ) - }), - ) - - //TODO: handle error (though it has no Error API nor any useful information) - //xhr.addEventListener('error', (e) => {}) - // @ts-ignore ??? this -> XMLHttpRequest - return nativeOpen.apply(this as XMLHttpRequest, arguments) - } - if (customEnv) { - customEnv.XMLHttpRequest.prototype.open = trackXMLHttpReqOpen.bind(customEnv) - } else { - XMLHttpRequest.prototype.open = trackXMLHttpReqOpen - } - - const nativeSend = XMLHttpRequest.prototype.send - function trackXHRSend(body: Document | XMLHttpRequestBodyInit | null | undefined) { - // @ts-ignore ??? this -> XMLHttpRequest - const rdo = getXHRRequestDataObject(this as XMLHttpRequest) - rdo.body = body - - // @ts-ignore ??? this -> XMLHttpRequest - return nativeSend.apply(this as XMLHttpRequest, arguments) - } - - if (customEnv) { - customEnv.XMLHttpRequest.prototype.send = trackXHRSend.bind(customEnv) - } else { - XMLHttpRequest.prototype.send = trackXHRSend - } - - const nativeSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader - - function trackSetReqHeader(name: string, value: string) { - if (!isHIgnored(name)) { - // @ts-ignore ??? this -> XMLHttpRequest - const rdo = getXHRRequestDataObject(this as XMLHttpRequest) - rdo.headers[name] = value + //TODO: handle error (though it has no Error API nor any useful information) + //xhr.addEventListener('error', (e) => {}) + return nativeOpen.apply(this, arguments) } - // @ts-ignore ??? this -> XMLHttpRequest - return nativeSetRequestHeader.apply(this as XMLHttpRequest, arguments) + + context.XMLHttpRequest.prototype.open = trackXMLHttpReqOpen + + const nativeSend = context.XMLHttpRequest.prototype.send + + function trackXHRSend( + this: XMLHttpRequest, + body: Document | XMLHttpRequestBodyInit | null | undefined, + ) { + const rdo = getXHRRequestDataObject(this) + rdo.body = body + + // @ts-ignore ??? this -> XMLHttpRequest + return nativeSend.apply(this, arguments) + } + + context.XMLHttpRequest.prototype.send = trackXHRSend + + const nativeSetRequestHeader = context.XMLHttpRequest.prototype.setRequestHeader + + function trackSetReqHeader(this: XMLHttpRequest, name: string, value: string) { + if (!isHIgnored(name)) { + const rdo = getXHRRequestDataObject(this) + rdo.headers[name] = value + } + return nativeSetRequestHeader.apply(this, arguments) + } + + context.XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader + + /* ====== <> ====== */ } - if (customEnv) { - customEnv.XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader.bind(customEnv) - } else { - XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader - } - /* ====== <> ====== */ + patchWindow(window) + + app.observer.attachContextCallback(app.safe(patchWindow)) } diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index b45707dbb..4ef650a0b 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -90,6 +90,9 @@ export function isIframeCrossdomain(e: HTMLIFrameElement): boolean { } } +/** + * checks if iframe is accessible + **/ export function canAccessIframe(iframe: HTMLIFrameElement) { try { return Boolean(iframe.contentDocument) diff --git a/tracker/tracker/src/webworker/index.ts b/tracker/tracker/src/webworker/index.ts index cd32ec5cf..699602e20 100644 --- a/tracker/tracker/src/webworker/index.ts +++ b/tracker/tracker/src/webworker/index.ts @@ -1,7 +1,6 @@ // Do strong type WebWorker as soon as it is possible: // https://github.com/microsoft/TypeScript/issues/14877 // At the moment "webworker" lib conflicts with jest-environment-jsdom that uses "dom" lib - import { Type as MType } from '../common/messages.gen.js' import { ToWorkerData, FromWorkerData } from '../common/interaction.js'