Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 159 additions & 34 deletions apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand All @@ -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 (
<span className="inline-flex items-center justify-center">
Expand All @@ -44,6 +51,14 @@ function FeatureX() {
);
}

function FeatureCheck() {
return (
<span className="inline-flex items-center justify-center">
<CheckIcon className="size-4 text-primary" weight="bold" />
</span>
);
}

function formatLimitCell(
limit: FeatureLimit,
featureId: GatedFeatureId
Expand All @@ -52,7 +67,7 @@ function formatLimitCell(
return <FeatureX />;
}
if (limit === "unlimited") {
return <span>Unlimited</span>;
return <FeatureCheck />;
}
const meta = FEATURE_METADATA[featureId];
const unit = meta.unit;
Expand Down Expand Up @@ -86,52 +101,162 @@ interface GatedFeaturePricingRowsProps {
planTdClassName: (planId: string) => string;
}

const GATED_FEATURE_LINKS: Partial<Record<GatedFeatureId, string>> = {
[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 (
<Link
className="underline decoration-border underline-offset-2 transition-colors hover:text-foreground hover:decoration-foreground"
href={href}
title={description}
>
{name}
</Link>
);
}
return <span title={description}>{name}</span>;
}

function AllPlansCheckRow({
name,
description,
href,
plans,
planTdClassName,
}: {
name: string;
description: string;
href?: string;
plans: Array<{ id: string }>;
planTdClassName: (planId: string) => string;
}) {
return (
<tr className="border-border border-t hover:bg-card/10">
<th
className="px-4 py-3 text-left font-normal text-muted-foreground text-sm sm:px-5 lg:px-6"
scope="row"
>
<FeatureLabel name={name} description={description} href={href} />
</th>
{plans.map((p) => (
<td className={planTdClassName(p.id)} key={`${name}-${p.id}`}>
<FeatureCheck />
</td>
))}
</tr>
);
}

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 && (
<>
<tr className="border-border border-t bg-muted/20">
<td
className="px-4 py-2 font-medium text-muted-foreground text-xs uppercase tracking-wide sm:px-5 lg:px-6"
colSpan={colSpan}
>
Analytics features
</td>
</tr>
{limited.map((featureId) => {
const meta = FEATURE_METADATA[featureId];
const href = GATED_FEATURE_LINKS[featureId];
return (
<tr
className="border-border border-t hover:bg-card/10"
key={featureId}
>
<th
className="px-4 py-3 text-left font-normal text-muted-foreground text-sm sm:px-5 lg:px-6"
scope="row"
>
<FeatureLabel name={meta.name} description={meta.description} href={href} />
</th>
{plans.map((p) => (
<td
className={planTdClassName(p.id)}
key={`${featureId}-${p.id}`}
>
<GatedLimitCell featureId={featureId} tablePlanId={p.id} />
</td>
))}
</tr>
);
})}
{unlimited.map((featureId) => {
const meta = FEATURE_METADATA[featureId];
const href = GATED_FEATURE_LINKS[featureId];
return (
<AllPlansCheckRow
key={featureId}
name={meta.name}
description={meta.description}
href={href}
plans={plans}
planTdClassName={planTdClassName}
/>
);
})}
</>
)}
Comment on lines +191 to +241
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unlimited features silently hidden when no limited features exist

The unlimited rows (Users, Web Vitals, Geographic — which are unlimited on all plans) are rendered inside the {limited.length > 0 && ...} guard. If all plan-differentiated features are ever removed or hidden, these rows will disappear from the table without any warning. The "Analytics features" section header and all its rows — including the unlimited ones — would be silently dropped. The unlimited rows should either be rendered unconditionally, or the guard should check limited.length > 0 || unlimited.length > 0.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

<tr className="border-border border-t bg-muted/20">
<td
className="px-4 py-2 font-medium text-muted-foreground text-xs uppercase tracking-wide sm:px-5 lg:px-6"
colSpan={colSpan}
>
Product features
Platform
</td>
</tr>
{features.map((featureId) => {
const meta = FEATURE_METADATA[featureId];
return (
<tr
className="border-border border-t hover:bg-card/10"
key={featureId}
>
<th
className="px-4 py-3 text-left font-normal text-muted-foreground text-sm sm:px-5 lg:px-6"
scope="row"
title={meta.description}
>
{meta.name}
</th>
{plans.map((p) => (
<td
className={planTdClassName(p.id)}
key={`${featureId}-${p.id}`}
>
<GatedLimitCell featureId={featureId} tablePlanId={p.id} />
</td>
))}
</tr>
);
})}
{PLATFORM_FEATURES.map((feat) => (
<AllPlansCheckRow
key={feat.name}
name={feat.name}
description={feat.description}
href={feat.href}
plans={plans}
planTdClassName={planTdClassName}
/>
))}
</>
);
}
18 changes: 16 additions & 2 deletions apps/docs/app/(home)/pricing/_pricing/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ export function PlansComparisonTable({ plans }: Props) {
plans={plans}
planTdClassName={planComparisonTdClass}
/>
{/* Enterprise section header */}
<tr className="border-border border-t bg-muted/20">
<td
className="px-4 py-2 font-medium text-muted-foreground text-xs uppercase tracking-wide sm:px-5 lg:px-6"
colSpan={1 + plans.length}
>
Enterprise
</td>
</tr>
{/* Support row */}
<tr className="border-border border-t hover:bg-card/10">
<td className="px-4 py-3 text-muted-foreground text-sm sm:px-5 lg:px-6">
Expand Down Expand Up @@ -282,14 +291,19 @@ export function PlansComparisonTable({ plans }: Props) {
<p>
<span className="text-foreground">What counts as an event?</span> 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.
</p>
<p>
<span className="text-foreground">Agent credits</span> power
Databunny, the AI assistant that analyzes your data, answers
questions, and surfaces insights automatically.
</p>
<p>Overage is tiered. Lower rates apply as volume increases.</p>
<p>
<span className="text-foreground">Unlimited seats & sites.</span>{" "}
Team members, websites, and API access are unlimited on every plan.
Overage is tiered with lower rates as volume increases.
</p>
</div>
</section>
);
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/app/(home)/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export default function PricingPage() {
Every feature, every plan.
</h1>
<p className="mx-auto max-w-2xl text-muted-foreground text-sm sm:text-base">
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.
</p>
</header>

Expand Down
4 changes: 1 addition & 3 deletions apps/docs/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -281,7 +280,6 @@ export function Footer() {
<FooterNav />
<ComplianceLinks />
<FooterBottom />
<Wordmark />
</div>
</footer>
);
Expand Down
253 changes: 0 additions & 253 deletions apps/docs/components/landing/wordmark.tsx

This file was deleted.

35 changes: 22 additions & 13 deletions apps/insights/src/generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ import {
buildInvestigationPrompt,
buildSystemPrompt,
fetchDismissedPatterns,
fetchRecentAnnotations,
fetchInsightHistory,
fetchRecentAnnotations,
fetchSiteCapabilities,
formatOrgWebsitesContext,
type OrgWebsiteRow,
} from "./prompts";
Expand Down Expand Up @@ -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(
Expand All @@ -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({
Expand Down
Loading
Loading