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
| Field | Type | Notes |
|---|---|---|
query | string | The search string entered |
resultCount | int | 0 = nothing found; low counts = poor coverage |
tenantId | string | For filtering by tenant type / plan tier |
portal | enum | dashboard / dm / manage |
createdAt | datetime | For 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
| File | Change |
|---|---|
packages/db/prisma/schema.prisma | Add HelpSearchEvent model + HelpPortal enum |
packages/ui/src/HelpCenter.tsx | Add onSearch callback prop; fire after 600ms debounce |
apps/api/src/routers/help.ts | Add POST /tenant/v1/help/search-event handler |
apps/dashboard/src/app/(dashboard)/help/page.tsx | Convert to client component; pass onSearch handler |