Add posthog

This commit is contained in:
CodingOnStar 2025-11-07 14:21:09 +08:00
parent 897458fbe1
commit 2b8be2c869
7 changed files with 202 additions and 15 deletions

View File

@ -3,6 +3,7 @@
import type { FC } from 'react'
import React, { useEffect } from 'react'
import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
export type IAmplitudeProps = {
apiKey?: string
@ -18,7 +19,12 @@ const AmplitudeProvider: FC<IAmplitudeProps> = ({
// return
// }
// Initialize Amplitude with proxy configuration to bypass CSP
// Create Session Replay plugin instance
const sessionReplay = sessionReplayPlugin({
sampleRate: 1,
})
// Initialize Amplitude with proxy configuration to bypass CSP and Session Replay
amplitude.init(apiKey, {
defaultTracking: {
sessions: true,
@ -31,10 +37,10 @@ const AmplitudeProvider: FC<IAmplitudeProps> = ({
// Use Next.js proxy to bypass CSP restrictions
serverUrl: '/api/amplitude/2/httpapi',
})
amplitude.add(sessionReplay)
// Log initialization success in development
if (process.env.NODE_ENV === 'development')
console.log('[Amplitude] Initialized successfully, API Key:', apiKey)
console.log('[Amplitude] Initialized successfully with Session Replay, API Key:', apiKey)
}, [apiKey])
// This is a client component that renders nothing

View File

@ -0,0 +1,57 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import posthog from 'posthog-js'
const PostHogProvider: FC = () => {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
posthog.init('phc_Nqsm7RqSX7ZUbH9C47ZRfiUsVBCiLZIrapbWjHAYTBV', {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // 为已识别用户创建 profile 并存储 UTM
autocapture: true, // 自动捕获用户交互
capture_exceptions: true, // 捕获异常
})
// 初始化后,确保捕获 UTM 参数
// PostHog 会自动将首次访问的 UTM 保存为 Initial UTM
if (typeof window !== 'undefined' && window.location.search) {
const urlParams = new URLSearchParams(window.location.search)
const utmParams: Record<string, string> = {}
// 提取所有 UTM 参数
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
utmKeys.forEach((key) => {
const value = urlParams.get(key)
if (value)
utmParams[key] = value
})
// 如果有 UTM 参数,注册为持久属性
if (Object.keys(utmParams).length > 0)
posthog.register(utmParams)
}
}, [])
// 追踪页面浏览
useEffect(() => {
if (pathname && posthog.__loaded) {
let url = window.origin + pathname
if (searchParams.toString())
url = `${url}?${searchParams.toString()}`
posthog.capture('$pageview', {
$current_url: url,
})
}
}, [pathname, searchParams])
// This is a client component that renders nothing
return null
}
export default React.memo(PostHogProvider)

View File

@ -3,6 +3,7 @@ import type { Viewport } from 'next'
import I18nServer from './components/i18n-server'
import BrowserInitializer from './components/browser-initializer'
import SentryInitializer from './components/sentry-initializer'
import PostHogProvider from './components/posthog-provider'
import { getLocaleOnServer } from '@/i18n-config/server'
import { TanstackQueryInitializer } from '@/context/query-client'
import { ThemeProvider } from 'next-themes'
@ -93,15 +94,18 @@ const LocaleLayout = async ({
enableColorScheme={false}
>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryInitializer>
</SentryInitializer>
<>
<PostHogProvider />
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryInitializer>
</SentryInitializer>
</>
</BrowserInitializer>
</ThemeProvider>
<RoutePrefixHandle />

View File

@ -131,6 +131,8 @@ const nextConfig = {
'@heroicons/react'
],
},
// This is required to support PostHog trailing slash API requests
skipTrailingSlashRedirect: true,
// fix all before production. Now it slow the develop speed.
eslint: {
// Warning: This allows production builds to successfully complete even if

View File

@ -44,6 +44,7 @@
"knip": "knip"
},
"dependencies": {
"@amplitude/plugin-session-replay-browser": "^1.23.2",
"@amplitude/unified": "1.0.0-beta.9",
"@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.26.28",
@ -106,6 +107,8 @@
"next-pwa": "^5.6.0",
"next-themes": "^0.4.6",
"pinyin-pro": "^3.27.0",
"posthog-js": "^1.288.0",
"posthog-node": "^5.11.1",
"qrcode.react": "^4.2.0",
"qs": "^6.14.0",
"react": "19.1.1",
@ -131,9 +134,9 @@
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"scheduler": "^0.26.0",
"socket.io-client": "^4.8.1",
"semver": "^7.7.3",
"sharp": "^0.33.5",
"socket.io-client": "^4.8.1",
"sortablejs": "^1.15.6",
"swr": "^2.3.6",
"tailwind-merge": "^2.6.0",

View File

@ -60,6 +60,9 @@ importers:
.:
dependencies:
'@amplitude/plugin-session-replay-browser':
specifier: ^1.23.2
version: 1.23.2(@amplitude/rrweb@2.0.0-alpha.33)(rollup@2.79.2)
'@amplitude/unified':
specifier: 1.0.0-beta.9
version: 1.0.0-beta.9(@amplitude/rrweb@2.0.0-alpha.33)(rollup@2.79.2)
@ -246,6 +249,12 @@ importers:
pinyin-pro:
specifier: ^3.27.0
version: 3.27.0
posthog-js:
specifier: ^1.288.0
version: 1.288.0
posthog-node:
specifier: ^5.11.1
version: 5.11.1
qrcode.react:
specifier: ^4.2.0
version: 4.2.0(react@19.1.1)
@ -2619,6 +2628,9 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/core@1.5.1':
resolution: {integrity: sha512-8fdEzfvdStr45iIncTD+gnqp45UBTUpRK/bwB4shP5usCKytnPIeilU8rIpNBOVjJPwfW+2N8yWhQ0l14x191Q==}
'@preact/signals-core@1.12.1':
resolution: {integrity: sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==}
@ -4304,6 +4316,9 @@ packages:
core-js-pure@3.46.0:
resolution: {integrity: sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==}
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@ -7064,6 +7079,16 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.288.0:
resolution: {integrity: sha512-KOeF8PK/zxBuFB4b3FVkj5JxSWAfSOrfDVvWj5VrJNBGYqr8igDbAl10huFv9NB4/K9XeIWQ7AzPPGV4D3lbEA==}
posthog-node@5.11.1:
resolution: {integrity: sha512-P6rtzdCVvS718r011x0W0cwmJo7gfP5YWXiWh0/S3OL+pnHtcqbWHDjrtRxN/IrMkjZWzMU4xDze5vRK/cZ23w==}
engines: {node: '>=20'}
preact@10.27.2:
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@ -8387,6 +8412,9 @@ packages:
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
web-vitals@5.1.0:
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
@ -8752,7 +8780,7 @@ snapshots:
'@amplitude/rrweb-packer@2.0.0-alpha.32':
dependencies:
'@amplitude/rrweb-types': 2.0.0-alpha.32
'@amplitude/rrweb-types': 2.0.0-alpha.33
fflate: 0.4.8
'@amplitude/rrweb-plugin-console-record@2.0.0-alpha.32(@amplitude/rrweb@2.0.0-alpha.33)':
@ -8762,7 +8790,7 @@ snapshots:
'@amplitude/rrweb-record@2.0.0-alpha.32':
dependencies:
'@amplitude/rrweb': 2.0.0-alpha.33
'@amplitude/rrweb-types': 2.0.0-alpha.32
'@amplitude/rrweb-types': 2.0.0-alpha.33
'@amplitude/rrweb-snapshot@2.0.0-alpha.33':
dependencies:
@ -11127,6 +11155,10 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@posthog/core@1.5.1':
dependencies:
cross-spawn: 7.0.6
'@preact/signals-core@1.12.1': {}
'@radix-ui/primitive@1.1.3': {}
@ -13059,6 +13091,8 @@ snapshots:
core-js-pure@3.46.0: {}
core-js@3.46.0: {}
core-util-is@1.0.3: {}
cose-base@1.0.3:
@ -16627,6 +16661,20 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.288.0:
dependencies:
'@posthog/core': 1.5.1
core-js: 3.46.0
fflate: 0.4.8
preact: 10.27.2
web-vitals: 4.2.4
posthog-node@5.11.1:
dependencies:
'@posthog/core': 1.5.1
preact@10.27.2: {}
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
@ -18108,6 +18156,8 @@ snapshots:
web-namespaces@2.0.1: {}
web-vitals@4.2.4: {}
web-vitals@5.1.0: {}
webidl-conversions@4.0.2: {}

65
web/utils/posthog.ts Normal file
View File

@ -0,0 +1,65 @@
import posthog from 'posthog-js'
import type { UserProfileResponse } from '@/models/common'
/**
* URL UTM
*/
function getUTMParams(): Record<string, string> {
const utmParams: Record<string, string> = {}
const urlParams = new URLSearchParams(window.location.search)
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
utmKeys.forEach((key) => {
const value = urlParams.get(key)
if (value)
utmParams[key] = value
})
return utmParams
}
/**
* PostHog
* @param userProfile -
*/
export function identifyUser(userProfile: UserProfileResponse) {
// 检查 PostHog 是否已加载
if (!posthog.__loaded)
return
// 检查是否有有效的用户 ID
if (!userProfile?.id)
return
try {
const properties: Record<string, any> = {}
// 用户基本信息
if (userProfile.email)
properties.email = userProfile.email
if (userProfile.name)
properties.name = userProfile.name
if (userProfile.created_at)
properties.created_at = userProfile.created_at
if (userProfile.interface_language)
properties.interface_language = userProfile.interface_language
if (userProfile.interface_theme)
properties.interface_theme = userProfile.interface_theme
if (userProfile.timezone)
properties.timezone = userProfile.timezone
// 添加当前 URL 的 UTM 参数(如果有)
const utmParams = getUTMParams()
if (Object.keys(utmParams).length > 0) {
Object.entries(utmParams).forEach(([key, value]) => {
properties[key] = value
})
}
posthog.identify(userProfile.id, properties)
}
catch (error) {
console.error('[PostHog] identify 失败:', error)
}
}