Skip to content

Commit 68561c0

Browse files
BenTaylorDevclaude
andcommitted
feat(docs): track cli command copies via global writeText hook
CopyTracker monkey-patches navigator.clipboard.writeText once at app boot to fire PostHog cli_command_copied for every programmatic copy — covering Fumadocs' built-in code-block button, custom-code-block, code-showcase, framework-overview, and any future copy widget without per-component instrumentation. Reo's own writeText patch continues to function alongside this one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7f997a9 commit 68561c0

3 files changed

Lines changed: 89 additions & 1 deletion

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
import { usePostHog } from "posthog-js/react";
5+
import { trackCommandCopy } from "@/lib/track-command-copy";
6+
7+
export function CopyTracker() {
8+
const posthog = usePostHog();
9+
10+
useEffect(() => {
11+
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) return;
12+
const original = navigator.clipboard.writeText.bind(navigator.clipboard);
13+
navigator.clipboard.writeText = async function (text: string) {
14+
try {
15+
trackCommandCopy(posthog, {
16+
command: text,
17+
location: typeof window !== "undefined" ? window.location.pathname : undefined,
18+
});
19+
} catch {
20+
// Never let analytics break the underlying copy.
21+
}
22+
return original(text);
23+
};
24+
return () => {
25+
navigator.clipboard.writeText = original;
26+
};
27+
}, [posthog]);
28+
29+
return null;
30+
}

docs/lib/providers/providers-wrapper.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React, { Suspense } from "react";
44
import { PostHogProvider } from "@/lib/providers/posthog-provider";
55
import { ScarfPixel } from "./scarf-pixel";
6+
import { CopyTracker } from "./copy-tracker";
67
import { useRB2B } from "@/lib/hooks/use-rb2b";
78
import { useGoogleAnalytics } from "../hooks/use-google-analytics";
89
import { ThemeOverride } from "@/components/theme-override";
@@ -13,7 +14,10 @@ export function ProvidersWrapper({ children }: { children: React.ReactNode }) {
1314

1415
return (
1516
<Suspense fallback={null}>
16-
<PostHogProvider>{children}</PostHogProvider>
17+
<PostHogProvider>
18+
<CopyTracker />
19+
{children}
20+
</PostHogProvider>
1721
<ThemeOverride />
1822
<ScarfPixel />
1923
</Suspense>

docs/lib/track-command-copy.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { PostHog } from "posthog-js";
2+
3+
const KNOWN_INSTALL_TYPES = [
4+
"npx",
5+
"npm",
6+
"pnpm",
7+
"yarn",
8+
"bun",
9+
"pip",
10+
"uv",
11+
"poetry",
12+
"cargo",
13+
"go",
14+
"docker",
15+
"curl",
16+
"brew",
17+
] as const;
18+
19+
export type InstallType = (typeof KNOWN_INSTALL_TYPES)[number] | "code";
20+
21+
const MAX_COMMAND_LENGTH = 240;
22+
23+
function inferInstallType(command: string): InstallType {
24+
const firstToken = command.trim().split(/\s+/)[0]?.toLowerCase();
25+
if (!firstToken) return "code";
26+
return (KNOWN_INSTALL_TYPES as readonly string[]).includes(firstToken)
27+
? (firstToken as InstallType)
28+
: "code";
29+
}
30+
31+
export type TrackCommandCopyArgs = {
32+
command: string;
33+
product?: string;
34+
location?: string;
35+
};
36+
37+
export function trackCommandCopy(
38+
posthog: PostHog | undefined,
39+
{ command, product, location }: TrackCommandCopyArgs,
40+
) {
41+
if (!posthog) return;
42+
const trimmed = command.trim();
43+
if (!trimmed) return;
44+
const truncated =
45+
trimmed.length > MAX_COMMAND_LENGTH
46+
? trimmed.slice(0, MAX_COMMAND_LENGTH) + "…"
47+
: trimmed;
48+
posthog.capture("cli_command_copied", {
49+
command: truncated,
50+
install_type: inferInstallType(trimmed),
51+
...(product ? { product } : {}),
52+
...(location ? { location } : {}),
53+
});
54+
}

0 commit comments

Comments
 (0)