feat(tracker): new axios capturing; tracker 7.0.1

This commit is contained in:
nick-delirium 2023-05-17 13:41:29 +02:00
parent 6d0fef3c67
commit 2a2ffa1dcd
4 changed files with 167 additions and 15 deletions

View file

@ -69,10 +69,10 @@ export default class MessageLoader {
}
}
loadDevtools() {
loadDevtools(parser: (b: Uint8Array) => Promise<void>) {
if (!this.isClickmap) {
this.store.update({ devtoolsLoading: true })
return loadFiles(this.session.devtoolsURL, this.createNewParser(true, 'devtools'))
return loadFiles(this.session.devtoolsURL, parser)
// TODO: also in case of dynamic update through assist
.then(() => {
// @ts-ignore ?
@ -91,6 +91,7 @@ export default class MessageLoader {
: { url: this.session.mobsUrl, parser: () => this.createNewParser(false, 'dom') }
const parser = loadMethod.parser()
const devtoolsParser = this.createNewParser(true, 'devtools')
/**
* We load first dom mob file before the rest
* to speed up time to replay
@ -100,7 +101,7 @@ export default class MessageLoader {
try {
await loadFiles([loadMethod.url[0]], parser)
const restDomFilesPromise = this.loadDomFiles([...loadMethod.url.slice(1)], parser)
const restDevtoolsFilesPromise = this.loadDevtools()
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser)
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise])
this.messageManager.onFileReadSuccess()

View file

@ -1,7 +1,8 @@
# 7.0.1
- fix time inputs capturing
- add option to disable network tracking inside iframes
- add option `{ network: { captureInIframes: boolean } }` to disable network tracking inside iframes (default true)
- added option `{ network: { axiosInstances: AxiosInstance[] } }` to include custom axios instances for better tracking
# 7.0.0

View file

@ -0,0 +1,144 @@
import type App from '../app/index.js'
import { NetworkRequest } from '../app/messages.gen.js'
import { getTimeOrigin } from '../utils.js'
import type { RequestResponseData, Options } from './network.js'
interface RawAxiosHeaders {
[key: string]: string
}
interface AxiosRequestConfig {
url: string
method?: string
baseURL?: string
status?: number
headers: {
toJSON(): RawAxiosHeaders
}
params?: any
data?: any
}
interface InternalAxiosRequestConfig extends AxiosRequestConfig {
__openreplay_timing: number
headers: {
toJSON(): RawAxiosHeaders
set(name: string, value: string): void
}
}
interface AxiosResponse<T = any> {
data: T
status: number
statusText: string
headers: {
toJSON(): RawAxiosHeaders
}
config: InternalAxiosRequestConfig
request?: any
response?: AxiosRequestConfig
}
export interface AxiosInstance extends Record<string, any> {
interceptors: {
request: AxiosInterceptorManager<InternalAxiosRequestConfig>
response: AxiosInterceptorManager<AxiosResponse>
}
}
export interface AxiosInterceptorOptions {
synchronous?: boolean
}
export interface AxiosInterceptorManager<V> {
use(
onFulfilled?: ((value: V) => V | Promise<V>) | null,
onRejected?: ((error: any) => any) | null,
options?: AxiosInterceptorOptions,
): number
eject?: (id: number) => void
clear?: () => void
}
export default function (
app: App,
instance: AxiosInstance,
opts: Options,
sanitize: (data: RequestResponseData) => RequestResponseData | null,
stringify: (data: { headers: Record<string, string>; body: any }) => string,
) {
function captureResponseData(axiosResponseObj: AxiosResponse) {
const { headers: reqHs, data: reqData, method, url } = axiosResponseObj.config
const { data: rData, headers: rHs, status: globStatus, response } = axiosResponseObj
const { data: resData, headers: resHs, status: resStatus } = response || {}
const reqResInfo = sanitize({
url,
method: method || '',
status: globStatus || resStatus || 0,
request: {
headers: reqHs.toJSON(),
body: reqData,
},
response: {
headers: resHs?.toJSON() || rHs.toJSON(),
body: resData || rData,
},
})
if (!reqResInfo) {
return
}
const requestStart = axiosResponseObj.config.__openreplay_timing
const duration = performance.now() - requestStart
app.send(
NetworkRequest(
'xhr',
String(method),
String(reqResInfo.url),
stringify(reqResInfo.request),
stringify(reqResInfo.response),
reqResInfo.status,
requestStart + getTimeOrigin(),
duration,
),
)
}
function getStartTime(config: InternalAxiosRequestConfig) {
config.__openreplay_timing = performance.now()
if (opts.sessionTokenHeader) {
const header =
typeof opts.sessionTokenHeader === 'string'
? opts.sessionTokenHeader
: 'X-OpenReplay-Session-Token'
const headerValue = app.getSessionToken()
if (headerValue) {
config.headers.set(header, headerValue)
}
}
return config
}
function captureNetworkRequest(response: AxiosResponse) {
if (opts.failuresOnly) return response
captureResponseData(response)
return response
}
function captureNetworkError(error: any) {
captureResponseData(error as AxiosResponse)
return Promise.reject(error)
}
const reqInt = instance.interceptors.request.use(getStartTime, null, { synchronous: true })
const resInt = instance.interceptors.response.use(captureNetworkRequest, captureNetworkError, {
synchronous: true,
})
app.attachStopCallback(() => {
instance.interceptors.request.eject?.(reqInt)
instance.interceptors.response.eject?.(resInt)
})
}

View file

@ -1,6 +1,8 @@
import type App from '../app/index.js'
import { NetworkRequest } from '../app/messages.gen.js'
import { getTimeOrigin } from '../utils.js'
import type { AxiosInstance } from './axiosSpy.js'
import axiosSpy from './axiosSpy.js'
type WindowFetch = typeof window.fetch
type XHRRequestBody = Parameters<XMLHttpRequest['send']>[0]
@ -45,7 +47,7 @@ interface ResponseData {
headers: Record<string, string>
}
interface RequestResponseData {
export interface RequestResponseData {
readonly status: number
readonly method: string
url: string
@ -81,6 +83,7 @@ export interface Options {
capturePayload: boolean
captureInIframes: boolean
sanitizer?: Sanitizer
axiosInstances?: Array<AxiosInstance>
}
export default function (app: App, opts: Partial<Options> = {}) {
@ -91,6 +94,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
capturePayload: false,
sessionTokenHeader: false,
captureInIframes: true,
axiosInstances: undefined,
},
opts,
)
@ -237,8 +241,9 @@ export default function (app: App, opts: Partial<Options> = {}) {
/* ====== <> ====== */
/* ====== XHR ====== */
const nativeOpen = context.XMLHttpRequest.prototype.open
const nativeSetRequestHeader = context.XMLHttpRequest.prototype.setRequestHeader
const nativeSend = context.XMLHttpRequest.prototype.send
function trackXMLHttpReqOpen(this: XMLHttpRequest, initMethod: string, url: string | URL) {
const xhr = this
@ -304,10 +309,6 @@ export default function (app: App, opts: Partial<Options> = {}) {
return nativeOpen.apply(this, arguments)
}
context.XMLHttpRequest.prototype.open = trackXMLHttpReqOpen
const nativeSend = context.XMLHttpRequest.prototype.send
function trackXHRSend(
this: XMLHttpRequest,
body: Document | XMLHttpRequestBodyInit | null | undefined,
@ -318,10 +319,6 @@ export default function (app: App, opts: Partial<Options> = {}) {
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)
@ -330,12 +327,21 @@ export default function (app: App, opts: Partial<Options> = {}) {
return nativeSetRequestHeader.apply(this, arguments)
}
context.XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader
if (!options.axiosInstances) {
context.XMLHttpRequest.prototype.open = trackXMLHttpReqOpen
context.XMLHttpRequest.prototype.send = trackXHRSend
context.XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader
}
/* ====== <> ====== */
}
patchWindow(window)
if (options.axiosInstances) {
options.axiosInstances.forEach((axiosInstance) => {
axiosSpy(app, axiosInstance, options, sanitize, stringify)
})
}
if (options.captureInIframes) {
app.observer.attachContextCallback(app.safe(patchWindow))