From d7710356e94505e2146528412f9517709168b2f2 Mon Sep 17 00:00:00 2001 From: ShiKhu Date: Fri, 1 Apr 2022 22:15:51 +0200 Subject: [PATCH] feat(tracker-fetch):3.5.3: common improved sanitiser --- tracker/tracker-fetch/README.md | 123 ++++++++++++++++++++++++----- tracker/tracker-fetch/package.json | 2 +- tracker/tracker-fetch/src/index.ts | 122 +++++++++++++++------------- 3 files changed, 170 insertions(+), 77 deletions(-) diff --git a/tracker/tracker-fetch/README.md b/tracker/tracker-fetch/README.md index b7fca2e4b..53fddcf8e 100644 --- a/tracker/tracker-fetch/README.md +++ b/tracker/tracker-fetch/README.md @@ -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 | 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 | 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; +} + +interface ResponseData { + body: string | Object | null; // Object if response is of JSON type + headers: Record; +} + +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 = "" + } + + 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. \ No newline at end of file diff --git a/tracker/tracker-fetch/package.json b/tracker/tracker-fetch/package.json index 0b1373edc..c13b1a28b 100644 --- a/tracker/tracker-fetch/package.json +++ b/tracker/tracker-fetch/package.json @@ -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", diff --git a/tracker/tracker-fetch/src/index.ts b/tracker/tracker-fetch/src/index.ts index 93e39cce5..922913923 100644 --- a/tracker/tracker-fetch/src/index.ts +++ b/tracker/tracker-fetch/src/index.ts @@ -1,38 +1,48 @@ import { App, Messages } from '@openreplay/tracker'; -interface Request { - url: string, - body: string | Object, - headers: Record, +interface RequestData { + body: BodyInit | null | undefined + headers: Record } -interface Response { - url: string, - status: number, - body: string, - headers: Record, +interface ResponseData { + body: string | Object | null + headers: Record } +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 | boolean; - requestSanitizer: ((Request) => Request | null) | null; - responseSanitizer: ((Response) => Response | null) | null; + sessionTokenHeader?: string + failuresOnly: boolean + overrideGlobal: boolean + ignoreHeaders: Array | boolean + sanitiser?: (RequestResponseData) => RequestResponseData | null + + requestSanitizer?: any + responseSanitizer?: any } export default function(opts: Partial = {}) { 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 = {}) { 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 = "" + //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 = {}) { ) }); return response; - }; - if (options.replaceDefault) { + } + + if (options.overrideGlobal) { window.fetch = fetch } return fetch;