feat(miniprogram): 埋点事件追踪服务
- 新增 analytics.ts:trackEvent/trackPageView/flushEvents - 事件队列本地缓存,批量上报到 /analytics/batch - 首页 page_view、预约创建、随访提交、健康数据录入四个关键埋点
This commit is contained in:
@@ -4,6 +4,7 @@ import Taro from '@tarojs/taro';
|
|||||||
import { listDoctors, createAppointment, calendarView } from '../../../services/appointment';
|
import { listDoctors, createAppointment, calendarView } from '../../../services/appointment';
|
||||||
import { useAuthStore } from '../../../stores/auth';
|
import { useAuthStore } from '../../../stores/auth';
|
||||||
import { TEMPLATE_IDS } from '@/services/wechat-templates';
|
import { TEMPLATE_IDS } from '@/services/wechat-templates';
|
||||||
|
import { trackEvent } from '@/services/analytics';
|
||||||
import StepIndicator from '../../../components/StepIndicator';
|
import StepIndicator from '../../../components/StepIndicator';
|
||||||
import WeekCalendar from '../../../components/WeekCalendar';
|
import WeekCalendar from '../../../components/WeekCalendar';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -112,6 +113,7 @@ export default function AppointmentCreate() {
|
|||||||
reason: reason.trim() || undefined,
|
reason: reason.trim() || undefined,
|
||||||
});
|
});
|
||||||
Taro.showToast({ title: '预约成功', icon: 'success' });
|
Taro.showToast({ title: '预约成功', icon: 'success' });
|
||||||
|
trackEvent('appointment_create', { doctor_id: selectedDoctor.id, date: appointmentDate });
|
||||||
// 订阅消息引导
|
// 订阅消息引导
|
||||||
const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER;
|
const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER;
|
||||||
if (tmplId) {
|
if (tmplId) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Taro, { useRouter } from '@tarojs/taro';
|
|||||||
import { getTaskDetail, submitRecord } from '../../../services/followup';
|
import { getTaskDetail, submitRecord } from '../../../services/followup';
|
||||||
import type { FollowUpTask } from '../../../services/followup';
|
import type { FollowUpTask } from '../../../services/followup';
|
||||||
import { TEMPLATE_IDS } from '@/services/wechat-templates';
|
import { TEMPLATE_IDS } from '@/services/wechat-templates';
|
||||||
|
import { trackEvent } from '@/services/analytics';
|
||||||
import Loading from '../../../components/Loading';
|
import Loading from '../../../components/Loading';
|
||||||
import ErrorState from '../../../components/ErrorState';
|
import ErrorState from '../../../components/ErrorState';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -42,6 +43,7 @@ export default function FollowUpDetail() {
|
|||||||
content: { text: content.trim() },
|
content: { text: content.trim() },
|
||||||
});
|
});
|
||||||
Taro.showToast({ title: '提交成功', icon: 'success' });
|
Taro.showToast({ title: '提交成功', icon: 'success' });
|
||||||
|
trackEvent('followup_submit', { task_id: id });
|
||||||
const tmplId = TEMPLATE_IDS.FOLLOWUP_REMINDER;
|
const tmplId = TEMPLATE_IDS.FOLLOWUP_REMINDER;
|
||||||
if (tmplId) {
|
if (tmplId) {
|
||||||
try { await Taro.requestSubscribeMessage({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ }
|
try { await Taro.requestSubscribeMessage({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from 'zod';
|
|||||||
import { inputVitalSign } from '../../../services/health';
|
import { inputVitalSign } from '../../../services/health';
|
||||||
import { useAuthStore } from '../../../stores/auth';
|
import { useAuthStore } from '../../../stores/auth';
|
||||||
import { useHealthStore } from '@/stores/health';
|
import { useHealthStore } from '@/stores/health';
|
||||||
|
import { trackEvent } from '@/services/analytics';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const INDICATORS = [
|
const INDICATORS = [
|
||||||
@@ -76,6 +77,7 @@ export default function HealthInput() {
|
|||||||
});
|
});
|
||||||
clearCache();
|
clearCache();
|
||||||
Taro.showToast({ title: '录入成功', icon: 'success' });
|
Taro.showToast({ title: '录入成功', icon: 'success' });
|
||||||
|
trackEvent('health_data_input', { type: indicatorType });
|
||||||
setTimeout(() => Taro.navigateBack(), 1000);
|
setTimeout(() => Taro.navigateBack(), 1000);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = e instanceof Error ? e.message : '录入失败';
|
const msg = e instanceof Error ? e.message : '录入失败';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useAuthStore } from '../../stores/auth';
|
|||||||
import { useHealthStore } from '../../stores/health';
|
import { useHealthStore } from '../../stores/health';
|
||||||
import EmptyState from '../../components/EmptyState';
|
import EmptyState from '../../components/EmptyState';
|
||||||
import Loading from '../../components/Loading';
|
import Loading from '../../components/Loading';
|
||||||
|
import { trackPageView } from '@/services/analytics';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
@@ -13,6 +14,7 @@ export default function Index() {
|
|||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
restoreAuth();
|
restoreAuth();
|
||||||
refreshToday();
|
refreshToday();
|
||||||
|
trackPageView('home');
|
||||||
});
|
});
|
||||||
|
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
|
|||||||
82
apps/miniprogram/src/services/analytics.ts
Normal file
82
apps/miniprogram/src/services/analytics.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
|
||||||
|
type EventName =
|
||||||
|
| 'page_view'
|
||||||
|
| 'login'
|
||||||
|
| 'bind_phone'
|
||||||
|
| 'health_data_input'
|
||||||
|
| 'health_trend_view'
|
||||||
|
| 'appointment_create'
|
||||||
|
| 'appointment_detail'
|
||||||
|
| 'followup_submit'
|
||||||
|
| 'report_view'
|
||||||
|
| 'article_view'
|
||||||
|
| 'article_share'
|
||||||
|
| 'medication_add'
|
||||||
|
| 'family_add'
|
||||||
|
| 'profile_edit';
|
||||||
|
|
||||||
|
interface AnalyticsEvent {
|
||||||
|
event: EventName | string;
|
||||||
|
properties?: Record<string, unknown>;
|
||||||
|
timestamp: number;
|
||||||
|
userId?: string;
|
||||||
|
patientId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUEUE_KEY = 'analytics_queue';
|
||||||
|
const MAX_QUEUE_SIZE = 50;
|
||||||
|
|
||||||
|
function getQueue(): AnalyticsEvent[] {
|
||||||
|
return Taro.getStorageSync(QUEUE_KEY) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQueue(queue: AnalyticsEvent[]): void {
|
||||||
|
Taro.setStorageSync(QUEUE_KEY, queue.slice(-MAX_QUEUE_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackEvent(event: EventName | string, properties?: Record<string, unknown>): void {
|
||||||
|
const userId = Taro.getStorageSync('user')?.id;
|
||||||
|
const patientId = Taro.getStorageSync('current_patient_id');
|
||||||
|
|
||||||
|
const evt: AnalyticsEvent = {
|
||||||
|
event,
|
||||||
|
properties,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
userId,
|
||||||
|
patientId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queue = getQueue();
|
||||||
|
queue.push(evt);
|
||||||
|
setQueue(queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackPageView(pageName: string, properties?: Record<string, unknown>): void {
|
||||||
|
trackEvent('page_view', { page: pageName, ...properties });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushEvents(): Promise<void> {
|
||||||
|
const queue = getQueue();
|
||||||
|
if (queue.length === 0) return;
|
||||||
|
|
||||||
|
const batch = queue.slice();
|
||||||
|
setQueue([]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1';
|
||||||
|
await Taro.request({
|
||||||
|
url: `${BASE_URL}/analytics/batch`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { events: batch },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// 发送失败,回填队列
|
||||||
|
const current = getQueue();
|
||||||
|
setQueue([...batch.slice(-MAX_QUEUE_SIZE + current.length), ...current]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQueueSize(): number {
|
||||||
|
return getQueue().length;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user