diff --git a/frontend/instrumentation-client.ts b/frontend/instrumentation-client.ts new file mode 100644 index 00000000..9d72b8a8 --- /dev/null +++ b/frontend/instrumentation-client.ts @@ -0,0 +1,11 @@ +import posthog from 'posthog-js'; + +if (process.env.NEXT_PUBLIC_POSTHOG_KEY) { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: '/ingest', + ui_host: 'https://eu.posthog.com', + defaults: '2025-05-24', + capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this + debug: process.env.NODE_ENV === 'development', + }); +} diff --git a/frontend/next.config.ts b/frontend/next.config.ts index efd187d5..eb96f75d 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,6 +2,23 @@ import type { NextConfig } from 'next'; const nextConfig = (): NextConfig => ({ output: (process.env.NEXT_OUTPUT as 'standalone') || undefined, + async rewrites() { + return [ + { + source: '/ingest/static/:path*', + destination: 'https://eu-assets.i.posthog.com/static/:path*', + }, + { + source: '/ingest/:path*', + destination: 'https://eu.i.posthog.com/:path*', + }, + { + source: '/ingest/flags', + destination: 'https://eu.i.posthog.com/flags', + }, + ]; + }, + skipTrailingSlashRedirect: true, }); export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3af55459..238ee6ee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -86,6 +86,8 @@ "papaparse": "^5.5.2", "pdfjs-dist": "4.8.69", "postcss": "8.4.33", + "posthog-js": "^1.258.6", + "posthog-node": "^5.6.0", "react": "^18", "react-day-picker": "^8.10.1", "react-diff-viewer-continued": "^3.4.0", @@ -7473,7 +7475,6 @@ "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", "hasInstallScript": true, "license": "MIT", - "optional": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -12488,6 +12489,45 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/posthog-js": { + "version": "1.258.6", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.258.6.tgz", + "integrity": "sha512-vL5AGG+rOoRg3LGquMfBPO55jD4bGl0CiV44SHdHAoBnOVDDAqxczRGDqMdxor+VLx3/ofTFOJ2FNprfAHp70Q==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@rrweb/types": "2.0.0-alpha.17", + "rrweb-snapshot": "2.0.0-alpha.17" + }, + "peerDependenciesMeta": { + "@rrweb/types": { + "optional": true + }, + "rrweb-snapshot": { + "optional": true + } + } + }, + "node_modules/posthog-js/node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, + "node_modules/posthog-node": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.6.0.tgz", + "integrity": "sha512-MVXxKmqAYp2cPBrN1YMhnhYsJYIu6yc6wumbHz1dbo67wZBf2WtMm67Uh+4VCrp07049qierWlxQqz1W5zGDeg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/preact": { "version": "10.26.8", "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.8.tgz", @@ -14915,6 +14955,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 88a12db3..a8ab89e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -89,6 +89,8 @@ "papaparse": "^5.5.2", "pdfjs-dist": "4.8.69", "postcss": "8.4.33", + "posthog-js": "^1.258.6", + "posthog-node": "^5.6.0", "react": "^18", "react-day-picker": "^8.10.1", "react-diff-viewer-continued": "^3.4.0", diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 16734734..084bece1 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -9,6 +9,7 @@ import { Analytics } from '@vercel/analytics/react'; import { GoogleAnalytics } from '@next/third-parties/google'; import { SpeedInsights } from '@vercel/speed-insights/next'; import Script from 'next/script'; +import { PostHogIdentify } from '@/components/posthog-identify'; const geistSans = Geist({ variable: '--font-geist-sans', @@ -152,6 +153,7 @@ export default function RootLayout({ + diff --git a/frontend/src/components/home/sections/pricing-section.tsx b/frontend/src/components/home/sections/pricing-section.tsx index 055884b0..fe63f487 100644 --- a/frontend/src/components/home/sections/pricing-section.tsx +++ b/frontend/src/components/home/sections/pricing-section.tsx @@ -16,6 +16,7 @@ import { import { toast } from 'sonner'; import { isLocalMode, isYearlyCommitmentDowngrade, isPlanChangeAllowed, getPlanInfo } from '@/lib/config'; import { useSubscription, useSubscriptionCommitment } from '@/hooks/react-query'; +import posthog from 'posthog-js'; // Constants export const SUBSCRIPTION_PLANS = { @@ -241,6 +242,7 @@ function PricingTier({ case 'checkout_created': case 'commitment_created': if (response.url) { + posthog.capture('plan_purchase_attempted'); window.location.href = response.url; } else { console.error( @@ -255,6 +257,7 @@ function PricingTier({ ? `Subscription upgraded from $${response.details.current_price} to $${response.details.new_price}` : 'Subscription updated successfully'; toast.success(upgradeMessage); + posthog.capture('plan_upgraded'); if (onSubscriptionUpdate) onSubscriptionUpdate(); break; case 'commitment_blocks_downgrade': @@ -276,6 +279,7 @@ function PricingTier({

, ); + posthog.capture('plan_downgraded'); if (onSubscriptionUpdate) onSubscriptionUpdate(); break; case 'no_change': diff --git a/frontend/src/components/posthog-identify.tsx b/frontend/src/components/posthog-identify.tsx new file mode 100644 index 00000000..d3e7f247 --- /dev/null +++ b/frontend/src/components/posthog-identify.tsx @@ -0,0 +1,24 @@ +'use client'; + +import posthog from 'posthog-js'; +import { useEffect } from 'react'; +import { createClient } from '@/lib/supabase/client'; + +export const PostHogIdentify = () => { + useEffect(() => { + const supabase = createClient(); + const listener = supabase.auth.onAuthStateChange((_, session) => { + if (session) { + posthog.identify(session.user.id, { email: session.user.email }); + } else { + posthog.reset(); + } + }); + + return () => { + listener.data.subscription.unsubscribe(); + }; + }, []); + + return null; +}; diff --git a/frontend/src/components/sidebar/sidebar-left.tsx b/frontend/src/components/sidebar/sidebar-left.tsx index cea05f56..db2b700a 100644 --- a/frontend/src/components/sidebar/sidebar-left.tsx +++ b/frontend/src/components/sidebar/sidebar-left.tsx @@ -41,6 +41,7 @@ import { useIsMobile } from '@/hooks/use-mobile'; import { cn } from '@/lib/utils'; import { usePathname, useSearchParams } from 'next/navigation'; import { useFeatureFlags } from '@/lib/feature-flags'; +import posthog from 'posthog-js'; export function SidebarLeft({ ...props @@ -150,7 +151,7 @@ export function SidebarLeft({ + })} onClick={() => posthog.capture('new_task_clicked')}> New Task diff --git a/frontend/src/components/thread/chat-input/chat-input.tsx b/frontend/src/components/thread/chat-input/chat-input.tsx index 3cf6074e..462a711e 100644 --- a/frontend/src/components/thread/chat-input/chat-input.tsx +++ b/frontend/src/components/thread/chat-input/chat-input.tsx @@ -28,6 +28,7 @@ import { useSubscriptionWithStreaming } from '@/hooks/react-query/subscriptions/ import { isLocalMode } from '@/lib/config'; import { BillingModal } from '@/components/billing/billing-modal'; import { useRouter } from 'next/navigation'; +import posthog from 'posthog-js'; export interface ChatInputHandles { getPendingFiles: () => File[]; @@ -282,6 +283,8 @@ export const ChatInput = forwardRef( thinkingEnabled = true; } + posthog.capture("task_prompt_submitted", { message }); + onSubmit(message, { model_name: baseModelName, enable_thinking: thinkingEnabled, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c5e3a83f..ac91d97a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,5 +1,6 @@ import { createClient } from '@/lib/supabase/client'; import { handleApiError } from './error-handler'; +import posthog from 'posthog-js'; // Get backend URL from environment variables const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; @@ -838,6 +839,8 @@ export const stopAgent = async (agentRunId: string): Promise => { cache: 'no-store', }); + posthog.capture('task_abandoned', { agentRunId }); + if (!response.ok) { const stopError = new Error(`Error stopping agent: ${response.statusText}`); handleApiError(stopError, { operation: 'stop agent', resource: 'AI assistant' });