diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..776484b90c8e74d31e2006bb47b54c34f1ec70ef --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,73 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, + extends: [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript' + ], + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'warn', + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', + quotes: ['error', 'single', { avoidEscape: true }], + 'quote-props': ['warn', 'as-needed'], + 'comma-dangle': ['error', 'only-multiline'], + camelcase: ['error', { properties: 'never' }], + 'array-bracket-spacing': 'warn', + 'arrow-spacing': 'warn', + 'block-spacing': 'warn', + 'comma-spacing': 'warn', + 'computed-property-spacing': 'warn', + 'generator-star-spacing': 'warn', + 'key-spacing': 'warn', + 'keyword-spacing': 'warn', + 'object-curly-spacing': ['warn', 'always'], + 'rest-spread-spacing': 'warn', + 'switch-colon-spacing': 'error', + 'semi-spacing': 'warn', + 'template-curly-spacing': 'warn', + 'template-tag-spacing': 'warn', + 'yield-star-spacing': 'warn', + semi: ['warn', 'always'], + 'no-trailing-spaces': 'warn', + 'prefer-template': 'error', + 'prefer-spread': 'error', + 'no-var': 'error', + 'max-lines-per-function': ['error', { + max: 100, + skipComments: true, + skipBlankLines: true + }], + complexity: ['warn', 20], + 'max-depth': ['warn', 4], + 'max-len': ['warn', { + code: 160, + ignoreTemplateLiterals: true, + ignoreStrings: true, + ignorePattern: 'd="([\\s\\S]*?)"', + }], + 'default-param-last': 'off', + 'no-param-reassign': ['error', { props: false }], + + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': ['warn', { + html: { + void: 'always', + normal: 'never', + }, + }], + + 'vue/singleline-html-element-content-newline': 'off', + 'vue/html-closing-bracket-newline': 'off', + 'vue/multiline-html-element-content-newline': 'warn', + + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + }, +}; \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000000000000000000000000000000000..9afbe5e1b84a792b4896f418f13aee76ef1c4d51 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,4 @@ +module.exports = { + printWidth: 160, + singleQuote: true, +}; diff --git a/packages/analytics/index.html b/packages/analytics/index.html index dd76078936216124c3ae1d6fb5d1cc8bfd5f682f..2ee13d0958d28e9520a551366c3df8c55ead754f 100644 --- a/packages/analytics/index.html +++ b/packages/analytics/index.html @@ -13,15 +13,24 @@
- - open link -
-
123
- -
+
+ + open link +
+
123
+ +
- - + + +
+
+
+
+

内部

+
+
+
diff --git a/packages/analytics/src/events/keys.ts b/packages/analytics/src/events/_keys.ts similarity index 82% rename from packages/analytics/src/events/keys.ts rename to packages/analytics/src/events/_keys.ts index 3c02cad703c5b26118d123f7997a6d0f9d9d40b2..5ca686b6be7d8d85135e93915be25c8c2cc03440 100644 --- a/packages/analytics/src/events/keys.ts +++ b/packages/analytics/src/events/_keys.ts @@ -3,4 +3,5 @@ export const OpenEventKeys = { PageBasePerformance: '$PageBasePerformance', LCP: '$LCP', INP: '$INP', + PageClick: '$PageClick', }; diff --git a/packages/analytics/src/events/click.ts b/packages/analytics/src/events/click.ts new file mode 100644 index 0000000000000000000000000000000000000000..2761d54a22b6cf8cfe06dddb057c209af060518b --- /dev/null +++ b/packages/analytics/src/events/click.ts @@ -0,0 +1,44 @@ +import { EventContent } from '../types'; +import { isFunction } from '../utils'; +import { OpenEventKeys } from './_keys'; + +async function handleClick(e: MouseEvent, customData?: (event: MouseEvent) => Promise | EventContent) { + const { pageX, pageY } = e; + const { scrollLeft, scrollTop } = document.scrollingElement || document.documentElement; + + const cData = isFunction(customData) ? await customData(e) : {}; + + return { + url: window.location.href, + pageX, + pageY, + documentScrollLeft: scrollLeft, + documentScrollTop: scrollTop, + ...cData, + }; +} + +export default { + event: OpenEventKeys.PageClick, + collector: (options?: { customData: (event: MouseEvent) => Promise; delay: Millisecond }) => { + const { customData, delay = 800 } = options || {}; + return new Promise((resolve) => { + let debounceId = 0; + window.addEventListener( + 'click', + (e) => { + clearTimeout(debounceId); + debounceId = globalThis.setTimeout(async () => { + const data = await handleClick(e, customData); + console.log(data); + + resolve(data); + }, delay); + }, + { + capture: true, + } + ); + }); + }, +}; diff --git a/packages/analytics/src/events/index.ts b/packages/analytics/src/events/index.ts index e18cc8f6ed29ce49f8051e4772624d05c4b96837..60fe28996c8b82b47cc077635107972609d82b95 100644 --- a/packages/analytics/src/events/index.ts +++ b/packages/analytics/src/events/index.ts @@ -1,12 +1,19 @@ -import { OpenEventKeys } from './keys'; +import { OpenEventKeys } from './_keys'; +import { EventContent } from '../types'; import { isFunction } from '../utils'; -type EventCollector = (...params: any[]) => Promise> | Record; +export interface CollectorOptions { + customData?: () => Promise | EventContent; +} + +type EventCollector = (options?: CollectorOptions) => Promise | EventContent; + interface Event { event: string; collector: EventCollector; } + const modules: Record> = import.meta.glob(['./*.ts', '!./keys.ts'], { eager: true, }); @@ -26,9 +33,9 @@ export function isInnerEvent(event: string) { return Events.has(event); } -export function getInnerEventData(event: string, params: any[] = []) { +export function getInnerEventData(event: string, collectorOption?: CollectorOptions) { const colloctor = Events.get(event); if (isFunction(colloctor)) { - return colloctor(...params); + return colloctor(collectorOption); } } diff --git a/packages/analytics/src/events/inp.ts b/packages/analytics/src/events/inp.ts index ed7fd19d33855363e2158391622a0d519a0cc471..a3641118e5b69241eacbb1f59d9e402884d1fba2 100644 --- a/packages/analytics/src/events/inp.ts +++ b/packages/analytics/src/events/inp.ts @@ -1,4 +1,4 @@ -import { OpenEventKeys } from './keys'; +import { OpenEventKeys } from './_keys'; import { onINP } from 'web-vitals'; export default { @@ -7,6 +7,7 @@ export default { return new Promise((resolve) => { onINP((m) => { resolve({ + url: window.location.href, inp: m.value, }); }); diff --git a/packages/analytics/src/events/lcp.ts b/packages/analytics/src/events/lcp.ts index 32b7a835a979542ddccdcc65ec508610979a10ae..397ba62e0d1014bd7f4fee7b22afc0883558e207 100644 --- a/packages/analytics/src/events/lcp.ts +++ b/packages/analytics/src/events/lcp.ts @@ -1,4 +1,4 @@ -import { OpenEventKeys } from './keys'; +import { OpenEventKeys } from './_keys'; import { onLCP } from 'web-vitals'; export default { @@ -7,6 +7,7 @@ export default { return new Promise((resolve) => { onLCP((m) => { resolve({ + url: window.location.href, lcp: m.value, }); }); diff --git a/packages/analytics/src/events/performance.ts b/packages/analytics/src/events/performance.ts index 96e0e58c5c11d2995a091e4e81183db115eabcdc..63630949f2bfa201730c4ffcd2c5ee9929b99434 100644 --- a/packages/analytics/src/events/performance.ts +++ b/packages/analytics/src/events/performance.ts @@ -1,7 +1,8 @@ -import { OpenEventKeys } from './keys'; +import { OpenEventKeys } from './_keys'; import { onFCP, onTTFB } from 'web-vitals'; interface PerformanceData { + url: string; fcp: number; ttfb: number; load: number; @@ -28,6 +29,7 @@ export default { collector: () => { return new Promise((resolve) => { const data: PerformanceData = { + url: window.location.href, fcp: -1, ttfb: -1, load: -1, diff --git a/packages/analytics/src/events/pv.ts b/packages/analytics/src/events/pv.ts index ab0e451750e11ccd5f0c5783d6436999bc898379..4d9e1b6b826fad046656c3301c441cdff28e358a 100644 --- a/packages/analytics/src/events/pv.ts +++ b/packages/analytics/src/events/pv.ts @@ -1,4 +1,4 @@ -import { OpenEventKeys } from './keys'; +import { OpenEventKeys } from './_keys'; export default { event: OpenEventKeys.PV, diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index 6cc8e36d7f62c74aefb3d78878b37cd98473528d..f027aa8cf56cfda8006aea94b9b9994eddf7bf1f 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -1,12 +1,13 @@ import { Storage } from './storage'; import { whenDocumentReady, getClientByUA, isFunction, isPromise, uniqueId } from './utils'; import { Constant } from './constant'; -import { getInnerEventData } from './events'; +import { getInnerEventData, isInnerEvent, CollectorOptions } from './events'; import packageJson from '../package.json'; +import { EventContent, EventData, EventHeader, OpenAnalyticsParams, ReportRequest } from './types'; -export { OpenEventKeys } from './events/keys'; +export { OpenEventKeys } from './events/_keys'; -class StoreKey { +class AnalyticsStoreKey { appPrefix: string; client: string; session: string; @@ -24,48 +25,7 @@ class StoreKey { return `${Constant.OA_PREFIX}-${this.appPrefix}${key}`; } } -type StoreKeyIns = InstanceType; - -export interface EventData { - event: string; // 事件名 - time: number; // 事件采集时间 - data: Record; // 上报的事件数据 - sId: string; // 会话id -} - -interface ReportOptions { - immediate: boolean; // 是否立即上报 -} -interface EventHeader { - cId?: string; // 客户端匿名标识,清除浏览器缓存销毁 - aId?: string; // 应用id - oa_version?: string; // OA版本 - screen_width?: number; // 屏幕宽度 - screen_height?: number; // 屏幕高度 - view_width?: number; // 视口宽度 - view_height?: number; // 视口高度 - os?: string; // 客户端操作系统 - os_version?: string; // 客户端操作系统版本 - browser?: string; // 客户端浏览器 - browser_version?: string; // 客户端浏览器版本 - device?: string; // 设备信息 - device_type?: string; // 设备类型 - device_vendor?: string; // 设备品牌 -} -interface ReportData { - header: EventHeader; - body: EventData[]; -} -type RequestFn = (data: ReportData) => Promise | void; - -export interface OpenAnalyticsParams { - request: (data: ReportData) => Promise | void; // 上报数据的接口 - appKey?: string; // 采集app的key,用于区分多app上报 - immediate?: boolean; // 全局设置是否立即上报 - requestInterval?: number; //上报间隔 - maxEvents?: number; - requestPlan?: (requestFn: () => void) => void; -} +type StoreKeyIns = InstanceType; const store = new Storage(localStorage); @@ -93,8 +53,8 @@ function initHeader(keys: StoreKeyIns, appId: string): EventHeader { cId: client.id, aId: appId, oa_version: packageJson.version, - view_width: window.innerWidth, - view_height: window.innerHeight, + viewport_width: window.innerWidth, + viewport_height: window.innerHeight, screen_width: window.screen.width || window.innerWidth, screen_height: window.screen.height || window.innerHeight, os: os.name ?? '', @@ -127,7 +87,7 @@ function getSessionId(sKey: string) { } export class OpenAnalytics { - request: RequestFn; + request: ReportRequest; eventData: EventData[]; immediate: boolean; sessionId: string = ''; @@ -151,7 +111,7 @@ export class OpenAnalytics { this.request = params.request; this.immediate = params.immediate ?? false; this.appKey = params.appKey ?? ''; - this.StoreKey = new StoreKey(params.appKey); + this.StoreKey = new AnalyticsStoreKey(params.appKey); this.requestInterval = params.requestInterval ?? Constant.DEFAULT_REQUEST_INTERVAL; this.#timer = null; this.maxEvents = params.maxEvents ?? Constant.MAX_EVENTS; @@ -175,7 +135,7 @@ export class OpenAnalytics { /** * 设置header */ - setHeader(header: Record) { + setHeader(header: Record) { Object.assign(this.header, header); } /** @@ -281,21 +241,25 @@ export class OpenAnalytics { /** * 采集数据 * @param event 事件名 - * @param data 事件数据 + * @param data 事件数据,如果是内部事件,则会在内部事件触发时执行 * @param options 配置 */ - async report(event: string, data?: Record | (() => Record), options?: ReportOptions) { - const innerData: Record = (await getInnerEventData(event)) || {}; + async report(event: string, content?: (...opts: any[]) => Promise | EventContent, collectOptions?: CollectorOptions, immediate?: boolean) { + let reportData: EventContent = {}; - const outerData = isFunction(data) ? data() : data; + if (isInnerEvent(event)) { + reportData = (await getInnerEventData(event, collectOptions)) || {}; + } else if (content) { + reportData = await content(); + } const eventData: EventData = { event: event, time: Date.now(), - data: Object.assign(innerData, outerData), + data: reportData, sId: this.enabled ? getSessionId(this.StoreKey.session) : '', }; - this.collect(eventData, options?.immediate); + this.collect(eventData, immediate); } } diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5e4dac05c7ff2a4ac7dd2c4e6b43db711b4105b --- /dev/null +++ b/packages/analytics/src/types.ts @@ -0,0 +1,43 @@ +export type EventContent = { + [k: string | number]: string | number | EventContent | undefined | null; +}; + +export interface EventData { + event: string; // 事件名 + time: number; // 事件采集时间 + data: EventContent; // 上报的事件数据 + sId: string; // 会话id +} + +export interface EventHeader { + cId?: string; // 客户端匿名标识,清除浏览器缓存销毁 + aId?: string; // 应用id + oa_version?: string; // OA版本 + screen_width?: number; // 屏幕宽度 + screen_height?: number; // 屏幕高度 + viewport_width?: number; // 视口宽度 + viewport_height?: number; // 视口高度 + os?: string; // 客户端操作系统 + os_version?: string; // 客户端操作系统版本 + browser?: string; // 客户端浏览器 + browser_version?: string; // 客户端浏览器版本 + device?: string; // 设备信息 + device_type?: string; // 设备类型 + device_vendor?: string; // 设备品牌 +} + +export interface ReportData { + header: EventHeader; + body: EventData[]; +} + +export type ReportRequest = (data: ReportData) => Promise | void; + +export interface OpenAnalyticsParams { + request: (data: ReportData) => Promise | void; // 上报数据的接口 + appKey?: string; // 采集app的key,用于区分多app上报 + immediate?: boolean; // 全局设置是否立即上报 + requestInterval?: number; //上报间隔 + maxEvents?: number; + requestPlan?: (requestFn: () => void) => void; +} diff --git a/packages/analytics/test/main.ts b/packages/analytics/test/main.ts index 863e62d53ec7b041a26f91c5f965a4c8a17daa9c..d160fff2ede4272a7aed70c6cfa9dc1e2213c94e 100644 --- a/packages/analytics/test/main.ts +++ b/packages/analytics/test/main.ts @@ -8,10 +8,10 @@ const oa = new OpenAnalytics({ appKey: 'test', request: (data) => { console.log('request to send content', data); - return fetch('report', { - method: 'POST', - body: JSON.stringify(data), - }).then((response) => response.ok); + // return fetch('report', { + // method: 'POST', + // body: JSON.stringify(data), + // }).then((response) => response.ok); }, // immediate: true, }); @@ -32,14 +32,23 @@ oa.report(OpenEventKeys.PV, () => ({ oa.report(OpenEventKeys.PageBasePerformance); oa.report(OpenEventKeys.LCP); oa.report(OpenEventKeys.INP); +oa.report(OpenEventKeys.PageClick, (e: MouseEvent) => { + const scroller = document.querySelector('.inner-screen'); + const el = e.target as HTMLElement | null; + return { + documentScrollLeft: 123, // 覆盖默认值 + name: el?.innerHTML, // 新增字段 + top: scroller?.scrollTop, // 新增字段 + }; +}); btn1?.addEventListener('click', () => { // window.open('/'); console.log('btn1 clicked'); - oa.report('btn-click', { + oa.report('btn-click', () => ({ date: Date.now(), - }); + })); }); btnOpen?.addEventListener('click', () => { diff --git a/packages/analytics/test/style.css b/packages/analytics/test/style.css index 1a8619a4bc503015381c6ec29e78d054c73bee37..738bf6b2facfd76e44632d23b346f9e2193de42f 100644 --- a/packages/analytics/test/style.css +++ b/packages/analytics/test/style.css @@ -1,6 +1,19 @@ #app { - width: 800px; + width: 1200px; margin: auto; + display: flex; + height: 100vh; + overflow: auto; + margin-top: 50px; +} + +* { + margin: 0; +} + +body { + margin: 0; + height: 100vh; } .box { @@ -11,4 +24,27 @@ .img { width: 50vw; +} + +.screen2 { + height: 150vh; + padding: 50px; + width: 50%; + background-color: rgba(1, 125, 1, .3); + overflow: auto; +} + +.screen { + height: 600px; + padding: 50px; + overflow: auto; +} + +.inner-screen { + background-color: rgba(130, 0, 0, .1); + height: 800px; + display: flex; + align-items: center; + justify-content: center; + position: relative; } \ No newline at end of file