hc-web-log-mon
Version:
基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
162 lines (148 loc) • 5.35 kB
text/typescript
import { record } from 'rrweb'
import pako from 'pako'
import { Base64 } from 'js-base64'
import { RecordEventScope } from '../types'
import { getTimestamp } from '../utils'
import { options } from './options'
import { watch } from '../observer'
/**
* 只存储最近30s的所有录屏 (分为3段)
* 第一段:0-10
* 第二段:10-20
* 第三段:20-30
*
* 当结束 30-40 的录屏时会把 0-10 录屏推出,并将此次录屏放入
* 数据结构: [
* {scope: '1684826667798-1684826669998', eventList: [...]},
* {scope: '1684826679998-1684826689998', eventList: [...]},
* {scope: '1684826699998-1684826799998', eventList: [...]},
* ]
*
* 举例:当第34秒发生错误时,此时的录屏数组是这样 [
* {scope: '0-10', eventList: [...]},
* {scope: '10-20', eventList: [...]},
* {scope: '20-30', eventList: [...]},
* {scope: '30-40', eventList: [...]},
* ]
*
* 此时30-40的 eventList 还在不断push中,但在错误发生时,我们可以把存住此时的录屏数组
* 然后直接去数组的最后一位的 eventList + 最后第二位的 eventList 拼接(会导致视频时长不固定,但会在10-20,如果想缩小范围,可更改 MAXSCOPETIME)
* 注意:如果拼接发现 eventList 长度为0或者很少,很大可能是用户没有手动操作且系统自动报错
*
* 而真正的录屏数组在填满 30-40 的 eventList 时则删除数组第一位数据
*
* 真实效果:
* MAXSCOPETIME 我这边设置为 5s,所以最终录制的时长为 5s-10s
* 但是会有几个特殊场景导致录屏时间过短:
* 1. 因为录制的插件只会在用户操作网页时运作,当用户停止操作页面或者页面处于休眠状态则会停止记录
* 直到用户重新操作页面,所以会出现用户停止操作1分钟后2s后触发了一个错误,此时sdk只会记录这个2s的操作
* 2. 另外就是程序刚进来的报错也会导致录屏时间小于5s
*/
const MAXSCOPETIME = 5000 // 每5s记录一个区间
const MAXSCOPELENGTH = 3 // 录屏数组最长长度 - 不要小于3
let recordScreen: RecordScreen | undefined
export class RecordScreen {
public eventList: RecordEventScope[] = [
{ scope: `${getTimestamp()}-`, eventList: [] }
]
private closeCallback: ReturnType<typeof record>
constructor() {
this.init()
}
private init() {
this.closeCallback = record({
emit: (event, isCheckout) => {
const lastEvents = this.eventList[this.eventList.length - 1]
lastEvents.eventList.push(event)
if (isCheckout) {
if (this.eventList.length > 0) {
this.eventList[this.eventList.length - 1].scope =
lastEvents.scope + getTimestamp()
}
if (this.eventList.length > MAXSCOPELENGTH) {
this.eventList.shift()
}
this.eventList.push({ scope: `${getTimestamp()}-`, eventList: [] })
}
},
recordCanvas: true,
checkoutEveryNms: MAXSCOPETIME // 每5s重新制作快照
})
}
public close() {
this.closeCallback?.()
this.closeCallback = undefined
}
}
export function initRecordScreen() {
watch(options, (newValue, oldValue) => {
if (newValue.recordScreen === oldValue.recordScreen) return
if (newValue.recordScreen) recordScreen = new RecordScreen()
else {
recordScreen!.close()
recordScreen = undefined
}
})
recordScreen = options.value.recordScreen ? new RecordScreen() : undefined
}
// 获取录屏数据
export function getEventList() {
return recordScreen?.eventList ?? []
}
/**
* 压缩
* @param data 压缩源
*/
export function zip(data: any): string {
if (!data) return data
// 判断数据是否需要转为JSON
const dataJson =
typeof data !== 'string' && typeof data !== 'number'
? JSON.stringify(data)
: data
// 使用Base64.encode处理字符编码,兼容中文
const str = Base64.encode(dataJson as string)
const binaryString = pako.gzip(str)
const arr = Array.from(binaryString)
let s = ''
arr.forEach((item: number) => {
s += String.fromCharCode(item)
})
return Base64.btoa(s)
}
/**
* 解压
* @param b64Data 解压源
*/
export function unzip(b64Data: string) {
const strData = Base64.atob(b64Data)
const charData = strData.split('').map(function (x) {
return x.charCodeAt(0)
})
const binData = new Uint8Array(charData)
const data: any = pako.ungzip(binData)
// ↓切片处理数据,防止内存溢出报错↓
let str = ''
const chunk = 8 * 1024
let i
for (i = 0; i < data.length / chunk; i++) {
str += String.fromCharCode.apply(
null,
data.slice(i * chunk, (i + 1) * chunk)
)
}
str += String.fromCharCode.apply(null, data.slice(i * chunk))
// ↑切片处理数据,防止内存溢出报错↑
const unzipStr = Base64.decode(str)
let result = ''
// 对象或数组进行JSON转换
try {
result = JSON.parse(unzipStr)
} catch (error: any) {
if (/Unexpected token o in JSON at position 0/.test(error)) {
// 如果没有转换成功,代表值为基本数据,直接赋值
result = unzipStr
}
}
return result
}