12.4 概念模块功能完善

This commit is contained in:
尚政杰
2025-12-04 17:41:33 +08:00
parent 4e64455b9b
commit 44842120da
5090 changed files with 9843 additions and 146120 deletions

View File

@@ -0,0 +1,72 @@
/**
* 存储Key常量
*/
export const STORAGE_KEYS = {
// 用户标识
DISTINCT_ID: 'ph_distinct_id',
ANONYMOUS_ID: 'ph_anonymous_id',
// 会话
SESSION_ID: 'ph_session_id',
SESSION_START_TIME: 'ph_session_start_time',
LAST_ACTIVE_TIME: 'ph_last_active_time',
// 离线事件缓存
OFFLINE_EVENTS: 'ph_offline_events',
// 用户属性缓存
USER_PROPERTIES: 'ph_user_properties',
// 设备ID
DEVICE_ID: 'ph_device_id',
}
/**
* PostHog保留事件名
*/
export const RESERVED_EVENTS = {
IDENTIFY: '$identify',
PAGE_VIEW: '$pageview',
PAGE_LEAVE: '$pageleave',
AUTOCAPTURE: '$autocapture',
}
/**
* 默认配置
*/
export const DEFAULT_CONFIG = {
// 队列配置
queue: {
maxSize: 20,
flushInterval: 30000,
},
// 请求配置
request: {
timeout: 10000,
maxRetries: 3,
retryDelay: 1000,
},
// 会话配置
session: {
timeout: 30 * 60 * 1000, // 30分钟
},
// 离线缓存配置
offline: {
maxEvents: 100,
},
}
/**
* HTTP状态码
*/
export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
SERVER_ERROR: 500,
}

289
src/utils/posthog/core.js Normal file
View File

@@ -0,0 +1,289 @@
import http from './http'
import storage from './storage'
import device from './device'
import identity from './identity'
import session from './session'
import queue from './queue'
import { RESERVED_EVENTS } from './constants'
/**
* PostHog Analytics 核心类
*/
class PostHogCore {
constructor() {
this.initialized = false
this.config = null
this.debug = false
this.superProperties = {} // 全局公共属性
}
/**
* 初始化SDK
*/
init(config) {
if (this.initialized) {
this._log('Already initialized')
return this
}
this.config = config
this.debug = config.debug || false
// 初始化各模块
http.init(config)
device.init()
identity.init()
session.init(config)
queue.init(config)
this.initialized = true
this._log('Initialized with config:', config)
return this
}
/**
* 开始会话(小程序启动/恢复时调用)
*/
startSession() {
const result = session.start()
if (result.isNewSession) {
this.track('session_start', {
session_id: result.sessionId,
})
}
return result
}
/**
* 结束会话(小程序退出/挂起时调用)
*/
endSession() {
const result = session.end()
if (result) {
this.track('session_end', {
session_id: result.sessionId,
session_duration: result.duration,
})
// 强制刷新队列
queue.forceFlush()
}
return result
}
/**
* 设置用户标识(登录时调用)
*/
identify(userId, userProperties = {}) {
this._checkInit()
const identifyData = identity.identify(userId, userProperties)
// 发送$identify事件
const event = {
event: RESERVED_EVENTS.IDENTIFY,
distinct_id: identifyData.distinctId,
properties: {
...this._getCommonProperties(),
...identifyData.properties,
},
timestamp: new Date().toISOString(),
}
queue.add(event)
this._log('User identified:', userId)
return this
}
/**
* 重置用户标识(登出时调用)
*/
reset() {
this._checkInit()
identity.reset()
session.reset()
this.superProperties = {}
this._log('User reset')
return this
}
/**
* 追踪事件
*/
track(eventName, properties = {}) {
this._checkInit()
const event = {
event: eventName,
distinct_id: identity.getDistinctId(),
properties: {
...this._getCommonProperties(),
...this.superProperties,
...properties,
},
timestamp: new Date().toISOString(),
}
queue.add(event)
this._log('Event tracked:', eventName, properties)
return this
}
/**
* 追踪页面浏览
*/
trackPageView(pagePath, pageTitle = '', properties = {}) {
return this.track(RESERVED_EVENTS.PAGE_VIEW, {
$current_url: pagePath,
$title: pageTitle,
page_path: pagePath,
page_title: pageTitle,
...properties,
})
}
/**
* 追踪页面离开
*/
trackPageLeave(pagePath, duration = 0, properties = {}) {
return this.track(RESERVED_EVENTS.PAGE_LEAVE, {
$current_url: pagePath,
page_path: pagePath,
duration: duration,
...properties,
})
}
/**
* 设置全局公共属性
*/
setSuperProperties(properties) {
this.superProperties = {
...this.superProperties,
...properties,
}
return this
}
/**
* 清除全局公共属性
*/
clearSuperProperties() {
this.superProperties = {}
return this
}
/**
* 设置用户属性
*/
setUserProperties(properties) {
this._checkInit()
identity.setUserProperties(properties)
// 发送更新用户属性的事件
this.track(RESERVED_EVENTS.IDENTIFY, {
$set: properties,
})
return this
}
/**
* 设置仅首次生效的用户属性
*/
setUserPropertiesOnce(properties) {
this._checkInit()
this.track(RESERVED_EVENTS.IDENTIFY, {
$set_once: properties,
})
return this
}
/**
* 获取公共属性
*/
_getCommonProperties() {
const deviceProps = device.getProperties()
const sessionInfo = session.getInfo()
return {
...deviceProps,
session_id: sessionInfo.sessionId,
$timestamp: new Date().toISOString(),
}
}
/**
* 更新活跃时间
*/
touch() {
session.touch()
return this
}
/**
* 强制刷新事件队列
*/
flush() {
queue.flush()
return this
}
/**
* 获取当前用户ID
*/
getDistinctId() {
return identity.getDistinctId()
}
/**
* 获取会话ID
*/
getSessionId() {
return session.getSessionId()
}
/**
* 检查是否已初始化
*/
_checkInit() {
if (!this.initialized) {
console.warn('[PostHog] SDK not initialized. Call init() first.')
}
}
/**
* 调试日志
*/
_log(...args) {
if (this.debug) {
console.log('[PostHog Core]', ...args)
}
}
/**
* 销毁SDK
*/
destroy() {
queue.destroy()
this.initialized = false
this._log('Destroyed')
}
}
// 导出单例
export default new PostHogCore()

205
src/utils/posthog/device.js Normal file
View File

