Skip to content
Open
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: 7 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ DIFFBOT_API_KEY=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Analytics (required)
CLICKHOUSE_URL=http://localhost:8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=clickhouse
CLICKHOUSE_DATABASE=smry_analytics
# Analytics - PostHog (required)
POSTHOG_API_KEY=phc_xxx
POSTHOG_HOST=https://us.i.posthog.com
POSTHOG_PROJECT_ID=12345
POSTHOG_PERSONAL_API_KEY=phx_xxx
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

# Alerting
ALERT_EMAIL=
Expand Down
3 changes: 3 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ThemeProvider } from "@/components/theme-provider";
import { ClerkProvider } from "@clerk/nextjs";
import { getLocale } from 'next-intl/server';
import { JsonLd, organizationSchema, websiteSchema } from "@/components/seo/json-ld";
import { PostHogProvider } from "@/components/providers/posthog-provider";

export const metadata: Metadata = {
metadataBase: new URL("https://smry.ai"),
Expand Down Expand Up @@ -84,6 +85,7 @@ export default async function RootLayout({
<body
className={`${GeistSans.className} ${syne.variable} bg-background text-foreground`}
>
<PostHogProvider>
<Databuddy
clientId="638f8e5f-f436-4d00-a459-66dee9152e3c"
trackPerformance
Expand All @@ -109,6 +111,7 @@ export default async function RootLayout({
</QueryProvider>
</NuqsAdapter>
</ThemeProvider>
</PostHogProvider>
</body>
</html>
</ClerkProvider>
Expand Down
79 changes: 73 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions components/providers/posthog-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import posthog from "posthog-js";
import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react";
import { useEffect, Suspense } from "react";
import { usePathname, useSearchParams } from "next/navigation";

function PostHogPageView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const ph = usePostHog();

useEffect(() => {
if (pathname && ph) {
let url = window.origin + pathname;
const search = searchParams?.toString();
if (search) url += `?${search}`;
ph.capture("$pageview", { $current_url: url });
}
Copy link

Choose a reason for hiding this comment

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

Bug: PostHog SDK race condition on initialization

PostHog SDK race condition on initialization. Events may be silently dropped on first render. Check posthog.__loaded before passing to PHProvider.

View Details

Location: components/providers/posthog-provider.tsx (lines 19)

Analysis

PostHog SDK race condition on initialization. Events may be silently dropped on first render

What fails The PHProvider is passed posthog (the uninitialized singleton) immediately, while posthog.init() runs in a useEffect. This creates a race condition where PostHogPageView and children may attempt to capture events before initialization completes.
Result Events captured during first render may be lost or cause 'PostHog not initialized' warnings
Expected All events should be queued until initialization completes, or initialization should happen synchronously before provider renders children
Impact Loss of analytics data for initial page loads, affecting traffic metrics accuracy.
How to reproduce
1. Load page with cold cache
2. Check browser console for PostHog errors
3. Observe pageview events may be silently dropped on initial render
AI Fix Prompt
Fix this issue: PostHog SDK race condition on initialization. Events may be silently dropped on first render. Check posthog.__loaded before passing to PHProvider.

Location: components/providers/posthog-provider.tsx (lines 19)
Problem: The PHProvider is passed posthog (the uninitialized singleton) immediately, while posthog.init() runs in a useEffect. This creates a race condition where PostHogPageView and children may attempt to capture events before initialization completes.
Current behavior: Events captured during first render may be lost or cause 'PostHog not initialized' warnings
Expected: All events should be queued until initialization completes, or initialization should happen synchronously before provider renders children
Steps to reproduce: 1. Load page with cold cache
2. Check browser console for PostHog errors
3. Observe pageview events may be silently dropped on initial render

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

}, [pathname, searchParams, ph]);

return null;
}

export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const host = process.env.NEXT_PUBLIC_POSTHOG_HOST;
if (!key || !host) return;

posthog.init(key, {
Copy link

Choose a reason for hiding this comment

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

Bug: Missing PostHog defaults config causes SSR hydration errors

Missing PostHog defaults config causes SSR hydration errors. React hydration mismatches may occur. Add defaults: '2025-11-30' to posthog.init().

View Details

Location: components/providers/posthog-provider.tsx (lines 31)

Analysis

Missing PostHog defaults config causes SSR hydration errors. React hydration mismatches may occur

What fails PostHog official Next.js documentation recommends setting defaults: '2025-11-30' to avoid SSR hydration errors. Without this, PostHog may inject scripts causing React hydration mismatches.
Result React hydration warnings/errors in console, potential UI glitches
Expected No hydration errors when using PostHog with Next.js SSR
Impact Poor developer experience, potential user-facing issues from hydration mismatches.
How to reproduce
1. Load page server-rendered
2. Watch for 'Prop dangerouslySetInnerHTML did not match' warnings
3. Especially with features like surveys enabled
Patch Details
-    posthog.init(key, {
-      api_host: host,
-      capture_pageview: false, // we handle it manually above
-      capture_pageleave: true,
-    });
+    posthog.init(key, {
+      api_host: host,
+      capture_pageview: false,
+      capture_pageleave: true,
+      defaults: '2025-11-30',
+    });
AI Fix Prompt
Fix this issue: Missing PostHog defaults config causes SSR hydration errors. React hydration mismatches may occur. Add defaults: '2025-11-30' to posthog.init().

Location: components/providers/posthog-provider.tsx (lines 31)
Problem: PostHog official Next.js documentation recommends setting defaults: '2025-11-30' to avoid SSR hydration errors. Without this, PostHog may inject scripts causing React hydration mismatches.
Current behavior: React hydration warnings/errors in console, potential UI glitches
Expected: No hydration errors when using PostHog with Next.js SSR
Steps to reproduce: 1. Load page server-rendered
2. Watch for 'Prop dangerouslySetInnerHTML did not match' warnings
3. Especially with features like surveys enabled

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

api_host: host,
capture_pageview: false, // we handle it manually above
capture_pageleave: true,
});
}, []);

return (
<PHProvider client={posthog}>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
{children}
</PHProvider>
);
Comment on lines +25 to +45
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

posthog-js initialization race condition Next.js PHProvider

💡 Result:

In the common Next.js App Router setup, people put posthog.init(...) inside a useEffect in a PHProvider. That can create an initialization race because useEffect runs after the first client render/hydration, so any code that calls posthog.capture(...), reads feature flags, etc. during the first render (or in effects that run before/without waiting) may run before PostHog is initialized. PostHog’s own docs now prefer initializing earlier via instrumentation-client.ts|js when available. [1][2]

Fix (recommended): initialize in instrumentation-client.ts (Next.js 15.3+)

Create instrumentation-client.ts at the project root and init there (runs early on the client):

// instrumentation-client.ts
import posthog from 'posthog-js'

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
  defaults: '2025-11-30',
})

