Skip to content

Commit b3c0d4c

Browse files
authored
Merge pull request #251 from InvolutionHell/umami-tracking-improvements-4727781891997572074
Enhance Umami tracking implementation
2 parents 9185eea + 713d126 commit b3c0d4c

9 files changed

Lines changed: 234 additions & 38 deletions

File tree

app/components/CopyTracking.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
export function CopyTracking() {
6+
useEffect(() => {
7+
// 监听全局 Copy 事件
8+
const handleCopy = () => {
9+
const selection = window.getSelection();
10+
if (!selection || selection.isCollapsed) return;
11+
12+
const text = selection.toString();
13+
if (!text) return;
14+
15+
// Determine if it's code or text
16+
let type = "text";
17+
const anchorNode = selection.anchorNode;
18+
const focusNode = selection.focusNode;
19+
20+
const isCode = (node: Node | null) => {
21+
let current = node;
22+
while (current) {
23+
if (
24+
current.nodeName === "PRE" ||
25+
current.nodeName === "CODE" ||
26+
(current instanceof Element &&
27+
(current.classList.contains("code-block") ||
28+
current.tagName === "PRE" ||
29+
current.tagName === "CODE"))
30+
) {
31+
return true;
32+
}
33+
current = current.parentNode;
34+
}
35+
return false;
36+
};
37+
38+
if (isCode(anchorNode) || isCode(focusNode)) {
39+
// 判断选中节点是否包含在 <pre> 或 <code> 标签内,区分代码块复制
40+
type = "code";
41+
}
42+
43+
// Umami 埋点: 记录复制行为,区分文本/代码类型和复制长度
44+
if (window.umami) {
45+
window.umami.track("content_copy", {
46+
type,
47+
content_length: text.length,
48+
});
49+
}
50+
};
51+
52+
document.addEventListener("copy", handleCopy);
53+
return () => document.removeEventListener("copy", handleCopy);
54+
}, []);
55+
56+
return null;
57+
}

app/components/CustomSearchDialog.tsx

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
TagsListItem,
1818
type SharedProps,
1919
} from "fumadocs-ui/components/dialog/search";
20+
import { useRouter } from "next/navigation";
2021

