UNPKG

hc-web-log-mon

Version:

基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段

276 lines (252 loc) 8.63 kB
import { isValidKey, getLocationHref, getTimestamp } from '../utils' import { getElByAttr, isSimpleEl, getNodeXPath } from '../utils/element' import { sendData } from './sendData' import { eventBus } from './eventBus' import { EVENTTYPES, SEDNEVENTTYPES } from '../common' import { options } from './options' class RequestTemplateClick { eventId = '' // 事件ID eventType = '' // 事件类型 title = '' // 事件名 triggerPageUrl = '' // 当前页面URL x = -1 // 被点击元素与屏幕左边距离 y = -1 // 被点击元素与屏幕上边距离 params = {} // 事件参数 elementPath = '' // 被点击元素的层级 triggerTime = -1 // 事件发生时间 constructor(config = {}) { Object.keys(config).forEach(key => { if (isValidKey(key, config)) { this[key] = config[key] || null } }) } } /** * 监听 - 点击事件 */ function clickCollection() { eventBus.addEvent({ type: EVENTTYPES.CLICK, callback: (e: MouseEvent) => { const _config = new RequestTemplateClick({ eventType: SEDNEVENTTYPES.CLICK }) // 获取被点击的元素到最外层元素组成的数组 const path: HTMLElement[] = e.composedPath() ? (e.composedPath() as HTMLElement[]) : e.target ? getNodePath(e.target as HTMLElement) : [] // 检查被点击的元素以及其父级元素是否有这些属性(从内到外) let target = path.find( el => el.hasAttribute && (el.hasAttribute('data-warden-container') || el.hasAttribute('data-warden-event-id') || el.hasAttribute('data-warden-title')) ) if (!target) { // 第一个元素作为target if (path.length) { target = path[0] } else { return } } const { scrollTop, scrollLeft } = document.documentElement // html距离上和左侧的距离(一般都是0) const { top, left } = (e.target as HTMLElement).getBoundingClientRect() // 元素距离html的距离 _config.x = left + scrollLeft _config.y = top + scrollTop _config.triggerTime = getTimestamp() // 点击时间 _config.triggerPageUrl = getLocationHref() // 当前页面的url _config.title = extractTitleByTarget(target) // 获取title属性 _config.eventId = extractDataByPath(path) // 提取数据事件ID _config.params = extractParamsByPath(path) // 提取数据参数 _config.elementPath = getNodeXPath(target).slice(-128) // 长度限制128字符 sendData.emit(_config) } }) } /** * 获取目标元素到最外层元素组成的数组 */ function getNodePath( node: HTMLElement, options = { includeSelf: true, order: 'asc' } ) { if (!node) return [] const { includeSelf, order } = options let parent = includeSelf ? node : node.parentElement let result: HTMLElement[] = [] while (parent) { result = order === 'asc' ? result.concat(parent) : [parent].concat(result) parent = parent.parentElement } return result } /** * 获取title属性(data-warden-title 或者 title) */ function extractTitleByTarget(target: any) { const selfTitle = getNodeTitle(target) if (selfTitle) return selfTitle // 获取当前元素的文本 if ( Object.prototype.hasOwnProperty.call(target, 'innerText') && target.innerText ) { return target.innerText } // 获取当前元素子节点的文本 if (target.children.length) { let innerText = '' for (const item of target.children) { if ( item.innerText && (Object.prototype.hasOwnProperty.call(item, 'innerText') || item.childNodes.length === 1) ) { innerText += item.innerText } } if (innerText) { return innerText } } // 向上找其父节点 let container = target.parentElement while (container && container !== document.body) { if (container.hasAttribute('data-warden-container')) break container = container.parentElement } const superTitle = getNodeTitle(container) if (superTitle) return superTitle // 自身以及父级都没有拿到 title 值的情况下 const { tagName } = target return !target.hasChildNodes() || tagName.toLowerCase() === 'svg' ? handleLeafNode(target) : handleNoLeafNode(target) } /** * 获取元素的 data-warden-title 属性或者 title属性 */ function getNodeTitle(node: HTMLElement | null) { if (node) { return node.hasAttribute('data-warden-title') ? node.getAttribute('data-warden-title') : node.title } return '' } /** * 获取 title - 叶子元素的情况下,取其特殊值 * 叶子元素(也就是不包含其他HTML元素,也不能有文本内容) */ function handleLeafNode(target: any) { const { tagName, textContent } = target if (tagName === 'IMG') return target.getAttribute('alt') || null if (tagName === 'svg') { const a = [...Array(target.children)].find(item => item.tagName === 'use') if (a) return a.getAttribute('xlink:href') || null } return textContent } /** * 获取 title - 非叶子元素的情况 */ function handleNoLeafNode(target: Element) { const { tagName, textContent } = target if (tagName === 'A') { const res = isSimpleEl([...Array.from(target.children)]) return res ? textContent : target.getAttribute('href') || null } if (tagName === 'BUTTON') { const name = target.getAttribute('name') const res = isSimpleEl([...Array.from(target.children)]) return name || res ? textContent : target.getAttribute('href') || null } const { length } = [...Array.from(target.children)].filter(() => target.hasChildNodes() ) return length > 0 ? null : textContent } /** * 提取数据事件ID */ function extractDataByPath(list: HTMLElement[] = []) { // data-warden-event-id const hasIdEl = getElByAttr(list, 'data-warden-event-id') if (hasIdEl) return hasIdEl.getAttribute('data-warden-event-id')! // title const hasTitleEl = getElByAttr(list, 'title') if (hasTitleEl) return hasTitleEl.getAttribute('title')! // container const container = getElByAttr(list, 'data-warden-container') if (container) { if (container.getAttribute('data-warden-event-id')) { return container.getAttribute('data-warden-event-id')! } if (container.getAttribute('title')) { return container.getAttribute('title')! } const id2 = container.getAttribute('data-warden-container')! if (typeof id2 === 'string' && id2) return id2 } // 都没有则以 tagname 去当做ID return list[0].tagName.toLowerCase() } /** * 提取数据参数 * 如果本身节点没有埋点属性的话会用父级埋点属性 */ function extractParamsByPath(list: HTMLElement[] = []) { const regex = /^data-warden-/ let target let targetIndex = -1 // 遍历从子节点到body下最大的节点,遍历他们的属性,直到某个节点的属性能通过校验的节点 for (let index = 0; index < list.length; index++) { const el = list[index] const attributes = (el && el.attributes && Array.from(el.attributes)) || [] target = attributes.find(item => item.nodeName.match(regex) ? item.nodeName.match(regex) : item.nodeName.indexOf('data-warden-container') !== -1 ) if (target) { targetIndex = index break } } if (targetIndex < 0) return {} const container = list[targetIndex] const attrList = Array.from(container.attributes) || [] const params: Record<string, string | null> = {} const defaultKey = ['container', 'title', 'event-id'] attrList.forEach(item => { if (item.nodeName.indexOf('data-warden') < 0) return // 过滤非标准命名 如 data-v-fbcf7454 const key = item.nodeName.replace(regex, '') if (defaultKey.includes(key)) return // 过滤sdk自定义属性 params[key] = item.nodeValue }) return params } function initEvent() { options.value.event.core && clickCollection() } /** * 主动触发事件上报 * @param options 自定义配置信息 */ function handleSendEvent(options = {}, flush = false) { sendData.emit( { ...options, eventType: SEDNEVENTTYPES.CUSTOM, triggerTime: getTimestamp(), triggerPageUrl: getLocationHref() }, flush ) } export { initEvent, handleSendEvent }