This is the official “lightweight, fast” Next.js integration path. [2]

If you must use a PHProvider (Next.js ≤ 15.2 / older pattern)

PostHog’s older doc pattern initializes in a provider useEffect (works, but is where the race comes from). [2] To avoid the race, either:

  1. Init synchronously in a client-only module before rendering children (and keep the provider just for context), or
  2. Gate children rendering until initialized (e.g., with local state), so nothing can call PostHog “too early”.

(If you share your Next.js version + whether you’re on App Router or Pages Router, I can give the exact snippet for your setup.)

Sources: PostHog’s Next.js library docs and Next.js installation docs (including the instrumentation-client.ts|js recommendation and the older useEffect provider pattern). [1][2]

[1] (posthog.com)
[2] (posthog.com)

Citations:


🌐 Web query:

posthog-js init() event queuing before client ready

💡 Result:

In posthog-js, “queueing before the client is ready” depends on how you load it:

1) If you use the standard HTML snippet (recommended)

The snippet creates a stub window.posthog where methods like capture, identify, etc. are functions that push their calls into an array. Once the real SDK (/static/array.js) finishes loading, it replays that queued call list. (posthog.com)

So this works (calls are queued safely even before the SDK is downloaded):

<script>
  // snippet...
  posthog.init('PH_PROJECT_KEY', { api_host: 'https://us.i.posthog.com' })

  // safe immediately: queued by the stub until array.js loads
  posthog.capture('signup_clicked')
</script>

2) If you install via npm (import posthog from 'posthog-js')

There is no stub/global queue unless you build one. You should call posthog.init(...) before calling posthog.capture(...). (posthog.com)

If what you mean by “ready” is “initialized and safe to run my setup (identify, register, etc.)”, use the loaded callback on init:

import posthog from 'posthog-js'

posthog.init('PH_PROJECT_KEY', {
  api_host: 'https://us.i.posthog.com',
  loaded: (ph) => {
    // run things that must wait until init finishes
    ph.identify(userId)
    ph.capture('client_ready')
  },
})

(posthog.com)

3) If “ready” means feature flags are available

Use posthog.onFeatureFlags(...) to run code after flags are loaded (the snippet exposes this API as well). (posthog.com)

Citations:


🌐 Web query:

PostHog React provider pattern "PHProvider" initialization timing

💡 Result:

PHProvider (i.e., PostHogProvider from @posthog/react) does not initialize PostHog for you—it just puts a client instance into React context. So initialization timing is entirely determined by when you call posthog.init(...). (posthog.com)

What this means in practice

  • Pure client-side React (no SSR): initialize before rendering the provider (module/top-level init), then render <PostHogProvider client={posthog}>…</PostHogProvider>. (posthog.com)
  • SSR/hydration frameworks (Next.js App Router, Remix, etc.): you must initialize client-side only.
    • Common pattern: call posthog.init(...) in a useEffect inside a client component, and (optionally) delay rendering the provider until after hydration so nothing consumes an uninitialized client on the first render. (posthog.com)
    • Alternative pattern: guard a top-level init with if (typeof window !== 'undefined') posthog.init(...) so it runs immediately on the client (not waiting for useEffect). (posthog.com)