@@ -0,0 +1,205 @@
import storage from './storage'
import { STORAGE_KEYS } from './constants'
/**
* 设备信息采集模块
*/
class Device {
constructor() {
this.info = null
this.deviceId = null
}
/**
* 初始化设备信息
*/
init() {
this._collectDeviceInfo()
this._ensureDeviceId()
}
/**
* 采集设备信息
*/
_collectDeviceInfo() {
try {
const systemInfo = uni.getSystemInfoSync()
this.info = {
// 操作系统
os: systemInfo.osName || systemInfo.platform || 'unknown',
osVersion: systemInfo.osVersion || 'unknown',
// 设备信息
brand: systemInfo.brand || 'unknown',
model: systemInfo.model || 'unknown',
deviceType: this._getDeviceType(systemInfo),
// 屏幕信息
screenWidth: systemInfo.screenWidth || 0,
screenHeight: systemInfo.screenHeight || 0,
windowWidth: systemInfo.windowWidth || 0,
windowHeight: systemInfo.windowHeight || 0,
pixelRatio: systemInfo.pixelRatio || 1,
// 小程序信息
appVersion: systemInfo.appVersion || 'unknown',
appLanguage: systemInfo.appLanguage || systemInfo.language || 'zh-CN',
SDKVersion: systemInfo.SDKVersion || 'unknown',
// 平台信息
platform: systemInfo.uniPlatform || 'mp-weixin',
hostName: systemInfo.hostName || 'unknown',
// 网络类型(初始值,后续会更新)
networkType: 'unknown',
}
// 异步获取网络类型
this._updateNetworkType()
} catch (error) {
console.warn('[PostHog Device] Collect info error:', error)
this.info = this._getDefaultInfo()
}
}
/**
* 获取设备类型
*/
_getDeviceType(systemInfo) {
const model = (systemInfo.model || '').toLowerCase()
if (model.includes('ipad') || model.includes('tablet')) {
return 'tablet'
}
if (model.includes('iphone') || model.includes('android')) {
return 'mobile'
}
return 'unknown'
}
/**
* 获取默认设备信息
*/
_getDefaultInfo() {
return {
os: 'unknown',
osVersion: 'unknown',
brand: 'unknown',
model: 'unknown',
deviceType: 'unknown',
screenWidth: 0,
screenHeight: 0,
windowWidth: 0,
windowHeight: 0,
pixelRatio: 1,
appVersion: 'unknown',
appLanguage: 'zh-CN',
SDKVersion: 'unknown',
platform: 'mp-weixin',
hostName: 'unknown',
networkType: 'unknown',
}
}
/**
* 更新网络类型
*/
_updateNetworkType() {
uni.getNetworkType({
success: (res) => {
if (this.info) {
this.info.networkType = res.networkType || 'unknown'
}
},
})
}
/**
* 确保设备ID存在
*/
_ensureDeviceId() {
let deviceId = storage.get(STORAGE_KEYS.DEVICE_ID)
if (!deviceId) {
deviceId = this._generateDeviceId()
storage.set(STORAGE_KEYS.DEVICE_ID, deviceId)
}
this.deviceId = deviceId
}
/**
* 生成设备ID
*/
_generateDeviceId() {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 10)
return `device_${timestamp}_${random}`
}
/**
* 获取设备ID
*/
getDeviceId() {
return this.deviceId
}
/**
* 获取设备信息
*/
getInfo() {
return this.info || this._getDefaultInfo()
}
/**
* 获取PostHog格式的设备属性
*/
getProperties() {
const info = this.getInfo()
return {
$os: info.os,
$os_version: info.osVersion,
$device_type: info.deviceType,
$device_id: this.deviceId,
$screen_width: info.screenWidth,
$screen_height: info.screenHeight,
$viewport_width: info.windowWidth,
$viewport_height: info.windowHeight,
$lib: 'uniapp-posthog',
$lib_version: '1.0.0',
// 扩展属性
mp_platform: info.platform,
mp_sdk_version: info.SDKVersion,
device_brand: info.brand,
device_model: info.model,
network_type: info.networkType,
app_language: info.appLanguage,
}
}
/**
* 监听网络状态变化
*/
onNetworkChange(callback) {
uni.onNetworkStatusChange((res) => {
if (this.info) {
this.info.networkType = res.networkType || 'unknown'
}
if (typeof callback === 'function') {
callback(res)
}
})
}
/**
* 检查是否在线
*/
isOnline() {
const networkType = this.info?.networkType || 'unknown'
return networkType !== 'none' && networkType !== 'unknown'
}
}
// 导出单例
export default new Device()

View File

@@ -0,0 +1,229 @@
import analytics from './index'
import { ERROR_EVENTS } from '../../constants/events'
/**
* 错误追踪器
*/
class ErrorTracker {
constructor() {
this.initialized = false
}
/**
* 初始化错误监控
*/
init() {
if (this.initialized) return
this.initialized = true
// 监听小程序全局错误
this._setupGlobalErrorHandler()
console.log('[ErrorTracker] Initialized')
}
/**
* 设置全局错误处理
*/
_setupGlobalErrorHandler() {
// 监听小程序错误
uni.onError((error) => {
this.trackJSError({
message: error,
source: 'uni.onError',
})
})
// 监听未处理的Promise错误
uni.onUnhandledRejection?.((res) => {
this.trackJSError({
message: res.reason?.message || String(res.reason),
stack: res.reason?.stack,
source: 'unhandledRejection',
})
})
}
/**
* 追踪API错误
* @param {object} error - 错误信息
*/
trackApiError(error) {
const {
path,
method = 'GET',
statusCode,
message,
duration,
params,
} = error
analytics.track(ERROR_EVENTS.API_ERROR, {
api_path: path,
api_method: method,
error_code: statusCode,
error_message: message,
request_duration: duration,
// 脱敏后的参数
request_params: this._sanitizeParams(params),
})
}
/**
* 追踪JS错误
* @param {object} error - 错误信息
*/
trackJSError(error) {
const {
message,
stack,
file,
line,
column,
source = 'unknown',
} = error
analytics.track(ERROR_EVENTS.JS_ERROR, {
error_message: this._truncate(message, 500),
error_stack: this._truncate(stack, 1000),
error_file: file,
error_line: line,
error_column: column,
error_source: source,
page_path: this._getCurrentPagePath(),
})
}
/**
* 追踪页面错误
* @param {object} error - 错误信息
*/
trackPageError(error) {
const {
type,
message,
pagePath,
} = error
analytics.track(ERROR_EVENTS.PAGE_ERROR, {
error_type: type,
error_message: message,
page_path: pagePath || this._getCurrentPagePath(),
})
}
/**
* 追踪网络错误
* @param {object} error - 错误信息
*/
trackNetworkError(error) {
const {
type = 'unknown',
message,
url,
} = error
analytics.track(ERROR_EVENTS.NETWORK_ERROR, {
error_type: type,
error_message: message,
request_url: this._sanitizeUrl(url),
network_type: this._getNetworkType(),
})
}
/**
* 追踪资源加载失败
* @param {object} error - 错误信息
*/
trackResourceError(error) {
const {
resourceType,
resourceUrl,
message,
} = error
analytics.track(ERROR_EVENTS.RESOURCE_LOAD_FAILED, {
resource_type: resourceType,
resource_url: this._sanitizeUrl(resourceUrl),
error_message: message,
page_path: this._getCurrentPagePath(),
})
}
/**
* 获取当前页面路径
*/
_getCurrentPagePath() {
try {
const pages = getCurrentPages()
if (pages.length > 0) {
return '/' + pages[pages.length - 1].route
}
} catch (e) {
// ignore
}
return 'unknown'
}
/**
* 获取网络类型
*/
_getNetworkType() {
try {
const res = uni.getNetworkType()
return res?.networkType || 'unknown'
} catch (e) {
return 'unknown'
}
}
/**
* 参数脱敏
*/
_sanitizeParams(params) {
if (!params) return {}
const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth']
const sanitized = { ...params }
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some((k) => key.toLowerCase().includes(k))) {
sanitized[key] = '***'
}
}
return sanitized
}
/**
* URL脱敏
*/
_sanitizeUrl(url) {
if (!url) return ''
// 移除查询参数中的敏感信息
try {
const urlObj = new URL(url)
const sensitiveParams = ['token', 'key', 'secret', 'password']
sensitiveParams.forEach((param) => {
if (urlObj.searchParams.has(param)) {
urlObj.searchParams.set(param, '***')
}
})
return urlObj.toString()
} catch (e) {
return url
}
}
/**
* 截断字符串
*/
_truncate(str, maxLength) {
if (!str) return ''
if (str.length <= maxLength) return str
return str.substring(0, maxLength) + '...'
}
}
// 导出单例
export default new ErrorTracker()

188
src/utils/posthog/http.js Normal file
View File

@@ -0,0 +1,188 @@
import { DEFAULT_CONFIG, HTTP_STATUS } from './constants'
/**
* PostHog HTTP请求封装
* 专门用于PostHog事件上报与业务请求隔离
*/
class PostHogHttp {
constructor() {
this.baseUrl = ''
this.apiKey = ''
this.timeout = DEFAULT_CONFIG.request.timeout
this.maxRetries = DEFAULT_CONFIG.request.maxRetries
this.retryDelay = DEFAULT_CONFIG.request.retryDelay
this.debug = false
}
/**
* 初始化配置
*/
init(config) {
this.baseUrl = config.apiHost || ''
this.apiKey = config.apiKey || ''
this.timeout = config.request?.timeout || this.timeout
this.maxRetries = config.request?.maxRetries || this.maxRetries
this.retryDelay = config.request?.retryDelay || this.retryDelay
this.debug = config.debug || false
}
/**
* 发送POST请求
*/
async post(path, data, options = {}) {
const url = `${this.baseUrl}${path}`
const retries = options.retries ?? this.maxRetries
return this._requestWithRetry(url, data, retries)
}
/**
* 带重试的请求
*/
async _requestWithRetry(url, data, retriesLeft) {
try {
return await this._doRequest(url, data)
} catch (error) {
// 如果还有重试次数且错误可重试
if (retriesLeft > 0 && this._isRetryableError(error)) {
this._log(`Request failed, retrying... (${retriesLeft} left)`)
// 等待一段时间后重试
await this._delay(this.retryDelay)
return this._requestWithRetry(url, data, retriesLeft - 1)
}
throw error
}
}
/**
* 执行实际请求
*/
_doRequest(url, data) {
return new Promise((resolve, reject) => {
this._log('Sending request:', url, data)
uni.request({
url,
method: 'POST',
header: {
'Content-Type': 'application/json',
},
data: {
api_key: this.apiKey,
...data,
},
timeout: this.timeout,
success: (res) => {
this._log('Response:', res.statusCode, res.data)
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({
success: true,
statusCode: res.statusCode,
data: res.data,
})
} else {
reject({
success: false,
statusCode: res.statusCode,
message: `HTTP ${res.statusCode}`,
data: res.data,
})
}
},
fail: (error) => {
this._log('Request failed:', error)
reject({
success: false,
statusCode: 0,
message: error.errMsg || 'Network error',
error,
})
},
})
})
}
/**
* 判断是否为可重试的错误
*/
_isRetryableError(error) {
// 网络错误可重试
if (error.statusCode === 0) return true
// 服务器错误可重试
if (error.statusCode >= 500) return true
// 请求超时可重试
if (error.message?.includes('timeout')) return true
return false
}
/**
* 延迟函数
*/
_delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* 调试日志
*/
_log(...args) {
if (this.debug) {
console.log('[PostHog HTTP]', ...args)
}
}
/**
* 发送单个事件
*/
async capture(event, distinctId, properties = {}, timestamp = null) {
const payload = {
event,
distinct_id: distinctId,
properties,
timestamp: timestamp || new Date().toISOString(),
}
return this.post('/capture/', payload)
}
/**
* 批量发送事件
*/
async batch(events) {
if (!events || events.length === 0) {
return { success: true, message: 'No events to send' }
}
const payload = {
batch: events,
}
return this.post('/batch/', payload)
}
/**
* 发送用户标识
*/
async identify(distinctId, userProperties = {}) {
const payload = {
event: '$identify',
distinct_id: distinctId,
properties: {
$set: userProperties.$set || {},
$set_once: userProperties.$set_once || {},
},
}
return this.post('/capture/', payload)
}
}
// 导出单例
export default new PostHogHttp()

View File

@@ -0,0 +1,151 @@
import storage from './storage'
import { STORAGE_KEYS } from './constants'
/**
* 用户标识管理模块
*/
class Identity {
constructor() {
this.distinctId = null
this.anonymousId = null
this.userId = null // 真实用户ID如OpenID
this.userProperties = {}
}
/**
* 初始化
*/
init() {
this._loadFromStorage()
}
/**
* 从本地存储加载标识
*/
_loadFromStorage() {
// 加载匿名ID
this.anonymousId = storage.get(STORAGE_KEYS.ANONYMOUS_ID)
if (!this.anonymousId) {
this.anonymousId = this._generateAnonymousId()
storage.set(STORAGE_KEYS.ANONYMOUS_ID, this.anonymousId)
}
// 加载distinct_id可能是匿名ID或用户ID
this.distinctId = storage.get(STORAGE_KEYS.DISTINCT_ID) || this.anonymousId
// 加载用户属性缓存
this.userProperties = storage.getJSON(STORAGE_KEYS.USER_PROPERTIES, {})
}
/**
* 生成匿名ID
*/
_generateAnonymousId() {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 10)
return `anon_${timestamp}_${random}`
}
/**
* 获取当前distinct_id
*/
getDistinctId() {
return this.distinctId || this.anonymousId
}
/**
* 获取匿名ID
*/
getAnonymousId() {
return this.anonymousId
}
/**
* 设置用户标识(登录时调用)
* @param {string} userId - 用户ID如OpenID
* @param {object} properties - 用户属性
* @returns {object} - 返回用于$identify事件的数据
*/
identify(userId, properties = {}) {
const previousDistinctId = this.distinctId
this.userId = userId
this.distinctId = userId
storage.set(STORAGE_KEYS.DISTINCT_ID, userId)
// 更新用户属性
this.setUserProperties(properties)
// 返回identify事件所需数据
return {
distinctId: userId,
previousDistinctId: previousDistinctId !== userId ? previousDistinctId : null,
properties: {
$set: properties,
$anon_distinct_id: this.anonymousId,
},
}
}
/**
* 设置用户属性
*/
setUserProperties(properties) {
this.userProperties = {
...this.userProperties,
...properties,
}
storage.setJSON(STORAGE_KEYS.USER_PROPERTIES, this.userProperties)
}
/**
* 获取用户属性
*/
getUserProperties() {
return this.userProperties
}
/**
* 重置用户标识(登出时调用)
*/
reset() {
this.userId = null
this.userProperties = {}
// 生成新的匿名ID
this.anonymousId = this._generateAnonymousId()
this.distinctId = this.anonymousId
// 更新存储
storage.set(STORAGE_KEYS.ANONYMOUS_ID, this.anonymousId)
storage.set(STORAGE_KEYS.DISTINCT_ID, this.distinctId)
storage.remove(STORAGE_KEYS.USER_PROPERTIES)
}
/**
* 检查用户是否已登录
*/
isIdentified() {
return this.userId !== null
}
/**
* 获取用户IDOpenID等
*/
getUserId() {
return this.userId
}
/**
* 关联匿名ID与用户ID用于$identify事件
*/
getAliasData() {
return {
distinct_id: this.distinctId,
$anon_distinct_id: this.anonymousId,
}
}
}
// 导出单例
export default new Identity()

162
src/utils/posthog/index.js Normal file
View File

@@ -0,0 +1,162 @@
import core from './core'
import config from '../../config/posthog.config'
/**
* PostHog Analytics 单例
*
* 使用方式:
* import analytics from '@/utils/posthog'
*
* // 在App.vue的onLaunch中初始化
* analytics.init()
*
* // 追踪事件
* analytics.track('button_click', { button_id: 'submit' })
*
* // 页面浏览
* analytics.trackPageView('/pages/index/index', '首页')
*
* // 用户登录
* analytics.identify('user_openid', { nickname: '张三' })
*
* // 用户登出
* analytics.reset()
*/
class Analytics {
constructor() {
this._core = core
this._initialized = false
}
/**
* 初始化(使用默认配置)
*/
init(customConfig = {}) {
if (this._initialized) return this
const finalConfig = {
...config,
...customConfig,
}
this._core.init(finalConfig)
this._initialized = true
return this
}
/**
* 开始会话
*/
startSession() {
return this._core.startSession()
}
/**
* 结束会话
*/
endSession() {
return this._core.endSession()
}
/**
* 用户登录标识
*/
identify(userId, userProperties = {}) {
return this._core.identify(userId, userProperties)
}
/**
* 用户登出重置
*/
reset() {
return this._core.reset()
}
/**
* 追踪事件
*/
track(eventName, properties = {}) {
return this._core.track(eventName, properties)
}
/**
* 追踪页面浏览
*/
trackPageView(pagePath, pageTitle = '', properties = {}) {
return this._core.trackPageView(pagePath, pageTitle, properties)
}
/**
* 追踪页面离开
*/
trackPageLeave(pagePath, duration = 0, properties = {}) {
return this._core.trackPageLeave(pagePath, duration, properties)
}
/**
* 设置全局公共属性
*/
setSuperProperties(properties) {
return this._core.setSuperProperties(properties)
}
/**
* 清除全局公共属性
*/
clearSuperProperties() {
return this._core.clearSuperProperties()
}
/**
* 设置用户属性
*/
setUserProperties(properties) {
return this._core.setUserProperties(properties)
}
/**
* 设置仅首次生效的用户属性
*/
setUserPropertiesOnce(properties) {
return this._core.setUserPropertiesOnce(properties)
}
/**
* 更新活跃时间
*/
touch() {
return this._core.touch()
}
/**
* 强制刷新事件队列
*/
flush() {
return this._core.flush()
}
/**
* 获取当前用户ID
*/
getDistinctId() {
return this._core.getDistinctId()
}
/**
* 获取会话ID
*/
getSessionId() {
return this._core.getSessionId()
}
/**
* 销毁SDK
*/
destroy() {
return this._core.destroy()
}
}
// 导出单例
export default new Analytics()

View File

@@ -0,0 +1,162 @@
import analytics from './index'
import { PAGE_EVENTS } from '../../constants/events'
/**
* 页面追踪器
* 自动追踪页面浏览、停留时长、滚动行为
*/
class PageTracker {
constructor() {
// 当前页面信息
this.currentPage = null
// 页面进入时间
this.enterTime = null
// 上一个页面路径
this.referrerPage = null
// 滚动追踪状态
this.scrollTracked = false
// 最大滚动位置
this.maxScrollTop = 0
// 页面高度
this.pageHeight = 0
}
/**
* 页面进入
* @param {string} pagePath - 页面路径
* @param {string} pageTitle - 页面标题
* @param {object} pageQuery - 页面参数
*/
onPageEnter(pagePath, pageTitle = '', pageQuery = {}) {
// 保存当前页面信息
this.currentPage = {
path: pagePath,
title: pageTitle,
query: pageQuery,
}
// 记录进入时间
this.enterTime = Date.now()
// 重置滚动追踪
this.scrollTracked = false
this.maxScrollTop = 0
// 发送页面浏览事件
analytics.track(PAGE_EVENTS.PAGE_VIEW, {
page_path: pagePath,
page_title: pageTitle,
page_query: pageQuery,
referrer_page: this.referrerPage || '',
$current_url: pagePath,
$title: pageTitle,
})
// 更新来源页面
this.referrerPage = pagePath
}
/**
* 页面离开
*/
onPageLeave() {
if (!this.currentPage || !this.enterTime) return
// 计算停留时长(秒)
const duration = Math.floor((Date.now() - this.enterTime) / 1000)
// 计算滚动百分比
const scrollPercentage = this.pageHeight > 0
? Math.min(100, Math.round((this.maxScrollTop / this.pageHeight) * 100))
: 0
// 发送页面离开事件
analytics.track(PAGE_EVENTS.PAGE_LEAVE, {
page_path: this.currentPage.path,
page_title: this.currentPage.title,
duration: duration,
max_scroll_top: this.maxScrollTop,
scroll_percentage: scrollPercentage,
$current_url: this.currentPage.path,
})
// 重置状态
this.currentPage = null
this.enterTime = null
}
/**
* 页面滚动
* @param {object} scrollInfo - 滚动信息
*/
onPageScroll(scrollInfo) {
const { scrollTop, scrollHeight } = scrollInfo
// 更新最大滚动位置
if (scrollTop > this.maxScrollTop) {
this.maxScrollTop = scrollTop
}
// 更新页面高度
if (scrollHeight) {
this.pageHeight = scrollHeight
}
// 首次滚动追踪
if (!this.scrollTracked && scrollTop > 100) {
this.scrollTracked = true
analytics.track(PAGE_EVENTS.PAGE_SCROLLED, {
page_path: this.currentPage?.path || '',
scroll_top: scrollTop,
scroll_direction: 'down',
first_scroll: true,
})
}
}
/**
* 下拉刷新
*/
onPullDownRefresh() {
analytics.track(PAGE_EVENTS.PAGE_REFRESH, {
page_path: this.currentPage?.path || '',
})
}
/**
* 触底加载更多
* @param {number} pageNum - 当前页码
*/
onReachBottom(pageNum = 1) {
analytics.track(PAGE_EVENTS.PAGE_LOAD_MORE, {
page_path: this.currentPage?.path || '',
page_number: pageNum,
})
}
/**
* 获取当前页面信息
*/
getCurrentPage() {
return this.currentPage
}
/**
* 获取停留时长(秒)
*/
getDuration() {
if (!this.enterTime) return 0
return Math.floor((Date.now() - this.enterTime) / 1000)
}
/**
* 设置页面高度(用于计算滚动百分比)
*/
setPageHeight(height) {
this.pageHeight = height
}
}
// 导出单例
export default new PageTracker()

View File

@@ -0,0 +1,74 @@
import analytics from './index'
import { PERFORMANCE_EVENTS } from '../../constants/events'
/**
* 性能追踪器
*/
class PerformanceTracker {
constructor() {
// 页面加载开始时间
this.pageLoadStartTimes = new Map()
}
/**
* 页面开始加载
* @param {string} pagePath - 页面路径
*/
startPageLoad(pagePath) {
this.pageLoadStartTimes.set(pagePath, Date.now())
}
/**
* 页面加载完成
* @param {string} pagePath - 页面路径
*/
endPageLoad(pagePath) {
const startTime = this.pageLoadStartTimes.get(pagePath)
if (!startTime) return
const loadTime = Date.now() - startTime
this.pageLoadStartTimes.delete(pagePath)
analytics.track(PERFORMANCE_EVENTS.PAGE_LOAD_PERFORMANCE, {
page_path: pagePath,
load_time: loadTime,
load_time_range: this._getLoadTimeRange(loadTime),
})
}
/**
* 追踪API响应时间
* @param {object} params - 参数
*/
trackApiPerformance(params) {
const {
path,
method,
duration,
success,
} = params
analytics.track(PERFORMANCE_EVENTS.API_PERFORMANCE, {
api_path: path,
api_method: method,
response_time: duration,
response_time_range: this._getLoadTimeRange(duration),
is_success: success,
})
}
/**
* 获取加载时间范围
*/
_getLoadTimeRange(ms) {
if (ms < 500) return '0-500ms'
if (ms < 1000) return '500ms-1s'
if (ms < 2000) return '1-2s'
if (ms < 3000) return '2-3s'
if (ms < 5000) return '3-5s'
return '5s+'
}
}
// 导出单例
export default new PerformanceTracker()

218
src/utils/posthog/queue.js Normal file
View File

@@ -0,0 +1,218 @@
import storage from './storage'
import http from './http'
import device from './device'
import { STORAGE_KEYS, DEFAULT_CONFIG } from './constants'
/**
* 事件队列管理
* 支持批量上报、离线缓存、失败重试
*/
class EventQueue {
constructor() {
this.queue = []
this.maxSize = DEFAULT_CONFIG.queue.maxSize
this.flushInterval = DEFAULT_CONFIG.queue.flushInterval
this.maxOfflineEvents = DEFAULT_CONFIG.offline.maxEvents
this.flushTimer = null
this.isFlushing = false
this.debug = false
this.immediateEvents = []
}
/**
* 初始化配置
*/
init(config = {}) {
this.maxSize = config.queue?.maxSize || this.maxSize
this.flushInterval = config.queue?.flushInterval || this.flushInterval
this.maxOfflineEvents = config.offline?.maxEvents || this.maxOfflineEvents
this.debug = config.debug || false
this.immediateEvents = config.immediateEvents || []
// 加载离线缓存的事件
this._loadOfflineEvents()
// 启动定时上报
this._startFlushTimer()
// 监听网络变化,联网时尝试上报
this._watchNetworkChange()
}
/**
* 加载离线缓存的事件
*/
_loadOfflineEvents() {
const offlineEvents = storage.getJSON(STORAGE_KEYS.OFFLINE_EVENTS, [])
if (offlineEvents.length > 0) {
this._log(`Loaded ${offlineEvents.length} offline events`)
this.queue = [...offlineEvents, ...this.queue]
// 清除存储
storage.remove(STORAGE_KEYS.OFFLINE_EVENTS)
}
}
/**
* 保存事件到离线缓存
*/
_saveOfflineEvents(events) {
const existingEvents = storage.getJSON(STORAGE_KEYS.OFFLINE_EVENTS, [])
const allEvents = [...existingEvents, ...events]
// 限制离线缓存数量
const trimmedEvents = allEvents.slice(-this.maxOfflineEvents)
storage.setJSON(STORAGE_KEYS.OFFLINE_EVENTS, trimmedEvents)
this._log(`Saved ${events.length} events to offline cache`)
}
/**
* 启动定时上报
*/
_startFlushTimer() {
if (this.flushTimer) {
clearInterval(this.flushTimer)
}
this.flushTimer = setInterval(() => {
this.flush()
}, this.flushInterval)
}
/**
* 停止定时上报
*/
_stopFlushTimer() {
if (this.flushTimer) {
clearInterval(this.flushTimer)
this.flushTimer = null
}
}
/**
* 监听网络变化
*/
_watchNetworkChange() {
device.onNetworkChange((res) => {
if (res.isConnected) {
this._log('Network connected, attempting to flush')
this.flush()
}
})
}
/**
* 添加事件到队列
*/
add(event) {
// 检查是否为需要立即上报的事件
if (this.immediateEvents.includes(event.event)) {
this._log(`Immediate event: ${event.event}`)
this._sendImmediately(event)
return
}
this.queue.push(event)
this._log(`Event added to queue: ${event.event} (queue size: ${this.queue.length})`)
// 检查是否达到上报阈值
if (this.queue.length >= this.maxSize) {
this.flush()
}
}
/**
* 立即发送单个事件
*/
async _sendImmediately(event) {
if (!device.isOnline()) {
this._saveOfflineEvents([event])
return
}
try {
await http.capture(event.event, event.distinct_id, event.properties, event.timestamp)
this._log(`Immediate event sent: ${event.event}`)
} catch (error) {
this._log(`Immediate event failed: ${event.event}`, error)
this._saveOfflineEvents([event])
}
}
/**
* 刷新队列,批量上报
*/
async flush() {
// 防止并发刷新
if (this.isFlushing || this.queue.length === 0) {
return
}
// 检查网络状态
if (!device.isOnline()) {
this._log('Offline, saving events to cache')
this._saveOfflineEvents([...this.queue])
this.queue = []
return
}
this.isFlushing = true
const eventsToSend = [...this.queue]
this.queue = []
this._log(`Flushing ${eventsToSend.length} events`)
try {
await http.batch(eventsToSend)
this._log(`Successfully sent ${eventsToSend.length} events`)
} catch (error) {
this._log('Batch send failed, saving to offline cache', error)
this._saveOfflineEvents(eventsToSend)
} finally {
this.isFlushing = false
}
}
/**
* 强制刷新(应用退出时调用)
*/
async forceFlush() {
this._stopFlushTimer()
await this.flush()
}
/**
* 获取队列长度
*/
getQueueSize() {
return this.queue.length
}
/**
* 清空队列
*/
clear() {
this.queue = []
storage.remove(STORAGE_KEYS.OFFLINE_EVENTS)
}
/**
* 调试日志
*/
_log(...args) {
if (this.debug) {
console.log('[PostHog Queue]', ...args)
}
}
/**
* 销毁队列
*/
destroy() {
this._stopFlushTimer()
this.forceFlush()
}
}
// 导出单例
export default new EventQueue()

View File

@@ -0,0 +1,253 @@
import analytics from './index'
import { CONTENT_EVENTS } from '../../constants/events'
/**
* 阅读时长追踪器
* 精确追踪用户阅读行为,包括:
* - 有效阅读时长(排除切后台、锁屏等无效时间)
* - 阅读进度
* - 阅读完成判断
*/
class ReadingTracker {
constructor() {
// 当前追踪的内容
this.content = null
// 开始阅读时间
this.startTime = null
// 累计阅读时长(毫秒)
this.totalDuration = 0
// 最后活跃时间
this.lastActiveTime = null
// 是否正在阅读
this.isReading = false
// 阅读进度(百分比)
this.progress = 0
// 内容总高度
this.contentHeight = 0
// 已滚动的最大位置
this.maxScrollPosition = 0
// 心跳定时器
this.heartbeatTimer = null
// 心跳间隔(毫秒)
this.heartbeatInterval = 5000
}
/**
* 开始阅读追踪
* @param {object} content - 内容信息
*/
start(content) {
this.content = {
id: content.id,
title: content.title,
category: content.category || '',
author: content.author || '',
publishTime: content.publishTime || '',
wordCount: content.wordCount || 0,
}
this.startTime = Date.now()
this.lastActiveTime = Date.now()
this.totalDuration = 0
this.isReading = true
this.progress = 0
this.maxScrollPosition = 0
// 启动心跳
this._startHeartbeat()
console.log('[ReadingTracker] Started tracking:', this.content.title)
}
/**
* 暂停阅读(切后台、跳转页面等)
*/
pause() {
if (!this.isReading) return
// 累计有效阅读时长
if (this.lastActiveTime) {
this.totalDuration += Date.now() - this.lastActiveTime
}
this.isReading = false
this._stopHeartbeat()
console.log('[ReadingTracker] Paused, duration:', this.totalDuration)
}
/**
* 恢复阅读
*/
resume() {
if (this.isReading) return
if (!this.content) return
this.lastActiveTime = Date.now()
this.isReading = true
this._startHeartbeat()
console.log('[ReadingTracker] Resumed')
}
/**
* 结束阅读追踪并上报
* @param {string} exitType - 退出类型normal/back/close
*/
end(exitType = 'normal') {
if (!this.content) return null
// 如果还在阅读状态,先暂停
if (this.isReading) {
this.pause()
}
// 计算最终阅读时长(秒)
const durationSeconds = Math.floor(this.totalDuration / 1000)
// 判断是否完成阅读
const isCompleted = this._isReadingCompleted(durationSeconds)
// 上报阅读事件
const eventData = {
news_id: this.content.id,
news_title: this.content.title,
news_category: this.content.category,
news_author: this.content.author,
reading_duration: durationSeconds,
reading_duration_range: this._getDurationRange(durationSeconds),
reading_progress: this.progress,
is_completed: isCompleted,
exit_type: exitType,
word_count: this.content.wordCount,
}
analytics.track(CONTENT_EVENTS.NEWS_VIEWED, eventData)
// 如果完成阅读,额外上报完成事件
if (isCompleted) {
analytics.track(CONTENT_EVENTS.NEWS_READ_COMPLETED, eventData)
}
console.log('[ReadingTracker] Ended:', eventData)
// 重置状态
const result = { ...eventData }
this._reset()
return result
}
/**
* 更新阅读进度
* @param {number} scrollTop - 滚动位置
* @param {number} viewportHeight - 可视区域高度
*/
updateProgress(scrollTop, viewportHeight) {
if (!this.contentHeight || !this.isReading) return
// 更新最大滚动位置
const currentPosition = scrollTop + viewportHeight
if (currentPosition > this.maxScrollPosition) {
this.maxScrollPosition = currentPosition
}
// 计算阅读进度
this.progress = Math.min(100, Math.round((this.maxScrollPosition / this.contentHeight) * 100))
// 更新活跃时间
this.lastActiveTime = Date.now()
}
/**
* 设置内容高度
*/
setContentHeight(height) {
this.contentHeight = height
}
/**
* 获取当前阅读时长(秒)
*/
getCurrentDuration() {
let duration = this.totalDuration
if (this.isReading && this.lastActiveTime) {
duration += Date.now() - this.lastActiveTime
}
return Math.floor(duration / 1000)
}
/**
* 启动心跳
*/
_startHeartbeat() {
this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.isReading) {
// 定期更新活跃时间,确保时长统计准确
this.lastActiveTime = Date.now()
}
}, this.heartbeatInterval)
}
/**
* 停止心跳
*/
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* 判断是否完成阅读
* 规则:阅读进度>=80% 或 阅读时长>=预估阅读时间的60%
*/
_isReadingCompleted(durationSeconds) {
// 进度判断
if (this.progress >= 80) return true
// 时长判断假设阅读速度为300字/分钟)
if (this.content.wordCount) {
const estimatedReadTime = (this.content.wordCount / 300) * 60 // 秒
if (durationSeconds >= estimatedReadTime * 0.6) return true
}
// 最低阅读时长判断超过60秒视为有效阅读
if (durationSeconds >= 60 && this.progress >= 50) return true
return false
}
/**
* 获取阅读时长范围
*/
_getDurationRange(seconds) {
if (seconds < 10) return '0-10s'
if (seconds < 30) return '10-30s'
if (seconds < 60) return '30-60s'
if (seconds < 180) return '1-3min'
if (seconds < 300) return '3-5min'
if (seconds < 600) return '5-10min'
return '10min+'
}
/**
* 重置状态
*/
_reset() {
this._stopHeartbeat()
this.content = null
this.startTime = null
this.totalDuration = 0
this.lastActiveTime = null
this.isReading = false
this.progress = 0
this.contentHeight = 0
this.maxScrollPosition = 0
}
}
// 导出单例
export default new ReadingTracker()

View File

@@ -0,0 +1,170 @@
import analytics from './index'
import { SEARCH_EVENTS } from '../../constants/events'
/**
* 搜索行为追踪器
*/
class SearchTracker {
constructor() {
// 当前搜索会话
this.currentSearch = null
// 搜索开始时间
this.searchStartTime = null
}
/**
* 搜索框聚焦
* @param {string} pagePath - 页面路径
*/
onSearchFocus(pagePath = '') {
analytics.track(SEARCH_EVENTS.SEARCH_FOCUS, {
page_path: pagePath,
})
}
/**
* 执行搜索
* @param {object} params - 搜索参数
*/
onSearch(params) {
const {
query,
type = 'keyword',
source = 'input', // input/suggestion/history/hot
} = params
this.currentSearch = {
query,
type,
source,
}
this.searchStartTime = Date.now()
console.log('[SearchTracker] Search started:', query)
}
/**
* 搜索结果返回
* @param {object} result - 搜索结果
*/
onSearchResult(result) {
if (!this.currentSearch) return
const searchTime = this.searchStartTime
? Date.now() - this.searchStartTime
: 0
const eventData = {
search_query: this.currentSearch.query,
search_type: this.currentSearch.type,
search_source: this.currentSearch.source,
result_count: result.total || result.list?.length || 0,
search_time: searchTime,
has_results: (result.total || result.list?.length || 0) > 0,
}
analytics.track(SEARCH_EVENTS.SEARCH_EXECUTED, eventData)
// 如果无结果,额外上报
if (!eventData.has_results) {
analytics.track(SEARCH_EVENTS.SEARCH_NO_RESULTS, {
search_query: this.currentSearch.query,
search_type: this.currentSearch.type,
})
}
console.log('[SearchTracker] Search completed:', eventData)
}
/**
* 点击搜索结果
* @param {object} item - 点击的结果项
* @param {number} position - 结果位置
*/
onResultClick(item, position) {
if (!this.currentSearch) return
analytics.track(SEARCH_EVENTS.SEARCH_RESULT_CLICKED, {
search_query: this.currentSearch.query,
result_id: item.id,
result_title: item.title,
result_position: position,
result_type: item.type || 'news',
})
}
/**
* 点击搜索建议
* @param {string} suggestion - 建议词
* @param {number} position - 位置
*/
onSuggestionClick(suggestion, position) {
analytics.track(SEARCH_EVENTS.SEARCH_SUGGESTION_CLICKED, {
suggestion_text: suggestion,
suggestion_position: position,
})
// 设置为新的搜索
this.onSearch({
query: suggestion,
type: 'keyword',
source: 'suggestion',
})
}
/**
* 点击热门搜索
* @param {string} keyword - 热门词
* @param {number} position - 位置
*/
onHotSearchClick(keyword, position) {
analytics.track(SEARCH_EVENTS.HOT_SEARCH_CLICKED, {
keyword: keyword,
position: position,
})
// 设置为新的搜索
this.onSearch({
query: keyword,
type: 'keyword',
source: 'hot',
})
}
/**
* 点击搜索历史
* @param {string} keyword - 历史词
* @param {number} position - 位置
*/
onHistoryClick(keyword, position) {
analytics.track(SEARCH_EVENTS.SEARCH_HISTORY_CLICKED, {
keyword: keyword,
position: position,
})
// 设置为新的搜索
this.onSearch({
query: keyword,
type: 'keyword',
source: 'history',
})
}
/**
* 清空搜索历史
*/
onHistoryClear() {
analytics.track(SEARCH_EVENTS.SEARCH_HISTORY_CLEARED, {})
}
/**
* 重置搜索会话
*/
reset() {
this.currentSearch = null
this.searchStartTime = null
}
}
// 导出单例
export default new SearchTracker()

View File

@@ -0,0 +1,182 @@
import storage from './storage'
import { STORAGE_KEYS, DEFAULT_CONFIG } from './constants'
/**
* 会话管理模块
*/
class Session {
constructor() {
this.sessionId = null
this.startTime = null
this.lastActiveTime = null
this.timeout = DEFAULT_CONFIG.session.timeout
this.isActive = false
}
/**
* 初始化配置
*/
init(config = {}) {
this.timeout = config.session?.timeout || this.timeout
this._loadFromStorage()
}
/**
* 从存储加载会话信息
*/
_loadFromStorage() {
this.sessionId = storage.get(STORAGE_KEYS.SESSION_ID)
this.startTime = storage.get(STORAGE_KEYS.SESSION_START_TIME)
this.lastActiveTime = storage.get(STORAGE_KEYS.LAST_ACTIVE_TIME)
}
/**
* 保存到存储
*/
_saveToStorage() {
storage.set(STORAGE_KEYS.SESSION_ID, this.sessionId)
storage.set(STORAGE_KEYS.SESSION_START_TIME, this.startTime)
storage.set(STORAGE_KEYS.LAST_ACTIVE_TIME, this.lastActiveTime)
}
/**
* 生成会话ID
*/
_generateSessionId() {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 8)
return `session_${timestamp}_${random}`
}
/**
* 开始新会话
*/
start() {
const now = Date.now()
// 检查是否需要新会话
if (this._shouldStartNewSession()) {
this.sessionId = this._generateSessionId()
this.startTime = now
this.isActive = true
this.lastActiveTime = now
this._saveToStorage()
return {
isNewSession: true,
sessionId: this.sessionId,
startTime: this.startTime,
}
}
// 恢复现有会话
this.isActive = true
this.lastActiveTime = now
storage.set(STORAGE_KEYS.LAST_ACTIVE_TIME, now)
return {
isNewSession: false,
sessionId: this.sessionId,
startTime: this.startTime,
}
}
/**
* 判断是否需要开始新会话
*/
_shouldStartNewSession() {
// 没有现有会话
if (!this.sessionId) return true
// 超过会话超时时间
if (this.lastActiveTime) {
const elapsed = Date.now() - this.lastActiveTime
if (elapsed > this.timeout) return true
}
return false
}
/**
* 结束会话
*/
end() {
if (!this.isActive) return null
const now = Date.now()
const duration = this.startTime ? now - this.startTime : 0
this.isActive = false
this.lastActiveTime = now
storage.set(STORAGE_KEYS.LAST_ACTIVE_TIME, now)
return {
sessionId: this.sessionId,
duration: Math.floor(duration / 1000), // 转换为秒
startTime: this.startTime,
endTime: now,
}
}
/**
* 更新活跃时间
*/
touch() {
if (!this.isActive) return
this.lastActiveTime = Date.now()
storage.set(STORAGE_KEYS.LAST_ACTIVE_TIME, this.lastActiveTime)
}
/**
* 获取会话ID
*/
getSessionId() {
return this.sessionId
}
/**
* 获取会话时长(秒)
*/
getDuration() {
if (!this.startTime) return 0
return Math.floor((Date.now() - this.startTime) / 1000)
}
/**
* 获取会话信息
*/
getInfo() {
return {
sessionId: this.sessionId,
startTime: this.startTime,
lastActiveTime: this.lastActiveTime,
duration: this.getDuration(),
isActive: this.isActive,
}
}
/**
* 检查会话是否活跃
*/
isSessionActive() {
return this.isActive
}
/**
* 重置会话
*/
reset() {
this.sessionId = null
this.startTime = null
this.lastActiveTime = null
this.isActive = false
storage.remove(STORAGE_KEYS.SESSION_ID)
storage.remove(STORAGE_KEYS.SESSION_START_TIME)
storage.remove(STORAGE_KEYS.LAST_ACTIVE_TIME)
}
}
// 导出单例
export default new Session()

View File

@@ -0,0 +1,135 @@
/**
* 本地存储封装
* 统一封装 uni.setStorage / uni.getStorage 等API
*/
class Storage {
constructor(prefix = 'ph_') {
this.prefix = prefix
}
/**
* 获取完整的存储Key
*/
_getKey(key) {
// 如果key已经包含前缀直接返回
if (key.startsWith(this.prefix)) {
return key
}
return `${this.prefix}${key}`
}
/**
* 同步获取数据
*/
get(key, defaultValue = null) {
try {
const data = uni.getStorageSync(this._getKey(key))
if (data === '' || data === undefined || data === null) {
return defaultValue
}
return data
} catch (error) {
console.warn('[PostHog Storage] Get error:', error)
return defaultValue
}
}
/**
* 同步设置数据
*/
set(key, value) {
try {
uni.setStorageSync(this._getKey(key), value)
return true
} catch (error) {
console.warn('[PostHog Storage] Set error:', error)
return false
}
}
/**
* 同步删除数据
*/
remove(key) {
try {
uni.removeStorageSync(this._getKey(key))
return true
} catch (error) {
console.warn('[PostHog Storage] Remove error:', error)
return false
}
}
/**
* 异步获取数据
*/
async getAsync(key, defaultValue = null) {
return new Promise((resolve) => {
uni.getStorage({
key: this._getKey(key),
success: (res) => {
resolve(res.data ?? defaultValue)
},
fail: () => {
resolve(defaultValue)
},
})
})
}
/**
* 异步设置数据
*/
async setAsync(key, value) {
return new Promise((resolve) => {
uni.setStorage({
key: this._getKey(key),
data: value,
success: () => resolve(true),
fail: () => resolve(false),
})
})
}
/**
* 清除所有PostHog相关存储
*/
clear() {
try {
const res = uni.getStorageInfoSync()
const keys = res.keys || []
keys.forEach((key) => {
if (key.startsWith(this.prefix)) {
uni.removeStorageSync(key)
}
})
return true
} catch (error) {
console.warn('[PostHog Storage] Clear error:', error)
return false
}
}
/**
* 获取JSON数据
*/
getJSON(key, defaultValue = null) {
const data = this.get(key)
if (data === null) return defaultValue
try {
return typeof data === 'string' ? JSON.parse(data) : data
} catch {
return defaultValue
}
}
/**
* 设置JSON数据
*/
setJSON(key, value) {
return this.set(key, JSON.stringify(value))
}
}
// 导出单例
export default new Storage('ph_')

View File

@@ -0,0 +1,141 @@
import analytics from './index'
import { USER_EVENTS, CONTENT_EVENTS } from '../../constants/events'
/**
* 用户行为追踪器
*/
class UserTracker {
/**
* 用户登录
* @param {object} params - 登录参数
*/
onLogin(params) {
const {
userId,
method = 'wechat',
success = true,
errorCode = '',
errorMessage = '',
userInfo = {},
} = params
// 上报登录事件
analytics.track(USER_EVENTS.USER_LOGIN, {
login_method: method,
login_success: success,
login_error_code: errorCode,
login_error_message: errorMessage,
})
// 如果登录成功,设置用户标识
if (success && userId) {
analytics.identify(userId, {
nickname: userInfo.nickname,
avatar_url: userInfo.avatarUrl,
gender: userInfo.gender,
city: userInfo.city,
province: userInfo.province,
country: userInfo.country,
})
// 设置仅首次生效的属性
analytics.setUserPropertiesOnce({
first_login_time: new Date().toISOString(),
register_source: method,
})
}
}
/**
* 用户登出
*/
onLogout() {
analytics.track(USER_EVENTS.USER_LOGOUT, {})
analytics.reset()
}
/**
* 微信授权
* @param {object} params - 授权参数
*/
onWechatAuth(params) {
const {
scope = 'userInfo',
success = true,
errorCode = '',
} = params
analytics.track(USER_EVENTS.WECHAT_AUTH, {
auth_scope: scope,
auth_success: success,
auth_error_code: errorCode,
})
}
/**
* 内容收藏
* @param {object} content - 内容信息
*/
onFavorite(content) {
analytics.track(CONTENT_EVENTS.CONTENT_FAVORITED, {
content_id: content.id,
content_type: content.type || 'news',
content_title: content.title,
content_category: content.category || '',
})
// 更新用户收藏计数
analytics.setUserProperties({
last_favorite_time: new Date().toISOString(),
})
}
/**
* 取消收藏
* @param {object} content - 内容信息
*/
onUnfavorite(content) {
analytics.track(CONTENT_EVENTS.CONTENT_UNFAVORITED, {
content_id: content.id,
content_type: content.type || 'news',
content_title: content.title,
})
}
/**
* 内容分享
* @param {object} params - 分享参数
*/
onShare(params) {
const {
content,
channel = 'wechat_friend',
success = true,
} = params
analytics.track(CONTENT_EVENTS.CONTENT_SHARED, {
content_id: content.id,
content_type: content.type || 'news',
content_title: content.title,
share_channel: channel,
share_success: success,
})
}
/**
* 内容点赞
* @param {object} content - 内容信息
* @param {boolean} isLike - 是否点赞false为取消
*/
onLike(content, isLike = true) {
analytics.track(CONTENT_EVENTS.CONTENT_LIKED, {
content_id: content.id,
content_type: content.type || 'news',
content_title: content.title,
is_like: isLike,
})
}
}
// 导出单例
export default new UserTracker()

View File

@@ -0,0 +1,114 @@
import { EVENTS } from '@/constants/events'
/**
* 事件属性验证器
* 确保事件属性符合预定义规范
*/
// 事件属性Schema定义
const eventSchemas = {
[EVENTS.NEWS_VIEWED]: {
required: ['news_id', 'news_title'],
optional: ['news_category', 'reading_duration', 'entry_source'],
types: {
news_id: 'string',
news_title: 'string',
news_category: 'string',
reading_duration: 'number',
entry_source: 'string',
},
},
[EVENTS.SEARCH_EXECUTED]: {
required: ['search_query'],
optional: ['search_type', 'result_count', 'search_time'],
types: {
search_query: 'string',
search_type: 'string',
result_count: 'number',
search_time: 'number',
},
},
[EVENTS.CONTENT_FAVORITED]: {
required: ['content_id', 'content_type'],
optional: ['content_title', 'content_category'],
types: {
content_id: 'string',
content_type: 'string',
content_title: 'string',
content_category: 'string',
},
},
[EVENTS.API_ERROR]: {
required: ['api_path', 'error_code', 'error_message'],
optional: ['api_method', 'request_duration'],
types: {
api_path: 'string',
api_method: 'string',
error_code: ['number', 'string'],
error_message: 'string',
request_duration: 'number',
},
},
}
/**
* 验证事件属性
* @param {string} eventName - 事件名称
* @param {object} properties - 事件属性
* @returns {object} - { valid: boolean, errors: string[] }
*/
export function validateEventProperties(eventName, properties) {
const schema = eventSchemas[eventName]
// 没有定义schema的事件跳过验证
if (!schema) {
return { valid: true, errors: [] }
}
const errors = []
// 检查必需属性
for (const field of schema.required || []) {
if (!(field in properties)) {
errors.push(`Missing required property: ${field}`)
}
}
// 检查属性类型
for (const [field, value] of Object.entries(properties)) {
const expectedType = schema.types?.[field]
if (expectedType) {
const actualType = typeof value
const validTypes = Array.isArray(expectedType) ? expectedType : [expectedType]
if (!validTypes.includes(actualType)) {
errors.push(`Invalid type for ${field}: expected ${validTypes.join(' or ')}, got ${actualType}`)
}
}
}
return {
valid: errors.length === 0,
errors,
}
}
/**
* 验证并警告(开发模式使用)
*/
export function validateAndWarn(eventName, properties, debug = false) {
if (!debug) return
const result = validateEventProperties(eventName, properties)
if (!result.valid) {
console.warn(`[PostHog Validator] Event "${eventName}" validation failed:`, result.errors)
}
}
export default {
validateEventProperties,
validateAndWarn,
}