feat(tracker):fetch/xhr module in core

This commit is contained in:
Alex Kaminskii 2022-12-12 16:03:44 +01:00
parent e6ad085f2e
commit 6e1fec8013
10 changed files with 382 additions and 350 deletions

View file

@ -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 %>(

View file

@ -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',
},
};

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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]
}

View file

@ -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))
}
}
}

View file

@ -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<XMLHttpRequest['send']>[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: `<Blob type: ${body.type}>; size: ${body.size}`,
// bodyType: BodyType.Blob,
// }
// }
// return {
// body,
// bodyType: BodyType.Literal,
// }
// }
interface RequestData {
body: XHRRequestBody | FetchRequestBody
headers: Record<string, string>
}
interface ResponseData {
body: any
headers: Record<string, string>
}
interface RequestResponseData {
readonly status: number
readonly method: string
url: string
request: RequestData
response: ResponseData
}
interface XHRRequestData {
body: XHRRequestBody
headers: Record<string, string>
}
function getXHRRequestDataObject(xhr: XMLHttpRequest): XHRRequestData {
// @ts-ignore this is 3x faster than using Map<XHR, XHRRequestData>
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<string> | boolean
capturePayload: boolean
sanitizer?: Sanitizer
}
export default function (app: App, opts: Partial<Options> = {}) {
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 = '<unable to stringify>'
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<string, string> = {}
const resHs: Record<string, string> = {}
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<string, string>,
)
: {}
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)
}
/* ====== <> ====== */
}

View file

@ -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<Options>): 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<Options>): 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<Options>): 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,

View file

@ -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