feat(tracker):fetch/xhr module in core
This commit is contained in:
parent
e6ad085f2e
commit
6e1fec8013
10 changed files with 382 additions and 350 deletions
|
|
@ -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 %>(
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
313
tracker/tracker/src/main/modules/network.ts
Normal file
313
tracker/tracker/src/main/modules/network.ts
Normal 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)
|
||||
}
|
||||
/* ====== <> ====== */
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue