Skip to content

Commit adf6ec1

Browse files
committed
feat: track workflow search context
1 parent e2f3c50 commit adf6ec1

3 files changed

Lines changed: 110 additions & 7 deletions

File tree

site/src/components/hub/SearchPopover.vue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ const visibleBadgesDesktop = computed(() => store.filterBadges.value.slice(0, MA
7979
const overflowCountDesktop = computed(() =>
8080
Math.max(0, store.filterBadges.value.length - MAX_BADGES_DESKTOP)
8181
);
82+
const activeFilters = computed(() =>
83+
store.filterBadges.value.map((badge) => `${badge.type}:${badge.value}`)
84+
);
8285
8386
// ── All tags and models with counts (for filter suggestions) ──
8487
@@ -186,7 +189,7 @@ const badgeOnlyResults = computed(() => {
186189
// ── Search with debounce ──
187190
188191
watchDebounced(
189-
[searchQuery, () => store.filterBadges.value],
192+
[searchQuery, activeFilters],
190193
async ([query]) => {
191194
const trimmed = (query as string).trim();
192195
if (!trimmed) {
@@ -195,17 +198,24 @@ watchDebounced(
195198
return;
196199
}
197200
isSearching.value = true;
201+
const filtersApplied = [...activeFilters.value];
198202
try {
199-
searchResults.value = await searchIndex(trimmed, {
203+
const results = await searchIndex(trimmed, {
200204
allowedNames: badgeFilteredNames.value ?? undefined,
201205
});
202-
trackSearchPerformed(trimmed);
206+
searchResults.value = results;
207+
const resultCount = results.workflows.length + matchedCreators.value.length;
208+
trackSearchPerformed({
209+
query: trimmed,
210+
resultCount,
211+
filtersApplied,
212+
});
203213
} finally {
204214
isSearching.value = false;
205215
hasSearched.value = true;
206216
}
207217
},
208-
{ debounce: 200 }
218+
{ debounce: 500 }
209219
);
210220
211221
// ── Displayed results (combines badge-only and text search) ──

site/src/lib/posthog.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ export function initPostHog(): void {
2727
* - past tense verbs
2828
* - e.g. hub:run_button_clicked, hub:template_viewed
2929
*/
30-
type EventProperties = Record<string, string | number | boolean | undefined>;
30+
type EventPropertyValue = string | number | boolean | string[] | undefined;
31+
type EventProperties = Record<string, EventPropertyValue>;
32+
33+
export interface SearchPerformedProperties {
34+
query: string;
35+
resultCount: number;
36+
filtersApplied: string[];
37+
}
3138

3239
export function capture(eventName: string, properties?: EventProperties): void {
3340
if (typeof window === 'undefined' || !initialized) return;
@@ -74,9 +81,19 @@ export function trackTemplateViewed(
7481
});
7582
}
7683

77-
export function trackSearchPerformed(query: string): void {
84+
export function trackSearchPerformed({
85+
query,
86+
resultCount,
87+
filtersApplied,
88+
}: SearchPerformedProperties): void {
89+
const trimmedQuery = query.trim();
90+
if (!trimmedQuery) return;
91+
7892
capture('hub:search_performed', {
79-
query,
93+
query: trimmedQuery,
94+
result_count: resultCount,
95+
surface: 'workflows',
96+
filters_applied: filtersApplied,
8097
});
8198
}
8299

site/tests/unit/posthog.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
const posthogMock = vi.hoisted(() => ({
4+
init: vi.fn(),
5+
capture: vi.fn(),
6+
}));
7+
8+
vi.mock('posthog-js', () => ({
9+
default: posthogMock,
10+
}));
11+
12+
async function loadPostHog() {
13+
vi.resetModules();
14+
vi.stubGlobal('window', {});
15+
vi.stubEnv('PUBLIC_POSTHOG_KEY', 'phc_test');
16+
return import('../../src/lib/posthog');
17+
}
18+
19+
afterEach(() => {
20+
posthogMock.init.mockReset();
21+
posthogMock.capture.mockReset();
22+
vi.unstubAllEnvs();
23+
vi.unstubAllGlobals();
24+
vi.resetModules();
25+
});
26+
27+
describe('trackSearchPerformed', () => {
28+
it('tracks workflow search metadata', async () => {
29+
const { initPostHog, trackSearchPerformed } = await loadPostHog();
30+
31+
initPostHog();
32+
trackSearchPerformed({
33+
query: ' flux ',
34+
resultCount: 12,
35+
filtersApplied: ['model:Flux', 'mode:app'],
36+
});
37+
38+
expect(posthogMock.capture).toHaveBeenCalledWith('hub:search_performed', {
39+
query: 'flux',
40+
result_count: 12,
41+
surface: 'workflows',
42+
filters_applied: ['model:Flux', 'mode:app'],
43+
});
44+
});
45+
46+
it('tracks zero-result searches', async () => {
47+
const { initPostHog, trackSearchPerformed } = await loadPostHog();
48+
49+
initPostHog();
50+
trackSearchPerformed({
51+
query: 'nonexistent workflow',
52+
resultCount: 0,
53+
filtersApplied: [],
54+
});
55+
56+
expect(posthogMock.capture).toHaveBeenCalledWith('hub:search_performed', {
57+
query: 'nonexistent workflow',
58+
result_count: 0,
59+
surface: 'workflows',
60+
filters_applied: [],
61+
});
62+
});
63+
64+
it('skips empty queries', async () => {
65+
const { initPostHog, trackSearchPerformed } = await loadPostHog();
66+
67+
initPostHog();
68+
trackSearchPerformed({
69+
query: ' ',
70+
resultCount: 1,
71+
filtersApplied: ['tag:image'],
72+
});
73+
74+
expect(posthogMock.capture).not.toHaveBeenCalled();
75+
});
76+
});

0 commit comments

Comments
 (0)