@uxda/appkit 4.2.93 → 4.2.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,986 @@
1
+ /**
2
+ * 全埋点SDK - 精简版
3
+ * 集成核心埋点功能,支持自动埋点和手动埋点
4
+ */
5
+
6
+ import { onAppHide, setStorageSync, getSystemInfo, getNetworkType, getStorageSync, getEnv, getAccountInfoSync, getPerformance, request } from '@tarojs/taro'
7
+ import pako from 'pako'
8
+ import { isApp } from '../../utils/utils'
9
+
10
+ // 埋点事件类型枚举
11
+ export enum TrackingEventType {
12
+ PAGE_VIEW = 'page_view', // 页面访问
13
+ PAGE_LEAVE = 'page_leave', // 页面离开
14
+ APP_DEVICE_INFO = 'app_device_info', // app和device信息
15
+ CLICK = 'click', // 点击事件
16
+ CUSTOM = 'custom', // 自定义事件
17
+ }
18
+
19
+ // 导出枚举值供外部使用
20
+ export const { PAGE_VIEW, PAGE_LEAVE, CLICK, CUSTOM, APP_DEVICE_INFO } = TrackingEventType
21
+
22
+ // 日志类型(支持组合类型,如 "page/event/error/device/app")
23
+ export type LogType = 'page' | 'event' | 'error' | 'device' | 'app' | 'performance' | string
24
+
25
+ // 来源类型
26
+ export type SourceType = 'app' | 'H5' | 'PC' | string
27
+
28
+ // 页面信息接口
29
+ export interface PageData {
30
+ page_name?: string // 页面名称
31
+ page_url?: string // 页面URL
32
+ page_referrer?: string // 页面来源
33
+ previous_page_name?: string // 上一页名称
34
+ page_start_time?: string // 页面开始时间 (ISO 8601格式)
35
+ page_duration?: number // 页面停留时长(毫秒)
36
+ }
37
+
38
+ // 事件信息接口
39
+ export interface EventData {
40
+ event_type?: string // 事件类型
41
+ event_name?: string // 事件名称
42
+ element_id?: string // 元素ID
43
+ }
44
+
45
+ // 应用信息接口
46
+ export interface AppData {
47
+ app_version?: string // 应用版本
48
+ package_name?: string // 包名
49
+ app_channel?: string // 应用渠道
50
+ }
51
+
52
+ // 设备信息接口
53
+ export interface DeviceData {
54
+ device_type?: string // 设备类型 (mobile/tablet/desktop)
55
+ os?: string // 操作系统
56
+ os_version?: string // 操作系统版本
57
+ brand?: string // 品牌
58
+ model?: string // 型号
59
+ network_type?: string // 网络类型
60
+ }
61
+
62
+ // 性能信息接口
63
+ export interface PerformanceData {
64
+ page_load_time?: number // 页面加载时间(毫秒)
65
+ response_time?: number // 响应时间(毫秒)
66
+ fps?: number // 帧率
67
+ memory_usage?: number // 内存使用率(%)
68
+ battery_usage?: number // 电池使用率(%)
69
+ network_latency?: number // 网络延迟(毫秒)
70
+ crash_rate?: number // 崩溃率(%)
71
+ }
72
+
73
+ // 错误信息接口
74
+ export interface ErrorData {
75
+ error_code?: string // 错误代码
76
+ error_message?: string // 错误消息
77
+ stack_trace?: string // 堆栈跟踪
78
+ error_level?: string // 错误级别 (info/warning/error)
79
+ error_context?: string // 错误上下文
80
+ }
81
+
82
+ // 日志数据接口
83
+ export interface LogData {
84
+ page?: PageData
85
+ event?: EventData
86
+ app?: AppData
87
+ device?: DeviceData
88
+ performance?: PerformanceData
89
+ error?: ErrorData
90
+ }
91
+
92
+ // 埋点主数据接口(符合新的数据结构)
93
+ export interface TrackingMainData {
94
+ log_id?: string // 日志ID
95
+ log_time: number // 日志时间戳
96
+ log_type: LogType // 日志类型(支持组合类型,如 "page/event/error/device/app")
97
+ tenant_id?: string // 租户ID
98
+ user_id?: string // 用户ID
99
+ source: SourceType // 来源 (app/H5/PC)
100
+ session_id: string // 会话ID
101
+ log_data: LogData // 日志数据
102
+ }
103
+
104
+ // 内部使用的数据结构(用于方法参数)
105
+ interface TrackingData {
106
+ logTime?: number // 时间戳
107
+ sessionId?: string // 会话ID
108
+ userId?: string // 用户ID
109
+ tenantId?: string // 租户ID
110
+ pagePath?: string // 页面路径
111
+ pageTitle?: string // 页面标题
112
+ lastPagePath?: string // 上一页路径
113
+ lastPageTitle?: string // 上一页标题
114
+ elementId?: string // 元素ID
115
+ elementText?: string // 元素文本
116
+ businessData?: Record<string, unknown> // 业务数据
117
+ }
118
+
119
+ // 用户信息接口
120
+ export interface TrackingUserInfo {
121
+ userId?: string // 用户ID
122
+ tenantId?: string // 租户ID
123
+ sessionId?: string // 会话ID
124
+ }
125
+
126
+ // 埋点配置接口
127
+ export interface TrackingConfig {
128
+ enabled: boolean // 是否启用埋点
129
+ debug: boolean // 是否开启调试模式
130
+ batchSize: number // 批量发送大小
131
+ flushInterval: number // 刷新间隔(ms)
132
+ apiEndpoint: string // API端点
133
+ userInfo?: TrackingUserInfo // 用户信息
134
+ }
135
+
136
+ // 默认配置
137
+ const DEFAULT_CONFIG: TrackingConfig = {
138
+ enabled: true,
139
+ debug: false,
140
+ batchSize: 30,
141
+ flushInterval: 100000,
142
+ apiEndpoint: '',
143
+ }
144
+
145
+ /**
146
+ * 全埋点SDK类
147
+ */
148
+ // 页面历史记录接口
149
+ interface PageHistory {
150
+ route: string
151
+ title: string
152
+ timestamp: number
153
+ }
154
+
155
+ class TrackingSDK {
156
+ private config: TrackingConfig
157
+ private eventQueue: TrackingMainData[] = []
158
+ private currentPage: string = ''
159
+ private pageStartTime: number = 0
160
+ private flushTimer: ReturnType<typeof setInterval> | null = null
161
+ private isInitialized: boolean = false
162
+ private pageHistory: PageHistory[] = [] // 页面历史记录
163
+ private readonly PAGE_HISTORY_KEY = 'tracking_page_history' // 存储键
164
+ private readonly MAX_HISTORY_LENGTH = 10 // 最大历史记录数
165
+ private userInfo: TrackingUserInfo | undefined = undefined // 用户信息(优先使用)
166
+
167
+ constructor(config: Partial<TrackingConfig> = {}) {
168
+ this.config = { ...DEFAULT_CONFIG, ...config }
169
+ // 保存传入的用户信息
170
+ if (config.userInfo) {
171
+ this.userInfo = config.userInfo
172
+ }
173
+ this.init()
174
+ }
175
+
176
+ /**
177
+ * 初始化SDK
178
+ */
179
+ private init(): void {
180
+ if (this.isInitialized) return
181
+
182
+ this.isInitialized = true
183
+ this.loadPageHistory() // 加载页面历史记录
184
+ this.setupAutoTracking()
185
+ this.startFlushTimer()
186
+
187
+ if (this.config.debug) {
188
+ console.log('[TrackingSDK] 初始化完成', this.config)
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 加载页面历史记录
194
+ */
195
+ private loadPageHistory(): void {
196
+ try {
197
+ const historyStr = getStorageSync(this.PAGE_HISTORY_KEY)
198
+ if (historyStr) {
199
+ this.pageHistory = JSON.parse(historyStr)
200
+ }
201
+ } catch (error) {
202
+ if (this.config.debug) {
203
+ console.log('[TrackingSDK] 加载页面历史失败:', error)
204
+ }
205
+ this.pageHistory = []
206
+ }
207
+ }
208
+
209
+ /**
210
+ * 简单的Token加密 (Base64 + 反转)
211
+ * @returns 加密后的token字符串
212
+ */
213
+ private encryptToken(): string {
214
+ const raw = `Ddjf@Log1125|${Date.now()}`
215
+
216
+ // Base64 编码(兼容 H5 和小程序环境)
217
+ let base64: string
218
+ if (typeof btoa !== 'undefined') {
219
+ // H5 环境使用 btoa
220
+ base64 = btoa(unescape(encodeURIComponent(raw)))
221
+ } else {
222
+ // 小程序环境使用手动实现的 Base64 编码
223
+ base64 = this.base64Encode(raw)
224
+ }
225
+
226
+ // 反转字符串
227
+ return base64.split('').reverse().join('')
228
+ }
229
+ /**
230
+ * Base64 编码(降级方案)
231
+ */
232
+ private base64Encode(str: string): string {
233
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
234
+ let output = ''
235
+ let i = 0
236
+
237
+ while (i < str.length) {
238
+ const a = str.charCodeAt(i++)
239
+ const b = i < str.length ? str.charCodeAt(i++) : 0
240
+ const c = i < str.length ? str.charCodeAt(i++) : 0
241
+
242
+ const bitmap = (a << 16) | (b << 8) | c
243
+
244
+ output += chars.charAt((bitmap >> 18) & 63)
245
+ output += chars.charAt((bitmap >> 12) & 63)
246
+ output += i - 2 < str.length ? chars.charAt((bitmap >> 6) & 63) : '='
247
+ output += i - 1 < str.length ? chars.charAt(bitmap & 63) : '='
248
+ }
249
+
250
+ return output
251
+ }
252
+
253
+ /**
254
+ * 保存页面历史记录
255
+ */
256
+ private savePageHistory(): void {
257
+ try {
258
+ setStorageSync(this.PAGE_HISTORY_KEY, JSON.stringify(this.pageHistory))
259
+ } catch (error) {
260
+ if (this.config.debug) {
261
+ console.log('[TrackingSDK] 保存页面历史失败:', error)
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 添加页面到历史记录
268
+ */
269
+ private addPageToHistory(route: string, title: string): void {
270
+ const history: PageHistory = {
271
+ route,
272
+ title,
273
+ timestamp: Date.now(),
274
+ }
275
+
276
+ // 添加到历史记录
277
+ this.pageHistory.push(history)
278
+
279
+ // 保持历史记录在最大长度内
280
+ if (this.pageHistory.length > this.MAX_HISTORY_LENGTH) {
281
+ this.pageHistory.shift()
282
+ }
283
+
284
+ // 保存到本地存储
285
+ this.savePageHistory()
286
+ }
287
+
288
+ /**
289
+ * 获取上一页信息(从历史记录中)
290
+ */
291
+ private getLastPageFromHistory(): PageHistory | null {
292
+ if (this.pageHistory.length >= 2) {
293
+ // 返回倒数第二个页面(最后一个是当前页面)
294
+ return this.pageHistory[this.pageHistory.length - 2]
295
+ }
296
+ return null
297
+ }
298
+
299
+ /**
300
+ * 清空页面历史记录
301
+ */
302
+ public clearPageHistory(): void {
303
+ this.pageHistory = []
304
+ this.savePageHistory()
305
+ }
306
+
307
+ /**
308
+ * 获取用户信息
309
+ * 优先级:1. 初始化时传入的userInfo 2. Storage中的用户信息 3. 生成新的sessionId
310
+ */
311
+ private getUserInfo(): { userId?: string; tenantId?: string; sessionId?: string } {
312
+ if (this.userInfo) {
313
+ return {
314
+ userId: this.userInfo.userId,
315
+ tenantId: this.userInfo.tenantId,
316
+ sessionId: this.userInfo.sessionId || this.generateSessionId(),
317
+ }
318
+ }
319
+
320
+ return { sessionId: this.generateSessionId() }
321
+ }
322
+
323
+ /**
324
+ * 生成会话ID
325
+ */
326
+ private generateSessionId(): string {
327
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
328
+ }
329
+
330
+ /**
331
+ * 获取设备信息
332
+ */
333
+ private async getDeviceInfo(): Promise<DeviceData> {
334
+ try {
335
+ const systemInfo = await getSystemInfo()
336
+
337
+ // 获取网络类型
338
+ let networkType = ''
339
+ try {
340
+ const networkInfo = await getNetworkType()
341
+ networkType = networkInfo.networkType || ''
342
+ } catch {
343
+ // 网络类型获取失败,使用空字符串
344
+ networkType = ''
345
+ }
346
+
347
+ return {
348
+ device_type:
349
+ systemInfo.platform === 'ios' || systemInfo.platform === 'android' ? 'mobile' : 'desktop',
350
+ os: systemInfo.platform,
351
+ os_version: systemInfo.system,
352
+ brand: systemInfo.brand || '',
353
+ model: systemInfo.model || '',
354
+ network_type: networkType,
355
+ }
356
+ } catch {
357
+ return {}
358
+ }
359
+ }
360
+
361
+ /**
362
+ * 获取应用信息
363
+ */
364
+ private getAppInfo(): AppData {
365
+ try {
366
+ const env = getEnv()
367
+ if (env === 'WEAPP') {
368
+ const accountInfo = getAccountInfoSync?.()
369
+ return {
370
+ app_version: accountInfo?.miniProgram?.version || '1.0.0',
371
+ package_name: accountInfo?.miniProgram?.appId || '',
372
+ app_channel: 'WeChat',
373
+ }
374
+ } else {
375
+ return {
376
+ app_version: '1.0.0',
377
+ package_name: '',
378
+ app_channel: env === 'WEB' ? 'H5' : 'App Store',
379
+ }
380
+ }
381
+ } catch {
382
+ return {
383
+ app_version: '1.0.0',
384
+ package_name: '',
385
+ app_channel: getEnv() === 'WEB' ? 'H5' : 'Unknown',
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * 获取来源类型
392
+ */
393
+ private getSourceType(): SourceType {
394
+ const env = getEnv()
395
+ if (isApp()) return 'app'
396
+ if (env === 'WEB') return 'H5'
397
+ if (env === 'WEAPP') return 'app'
398
+ return 'PC'
399
+ }
400
+
401
+ /**
402
+ * 获取页面性能数据
403
+ */
404
+ private async getPerformanceData(): Promise<PerformanceData> {
405
+ try {
406
+ const performanceData: PerformanceData = {}
407
+ const env = getEnv()
408
+
409
+ if (env === 'WEB') {
410
+ // H5 环境:使用浏览器原生 Performance API
411
+ if (typeof window !== 'undefined' && window.performance) {
412
+ const perfEntries = window.performance.getEntriesByType('navigation')
413
+ if (perfEntries && perfEntries.length > 0) {
414
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
415
+ const navigationTiming = perfEntries[0] as any
416
+ // 页面加载完成时间
417
+ performanceData.page_load_time = Math.round(navigationTiming.duration || 0)
418
+ // 响应时间
419
+ performanceData.response_time = Math.round(
420
+ (navigationTiming.responseEnd || 0) - (navigationTiming.requestStart || 0)
421
+ )
422
+ }
423
+
424
+ // 获取内存信息(部分浏览器支持)
425
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
+ const memory = (window.performance as any).memory
427
+ if (memory) {
428
+ const usedMemory = memory.usedJSHeapSize || 0
429
+ const totalMemory = memory.jsHeapSizeLimit || 0
430
+ if (totalMemory > 0) {
431
+ performanceData.memory_usage = Number(((usedMemory / totalMemory) * 100).toFixed(2))
432
+ }
433
+ }
434
+ }
435
+ } else {
436
+ // 小程序环境:使用 Taro.getPerformance()
437
+ const performance = await getPerformance()
438
+ if (performance) {
439
+ // 页面加载时间
440
+ const entryList = performance.getEntriesByType?.('navigation')
441
+ if (entryList && entryList.length > 0) {
442
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
443
+ const navigationTiming = entryList[0] as any
444
+ // 页面加载完成时间
445
+ performanceData.page_load_time = Math.round(navigationTiming.duration || 0)
446
+ // 响应时间
447
+ performanceData.response_time = Math.round(
448
+ (navigationTiming.responseEnd || 0) - (navigationTiming.requestStart || 0)
449
+ )
450
+ }
451
+
452
+ // 获取内存信息(仅小程序支持)
453
+ if (env === 'WEAPP') {
454
+ try {
455
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
456
+ const performanceObj = performance as any
457
+ const memoryInfo = performanceObj?.getMemoryInfo?.()
458
+ if (memoryInfo) {
459
+ // 计算内存使用率
460
+ const usedMemory = memoryInfo.usedJSHeapSize || 0
461
+ const totalMemory = memoryInfo.totalJSHeapSize || memoryInfo.jsHeapSizeLimit || 0
462
+ if (totalMemory > 0) {
463
+ performanceData.memory_usage = Number(
464
+ ((usedMemory / totalMemory) * 100).toFixed(2)
465
+ )
466
+ }
467
+ }
468
+ } catch {
469
+ // 内存信息获取失败,忽略
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ return performanceData
476
+ } catch (error) {
477
+ if (this.config.debug) {
478
+ console.log('[TrackingSDK] 获取性能数据失败:', error)
479
+ }
480
+ return {}
481
+ }
482
+ }
483
+
484
+ /**
485
+ * 创建基础埋点数据(新版数据结构)
486
+ */
487
+ private async createBaseTrackingData(
488
+ eventType: TrackingEventType,
489
+ customData?: Partial<TrackingData>
490
+ ): Promise<TrackingMainData> {
491
+ const userInfo = this.getUserInfo()
492
+ const pagePath = customData?.pagePath || this.currentPage || ''
493
+ const now = Date.now()
494
+ const pageData: PageData = {}
495
+ const eventData: EventData = {}
496
+ let logType: string = ''
497
+
498
+ // 判断是否需要包含app和device信息
499
+ let appInfo: AppData = {}
500
+ let deviceInfo: DeviceData = {}
501
+ let performanceData: PerformanceData = {}
502
+
503
+ if (eventType === TrackingEventType.APP_DEVICE_INFO) {
504
+ deviceInfo = await this.getDeviceInfo()
505
+ appInfo = this.getAppInfo()
506
+ } else {
507
+ // 构建页面数据
508
+ if (pagePath) {
509
+ pageData.page_url = pagePath
510
+ pageData.page_name = customData?.pageTitle || this.getPageTitle(pagePath)
511
+ pageData.page_referrer = customData?.lastPagePath || ''
512
+ pageData.previous_page_name = customData?.lastPageTitle || ''
513
+ if (this.pageStartTime) {
514
+ pageData.page_start_time = new Date(this.pageStartTime).toISOString()
515
+ pageData.page_duration = now - this.pageStartTime
516
+ }
517
+ }
518
+
519
+ // 构建事件数据
520
+ if (eventType === TrackingEventType.CLICK) {
521
+ eventData.event_type = 'click'
522
+ eventData.event_name =
523
+ customData?.elementText || (customData?.businessData?.elementText as string) || '点击事件'
524
+ eventData.element_id = customData?.elementId || ''
525
+ } else if (eventType === TrackingEventType.CUSTOM) {
526
+ eventData.event_type = 'custom'
527
+ eventData.event_name = (customData?.businessData?.eventName as string) || '自定义事件'
528
+ } else {
529
+ eventData.event_type = eventType
530
+ eventData.event_name = eventType
531
+ }
532
+
533
+ // 如果是页面访问事件,自动采集性能数据
534
+ if (eventType === TrackingEventType.PAGE_VIEW || customData?.businessData?.performance) {
535
+ performanceData = await this.getPerformanceData()
536
+ }
537
+ }
538
+
539
+ // 构建日志类型
540
+ if ([TrackingEventType.CLICK, TrackingEventType.CUSTOM].includes(eventType)) {
541
+ logType = 'event'
542
+ } else if ([TrackingEventType.PAGE_VIEW, TrackingEventType.PAGE_LEAVE].includes(eventType)) {
543
+ logType = 'page'
544
+ } else if ([TrackingEventType.APP_DEVICE_INFO].includes(eventType)) {
545
+ logType = 'device'
546
+ }
547
+
548
+ // 合并性能数据:优先使用自动采集的,其次使用手动传入的
549
+ const finalPerformanceData: PerformanceData = {
550
+ ...performanceData,
551
+ ...(customData?.businessData?.performance as PerformanceData),
552
+ }
553
+
554
+ // 构建 log_data,过滤掉空值
555
+ const logData: LogData = {}
556
+ if (Object.keys(pageData).length > 0) {
557
+ logData.page = pageData
558
+ }
559
+ if (Object.keys(eventData).length > 0) {
560
+ logData.event = eventData
561
+ }
562
+ if (Object.keys(appInfo).length > 0) {
563
+ logData.app = appInfo
564
+ }
565
+ if (Object.keys(deviceInfo).length > 0) {
566
+ logData.device = deviceInfo
567
+ }
568
+ if (Object.keys(finalPerformanceData).length > 0) {
569
+ logData.performance = finalPerformanceData
570
+ }
571
+ const errorData = customData?.businessData?.error as ErrorData
572
+ if (errorData && Object.keys(errorData).length > 0) {
573
+ logData.error = errorData
574
+ }
575
+
576
+ const trackingData: TrackingMainData = {
577
+ log_id: `${now}_${Math.random().toString(36).substr(2, 9)}`,
578
+ log_time: now,
579
+ log_type: logType || 'event',
580
+ tenant_id: customData?.tenantId || userInfo.tenantId || '',
581
+ user_id: customData?.userId || userInfo.userId || '',
582
+ source: this.getSourceType(),
583
+ session_id: userInfo.sessionId || '',
584
+ log_data: logData,
585
+ }
586
+
587
+ return trackingData
588
+ }
589
+
590
+ /**
591
+ * 根据页面路径获取页面标题
592
+ */
593
+ private getPageTitle(pagePath: string): string {
594
+ const titleMap: Record<string, string> = {
595
+ '/aiapprove/pages': 'AI审批',
596
+ '/aiapprove/views': '企明星',
597
+ '/pages/beidou': '北斗星',
598
+ '/pages/contracts': '蜂鸟签约',
599
+ '/pages/user': '我的',
600
+ }
601
+
602
+ for (const [path, title] of Object.entries(titleMap)) {
603
+ if (pagePath.includes(path)) {
604
+ return title
605
+ }
606
+ }
607
+
608
+ return pagePath
609
+ }
610
+
611
+ /**
612
+ * 添加埋点数据到队列
613
+ */
614
+ private addToQueue(data: TrackingMainData): void {
615
+ if (!this.config.enabled) return
616
+
617
+ this.eventQueue.push(data)
618
+
619
+ if (this.config.debug) {
620
+ console.log('[TrackingSDK] 添加埋点数据:', data)
621
+ }
622
+
623
+ // 检查是否需要立即发送
624
+ if (this.eventQueue.length >= this.config.batchSize) {
625
+ this.flush()
626
+ }
627
+ }
628
+
629
+ /**
630
+ * 发送埋点数据
631
+ */
632
+ private async flush(): Promise<void> {
633
+ if (this.eventQueue.length === 0) return
634
+
635
+ const events = [...this.eventQueue]
636
+ this.eventQueue = []
637
+
638
+ try {
639
+ // 将对象数组转为 JSON 字符串
640
+ const jsonStr = JSON.stringify(events)
641
+
642
+ // 使用 pako 压缩
643
+ const compressed = pako.deflate(jsonStr)
644
+
645
+ // if (this.config.debug) {
646
+ // console.log('[TrackingSDK] 原始数据大小:', new Blob([jsonStr]).size, 'bytes')
647
+ // console.log('[TrackingSDK] 压缩后大小:', compressed.length, 'bytes')
648
+ // console.log(
649
+ // '[TrackingSDK] 压缩率:',
650
+ // ((compressed.length / new Blob([jsonStr]).size) * 100).toFixed(2) + '%'
651
+ // )
652
+ // }
653
+
654
+ // 批量发送埋点数据
655
+ const encryptedToken = this.encryptToken()
656
+ const { success } = await this.http({
657
+ url: `${this.config.apiEndpoint}?contentId=${encryptedToken}`,
658
+ method: 'POST',
659
+ data: compressed,
660
+ header: {
661
+ 'Content-Encoding': 'deflate',
662
+ },
663
+ })
664
+
665
+ if (success && this.config.debug) {
666
+ console.log('[TrackingSDK] 埋点数据发送成功:', events.length, '条')
667
+ }
668
+ } catch (error) {
669
+ console.error('[TrackingSDK] 埋点数据发送失败:', error)
670
+
671
+ // 重试逻辑 - 将失败的数据放回队列
672
+ if (events.length > 0) {
673
+ this.eventQueue.unshift(...events)
674
+ }
675
+ }
676
+ }
677
+
678
+ /**
679
+ * 请求拦截
680
+ */
681
+ private http(params: {
682
+ url: string
683
+ method: 'GET' | 'POST'
684
+ data: any
685
+ header: any
686
+ }): Promise<any> {
687
+ return new Promise((resolve, reject) => {
688
+ request({
689
+ url: params.url,
690
+ method: params.method || 'GET',
691
+ data: params.data,
692
+ header: params.header,
693
+ })
694
+ .then((res) => {
695
+ console.log('===res', res)
696
+ resolve(res.data)
697
+ })
698
+ .catch((err) => {
699
+ reject(err)
700
+ })
701
+ })
702
+ }
703
+
704
+ /**
705
+ * 启动定时刷新
706
+ */
707
+ private startFlushTimer(): void {
708
+ if (this.flushTimer) {
709
+ clearInterval(this.flushTimer)
710
+ }
711
+
712
+ this.flushTimer = setInterval(() => {
713
+ this.flush()
714
+ }, this.config.flushInterval)
715
+ }
716
+
717
+ /**
718
+ * 设置自动埋点
719
+ */
720
+ private setupAutoTracking(): void {
721
+ // 监听页面隐藏 - 立即发送剩余数据
722
+ onAppHide(() => {
723
+ this.trackPageLeave()
724
+ // 立即发送剩余的埋点数据
725
+ this.flushNow()
726
+ })
727
+
728
+ // H5 环境:监听页面卸载事件
729
+ if (getEnv() === 'WEB' && typeof window !== 'undefined') {
730
+ // 使用 beforeunload 事件
731
+ window.addEventListener('beforeunload', () => {
732
+ this.flushNow()
733
+ })
734
+
735
+ // 使用 visibilitychange 事件(页面隐藏时发送)
736
+ document.addEventListener('visibilitychange', () => {
737
+ if (document.visibilityState === 'hidden') {
738
+ this.flushNow()
739
+ }
740
+ })
741
+
742
+ // 使用 pagehide 事件(更可靠)
743
+ window.addEventListener('pagehide', () => {
744
+ this.flushNow()
745
+ })
746
+ }
747
+ }
748
+
749
+ /**
750
+ * 手动埋点 - 页面访问
751
+ */
752
+ public async trackPageView(pageTitle?: string): Promise<void> {
753
+ const currentPage = this.getPageInfoByIndex()
754
+ const currentPath = (currentPage.route as string) || ''
755
+ const currentTitle =
756
+ pageTitle ||
757
+ (currentPage.options?.title as string) ||
758
+ currentPage.config?.navigationBarTitleText ||
759
+ ''
760
+
761
+ // 添加当前页到历史记录
762
+ if (currentPath) {
763
+ this.addPageToHistory(currentPath, currentTitle)
764
+ }
765
+
766
+ // 从历史记录获取上一页信息
767
+ let lastPath = ''
768
+ let lastTitle = ''
769
+ const lastPageHistory = this.getLastPageFromHistory()
770
+ if (lastPageHistory) {
771
+ lastPath = lastPageHistory.route
772
+ lastTitle = lastPageHistory.title
773
+ }
774
+
775
+ this.currentPage = currentPath
776
+ this.pageStartTime = Date.now()
777
+
778
+ const data = await this.createBaseTrackingData(TrackingEventType.PAGE_VIEW, {
779
+ pagePath: currentPath,
780
+ pageTitle: currentTitle,
781
+ lastPagePath: lastPath,
782
+ lastPageTitle: lastTitle,
783
+ })
784
+
785
+ // if (this.config.debug) {
786
+ // console.log('[TrackingSDK] 页面访问埋点:', {
787
+ // current: { path: currentPath, title: currentTitle },
788
+ // last: { path: lastPath, title: lastTitle },
789
+ // historyLength: this.pageHistory.length,
790
+ // })
791
+ // }
792
+
793
+ this.addToQueue(data)
794
+ }
795
+
796
+ /**
797
+ * 手动埋点 - 页面离开
798
+ */
799
+ public async trackPageLeave(pagePath?: string): Promise<void> {
800
+ const path = pagePath || this.currentPage
801
+ if (!path) return
802
+
803
+ const data = await this.createBaseTrackingData(TrackingEventType.PAGE_LEAVE, {
804
+ pagePath: path,
805
+ })
806
+
807
+ this.addToQueue(data)
808
+ }
809
+
810
+ /**
811
+ * 手动埋点 - 点击事件
812
+ */
813
+ public async trackClick(elementId?: string, elementText?: string): Promise<void> {
814
+ const data = await this.createBaseTrackingData(TrackingEventType.CLICK, {
815
+ elementId,
816
+ elementText,
817
+ })
818
+
819
+ this.addToQueue(data)
820
+ }
821
+
822
+ /**
823
+ * 手动埋点 - 自定义事件
824
+ */
825
+ public async trackCustom(eventName: string, customData?: Record<string, unknown>): Promise<void> {
826
+ const data = await this.createBaseTrackingData(TrackingEventType.CUSTOM, {
827
+ businessData: {
828
+ eventName,
829
+ ...customData,
830
+ },
831
+ })
832
+
833
+ this.addToQueue(data)
834
+ }
835
+
836
+ /**
837
+ * 手动埋点 - 业务事件
838
+ */
839
+ public async trackBusiness(
840
+ event: TrackingEventType,
841
+ businessData?: Record<string, unknown>
842
+ ): Promise<void> {
843
+ const data = await this.createBaseTrackingData(event, {
844
+ businessData,
845
+ })
846
+
847
+ this.addToQueue(data)
848
+ }
849
+
850
+ /**
851
+ * 手动埋点 - app 和 device 信息
852
+ */
853
+ public async trackAppDeviceInfo(): Promise<void> {
854
+ const data = await this.createBaseTrackingData(TrackingEventType.APP_DEVICE_INFO, {})
855
+
856
+ this.addToQueue(data)
857
+ }
858
+
859
+ /**
860
+ * 获取指定页面信息
861
+ */
862
+ public getPageInfoByIndex(index = 1): {
863
+ route?: string
864
+ options?: Record<string, unknown>
865
+ config?: { navigationBarTitleText?: string }
866
+ [key: string]: unknown
867
+ } {
868
+ try {
869
+ const pages = getCurrentPages()
870
+ const currentPage = pages[pages.length - index]
871
+ return currentPage || {}
872
+ } catch {
873
+ return {}
874
+ }
875
+ }
876
+
877
+ /**
878
+ * 更新配置
879
+ */
880
+ public updateConfig(newConfig: Partial<TrackingConfig>): void {
881
+ this.config = {
882
+ enabled: newConfig.enabled ?? this.config.enabled,
883
+ debug: newConfig.debug ?? this.config.debug,
884
+ batchSize: newConfig.batchSize ?? this.config.batchSize,
885
+ flushInterval: newConfig.flushInterval ?? this.config.flushInterval,
886
+ apiEndpoint: newConfig.apiEndpoint ?? this.config.apiEndpoint,
887
+ userInfo: newConfig.userInfo ?? this.userInfo,
888
+ };
889
+
890
+ // 更新用户信息
891
+ if (newConfig.userInfo !== undefined) {
892
+ this.userInfo = newConfig.userInfo
893
+ }
894
+
895
+ if (newConfig.flushInterval) {
896
+ this.startFlushTimer()
897
+ }
898
+ }
899
+
900
+ /**
901
+ * 立即发送所有待发送的数据(同步方式)
902
+ */
903
+ public flushNow(): void {
904
+ if (this.eventQueue.length === 0) return
905
+
906
+ const events = [...this.eventQueue]
907
+ this.eventQueue = []
908
+
909
+ try {
910
+ // 使用同步方式发送(适用于页面关闭场景)
911
+ const env = getEnv()
912
+
913
+ if (env === 'WEB' && typeof navigator !== 'undefined' && navigator.sendBeacon) {
914
+ // H5 环境:使用 sendBeacon API(专为页面卸载场景设计)(sendBeacon无法指定header头, 无法deflate压缩)
915
+ const blob = new Blob([JSON.stringify(events)], {
916
+ type: 'application/json',
917
+ })
918
+ const encryptedToken = this.encryptToken()
919
+ const sent = navigator.sendBeacon(
920
+ `${this.config.apiEndpoint}?contentId=${encryptedToken}`,
921
+ blob
922
+ )
923
+
924
+ if (this.config.debug) {
925
+ console.log(
926
+ '[TrackingSDK] 使用 sendBeacon 发送:',
927
+ sent ? '成功' : '失败',
928
+ events.length,
929
+ '条'
930
+ )
931
+ }
932
+
933
+ // 如果 sendBeacon 失败,降级使用普通请求
934
+ if (!sent) {
935
+ this.eventQueue.unshift(...events)
936
+ this.flush().catch(console.error)
937
+ }
938
+ } else {
939
+ // 小程序环境:使用普通异步请求
940
+ this.flush().catch((error) => {
941
+ console.error('[TrackingSDK] 立即发送失败:', error)
942
+ // 发送失败,重新加入队列
943
+ this.eventQueue.unshift(...events)
944
+ })
945
+ }
946
+ } catch (error) {
947
+ console.error('[TrackingSDK] flushNow 执行失败:', error)
948
+ // 发送失败,重新加入队列
949
+ this.eventQueue.unshift(...events)
950
+ }
951
+ }
952
+
953
+ /**
954
+ * 销毁SDK
955
+ */
956
+ public destroy(): void {
957
+ if (this.flushTimer) {
958
+ clearInterval(this.flushTimer)
959
+ this.flushTimer = null
960
+ }
961
+
962
+ // 立即发送剩余数据
963
+ this.flushNow()
964
+
965
+ this.isInitialized = false
966
+ }
967
+ }
968
+
969
+ // 创建全局实例
970
+ export const trackingSDK = new TrackingSDK()
971
+
972
+ // 导出便捷方法(异步版本)
973
+ export const trackPageView = async (pageTitle?: string): Promise<void> =>
974
+ await trackingSDK.trackPageView(pageTitle)
975
+ export const trackClick = async (elementId?: string, elementText?: string): Promise<void> =>
976
+ await trackingSDK.trackClick(elementId, elementText)
977
+ export const trackCustom = async (
978
+ eventName: string,
979
+ customData?: Record<string, unknown>
980
+ ): Promise<void> => await trackingSDK.trackCustom(eventName, customData)
981
+ export const trackBusiness = async (
982
+ event: TrackingEventType,
983
+ businessData?: Record<string, unknown>
984
+ ): Promise<void> => await trackingSDK.trackBusiness(event, businessData)
985
+
986
+ export default trackingSDK