feat(tracker): new axios capturing; tracker 7.0.1
This commit is contained in:
parent
6d0fef3c67
commit
2a2ffa1dcd
4 changed files with 167 additions and 15 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
144
tracker/tracker/src/main/modules/axiosSpy.ts
Normal file
144
tracker/tracker/src/main/modules/axiosSpy.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue