feat(tracker-axios):3.5.0:sanitiser
This commit is contained in:
parent
970cfa352e
commit
4c8ab3d381
3 changed files with 124 additions and 14 deletions
|
|
@ -32,14 +32,72 @@ Options:
|
|||
captureWhen: (AxiosRequestConfig) => boolean; // default: () => true
|
||||
sessionTokenHeader: string; // default: undefined
|
||||
ignoreHeaders: Array<string> | boolean, // default [ 'Cookie', 'Set-Cookie', 'Authorization' ]
|
||||
sanitiser: (RequestResponseData) => RequestResponseData | null, // default: undefined
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
By default plugin connects to the static `axios` instance, but you can specify one with the `instance` option.
|
||||
|
||||
Set `failuresOnly` option to `true` if you want to record only failed requests, when the axios' promise is rejected. You can also [regulate](https://github.com/axios/axios#request-config) axios failing behaviour with the `validateStatus` option.
|
||||
|
||||
`captureWhen` parameter allows you to set a filter on what should be captured. The function will be called with the axios config object and expected to return `true` or `false`.
|
||||
`captureWhen` parameter allows you to set a filter on request should be captured. The function will be called with the axios config object and expected to return `true` or `false`.
|
||||
|
||||
In case you use [OpenReplay integrations (sentry, bugsnag or others)](https://docs.openreplay.com/integrations), you can use `sessionTokenHeader` option to specify the header name. This header will be appended automatically to the each axios request and will contain OpenReplay session identificator value.
|
||||
|
||||
You can define list of headers that you don't want to capture with the `ignoreHeaders` options. Set its value to `false` if you want to catch them all (`true` if opposite). By default plugin ignores the list of headers that might be sensetive such as `[ 'Cookie', 'Set-Cookie', 'Authorization' ]`.
|
||||
|
||||
Sanitise sensitive data from fetch request/response or ignore request comletely with `sanitiser`. You can redact fields on the request object by modifying then returning it from the function:
|
||||
|
||||
```typescript
|
||||
interface RequestData {
|
||||
body: BodyInit | null | undefined; // whatewer you've put in the init.body in fetch(url, init)
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
body: string | Object | null; // Object if response is of JSON type
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface RequestResponseData {
|
||||
readonly status: number;
|
||||
readonly method: string;
|
||||
url: string;
|
||||
request: RequestData;
|
||||
response: ResponseData;
|
||||
}
|
||||
|
||||
sanitiser: (data: RequestResponseData) => { // sanitise the body or headers
|
||||
if (data.url === "/auth") {
|
||||
data.request.body = null
|
||||
}
|
||||
|
||||
if (data.request.headers['x-auth-token']) { // can also use ignoreHeaders option instead
|
||||
data.request.headers['x-auth-token'] = 'SANITISED';
|
||||
}
|
||||
|
||||
// Sanitise response
|
||||
if (data.status < 400 && data.response.body.token) {
|
||||
data.response.body.token = "<TOKEN>"
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// OR
|
||||
|
||||
sanitiser: data => { // ignore requests that start with /secure
|
||||
if (data.url.startsWith("/secure")) {
|
||||
return null
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// OR
|
||||
|
||||
sanitiser: data => { // sanitise request url: replace all numbers
|
||||
data.url = data.url.replace(/\d/g, "*")
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-axios",
|
||||
"description": "Tracker plugin for axios requests recording",
|
||||
"version": "3.4.3",
|
||||
"version": "3.5.0",
|
||||
"keywords": [
|
||||
"axios",
|
||||
"logging",
|
||||
|
|
|
|||
|
|
@ -4,12 +4,32 @@ import { App, Messages } from '@openreplay/tracker';
|
|||
import { getExceptionMessage } from '@openreplay/tracker/lib/modules/exception.js'; // TODO: export from tracker root
|
||||
import { buildFullPath } from './url.js';
|
||||
|
||||
|
||||
interface RequestData {
|
||||
body: BodyInit | null | undefined
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
body: string | Object | null
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
interface RequestResponseData {
|
||||
readonly status: number
|
||||
readonly method: string
|
||||
url: string
|
||||
request: RequestData
|
||||
response: ResponseData
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
sessionTokenHeader?: string;
|
||||
instance: AxiosInstance;
|
||||
failuresOnly: boolean;
|
||||
captureWhen: (AxiosRequestConfig) => boolean;
|
||||
ignoreHeaders: Array<string> | boolean;
|
||||
sanitiser?: (RequestResponseData) => RequestResponseData | null;
|
||||
}
|
||||
|
||||
// TODO: test webpack 5 for axios imports
|
||||
|
|
@ -27,6 +47,7 @@ export default function(opts: Partial<Options> = {}) {
|
|||
failuresOnly: false,
|
||||
captureWhen: () => true,
|
||||
ignoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ],
|
||||
sanitiser: null,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
|
|
@ -88,22 +109,53 @@ export default function(opts: Partial<Options> = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: split the code on functions & files
|
||||
// Why can't axios propogate the final request URL somewhere?
|
||||
const fullURL = buildFullPath(res.config.baseURL, options.instance.getUri(res.config));
|
||||
const url = buildFullPath(res.config.baseURL, options.instance.getUri(res.config))
|
||||
const method = typeof res.config.method === 'string' ? res.config.method.toUpperCase() : 'GET'
|
||||
const status = res.status
|
||||
let reqResData: RequestResponseData | null = {
|
||||
status,
|
||||
method,
|
||||
url,
|
||||
request: {
|
||||
headers: reqHs,
|
||||
body: reqBody,
|
||||
},
|
||||
response: {
|
||||
headers: resHs,
|
||||
body: resBody,
|
||||
},
|
||||
}
|
||||
if (options.sanitiser) {
|
||||
try {
|
||||
reqResData.response.body = JSON.parse(resBody) as Object // Why the returning type is "any"?
|
||||
} catch {}
|
||||
reqResData = options.sanitiser(reqResData)
|
||||
if (!reqResData) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const getStj = (r: RequestData | ResponseData): string => {
|
||||
if (r && typeof r.body !== 'string') {
|
||||
try {
|
||||
r.body = JSON.stringify(r.body)
|
||||
} catch {
|
||||
r.body = "<unable to stringify>"
|
||||
//app.log.warn("Openreplay fetch") // TODO: version check
|
||||
}
|
||||
}
|
||||
return JSON.stringify(r)
|
||||
}
|
||||
|
||||
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,
|
||||
method,
|
||||
String(reqResData.url),
|
||||
getStj(reqResData.request),
|
||||
getStj(reqResData.response),
|
||||
status,
|
||||
startTime + performance.timing.navigationStart,
|
||||
duration,
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue