@rk911/tracker-sdk 1.6.4

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,172 @@
1
+ export class PageViewPlugin {
2
+ constructor() {
3
+ this.name = 'page-view'
4
+ this.tracker = null
5
+ this._lastPage = ''
6
+ this._pendingTimer = null
7
+ this._lastFiredRoute = ''
8
+ this._lastFiredTime = 0
9
+ }
10
+
11
+ install(tracker) {
12
+ this.tracker = tracker
13
+ var adapter = tracker.getAdapter()
14
+ var platform = adapter.getPlatformName()
15
+ var self = this
16
+
17
+ if (platform === 'web' || platform === 'app') {
18
+ adapter.onRouteChange(function () {
19
+ // 取消上一次未完成的检测(防止 pushState+hashchange 双触发)
20
+ if (self._pendingTimer) {
21
+ clearTimeout(self._pendingTimer)
22
+ self._pendingTimer = null
23
+ }
24
+ self._waitForNewRoute(function () {
25
+ self._reportPageView()
26
+ })
27
+ })
28
+ // 首次 PV
29
+ self._waitForRoute(function () {
30
+ self._reportPageView()
31
+ })
32
+
33
+ // 会话过期重建时补报当前页 page_view(保证每个 session 都有入口页)
34
+ tracker.getSession().onRenew(function () {
35
+ var currentPage = adapter.getCurrentPagePath()
36
+ if (currentPage && currentPage !== '/') {
37
+ self._lastPage = ''
38
+ self._reportPageView()
39
+ }
40
+ })
41
+ }
42
+ }
43
+
44
+ // 首次加载:等路径有效即可
45
+ _waitForRoute(callback) {
46
+ var adapter = this.tracker.getAdapter()
47
+ var attempts = 0
48
+ var maxAttempts = 10
49
+
50
+ function check() {
51
+ var path = adapter.getCurrentPagePath()
52
+ if (path && path !== '/') {
53
+ callback()
54
+ } else if (attempts < maxAttempts) {
55
+ attempts++
56
+ setTimeout(check, 20)
57
+ } else {
58
+ callback()
59
+ }
60
+ }
61
+ check()
62
+ }
63
+
64
+ // 后续导航:等路径真正变化后再上报 page_view
65
+ _waitForNewRoute(callback) {
66
+ var adapter = this.tracker.getAdapter()
67
+ var self = this
68
+ var attempts = 0
69
+ var maxAttempts = 25
70
+
71
+ function check() {
72
+ var path = adapter.getCurrentPagePath()
73
+ if (path && path !== '/' && path !== self._lastPage) {
74
+ self._pendingTimer = null
75
+ callback()
76
+ } else if (attempts < maxAttempts) {
77
+ attempts++
78
+ self._pendingTimer = setTimeout(check, 20)
79
+ } else {
80
+ self._pendingTimer = null
81
+ }
82
+ }
83
+ check()
84
+ }
85
+
86
+ _reportPageView() {
87
+ var adapter = this.tracker.getAdapter()
88
+ var currentPage = adapter.getCurrentPagePath()
89
+
90
+ // 去重:同一个页面不重复上报
91
+ if (currentPage === this._lastPage) return
92
+
93
+ // 防抖:同路由 500ms 内不重复上报(防框架路由重入导致的竞态)
94
+ var now = Date.now()
95
+ if (this._lastFiredRoute === currentPage && now - this._lastFiredTime < 500) return
96
+
97
+ this._lastPage = currentPage
98
+ this._lastFiredRoute = currentPage
99
+ this._lastFiredTime = now
100
+ this.tracker.updatePageRoute(currentPage)
101
+
102
+ var self = this
103
+ // 代次令牌:每次进入生成新代次,旧的 title 重试链检测到代次变化即中止
104
+ // 防止导航交叠时,未完成的重试链把过期路由的 page_view 发出去
105
+ var myGen = (this._pvGen = (this._pvGen || 0) + 1)
106
+ var titleAttempts = 0
107
+ var prevTitle = ''
108
+ function reportWithTitle() {
109
+ if (myGen !== self._pvGen) return
110
+ var title = typeof document !== 'undefined' ? document.title : ''
111
+ // 第一轮:无论 title 有无值,都等一个 tick 让框架有机会更新
112
+ if (titleAttempts === 0) {
113
+ prevTitle = title
114
+ titleAttempts++
115
+ setTimeout(reportWithTitle, 50)
116
+ return
117
+ }
118
+ // title 为空:继续等
119
+ if (!title && titleAttempts < 10) {
120
+ titleAttempts++
121
+ setTimeout(reportWithTitle, 50)
122
+ return
123
+ }
124
+ // title 在两次检查间发生变化:说明框架刚更新了,再等一轮确认稳定
125
+ if (title !== prevTitle && titleAttempts < 10) {
126
+ prevTitle = title
127
+ titleAttempts++
128
+ setTimeout(reportWithTitle, 50)
129
+ return
130
+ }
131
+ // 兜底:如果 title 在所有重试后仍未变化(框架未更新 document.title),
132
+ // 尝试从 uni-app 页面栈读取 navigationBarTitle
133
+ if (titleAttempts >= 10 && title === prevTitle) {
134
+ try {
135
+ var pages = getCurrentPages()
136
+ var cur = pages[pages.length - 1]
137
+ var stackTitle = (cur && cur.$page && cur.$page.meta && cur.$page.meta.navigationBar
138
+ ? cur.$page.meta.navigationBar.titleText : '') || ''
139
+ if (stackTitle) title = stackTitle
140
+ } catch (e) {}
141
+ }
142
+ self._lastTitle = title
143
+ self.tracker.track('page_view', {
144
+ pageRoute: currentPage,
145
+ page: currentPage,
146
+ pageTitle: title,
147
+ })
148
+ }
149
+ reportWithTitle()
150
+ }
151
+
152
+ // 供 mixin 调用(小程序端)
153
+ trackPageShow() {
154
+ var adapter = this.tracker.getAdapter()
155
+ var currentPage = adapter.getCurrentPagePath()
156
+ this._lastPage = currentPage
157
+ this.tracker.updatePageRoute(currentPage)
158
+
159
+ this.tracker.track('page_view', {
160
+ pageRoute: currentPage,
161
+ page: currentPage,
162
+ })
163
+ }
164
+
165
+ destroy() {
166
+ if (this._pendingTimer) {
167
+ clearTimeout(this._pendingTimer)
168
+ this._pendingTimer = null
169
+ }
170
+ this.tracker = null
171
+ }
172
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * 页面性能监控插件
3
+ *
4
+ * 采集指标:
5
+ * dns — DNS 解析耗时
6
+ * tcp — TCP 连接耗时
7
+ * ttfb — 首字节时间(Time To First Byte)
8
+ * domReady — DOM 加载完成时间
9
+ * load — 页面完全加载时间
10
+ * fcp — 首次内容绘制(First Contentful Paint)
11
+ * lcp — 最大内容绘制(Largest Contentful Paint)
12
+ *
13
+ * 兼容策略:
14
+ * 优先使用 PerformanceObserver(现代浏览器)
15
+ * 降级使用 performance.timing(旧浏览器,已弃用但兼容性好)
16
+ *
17
+ * 移植自 smart-tracker 的 _initPerformanceMonitor,并增加了现代 API 支持
18
+ */
19
+ export class PerformancePlugin {
20
+ constructor() {
21
+ this.name = 'performance'
22
+ this.tracker = null
23
+ this._reported = false
24
+ }
25
+
26
+ install(tracker) {
27
+ this.tracker = tracker
28
+ var adapter = tracker.getAdapter()
29
+
30
+ // 仅 Web 和 APP(WebView) 环境支持
31
+ if (adapter.getPlatformName() === 'weapp') return
32
+ if (typeof window === 'undefined' || typeof performance === 'undefined') return
33
+
34
+ this._collectMetrics()
35
+ }
36
+
37
+ _collectMetrics() {
38
+ var self = this
39
+
40
+ // 等待页面加载完成后采集
41
+ if (document.readyState === 'complete') {
42
+ setTimeout(function () {
43
+ self._report()
44
+ }, 0)
45
+ } else {
46
+ window.addEventListener('load', function () {
47
+ // 延迟一点确保 timing 数据完整
48
+ setTimeout(function () {
49
+ self._report()
50
+ }, 100)
51
+ })
52
+ }
53
+
54
+ // 使用 PerformanceObserver 采集 FCP 和 LCP
55
+ this._observePaintMetrics()
56
+ }
57
+
58
+ _report() {
59
+ if (this._reported) return
60
+ this._reported = true
61
+
62
+ var data = {}
63
+
64
+ // 优先用 Navigation Timing Level 2
65
+ if (performance.getEntriesByType) {
66
+ var navEntries = performance.getEntriesByType('navigation')
67
+ if (navEntries && navEntries.length > 0) {
68
+ var nav = navEntries[0]
69
+ data.dns = Math.round(nav.domainLookupEnd - nav.domainLookupStart)
70
+ data.tcp = Math.round(nav.connectEnd - nav.connectStart)
71
+ data.ttfb = Math.round(nav.responseStart - nav.requestStart)
72
+ data.domReady = Math.round(nav.domContentLoadedEventEnd - nav.startTime)
73
+ data.load = Math.round(nav.loadEventEnd - nav.startTime)
74
+ data.redirect = Math.round(nav.redirectEnd - nav.redirectStart)
75
+ data.domParse = Math.round(nav.domInteractive - nav.responseEnd)
76
+ }
77
+ }
78
+
79
+ // 降级到 performance.timing(旧浏览器)
80
+ if (!data.dns && performance.timing) {
81
+ var timing = performance.timing
82
+ data.dns = timing.domainLookupEnd - timing.domainLookupStart
83
+ data.tcp = timing.connectEnd - timing.connectStart
84
+ data.ttfb = timing.responseStart - timing.requestStart
85
+ data.domReady = timing.domContentLoadedEventEnd - timing.navigationStart
86
+ data.load = timing.loadEventEnd - timing.navigationStart
87
+ data.redirect = timing.redirectEnd - timing.redirectStart
88
+ data.domParse = timing.domInteractive - timing.responseEnd
89
+ }
90
+
91
+ // FCP(如果 PerformanceObserver 已采集到)
92
+ if (this._fcp) data.fcp = this._fcp
93
+ if (this._lcp) data.lcp = this._lcp
94
+
95
+ // 过滤无效值(0 或负数)
96
+ for (var key in data) {
97
+ if (data[key] <= 0) delete data[key]
98
+ }
99
+
100
+ if (Object.keys(data).length > 0) {
101
+ this.tracker.track('performance', data)
102
+ }
103
+ }
104
+
105
+ _observePaintMetrics() {
106
+ if (typeof PerformanceObserver === 'undefined') return
107
+
108
+ var self = this
109
+
110
+ // FCP
111
+ try {
112
+ var fcpObserver = new PerformanceObserver(function (list) {
113
+ var entries = list.getEntries()
114
+ for (var i = 0; i < entries.length; i++) {
115
+ if (entries[i].name === 'first-contentful-paint') {
116
+ self._fcp = Math.round(entries[i].startTime)
117
+ fcpObserver.disconnect()
118
+ break
119
+ }
120
+ }
121
+ })
122
+ fcpObserver.observe({ type: 'paint', buffered: true })
123
+ } catch (e) {}
124
+
125
+ // LCP
126
+ try {
127
+ var lcpObserver = new PerformanceObserver(function (list) {
128
+ var entries = list.getEntries()
129
+ if (entries.length > 0) {
130
+ self._lcp = Math.round(entries[entries.length - 1].startTime)
131
+ }
132
+ })
133
+ lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true })
134
+
135
+ // LCP 在用户交互后不再更新,页面可见性变化时停止观察
136
+ if (typeof document !== 'undefined') {
137
+ document.addEventListener(
138
+ 'visibilitychange',
139
+ function () {
140
+ if (document.visibilityState === 'hidden') {
141
+ lcpObserver.disconnect()
142
+ // 如果此时已有 LCP 数据但还没上报过主性能数据
143
+ if (self._lcp && !self._reported) {
144
+ self._report()
145
+ }
146
+ }
147
+ },
148
+ { once: true }
149
+ )
150
+ }
151
+ } catch (e) {}
152
+ }
153
+
154
+ destroy() {
155
+ this.tracker = null
156
+ }
157
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * 页面滚动深度插件
3
+ *
4
+ * scrollDepth 字段上报实际滚动距离(像素值)
5
+ * 每滚动一屏距离上报一次 scroll_depth 事件
6
+ */
7
+ export class ScrollDepthPlugin {
8
+ constructor() {
9
+ this.name = 'scroll-depth'
10
+ this.tracker = null
11
+ this._maxScrollTop = 0
12
+ this._lastReportedDistance = 0
13
+ this._config = null
14
+ this._cachedContainers = null
15
+ this._rafId = null
16
+ this._debounceTimer = null
17
+ this._touchEndHandler = null
18
+ this._touchMoveHandler = null
19
+ this._wheelHandler = null
20
+ this._unloadHandler = null
21
+ this._hideHandler = null
22
+ this._isScrolling = false
23
+ this._screenHeight = 0
24
+ this._trackingPage = null
25
+ }
26
+
27
+ install(tracker) {
28
+ this.tracker = tracker
29
+ var adapter = tracker.getAdapter()
30
+
31
+ if (!adapter.supportsDOM()) return
32
+ if (typeof window === 'undefined') return
33
+
34
+ this._screenHeight = window.innerHeight || 800
35
+
36
+ var trackerConfig = tracker.getConfig()
37
+ this._config = {
38
+ throttle: 2000,
39
+ ...(trackerConfig.scrollDepth || {}),
40
+ }
41
+
42
+ this._trackingPage = tracker._currentPage || (adapter.getCurrentPagePath ? adapter.getCurrentPagePath() : null)
43
+
44
+ this._initTracking()
45
+ this._setupFinalOnHide()
46
+
47
+ this._unloadHandler = function () {
48
+ if (tracker._destroyed) return
49
+ }
50
+ window.addEventListener('beforeunload', this._unloadHandler)
51
+
52
+ var self = this
53
+ adapter.onRouteChange(function () {
54
+ if (self._maxScrollTop > 0) {
55
+ self._reportDepth(self._maxScrollTop, true, self._trackingPage)
56
+ }
57
+ self._maxScrollTop = 0
58
+ self._lastReportedDistance = 0
59
+ self._cachedContainers = null
60
+ if (self.tracker) {
61
+ self.tracker._currentScrollDepth = null
62
+ }
63
+ self._trackingPage = self.tracker ? self.tracker._currentPage : null
64
+ })
65
+ }
66
+
67
+ getMaxDepth() {
68
+ return this._maxScrollTop
69
+ }
70
+
71
+ _setupFinalOnHide() {
72
+ if (typeof document === 'undefined') return
73
+ var self = this
74
+ this._hideHandler = function () {
75
+ if (document.visibilityState === 'hidden') {
76
+ if (self._maxScrollTop > self._lastReportedDistance) {
77
+ self._reportDepth(self._maxScrollTop, true)
78
+ }
79
+ }
80
+ }
81
+ document.addEventListener('visibilitychange', this._hideHandler)
82
+ }
83
+
84
+ _initTracking() {
85
+ var self = this
86
+
87
+ this._touchEndHandler = function () {
88
+ self._scheduleCheck()
89
+ }
90
+
91
+ this._touchMoveHandler = function () {
92
+ // 立即更新 _currentScrollDepth(不触发上报),确保其他事件能实时拿到值
93
+ self._quickUpdate()
94
+ if (self._isScrolling) return
95
+ self._isScrolling = true
96
+ clearTimeout(self._debounceTimer)
97
+ self._debounceTimer = setTimeout(function () {
98
+ self._isScrolling = false
99
+ self._doCheck()
100
+ }, self._config.throttle)
101
+ }
102
+
103
+ this._wheelHandler = function () {
104
+ self._quickUpdate()
105
+ self._scheduleCheck()
106
+ }
107
+
108
+ document.addEventListener('touchend', this._touchEndHandler, { capture: true, passive: true })
109
+ document.addEventListener('touchmove', this._touchMoveHandler, { capture: true, passive: true })
110
+ document.addEventListener('wheel', this._wheelHandler, { capture: true, passive: true })
111
+ }
112
+
113
+ _scheduleCheck() {
114
+ var self = this
115
+ clearTimeout(this._debounceTimer)
116
+ this._debounceTimer = setTimeout(function () {
117
+ self._isScrolling = false
118
+ if (self._rafId) return
119
+ self._rafId = requestAnimationFrame(function () {
120
+ self._rafId = null
121
+ self._doCheck()
122
+ })
123
+ }, 300)
124
+ }
125
+
126
+ _quickUpdate() {
127
+ var containers = this._getContainers()
128
+ var maxSt = 0
129
+ for (var i = 0; i < containers.length; i++) {
130
+ var c = containers[i]
131
+ var st = 0
132
+ if (c.type === 'window') {
133
+ st = window.pageYOffset || document.documentElement.scrollTop || 0
134
+ } else {
135
+ st = c.el.scrollTop || 0
136
+ }
137
+ if (st > maxSt) maxSt = st
138
+ }
139
+ if (maxSt > this._maxScrollTop) {
140
+ this._maxScrollTop = maxSt
141
+ if (this.tracker) {
142
+ this.tracker._currentScrollDepth = maxSt
143
+ }
144
+ }
145
+ }
146
+
147
+ _doCheck() {
148
+ var containers = this._getContainers()
149
+ var maxSt = 0
150
+
151
+ for (var i = 0; i < containers.length; i++) {
152
+ var c = containers[i]
153
+ var st = 0
154
+ if (c.type === 'window') {
155
+ st = window.pageYOffset || document.documentElement.scrollTop || 0
156
+ } else {
157
+ st = c.el.scrollTop || 0
158
+ }
159
+ if (st > maxSt) maxSt = st
160
+ }
161
+
162
+ if (maxSt <= 0) return
163
+
164
+ if (maxSt > this._maxScrollTop) {
165
+ this._maxScrollTop = maxSt
166
+ if (this.tracker) {
167
+ this.tracker._currentScrollDepth = maxSt
168
+ }
169
+ // 每滚动一屏上报一次
170
+ var nextMilestone = this._lastReportedDistance + this._screenHeight
171
+ if (maxSt >= nextMilestone) {
172
+ this._reportDepth(maxSt, false)
173
+ }
174
+ }
175
+ }
176
+
177
+ _getContainers() {
178
+ if (this._cachedContainers) return this._cachedContainers
179
+
180
+ var containers = [{ type: 'window' }]
181
+
182
+ var scrollViews = document.querySelectorAll('uni-scroll-view')
183
+ for (var i = 0; i < scrollViews.length; i++) {
184
+ var found = this._findScrollEl(scrollViews[i])
185
+ if (found) {
186
+ containers.push({ type: 'element', el: found })
187
+ }
188
+ }
189
+
190
+ var overflowEls = document.querySelectorAll('.uni-scroll-view')
191
+ for (var i = 0; i < overflowEls.length; i++) {
192
+ var el = overflowEls[i]
193
+ if (el.scrollHeight > el.clientHeight && el.clientHeight >= 200) {
194
+ containers.push({ type: 'element', el: el })
195
+ }
196
+ }
197
+
198
+ if (containers.length > 1) {
199
+ this._cachedContainers = containers
200
+ }
201
+
202
+ return containers
203
+ }
204
+
205
+ _findScrollEl(el) {
206
+ if (this._isScrollContainer(el)) return el
207
+ var children = el.children
208
+ for (var i = 0; i < children.length; i++) {
209
+ if (this._isScrollContainer(children[i])) return children[i]
210
+ var grandchildren = children[i].children
211
+ for (var j = 0; j < grandchildren.length; j++) {
212
+ if (this._isScrollContainer(grandchildren[j])) return grandchildren[j]
213
+ var great = grandchildren[j].children
214
+ for (var k = 0; k < great.length; k++) {
215
+ if (this._isScrollContainer(great[k])) return great[k]
216
+ }
217
+ }
218
+ }
219
+ return null
220
+ }
221
+
222
+ _isScrollContainer(el) {
223
+ return el && el.clientHeight >= 200 && el.scrollHeight > el.clientHeight + 10
224
+ }
225
+
226
+ _reportDepth(distance, isFinal, pageRoute) {
227
+ this._lastReportedDistance = distance
228
+
229
+ var data = {
230
+ scrollDepth: distance,
231
+ extraData: {
232
+ isFinal: isFinal,
233
+ },
234
+ }
235
+ if (pageRoute) {
236
+ data.pageRoute = pageRoute
237
+ }
238
+ this.tracker.track('scroll_depth', data)
239
+ }
240
+
241
+ destroy() {
242
+ if (typeof document !== 'undefined') {
243
+ if (this._touchEndHandler) document.removeEventListener('touchend', this._touchEndHandler, { capture: true })
244
+ if (this._touchMoveHandler) document.removeEventListener('touchmove', this._touchMoveHandler, { capture: true })
245
+ if (this._wheelHandler) document.removeEventListener('wheel', this._wheelHandler, { capture: true })
246
+ if (this._hideHandler) document.removeEventListener('visibilitychange', this._hideHandler)
247
+ }
248
+ if (typeof window !== 'undefined' && this._unloadHandler) {
249
+ window.removeEventListener('beforeunload', this._unloadHandler)
250
+ }
251
+ if (this._rafId) cancelAnimationFrame(this._rafId)
252
+ if (this._debounceTimer) clearTimeout(this._debounceTimer)
253
+ this._cachedContainers = null
254
+ this.tracker = null
255
+ }
256
+ }