Why people use a “hydrated gate” in PHProvider

If you render <PostHogProvider client={posthog}> immediately but only call posthog.init in useEffect, then the first client render happens before init, and anything that calls usePostHog() during that render can see an uninitialized client (docs also warn against directly importing the client for this reason). (posthog.com)

Citations:


Fix initialization race condition: PostHog client used before init() completes.

When using the npm posthog-js SDK, there is no automatic event queueing. The code renders PHProvider immediately with an uninitialized posthog client while init() runs asynchronously in useEffect. This means:

  • First render passes an uninitialized client to the provider
  • PostHogPageView may attempt to capture pageviews before initialization completes
  • Components using usePostHog() hook during first render encounter an uninitialized client
  • React Strict Mode (dev) triggers the effect twice, potentially calling init() multiple times

Recommended for Next.js 15.3+: Initialize PostHog in instrumentation-client.ts (runs early, before provider renders), avoiding the race entirely.

For current code: Gate the provider's children until initialization completes:

♻️ Suggested fix: wait for initialization
+import { useState } from 'react'
 
 export function PostHogProvider({ children }: { children: React.ReactNode }) {
+  const [isReady, setIsReady] = useState(false);
+
   useEffect(() => {
     const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
     const host = process.env.NEXT_PUBLIC_POSTHOG_HOST;
     if (!key || !host) return;
 
     posthog.init(key, {
       api_host: host,
       capture_pageview: false,
       capture_pageleave: true,
+      loaded: () => setIsReady(true),
     });
   }, []);
 
   return (
     <PHProvider client={posthog}>
-      <Suspense fallback={null}>
-        <PostHogPageView />
-      </Suspense>
+      {isReady && (
+        <Suspense fallback={null}>
+          <PostHogPageView />
+        </Suspense>
+      )}
       {children}
     </PHProvider>
   );
 }
🤖 Prompt for AI Agents
In `@components/providers/posthog-provider.tsx` around lines 25 - 45,
PostHogProvider currently renders PHProvider with an uninitialized posthog
client; change PostHogProvider to track initialization (e.g., useState
isInitialized) and only render PHProvider, PostHogPageView and children after
posthog.init(key, { api_host: host, ... }) has completed and the flag is set;
call posthog.init inside the useEffect and set isInitialized true in the same
effect (and guard to avoid re-initializing by checking a posthog init flag or a
local ref) so usePostHog()/PostHogPageView never see an uninitialized client and
double-init in Strict Mode is prevented.

}
46 changes: 7 additions & 39 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,7 @@
# Local dev services - ClickHouse starts automatically with: bun run dev
# Local dev services
# For full Docker stack: bun run dev:docker

services:
# Clickhouse analytics database (memory-optimized)
clickhouse:
build:
context: ./docker/clickhouse
dockerfile: Dockerfile
container_name: smry-clickhouse
ports:
- "8123:8123"
- "9000:9000"
volumes:
- clickhouse-data:/var/lib/clickhouse
- ./docs/clickhouse-schema.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
CLICKHOUSE_DB: smry_analytics
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: clickhouse
healthcheck:
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 3
ulimits:
nofile:
soft: 262144
hard: 262144

# Next.js app
app:
build:
Expand All @@ -42,22 +16,16 @@ services:
- /app/node_modules
- /app/.next
environment:
# Clickhouse (internal Docker network)
CLICKHOUSE_URL: http://clickhouse:8123
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: clickhouse
CLICKHOUSE_DATABASE: smry_analytics
ANALYTICS_SECRET_KEY: dev_secret_key
# Pass through from host .env
POSTHOG_API_KEY: ${POSTHOG_API_KEY}
POSTHOG_HOST: ${POSTHOG_HOST}
POSTHOG_PROJECT_ID: ${POSTHOG_PROJECT_ID}
POSTHOG_PERSONAL_API_KEY: ${POSTHOG_PERSONAL_API_KEY}
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY}
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST}
UPSTASH_REDIS_REST_URL: ${UPSTASH_REDIS_REST_URL}
UPSTASH_REDIS_REST_TOKEN: ${UPSTASH_REDIS_REST_TOKEN}
DIFFBOT_API_KEY: ${DIFFBOT_API_KEY}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
CLERK_SECRET_KEY: ${CLERK_SECRET_KEY}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
depends_on:
clickhouse:
condition: service_healthy

volumes:
clickhouse-data:
4 changes: 0 additions & 4 deletions docker/clickhouse/Dockerfile

This file was deleted.

42 changes: 0 additions & 42 deletions docker/clickhouse/memory.xml

This file was deleted.

Loading