@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,287 @@
1
+ import { mergeConfig } from './config.js'
2
+ import { SessionManager } from './session.js'
3
+ import { ReportQueue } from './queue.js'
4
+
5
+ export class Tracker {
6
+ constructor(config, adapter) {
7
+ this.config = mergeConfig(config)
8
+ this.adapter = adapter
9
+ this.session = new SessionManager(adapter, this.config)
10
+ this.queue = new ReportQueue(adapter, this.config, this.session)
11
+ this.plugins = []
12
+ this._destroyed = false
13
+ this._userProperties = null
14
+
15
+ this._paramResolvers = {}
16
+ this._referrer = null
17
+ this._currentPage = null
18
+ this._currentScrollDepth = null
19
+ this._sampled = this._decideSampling()
20
+ this._validEventTypes = {
21
+ page_view: true, page_stay: true, click: true, custom: true,
22
+ api_request: true, js_error: true, app_lifecycle: true, page_show: true,
23
+ exposure: true, scroll_depth: true, performance: true,
24
+ }
25
+ }
26
+
27
+ _decideSampling() {
28
+ var sampling = this.config.sampling
29
+ if (sampling === undefined || sampling === null || sampling >= 1) {
30
+ return true
31
+ }
32
+ if (sampling <= 0) return false
33
+ var sessionId = this.session.getSessionId()
34
+ var hash = 0
35
+ for (var i = 0; i < sessionId.length; i++) {
36
+ hash = ((hash << 5) - hash + sessionId.charCodeAt(i)) | 0
37
+ }
38
+ return Math.abs(hash % 100) / 100 < sampling
39
+ }
40
+
41
+ _getDeviceFields() {
42
+ var info = this.adapter.getDeviceInfo() || {}
43
+ return {
44
+ deviceBrand: info.brand || null,
45
+ deviceModel: info.model || null,
46
+ os: info.platform || info.os || null,
47
+ osVersion: info.system || null,
48
+ screenWidth: info.screenWidth || null,
49
+ screenHeight: info.screenHeight || null,
50
+ }
51
+ }
52
+
53
+ // ---- 插件管理 ----
54
+
55
+ use(plugin) {
56
+ if (this._destroyed) return this
57
+ if (plugin.name && this.plugins.some(p => p.name === plugin.name)) {
58
+ if (this.config.debug) {
59
+ console.warn('[TrackerSDK] plugin "' + plugin.name + '" already registered, skipping')
60
+ }
61
+ return this
62
+ }
63
+ this.plugins.push(plugin)
64
+ plugin.install(this)
65
+ return this
66
+ }
67
+
68
+ // ---- 自动采集上报 ----
69
+
70
+ track(eventType, data) {
71
+ if (this._destroyed) return
72
+ if (!this._sampled) return
73
+
74
+ if (!this._validEventTypes[eventType]) {
75
+ if (this.config.debug) {
76
+ console.warn('[TrackerSDK] invalid eventType "' + eventType + '", dropped')
77
+ }
78
+ return
79
+ }
80
+
81
+ // 先评估会话是否过期(空闲超 timeout 会在此滚动到新 session 并重置序列号),
82
+ // 必须在 touch() 之前,否则 touch 刷新 lastActivityTime 后过期判断永远为 false
83
+ this.session.getSessionId()
84
+
85
+ var seq = this.session.nextSequence()
86
+ this.session.touch()
87
+
88
+ var eventData = data || {}
89
+
90
+ // 只有导航事件(page_view)才更新页面链路状态(_currentPage/_referrer)
91
+ // page_stay/click 等事件传的 pageRoute 仅表示"事件发生在哪个页面",不是导航信号
92
+ var declaredPage = eventData.pageRoute || null
93
+
94
+ if (eventType === 'page_view' && declaredPage && declaredPage !== '/' && declaredPage !== this._currentPage) {
95
+ if (this._currentPage) {
96
+ this._referrer = this._currentPage
97
+ }
98
+ this._currentPage = declaredPage
99
+ }
100
+
101
+ if (!this._currentPage) {
102
+ var initRoute = this.adapter.getCurrentPagePath()
103
+ if (initRoute && initRoute !== '/') {
104
+ this._currentPage = initRoute
105
+ }
106
+ }
107
+
108
+ // 动态参数解析:click 插件传入 _paramName 和 _element
109
+ if (eventData._paramName && this._paramResolvers[eventData._paramName]) {
110
+ try {
111
+ var resolved = this._paramResolvers[eventData._paramName](eventData._element)
112
+ if (resolved && typeof resolved === 'object') {
113
+ if (resolved.extKey) eventData.extKey = resolved.extKey
114
+ if (resolved.extValue) eventData.extValue = resolved.extValue
115
+ if (resolved.extIntValue !== undefined) eventData.extIntValue = resolved.extIntValue
116
+ if (resolved.postParam) eventData.postParam = resolved.postParam
117
+ }
118
+ } catch (e) {
119
+ if (this.config.debug) {
120
+ console.warn('[TrackerSDK] paramResolver error:', eventData._paramName, e)
121
+ }
122
+ }
123
+ }
124
+ delete eventData._paramName
125
+ delete eventData._element
126
+
127
+ var event = {
128
+ eventId: this.adapter.generateId('e'),
129
+ eventType: eventType,
130
+ eventName: eventData.eventName || eventType,
131
+ timestamp: this.adapter.getTimestamp(),
132
+ sequence: seq,
133
+ pageRoute: 'pageRoute' in eventData ? (eventData.pageRoute || null) : (this._currentPage || this.adapter.getCurrentPagePath()),
134
+ pageTitle: 'pageTitle' in eventData ? (eventData.pageTitle || null) : (this.adapter.getPageTitle ? this.adapter.getPageTitle() : null),
135
+ elementId: eventData.elementId || null,
136
+ elementText: eventData.elementText || null,
137
+ duration: eventData.duration || null,
138
+ scrollDepth: eventData.scrollDepth || this._currentScrollDepth || null,
139
+ errorMessage: eventData.errorMessage || null,
140
+ errorStack: eventData.errorStack || null,
141
+ extraData: eventData.extraData ? JSON.stringify(eventData.extraData) : null,
142
+ extKey: eventData.extKey || null,
143
+ extValue: eventData.extValue || null,
144
+ extIntValue: eventData.extIntValue || null,
145
+ frontUrl: eventData.frontUrl || null,
146
+ postParam: eventData.postParam != null && eventData.postParam !== ''
147
+ ? (typeof eventData.postParam === 'string'
148
+ ? eventData.postParam
149
+ : JSON.stringify(eventData.postParam))
150
+ : '',
151
+ networkType: this.adapter.getNetworkType
152
+ ? this.adapter.getNetworkType()
153
+ : null,
154
+ referrer: eventData.referrer || this._referrer || null,
155
+ }
156
+
157
+ // 自引用 referrer(来源页 = 当前页)对路径还原无意义,清空。
158
+ // 常见于后台切回后 _referrer 未随导航更新,page_stay 继承到当前页自身。
159
+ if (event.referrer && event.referrer === event.pageRoute) {
160
+ event.referrer = null
161
+ }
162
+
163
+ var deviceFields = this._getDeviceFields()
164
+ Object.keys(deviceFields).forEach(function (key) {
165
+ event[key] = deviceFields[key]
166
+ })
167
+
168
+ this.queue.push(event)
169
+
170
+ if (this.config.debug) {
171
+ console.log('[TrackerSDK] track:', eventType, event)
172
+ }
173
+ }
174
+
175
+ // ---- 手动上报(走旧接口)----
176
+
177
+ trackLegacy(frontUrl, postParam, extraHeader) {
178
+ if (this._destroyed) return
179
+
180
+ this.session.touch()
181
+
182
+ var url = this.config.dadianUrl + '/api/point/task/activeReport'
183
+ var header = {
184
+ 'content-type': 'application/json',
185
+ token: this.adapter.getToken() || '',
186
+ }
187
+ if (extraHeader) {
188
+ Object.keys(extraHeader).forEach(function (k) {
189
+ header[k] = extraHeader[k]
190
+ })
191
+ }
192
+ var data = {
193
+ frontUrl: frontUrl,
194
+ postParam:
195
+ typeof postParam === 'string' ? postParam : JSON.stringify(postParam),
196
+ }
197
+
198
+ this.queue.pushLegacy(url, data, header)
199
+ }
200
+
201
+ // ---- 用户标识 ----
202
+
203
+ setUser(userId) {
204
+ this.session.setUser(userId)
205
+ }
206
+
207
+ setDeviceId(deviceId) {
208
+ this.session.setDeviceId(deviceId)
209
+ }
210
+
211
+ setUserProperties(properties) {
212
+ this._userProperties = properties
213
+ }
214
+
215
+ getUserProperties() {
216
+ return this._userProperties
217
+ }
218
+
219
+ // ---- 页面导航状态(referrer)----
220
+
221
+ updatePageRoute(newPage) {
222
+ if (newPage && newPage !== this._currentPage) {
223
+ if (this._currentPage) {
224
+ this._referrer = this._currentPage
225
+ }
226
+ this._currentPage = newPage
227
+ }
228
+ }
229
+
230
+ getReferrer() {
231
+ return this._referrer
232
+ }
233
+
234
+ // ---- 动态参数解析器 ----
235
+
236
+ registerParam(name, fn) {
237
+ if (typeof name === 'string' && typeof fn === 'function') {
238
+ this._paramResolvers[name] = fn
239
+ }
240
+ }
241
+
242
+ unregisterParam(name) {
243
+ delete this._paramResolvers[name]
244
+ }
245
+
246
+ // ---- 访问器 ----
247
+
248
+ getAdapter() {
249
+ return this.adapter
250
+ }
251
+
252
+ getSession() {
253
+ return this.session
254
+ }
255
+
256
+ getQueue() {
257
+ return this.queue
258
+ }
259
+
260
+ getConfig() {
261
+ return this.config
262
+ }
263
+
264
+ // ---- Hook ----
265
+
266
+ onReport(fn) {
267
+ this.queue.onReport(fn)
268
+ }
269
+
270
+ // ---- 清理 ----
271
+
272
+ destroy() {
273
+ if (this._destroyed) return
274
+ this._destroyed = true
275
+
276
+ this.plugins.forEach(function (plugin) {
277
+ if (typeof plugin.destroy === 'function') {
278
+ plugin.destroy()
279
+ }
280
+ })
281
+ this.plugins = []
282
+
283
+ this.queue.destroy()
284
+ this.session.destroy()
285
+ this.adapter.destroy()
286
+ }
287
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * SDK 定制化调度入口
3
+ *
4
+ * 目录结构:
5
+ * customize/
6
+ * index.js — 当前文件,统一调度入口
7
+ * modules/
8
+ * common.js — 技术栈通用方法(uni-app 所有项目共享)
9
+ * store-manager.js — 店长端项目定制(appId=1)
10
+ * (后续项目在此添加,如 customer-app.js、mini-program.js)
11
+ *
12
+ * 两种调用方式:
13
+ * 1. SDK 内部:config.projectRules.isTabbarClick(el)
14
+ * 2. 业务代码:customize.callFunction('store-manager', 'extractNavTarget', eventName)
15
+ */
16
+
17
+ // appId → 模块文件名映射
18
+ var APP_ID_MAP = {
19
+ '1': 'store-manager',
20
+ // '12': 'customer-app',
21
+ // '18': 'mini-program',
22
+ }
23
+
24
+ /**
25
+ * 动态加载 modules 下的模块
26
+ */
27
+ function loadModule(moduleName) {
28
+ try {
29
+ var m = require('./modules/' + moduleName)
30
+ return m.default || m
31
+ } catch (e) {
32
+ console.warn('[customize] module not found:', moduleName)
33
+ return null
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 调用指定模块的指定方法
39
+ * @param {string} moduleName - 模块名(common / store-manager / ...)
40
+ * @param {string} methodName - 方法名
41
+ * @param {...any} args - 参数(直接传给方法)
42
+ * @returns {any} 方法返回值
43
+ */
44
+ function callFunction(moduleName, methodName) {
45
+ var mod = loadModule(moduleName)
46
+ if (!mod) return undefined
47
+ if (!methodName || typeof mod[methodName] !== 'function') {
48
+ console.warn('[customize] method not found:', moduleName + '.' + methodName)
49
+ return undefined
50
+ }
51
+ var args = Array.prototype.slice.call(arguments, 2)
52
+ return mod[methodName].apply(null, args)
53
+ }
54
+
55
+ /**
56
+ * 根据 appId 获取合并后的项目规则(common + 项目模块)
57
+ * SDK 初始化时调用,返回值赋给 config.projectRules
58
+ *
59
+ * @param {string|number} appId
60
+ * @returns {object|null} 合并后的 rules 对象
61
+ */
62
+ function getProjectRules(appId) {
63
+ if (!appId) return null
64
+ var projectName = APP_ID_MAP[String(appId)]
65
+ if (!projectName) return null
66
+
67
+ var commonMod = loadModule('common') || {}
68
+ var projectMod = loadModule(projectName) || {}
69
+
70
+ // 项目模块优先,common 兜底
71
+ var rules = {}
72
+ var key
73
+ for (key in commonMod) {
74
+ if (commonMod.hasOwnProperty(key)) {
75
+ rules[key] = commonMod[key]
76
+ }
77
+ }
78
+ for (key in projectMod) {
79
+ if (projectMod.hasOwnProperty(key)) {
80
+ rules[key] = projectMod[key]
81
+ }
82
+ }
83
+
84
+ return rules
85
+ }
86
+
87
+ export { callFunction, getProjectRules }
88
+
89
+ export default {
90
+ callFunction: callFunction,
91
+ getProjectRules: getProjectRules,
92
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 技术栈通用定制方法(uni-app 全项目共享)
3
+ *
4
+ * 这里放的是所有 uni-app 项目都需要的通用检测/处理逻辑,
5
+ * 与具体业务项目无关。项目级别的特殊处理放各自的模块文件里。
6
+ */
7
+ export default {
8
+
9
+ /**
10
+ * 检测元素是否在原生 uni-tabbar 内
11
+ * uni-app 内置 tabbar 会渲染为 <uni-tabbar> 标签或带 uni-tabbar class 的元素
12
+ * 所有 uni-app 项目通用,不需要项目定制
13
+ */
14
+ isNativeTabbar: function (_this, el) {
15
+ var current = el
16
+ var depth = 0
17
+ while (current && current !== document.body && depth < 10) {
18
+ var tag = (current.tagName || '').toLowerCase()
19
+ if (tag === 'uni-tabbar' || tag === 'tabbar') return true
20
+ var cls = current.className || ''
21
+ if (typeof cls === 'string') {
22
+ if (cls.indexOf('uni-tabbar') > -1 || cls.indexOf('tabBar') > -1 || cls.indexOf('tab-bar') > -1) return true
23
+ }
24
+ current = current.parentElement || current.parentNode
25
+ depth++
26
+ }
27
+ return false
28
+ },
29
+
30
+ /**
31
+ * 获取当前页面路由(uni-app 通用)
32
+ * 通过 getCurrentPages() 获取,所有 uni-app 项目一致
33
+ */
34
+ getCurrentRoute: function () {
35
+ try {
36
+ var pages = getCurrentPages()
37
+ if (pages.length > 0) {
38
+ return pages[pages.length - 1].route || ''
39
+ }
40
+ } catch (e) {}
41
+ return ''
42
+ },
43
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 店长端 (store-manager) 项目定制方法
3
+ * appId: 1
4
+ *
5
+ * 只放和店长端项目业务相关的特殊处理,
6
+ * 技术栈通用的逻辑在 common.js 里。
7
+ */
8
+ export default {
9
+
10
+ name: 'store-manager',
11
+ appId: '1',
12
+
13
+ /**
14
+ * 判断点击是否在店长端自定义底部导航区域
15
+ * 店长端用 footer-h.vue 组件,class="footers" / "footer-item"
16
+ * 返回 true 时 click.js 跳过自动采集(由业务代码手动 $track 上报)
17
+ */
18
+ isTabbarClick: function (_this, el) {
19
+ var current = el
20
+ var depth = 0
21
+ while (current && current !== document.body && depth < 10) {
22
+ var cls = current.className || ''
23
+ if (typeof cls === 'string') {
24
+ if (cls.indexOf('footers') > -1 || cls.indexOf('footer-item') > -1) return true
25
+ }
26
+ current = current.parentElement || current.parentNode
27
+ depth++
28
+ }
29
+ return false
30
+ },
31
+
32
+ /**
33
+ * 从导航事件中提取目标页面路由
34
+ * 店长端 footer-h 组件的 $track 调用格式:this.$track('footer/click' + url)
35
+ * 其中 url 为 /pages/xxx/index
36
+ * 返回目标路由供 SDK 提前更新 referrer 链路
37
+ */
38
+ extractNavTarget: function (_this, eventName) {
39
+ if (!eventName) return null
40
+ var match = eventName.match(/footer\/click(\/pages\/[^\s?#]+)/)
41
+ if (match) return match[1]
42
+ return null
43
+ },
44
+
45
+ /**
46
+ * 不需要自动采集 click 的选择器/区域
47
+ * 这些区域已有手动埋点,自动采集会导致重复或错误 pageRoute
48
+ */
49
+ ignoreClickSelectors: ['.footers', '.footer-item'],
50
+
51
+ /**
52
+ * 旧接口上报用的 frontUrl(各项目已在后台注册的值)
53
+ */
54
+ pageStayFrontUrl: '/storeManager/page_stay',
55
+ pageShowFrontUrl: '/storeManager/page_show',
56
+ }