From 6e1fec8013240473157e11a11da995986a9172d2 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Mon, 12 Dec 2022 16:03:44 +0100 Subject: [PATCH] feat(tracker):fetch/xhr module in core --- ...r~tracker~src~main~app~messages.gen.ts.erb | 2 +- tracker/tracker/.eslintrc.cjs | 1 + tracker/tracker/src/common/messages.gen.ts | 15 +- tracker/tracker/src/main/app/index.ts | 14 +- tracker/tracker/src/main/app/messages.gen.ts | 25 +- tracker/tracker/src/main/app/messages.ts | 339 ------------------ tracker/tracker/src/main/index.ts | 8 +- tracker/tracker/src/main/modules/network.ts | 313 ++++++++++++++++ tracker/tracker/src/main/modules/timing.ts | 11 +- .../src/webworker/MessageEncoder.gen.ts | 4 + 10 files changed, 382 insertions(+), 350 deletions(-) delete mode 100644 tracker/tracker/src/main/app/messages.ts create mode 100644 tracker/tracker/src/main/modules/network.ts diff --git a/mobs/templates/tracker~tracker~src~main~app~messages.gen.ts.erb b/mobs/templates/tracker~tracker~src~main~app~messages.gen.ts.erb index d4c132f8a..40aec9ce0 100644 --- a/mobs/templates/tracker~tracker~src~main~app~messages.gen.ts.erb +++ b/mobs/templates/tracker~tracker~src~main~app~messages.gen.ts.erb @@ -2,7 +2,7 @@ /* eslint-disable */ import * as Messages from '../../common/messages.gen.js' -export { default } from '../../common/messages.gen.js' +export { default, Type } from '../../common/messages.gen.js' <% $messages.select { |msg| msg.tracker }.each do |msg| %> export function <%= msg.name %>( diff --git a/tracker/tracker/.eslintrc.cjs b/tracker/tracker/.eslintrc.cjs index de1fed019..72c31e9a4 100644 --- a/tracker/tracker/.eslintrc.cjs +++ b/tracker/tracker/.eslintrc.cjs @@ -45,5 +45,6 @@ module.exports = { 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-useless-constructor': 'warn', + 'prefer-rest-params': 'off', }, }; diff --git a/tracker/tracker/src/common/messages.gen.ts b/tracker/tracker/src/common/messages.gen.ts index 9dbdb755a..2d6624b5e 100644 --- a/tracker/tracker/src/common/messages.gen.ts +++ b/tracker/tracker/src/common/messages.gen.ts @@ -21,6 +21,7 @@ export declare const enum Type { SetInputValue = 18, SetInputChecked = 19, MouseMove = 20, + NetworkRequest = 21, ConsoleLog = 22, PageLoadTiming = 23, PageRenderTiming = 24, @@ -187,6 +188,18 @@ export type MouseMove = [ /*y:*/ number, ] +export type NetworkRequest = [ + /*type:*/ Type.NetworkRequest, + /*type:*/ string, + /*method:*/ string, + /*url:*/ string, + /*request:*/ string, + /*response:*/ string, + /*status:*/ number, + /*timestamp:*/ number, + /*duration:*/ number, +] + export type ConsoleLog = [ /*type:*/ Type.ConsoleLog, /*level:*/ string, @@ -471,5 +484,5 @@ export type JSException = [ ] -type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PageLoadTiming | PageRenderTiming | JSExceptionDeprecated | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | Zustand | JSException +type Message = BatchMetadata | PartitionedMessage | Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequest | ConsoleLog | PageLoadTiming | PageRenderTiming | JSExceptionDeprecated | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | ResourceTiming | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | Zustand | JSException export default Message diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index bec00d01b..a59f0ac28 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -1,5 +1,5 @@ import type Message from './messages.gen.js' -import { Timestamp, Metadata, UserID } from './messages.gen.js' +import { Timestamp, Metadata, UserID, Type as MType } from './messages.gen.js' import { now, adjustTimeOrigin, deprecationWarn } from '../utils.js' import Nodes from './nodes.js' import Observer from './observer/top_observer.js' @@ -199,10 +199,22 @@ export default class App { this.debug.error('OpenReplay error: ', context, e) } + private _usingOldFetchPlugin = false send(message: Message, urgent = false): void { if (this.activityState === ActivityState.NotActive) { return } + // === Back compatibility with Fetch/Axios plugins === + if (message[0] === MType.Fetch) { + this._usingOldFetchPlugin = true + deprecationWarn('Fetch plugin', "'network' init option") + deprecationWarn('Axios plugin', "'network' init option") + } + if (this._usingOldFetchPlugin && message[0] === MType.NetworkRequest) { + return + } + // ==================================================== + this.messages.push(message) // TODO: commit on start if there were `urgent` sends; // Clarify where urgent can be used for; diff --git a/tracker/tracker/src/main/app/messages.gen.ts b/tracker/tracker/src/main/app/messages.gen.ts index c9f42e610..82f106e7d 100644 --- a/tracker/tracker/src/main/app/messages.gen.ts +++ b/tracker/tracker/src/main/app/messages.gen.ts @@ -2,7 +2,7 @@ /* eslint-disable */ import * as Messages from '../../common/messages.gen.js' -export { default } from '../../common/messages.gen.js' +export { default, Type } from '../../common/messages.gen.js' export function BatchMetadata( @@ -232,6 +232,29 @@ export function MouseMove( ] } +export function NetworkRequest( + type: string, + method: string, + url: string, + request: string, + response: string, + status: number, + timestamp: number, + duration: number, +): Messages.NetworkRequest { + return [ + Messages.Type.NetworkRequest, + type, + method, + url, + request, + response, + status, + timestamp, + duration, + ] +} + export function ConsoleLog( level: string, value: string, diff --git a/tracker/tracker/src/main/app/messages.ts b/tracker/tracker/src/main/app/messages.ts deleted file mode 100644 index 4b740eddb..000000000 --- a/tracker/tracker/src/main/app/messages.ts +++ /dev/null @@ -1,339 +0,0 @@ -// Auto-generated, do not edit - -import * as Messages from '../../common/messages.gen.js' -export { default } from '../../common/messages.gen.js' - -export function BatchMetadata( - version: number, - pageNo: number, - firstIndex: number, - timestamp: number, - location: string, -): Messages.BatchMetadata { - return [Messages.Type.BatchMetadata, version, pageNo, firstIndex, timestamp, location] -} - -export function PartitionedMessage(partNo: number, partTotal: number): Messages.PartitionedMessage { - return [Messages.Type.PartitionedMessage, partNo, partTotal] -} - -export function Timestamp(timestamp: number): Messages.Timestamp { - return [Messages.Type.Timestamp, timestamp] -} - -export function SetPageLocation( - url: string, - referrer: string, - navigationStart: number, -): Messages.SetPageLocation { - return [Messages.Type.SetPageLocation, url, referrer, navigationStart] -} - -export function SetViewportSize(width: number, height: number): Messages.SetViewportSize { - return [Messages.Type.SetViewportSize, width, height] -} - -export function SetViewportScroll(x: number, y: number): Messages.SetViewportScroll { - return [Messages.Type.SetViewportScroll, x, y] -} - -export function CreateDocument(): Messages.CreateDocument { - return [Messages.Type.CreateDocument] -} - -export function CreateElementNode( - id: number, - parentID: number, - index: number, - tag: string, - svg: boolean, -): Messages.CreateElementNode { - return [Messages.Type.CreateElementNode, id, parentID, index, tag, svg] -} - -export function CreateTextNode( - id: number, - parentID: number, - index: number, -): Messages.CreateTextNode { - return [Messages.Type.CreateTextNode, id, parentID, index] -} - -export function MoveNode(id: number, parentID: number, index: number): Messages.MoveNode { - return [Messages.Type.MoveNode, id, parentID, index] -} - -export function RemoveNode(id: number): Messages.RemoveNode { - return [Messages.Type.RemoveNode, id] -} - -export function SetNodeAttribute( - id: number, - name: string, - value: string, -): Messages.SetNodeAttribute { - return [Messages.Type.SetNodeAttribute, id, name, value] -} - -export function RemoveNodeAttribute(id: number, name: string): Messages.RemoveNodeAttribute { - return [Messages.Type.RemoveNodeAttribute, id, name] -} - -export function SetNodeData(id: number, data: string): Messages.SetNodeData { - return [Messages.Type.SetNodeData, id, data] -} - -export function SetNodeScroll(id: number, x: number, y: number): Messages.SetNodeScroll { - return [Messages.Type.SetNodeScroll, id, x, y] -} - -export function SetInputTarget(id: number, label: string): Messages.SetInputTarget { - return [Messages.Type.SetInputTarget, id, label] -} - -export function SetInputValue(id: number, value: string, mask: number): Messages.SetInputValue { - return [Messages.Type.SetInputValue, id, value, mask] -} - -export function SetInputChecked(id: number, checked: boolean): Messages.SetInputChecked { - return [Messages.Type.SetInputChecked, id, checked] -} - -export function MouseMove(x: number, y: number): Messages.MouseMove { - return [Messages.Type.MouseMove, x, y] -} - -export function ConsoleLog(level: string, value: string): Messages.ConsoleLog { - return [Messages.Type.ConsoleLog, level, value] -} - -export function PageLoadTiming( - requestStart: number, - responseStart: number, - responseEnd: number, - domContentLoadedEventStart: number, - domContentLoadedEventEnd: number, - loadEventStart: number, - loadEventEnd: number, - firstPaint: number, - firstContentfulPaint: number, -): Messages.PageLoadTiming { - return [ - Messages.Type.PageLoadTiming, - requestStart, - responseStart, - responseEnd, - domContentLoadedEventStart, - domContentLoadedEventEnd, - loadEventStart, - loadEventEnd, - firstPaint, - firstContentfulPaint, - ] -} - -export function PageRenderTiming( - speedIndex: number, - visuallyComplete: number, - timeToInteractive: number, -): Messages.PageRenderTiming { - return [Messages.Type.PageRenderTiming, speedIndex, visuallyComplete, timeToInteractive] -} - -export function JSException( - name: string, - message: string, - payload: string, - metadata: string, -): Messages.JSException { - return [Messages.Type.JSException, name, message, payload, metadata] -} - -export function RawCustomEvent(name: string, payload: string): Messages.RawCustomEvent { - return [Messages.Type.RawCustomEvent, name, payload] -} - -export function UserID(id: string): Messages.UserID { - return [Messages.Type.UserID, id] -} - -export function UserAnonymousID(id: string): Messages.UserAnonymousID { - return [Messages.Type.UserAnonymousID, id] -} - -export function Metadata(key: string, value: string): Messages.Metadata { - return [Messages.Type.Metadata, key, value] -} - -export function CSSInsertRule(id: number, rule: string, index: number): Messages.CSSInsertRule { - return [Messages.Type.CSSInsertRule, id, rule, index] -} - -export function CSSDeleteRule(id: number, index: number): Messages.CSSDeleteRule { - return [Messages.Type.CSSDeleteRule, id, index] -} - -export function Fetch( - method: string, - url: string, - request: string, - response: string, - status: number, - timestamp: number, - duration: number, -): Messages.Fetch { - return [Messages.Type.Fetch, method, url, request, response, status, timestamp, duration] -} - -export function Profiler( - name: string, - duration: number, - args: string, - result: string, -): Messages.Profiler { - return [Messages.Type.Profiler, name, duration, args, result] -} - -export function OTable(key: string, value: string): Messages.OTable { - return [Messages.Type.OTable, key, value] -} - -export function StateAction(type: string): Messages.StateAction { - return [Messages.Type.StateAction, type] -} - -export function Redux(action: string, state: string, duration: number): Messages.Redux { - return [Messages.Type.Redux, action, state, duration] -} - -export function Vuex(mutation: string, state: string): Messages.Vuex { - return [Messages.Type.Vuex, mutation, state] -} - -export function MobX(type: string, payload: string): Messages.MobX { - return [Messages.Type.MobX, type, payload] -} - -export function NgRx(action: string, state: string, duration: number): Messages.NgRx { - return [Messages.Type.NgRx, action, state, duration] -} - -export function GraphQL( - operationKind: string, - operationName: string, - variables: string, - response: string, -): Messages.GraphQL { - return [Messages.Type.GraphQL, operationKind, operationName, variables, response] -} - -export function PerformanceTrack( - frames: number, - ticks: number, - totalJSHeapSize: number, - usedJSHeapSize: number, -): Messages.PerformanceTrack { - return [Messages.Type.PerformanceTrack, frames, ticks, totalJSHeapSize, usedJSHeapSize] -} - -export function ResourceTiming( - timestamp: number, - duration: number, - ttfb: number, - headerSize: number, - encodedBodySize: number, - decodedBodySize: number, - url: string, - initiator: string, -): Messages.ResourceTiming { - return [ - Messages.Type.ResourceTiming, - timestamp, - duration, - ttfb, - headerSize, - encodedBodySize, - decodedBodySize, - url, - initiator, - ] -} - -export function ConnectionInformation( - downlink: number, - type: string, -): Messages.ConnectionInformation { - return [Messages.Type.ConnectionInformation, downlink, type] -} - -export function SetPageVisibility(hidden: boolean): Messages.SetPageVisibility { - return [Messages.Type.SetPageVisibility, hidden] -} - -export function LongTask( - timestamp: number, - duration: number, - context: number, - containerType: number, - containerSrc: string, - containerId: string, - containerName: string, -): Messages.LongTask { - return [ - Messages.Type.LongTask, - timestamp, - duration, - context, - containerType, - containerSrc, - containerId, - containerName, - ] -} - -export function SetNodeAttributeURLBased( - id: number, - name: string, - value: string, - baseURL: string, -): Messages.SetNodeAttributeURLBased { - return [Messages.Type.SetNodeAttributeURLBased, id, name, value, baseURL] -} - -export function SetCSSDataURLBased( - id: number, - data: string, - baseURL: string, -): Messages.SetCSSDataURLBased { - return [Messages.Type.SetCSSDataURLBased, id, data, baseURL] -} - -export function TechnicalInfo(type: string, value: string): Messages.TechnicalInfo { - return [Messages.Type.TechnicalInfo, type, value] -} - -export function CustomIssue(name: string, payload: string): Messages.CustomIssue { - return [Messages.Type.CustomIssue, name, payload] -} - -export function CSSInsertRuleURLBased( - id: number, - rule: string, - index: number, - baseURL: string, -): Messages.CSSInsertRuleURLBased { - return [Messages.Type.CSSInsertRuleURLBased, id, rule, index, baseURL] -} - -export function MouseClick( - id: number, - hesitationTime: number, - label: string, - selector: string, -): Messages.MouseClick { - return [Messages.Type.MouseClick, id, hesitationTime, label, selector] -} - -export function CreateIFrameDocument(frameID: number, id: number): Messages.CreateIFrameDocument { - return [Messages.Type.CreateIFrameDocument, frameID, id] -} diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index 069336ef7..ca08d54cd 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -1,7 +1,7 @@ import App, { DEFAULT_INGEST_POINT } from './app/index.js' export { default as App } from './app/index.js' -import { UserAnonymousID, RawCustomEvent, CustomIssue } from './app/messages.gen.js' +import { UserAnonymousID, CustomEvent, CustomIssue } from './app/messages.gen.js' import * as _Messages from './app/messages.gen.js' export const Messages = _Messages export { SanitizeLevel } from './app/sanitizer.js' @@ -22,6 +22,7 @@ import Viewport from './modules/viewport.js' import CSSRules from './modules/cssrules.js' import Focus from './modules/focus.js' import Fonts from './modules/fonts.js' +import Network from './modules/network.js' import ConstructedStyleSheets from './modules/constructedStyleSheets.js' import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js' @@ -31,6 +32,7 @@ import type { Options as ExceptionOptions } from './modules/exception.js' import type { Options as InputOptions } from './modules/input.js' import type { Options as PerformanceOptions } from './modules/performance.js' import type { Options as TimingOptions } from './modules/timing.js' +import type { Options as NetworkOptions } from './modules/network.js' import type { StartOptions } from './app/index.js' //TODO: unique options init import type { StartPromiseReturn } from './app/index.js' @@ -43,6 +45,7 @@ export type Options = Partial< sessionToken?: string respectDoNotTrack?: boolean autoResetOnWindowOpen?: boolean + network?: NetworkOptions // dev only __DISABLE_SECURE_MODE?: boolean } @@ -127,6 +130,7 @@ export default class API { Scroll(app) Focus(app) Fonts(app) + Network(app, options.network) ;(window as any).__OPENREPLAY__ = this if (options.autoResetOnWindowOpen) { @@ -259,7 +263,7 @@ export default class API { } catch (e) { return } - this.app.send(RawCustomEvent(key, payload)) + this.app.send(CustomEvent(key, payload)) } } } diff --git a/tracker/tracker/src/main/modules/network.ts b/tracker/tracker/src/main/modules/network.ts new file mode 100644 index 000000000..fdaed16c6 --- /dev/null +++ b/tracker/tracker/src/main/modules/network.ts @@ -0,0 +1,313 @@ +import type App from '../app/index.js' +import { NetworkRequest } from '../app/messages.gen.js' +import { getTimeOrigin } from '../utils.js' + +type WindowFetch = typeof window.fetch +type XHRRequestBody = Parameters[0] +type FetchRequestBody = RequestInit['body'] + +// Request: +// declare const enum BodyType { +// Blob = "Blob", +// ArrayBuffer = "ArrayBuffer", +// TypedArray = "TypedArray", +// DataView = "DataView", +// FormData = "FormData", +// URLSearchParams = "URLSearchParams", +// Document = "Document", // XHR only +// ReadableStream = "ReadableStream", // Fetch only +// Literal = "literal", +// Unknown = "unk", +// } +// 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 +// function objectifyBody(body: any): RequestBody { +// if (body instanceof Blob) { +// return { +// body: `; size: ${body.size}`, +// bodyType: BodyType.Blob, +// } +// } +// return { +// body, +// bodyType: BodyType.Literal, +// } +// } + +interface RequestData { + body: XHRRequestBody | FetchRequestBody + headers: Record +} +interface ResponseData { + body: any + headers: Record +} + +interface RequestResponseData { + readonly status: number + readonly method: string + url: string + request: RequestData + response: ResponseData +} + +interface XHRRequestData { + body: XHRRequestBody + headers: Record +} + +function getXHRRequestDataObject(xhr: XMLHttpRequest): XHRRequestData { + // @ts-ignore this is 3x faster than using Map + if (!xhr.__or_req_data__) { + // @ts-ignore + xhr.__or_req_data__ = { body: undefined, headers: {} } + } + // @ts-ignore + return xhr.__or_req_data__ +} + +function strMethod(method?: string) { + return typeof method === 'string' ? method.toUpperCase() : 'GET' +} + +type Sanitizer = (data: RequestResponseData) => RequestResponseData | null + +export interface Options { + sessionTokenHeader: string | boolean + failuresOnly: boolean + ignoreHeaders: Array | boolean + capturePayload: boolean + sanitizer?: Sanitizer +} + +export default function (app: App, opts: Partial = {}) { + const options: Options = Object.assign( + { + failuresOnly: false, + ignoreHeaders: ['Cookie', 'Set-Cookie', 'Authorization'], + capturePayload: false, + sessionTokenHeader: false, + }, + opts, + ) + + const ignoreHeaders = options.ignoreHeaders + const isHIgnored = Array.isArray(ignoreHeaders) + ? (name: string) => ignoreHeaders.includes(name) + : () => ignoreHeaders + + const stHeader = + options.sessionTokenHeader === true ? 'X-OpenReplay-SessionToken' : options.sessionTokenHeader + function setSessionTokenHeader(setRequestHeader: (name: string, value: string) => void) { + if (stHeader) { + const sessionToken = app.getSessionToken() + if (sessionToken) { + app.safe(setRequestHeader)(stHeader, sessionToken) + } + } + } + + function sanitize(reqResInfo: RequestResponseData) { + if (!options.capturePayload) { + delete reqResInfo.request.body + delete reqResInfo.response.body + } + if (options.sanitizer) { + const resBody = reqResInfo.response.body + if (typeof resBody === 'string') { + // Parse response in order to have handy view in sanitisation function + try { + reqResInfo.response.body = JSON.parse(resBody) + } catch {} + } + return options.sanitizer(reqResInfo) + } + return reqResInfo + } + + function stringify(r: RequestData | ResponseData): string { + if (r && typeof r.body !== 'string') { + try { + r.body = JSON.stringify(r.body) + } catch { + r.body = '' + app.notify.warn("Openreplay fetch couldn't stringify body:", r.body) + } + } + return JSON.stringify(r) + } + + /* ====== Fetch ====== */ + const origFetch = window.fetch.bind(window) as WindowFetch + window.fetch = (input, init = {}) => { + 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 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 + }) + } + 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 + }) + } + /* ====== <> ====== */ + + /* ====== XHR ====== */ + const nativeOpen = XMLHttpRequest.prototype.open + XMLHttpRequest.prototype.open = function (initMethod, 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(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) => {}) + return nativeOpen.apply(this, arguments) + } + const nativeSend = XMLHttpRequest.prototype.send + XMLHttpRequest.prototype.send = function (body) { + const rdo = getXHRRequestDataObject(this) + rdo.body = body + + return nativeSend.apply(this, arguments) + } + const nativeSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader + XMLHttpRequest.prototype.setRequestHeader = function (name, value) { + if (!isHIgnored(name)) { + const rdo = getXHRRequestDataObject(this) + rdo.headers[name] = value + } + + return nativeSetRequestHeader.apply(this, arguments) + } + /* ====== <> ====== */ +} diff --git a/tracker/tracker/src/main/modules/timing.ts b/tracker/tracker/src/main/modules/timing.ts index f2d2cbf11..89f26ee7c 100644 --- a/tracker/tracker/src/main/modules/timing.ts +++ b/tracker/tracker/src/main/modules/timing.ts @@ -1,6 +1,6 @@ import type App from '../app/index.js' import { hasTag } from '../app/guards.js' -import { isURL } from '../utils.js' +import { isURL, getTimeOrigin } from '../utils.js' import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js' // Inspired by https://github.com/WPO-Foundation/RUM-SpeedIndex/blob/master/src/rum-speedindex.js @@ -110,7 +110,7 @@ export default function (app: App, opts: Partial): void { } app.send( ResourceTiming( - entry.startTime + performance.timing.navigationStart, + entry.startTime + getTimeOrigin(), entry.duration, entry.responseStart && entry.startTime ? entry.responseStart - entry.startTime : 0, entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0, @@ -122,9 +122,7 @@ export default function (app: App, opts: Partial): void { ) } - const observer: PerformanceObserver = new PerformanceObserver((list) => - list.getEntries().forEach(resourceTiming), - ) + const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming)) let prevSessionID: string | undefined app.attachStartCallback(function ({ sessionID }) { @@ -165,6 +163,9 @@ export default function (app: App, opts: Partial): void { if (performance.timing.loadEventEnd || performance.now() > 30000) { pageLoadTimingSent = true const { + // should be ok to use here, (https://github.com/mdn/content/issues/4713) + // since it is compared with the values obtained on the page load (before any possible sleep state) + // deprecated though navigationStart, requestStart, responseStart, diff --git a/tracker/tracker/src/webworker/MessageEncoder.gen.ts b/tracker/tracker/src/webworker/MessageEncoder.gen.ts index 74e51f528..d7772d54a 100644 --- a/tracker/tracker/src/webworker/MessageEncoder.gen.ts +++ b/tracker/tracker/src/webworker/MessageEncoder.gen.ts @@ -86,6 +86,10 @@ export default class MessageEncoder extends PrimitiveEncoder { return this.uint(msg[1]) && this.uint(msg[2]) break + case Messages.Type.NetworkRequest: + return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.string(msg[5]) && this.uint(msg[6]) && this.uint(msg[7]) && this.uint(msg[8]) + break + case Messages.Type.ConsoleLog: return this.string(msg[1]) && this.string(msg[2]) break