Skip to content

Commit f6b4390

Browse files
committed
feat(api): improve insights engine from product evaluation
- Reweight priority scoring toward actionability × business impact (prompt + Zod) - Require prescriptive suggestions; forbid generic monitor/review fluff - Inject last 14d insight titles per website into the prompt to cut redundant cards - Document data boundaries (no invented funnel/MRR without metrics in query data) - Note living-dedupe limitation in databuddy codebase-map skill
1 parent 21e7451 commit f6b4390

2 files changed

Lines changed: 85 additions & 30 deletions

File tree

.agents/skills/databuddy/references/codebase-map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Use this file when the task spans multiple packages or when the right edit locat
1919

2020
- Elysia API service
2121
- Default dev port: `3001`
22+
- AI insights: [`apps/api/src/routes/insights.ts`](/Users/iza/Dev/Databuddy/apps/api/src/routes/insights.ts) — loads last ~14d insight titles per site into the model prompt to reduce repeat narratives; true “living” dedupe/merge would need schema or upsert work. Funnel/MRR/retention insights require those metrics in query data—do not invent in prompts alone.
2223
- Handles routes such as public endpoints, webhooks, health, query, MCP, and agent-related APIs
2324
- Typical work:
2425
- route handlers

apps/api/src/routes/insights.ts

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const TIMEOUT_MS = 60_000;
2828
const MAX_WEBSITES = 5;
2929
const CONCURRENCY = 3;
3030
const GENERATION_COOLDOWN_HOURS = 6;
31+
const RECENT_INSIGHTS_LOOKBACK_DAYS = 14;
32+
const RECENT_INSIGHTS_PROMPT_LIMIT = 12;
3133

3234
const insightSchema = z.object({
3335
title: z
@@ -38,12 +40,12 @@ const insightSchema = z.object({
3840
description: z
3941
.string()
4042
.describe(
41-
"2-3 sentences with specific numbers from BOTH periods. Always include the actual values and the delta."
43+
"2-3 sentences: what changed and why it might matter, with specific numbers from BOTH periods. Explain cause only when grounded in the data or annotations."
4244
),
4345
suggestion: z
4446
.string()
4547
.describe(
46-
'One concrete, specific action. Good: "Your /blog/seo-guide drove 40% of traffic - share it on social." Bad: "Monitor your traffic."'
48+
"Required prescriptive 'now what': one or two sentences telling the user what to do next (product, marketing, or ops). Tie to the metric (e.g. add a CTA, capture email from volatile channel, prioritize top error). Never generic: no 'monitor', 'keep watching', or 'consider reviewing' without a concrete step."
4749
),
4850
severity: z.enum(["critical", "warning", "info"]),
4951
sentiment: z
@@ -56,7 +58,7 @@ const insightSchema = z.object({
5658
.min(1)
5759
.max(10)
5860
.describe(
59-
"1-10. Errors affecting users = 8-10, significant traffic change = 5-7, stable/informational = 1-4"
61+
"1-10 from actionability × business impact, NOT raw % magnitude. User-facing errors, conversion/session drops, or reliability issues outrank vanity traffic spikes. A 5% drop in a meaningful engagement metric can score higher than a 70% visitor increase with no conversion context. Reserve 8-10 for issues that hurt users or revenue signals in the data."
6062
),
6163
type: z.enum([
6264
"error_spike",
@@ -84,7 +86,7 @@ const insightsOutputSchema = z.object({
8486
.array(insightSchema)
8587
.max(3)
8688
.describe(
87-
"1-3 insights ranked by surprise-factor x impact. Focus on what changed and why it matters."
89+
"1-3 insights ranked by actionability × business impact. Skip repeating a narrative already listed under recently reported insights unless the change is materially new."
8890
),
8991
});
9092

@@ -227,21 +229,71 @@ async function fetchRecentAnnotations(websiteId: string): Promise<string> {
227229
return `\n\nUser annotations (known events that may explain changes):\n${lines.join("\n")}`;
228230
}
229231

230-
const INSIGHTS_SYSTEM_PROMPT = `You are an analytics insights engine. Your job is to find the 1-3 most significant, actionable findings from week-over-week website data.
232+
async function fetchRecentInsightsForPrompt(
233+
organizationId: string,
234+
websiteId: string
235+
): Promise<string> {
236+
const since = dayjs().subtract(RECENT_INSIGHTS_LOOKBACK_DAYS, "day").toDate();
237+
238+
const rows = await db
239+
.select({
240+
title: analyticsInsights.title,
241+
type: analyticsInsights.type,
242+
createdAt: analyticsInsights.createdAt,
243+
})
244+
.from(analyticsInsights)
245+
.where(
246+
and(
247+
eq(analyticsInsights.organizationId, organizationId),
248+
eq(analyticsInsights.websiteId, websiteId),
249+
gte(analyticsInsights.createdAt, since)
250+
)
251+
)
252+
.orderBy(desc(analyticsInsights.createdAt))
253+
.limit(RECENT_INSIGHTS_PROMPT_LIMIT);
254+
255+
if (rows.length === 0) {
256+
return "";
257+
}
231258

232-
Significance thresholds:
233-
- Traffic (pageviews/visitors/sessions): <5% change = only mention if nothing else notable. 5-15% = worth noting. >15% = significant. >30% = critical.
259+
const lines = rows.map(
260+
(r) =>
261+
`- [${r.type}] ${r.title} (${dayjs(r.createdAt).format("YYYY-MM-DD")})`
262+
);
263+
264+
return `\n\n## Recently reported insights for this website (avoid repeating the same narrative unless something materially changed)\n${lines.join("\n")}`;
265+
}
266+
267+
const INSIGHTS_SYSTEM_PROMPT = `You are an analytics insights engine. Your job is to find the 1-3 most significant findings from week-over-week website data, written like an analyst: descriptive where needed, but every insight MUST include a prescriptive "so what / now what" in the suggestion field.
268+
269+
Priority scoring (priority 1-10):
270+
- Score by actionability × business impact, NOT by how large the percentage move is. Traffic spikes without conversion or outcome context are lower priority than errors, session/engagement collapses, or clear negative trends affecting users.
271+
- Operational health (errors, reliability) often matters more than vanity traffic growth. A moderate error-rate improvement during high traffic can be high value.
272+
- Do not assign 8-10 to pure volume spikes unless the data also shows a linked risk or opportunity worth acting on.
273+
274+
Significance thresholds (for what to mention):
275+
- Traffic (pageviews/visitors/sessions): <5% change = only mention if nothing else notable. 5-15% = worth noting. >15% = significant. >30% = notable volume change.
234276
- Errors: new error types = always report. Error rate up >0.5% = warning. Error rate up >2% = critical.
235277
- Bounce rate: change >5 percentage points = notable.
236278
- Pages: new page entering top 10 or page dropping out = notable. Individual page change >25% = significant.
237279
- Referrers: new source appearing or major source declining >20% = notable.
238280
281+
Anti-redundancy:
282+
- If the user message includes a "Recently reported insights" section, treat those as already surfaced. Do NOT output a new insight that tells the same story (same underlying signal and direction) unless the narrative would be materially different (e.g. new root cause, reversal, or threshold crossed). Prefer novel angles or omit.
283+
284+
Data boundaries:
285+
- Only use metrics present in the JSON (summary, pages, errors, referrers). Do not invent funnel conversion rates, MRR, revenue, cohort retention, or signup counts unless they appear in the data.
286+
- If conversion or goal data appears in summary_metrics, you may connect traffic to outcomes. If absent, do not fabricate funnel or revenue insights.
287+
288+
Suggestion field (required quality):
289+
- Must answer "what should we do next?" in one or two sentences: concrete product, marketing, or engineering action tied to the numbers.
290+
- Bad: "Monitor traffic", "Keep an eye on this", "Consider reviewing analytics."
291+
- Good: tie to pages, channels, CTAs, error classes, or experiments suggested by the data.
292+
239293
Rules:
240-
- Every insight MUST include specific numbers from both periods (e.g. "1,234 visitors, up from 987 last week")
241-
- Every suggestion MUST be a concrete next step, not generic advice
242-
- If annotations explain a change, mention it but still report the data
243-
- If everything is stable, return ONE positive/neutral insight (e.g. "Steady at 2,400 weekly visitors")
244-
- Rank by surprise-factor x business-impact
294+
- Every insight MUST include specific numbers from both periods where applicable.
295+
- If annotations explain a change, mention it but still report the data.
296+
- If everything is stable, return ONE positive/neutral insight (e.g. "Steady at 2,400 weekly visitors") with a light suggestion if appropriate.
245297
- Never fabricate or round numbers beyond what's in the data`;
246298

247299
async function analyzeWebsite(
@@ -255,23 +307,25 @@ async function analyzeWebsite(
255307
const currentRange = period.current;
256308
const previousRange = period.previous;
257309

258-
const [current, previous, annotationContext] = await Promise.all([
259-
fetchPeriodData(
260-
websiteId,
261-
domain,
262-
currentRange.from,
263-
currentRange.to,
264-
timezone
265-
),
266-
fetchPeriodData(
267-
websiteId,
268-
domain,
269-
previousRange.from,
270-
previousRange.to,
271-
timezone
272-
),
273-
fetchRecentAnnotations(websiteId),
274-
]);
310+
const [current, previous, annotationContext, recentInsightsBlock] =
311+
await Promise.all([
312+
fetchPeriodData(
313+
websiteId,
314+
domain,
315+
currentRange.from,
316+
currentRange.to,
317+
timezone
318+
),
319+
fetchPeriodData(
320+
websiteId,
321+
domain,
322+
previousRange.from,
323+
previousRange.to,
324+
timezone
325+
),
326+
fetchRecentAnnotations(websiteId),
327+
fetchRecentInsightsForPrompt(organizationId, websiteId),
328+
]);
275329

276330
const hasData = current.summary.length > 0 || current.topPages.length > 0;
277331
if (!hasData) {
@@ -285,7 +339,7 @@ async function analyzeWebsite(
285339
previousRange
286340
);
287341

288-
const prompt = `Analyze this website's week-over-week data and return insights.\n\n${dataSection}${annotationContext}`;
342+
const prompt = `Analyze this website's week-over-week data and return insights.\n\n${dataSection}${annotationContext}${recentInsightsBlock}`;
289343

290344
try {
291345
const result = await generateText({

0 commit comments

Comments
 (0)