diff --git a/README.md b/README.md index 5b41e0e..6ce4cf1 100644 --- a/README.md +++ b/README.md @@ -765,7 +765,7 @@ ALLOW_LAN_ACCESS=true LAN_ALLOWED_DEV_ORIGINS="kvideo.lan,192.168.50.10" npm run - **ESLint 10**:代码质量检查 - **PostCSS 8**:CSS 处理器 -- **Vercel Analytics**:性能监控和分析 +- **Vercel Analytics**:仅在 Vercel 部署中启用的性能监控和分析 - **Cloudflare Pages**:边缘部署支持 ### 架构特点 diff --git a/app-release.json b/app-release.json index ad2e9ed..c69f9db 100644 --- a/app-release.json +++ b/app-release.json @@ -4,8 +4,18 @@ "name": "KVideo", "branch": "main" }, - "currentVersion": "4.9.4", + "currentVersion": "4.9.5", "releases": [ + { + "version": "4.9.5", + "publishedAt": "2026-07-09", + "title": "修复 Docker 登录页 Analytics 404", + "notes": [ + "Vercel Analytics 现在只会在真实 Vercel 部署中注入,Docker、自托管和 Cloudflare Pages 不再请求 /_vercel/insights/script.js。", + "Cloudflare 部署环境判定优先于 Vercel 风格环境变量,避免适配器环境被误判为 Vercel。", + "新增部署环境单元测试,覆盖自托管、Vercel、Cloudflare 和混合环境变量场景。" + ] + }, { "version": "4.9.4", "publishedAt": "2026-07-08", diff --git a/app/layout.tsx b/app/layout.tsx index f72b002..68a1c38 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -16,6 +16,7 @@ import { ScrollPositionManager } from "@/components/ScrollPositionManager"; import { LocaleProvider } from "@/components/LocaleProvider"; import { RuntimeFeaturesProvider } from "@/components/RuntimeFeaturesProvider"; import { VideoTogetherController } from '@/components/VideoTogetherController'; +import { shouldEnableVercelAnalytics } from '@/lib/config/deployment'; import { getRuntimeFeatures } from "@/lib/server/runtime-features"; import { resolveSiteIconSrc } from '@/lib/server/site-icon'; import fs from 'fs'; @@ -86,6 +87,7 @@ export default async function RootLayout({ process.env.VIDEOTOGETHER_SCRIPT_URL?.trim() || DEFAULT_VIDEOTOGETHER_SCRIPT_URL; const videoTogetherSettingUrl = process.env.VIDEOTOGETHER_SETTING_URL?.trim(); const videoTogetherEnvEnabled = process.env.VIDEOTOGETHER_ENABLED !== 'false'; + const vercelAnalyticsEnabled = shouldEnableVercelAnalytics(); return ( @@ -136,7 +138,7 @@ export default async function RootLayout({ - + {vercelAnalyticsEnabled ? : null} diff --git a/lib/config/deployment.ts b/lib/config/deployment.ts new file mode 100644 index 0000000..38d2c21 --- /dev/null +++ b/lib/config/deployment.ts @@ -0,0 +1,19 @@ +type EnvLike = Record; + +export function isVercelDeployment(env: EnvLike = process.env): boolean { + return env.VERCEL === '1' || Boolean(env.VERCEL_ENV); +} + +export function isCloudflareDeployment(env: EnvLike = process.env): boolean { + return ( + env.CF_PAGES === '1' || + Boolean(env.CF_PAGES_URL) || + Boolean(env.CLOUDFLARE_ACCOUNT_ID) || + Boolean(env.CF_ACCOUNT_ID) || + Boolean(env.WORKERS_CI) + ); +} + +export function shouldEnableVercelAnalytics(env: EnvLike = process.env): boolean { + return isVercelDeployment(env) && !isCloudflareDeployment(env); +} diff --git a/lib/server/runtime-features.ts b/lib/server/runtime-features.ts index b0cc56b..46968ea 100644 --- a/lib/server/runtime-features.ts +++ b/lib/server/runtime-features.ts @@ -1,20 +1,7 @@ import 'server-only'; import type { RuntimeFeatures } from '@/lib/config/runtime-features'; - -function isVercelDeployment(): boolean { - return process.env.VERCEL === '1' || Boolean(process.env.VERCEL_ENV); -} - -function isCloudflareDeployment(): boolean { - return ( - process.env.CF_PAGES === '1' || - Boolean(process.env.CF_PAGES_URL) || - Boolean(process.env.CLOUDFLARE_ACCOUNT_ID) || - Boolean(process.env.CF_ACCOUNT_ID) || - Boolean(process.env.WORKERS_CI) - ); -} +import { isCloudflareDeployment, isVercelDeployment } from '@/lib/config/deployment'; function getRestrictedFeatures( deploymentProvider: RuntimeFeatures['deploymentProvider'], @@ -31,14 +18,14 @@ function getRestrictedFeatures( } export function getRuntimeFeatures(): RuntimeFeatures { - if (isVercelDeployment()) { - return getRestrictedFeatures('vercel', 'Vercel'); - } - if (isCloudflareDeployment()) { return getRestrictedFeatures('cloudflare', 'Cloudflare'); } + if (isVercelDeployment()) { + return getRestrictedFeatures('vercel', 'Vercel'); + } + return { deploymentProvider: 'self-hosted', deploymentProviderLabel: '自托管', @@ -48,4 +35,3 @@ export function getRuntimeFeatures(): RuntimeFeatures { restrictionSummary: null, }; } - diff --git a/package-lock.json b/package-lock.json index 3cdac9f..1e031f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvideo", - "version": "4.9.4", + "version": "4.9.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "kvideo", - "version": "4.9.4", + "version": "4.9.5", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 966330c..f701178 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kvideo", - "version": "4.9.4", + "version": "4.9.5", "private": true, "scripts": { "dev": "node scripts/next-with-lan-access.mjs dev", diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts new file mode 100644 index 0000000..3106565 --- /dev/null +++ b/tests/deployment.test.ts @@ -0,0 +1,38 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + isCloudflareDeployment, + isVercelDeployment, + shouldEnableVercelAnalytics, +} from '@/lib/config/deployment'; + +test('self-hosted deployments do not enable Vercel Analytics', () => { + const env = {}; + + assert.equal(isVercelDeployment(env), false); + assert.equal(isCloudflareDeployment(env), false); + assert.equal(shouldEnableVercelAnalytics(env), false); +}); + +test('Vercel deployments enable Vercel Analytics', () => { + assert.equal(shouldEnableVercelAnalytics({ VERCEL: '1' }), true); + assert.equal(shouldEnableVercelAnalytics({ VERCEL_ENV: 'production' }), true); +}); + +test('Cloudflare deployments do not enable Vercel Analytics', () => { + assert.equal(shouldEnableVercelAnalytics({ CF_PAGES: '1' }), false); + assert.equal(shouldEnableVercelAnalytics({ CF_PAGES_URL: 'https://kvideo.pages.dev' }), false); +}); + +test('Cloudflare wins when adapter environments expose Vercel-like variables', () => { + const env = { + VERCEL: '1', + VERCEL_ENV: 'production', + CF_PAGES: '1', + }; + + assert.equal(isVercelDeployment(env), true); + assert.equal(isCloudflareDeployment(env), true); + assert.equal(shouldEnableVercelAnalytics(env), false); +});