monitor-usage
Version:
a code monitoring tool based on Javascript, used to monitor and track target code usage in real time, collect code running log reports, etc.
360 lines (359 loc) • 15.5 kB
JavaScript
import showErrorView from './error-view';
import { JSONStringify, getDateString, isInNodeEnv, isPrimitiveType, typeOf, } from './utils';
const specialTypes = ['Map', 'Set', 'WeakMap'];
const defaultMonitorUsageOptions = {
key: undefined,
isModifyValue: true,
showErrorView: false,
allowAdditionalProps: true,
};
export default class MonitorUsageClass {
static proxyMemo = new WeakMap();
/**
* 存储需要导出的报告信息
*/
static recordsToExport = {};
/**
* 存储监控对象的key调用次数
*/
static keyUsesMap = {};
/**
* 临时存储报告记录
*/
static tempRecords = new WeakMap();
/**
* 当外部调用create方法时,创建的监控对象会和它的报告回调,弱存储在一起
*/
static saveTraps = new WeakMap();
/**
* 监控对象为匿名函数时,因为没有函数名所有用id表示
*/
static anonymousFid = 0;
getReportObj(source) {
return {
status: 'OK',
source,
useageName: '',
usageCount: 0,
usageType: 'Unkown',
usageTime: getDateString(),
dailyUsageRate: '0%',
info: 'no problem',
oldValue: undefined,
newValue: undefined,
isNewlyAdded: false,
whenToAdd: undefined,
lastModify: undefined,
position: '',
filepath: '',
};
}
/**
* 获取被监控代码所在的行和列数
* @returns {GclResult}
*/
static gcl(err) {
// 利用Error方法记录的执行堆栈信息,获取代码位置
const stackLines = err.stack.split('\n')[2].trim();
const lines = stackLines.split(':');
/**
* lines example result:
* [
* 'at Object.<anonymous> (d',
* '\\project\\ideas\\monitor-usage\\src\\core.ts',
* '300', --> line
* '21)' --> row
* ]
*/
const row = lines.pop().replace(')', '');
const line = lines.pop();
return {
filepath: stackLines.match(/\((.*?)\)/)[1],
position: `trace the code to line ${line}, row ${row}`,
};
}
/**
* 触发监视回调执行以及存储报告信息
*/
static trap(data) {
const { target, repo, err, recordKey, callback } = data;
if (!this.saveTraps.has(target)) {
this.saveTraps.set(target, callback);
}
if (!this.tempRecords.has(target)) {
this.tempRecords.set(target, []);
}
const fn = this.saveTraps.get(target);
const records = this.tempRecords.get(target).slice();
const newRepo = { ...repo, ...this.gcl(err) };
records.push(newRepo);
fn?.(records);
this.tempRecords.set(target, records);
recordKey && this.recordsToExport[recordKey]?.push(newRepo);
}
createStares(source, callback, options, prevLevelKey) {
if (isPrimitiveType(source)) {
throw TypeError('[MonitorUsage error]: you must pass in non-basic data types as monitoring source');
}
MonitorUsageClass.saveTraps.set(source, callback);
if (options.key && !(options.key in MonitorUsageClass.recordsToExport)) {
MonitorUsageClass.recordsToExport[options.key] = [];
}
const that = this;
// 通过工厂函数返回创建方法,每次拦截操作都创建新的对象,防止重复使用
const createRepoFactory = () => this.getReportObj(source != null && typeof source === 'object'
? JSONStringify(source)
: source.toString());
const changeUsageCountAndTime = (key, repo) => {
const newCount = (MonitorUsageClass.keyUsesMap[key] ?? 0) + 1;
repo.usageCount = newCount;
repo.usageTime = getDateString();
// 计算使用目标的当天平均使用率
const rate = newCount / 24;
repo.dailyUsageRate =
rate > 100
? '100%'
: rate > 0 && rate < 1
? '1%'
: rate.toPrecision(3) + '%';
MonitorUsageClass.keyUsesMap[key] = newCount;
};
const triggerTrap = (target, repo, err) => {
MonitorUsageClass.trap({
target,
repo,
err,
recordKey: options.key,
});
};
const accessedName = (key) => prevLevelKey ? `${String(prevLevelKey)}.${String(key)}` : key;
const proxyHandler = {
has(target, key) {
const repo = createRepoFactory();
const res = Reflect.has(target, key);
repo.useageName = accessedName(key);
repo.usageType = 'Has';
repo.info = 'this property exists in source';
if (!res) {
repo.status = 'Fail';
repo.info = 'this property does not exist in the target';
}
changeUsageCountAndTime(key, repo);
triggerTrap(target, repo, new Error());
return res;
},
get(target, key, receiver) {
const repo = createRepoFactory();
const value = Reflect.get(target, key, receiver);
// 解决嵌套对象代理不到的问题,递归
if (value != null && typeof value === 'object') {
return that.createStares(value, callback, options, key);
}
repo.usageType = 'Get';
repo.useageName = accessedName(key);
changeUsageCountAndTime(key, repo);
const targetType = typeOf(target);
// 对于特殊类型:Map、Set、WeakMap,调用其方法时,实际上相当于get操作,直接返回即可
if (specialTypes.includes(typeOf(target))) {
repo.usageType =
`${targetType} ${key.toString()}`;
triggerTrap(target, repo, new Error());
// 返回的目标属性方法需要使用 bind 重新指定 this 指向代理目标,否则会丢失 this
return value.bind(target);
}
// 调用对象的方法
if (typeOf(value) === 'Function') {
repo.usageType = 'Method Call';
try {
repo.info = 'object method is called';
value.apply(target);
}
catch (error) {
if (options.showErrorView)
showErrorView(error);
repo.status = 'Fail';
repo.info = 'an error occurred while calling the method';
console.error('[MonitorUsage error]: an error occurred while calling the method', '\n key: ', key, '\n target: ', target, '\n', error);
}
finally {
triggerTrap(target, repo, new Error());
}
}
else if (!Reflect.has(target, key)) {
if (!options.allowAdditionalProps) {
repo.status = 'Fail';
repo.info = 'this property does not exist in the source';
triggerTrap(target, repo, new Error());
console.warn("[MonitorUsage warn]: cannot add new properties because 'options.allowAdditionalProps' is set to 'false'");
}
else {
repo.isNewlyAdded = true;
repo.info = 'this is a newly added property';
Reflect.set(target, key, undefined);
triggerTrap(target, repo, new Error());
console.log('[MonitorUsage info]: added a new property', '\n key: ', key, '\n target: ', target, '\n');
}
}
else {
repo.info = 'property accessed';
triggerTrap(target, repo, new Error());
}
return value;
},
set(target, key, newValue, receiver) {
const repo = createRepoFactory();
const oldValue = Reflect.get(target, key, receiver);
repo.usageType = 'Modify';
repo.oldValue = oldValue;
repo.useageName = accessedName(key);
changeUsageCountAndTime(key, repo);
if (!options.isModifyValue) {
repo.status = 'Fail';
repo.info = 'this value is not allowed to be modified';
triggerTrap(target, repo, new Error());
console.warn("[MonitorUsage warn]: cannot modify value because 'options.isModifyValue' is set to 'false'");
return false;
}
if (!Reflect.has(target, key)) {
repo.isNewlyAdded = true;
repo.info = 'a newly set property';
repo.oldValue = newValue.toString();
triggerTrap(target, repo, new Error());
console.log('[MonitorUsage info]: added a new property', '\n key: ', key, '\n target: ', target, '\n');
return Reflect.set(target, key, newValue, receiver);
}
if (Object.is(oldValue, newValue)) {
repo.info = 'ignore updating old and new equal values';
triggerTrap(target, repo, new Error());
return false;
}
repo.info = 'update value';
repo.newValue = newValue.toString();
repo.lastModify = getDateString();
triggerTrap(target, repo, new Error());
return Reflect.set(target, key, newValue, receiver);
},
deleteProperty(target, key) {
const repo = createRepoFactory();
const res = Reflect.deleteProperty(target, key);
repo.useageName = accessedName(key);
repo.usageType = 'Delete';
repo.info = 'the property has been deleted';
if (!res) {
repo.status = 'Fail';
repo.info = 'the property cannot be deleted';
}
triggerTrap(target, repo, new Error());
return res;
},
apply(target, thisArg, argArray) {
const repo = createRepoFactory();
const funcName = target?.name ||
`anonymous_fid_${++MonitorUsageClass.anonymousFid}`;
repo.useageName = funcName;
repo.usageType = 'Function Call';
changeUsageCountAndTime(funcName, repo);
try {
const result = Reflect.apply(target, thisArg, argArray);
repo.info = `is called with arguments: ${JSONStringify(argArray)}, and the result of the call is ${JSONStringify(result)}`;
return result;
}
catch (error) {
if (options.showErrorView)
showErrorView(error);
repo.status = 'Fail';
repo.info = 'function call error';
console.error('[MonitorUsage error]: function call error', '\n useageName: ', funcName, '\n function: ', JSONStringify(target), '\n', error);
}
finally {
triggerTrap(target, repo, new Error());
}
},
construct(target, argArray, newTarget) {
const repo = createRepoFactory();
const constructorName = newTarget.name;
repo.usageType = 'Construct Function';
changeUsageCountAndTime(constructorName, repo);
try {
repo.info = `constructor called with arguments: ${JSONStringify(argArray)}`;
return Reflect.construct(target, argArray, newTarget);
}
catch (error) {
if (options.showErrorView)
showErrorView(error);
repo.status = 'Fail';
repo.info = 'an error occurred construct function';
console.error('[MonitorUsage error]: an error occurred construct function', '\n useageName: ', constructorName, '\n function: ', JSONStringify(newTarget), '\n', error);
}
finally {
triggerTrap(target, repo, new Error());
}
},
ownKeys(target) {
const repo = createRepoFactory();
repo.useageName = '@@the key of the entire source';
repo.info = 'target is traversed';
repo.usageTime = getDateString();
triggerTrap(target, repo, new Error());
return Reflect.ownKeys(target);
},
};
return new Proxy(source, proxyHandler);
}
/**
* 创建监控对象
* @param {object} source 目标对象,非基本数据类型
* @param {ReportingFunc} callback 监控的回调事件
* @param {MonitorUsageOptions} options 配置选项
* @returns
*/
stares(source, callback, options = defaultMonitorUsageOptions) {
if (MonitorUsageClass.proxyMemo.has(source)) {
return MonitorUsageClass.proxyMemo.get(source);
}
const proxy = this.createStares(source, callback, options);
MonitorUsageClass.proxyMemo.set(source, proxy);
return proxy;
}
/**
* 导出JSON格式的日志文件
* @param useageName 文件名
* @param _path 存放路径
*/
export(useageName = 'reports', _path) {
const reports = JSON.stringify(MonitorUsageClass.recordsToExport);
if (isInNodeEnv() && process.env.NODE_ENV !== 'production') {
const fs = require('fs');
const path = require('path');
const jsonString = JSON.stringify(reports);
const filePath = path.join(_path ?? __dirname, `${useageName}.json`);
return new Promise((res, rej) => {
fs.writeFile(filePath, jsonString, 'utf8', (err) => {
if (err) {
console.error(`[MonitorUsage error]: an error occurred while exporting the file:
${err}
`);
rej(err);
return;
}
res(filePath);
});
});
}
return new Promise((res, rej) => {
try {
const blod = new Blob([reports], { type: 'application/json' });
const url = URL.createObjectURL(blod);
const link = document.createElement('a');
link.href = url;
link.download = `${useageName}.json`;
link.click();
res(url);
URL.revokeObjectURL(url);
}
catch (error) {
rej(error);
}
});
}
}