openreplay/tracker/tracker-axios/src/index.ts
2021-10-04 15:12:36 +05:30

151 lines
No EOL
4.8 KiB
TypeScript

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<string> | boolean;
}
export default function(opts: Partial<Options> = {}) {
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<string, string> = {}
const resHs: Record<string, string> = {}
// 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<string, string>).forEach(writeReqHeader)
}
// TODO: type safe axios headers
if (typeof res.headers === 'object') {
Object.entries(res.headers as Record<string, string>).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);
});
}
}