Merge pull request #479 from sarinali/docs/telemetry

Telemetry on Docs
This commit is contained in:
James Murdza
2025-10-17 10:55:06 -07:00
committed by GitHub
13 changed files with 385 additions and 3 deletions

2
docs/.env.example Normal file
View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_POSTHOG_API_KEY=
NEXT_PUBLIC_POSTHOG_HOST=

View File

@@ -34,6 +34,14 @@ A `source.config.ts` config file has been included, you can customise different
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
## Setup Telemetry
We use PostHog for telemetry to improve the clarity and structure of our documentation. Start by copying the `.env.example` and adding in your PostHog API key and host.
```bash
cp .env.example .env
```
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following

View File

@@ -16,6 +16,7 @@
"mermaid": "^11.8.1",
"next": "15.3.3",
"next-themes": "^0.4.6",
"posthog-js": "^1.276.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"remark": "^15.0.1",

47
docs/pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
posthog-js:
specifier: ^1.276.0
version: 1.276.0
react:
specifier: ^19.1.0
version: 19.1.0
@@ -489,6 +492,9 @@ packages:
resolution: {integrity: sha512-6yB0117ZjsgNevZw3LP+bkrZa9mU/POPVaXgzMPOBbBc35w2P3R+1vMMhEfC06kYCpd5bf0jodBaTkYQW5TVeQ==}
engines: {node: '>= 20.0.0'}
'@posthog/core@1.3.0':
resolution: {integrity: sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -1221,6 +1227,9 @@ packages:
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
cose-base@1.0.3:
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
@@ -1492,6 +1501,9 @@ packages:
picomatch:
optional: true
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fumadocs-core@15.5.1:
resolution: {integrity: sha512-5eJPJw+BFWFdgrtWPQ9aAZAhhsyuZAwth8OjBd9R77sXoIoae4Y4lJZMq3BeSpJZcuIAOVbSCS+pJhsBAoXJ8g==}
peerDependencies:
@@ -2012,6 +2024,20 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.276.0:
resolution: {integrity: sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw==}
peerDependencies:
'@rrweb/types': 2.0.0-alpha.17
rrweb-snapshot: 2.0.0-alpha.17
peerDependenciesMeta:
'@rrweb/types':
optional: true
rrweb-snapshot:
optional: true
preact@10.27.2:
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
@@ -2317,6 +2343,9 @@ packages:
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
@@ -2642,6 +2671,8 @@ snapshots:
'@orama/orama@3.1.7': {}
'@posthog/core@1.3.0': {}
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.2': {}
@@ -3378,6 +3409,8 @@ snapshots:
confbox@0.2.2: {}
core-js@3.46.0: {}
cose-base@1.0.3:
dependencies:
layout-base: 1.0.2
@@ -3702,6 +3735,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fflate@0.4.8: {}
fumadocs-core@15.5.1(@types/react@19.1.8)(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@formatjs/intl-localematcher': 0.6.1
@@ -4566,6 +4601,16 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.276.0:
dependencies:
'@posthog/core': 1.3.0
core-js: 3.46.0
fflate: 0.4.8
preact: 10.27.2
web-vitals: 4.2.4
preact@10.27.2: {}
prettier@3.6.2: {}
property-information@7.1.0: {}
@@ -4934,6 +4979,8 @@ snapshots:
vscode-uri@3.0.8: {}
web-vitals@4.2.4: {}
yallist@5.0.0: {}
zod@3.25.76: {}

View File

@@ -18,6 +18,7 @@ import { ChevronDown, CodeXml, ExternalLink } from 'lucide-react';
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound, redirect } from 'next/navigation';
import { PageFeedback } from '@/components/page-feedback';
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
@@ -270,6 +271,7 @@ export default async function Page(props: {
a: createRelativeLink(source, page),
})}
/>
<PageFeedback />
</DocsBody>
</DocsPage>
);

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const url = new URL(request.url);
const targetUrl = `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/${path.join('/')}${url.search}`;
try {
const response = await fetch(targetUrl, {
method: 'GET',
headers: {
'Content-Type': request.headers.get('Content-Type') || 'application/json',
},
});
// Handle 204 No Content responses
if (response.status === 204) {
return new NextResponse(null, { status: 204 });
}
const data = await response.arrayBuffer();
return new NextResponse(data, {
status: response.status,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json',
},
});
} catch (error) {
console.error('PostHog proxy error:', error);
return new NextResponse('Error proxying request', { status: 500 });
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const url = new URL(request.url);
const targetUrl = `${process.env.NEXT_PUBLIC_POSTHOG_HOST}/${path.join('/')}${url.search}`;
try {
const body = await request.arrayBuffer();
const contentType = request.headers.get('Content-Type') || 'application/x-www-form-urlencoded';
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': contentType,
},
body,
});
// Handle 204 No Content responses
if (response.status === 204) {
return new NextResponse(null, { status: 204 });
}
const data = await response.arrayBuffer();
return new NextResponse(data, {
status: response.status,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json',
},
});
} catch (error) {
console.error('PostHog proxy error:', error);
return new NextResponse('Error proxying request', { status: 500 });
}
}

View File

@@ -2,6 +2,11 @@ import './global.css';
import { RootProvider } from 'fumadocs-ui/provider';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import { PHProvider, PostHogPageView } from '@/providers/posthog-provider';
import { AnalyticsTracker } from '@/components/analytics-tracker';
import { CookieConsent } from '@/components/cookie-consent';
import { Footer } from '@/components/footer';
import { Suspense } from 'react';
const inter = Inter({
subsets: ['latin'],
@@ -14,9 +19,17 @@ export default function Layout({ children }: { children: ReactNode }) {
<link rel="icon" href="/docs/favicon.ico" sizes="any" />
</head>
<body className="flex min-h-screen flex-col">
<RootProvider search={{ options: { api: '/docs/api/search' } }}>
{children}
</RootProvider>
<PHProvider>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
<AnalyticsTracker />
<RootProvider search={{ options: { api: '/docs/api/search' } }}>
{children}
</RootProvider>
<Footer />
<CookieConsent />
</PHProvider>
</body>
</html>
);

View File

@@ -0,0 +1,71 @@
'use client';
import { useEffect } from 'react';
import posthog from 'posthog-js';
export function AnalyticsTracker() {
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest('a');
if (!link) return;
const href = link.href;
const text = link.textContent || link.getAttribute('aria-label') || '';
if (href.includes('github.com/trycua')) {
posthog.capture('github_link_clicked', {
url: href,
link_text: text,
page: window.location.pathname,
});
}
if (href.includes('discord.com/invite') || href.includes('discord.gg')) {
posthog.capture('discord_link_clicked', {
url: href,
link_text: text,
page: window.location.pathname,
});
}
if (
(href.includes('trycua.com') && !href.includes('trycua.com/docs')) ||
href.includes('cua.ai')
) {
posthog.capture('main_website_clicked', {
url: href,
link_text: text,
page: window.location.pathname,
});
}
if (link.hostname && link.hostname !== window.location.hostname) {
if (
href.includes('github.com/trycua') ||
href.includes('discord.com') ||
href.includes('trycua.com') ||
href.includes('cua.ai')
) {
return;
}
posthog.capture('external_link_clicked', {
url: href,
link_text: text,
page: window.location.pathname,
domain: link.hostname,
});
}
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, []);
return null;
}

View File

@@ -0,0 +1,44 @@
'use client';
import { useEffect, useState } from 'react';
import posthog from 'posthog-js';
export function CookieConsent() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Check if user has already accepted cookies
const hasAccepted = localStorage.getItem('cookie-consent');
if (!hasAccepted) {
setIsVisible(true);
}
}, []);
const handleAccept = () => {
localStorage.setItem('cookie-consent', 'accepted');
setIsVisible(false);
// Track cookie acceptance
posthog.capture('cookie_consent_accepted', {
page: window.location.pathname,
});
};
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-fd-background border-t border-fd-border shadow-lg">
<div className="container mx-auto px-4 py-2 flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-xs text-fd-muted-foreground">
This site uses cookies for website functionality, analytics, and personalized content.
</p>
<button
onClick={handleAccept}
className="px-4 py-1 text-xs bg-fd-primary text-fd-primary-foreground rounded hover:opacity-90 transition-opacity whitespace-nowrap"
>
Okay
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
export function Footer() {
return (
<footer className="mt-auto border-t border-fd-border py-4">
<div className="container mx-auto px-4 flex justify-end">
<a
href="https://www.cua.ai/cookie-policy"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-fd-muted-foreground hover:text-fd-foreground transition-colors"
>
Cookie Policy
</a>
</div>
</footer>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import { useState } from 'react';
import posthog from 'posthog-js';
import { ThumbsUp, ThumbsDown } from 'lucide-react';
export function PageFeedback() {
const [feedback, setFeedback] = useState<'helpful' | 'not_helpful' | null>(null);
const handleFeedback = (isHelpful: boolean) => {
const feedbackType = isHelpful ? 'helpful' : 'not_helpful';
setFeedback(feedbackType);
posthog.capture(`page_feedback_${feedbackType}`, {
page: window.location.pathname,
page_title: document.title,
});
};
return (
<div className="mt-8 pt-4 border-t border-fd-border">
{feedback === null ? (
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<p className="text-sm text-fd-muted-foreground">Was this page helpful?</p>
<div className="flex gap-2">
<button
onClick={() => handleFeedback(true)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm hover:bg-fd-accent rounded transition-colors"
aria-label="This page was helpful"
>
<ThumbsUp className="w-4 h-4" />
Yes
</button>
<button
onClick={() => handleFeedback(false)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm hover:bg-fd-accent rounded transition-colors"
aria-label="This page was not helpful"
>
<ThumbsDown className="w-4 h-4" />
No
</button>
</div>
</div>
) : (
<p className="text-sm text-fd-muted-foreground text-left">
{feedback === 'helpful'
? 'Thanks for your feedback!'
: 'Thanks for your feedback. We\'ll work on improving this page.'}
</p>
)}
</div>
);
}

View File

@@ -3,6 +3,12 @@ import * as TabsComponents from 'fumadocs-ui/components/tabs';
import type { MDXComponents } from 'mdx/types';
import { Mermaid } from './components/mermaid';
import IOU from './components/iou';
import {
EditableCodeBlock,
EditableValue,
EditableForm,
EditableInput,
} from './components/editable-code-block';
// use this function to get MDX components, you will need it for rendering MDX
export function getMDXComponents(components?: MDXComponents): MDXComponents {
@@ -10,6 +16,10 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
...defaultMdxComponents,
Mermaid,
IOU,
EditableCodeBlock,
EditableValue,
EditableForm,
EditableInput,
...TabsComponents,
...components,
};

View File

@@ -0,0 +1,40 @@
'use client';
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_API_KEY!, {
api_host: '/docs/api/posthog',
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'always',
capture_pageview: false,
capture_pageleave: true,
});
}
export function PHProvider({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
export function PostHogPageView(): null {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (pathname) {
let url = window.origin + pathname;
if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}`;
}
posthog.capture('$pageview', {
$current_url: url,
});
}
}, [pathname, searchParams]);
return null;
}