Skip to Content
HelpHelp Improvement 7 — Search Analytics

Help Improvement 7 — Search Analytics

Status: [To Build]

Purpose: Record every help search query so we can see what users look for and fail to find. Zero data is collected today — this adds a passive collection layer with no visible UI change.


What to Collect

FieldTypeNotes
querystringThe search string entered
resultCountint0 = nothing found; low counts = poor coverage
tenantIdstringFor filtering by tenant type / plan tier
portalenumdashboard / dm / manage
createdAtdatetimeFor time-series trends

A resultCount of 0 is the most actionable signal — it means a user searched for something and the help system had nothing to show. Aggregating zero-result queries gives a direct backlog for new help content.


Data Model

model HelpSearchEvent { id String @id @default(cuid()) query String resultCount Int portal HelpPortal tenantId String createdAt DateTime @default(now()) tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@index([createdAt]) @@index([resultCount]) @@map("help_search_event") } enum HelpPortal { dashboard dm manage }

API Endpoint

POST /tenant/v1/help/search-event

Fire-and-forget — the client does not await the response for rendering.

// Request { query: string; resultCount: number; portal: "dashboard" | "dm" | "manage" } // Response { ok: true }
// apps/api/src/routers/help.ts fastify.post("/tenant/v1/help/search-event", { preHandler: [requireTenantUser], schema: { body: { type: "object", required: ["query", "resultCount", "portal"], properties: { query: { type: "string", maxLength: 200 }, resultCount: { type: "integer", minimum: 0 }, portal: { type: "string", enum: ["dashboard", "dm", "manage"] }, }, }, response: { 200: { type: "object", additionalProperties: true } }, }, async handler(req, reply) { const { query, resultCount, portal } = req.body as { query: string; resultCount: number; portal: "dashboard" | "dm" | "manage" }; await db.helpSearchEvent.create({ data: { query, resultCount, portal, tenantId: req.user.tenantId }, }); reply.send({ ok: true }); }, });

Client Integration

In HelpCenter, fire the event after a 600ms debounce (so we record a “settled” query, not every keystroke):

// packages/ui/src/HelpCenter.tsx interface HelpCenterProps { pages: HelpPageData[]; basePath?: string; onSearch?: (query: string, resultCount: number) => void; // ← new callback } // In the search effect: useEffect(() => { if (!query.trim() || !onSearch) return; const t = setTimeout(() => { onSearch(query.trim(), filtered?.length ?? 0); }, 600); return () => clearTimeout(t); }, [query, filtered, onSearch]);

The dashboard passes onSearch from a client wrapper:

// apps/dashboard/src/app/(dashboard)/help/page.tsx (client wrapper) "use client"; import { HelpCenter } from "@leadmetrics/ui"; import { dashboardHelpPages } from "./_data"; import { apiFetch } from "@/lib/api-fetch"; export default function HelpCenterPage() { async function handleSearch(query: string, resultCount: number) { // fire-and-forget — no await apiFetch("/tenant/v1/help/search-event", { method: "POST", body: JSON.stringify({ query, resultCount, portal: "dashboard" }), }).catch(() => {}); } return <HelpCenter pages={dashboardHelpPages} onSearch={handleSearch} />; }

Querying the Data (No UI Required Yet)

These SQL queries answer the most useful questions directly:

-- Top zero-result queries (content gaps) SELECT query, COUNT(*) AS searches FROM help_search_event WHERE result_count = 0 GROUP BY query ORDER BY searches DESC LIMIT 20; -- Most searched topics overall SELECT query, AVG(result_count) AS avg_results, COUNT(*) AS searches FROM help_search_event GROUP BY query ORDER BY searches DESC LIMIT 20; -- Search volume by day SELECT DATE(created_at) AS day, COUNT(*) AS searches FROM help_search_event GROUP BY day ORDER BY day DESC;

These can be run directly against the production DB when needed. A manage portal analytics tab can be added later once patterns emerge from real data.


Affected Files

FileChange
packages/db/prisma/schema.prismaAdd HelpSearchEvent model + HelpPortal enum
packages/ui/src/HelpCenter.tsxAdd onSearch callback prop; fire after 600ms debounce
apps/api/src/routers/help.tsAdd POST /tenant/v1/help/search-event handler
apps/dashboard/src/app/(dashboard)/help/page.tsxConvert to client component; pass onSearch handler

© 2026 Leadmetrics — Internal use only