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) {
|
if (!this.isClickmap) {
|
||||||
this.store.update({ devtoolsLoading: true })
|
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
|
// TODO: also in case of dynamic update through assist
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// @ts-ignore ?
|
// @ts-ignore ?
|
||||||
|
|
@ -91,6 +91,7 @@ export default class MessageLoader {
|
||||||
: { url: this.session.mobsUrl, parser: () => this.createNewParser(false, 'dom') }
|
: { url: this.session.mobsUrl, parser: () => this.createNewParser(false, 'dom') }
|
||||||
|
|
||||||
const parser = loadMethod.parser()
|
const parser = loadMethod.parser()
|
||||||
|
const devtoolsParser = this.createNewParser(true, 'devtools')
|
||||||
/**
|
/**
|
||||||
* We load first dom mob file before the rest
|
* We load first dom mob file before the rest
|
||||||
* to speed up time to replay
|
* to speed up time to replay
|
||||||
|
|
@ -100,7 +101,7 @@ export default class MessageLoader {
|
||||||
try {
|
try {
|
||||||
await loadFiles([loadMethod.url[0]], parser)
|
await loadFiles([loadMethod.url[0]], parser)
|
||||||
const restDomFilesPromise = this.loadDomFiles([...loadMethod.url.slice(1)], parser)
|
const restDomFilesPromise = this.loadDomFiles([...loadMethod.url.slice(1)], parser)
|
||||||
const restDevtoolsFilesPromise = this.loadDevtools()
|
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser)
|
||||||
|
|
||||||
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise])
|
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise])
|
||||||
this.messageManager.onFileReadSuccess()
|
this.messageManager.onFileReadSuccess()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# 7.0.1
|
# 7.0.1
|
||||||
|
|
||||||
- fix time inputs capturing
|
- 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
|
# 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 type App from '../app/index.js'
|
||||||
import { NetworkRequest } from '../app/messages.gen.js'
|
import { NetworkRequest } from '../app/messages.gen.js'
|
||||||
import { getTimeOrigin } from '../utils.js'
|
import { getTimeOrigin } from '../utils.js'
|
||||||
|
import type { AxiosInstance } from './axiosSpy.js'
|
||||||
|
import axiosSpy from './axiosSpy.js'
|
||||||
|
|
||||||
type WindowFetch = typeof window.fetch
|
type WindowFetch = typeof window.fetch
|
||||||
type XHRRequestBody = Parameters<XMLHttpRequest['send']>[0]
|
type XHRRequestBody = Parameters<XMLHttpRequest['send']>[0]
|
||||||
|
|
@ -45,7 +47,7 @@ interface ResponseData {
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestResponseData {
|
export interface RequestResponseData {
|
||||||
readonly status: number
|
readonly status: number
|
||||||
readonly method: string
|
readonly method: string
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -81,6 +83,7 @@ export interface Options {
|
||||||
capturePayload: boolean
|
capturePayload: boolean
|
||||||
captureInIframes: boolean
|
captureInIframes: boolean
|
||||||
sanitizer?: Sanitizer
|
sanitizer?: Sanitizer
|
||||||
|
axiosInstances?: Array<AxiosInstance>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (app: App, opts: Partial<Options> = {}) {
|
export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
|
|
@ -91,6 +94,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
capturePayload: false,
|
capturePayload: false,
|
||||||
sessionTokenHeader: false,
|
sessionTokenHeader: false,
|
||||||
captureInIframes: true,
|
captureInIframes: true,
|
||||||
|
axiosInstances: undefined,
|
||||||
},
|
},
|
||||||
opts,
|
opts,
|
||||||
)
|
)
|
||||||
|
|
@ -237,8 +241,9 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
/* ====== <> ====== */
|
/* ====== <> ====== */
|
||||||
|
|
||||||
/* ====== XHR ====== */
|
/* ====== XHR ====== */
|
||||||
|
|
||||||
const nativeOpen = context.XMLHttpRequest.prototype.open
|
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) {
|
function trackXMLHttpReqOpen(this: XMLHttpRequest, initMethod: string, url: string | URL) {
|
||||||
const xhr = this
|
const xhr = this
|
||||||
|
|
@ -304,10 +309,6 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
return nativeOpen.apply(this, arguments)
|
return nativeOpen.apply(this, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.XMLHttpRequest.prototype.open = trackXMLHttpReqOpen
|
|
||||||
|
|
||||||
const nativeSend = context.XMLHttpRequest.prototype.send
|
|
||||||
|
|
||||||
function trackXHRSend(
|
function trackXHRSend(
|
||||||
this: XMLHttpRequest,
|
this: XMLHttpRequest,
|
||||||
body: Document | XMLHttpRequestBodyInit | null | undefined,
|
body: Document | XMLHttpRequestBodyInit | null | undefined,
|
||||||
|
|
@ -318,10 +319,6 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
return nativeSend.apply(this, arguments)
|
return nativeSend.apply(this, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.XMLHttpRequest.prototype.send = trackXHRSend
|
|
||||||
|
|
||||||
const nativeSetRequestHeader = context.XMLHttpRequest.prototype.setRequestHeader
|
|
||||||
|
|
||||||
function trackSetReqHeader(this: XMLHttpRequest, name: string, value: string) {
|
function trackSetReqHeader(this: XMLHttpRequest, name: string, value: string) {
|
||||||
if (!isHIgnored(name)) {
|
if (!isHIgnored(name)) {
|
||||||
const rdo = getXHRRequestDataObject(this)
|
const rdo = getXHRRequestDataObject(this)
|
||||||
|
|
@ -330,12 +327,21 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
return nativeSetRequestHeader.apply(this, arguments)
|
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)
|
patchWindow(window)
|
||||||
|
if (options.axiosInstances) {
|
||||||
|
options.axiosInstances.forEach((axiosInstance) => {
|
||||||
|
axiosSpy(app, axiosInstance, options, sanitize, stringify)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (options.captureInIframes) {
|
if (options.captureInIframes) {
|
||||||
app.observer.attachContextCallback(app.safe(patchWindow))
|
app.observer.attachContextCallback(app.safe(patchWindow))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue