feat(insights): mechanical HYPOTHESES from custom events

The HYPOTHESES block was empty on most sessions because the only rules
covered backend failure patterns (5xx + rage). Add 7 behavioral patterns
derived from canonical custom events — these give the LLM analyzer
concrete starting points instead of reasoning from raw timeline alone.

Patterns (all conservative — fire only on clear evidence):
1. Catalog evaluated, no checkout (parts_panel_viewed>=3 + on upgrade
   page + !checkout_started)
2. Fresh trial + successful decode (happy onboarding)
3. VIN decode failure pattern (failures>=2 or fallback>=1)
4. Rage click on subscription UI (upgrade friction)
5. Cancel flow entered without resume (churn risk)
6. Trial urgency banner ignored (banner_viewed without CTA)
7. Search opened but no decode attempted (funnel drop)
This commit is contained in:
Semih
2026-05-20 17:09:26 +03:00
parent 4984190273
commit 94f279665c

View File

@@ -259,6 +259,59 @@ export function compressSnapshots(
hypotheses.push("Frontend error + retry pattern suggests silent failure (no error toast)");
}
// Behavioral hypotheses from canonical custom events. Conservative — only
// fire when the pattern is clear. The LLM analyzer takes these as starting
// points; vague guesses would dilute its prompt.
const eventCounts: Record<string, number> = {};
for (const c of customEvents) eventCounts[c.name] = (eventCounts[c.name] ?? 0) + 1;
const has = (n: string) => (eventCounts[n] ?? 0) > 0;
const count = (n: string) => eventCounts[n] ?? 0;
const startUrl = header.startUrl ?? "";
const onSubscriptionPage = /\/(dashboard\/)?subscription|\/upgrade|\/billing/i.test(startUrl);
const hasPayment = has("payment_success") || has("payment_succeeded") || has("payment_initiated");
const hasCheckout = has("checkout_started");
const hasTrial = has("trial_started");
// Pattern 1: Evaluated the catalog deeply on the upgrade path but never started checkout.
if (count("parts_panel_viewed") >= 3 && onSubscriptionPage && !hasCheckout && !hasPayment) {
hypotheses.push(
`Catalog evaluated (${count("parts_panel_viewed")}x parts_panel_viewed) on upgrade page — no checkout initiated`,
);
}
// Pattern 2: Fresh trial + immediate decode success — happy path.
if (hasTrial && (has("vin_decode_succeeded") || has("vin_decoded"))) {
hypotheses.push("Fresh trial activated and decoded a VIN successfully — onboarding succeeded");
}
// Pattern 3: VIN decode failure or provider fallback — investigate upstream.
const vinFailures = count("vin_decode_failed") + count("vin_decode_error");
if (vinFailures >= 2 || count("provider_fallback_triggered") >= 1) {
hypotheses.push(
`VIN decode failure pattern (failures=${vinFailures}, fallbacks=${count("provider_fallback_triggered")}) — check provider health`,
);
}
// Pattern 4: Rage click on subscription/billing UI — friction in upgrade flow.
if (rageEmitted && onSubscriptionPage) {
hypotheses.push("Rage click on subscription UI — friction or unresponsive element in upgrade flow");
}
// Pattern 5: Entered cancel flow and didn't resume.
if ((has("cancel_flow_viewed") || has("cancel_save_clicked")) && !has("subscription_resumed")) {
hypotheses.push("Entered cancellation flow without resuming — churn risk");
}
// Pattern 6: Trial urgency banner shown but ignored.
if (has("trial_urgency_banner_viewed") && !has("trial_urgency_banner_cta_clicked")) {
hypotheses.push("Trial urgency banner shown but CTA not clicked — banner ignored");
}
// Pattern 7: Search opened but never decoded — funnel drop at the search step.
if (count("search_input_focused") >= 1 && !has("vin_decode_succeeded") && !has("vin_decoded") && !has("vin_decode_initiated")) {
hypotheses.push("Search opened but no VIN decode attempted — input abandoned");
}
const header_text = [
"=== SESSION ===",
`id: ${header.sessionId}`,