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
12 changes: 12 additions & 0 deletions apps/api/src/routes/dev-plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
executeSelfRefund,
isSelfRefundCandidateType,
} from "@/lib/self-refund.js";
import { posthog } from "@/posthog.js";
import {
ensureStripeCustomer,
finalizeDevPlanSetupSession,
Expand Down Expand Up @@ -3216,6 +3217,17 @@ devPlans.openapi(redeemResetPass, async (c) => {
},
});

posthog.capture({
distinctId: user.id,
event: "reset_pass_redeemed",
groups: { organization: personalOrg.id },
properties: {
devPlan: tier,
source,
organization: personalOrg.id,
},
});

return c.json({
success: true,
source,
Expand Down
27 changes: 27 additions & 0 deletions apps/code/src/app/dashboard/components/UsageOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import { format, formatDistanceToNowStrict } from "date-fns";
import { Activity, Coins, Cpu, Gem, TrendingUp } from "lucide-react";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";

import { useAppConfig } from "@/lib/config";
import { useApi } from "@/lib/fetch-client";

import { AgentModelUsageChart } from "./AgentModelUsageChart";
Expand Down Expand Up @@ -219,13 +222,37 @@ export default function UsageOverview({
cycle = "monthly",
}: UsageOverviewProps) {
const api = useApi();
const posthog = usePostHog();
const { posthogKey } = useAppConfig();

const tierKey = planName.toLowerCase();
// The monthly credit pool (not the weekly premium allowance) is the hard
// ceiling: once it's gone, Reset Passes can't unlock anything, so the pass
// card gives way to the upgrade/PAYG promo.
const monthlyExhausted = creditsLimit > 0 && creditsUsed >= creditsLimit;

// Top of the Reset Pass upsell funnel: the user sees the exhausted weekly
// premium meter. reset_pass_purchased/redeemed are captured server-side,
// so without this event the funnel has a bottom but no top.
const weeklyExhausted =
premiumWeeklyLimit > 0 && premiumCreditsUsed >= premiumWeeklyLimit;
useEffect(() => {
if (!posthogKey || !weeklyExhausted) {
return;
}
posthog.capture("devpass_weekly_cap_hit_viewed", {
tier: tierKey,
weeklyLimit: premiumWeeklyLimit,
weeklyUsed: premiumCreditsUsed,
resetsAt: premiumWeekResetsAt,
purchasedPasses: resetPasses,
includedPassesRemaining: includedResetPassesRemaining,
monthlyExhausted,
});
// Fire once per exhausted dashboard view, not on every prop tick.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [posthogKey, weeklyExhausted]);

const { data: activity } = api.useQuery(
"get",
"/activity",
Expand Down
15 changes: 15 additions & 0 deletions apps/playground/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ const nextConfig: NextConfig = {
"@json-render/react-pdf",
"@json-render/image",
],
async rewrites() {
return [
// First-party PostHog ingestion proxy — ad blockers block
// *.posthog.com directly, silently dropping client events. The
// client is configured with api_host: "/ingest" (providers.tsx).
{
source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://us.i.posthog.com/:path*",
},
];
},
typescript: {
ignoreBuildErrors: true,
},
Expand Down
13 changes: 10 additions & 3 deletions apps/playground/src/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,23 @@ export function Providers({ children, config }: ProvidersProps) {
const host = config.posthogHost;
const init = () => {
posthog.init(key, {
api_host: host,
// Ingest through our own origin (see the /ingest rewrites in
// next.config.ts) so ad blockers that block *.posthog.com don't
// silently drop client events.
api_host: "/ingest",
ui_host: host,
capture_pageview: "history_change",
autocapture: true,
});
};
// Captures fired before init() are dropped by posthog-js, so the idle
// deferral must be bounded — a busy main thread (e.g. the chat page)
// can starve requestIdleCallback long enough for a user to act.
if (typeof requestIdleCallback !== "undefined") {
const id = requestIdleCallback(init);
const id = requestIdleCallback(init, { timeout: 800 });
return () => cancelIdleCallback(id);
}
const timer = setTimeout(init, 1000);
const timer = setTimeout(init, 300);
return () => clearTimeout(timer);
}, [config.posthogKey, config.posthogHost]);

Expand Down
11 changes: 11 additions & 0 deletions apps/ui/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,17 @@ const nextConfig: NextConfig = {
source: "/docs-health",
destination: "https://docs.llmgateway.io/health",
},
// First-party PostHog ingestion proxy — ad blockers block
// *.posthog.com directly, silently dropping client events. The
// client is configured with api_host: "/ingest" (providers.tsx).
{
source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://us.i.posthog.com/:path*",
},
];
},
typescript: {
Expand Down
112 changes: 112 additions & 0 deletions apps/ui/src/components/blog/blog-cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { usePostHog } from "posthog-js/react";

import { Button } from "@/lib/components/button";

// Conversion card embeddable in markdown content via the `BlogCta` override in
// getMarkdownOptions. The DevPass variant leans into the passport/boarding-pass
// brand language used across DevPass surfaces.
export function BlogCta({
variant = "devpass",
location = "inline",
}: {
variant?: "devpass" | "gateway";
location?: string;
}) {
const posthog = usePostHog();
const pathname = usePathname();
const post = pathname?.split("/").pop() ?? "unknown";

const track = (cta: string) => {
posthog.capture("cta_clicked", {
location: `blog_${location}`,
cta,
post,
});
};
Comment on lines +19 to +29

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== blog-cta.tsx ==\n'
cat -n apps/ui/src/components/blog/blog-cta.tsx | sed -n '1,120p'

printf '\n== search usePostHog/capture ==\n'
rg -n "usePostHog|posthog\.capture|posthog\?\.\s*capture|PostHogProvider|ready" apps/ui/src -g '!**/node_modules/**'

printf '\n== navbar.tsx ==\n'
if [ -f apps/ui/src/components/navbar.tsx ]; then
  cat -n apps/ui/src/components/navbar.tsx | sed -n '1,220p'
fi

Repository: theopenco/llmgateway

Length of output: 45642


🌐 Web query:

PostHog React usePostHog capture before provider initialization undefined TypeError docs

💡 Result:

The TypeError: Cannot read properties of undefined (reading '...') when using the PostHog React usePostHog hook typically occurs because you are attempting to access or call methods on the posthog instance before it has fully initialized [1][2]. This often happens during the initial component render [1][2]. To resolve this issue, use the following approaches recommended by PostHog documentation: 1. Use Optional Chaining: Use the optional chaining operator (?.) when calling methods on the posthog instance returned by the usePostHog hook [1][2]. This prevents the code from throwing an error if the instance is currently undefined. posthog?.capture('event_name') 2. Check for Initialization: Wrap your calls in a conditional check to ensure the instance exists before executing any methods [1][2]. useEffect( => { if (posthog) { posthog.capture('event_name'); } }, [posthog]); 3. Ensure Correct Provider Placement: Verify that your application is wrapped by the PostHogProvider at the root of your application (e.g., in main.tsx or App.tsx) and that it is provided with a valid apiKey or a pre-initialized client [3][4][5]. Avoid importing the posthog-js singleton directly in your components for React-specific logic, as this bypasses the React context and can lead to uninitialized state errors [1][2]. If you are manually passing a client instance to the PostHogProvider, ensure you are handling the lifecycle of that client correctly, as the provider assumes you are controlling its initialization [6]. Always prefer the usePostHog hook for accessing the instance within components [1][3].

Citations:


Guard capture behind the PostHog instance. usePostHog() can be undefined while the provider is still initializing, so this click handler should use posthog?.capture(...) or an if (posthog) check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ui/src/components/blog/blog-cta.tsx` around lines 19 - 29, Update the
track handler to guard the posthog capture call when usePostHog() returns
undefined during initialization, using optional chaining or an equivalent if
check while preserving the existing event payload.


if (variant === "gateway") {
return (
<div className="not-prose my-10 rounded-xl border bg-muted/30 p-6 sm:p-8">
<div className="font-mono text-[10px] uppercase tracking-[0.35em] text-muted-foreground">
LLM Gateway
</div>
<h3 className="mt-3 text-2xl font-bold tracking-tight text-foreground">
One API key for every model.
</h3>
<p className="mt-2 max-w-xl text-sm leading-6 text-muted-foreground">
Route to 200+ models with automatic failover, caching, and real-time
cost analytics. Free to start — no credit card required.
</p>
<div className="mt-5 flex flex-wrap items-center gap-3">
<Button
asChild
className="bg-zinc-900 font-medium text-white hover:bg-zinc-700 dark:bg-white dark:text-black dark:hover:bg-zinc-200"
>
<Link
href="/signup"
prefetch={true}
onClick={() => track("create_free_account")}
>
Create free account
</Link>
</Button>
<Link
href="/models"
prefetch={true}
onClick={() => track("browse_models")}
className="text-sm text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground"
>
Browse all models
</Link>
</div>
</div>
);
}

return (
<div className="not-prose relative my-10 overflow-hidden rounded-xl border border-dashed border-stone-400/70 bg-stone-50/70 dark:border-stone-600/70 dark:bg-stone-900/30">
<div className="p-6 sm:p-8">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<div className="font-mono text-[10px] uppercase tracking-[0.35em] text-stone-500 dark:text-stone-400">
DevPass · Boarding pass
</div>
<div className="font-mono text-[9px] tracking-[0.25em] text-stone-400 dark:text-stone-500">
No. DP-{post.slice(0, 6).toUpperCase()}
</div>
</div>
<h3 className="mt-3 text-2xl font-bold tracking-tight text-foreground">
Every frontier model. One flat rate.
</h3>
<p className="mt-2 max-w-xl text-sm leading-6 text-muted-foreground">
Run Claude, GPT-5.5, Gemini, and 200+ more through Claude Code,
Cursor, or whatever tool you already use. From $29/mo — no token math,
no surprise invoices.
</p>
<div className="mt-5 flex flex-wrap items-center gap-3">
<Button
asChild
className="bg-zinc-900 font-medium text-white hover:bg-zinc-700 dark:bg-white dark:text-black dark:hover:bg-zinc-200"
>
<a
href={`https://devpass.llmgateway.io/signup?utm_source=blog&utm_medium=cta&utm_campaign=${post}`}
onClick={() => track("get_devpass")}
>
Get DevPass — from $29/mo
</a>
</Button>
<a
href={`https://devpass.llmgateway.io/pricing?utm_source=blog&utm_medium=cta&utm_campaign=${post}`}
onClick={() => track("compare_devpass_plans")}
className="text-sm text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground"
>
Compare plans
</a>
</div>
</div>
</div>
);
}
Loading
Loading