2122
interface TagItem {
2223
name: string;
@@ -34,6 +35,12 @@ interface DefaultSearchDialogProps extends SharedProps {
3435
allowClear?: boolean;
3536
}
3637

38+
interface SearchItem {
39+
url?: string;
40+
onSelect?: (value: string) => void;
41+
[key: string]: unknown;
42+
}
43+
3744
export function CustomSearchDialog({
3845
defaultTag,
3946
tags = [],
@@ -46,7 +53,12 @@ export function CustomSearchDialog({
4653
...props
4754
}: DefaultSearchDialogProps) {
4855
const { locale } = useI18n();
56+
const router = useRouter();
4957
const [tag, setTag] = useState(defaultTag);
58+
59+
// Extract onOpenChange to use in dependency array cleanly
60+
const { onOpenChange, ...otherProps } = props;
61+
5062
const { search, setSearch, query } = useDocsSearch(
5163
type === "fetch"
5264
? {
@@ -65,11 +77,12 @@ export function CustomSearchDialog({
6577
},
6678
);
6779

68-
// Tracking logic
80+
// Tracking logic for queries
6981
useEffect(() => {
7082
if (!search) return;
7183

7284
const timer = setTimeout(() => {
85+
// Umami 埋点: 搜索结果点击
7386
if (window.umami) {
7487
window.umami.track("search_query", { query: search });
7588
}
@@ -88,12 +101,48 @@ export function CustomSearchDialog({
88101
}));
89102
}, [links]);
90103

104+
// 使用 useMemo 劫持 search items,注入埋点逻辑
105+
const trackedItems = useMemo(() => {
106+
const data = query.data !== "empty" && query.data ? query.data : defaultItems;
107+
if (!data) return [];
108+
109+
return data.map((item: unknown, index: number) => {
110+
const searchItem = item as SearchItem;
111+
return {
112+
...searchItem,
113+
onSelect: (value: string) => {
114+
// Umami 埋点: 搜索结果点击
115+
if (window.umami) {
116+
window.umami.track("search_result_click", {
117+
query: search,
118+
rank: index + 1,
119+
url: searchItem.url,
120+
});
121+
}
122+
123+
// Call original onSelect if it exists
124+
if (searchItem.onSelect) searchItem.onSelect(value);
125+
126+
// Handle navigation if URL exists
127+
if (searchItem.url) {
128+
// 显式执行路由跳转和关闭弹窗,确保点击行为能够同时触发埋点和导航
129+
router.push(searchItem.url);
130+
if (onOpenChange) {
131+
onOpenChange(false);
132+
}
133+
}
134+
},
135+
};
136+
});
137+
}, [query.data, defaultItems, search, router, onOpenChange]);
138+
91139
return (
92140
<SearchDialog
93141
search={search}
94142
onSearchChange={setSearch}
95143
isLoading={query.isLoading}
96-
{...props}
144+
onOpenChange={onOpenChange}
145+
{...otherProps}
97146
>
98147
<SearchDialogOverlay />
99148
<SearchDialogContent>
@@ -102,11 +151,8 @@ export function CustomSearchDialog({
102151
<SearchDialogInput />
103152
<SearchDialogClose />
104153
</SearchDialogHeader>
105-
<SearchDialogList
106-
items={
107-
query.data !== "empty" && query.data ? query.data : defaultItems
108-
}
109-
/>
154+
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
155+
<SearchDialogList items={trackedItems as any} />
110156
</SearchDialogContent>
111157
<SearchDialogFooter>
112158
{tags.length > 0 && (

app/components/Footer.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export function Footer() {
6363
<Link
6464
href="/docs/ai"
6565
className="hover:text-[#CC0000] transition-colors"
66+
// Umami 埋点: Footer 快速链接点击
6667
data-umami-event="navigation_click"
6768
data-umami-event-region="footer"
6869
data-umami-event-label="AI & Mathematics"
@@ -74,6 +75,7 @@ export function Footer() {
7475
<Link
7576
href="/docs/computer-science"
7677
className="hover:text-[#CC0000] transition-colors"
78+
// Umami 埋点: Footer 快速链接点击
7779
data-umami-event="navigation_click"
7880
data-umami-event-region="footer"
7981
data-umami-event-label="Computer Science"
@@ -85,6 +87,7 @@ export function Footer() {
8587
<Link
8688
href="/docs/CommunityShare"
8789
className="hover:text-[#CC0000] transition-colors"
90+
// Umami 埋点: Footer 快速链接点击
8891
data-umami-event="navigation_click"
8992
data-umami-event-region="footer"
9093
data-umami-event-label="Community Sharing"
@@ -96,6 +99,7 @@ export function Footer() {
9699
<Link
97100
href="/docs/jobs"
98101
className="hover:text-[#CC0000] transition-colors"
102+
// Umami 埋点: Footer 快速链接点击
99103
data-umami-event="navigation_click"
100104
data-umami-event-region="footer"
101105
data-umami-event-label="Career Prep"
@@ -128,6 +132,10 @@ export function Footer() {
128132
<li>
129133
<a
130134
href="#features"
135+
// Umami 埋点: Footer 快速链接点击
136+
data-umami-event="navigation_click"
137+
data-umami-event-region="footer"
138+
data-umami-event-label="Mission Brief"
131139
className="hover:text-[#CC0000] transition-colors"
132140
>
133141
Mission Brief
@@ -136,6 +144,10 @@ export function Footer() {
136144
<li>
137145
<a
138146
href="#community"
147+
// Umami 埋点: Footer 快速链接点击
148+
data-umami-event="navigation_click"
149+
data-umami-event-region="footer"
150+
data-umami-event-label="Network Status"
139151
className="hover:text-[#CC0000] transition-colors"
140152
>
141153
Network Status

app/components/Hero.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ export function Hero() {
8383
<Link
8484
href="/docs/ai"
8585
className="block w-full"
86-
data-umami-event="feature_cta_click"
87-
data-umami-event-action="access_articles"
88-
data-umami-event-location="hero_sidebar"
86+
// Umami 埋点: Hero CTA 按钮点击
87+
data-umami-event="navigation_click"
88+
data-umami-event-region="hero_cta"
89+
data-umami-event-label="Access Articles"
8990
>
9091
<button className="w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer">
9192
Access Articles / 访问文章
@@ -113,8 +114,9 @@ export function Hero() {
113114
<Link
114115
href={c.href}
115116
className="p-8 hover:bg-neutral-100 dark:hover:bg-neutral-900 transition-colors h-full flex flex-col hard-shadow-hover"
116-
data-umami-event="home_category_click"
117-
data-umami-event-category={c.title}
117+
// Umami 埋点: 首页分类卡片点击
118+
data-umami-event="navigation_click"
119+
data-umami-event-region="home_categories" data-umami-event-label={c.title}
118120
>
119121
<div className="font-mono text-[10px] text-neutral-400 mb-4">
120122
00{idx + 1}

app/components/PageFeedback.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { usePathname } from "next/navigation";
5+
import { Button } from "@/app/components/ui/button";
6+
import { ThumbsUp, ThumbsDown } from "lucide-react";
7+
8+
export function PageFeedback() {
9+
const pathname = usePathname();
10+
const [voted, setVoted] = useState<"helpful" | "not_helpful" | null>(null);
11+
12+
const handleVote = (vote: "helpful" | "not_helpful") => {
13+
if (voted) return;
14+
15+
if (window.umami) {
16+
// Umami 埋点: 记录用户是否有帮助的投票
17+
window.umami.track("feedback_submit", {
18+
page: pathname,
19+
vote,
20+
});
21+
}
22+
setVoted(vote);
23+
};
24+
25+
if (voted) {
26+
return (
27+
<div className="flex items-center gap-2 text-sm text-neutral-500 mt-8 py-4 border-t border-[var(--foreground)] font-serif italic">
28+
<span>Thanks for your feedback! / 感谢您的反馈!</span>
29+
</div>
30+
);
31+
}
32+
33+
return (
34+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4 border-t border-[var(--foreground)] mt-8">
35+
<span className="text-sm font-medium text-[var(--foreground)] font-serif">
36+
Was this page helpful? / 这篇文章有帮助吗?
37+
</span>
38+
<div className="flex gap-2">
39+
<Button
40+
variant="outline"
41+
size="sm"
42+
onClick={() => handleVote("helpful")}
43+
className="gap-2 border-[var(--foreground)] text-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors rounded-none font-sans"
44+
>
45+
<ThumbsUp className="h-4 w-4" />
46+
Yes
47+
</Button>
48+
<Button
49+
variant="outline"
50+
size="sm"
51+
onClick={() => handleVote("not_helpful")}
52+
className="gap-2 border-[var(--foreground)] text-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors rounded-none font-sans"
53+
>
54+
<ThumbsDown className="h-4 w-4" />
55+
No
56+
</Button>
57+
</div>
58+
</div>
59+
);
60+
}

app/docs/[...slug]/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { Contributors } from "@/app/components/Contributors";
1414
import { DocsAssistant } from "@/app/components/DocsAssistant";
1515
import { LicenseNotice } from "@/app/components/LicenseNotice";
16+
import { PageFeedback } from "@/app/components/PageFeedback";
1617
import fs from "fs/promises";
1718
import path from "path";
1819

@@ -92,6 +93,7 @@ export default async function DocPage({ params }: Param) {
9293
</div>
9394
<Mdx components={getMDXComponents()} />
9495
<Contributors entry={contributorsEntry} />
96+
<PageFeedback />
9597
<section className="mt-16">
9698
<GiscusComments docId={docIdFromPage ?? null} />
9799
</section>

app/docs/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { baseOptions } from "@/lib/layout.shared";
44
import type { ReactNode } from "react";
55
import { DocsRouteFlag } from "@/app/components/RouteFlags";
66
import type { PageTree } from "fumadocs-core/server";
7+
import { CopyTracking } from "@/app/components/CopyTracking";
78

89
function pruneEmptyFolders(root: PageTree.Root): PageTree.Root {
910
const transformNode = (node: PageTree.Node): PageTree.Node | null => {
@@ -68,6 +69,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
6869
return (
6970
<>
7071
{/* Add a class on <html> while in docs to adjust global backgrounds */}
72+
<CopyTracking />
7173
<DocsRouteFlag />
7274
<DocsLayout
7375
tree={tree}

app/not-found.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ export default function NotFound() {
99
const pathname = usePathname();
1010

1111
useEffect(() => {
12+
// Umami 埋点: 记录 404 错误和来源页面 (Referrer)
1213
if (window.umami) {
13-
window.umami.track("404", { path: pathname });
14+
window.umami.track("error_404", {
15+
path: pathname,
16+
referrer: document.referrer || "direct",
17+
});
1418
}
1519
}, [pathname]);
1620

0 commit comments

Comments
 (0)