import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; import { App, Messages } from '@openreplay/tracker'; import { getExceptionMessage } from '@openreplay/tracker/lib/modules/exception'; // TODO: export from tracker root import { buildFullPath } from './url'; export interface Options { sessionTokenHeader?: string; instance: AxiosInstance; failuresOnly: boolean; captureWhen: (AxiosRequestConfig) => boolean; ignoreHeaders: Array | boolean; } export default function(opts: Partial = {}) { const options: Options = Object.assign( { instance: axios, failuresOnly: false, captureWhen: () => true, ignoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ], }, opts, ); return (app: App | null) => { if (app === null) { return; } const ihOpt = options.ignoreHeaders const isHIgnoring = Array.isArray(ihOpt) ? name => ihOpt.includes(name) : () => ihOpt const sendFetchMessage = (res: AxiosResponse) => { // @ts-ignore const startTime: number = res.config.__openreplayStartTs; const duration = performance.now() - startTime; if (typeof startTime !== 'number') { return; } let reqBody: string = ''; if (typeof res.config.data === 'string') { reqBody = res.config.data; } else { try { reqBody = JSON.stringify(res.config.data) || ''; } catch (e) {} // TODO: app debug } let resBody: string = ''; if (typeof res.data === 'string') { resBody = res.data; } else { try { resBody = JSON.stringify(res.data) || ''; } catch (e) {} } const reqHs: Record = {} const resHs: Record = {} // TODO: type safe axios headers if (ihOpt !== true) { function writeReqHeader([n, v]: [string, string]) { if (!isHIgnoring(n)) { reqHs[n] = v } } if (res.config.headers instanceof Headers) { res.config.headers.forEach((v, n) => writeReqHeader([n, v])) } else if (Array.isArray(res.config.headers)) { res.config.headers.forEach(writeReqHeader); } else if (typeof res.config.headers === 'object') { Object.entries(res.config.headers as Record).forEach(writeReqHeader) } // TODO: type safe axios headers if (typeof res.headers === 'object') { Object.entries(res.headers as Record).forEach(([v, n]) => { if (!isHIgnoring(n)) resHs[n] = v }) } } // Why can't axios propogate the final request URL somewhere? const fullURL = buildFullPath(res.config.baseURL, options.instance.getUri(res.config)); app.send( Messages.Fetch( typeof res.config.method === 'string' ? res.config.method.toUpperCase() : 'GET', fullURL, JSON.stringify({ headers: reqHs, body: reqBody, }), JSON.stringify({ headers: resHs, body: resBody, }), res.status, startTime + performance.timing.navigationStart, duration, ), ); } // TODO: why app.safe doesn't work here? options.instance.interceptors.request.use(function (config) { if (options.sessionTokenHeader) { const sessionToken = app.getSessionToken(); if (sessionToken) { if (config.headers === undefined) { config.headers = {}; } if (config.headers instanceof Headers) { config.headers.append(options.sessionTokenHeader, sessionToken); } else if (Array.isArray(config.headers)) { config.headers.push([options.sessionTokenHeader, sessionToken]); } else if (typeof config.headers === 'object') { config.headers[options.sessionTokenHeader] = sessionToken; } } } if (options.captureWhen(config)) { // TODO: use runWhen as axios publish a version with it // @ts-ignore config.__openreplayStartTs = performance.now(); } return config; }, function (error) { if (error instanceof Error) { app.send(getExceptionMessage(error, [])); } return Promise.reject(error); }, // { synchronous: true, /* runWhen: captureWhen // is not published in axios yet */ } ); options.instance.interceptors.response.use(function (response) { if (!options.failuresOnly) { sendFetchMessage(response); } return response; }, function (error) { if (axios.isAxiosError(error) && error.response) { sendFetchMessage(error.response) } else if (!axios.isCancel(error) && error instanceof Error) { app.send(getExceptionMessage(error, [])); } return Promise.reject(error); }); } }