Skip to content

Commit 1b0c994

Browse files
devakoneclaude
andcommitted
feat: add analytics tracking for key user actions
- Enable formSubmissions tracking in Plausible config - Track repo connect/disconnect/analyze in ReposClient - Track all share actions (copy, social, download) in ShareActions - Track public profile enable/disable in PublicProfileSettings Events tracked: - repo_connect, repo_disconnect, repo_analyze - profile_share (with method: twitter, linkedin, etc.) - profile_download_image (with format: og, square, story) - public_profile_enabled, public_profile_disabled Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bd3a774 commit 1b0c994

4 files changed

Lines changed: 26 additions & 2 deletions

File tree

apps/web/src/app/repos/ReposClient.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
55
import { ChevronsUpDown, RefreshCw, Trash2 } from "lucide-react";
66
import { cn } from "@/lib/utils";
77
import { wrappedTheme } from "@/lib/theme";
8+
import { trackEvent, AnalyticsEvents } from "@/lib/analytics";
89
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
910
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
1011
import { ToastAction } from "@/components/ui/toast";
@@ -158,6 +159,7 @@ export default function ReposClient({
158159
}
159160

160161
toast({ title: "Repo added", description: `${repo.fullName} is now connected.` });
162+
trackEvent(AnalyticsEvents.REPO_CONNECT, { platform: repo.platform });
161163
if (options?.startVibe && body.repo_id) {
162164
await startAnalysis(body.repo_id, repo.fullName);
163165
}
@@ -195,6 +197,7 @@ export default function ReposClient({
195197
</ToastAction>
196198
),
197199
});
200+
trackEvent(AnalyticsEvents.REPO_ANALYZE);
198201
} catch (error) {
199202
toast({
200203
variant: "destructive",
@@ -225,6 +228,7 @@ export default function ReposClient({
225228
}
226229

227230
toast({ title: "Repo disconnected", description: `${repoName} has been removed.` });
231+
trackEvent(AnalyticsEvents.REPO_DISCONNECT);
228232
router.refresh();
229233
} catch (error) {
230234
toast({

apps/web/src/components/PlausibleProvider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ function PlausibleTracker() {
3232
outboundLinks: true,
3333
// Track file downloads
3434
fileDownloads: true,
35+
// Track form submissions
36+
formSubmissions: true,
3537
// Disable auto capture - we handle it manually for Next.js App Router
3638
autoCapturePageviews: false,
3739
});

apps/web/src/components/settings/PublicProfileSettings.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState, useEffect } from "react";
44
import type { PublicProfileSettings } from "@/types/public-profile";
55
import { DEFAULT_PUBLIC_PROFILE_SETTINGS } from "@/types/public-profile";
6+
import { trackEvent, AnalyticsEvents } from "@/lib/analytics";
67

78
interface ToggleItem {
89
key: keyof PublicProfileSettings;
@@ -93,6 +94,12 @@ export function PublicProfileSettingsPanel() {
9394
if (res.ok) {
9495
setSuccess("Settings saved!");
9596
setDirty(false);
97+
// Track profile visibility change
98+
if (settings.profile_enabled) {
99+
trackEvent(AnalyticsEvents.PUBLIC_PROFILE_ENABLED);
100+
} else {
101+
trackEvent(AnalyticsEvents.PUBLIC_PROFILE_DISABLED);
102+
}
96103
setTimeout(() => setSuccess(null), 3000);
97104
} else {
98105
const data = await res.json();

apps/web/src/components/share/ShareActions.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState, useEffect } from "react";
44
import { Copy, Check, Link2, Share2, Download, FileText } from "lucide-react";
55
import { SHARE_FORMATS, downloadBlob } from "./share-image";
6+
import { trackEvent, AnalyticsEvents } from "@/lib/analytics";
67
import type { ShareActionsProps, ShareFormat } from "./types";
78

89
// Brand icons (Lucide doesn't include these)
@@ -70,6 +71,7 @@ export function ShareActions({
7071
try {
7172
await navigator.clipboard.writeText(shareText);
7273
setCopied(true);
74+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "copy_text" });
7375
setTimeout(() => setCopied(false), 2000);
7476
} catch {
7577
setCopied(false);
@@ -81,6 +83,7 @@ export function ShareActions({
8183
try {
8284
await navigator.clipboard.writeText(shareUrl);
8385
setCopiedLink(true);
86+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "copy_link" });
8487
setTimeout(() => setCopiedLink(false), 2000);
8588
} catch {
8689
setCopiedLink(false);
@@ -96,6 +99,7 @@ export function ShareActions({
9699
const u = new URL("https://twitter.com/intent/tweet");
97100
u.searchParams.set("text", shareCaption);
98101
u.searchParams.set("url", shareUrl);
102+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "twitter" });
99103
openSharePopup(u.toString());
100104
};
101105

@@ -104,13 +108,15 @@ export function ShareActions({
104108
const u = new URL("https://www.facebook.com/sharer/sharer.php");
105109
u.searchParams.set("u", shareUrl);
106110
if (shareCaption) u.searchParams.set("quote", shareCaption);
111+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "facebook" });
107112
openSharePopup(u.toString());
108113
};
109114

110115
const handleShareLinkedIn = () => {
111116
if (!shareUrl) return;
112117
const u = new URL("https://www.linkedin.com/sharing/share-offsite/");
113118
u.searchParams.set("url", shareUrl);
119+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "linkedin" });
114120
openSharePopup(u.toString());
115121
};
116122

@@ -119,13 +125,15 @@ export function ShareActions({
119125
const u = new URL("https://www.reddit.com/submit");
120126
u.searchParams.set("url", shareUrl);
121127
if (shareHeadline) u.searchParams.set("title", shareHeadline);
128+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "reddit" });
122129
openSharePopup(u.toString());
123130
};
124131

125132
const handleShareWhatsApp = () => {
126133
if (!shareUrl) return;
127134
const u = new URL("https://wa.me/");
128135
u.searchParams.set("text", `${shareCaption}\n${shareUrl}`.trim());
136+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "whatsapp" });
129137
openSharePopup(u.toString());
130138
};
131139

@@ -158,12 +166,13 @@ export function ShareActions({
158166
files: [file],
159167
title: shareHeadline,
160168
// Some platforms ignore text if files are present, but good to include
161-
text: shareCaption,
169+
text: shareCaption,
162170
// Note: Including URL with files is often flaky on Android/iOS (sometimes creates a link card instead of image).
163171
// We prioritize the IMAGE here as per user request ("share that [the image]").
164172
// If you really need the URL, append it to text.
165-
// url: shareUrl,
173+
// url: shareUrl,
166174
});
175+
trackEvent(AnalyticsEvents.PROFILE_SHARE, { method: "native_share" });
167176
} else {
168177
// Fallback if file sharing not supported
169178
await navigator.share({
@@ -200,6 +209,7 @@ export function ShareActions({
200209
}
201210
const blob = await res.blob();
202211
downloadBlob(blob, `vcp-${entityId}-${shareFormat}.png`);
212+
trackEvent(AnalyticsEvents.PROFILE_DOWNLOAD_IMAGE, { format: shareFormat });
203213
console.log("PNG download complete");
204214
} catch (e) {
205215
console.error("PNG download failed:", e);
@@ -220,6 +230,7 @@ export function ShareActions({
220230
if (!res.ok) throw new Error("story_failed");
221231
const blob = await res.blob();
222232
downloadBlob(blob, `vcp-${entityId}-story.png`);
233+
trackEvent(AnalyticsEvents.PROFILE_DOWNLOAD_IMAGE, { format: "story" });
223234
} catch (e) {
224235
console.error("Story download failed:", e);
225236
setDownloadError(e instanceof Error ? e.message : "story_download_failed");

0 commit comments

Comments
 (0)