Commit c2a63c00 authored by fanxuehui's avatar fanxuehui

Merge branch 'opt' into 'master'

Opt

See merge request !1
parents 65764207 feb99f13
...@@ -3205,7 +3205,8 @@ ...@@ -3205,7 +3205,8 @@
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.15",
"resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.15.tgz", "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.15.tgz",
"integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=" "integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=",
"dev": true
}, },
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
"author": "Dec-F", "author": "Dec-F",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"lodash": "^4.17.15",
"md5": "^2.2.1", "md5": "^2.2.1",
"rrweb": "^0.7.18", "rrweb": "^0.7.18",
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0"
......
import 'whatwg-fetch';
import utils from './utils';
const common = {
/**
* fetch请求封装
* @param {*} action 请求线上地址
* @param {*} params 请求入参
* @param {*} method 请求方式,默认get
* @param {*} options 其他参数
* options为对象格式,值:
* isLoading(是否激活请求加载动画)
* isJson(是否设置post请求头contentType为application/json)
* content 自定义请求参数
*/
fetch(action, params = {}, method = 'get', options = {}) {
const token = utils.getCookie('token');
const XCsrfToken = utils.getCookie('csrf_token');
let url = action;
let option = {
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'token': token,
'X-Csrf-Token': XCsrfToken
}
};
if (options && options.content) {
option = Object.assign({}, option, options.content);
}
if (method === 'post') {
if (options && options.isJson) {
option.body = JSON.stringify(params);
} else {
option = Object.assign({}, option, {
headers: {
Accept: 'application/json,text/plain,*/*',
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
'token': token,
'X-Csrf-Token': XCsrfToken
}
});
option.body = utils.serialize(params);
}
}
if (method === 'get') {
url = Object.keys(params).length ? url + '?' + utils.serialize(params) : url;
}
return fetch(url, option)
.then(response => {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
let error = new Error(response.statusText);
error.response = response;
throw error;
}
});
}
}
export default common;
\ No newline at end of file
// 分片计数器
class Counter {
count = 0;
next() {
return ++this.count;
}
reset() {
this.count = 0;
}
}
export default Counter;
import * as rrweb from "rrweb"; import * as rrweb from "rrweb";
import TracksWorker from "./tracks.worker"; import TracksWorker from "./tracks.worker";
import common from './comment'; import utils from './utils';
export default class Monitor { export default class Monitor {
rrwebHandler = null; rrwebHandler = null;
tracksWorker = new TracksWorker(); tracksWorker = new TracksWorker();
// 记录 constructor(props) {
constructor() { console.log('初始化');
this.tracksWorker.onmessage = ({ data }) => { this.tracksWorker.postMessage({
console.log('local', data); type: "init",
window.localStorage.setItem("rrevents", JSON.stringify(data)); payload: {
// common.fetch('https://manager.tuiatest.cn/homePage/data','get'); system: props.system,
}; config: props.config
} }
record() { });
}
resetRrwebHandler() {
this.rrwebHandler && this.rrwebHandler(); this.rrwebHandler && this.rrwebHandler();
console.log('开始录制') }
/**
* 录制
* @memberof Monitor
*/
record() {
this.resetRrwebHandler();
console.log("开始录制");
this.rrwebHandler = rrweb.record({ this.rrwebHandler = rrweb.record({
emit: event => { emit: event => {
this.tracksWorker.postMessage({ type: "record", payload: event }); this.tracksWorker.postMessage({ type: "record", payload: event });
}, },
}); });
} }
// 停止 /**
* 停止录制
* @memberof Monitor
*/
stop() { stop() {
if (!this.rrwebHandler) { if (!this.rrwebHandler) {
throw new Error("没有正在录制的实例"); throw new Error("没有正在录制的实例");
} }
console.log('停止录制') console.log("停止录制");
this.rrwebHandler(); this.rrwebHandler();
} }
// 重置 /**
* 重置
* @param {*} event
*/
reset(event) { reset(event) {
this.rrwebHandler && this.rrwebHandler(); this.resetRrwebHandler();
console.log('重置数据') console.log("重置数据");
this.tracksWorker.postMessage({ type: "reset", payload: event }); this.tracksWorker.postMessage({ type: "reset", payload: event });
} }
// 发送信息 /**
* 发送信息
* @param {*} action
* @memberof Monitor
*/
postMessage(action) { postMessage(action) {
this.tracksWorker.postMessage(action); this.tracksWorker.postMessage(action);
} }
......
// 数据维护,上报
import Counter from "./counter";
import { dataWrapper, log } from './utils';
class Reporter {
system = ''; // 系统名
config = {
env: 'prod',
log: false
}; // 系统配置
environmentUrl = ''; // 环境
userIdentifier = ''; //用户标示
path = ''; // 当前路径
isUploading = false; // 是否在上传cdn
bus = []; // 上传分片数据
counter = new Counter(); // 计数器和recordKey一起重置
cache = [] // cdn数据缓存
constructor(recordKey) {
this.recordKey = recordKey;
}
// 设置系统名
setSystem(system) {
this.system = system;
}
// 设置配置内容
setConfig(config) {
this.config = config;
this.setEnvironment(config.env);
}
// 设置系统环境变量
setEnvironment(env) {
if (env === 'dev') {
this.environmentUrl = 'http://hunter.duibadev.com.cn';
} else if (env === 'prod') {
this.environmentUrl = 'http://hunter.dui88.com.cn';
}
}
// 设置用户标示
setUserIdentifier(userIdentifier) {
this.userIdentifier = userIdentifier;
}
// 设置当前访问路径
setPath(url) {
this.path = url
}
// 上传CDN
toCDN(payload) {
this.isUploading = true;
const blob = new Blob([JSON.stringify(payload)], {type : 'application/json'});
const formData = new FormData();
const trackId = this.counter.count;
formData.append('file', blob, `${this.recordKey + trackId}.json`);
try {
fetch(`${this.environmentUrl}/upload`, {
// fetch(`http://172.16.47.148:3000/upload`, {
method: 'POST',
credentials: 'include',
body: formData
}).then(res => {
return res.json();
}).then(res => {
// 如果cache中有数据,说明是多次提交并且数据缓存在了cache中,那么我们全量快照可能在cache[0]
const snapArr = this.cache && this.cache.length > 0 ? this.cache : this.bus;
const snapIndex = snapArr.findIndex((item, index) => {
if (item.track.type === 2) {
return index;
}
});
const extra = {
system: this.system,
userIdentifier: this.userIdentifier,
path: this.path,
recordKey: this.recordKey,
trackId: trackId,
isCDN: true
}
log(this.config.log, 'type=2定位', snapIndex);
log(this.config.log, 'cnd Url', res.data.url);
// 向cache或者bus中注入
if(this.cache && this.cache.length > 0) {
this.cache[0].splice(snapIndex, 1, dataWrapper(extra, res.data.url));
} else {
this.bus.splice(snapIndex, 1, dataWrapper(extra, res.data.url));
}
this.isUploading = false;
// 如果cache里面有数据需要上传的,那么先上传
if(this.cache && this.cache.length > 0) {
this.cache.map(item => {
this.report(item);
});
this.cache = [];
}
return res;
});
} catch (e) {
log(this.config.log, '上传失败,原因:', e.message);
}
}
toBus(data) {
const extra = {
system: this.system,
userIdentifier: this.userIdentifier,
path: this.path,
recordKey: this.recordKey,
trackId: this.counter.next(),
isCDN: false
}
this.bus.push(dataWrapper(extra, data));
}
// 上传
report(data) {
log(this.config.log, '上传数据', data);
const reportData = data;
this.bus = [];
if (this.isUploading) {
log(this.config.log, 'cdn数据正在上传,先将内容存到cache', this.cache);
this.cache.push(reportData);
return;
}
try {
fetch(`${this.environmentUrl}/behavior/record`, {
// fetch(`http://172.16.47.148:3000/behavior/record`, {
method: 'POST',
credentials: 'include',
'Content-Type': 'application/json;charset=utf-8',
body: JSON.stringify({tracks: data})
}).then(res => {
log(this.config.log, '上传返回的内容', res);
});
} catch (e) {
log(this.config.log, '上传失败,原因:', e.message);
}
}
// 重置数据(分条使用)
reset(recordKey) {
this.cache = [];
this.bus = [];
this.counter.reset();
this.recordKey = recordKey;
}
}
export default Reporter;
import md5 from 'md5'; // 入口,格式化数据,模块管理
import utils from './utils'; import md5 from "md5";
import _ from 'lodash'; import Reporter from "./reporter";
// 原始数据数组 import { log } from './utils';
let events = []; const reporter = new Reporter("recordKey");
// 包装后的数据对象
let wrapData = [];
// 分条视频id
let recordKey = '';
// 全量快照是否已经返回
let isCdnReturn = false;
onmessage = ({ data: { type, payload } }) => { onmessage = ({ data: { type, payload } }) => {
switch (type) { switch (type) {
case "init":
reporter.setSystem(payload.system);
reporter.setConfig(payload.config);
log(reporter.config.log, 'init参数', payload);
break;
case "record": case "record":
// todo : 数据本地存储 // todo : 数据本地存储
console.log(wrapData); reporter.toBus(payload);
wrapData.push(utils.dataWrapper({ log(reporter.config.log, 'bus', reporter.bus);
recordKey,
isCdn: false
}, payload));
events.push(payload);
// todo : 全量快照上传cdn // todo : 全量快照上传cdn
if(payload.type === 2) { log(reporter.config.log, 'track type', payload.type);
const cdnIndex = _.findIndex(events, { 'type': 2 }); if (payload.type === 2) {
setTimeout(() => { reporter.toCDN(payload);
wrapData.splice(cdnIndex, 1, utils.dataWrapper({
recordKey,
isCdn: true
}, payload));
isCdnReturn = true;
}, 1000);
} }
// todo : 数据压缩 // todo : 数据压缩
// todo : 根据事件类型优先级触发上传策略(click) // todo : 根据事件类型优先级触发上传策略(click)
if(payload.data.source === 2 && isCdnReturn) { if (payload.data.source === 2 && !reporter.isUploading) {
console.log('点击上上传events', wrapData); postMessage({ type: "localData", payload: reporter.bus })
postMessage(wrapData); reporter.report(reporter.bus);
events = [];
wrapData = [];
} }
// todo : 数据超出100条上线,自动上传 // todo : 数据超出100条上线,自动上传
if (wrapData.length > 100 && isCdnReturn) { if (reporter.bus.length > 100 && !reporter.isUploading) {
console.log('超出线上100条上传events', wrapData) postMessage({ type: "localData", payload: reporter.bus })
postMessage(wrapData); reporter.report(reporter.bus);
events = [];
wrapData = [];
} }
break; break;
case "reset": case "reset":
// todo : 重置参数,重新生成recordKey // todo : 重置参数,重新生成recordKey
events = []; const { url, email, userInfo } = payload;
wrapData = []; reporter.setUserIdentifier(userInfo);
recordKey = ''; reporter.setPath(url);
isCdnReturn = false; let recordKey = md5(url + email + Date.parse(new Date()));
const { url, email } = payload; reporter.reset(recordKey);
recordKey = md5(url + email + Date.parse(new Date())); log(reporter.config.log, '分片id', reporter.recordKey);
console.log(url, '分片id', recordKey);
break; break;
default: default:
console.log("unknow action"); log(reporter.config.log, 'unknow action', type);
} }
}; };
const utils = { /**
// 包装event * 包装event
dataWrapper(extraData, event) { * @param {*} extraData
* @param {*} event
*/
export const dataWrapper = (extraData, event) => {
return { return {
...extraData, ...extraData,
event track: event
} }
}, };
// 判断是否有值
isNothing(value) {
return value === '' || value === undefined || value === null || (typeof value === 'number' && (isNaN(value) || !isFinite(value)));
},
// 获取token
getCookie(name) {
const regexp = new RegExp('(^| )' + name + '=([^;]*)(;|$)');
const matches = regexp.exec(document.cookie);
return matches ? matches[2] : null;
},
// 拼接URL请求参数
serialize(obj) {
const str = [];
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(this.isNothing(obj[p]) ? '' : obj[p]));
}
}
return str.join('&');
},
}
export default utils; /**
\ No newline at end of file * 是否打印日志
* @param {*} logMes
*/
export const log = (log, logMes, logContent) => {
if( log) {
return console.log(logMes, logContent);
} else {
return;
}
};
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment