tracking.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import type { MouseEvent as ReactMouseEvent } from 'react';
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { reportUserEvent, type UserEventPayload } from '@/utils/api';
  4. import { getUserId } from '@/utils/storage';
  5. // 路由 path → page_id 映射,需与 .cursorrules 中的命名约定保持一致
  6. const PATH_TO_PAGE_ID: Record<string, string> = {
  7. '/welcome': 'welcome',
  8. '/begin': 'begin',
  9. '/login-register': 'login_register',
  10. '/loading': 'loading',
  11. '/naming': 'naming',
  12. '/lumi-naming': 'lumi_naming',
  13. '/chatbot': 'chatbot',
  14. '/tutorial-1': 'tutorial_1',
  15. '/tutorial-2': 'tutorial_2',
  16. '/tutorial-3': 'tutorial_3',
  17. '/tutorial-4': 'tutorial_4',
  18. '/tutorial-5': 'tutorial_5',
  19. '/tutorial-6': 'tutorial_6',
  20. '/tutorial-7': 'tutorial_7',
  21. '/tutorial-8': 'tutorial_8',
  22. '/tutorial-9': 'tutorial_9',
  23. '/tutorial-10': 'tutorial_10',
  24. '/level5-welcome': 'level5_welcome',
  25. '/level34-welcome': 'level34_welcome',
  26. '/first-login': 'first_login',
  27. '/verification-code': 'verification_code',
  28. '/verification-input': 'verification_input',
  29. '/login-verification-code': 'login_verification_code',
  30. '/login-verification-input': 'login_verification_input',
  31. '/first-login-reward': 'first_login_reward',
  32. '/share-success': 'share_success',
  33. '/email-login': 'email_login',
  34. '/phone-login': 'phone_login',
  35. '/profile': 'profile',
  36. '/error': 'error',
  37. };
  38. const getPageIdFromPath = (path: string): string | null => {
  39. if (!path) return null;
  40. const clean = path.split('?')[0];
  41. return PATH_TO_PAGE_ID[clean] ?? null;
  42. };
  43. const getPlatform = (): UserEventPayload['platform'] => 'web';
  44. const getTimezoneOffsetMinutes = (): number => -new Date().getTimezoneOffset();
  45. const getAppVersion = (): string | undefined => import.meta.env.VITE_APP_VERSION;
  46. const SESSION_ID_KEY = 'lyrria_session_id';
  47. function getOrCreateSessionId(): string {
  48. try {
  49. let sid = sessionStorage.getItem(SESSION_ID_KEY);
  50. if (!sid) {
  51. sid = uuidv4();
  52. sessionStorage.setItem(SESSION_ID_KEY, sid);
  53. }
  54. return sid;
  55. } catch {
  56. return uuidv4();
  57. }
  58. }
  59. export const trackPageEnter = (path: string): void => {
  60. const userId = getUserId();
  61. const pageId = getPageIdFromPath(path);
  62. if (!userId || !pageId) {
  63. if (import.meta.env.DEV) {
  64. console.log('[tracking] trackPageEnter 跳过:', { path, hasUserId: !!userId, pageId });
  65. }
  66. return;
  67. }
  68. const event: UserEventPayload = {
  69. event_id: uuidv4(),
  70. user_id: userId,
  71. event_name: `page.${pageId}__enter`,
  72. event_label: `进入页面:${pageId}`,
  73. event_time: new Date().toISOString(),
  74. session_id: getOrCreateSessionId(),
  75. platform: getPlatform(),
  76. app_version: getAppVersion(),
  77. timezone_offset_minutes: getTimezoneOffsetMinutes(),
  78. properties: {
  79. page: pageId,
  80. path,
  81. },
  82. };
  83. reportUserEvent(event);
  84. };
  85. export const trackButtonClick = (
  86. pageId: string,
  87. buttonId: string,
  88. options?: {
  89. label?: string;
  90. properties?: Record<string, unknown>;
  91. },
  92. ): void => {
  93. const userId = getUserId();
  94. if (!userId) {
  95. if (import.meta.env.DEV) {
  96. console.log('[tracking] trackButtonClick 跳过: 未登录', { pageId, buttonId });
  97. }
  98. return;
  99. }
  100. const eventName = `button.${pageId}.${buttonId}__click`;
  101. const event: UserEventPayload = {
  102. event_id: uuidv4(),
  103. user_id: userId,
  104. event_name: eventName,
  105. event_label: options?.label ?? `按钮点击:${pageId}.${buttonId}`,
  106. event_time: new Date().toISOString(),
  107. session_id: getOrCreateSessionId(),
  108. platform: getPlatform(),
  109. app_version: getAppVersion(),
  110. timezone_offset_minutes: getTimezoneOffsetMinutes(),
  111. properties: {
  112. page: pageId,
  113. button_id: buttonId,
  114. ...(options?.properties ?? {}),
  115. },
  116. };
  117. reportUserEvent(event);
  118. };
  119. /** 包装点击回调:先上报 button 埋点再执行原 onClick(用于原生 button/div 等) */
  120. export function wrapTrackedClick(
  121. pageId: string,
  122. buttonId: string,
  123. onClick?: (e?: ReactMouseEvent<HTMLElement>) => void,
  124. options?: { label?: string; properties?: Record<string, unknown> },
  125. ): (e?: ReactMouseEvent<HTMLElement>) => void {
  126. return (e?: ReactMouseEvent<HTMLElement>) => {
  127. trackButtonClick(pageId, buttonId, options);
  128. onClick?.(e);
  129. };
  130. }