Skip to content

Commit e2c26fa

Browse files
feat(analytics): enhance Plausible tracking with journey context (#3169)
## Summary - **User journey tracking**: Added `page` property to `copy_code` and `download_image` events to distinguish where users perform actions (`home`, `spec_overview`, `spec_detail`) - **New events**: `toggle_grid_size` (view preference), `filter_remove` (filter removal tracking) - **Bug fix**: Added missing `spec` property to SpecTabs `copy_code` event - **Faster tracking**: Reduced debounce times for better event capture - `pageview`: 300ms → 150ms - `search_no_results`: 500ms → 200ms - **Documentation**: Comprehensive Plausible setup guide with custom properties list ## Changes | File | Change | |------|--------| | `useAnalytics.ts` | Reduced pageview debounce | | `FilterBar.tsx` | Added `toggle_grid_size`, reduced search debounce | | `ImageCard.tsx` | Added `page: 'home'` to copy_code | | `SpecPage.tsx` | Added `page` to copy_code and download_image | | `SpecTabs.tsx` | Added `spec` and `page` to copy_code | | `useFilterState.ts` | Added `filter_remove` event | | `docs/architecture/plausible.md` | Full documentation with Plausible dashboard setup | ## Plausible Dashboard Setup Required After merge, register these **custom properties** in Plausible: ``` spec, library, method, page, category, value, query, destination, tab, size ``` ## Test plan - [ ] Verify copy_code events include `page` property in different contexts - [ ] Verify toggle_grid_size fires on view toggle - [ ] Verify filter_remove fires on filter removal - [ ] Check Plausible dashboard receives events correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec44f62 commit e2c26fa

File tree

7 files changed

+387
-16
lines changed

7 files changed

+387
-16
lines changed

app/src/components/FilterBar.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export function FilterBar({
221221
const timer = setTimeout(() => {
222222
onTrackEvent('search_no_results', { query });
223223
lastTrackedQueryRef.current = query;
224-
}, 500);
224+
}, 200);
225225
return () => clearTimeout(timer);
226226
}
227227
}, [searchQuery, searchResults.length, onTrackEvent]);
@@ -378,7 +378,11 @@ export function FilterBar({
378378
{/* Grid size toggle */}
379379
<Tooltip title={imageSize === 'normal' ? 'compact view' : 'normal view'}>
380380
<Box
381-
onClick={() => onImageSizeChange(imageSize === 'normal' ? 'compact' : 'normal')}
381+
onClick={() => {
382+
const newSize = imageSize === 'normal' ? 'compact' : 'normal';
383+
onImageSizeChange(newSize);
384+
onTrackEvent('toggle_grid_size', { size: newSize });
385+
}}
382386
sx={{
383387
display: 'flex',
384388
alignItems: 'center',
@@ -588,7 +592,11 @@ export function FilterBar({
588592
{/* Grid size toggle */}
589593
<Tooltip title={imageSize === 'normal' ? 'compact view' : 'normal view'}>
590594
<Box
591-
onClick={() => onImageSizeChange(imageSize === 'normal' ? 'compact' : 'normal')}
595+
onClick={() => {
596+
const newSize = imageSize === 'normal' ? 'compact' : 'normal';
597+
onImageSizeChange(newSize);
598+
onTrackEvent('toggle_grid_size', { size: newSize });
599+
}}
592600
sx={{
593601
display: 'flex',
594602
alignItems: 'center',

app/src/components/ImageCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const ImageCard = memo(function ImageCard({
8989
if (code) {
9090
await navigator.clipboard.writeText(code);
9191
setCopyState('copied');
92-
onTrackEvent?.('copy_code', { spec: image.spec_id, library: image.library, method: 'card' });
92+
onTrackEvent?.('copy_code', { spec: image.spec_id, library: image.library, method: 'card', page: 'home' });
9393
setTimeout(() => setCopyState('idle'), 2000);
9494
} else {
9595
setCopyState('idle');

app/src/components/SpecTabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export function SpecTabs({
164164
try {
165165
await navigator.clipboard.writeText(code);
166166
setCopied(true);
167-
onTrackEvent?.('copy_code', { library: libraryId, method: 'tab' });
167+
onTrackEvent?.('copy_code', { spec: specId, library: libraryId, method: 'tab', page: 'spec_detail' });
168168
setTimeout(() => setCopied(false), 2000);
169169
} catch (err) {
170170
console.error('Copy failed:', err);

app/src/hooks/useAnalytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function useAnalytics() {
6464
[isProduction]
6565
);
6666

67-
const trackPageview = useMemo(() => debounce(sendPageview, 300), [sendPageview]);
67+
const trackPageview = useMemo(() => debounce(sendPageview, 150), [sendPageview]);
6868

6969
const trackEvent = useCallback(
7070
(name: string, props?: EventProps) => {

app/src/hooks/useFilterState.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,24 +210,32 @@ export function useFilterState({
210210

211211
// Remove a filter value from a specific group
212212
const handleRemoveFilter = useCallback((groupIndex: number, value: string) => {
213+
const group = activeFiltersRef.current[groupIndex];
214+
if (group) {
215+
onTrackEvent('filter_remove', { category: group.category, value });
216+
}
213217
setActiveFilters((prev) => {
214218
const newFilters = [...prev];
215-
const group = newFilters[groupIndex];
216-
if (!group) return prev;
219+
const grp = newFilters[groupIndex];
220+
if (!grp) return prev;
217221

218-
const updatedValues = group.values.filter((v) => v !== value);
222+
const updatedValues = grp.values.filter((v) => v !== value);
219223
if (updatedValues.length === 0) {
220224
return newFilters.filter((_, i) => i !== groupIndex);
221225
}
222-
newFilters[groupIndex] = { ...group, values: updatedValues };
226+
newFilters[groupIndex] = { ...grp, values: updatedValues };
223227
return newFilters;
224228
});
225-
}, []);
229+
}, [onTrackEvent]);
226230

227231
// Remove entire group by index
228232
const handleRemoveGroup = useCallback((groupIndex: number) => {
233+
const group = activeFiltersRef.current[groupIndex];
234+
if (group) {
235+
onTrackEvent('filter_remove', { category: group.category, value: group.values.join(',') });
236+
}
229237
setActiveFilters((prev) => prev.filter((_, i) => i !== groupIndex));
230-
}, []);
238+
}, [onTrackEvent]);
231239

232240
// Random filter - replaces last filter slot (or adds first one)
233241
const handleRandom = useCallback(

app/src/pages/SpecPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ export function SpecPage() {
156156
link.href = impl.preview_url;
157157
link.download = `${specId}-${impl.library_id}.png`;
158158
link.click();
159-
trackEvent('download_image', { spec: specId, library: impl.library_id });
159+
trackEvent('download_image', { spec: specId, library: impl.library_id, page: isOverviewMode ? 'spec_overview' : 'spec_detail' });
160160
},
161-
[specId, trackEvent]
161+
[specId, trackEvent, isOverviewMode]
162162
);
163163

164164
// Handle copy code (works for both overview and detail mode)
@@ -168,13 +168,13 @@ export function SpecPage() {
168168
try {
169169
await navigator.clipboard.writeText(impl.code);
170170
setCodeCopied(impl.library_id);
171-
trackEvent('copy_code', { spec: specId, library: impl.library_id, method: 'image' });
171+
trackEvent('copy_code', { spec: specId, library: impl.library_id, method: 'image', page: isOverviewMode ? 'spec_overview' : 'spec_detail' });
172172
setTimeout(() => setCodeCopied(null), 2000);
173173
} catch (err) {
174174
console.error('Copy failed:', err);
175175
}
176176
},
177-
[specId, trackEvent]
177+
[specId, trackEvent, isOverviewMode]
178178
);
179179

180180
// Track page view

0 commit comments

Comments
 (0)