From 8d40e52010f9e1986bbc29620e04054e99a87178 Mon Sep 17 00:00:00 2001 From: Peter Chen <13093023233@163.com> Date: Sat, 7 Aug 2021 23:06:50 +0800 Subject: [PATCH] fix: fix console plugin's OOM problem (#656) * fix: fix console plugin's OOM problem * fix: fix console plugin * feat: patch * feat: patch * feat: patch Co-authored-by: chenyangbj01 --- .gitignore | 3 ++ docs/recipes/console.md | 13 ++--- docs/recipes/console.zh_CN.md | 13 ++--- .../rrweb/src/plugins/console/record/index.ts | 9 +++- .../src/plugins/console/record/stringify.ts | 50 ++++++++++++++++--- .../typings/plugins/console/record/index.d.ts | 4 +- 6 files changed, 70 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 753186ec16..3a22b849b7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ temp .env .DS_Store + +build +dist diff --git a/docs/recipes/console.md b/docs/recipes/console.md index 5cace407b3..a9fa14b6d1 100644 --- a/docs/recipes/console.md +++ b/docs/recipes/console.md @@ -38,6 +38,7 @@ rrweb.record({ stringifyOptions: { stringLengthLimit: 1000, numOfKeysLimit: 100, + depthOfLimit: 1 }, logger: window.console, })], @@ -45,12 +46,12 @@ rrweb.record({ ``` All recordLog options are described below: -| key | default | description | -| ---------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| level | ['log','warn','error',...] | Default value contains names of all console functions. You can override it by setting console levels you need. | -| lengthThreshold | 1000 | Maximum number of records of console output. | -| stringifyOptions | { stringLengthLimit: undefined, numOfKeysLimit: 50 } | If console output includes js objects, we need to stringify them. `stringLengthLimit` limits the string length of single value. `numOfKeysLimit` limits the number of keys in an object. If an object contains more keys than this limit, we would only save object's name. You can reduce the size of events by setting these options. | -| logger | window.console | the console object we would record.You can set a console object from another execution environment where you would like to record. | +| key | default | description | +| ---------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| level | ['log','warn','error',...] | Default value contains names of all console functions. You can override it by setting console levels you need. | +| lengthThreshold | 1000 | Maximum number of records of console output. | +| stringifyOptions | { stringLengthLimit: undefined, numOfKeysLimit: 50, depthOfLimit: 4 } | If console output includes js objects, we need to stringify them. `stringLengthLimit` limits the string length of single value. `numOfKeysLimit` limits the number of keys in an object. `depthOfLimit` limits the depth of object. If an object contains more keys than this limit, we would only save object's name. You can reduce the size of events by setting these options. | +| logger | window.console | the console object we would record.You can set a console object from another execution environment where you would like to record. | ## replay console diff --git a/docs/recipes/console.zh_CN.md b/docs/recipes/console.zh_CN.md index de0462174e..e5d895252e 100644 --- a/docs/recipes/console.zh_CN.md +++ b/docs/recipes/console.zh_CN.md @@ -37,6 +37,7 @@ rrweb.record({ stringifyOptions: { stringLengthLimit: 1000, numOfKeysLimit: 100, + depthOfLimit: 1 }, logger: window.console, })], @@ -44,12 +45,12 @@ rrweb.record({ ``` 如下是配置选项的详细说明: -| key | 默认值 | 功能 | -| ---------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| level | ['log','warn','error',...] | 默认值包含了 console 的全部函数,你也可以传入想要录制的 console 函数。 | -| lengthThreshold | 1000 | 录制 console 输出信息的最大条数。 | -| stringifyOptions | { stringLengthLimit: undefined, numOfKeysLimit: 50 } | 如果 console 输出包含了 js 对象,我们需要对其进行序列化,`stringLengthLimit` 限制了单个值能转化的最大字符串长度,`numOfKeysLimit` 限制了一个被序列化的 js 对象能够包含的最大数量 key,如果对象的 key 数量超过了这个限制,我们将只保留对象的名字。你能通过这些选项来减小生成的 events 的体积。 | -| logger | window.console | 要录制的 console 对象,你也可以传入一个想要录制的其他 js 执行环境的 console 对象。 | +| key | 默认值 | 功能 | +| ---------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| level | ['log','warn','error',...] | 默认值包含了 console 的全部函数,你也可以传入想要录制的 console 函数。 | +| lengthThreshold | 1000 | 录制 console 输出信息的最大条数。 | +| stringifyOptions | { stringLengthLimit: undefined, numOfKeysLimit: 50, depthOfLimit: 4 } | 如果 console 输出包含了 js 对象,我们需要对其进行序列化,`stringLengthLimit` 限制了单个值能转化的最大字符串长度,`numOfKeysLimit` 限制了一个被序列化的 js 对象能够包含的最大数量 key,`depthOfLimit` 限制了一个被序列化的 js 对象能够拥有的对象深度(层数),如果对象的 key 数量超过了这个限制,我们将只保留对象的名字。你能通过这些选项来减小生成的 events 的体积。 | +| logger | window.console | 要录制的 console 对象,你也可以传入一个想要录制的其他 js 执行环境的 console 对象。 | ## 播放 console 数据 diff --git a/packages/rrweb/src/plugins/console/record/index.ts b/packages/rrweb/src/plugins/console/record/index.ts index 250ffd2bbe..b7c83555e7 100644 --- a/packages/rrweb/src/plugins/console/record/index.ts +++ b/packages/rrweb/src/plugins/console/record/index.ts @@ -1,7 +1,7 @@ import { listenerHandler, RecordPlugin } from '../../../types'; -import { stringify } from './stringify'; -import { StackFrame, ErrorStackParser } from './error-stack-parser'; import { patch } from '../../../utils'; +import { ErrorStackParser, StackFrame } from './error-stack-parser'; +import { stringify } from './stringify'; export type StringifyOptions = { // limit of string length @@ -11,6 +11,11 @@ export type StringifyOptions = { * if an object contains more keys than this limit, we would call its toString function directly */ numOfKeysLimit: number; + /** + * limit number of depth in an object + * if an object is too deep, toString process may cause browser OOM + */ + depthOfLimit: number; }; type LogRecordOptions = { diff --git a/packages/rrweb/src/plugins/console/record/stringify.ts b/packages/rrweb/src/plugins/console/record/stringify.ts index e67c35a982..f7bfac3226 100644 --- a/packages/rrweb/src/plugins/console/record/stringify.ts +++ b/packages/rrweb/src/plugins/console/record/stringify.ts @@ -48,6 +48,31 @@ function pathToSelector(node: HTMLElement): string | '' { return path; } +/** + * judge is object + */ +function isObject(obj: any): boolean { + return Object.prototype.toString.call(obj) === '[object Object]'; +} + +/** + * judge the object's depth + */ +function isObjTooDeep(obj: any, limit: number): boolean { + if (limit === 0) { + return true; + } + + const keys = Object.keys(obj); + for (const key of keys) { + if (isObject(obj[key]) && isObjTooDeep(obj[key], limit - 1)) { + return true; + } + } + + return false; +} + /** * stringify any js object * @param obj the object to stringify @@ -58,6 +83,7 @@ export function stringify( ): string { const options: StringifyOptions = { numOfKeysLimit: 50, + depthOfLimit: 4, }; Object.assign(options, stringifyOptions); const stack: any[] = []; @@ -89,7 +115,7 @@ export function stringify( if (value === null || value === undefined) { return value; } - if (shouldToString(value)) { + if (shouldIgnore(value)) { return toString(value); } if (value instanceof Event) { @@ -115,18 +141,28 @@ export function stringify( }); /** - * whether we should call toString function of this object + * whether we should ignore obj's info and call toString() function instead */ - function shouldToString(_obj: object): boolean { - if ( - typeof _obj === 'object' && - Object.keys(_obj).length > options.numOfKeysLimit - ) { + function shouldIgnore(_obj: object): boolean { + // outof keys limit + if (isObject(_obj) && Object.keys(_obj).length > options.numOfKeysLimit) { return true; } + + // is function if (typeof _obj === 'function') { return true; } + + /** + * judge object's depth to avoid browser's OOM + * + * issues: https://github.com/rrweb-io/rrweb/issues/653 + */ + if (isObject(_obj) && isObjTooDeep(_obj, options.depthOfLimit)) { + return true; + } + return false; } diff --git a/packages/rrweb/typings/plugins/console/record/index.d.ts b/packages/rrweb/typings/plugins/console/record/index.d.ts index 9461dd1dfb..b13a522ab7 100644 --- a/packages/rrweb/typings/plugins/console/record/index.d.ts +++ b/packages/rrweb/typings/plugins/console/record/index.d.ts @@ -2,6 +2,7 @@ import { RecordPlugin } from '../../../types'; export declare type StringifyOptions = { stringLengthLimit?: number; numOfKeysLimit: number; + depthOfLimit: number; }; declare type LogRecordOptions = { level?: LogLevel[] | undefined; @@ -38,4 +39,5 @@ export declare type Logger = { }; export declare const PLUGIN_NAME = "rrweb/console@1"; export declare const getRecordConsolePlugin: (options?: LogRecordOptions) => RecordPlugin; -export {}; +export { }; +