From e7af5fae1c8991e68968e35b7d7c43c609e94b25 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Fri, 29 May 2026 12:08:14 +0300 Subject: [PATCH 1/3] feat(insights): inject site capabilities into investigation prompt Query custom events, errors, vitals, funnels, and goals upfront and include them in the prompt so the agent knows what data sources exist before making tool calls. Prevents wasted queries on unconfigured features (revenue, custom events on sites without them). --- apps/insights/src/generation.ts | 35 +++++---- apps/insights/src/prompts.ts | 130 +++++++++++++++++++++++++++++++- packages/evals/ui/index.html | 22 +++--- 3 files changed, 162 insertions(+), 25 deletions(-) diff --git a/apps/insights/src/generation.ts b/apps/insights/src/generation.ts index 427115689..b02742417 100644 --- a/apps/insights/src/generation.ts +++ b/apps/insights/src/generation.ts @@ -48,8 +48,9 @@ import { buildInvestigationPrompt, buildSystemPrompt, fetchDismissedPatterns, - fetchRecentAnnotations, fetchInsightHistory, + fetchRecentAnnotations, + fetchSiteCapabilities, formatOrgWebsitesContext, type OrgWebsiteRow, } from "./prompts"; @@ -295,17 +296,24 @@ async function analyzeWebsite(params: { const investigationMode = enrichedSignals.length > 0; - const [annotationContext, historyBlock, siteContext, dismissedBlock] = - await Promise.all([ - fetchRecentAnnotations(params.websiteId, params.config), - fetchInsightHistory( - params.organizationId, - params.websiteId, - params.config - ), - getCachedSiteContext(params.domain), - fetchDismissedPatterns(params.organizationId, params.websiteId), - ]); + const [ + annotationContext, + historyBlock, + siteContext, + dismissedBlock, + capabilitiesBlock, + ] = await Promise.all([ + fetchRecentAnnotations(params.websiteId, params.config), + fetchInsightHistory(params.organizationId, params.websiteId, params.config), + getCachedSiteContext(params.domain), + fetchDismissedPatterns(params.organizationId, params.websiteId), + fetchSiteCapabilities( + params.websiteId, + params.config.timezone, + currentRange.from, + currentRange.to + ), + ]); const allowedTools = normalizeAllowedTools(params.config.allowedTools); const orgContext = formatOrgWebsitesContext( @@ -324,10 +332,11 @@ async function analyzeWebsite(params: { historyBlock, annotationContext, dismissedBlock, + capabilitiesBlock, orgContext, siteContext: siteBlock, }) - : `Analyze ${params.domain} (${currentRange.from} to ${currentRange.to} vs ${previousRange.from} to ${previousRange.to}, ${params.config.timezone}). Use web_metrics with period="both" to compare periods efficiently.${siteBlock} + : `Analyze ${params.domain} (${currentRange.from} to ${currentRange.to} vs ${previousRange.from} to ${previousRange.to}, ${params.config.timezone}). Use web_metrics with period="both" to compare periods efficiently.${siteBlock}${capabilitiesBlock} ${orgContext}${annotationContext}${historyBlock}${dismissedBlock}`; const { tools: analyticsTools } = createInsightsAgentTools({ diff --git a/apps/insights/src/prompts.ts b/apps/insights/src/prompts.ts index e243cdd61..01f72cc18 100644 --- a/apps/insights/src/prompts.ts +++ b/apps/insights/src/prompts.ts @@ -1,8 +1,11 @@ import type { WeekOverWeekPeriod } from "@databuddy/ai/insights/types"; +import { executeQuery } from "@databuddy/ai/query"; import { and, db, desc, eq, gte, isNull } from "@databuddy/db"; import { analyticsInsights, annotations, + funnelDefinitions, + goals, type InsightGenerationConfigSnapshot, insightUserFeedback, } from "@databuddy/db/schema"; @@ -11,6 +14,130 @@ import type { EnrichedSignal } from "./enrichment"; const RECENT_INSIGHTS_PROMPT_LIMIT = 12; +export async function fetchSiteCapabilities( + websiteId: string, + timezone: string, + from: string, + to: string +): Promise { + const [customEventRows, errorSummaryRows, vitalRows, funnelRows, goalRows] = + await Promise.all([ + executeQuery( + { + projectId: websiteId, + type: "custom_events_discovery", + from, + to, + timezone, + limit: 50, + }, + undefined, + timezone + ), + executeQuery( + { + projectId: websiteId, + type: "error_summary", + from, + to, + timezone, + }, + undefined, + timezone + ), + executeQuery( + { + projectId: websiteId, + type: "vitals_overview", + from, + to, + timezone, + }, + undefined, + timezone + ), + db + .select({ name: funnelDefinitions.name }) + .from(funnelDefinitions) + .where( + and( + eq(funnelDefinitions.websiteId, websiteId), + eq(funnelDefinitions.isActive, true), + isNull(funnelDefinitions.deletedAt) + ) + ) + .limit(20), + db + .select({ name: goals.name, type: goals.type, target: goals.target }) + .from(goals) + .where( + and( + eq(goals.websiteId, websiteId), + eq(goals.isActive, true), + isNull(goals.deletedAt) + ) + ) + .limit(20), + ]); + + const parts: string[] = []; + + const eventNames = [ + ...new Set( + ( + customEventRows as Array<{ + event_name?: string; + total_events?: number; + }> + ) + .filter((r) => r.event_name && (r.total_events ?? 0) > 0) + .map((r) => r.event_name as string) + ), + ]; + if (eventNames.length > 0) { + parts.push( + `Custom events (${eventNames.length}): ${eventNames.join(", ")}` + ); + } else { + parts.push("Custom events: none configured"); + } + + const errors = errorSummaryRows[0] as { totalErrors?: number } | undefined; + const errorCount = Number(errors?.totalErrors ?? 0); + parts.push( + errorCount > 0 + ? `Errors: ${errorCount} in current period` + : "Errors: none recorded" + ); + + const vitals = (vitalRows as Array<{ metric_name?: string }>).map( + (r) => r.metric_name + ); + if (vitals.length > 0) { + parts.push(`Vitals: ${vitals.join(", ")}`); + } else { + parts.push("Vitals: no data"); + } + + if (funnelRows.length > 0) { + parts.push( + `Funnels (${funnelRows.length}): ${funnelRows.map((f) => f.name).join(", ")}` + ); + } else { + parts.push("Funnels: none configured"); + } + + if (goalRows.length > 0) { + parts.push( + `Goals (${goalRows.length}): ${goalRows.map((g) => `${g.name} (${g.type}: ${g.target})`).join(", ")}` + ); + } else { + parts.push("Goals: none configured"); + } + + return `\nSite capabilities:\n${parts.join("\n")}`; +} + export function promptLookbackDays( config: InsightGenerationConfigSnapshot ): number { @@ -284,6 +411,7 @@ export function buildInvestigationPrompt( enrichedSignals: EnrichedSignal[], params: { annotationContext: string; + capabilitiesBlock: string; dismissedBlock: string; domain: string; githubRepo?: { owner: string; repo: string }; @@ -305,7 +433,7 @@ export function buildInvestigationPrompt( return `Investigating ${enrichedSignals.length} anomalies on ${domain}. Period: ${period.current.from} to ${period.current.to} vs ${period.previous.from} to ${period.previous.to} (${timezone}) -${params.siteContext} +${params.siteContext}${params.capabilitiesBlock} SIGNALS: diff --git a/packages/evals/ui/index.html b/packages/evals/ui/index.html index bdc50fe4c..b1b442c00 100644 --- a/packages/evals/ui/index.html +++ b/packages/evals/ui/index.html @@ -794,16 +794,16 @@

Latest model board

const id = escapeHtml(c.id); const cost = (c.metrics?.costUsd || 0) + (c.metrics?.judgeCostUsd || 0); return ` -
${id}
- ${escapeHtml(c.category || "case")} - ${c.passed ? "Pass" : "Fail"} - ${c.scores?.tool_routing ?? "--"} - ${c.scores?.quality ?? "--"} - ${((c.metrics?.latencyMs || 0) / 1000).toFixed(1)}s - ${c.metrics?.steps ?? "--"} - ${money(cost)} - -
${detail(c)}
`; +
${id}
+ ${escapeHtml(c.category || "case")} + ${c.passed ? "Pass" : "Fail"} + ${c.scores?.tool_routing ?? "--"} + ${c.scores?.quality ?? "--"} + ${((c.metrics?.latencyMs || 0) / 1000).toFixed(1)}s + ${c.metrics?.steps ?? "--"} + ${money(cost)} + +
${detail(c)}
`; } function detail(c) { @@ -815,7 +815,7 @@

Latest model board

.map((t) => `${escapeHtml(t)}`) .join("") || 'No tools called'; return `

Response

${escapeHtml(c.response || "No response captured.")}
-

Failures

Tools

${tools}
`; +

Failures

Tools

${tools}
`; } function toggle(id) { From 92099a076da7e7254bd4122b373c07e89cd4edd4 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Fri, 29 May 2026 14:46:34 +0300 Subject: [PATCH 2/3] chore(docs): remove unused Wordmark component from footer The decorative SVG wordmark at the bottom of the footer was removed. Drops the component file and its import. --- apps/docs/components/footer.tsx | 4 +- apps/docs/components/landing/wordmark.tsx | 253 ---------------------- 2 files changed, 1 insertion(+), 256 deletions(-) delete mode 100644 apps/docs/components/landing/wordmark.tsx diff --git a/apps/docs/components/footer.tsx b/apps/docs/components/footer.tsx index b8cf1cf7d..f31c92b1e 100644 --- a/apps/docs/components/footer.tsx +++ b/apps/docs/components/footer.tsx @@ -1,13 +1,12 @@ "use client"; -import { SiDiscord, SiX } from "@icons-pack/react-simple-icons"; import { Button } from "@databuddy/ui"; import { EnvelopeIcon } from "@databuddy/ui/icons"; +import { SiDiscord, SiX } from "@icons-pack/react-simple-icons"; import Image from "next/image"; import Link from "next/link"; import { CCPAIcon } from "./icons/ccpa"; import { GDPRIcon } from "./icons/gdpr"; -import { Wordmark } from "./landing/wordmark"; import { LogoContent } from "./logo"; import { NavLink } from "./nav-link"; import { NewsletterForm } from "./newsletter-form"; @@ -281,7 +280,6 @@ export function Footer() { - ); diff --git a/apps/docs/components/landing/wordmark.tsx b/apps/docs/components/landing/wordmark.tsx deleted file mode 100644 index 67c5cb934..000000000 --- a/apps/docs/components/landing/wordmark.tsx +++ /dev/null @@ -1,253 +0,0 @@ -"use client"; - -import { - animate, - motion, - useMotionTemplate, - useMotionValue, -} from "motion/react"; -import React, { useCallback, useEffect, useRef } from "react"; -import { cn } from "@/lib/utils"; - -export const Wordmark = () => { - return ( -
-
- {/* Logo SVG */} -
-
- - - - - - - - - - - -
-
-
-
- ); -}; - -interface MagicSVGProps { - children: React.ReactNode; - className?: string; - fill?: string; - gradientFrom?: string; - gradientSize?: number; - gradientTo?: string; - height: number; - strokeColor?: string; - strokeWidth?: number; - width: number; -} - -export function MagicSVG({ - children, - width, - height, - className, - gradientSize = 50, - gradientFrom = "#E3A514", - gradientTo = "#B74677", - strokeWidth = 1, - fill = "none", - strokeColor = "#27282D", -}: MagicSVGProps) { - const svgRef = useRef(null); - - const animatedX = useMotionValue(-gradientSize * 2); - const animatedY = useMotionValue(-gradientSize * 2); - - const handleMouseMove = useCallback( - (e: MouseEvent) => { - if (svgRef.current) { - const { left, top } = svgRef.current.getBoundingClientRect(); - const clientX = e.clientX; - const clientY = e.clientY; - const newX = clientX - left; - const newY = clientY - top; - - animate(animatedX, newX, { - type: "spring", - stiffness: 150, - damping: 25, - mass: 0.8, - }); - - animate(animatedY, newY, { - type: "spring", - stiffness: 150, - damping: 25, - mass: 0.8, - }); - } - }, - [animatedX, animatedY] - ); - - const handleMouseLeave = useCallback(() => { - animate(animatedX, -gradientSize * 2, { - type: "spring", - stiffness: 100, - damping: 30, - }); - - animate(animatedY, -gradientSize * 2, { - type: "spring", - stiffness: 100, - damping: 30, - }); - }, [animatedX, animatedY, gradientSize]); - - const handleMouseEnter = useCallback(() => { - document.addEventListener("mousemove", handleMouseMove); - }, [handleMouseMove]); - - useEffect(() => { - const svgElement = svgRef.current; - if (svgElement) { - svgElement.addEventListener("mouseenter", handleMouseEnter); - svgElement.addEventListener("mouseleave", handleMouseLeave); - } - return () => { - if (svgElement) { - svgElement.removeEventListener("mouseenter", handleMouseEnter); - svgElement.removeEventListener("mouseleave", handleMouseLeave); - } - document.removeEventListener("mousemove", handleMouseMove); - }; - }, [handleMouseEnter, handleMouseLeave, handleMouseMove]); - - useEffect(() => { - animatedX.set(-gradientSize * 2); - animatedY.set(-gradientSize * 2); - }, [gradientSize, animatedX, animatedY]); - - const gradientId = "magic-gradient-wordmark"; - const maskId = "magic-mask-wordmark"; - - return ( - - Magic SVG - - - - - - - - - - - - - - {React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - const childType = (child as React.ReactElement).type; - if ( - childType === "defs" || - childType === "mask" || - childType === "clipPath" - ) { - return child; - } - } - return null; - })} - - - {React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - const childType = (child as React.ReactElement).type; - if ( - childType !== "defs" && - childType !== "mask" && - childType !== "clipPath" - ) { - return React.cloneElement( - child as React.ReactElement>, - { - stroke: strokeColor, - strokeWidth, - fill, - } - ); - } - } - return null; - })} - - - - {React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - const childType = (child as React.ReactElement).type; - if ( - childType !== "defs" && - childType !== "mask" && - childType !== "clipPath" - ) { - return React.cloneElement( - child as React.ReactElement>, - { - stroke: `url(#${gradientId})`, - strokeWidth: strokeWidth + 1, - fill, - } - ); - } - } - return null; - })} - - - ); -} From 4461cec3393128ed635ec2925aa21999bad0f01f Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Fri, 29 May 2026 14:46:48 +0300 Subject: [PATCH 3/3] feat(docs): overhaul pricing table to reflect actual product features The comparison table was underselling the product. This adds all shipping features that were missing, organizes the table into sections, and links feature names to their dedicated pages. Changes: - Unhide Error Tracking from pricing (it ships on Hobby+) - Add "Analytics features" section with per-plan limits and unlimited features shown as checkmarks - Add "Platform" section: Uptime Monitoring, Short Links, Revenue Tracking, Alerts, Team Members, Websites, API Access, Slack, SDKs - Add "Enterprise" section header for SSO, Audit Logs, Support - Link feature names to /feature-flags, /web-vitals, /errors, /uptime, /links where dedicated pages exist - Update page subtitle and footer notes to reflect full product surface --- .../pricing/_pricing/gated-feature-rows.tsx | 193 +++++++++++++++--- .../app/(home)/pricing/_pricing/table.tsx | 18 +- apps/docs/app/(home)/pricing/page.tsx | 6 +- packages/shared/src/types/features.ts | 4 +- 4 files changed, 179 insertions(+), 42 deletions(-) diff --git a/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx b/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx index 60554db6a..dc362bbf9 100644 --- a/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx +++ b/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx @@ -9,7 +9,8 @@ import { PLAN_IDS, type PlanId, } from "@databuddy/shared/types/features"; -import { XMarkIcon as XIcon } from "@databuddy/ui/icons"; +import { CheckIcon, XMarkIcon as XIcon } from "@databuddy/ui/icons"; +import Link from "next/link"; import type { ReactNode } from "react"; /** Docs pricing column ids → shared plan ids (enterprise maps to Scale limits). */ @@ -30,12 +31,18 @@ function isUnlimitedOnAllPlans(featureId: GatedFeatureId): boolean { return true; } -function pricingVisibleGatedFeatures(): GatedFeatureId[] { +function featuresWithLimits(): GatedFeatureId[] { return (Object.values(GATED_FEATURES) as GatedFeatureId[]) .filter((id) => !HIDDEN_PRICING_FEATURES.includes(id)) .filter((id) => !isUnlimitedOnAllPlans(id)); } +function featuresUnlimitedOnAll(): GatedFeatureId[] { + return (Object.values(GATED_FEATURES) as GatedFeatureId[]) + .filter((id) => !HIDDEN_PRICING_FEATURES.includes(id)) + .filter((id) => isUnlimitedOnAllPlans(id)); +} + function FeatureX() { return ( @@ -44,6 +51,14 @@ function FeatureX() { ); } +function FeatureCheck() { + return ( + + + + ); +} + function formatLimitCell( limit: FeatureLimit, featureId: GatedFeatureId @@ -52,7 +67,7 @@ function formatLimitCell( return ; } if (limit === "unlimited") { - return Unlimited; + return ; } const meta = FEATURE_METADATA[featureId]; const unit = meta.unit; @@ -86,52 +101,162 @@ interface GatedFeaturePricingRowsProps { planTdClassName: (planId: string) => string; } +const GATED_FEATURE_LINKS: Partial> = { + [GATED_FEATURES.FEATURE_FLAGS]: "/feature-flags", + [GATED_FEATURES.WEB_VITALS]: "/web-vitals", + [GATED_FEATURES.ERROR_TRACKING]: "/errors", +}; + +interface PlatformFeature { + name: string; + description: string; + href?: string; +} + +const PLATFORM_FEATURES: PlatformFeature[] = [ + { name: "Uptime Monitoring", description: "Endpoint checks, alerts, and status pages", href: "/uptime" }, + { name: "Short Links", description: "Branded links with click analytics and deep linking", href: "/links" }, + { name: "Revenue Tracking", description: "Stripe and Paddle revenue attribution" }, + { name: "Alerts & Notifications", description: "Traffic, error, and anomaly alerts" }, + { name: "Team Members", description: "Unlimited seats on all plans" }, + { name: "Websites", description: "Unlimited websites on all plans" }, + { name: "API Access", description: "REST API with scoped API keys" }, + { name: "Slack Integration", description: "Alerts and digests in Slack" }, + { name: "SDKs", description: "JavaScript, React, Vue, Swift" }, +]; + +function FeatureLabel({ + name, + description, + href, +}: { + name: string; + description: string; + href?: string; +}) { + if (href) { + return ( + + {name} + + ); + } + return {name}; +} + +function AllPlansCheckRow({ + name, + description, + href, + plans, + planTdClassName, +}: { + name: string; + description: string; + href?: string; + plans: Array<{ id: string }>; + planTdClassName: (planId: string) => string; +}) { + return ( + + + + + {plans.map((p) => ( + + + + ))} + + ); +} + export function GatedFeaturePricingRows({ plans, planTdClassName, }: GatedFeaturePricingRowsProps) { - const features = pricingVisibleGatedFeatures(); - if (features.length === 0) { - return null; - } - + const limited = featuresWithLimits(); + const unlimited = featuresUnlimitedOnAll(); const colSpan = 1 + plans.length; return ( <> + {limited.length > 0 && ( + <> + + + Analytics features + + + {limited.map((featureId) => { + const meta = FEATURE_METADATA[featureId]; + const href = GATED_FEATURE_LINKS[featureId]; + return ( + + + + + {plans.map((p) => ( + + + + ))} + + ); + })} + {unlimited.map((featureId) => { + const meta = FEATURE_METADATA[featureId]; + const href = GATED_FEATURE_LINKS[featureId]; + return ( + + ); + })} + + )} - Product features + Platform - {features.map((featureId) => { - const meta = FEATURE_METADATA[featureId]; - return ( - - - {meta.name} - - {plans.map((p) => ( - - - - ))} - - ); - })} + {PLATFORM_FEATURES.map((feat) => ( + + ))} ); } diff --git a/apps/docs/app/(home)/pricing/_pricing/table.tsx b/apps/docs/app/(home)/pricing/_pricing/table.tsx index 899cd8eb4..295e82617 100644 --- a/apps/docs/app/(home)/pricing/_pricing/table.tsx +++ b/apps/docs/app/(home)/pricing/_pricing/table.tsx @@ -163,6 +163,15 @@ export function PlansComparisonTable({ plans }: Props) { plans={plans} planTdClassName={planComparisonTdClass} /> + {/* Enterprise section header */} + + + Enterprise + + {/* Support row */} @@ -282,14 +291,19 @@ export function PlansComparisonTable({ plans }: Props) {

What counts as an event? A pageview, custom event, error, or Web Vital measurement. Feature flag - evaluations are free and don't count toward your quota. + evaluations and uptime checks are free and don't count toward your + quota.

Agent credits power Databunny, the AI assistant that analyzes your data, answers questions, and surfaces insights automatically.

-

Overage is tiered. Lower rates apply as volume increases.

+

+ Unlimited seats & sites.{" "} + Team members, websites, and API access are unlimited on every plan. + Overage is tiered with lower rates as volume increases. +

); diff --git a/apps/docs/app/(home)/pricing/page.tsx b/apps/docs/app/(home)/pricing/page.tsx index 00e26297f..b50383a1e 100644 --- a/apps/docs/app/(home)/pricing/page.tsx +++ b/apps/docs/app/(home)/pricing/page.tsx @@ -47,9 +47,9 @@ export default function PricingPage() { Every feature, every plan.

- Analytics, errors, vitals, and flags included at every tier. Pick a - plan based on volume, not features. Fair tiered overage, and you - only pay for what you use. + Analytics, uptime monitoring, link management, error tracking, web + vitals, feature flags, and more included at every tier. Pick a plan + based on volume, not features.

diff --git a/packages/shared/src/types/features.ts b/packages/shared/src/types/features.ts index 77df9eef4..0aa899fbe 100644 --- a/packages/shared/src/types/features.ts +++ b/packages/shared/src/types/features.ts @@ -34,9 +34,7 @@ export const GATED_FEATURES = { export type GatedFeatureId = (typeof GATED_FEATURES)[keyof typeof GATED_FEATURES]; -export const HIDDEN_PRICING_FEATURES: GatedFeatureId[] = [ - GATED_FEATURES.ERROR_TRACKING, -]; +export const HIDDEN_PRICING_FEATURES: GatedFeatureId[] = []; export type FeatureLimit = number | "unlimited" | false;