feat(tracker-fetch):3.5.3: common improved sanitiser
This commit is contained in:
parent
c8872064ec
commit
d7710356e9
3 changed files with 170 additions and 77 deletions
|
|
@ -1,7 +1,6 @@
|
|||
# OpenReplay Tracker Fetch plugin
|
||||
# Fetch plugin for OpenReplay
|
||||
|
||||
Tracker plugin to support tracking of the `fetch` requests payload.
|
||||
Additionally it populates the requests with `sessionToken` header for backend logging.
|
||||
This plugin allows you to capture `fetch` payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -11,36 +10,120 @@ npm i @openreplay/tracker-fetch
|
|||
|
||||
## Usage
|
||||
|
||||
Initialize the `@openreplay/tracker` package as usual and load the plugin into it.
|
||||
Then you can use the provided `fetch` method from the plugin instead of built-in.
|
||||
Use the provided `fetch` method from the plugin instead of the one built-in.
|
||||
|
||||
### If your website is a Single Page Application (SPA)
|
||||
|
||||
```js
|
||||
import Tracker from '@openreplay/tracker';
|
||||
import tracker from '@openreplay/tracker';
|
||||
import trackerFetch from '@openreplay/tracker-fetch';
|
||||
|
||||
const tracker = new Tracker({
|
||||
projectKey: YOUR_PROJECT_KEY,
|
||||
const tracker = new OpenReplay({
|
||||
projectKey: PROJECT_KEY
|
||||
});
|
||||
const fetch = tracker.use(trackerFetch(options)); // check list of available options below
|
||||
|
||||
tracker.start();
|
||||
|
||||
export const fetch = tracker.use(trackerFetch({ /* options here*/ }));
|
||||
|
||||
fetch('https://my.api.io/resource').then(response => response.json()).then(body => console.log(body));
|
||||
fetch('https://myapi.com/').then(response => console.log(response.json()));
|
||||
```
|
||||
|
||||
Options:
|
||||
```ts
|
||||
{
|
||||
failuresOnly: boolean, // default false
|
||||
sessionTokenHeader: string | undefined, // default undefined
|
||||
ignoreHeaders: Array<string> | boolean, // default [ 'Cookie', 'Set-Cookie', 'Authorization' ]
|
||||
### If your web app is Server-Side-Rendered (SSR)
|
||||
|
||||
Follow the below example if your app is SSR. Ensure `tracker.start()` is called once the app is started (in `useEffect` or `componentDidMount`).
|
||||
|
||||
```js
|
||||
import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerFetch from '@openreplay/tracker-fetch/cjs';
|
||||
|
||||
const tracker = new OpenReplay({
|
||||
projectKey: PROJECT_KEY
|
||||
});
|
||||
const fetch = tracker.use(trackerFetch(options)); // check list of available options below
|
||||
|
||||
//...
|
||||
function MyApp() {
|
||||
useEffect(() => { // use componentDidMount in case of React Class Component
|
||||
tracker.start();
|
||||
|
||||
fetch('https://myapi.com/').then(response => console.log(response.json()));
|
||||
}, [])
|
||||
//...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Set `failuresOnly` option to `true` if you want to record only requests with the status code >= 400.
|
||||
## Options
|
||||
|
||||
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 fetch request and will contain OpenReplay session identificator value.
|
||||
```js
|
||||
trackerFetch({
|
||||
overrideGlobal: boolean;
|
||||
failuresOnly: boolean;
|
||||
sessionTokenHeader: string;
|
||||
ignoreHeaders: Array<string> | boolean;
|
||||
sanitiser: (RequestResponseData) => RequestResponseData | null;
|
||||
})
|
||||
```
|
||||
|
||||
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' ]`.
|
||||
- `overrideGlobal`: Overrides the default `window.fetch`. Default: `false`.
|
||||
- `failuresOnly`: Captures requests having 4xx-5xx HTTP status code. Default: `false`.
|
||||
- `sessionTokenHeader`: In case you have enabled some of our backend [integrations](/integrations) (i.e. Sentry), you can use this option to specify the header name (i.e. 'X-OpenReplay-SessionToken'). This latter gets appended automatically to each fetch request to contain the OpenReplay sessionToken's value. Default: `undefined`.
|
||||
- `ignoreHeaders`: Helps define a list of headers you don't wish to capture. Set its value to `false` to capture all of them (`true` if none). Default: `['Cookie', 'Set-Cookie', 'Authorization']` so sensitive headers won't be captured.
|
||||
- `sanitiser`: Sanitise sensitive data from fetch request/response or ignore request comletely. 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
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Having trouble setting up this plugin? please connect to our [Discord](https://discord.openreplay.com) and get help from our community.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-fetch",
|
||||
"description": "Tracker plugin for fetch requests recording ",
|
||||
"version": "3.5.2",
|
||||
"version": "3.5.3",
|
||||
"keywords": [
|
||||
"fetch",
|
||||
"logging",
|
||||
|
|
|
|||
|
|
@ -1,38 +1,48 @@
|
|||
import { App, Messages } from '@openreplay/tracker';
|
||||
|
||||
interface Request {
|
||||
url: string,
|
||||
body: string | Object,
|
||||
headers: Record<string, string>,
|
||||
interface RequestData {
|
||||
body: BodyInit | null | undefined
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
interface Response {
|
||||
url: string,
|
||||
status: number,
|
||||
body: string,
|
||||
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;
|
||||
replaceDefault: boolean; // overrideDefault ?
|
||||
failuresOnly: boolean;
|
||||
ignoreHeaders: Array<string> | boolean;
|
||||
requestSanitizer: ((Request) => Request | null) | null;
|
||||
responseSanitizer: ((Response) => Response | null) | null;
|
||||
sessionTokenHeader?: string
|
||||
failuresOnly: boolean
|
||||
overrideGlobal: boolean
|
||||
ignoreHeaders: Array<string> | boolean
|
||||
sanitiser?: (RequestResponseData) => RequestResponseData | null
|
||||
|
||||
requestSanitizer?: any
|
||||
responseSanitizer?: any
|
||||
}
|
||||
|
||||
export default function(opts: Partial<Options> = {}) {
|
||||
const options: Options = Object.assign(
|
||||
{
|
||||
replaceDefault: false,
|
||||
overrideGlobal: false,
|
||||
failuresOnly: false,
|
||||
ignoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ],
|
||||
requestSanitizer: null,
|
||||
responseSanitizer: null,
|
||||
},
|
||||
opts,
|
||||
);
|
||||
if (options.requestSanitizer && options.responseSanitizer) {
|
||||
console.warn("OpenReplay fetch plugin: `requestSanitizer` and `responseSanitizer` options are depricated. Please, use `sanitiser` instead (check out documentation at https://docs.openreplay.com/plugins/fetch).")
|
||||
}
|
||||
|
||||
const origFetch = window.fetch
|
||||
return (app: App | null) => {
|
||||
if (app === null) {
|
||||
|
|
@ -90,56 +100,55 @@ export default function(opts: Partial<Options> = {}) {
|
|||
r.headers.forEach((v, n) => { if (!isHIgnoring(n)) resHs[n] = v })
|
||||
}
|
||||
|
||||
// Request forming
|
||||
let reqBody = ''
|
||||
if (typeof init.body === 'string') {
|
||||
reqBody = init.body
|
||||
} else if (typeof init.body === 'object') {
|
||||
try {
|
||||
reqBody = JSON.stringify(init.body)
|
||||
} catch {}
|
||||
}
|
||||
let req: Request | null = {
|
||||
url: input,
|
||||
const req: RequestData = {
|
||||
headers: reqHs,
|
||||
body: reqBody,
|
||||
}
|
||||
if (options.requestSanitizer !== null) {
|
||||
req = options.requestSanitizer(req)
|
||||
if (!req) {
|
||||
return
|
||||
}
|
||||
body: init.body,
|
||||
}
|
||||
|
||||
// Response forming
|
||||
let res: Response | null = {
|
||||
url: input,
|
||||
status: r.status,
|
||||
const res: ResponseData = {
|
||||
headers: resHs,
|
||||
body: text,
|
||||
}
|
||||
if (options.responseSanitizer !== null) {
|
||||
res = options.responseSanitizer(res)
|
||||
if (!res) {
|
||||
|
||||
const method = typeof init.method === 'string'
|
||||
? init.method.toUpperCase()
|
||||
: 'GET'
|
||||
let reqResInfo: RequestResponseData | null = {
|
||||
url: input,
|
||||
method,
|
||||
status: r.status,
|
||||
request: req,
|
||||
response: res,
|
||||
}
|
||||
if (options.sanitiser) {
|
||||
try {
|
||||
reqResInfo.response.body = JSON.parse(text) as Object // Why the returning type is "any"?
|
||||
} catch {}
|
||||
reqResInfo = options.sanitiser(reqResInfo)
|
||||
if (!reqResInfo) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const reqStr = JSON.stringify({
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
})
|
||||
const resStr = JSON.stringify({
|
||||
headers: res.headers,
|
||||
body: res.body,
|
||||
})
|
||||
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 init.method === 'string' ? init.method.toUpperCase() : 'GET',
|
||||
input,
|
||||
reqStr,
|
||||
resStr,
|
||||
method,
|
||||
String(reqResInfo.url),
|
||||
getStj(reqResInfo.request),
|
||||
getStj(reqResInfo.response),
|
||||
r.status,
|
||||
startTime + performance.timing.navigationStart,
|
||||
duration,
|
||||
|
|
@ -147,8 +156,9 @@ export default function(opts: Partial<Options> = {}) {
|
|||
)
|
||||
});
|
||||
return response;
|
||||
};
|
||||
if (options.replaceDefault) {
|
||||
}
|
||||
|
||||
if (options.overrideGlobal) {
|
||||
window.fetch = fetch
|
||||
}
|
||||
return fetch;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue