mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-24 15:24:06 +08:00
feat(analytics): implement runtime configuration and analytics tracking for GA4 and Baidu
This commit is contained in:
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# 由 nginx 官方镜像的入口在启动前自动执行(/docker-entrypoint.d/*.sh),随后 nginx 正常拉起。
|
||||
# 从环境变量生成运行期配置 config.js;每家统计一个独立变量,未设置的留空,
|
||||
# 前端据此判定该家「关闭」,不加载对应脚本、不发外部请求。可同时启用多家。
|
||||
|
||||
# GA4 / 百度 ID 只含字母、数字和连字符;过滤掉其它字符,
|
||||
# 避免值里的引号等破坏 config.js 的 JS 字符串(纵深防御)。
|
||||
sanitize_id() {
|
||||
printf '%s' "$1" | tr -cd 'A-Za-z0-9-'
|
||||
}
|
||||
|
||||
GA4_ID=$(sanitize_id "${ANALYTICS_GA4_ID:-}")
|
||||
BAIDU_ID=$(sanitize_id "${ANALYTICS_BAIDU_ID:-}")
|
||||
|
||||
cat > /usr/share/nginx/html/config.js <<EOF
|
||||
window.__RUNTIME_CONFIG__ = {
|
||||
ANALYTICS_GA4_ID: "${GA4_ID}",
|
||||
ANALYTICS_BAIDU_ID: "${BAIDU_ID}"
|
||||
};
|
||||
EOF
|
||||
@@ -17,6 +17,7 @@
|
||||
</head>
|
||||
<body class="bg-background text-foreground antialiased">
|
||||
<div id="root"></div>
|
||||
<script src="/config.js"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// 运行期配置。容器启动时由 docker-entrypoint.sh 从环境变量重新生成此文件;
|
||||
// 本地开发与未经 entrypoint 处理时使用这份默认空配置(统计默认关闭)。
|
||||
window.__RUNTIME_CONFIG__ = window.__RUNTIME_CONFIG__ || {};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import { trackPageview } from "@/lib/analytics";
|
||||
|
||||
// 监听 SPA 路由变化并上报 pageview。无统计配置时 trackPageview 为空操作。
|
||||
export function AnalyticsTracker() {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
trackPageview(`${location.pathname}${location.search}`);
|
||||
}, [location.pathname, location.search]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 运行期配置读取层。
|
||||
// 优先级:window.__RUNTIME_CONFIG__(容器启动时由 entrypoint 注入)> 构建期 VITE_ 变量 > 默认值。
|
||||
// 这样既支持「同一镜像 docker run -e 配置」,也兼容自行 build 时的构建期注入。
|
||||
//
|
||||
// 统计按「每家一个独立变量」配置:填了谁就启用谁,可同时启用多家,默认全空即关闭。
|
||||
// 仅支持 GA4 与百度:两者都只接受 ID,脚本地址由代码固定拼接,不接受任意脚本/内联 JS。
|
||||
|
||||
type RuntimeConfig = {
|
||||
ANALYTICS_GA4_ID?: string; // GA4 衡量 ID(G-XXXX)
|
||||
ANALYTICS_BAIDU_ID?: string; // 百度统计站点 ID
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__RUNTIME_CONFIG__?: RuntimeConfig;
|
||||
}
|
||||
}
|
||||
|
||||
const runtime: RuntimeConfig = (typeof window !== "undefined" && window.__RUNTIME_CONFIG__) || {};
|
||||
|
||||
function read(key: keyof RuntimeConfig, buildTime: string | undefined, fallback = ""): string {
|
||||
const value = runtime[key];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
if (typeof buildTime === "string" && buildTime.trim()) return buildTime.trim();
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export const ANALYTICS_GA4_ID = read("ANALYTICS_GA4_ID", import.meta.env.VITE_ANALYTICS_GA4_ID);
|
||||
export const ANALYTICS_BAIDU_ID = read("ANALYTICS_BAIDU_ID", import.meta.env.VITE_ANALYTICS_BAIDU_ID);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// 统计分析加载器:默认关闭,可同时启用多家。
|
||||
// 仅支持 GA4 与百度:两者都只接受 ID,脚本地址由代码固定拼接——
|
||||
// 不接受任意脚本 URL / 内联 JS,避免「配置项被用来在访客浏览器执行任意代码」。
|
||||
// 全空时不注入任何脚本、不发任何外部请求。这是开源项目的硬要求:
|
||||
// fork/自托管者默认零统计,官方站点仅通过环境变量注入自己的 ID(ID 不入库)。
|
||||
|
||||
import { ANALYTICS_BAIDU_ID, ANALYTICS_GA4_ID } from "@/constant/runtime-config";
|
||||
|
||||
type GtagFn = (...args: unknown[]) => void;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer?: unknown[];
|
||||
gtag?: GtagFn;
|
||||
_hmt?: unknown[][];
|
||||
}
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
// 记录实际启用了哪些统计,供路由上报时按需分发。
|
||||
const active = { ga4: false, baidu: false };
|
||||
|
||||
function appendScript(src: string, attrs: Record<string, string> = {}) {
|
||||
const el = document.createElement("script");
|
||||
el.async = true;
|
||||
el.src = src;
|
||||
for (const [key, value] of Object.entries(attrs)) el.setAttribute(key, value);
|
||||
document.head.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function initGa4(id: string) {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
const gtag: GtagFn = (...args) => {
|
||||
window.dataLayer!.push(args);
|
||||
};
|
||||
window.gtag = gtag;
|
||||
appendScript(`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`);
|
||||
gtag("js", new Date());
|
||||
// SPA 路由上报交给 trackPageview,这里关闭默认的自动 page_view,避免重复。
|
||||
gtag("config", id, { send_page_view: false });
|
||||
active.ga4 = true;
|
||||
}
|
||||
|
||||
function initBaidu(id: string) {
|
||||
window._hmt = window._hmt || [];
|
||||
appendScript(`https://hm.baidu.com/hm.js?${encodeURIComponent(id)}`);
|
||||
active.baidu = true;
|
||||
}
|
||||
|
||||
export function initAnalytics() {
|
||||
if (initialized || typeof window === "undefined") return;
|
||||
initialized = true;
|
||||
|
||||
// 各家相互独立,逐个判断并启用;任一家出错都不影响其它家与主应用。
|
||||
if (ANALYTICS_GA4_ID) {
|
||||
try {
|
||||
initGa4(ANALYTICS_GA4_ID);
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
if (ANALYTICS_BAIDU_ID) {
|
||||
try {
|
||||
initBaidu(ANALYTICS_BAIDU_ID);
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SPA 路由切换时上报页面浏览,分发给所有已启用的统计。
|
||||
export function trackPageview(path: string) {
|
||||
try {
|
||||
if (active.ga4 && window.gtag) {
|
||||
window.gtag("event", "page_view", { page_path: path, page_location: window.location.href });
|
||||
}
|
||||
if (active.baidu && window._hmt) {
|
||||
window._hmt.push(["_trackPageview", path]);
|
||||
}
|
||||
} catch {
|
||||
/* 忽略 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import "./styles/globals.css";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import { AppProviders } from "@/components/layout/app-providers";
|
||||
import { initAnalytics } from "@/lib/analytics";
|
||||
import { router } from "@/router";
|
||||
|
||||
initAnalytics();
|
||||
|
||||
document.body.style.fontFamily = '"SF Pro Display","SF Pro Text","PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif';
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createBrowserRouter, Outlet } from "react-router-dom";
|
||||
|
||||
import { AnalyticsTracker } from "@/components/layout/analytics-tracker";
|
||||
import UserLayout from "@/layouts/user-layout";
|
||||
import AssetsPage from "@/pages/assets";
|
||||
import CanvasPage from "@/pages/canvas";
|
||||
@@ -15,6 +16,7 @@ export const router = createBrowserRouter([
|
||||
{
|
||||
element: (
|
||||
<UserLayout>
|
||||
<AnalyticsTracker />
|
||||
<Outlet />
|
||||
</UserLayout>
|
||||
),
|
||||
|
||||
Vendored
+5
@@ -6,4 +6,9 @@ declare const __APP_RELEASES__: import("@/lib/release").ReleaseInfo[];
|
||||
interface ImportMetaEnv {
|
||||
// 逗号分隔的本地开发插件 URL,每次启动重新拉取(不缓存、不落库)
|
||||
readonly VITE_DEV_PLUGINS?: string;
|
||||
// 统计分析(可选,构建期注入):每家一个独立变量,填了谁就启用谁,可同时启用多家
|
||||
// GA4 衡量 ID(G-XXXX)
|
||||
readonly VITE_ANALYTICS_GA4_ID?: string;
|
||||
// 百度统计站点 ID
|
||||
readonly VITE_ANALYTICS_BAIDU_ID?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user