diff --git a/bin.ts b/bin.ts index 1b9d9f1..25f6395 100644 --- a/bin.ts +++ b/bin.ts @@ -20,12 +20,7 @@ if (!satisfies(process.version, NODE_VERSION_RANGE)) { import { runMCPInstall, runMCPRemove } from './src/mcp'; import type { CloudRegion, WizardOptions } from './src/utils/types'; import { runWizard } from './src/run'; -import { runEventSetupWizard } from './src/nextjs/event-setup'; -import { - readEnvironment, - isNonInteractiveEnvironment, -} from './src/utils/environment'; -import path from 'path'; +import { isNonInteractiveEnvironment } from './src/utils/environment'; import clack from './src/utils/clack'; if (isNonInteractiveEnvironment()) { @@ -113,48 +108,6 @@ yargs(hideBin(process.argv)) void runWizard(options as unknown as WizardOptions); }, ) - .command( - 'event-setup', - 'Run the event setup wizard', - (yargs) => { - return yargs.options({ - 'install-dir': { - describe: - 'Directory to run the wizard in\nenv: POSTHOG_WIZARD_INSTALL_DIR', - type: 'string', - }, - }); - }, - (argv) => { - const finalArgs = { - ...argv, - ...readEnvironment(), - } as any; - - let resolvedInstallDir: string; - if (finalArgs.installDir) { - if (path.isAbsolute(finalArgs.installDir)) { - resolvedInstallDir = finalArgs.installDir; - } else { - resolvedInstallDir = path.join(process.cwd(), finalArgs.installDir); - } - } else { - resolvedInstallDir = process.cwd(); - } - - const wizardOptions: WizardOptions = { - debug: finalArgs.debug ?? false, - installDir: resolvedInstallDir, - cloudRegion: finalArgs.region as CloudRegion | undefined, - default: finalArgs.default ?? false, - signup: finalArgs.signup ?? false, - forceInstall: false, - localMcp: finalArgs.localMcp ?? false, - }; - - void runEventSetupWizard(wizardOptions); - }, - ) .command('mcp ', 'MCP server management commands', (yargs) => { return yargs .command( diff --git a/src/__tests__/run.test.ts b/src/__tests__/run.test.ts index 6981943..5ddfa83 100644 --- a/src/__tests__/run.test.ts +++ b/src/__tests__/run.test.ts @@ -1,15 +1,14 @@ import { runWizard } from '../run'; -import { runNextjsWizard } from '../nextjs/nextjs-wizard'; +import { runNextjsWizardAgent } from '../nextjs/nextjs-wizard-agent'; import { analytics } from '../utils/analytics'; import { Integration } from '../lib/constants'; -jest.mock('../nextjs/nextjs-wizard'); jest.mock('../nextjs/nextjs-wizard-agent'); jest.mock('../utils/analytics'); jest.mock('../utils/clack'); -const mockRunNextjsWizard = runNextjsWizard as jest.MockedFunction< - typeof runNextjsWizard +const mockRunNextjsWizardAgent = runNextjsWizardAgent as jest.MockedFunction< + typeof runNextjsWizardAgent >; const mockAnalytics = analytics as jest.Mocked; @@ -38,7 +37,7 @@ describe('runWizard error handling', () => { forceInstall: false, }; - mockRunNextjsWizard.mockRejectedValue(testError); + mockRunNextjsWizardAgent.mockRejectedValue(testError); await expect(runWizard(testArgs)).rejects.toThrow('process.exit called'); @@ -53,7 +52,7 @@ describe('runWizard error handling', () => { it('should not call captureException when wizard succeeds', async () => { const testArgs = { integration: Integration.nextjs }; - mockRunNextjsWizard.mockResolvedValue(undefined); + mockRunNextjsWizardAgent.mockResolvedValue(undefined); await runWizard(testArgs); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6a99ef1..a563dcb 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -6,15 +6,6 @@ export enum Integration { astro = 'astro', } -export enum FeatureFlagDefinition { - NextV2 = 'wizard-next-v2', -} - -export enum WizardVariant { - Legacy = 'legacy', - Agent = 'agent', -} - export function getIntegrationDescription(type: string): string { switch (type) { case Integration.nextjs: diff --git a/src/lib/framework-config.ts b/src/lib/framework-config.ts index 5c9c690..7b83ee6 100644 --- a/src/lib/framework-config.ts +++ b/src/lib/framework-config.ts @@ -30,6 +30,12 @@ export interface FrameworkMetadata { /** Message shown when user declines AI consent */ abortMessage: string; + /** + * Optional URL to docs for users with unsupported framework versions. + * If not provided, defaults to docsUrl. + */ + unsupportedVersionDocsUrl?: string; + /** * Optional function to gather framework-specific context before agent runs. * For Next.js: detects router type diff --git a/src/nextjs/docs.ts b/src/nextjs/docs.ts deleted file mode 100644 index c011249..0000000 --- a/src/nextjs/docs.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { getAssetHostFromHost, getUiHostFromHost } from '../utils/urls'; - -export const getNextjsAppRouterDocs = ({ - host, - language, -}: { - host: string; - language: 'typescript' | 'javascript'; -}) => { - return ` -============================== -FILE: PostHogProvider.${ - language === 'typescript' ? 'tsx' : 'jsx' - } (put it somewhere where client files are, like the components folder) -LOCATION: Wherever other providers are, or the components folder -============================== -Changes: -- Create a PostHogProvider component that will be imported into the layout file. -- Make sure to include the defaults: '2025-05-24' option in the init call. - -Example: --------------------------------------------------- -"use client" - -import posthog from "posthog-js" -import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react" -import { Suspense, useEffect } from "react" -import { usePathname, useSearchParams } from "next/navigation" - -export function PostHogProvider({ children }: { children: React.ReactNode }) { - useEffect(() => { - posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - api_host: "/ingest", - ui_host: "${getUiHostFromHost(host)}", - 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", - }) - }, []) - - return ( - - {children} - - ) -} --------------------------------------------------- - -============================== -FILE: layout.${language === 'typescript' ? 'tsx' : 'jsx'} -LOCATION: Wherever the root layout is -============================== -Changes: -- Import the PostHogProvider from the providers file and wrap the app in it. - -Example: --------------------------------------------------- -// other imports -import { PostHogProvider } from "LOCATION_OF_POSTHOG_PROVIDER" - -export default function RootLayout({ children }) { - return ( - - - - {/* other providers */} - {children} - {/* other providers */} - - - - ) -} --------------------------------------------------- - -============================== -FILE: posthog.${language === 'typescript' ? 'ts' : 'js'} -LOCATION: Wherever works best given the project structure -============================== -Changes: -- Initialize the PostHog Node.js client - -Example: --------------------------------------------------- -import { PostHog } from "posthog-node" - -// NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog. -export default function PostHogClient() { - const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - flushAt: 1, - flushInterval: 0, - }) - return posthogClient -} --------------------------------------------------- - -============================== -FILE: next.config.{js,ts,mjs,cjs} -LOCATION: Wherever the root next config is -============================== -Changes: -- Add rewrites to the Next.js config to support PostHog, if there are existing rewrites, add the PostHog rewrites to them. -- Add skipTrailingSlashRedirect to the Next.js config to support PostHog trailing slash API requests. -- This can be of type js, ts, mjs, cjs etc. You should adapt the file according to what extension it uses, and if it does not exist yet use '.js'. - -Example: --------------------------------------------------- -const nextConfig = { - // other config - async rewrites() { - return [ - { - source: "/ingest/static/:path*", - destination: "${getAssetHostFromHost(host)}/static/:path*", - }, - { - source: "/ingest/:path*", - destination: "${host}/:path*", - }, - ]; - }, - // This is required to support PostHog trailing slash API requests - skipTrailingSlashRedirect: true, -} -module.exports = nextConfig ---------------------------------------------------`; -}; - -export const getNextjsPagesRouterDocs = ({ - host, - language, -}: { - host: string; - language: 'typescript' | 'javascript'; -}) => { - return ` -============================== -FILE: _app.${language === 'typescript' ? 'tsx' : 'jsx'} -LOCATION: Wherever the root _app.${ - language === 'typescript' ? 'tsx' : 'jsx' - } file is -============================== -Changes: -- Initialize PostHog in _app.js. -- Wrap the application in PostHogProvider. -- Make sure to include the defaults: '2025-05-24' option in the init call. - -Example: --------------------------------------------------- -import { useEffect } from "react" -import posthog from "posthog-js" -import { PostHogProvider } from "posthog-js/react" - -export default function App({ Component, pageProps }) { - useEffect(() => { - posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { - api_host: "/ingest", - ui_host: "${getUiHostFromHost(host)}", - 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", - }) - }, []) - - return ( - - - - ) -} --------------------------------------------------- - -============================== -FILE: posthog.${language === 'typescript' ? 'ts' : 'js'} -LOCATION: Wherever works best given the project structure -============================== -Changes: -- Initialize the PostHog Node.js client - -Example: --------------------------------------------------- -import { PostHog } from "posthog-node" - -// NOTE: This is a Node.js client, so you can use it for sending events from the server side to PostHog. -export default function PostHogClient() { - const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - host: process.env.NEXT_PUBLIC_POSTHOG_HOST, - flushAt: 1, - flushInterval: 0, - }) - return posthogClient -} --------------------------------------------------- - -============================== -FILE: next.config.{js,ts,mjs,cjs} -LOCATION: Wherever the root next config is -============================== -Changes: -- Add rewrites to the Next.js config to support PostHog, if there are existing rewrites, add the PostHog rewrites to them. -- Add skipTrailingSlashRedirect to the Next.js config to support PostHog trailing slash API requests. -- This can be of type js, ts, mjs, cjs etc. You should adapt the file according to what extension it uses, and if it does not exist yet use '.js'. - -Example: --------------------------------------------------- -const nextConfig = { - // other config - async rewrites() { - return [ - { - source: "/ingest/static/:path*", - destination: "${getAssetHostFromHost(host)}/static/:path*", - }, - { - source: "/ingest/:path*", - destination: "${host}/:path*", - }, - ]; - }, - // This is required to support PostHog trailing slash API requests - skipTrailingSlashRedirect: true, -} -module.exports = nextConfig ---------------------------------------------------`; -}; - -export const getModernNextjsDocs = ({ - host, - language, -}: { - host: string; - language: 'typescript' | 'javascript'; -}) => { - return ` -============================== -FILE: instrumentation-client.${language === 'typescript' ? 'ts' : 'js'} -LOCATION: in the root of the application or inside an src folder. -============================== -Changes: -- Create or update the instrumentation-client.${ - language === 'typescript' ? 'ts' : 'js' - } file to use the PostHog client. If the file does not exist yet, create it. -- Do *not* import instrumentation-client.${ - language === 'typescript' ? 'ts' : 'js' - } in any other file; Next.js will automatically handle it. -- Do not modify any other pages/components in the Next.js application; the PostHog client will be automatically initialized and handle all pageview tasks on its own. -- Make sure to include the defaults: '2025-05-24' option in the init call. - -Example: --------------------------------------------------- - -import posthog from "posthog-js" - -posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - api_host: "/ingest", - ui_host: "${getUiHostFromHost(host)}", - 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", -}); --------------------------------------------------- - -============================== -FILE: next.config.{js,ts,mjs,cjs} -LOCATION: Wherever the root next config is -============================== -Changes: -- Add rewrites to the Next.js config to support PostHog, if there are existing rewrites, add the PostHog rewrites to them. -- Add skipTrailingSlashRedirect to the Next.js config to support PostHog trailing slash API requests. -- This can be of type js, ts, mjs, cjs etc. You should adapt the file according to what extension it uses, and if it does not exist yet use '.js'. - -Example: --------------------------------------------------- -const nextConfig = { - // other config - async rewrites() { - return [ - { - source: "/ingest/static/:path*", - destination: "${getAssetHostFromHost(host)}/static/:path*", - }, - { - source: "/ingest/:path*", - destination: "${host}/:path*", - }, - ]; - }, - // This is required to support PostHog trailing slash API requests - skipTrailingSlashRedirect: true, -} -module.exports = nextConfig ---------------------------------------------------`; -}; diff --git a/src/nextjs/event-setup.ts b/src/nextjs/event-setup.ts deleted file mode 100644 index c7eaf9c..0000000 --- a/src/nextjs/event-setup.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { - abort, - getOrAskForProjectData, - askForCloudRegion, - getPackageDotJson, - getUncommittedOrUntrackedFiles, - isInGitRepo, - abortIfCancelled, -} from '../utils/clack-utils'; -import clack from '../utils/clack'; -import { WizardOptions } from '../utils/types'; -import * as fs from 'fs/promises'; -import * as path from 'path'; -import chalk from 'chalk'; -import { query } from '../utils/query'; -import { z } from 'zod'; -import { getAllFilesInProject, updateFile } from '../utils/file-utils'; -import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; -import * as semver from 'semver'; -import { enableDebugLogs, debug } from '../utils/debug'; -import { analytics } from '../utils/analytics'; - -// Analytics constants -const WIZARD_INTERACTION = 'wizard interaction'; -const INTEGRATION_NAME = 'event-setup'; - -// Schema for file selection from AI -const FileSelectionSchema = z.object({ - files: z.array(z.string()).max(10), -}); - -// Schema for enhanced file with events -const EnhancedFileSchema = z.object({ - filePath: z.string(), - content: z.string(), - events: z.array( - z.object({ - name: z.string(), - description: z.string(), - }), - ), -}); - -export async function runEventSetupWizard( - options: WizardOptions, -): Promise { - if (options.debug) { - enableDebugLogs(); - } - - clack.intro( - `Let's do a first pass on PostHog event tracking for your project. - - We'll start by analyzing your project structure, then choose up to ten files to enhance. Use git to discard any events you're not happy with. - - This will give you a starting point, then you can add any events that we missed. - `, - ); - - // Check for uncommitted changes - if (isInGitRepo()) { - const uncommittedOrUntrackedFiles = getUncommittedOrUntrackedFiles(); - if (uncommittedOrUntrackedFiles.length) { - clack.log.warn( - `You have uncommitted or untracked files in your repo: - -${uncommittedOrUntrackedFiles.join('\n')} - -The event setup wizard will modify multiple files. For the best experience, commit or stash your changes first.`, - ); - const continueWithDirtyRepo = await abortIfCancelled( - clack.confirm({ - message: 'Do you want to continue anyway?', - }), - ); - if (!continueWithDirtyRepo) { - analytics.capture(WIZARD_INTERACTION, { - action: 'aborted due to uncommitted changes', - integration: INTEGRATION_NAME, - }); - return abort('Please commit your changes and try again.', 0); - } - analytics.capture(WIZARD_INTERACTION, { - action: 'continued with uncommitted changes', - integration: INTEGRATION_NAME, - }); - } - } - - const cloudRegion = options.cloudRegion ?? (await askForCloudRegion()); - - const { accessToken, projectId } = await getOrAskForProjectData({ - ...options, - cloudRegion, - }); - - // Check if this is a Next.js 15.3+ project with instrumentation-client - const packageJson = await getPackageDotJson(options); - const isNextJs = hasPackageInstalled('next', packageJson); - - if (!isNextJs) { - return abort('This feature is only available for Next.js projects.'); - } - - const nextVersion = getPackageVersion('next', packageJson); - const isNext15_3Plus = nextVersion && semver.gte(nextVersion, '15.3.0'); - - analytics.setTag('nextjs-version', nextVersion); - - if (!isNext15_3Plus) { - analytics.capture(WIZARD_INTERACTION, { - action: 'aborted due to nextjs version', - integration: INTEGRATION_NAME, - nextjsVersion: nextVersion, - }); - return abort('This feature requires Next.js 15.3.0 or higher.'); - } - - // Check for instrumentation-client file - const allFiles = await getAllFilesInProject(options.installDir); - const instrumentationFiles = allFiles.filter( - (f) => - f.includes('instrumentation') && - (f.endsWith('.ts') || f.endsWith('.js')) && - (f.includes('client') || f.includes('Client')), - ); - - if (instrumentationFiles.length === 0) { - analytics.capture(WIZARD_INTERACTION, { - action: 'aborted due to missing instrumentation-client', - integration: INTEGRATION_NAME, - }); - return abort( - 'No instrumentation-client file found. Please set up Next.js instrumentation-client first. Try using this wizard to do it!', - ); - } - - analytics.capture(WIZARD_INTERACTION, { - action: 'started event setup', - integration: INTEGRATION_NAME, - instrumentationFilesCount: instrumentationFiles.length, - }); - - // Get the project file tree - const s = clack.spinner(); - s.start('Analyzing your project structure'); - - const projectFiles = await getAllFilesInProject(options.installDir); - const relativeFiles = projectFiles - .map((f) => path.relative(options.installDir, f)) - .filter((f) => { - // Exclude instrumentation files and next.config - const isInstrumentation = - f.includes('instrumentation') && - (f.endsWith('.ts') || f.endsWith('.js')); - const isNextConfig = f.startsWith('next.config.') || f === 'next.config'; - return !isInstrumentation && !isNextConfig; - }); - - debug('Total files found:', projectFiles.length); - debug('Files after filtering:', relativeFiles.length); - s.stop('Project structure analyzed'); - - analytics.capture(WIZARD_INTERACTION, { - action: 'analyzed project structure', - integration: INTEGRATION_NAME, - totalFiles: projectFiles.length, - eligibleFiles: relativeFiles.length, - }); - - // Send file tree to AI to get 10 most useful files - s.start('Selecting some files to enhance with events...'); - - const fileSelectionPrompt = `Given this Next.js 15.3+ project structure and package.json, select up to 10 CLIENT-SIDE FILES for adding PostHog analytics events. - - IMPORTANT: Only select files that: - - Have "use client" directive at the top, OR - - Use React hooks (useState, useEffect, etc.), OR - - Have event handlers (onClick, onSubmit, onChange) - - DO NOT select: - - API routes (files in /api/ or route.ts/route.js files) - - Server Components (files without "use client" and no hooks/handlers) - - Layout files (layout.tsx/layout.js) - - Configuration files - - Pure utility files - - Focus on: - - User interaction points (buttons, forms, navigation) - - Key user flows (auth, checkout, main features) - - Business-critical paths - - Files that represent important user actions - - Package.json: - ${JSON.stringify(packageJson, null, 2)} - - Project files: - ${relativeFiles.join('\n')} - - Return file paths for client-side files ONLY that would benefit most from analytics tracking. If there are fewer than 10 suitable client files, return only those.`; - - let selectedFiles: string[] = []; - try { - const response = await query({ - message: fileSelectionPrompt, - model: 'gemini-2.5-flash', - region: cloudRegion, - schema: FileSelectionSchema, - accessToken, - projectId, - }); - selectedFiles = response.files; - s.stop(`Selected ${selectedFiles.length} files for event tracking`); - analytics.capture(WIZARD_INTERACTION, { - action: 'selected files for tracking', - integration: INTEGRATION_NAME, - filesSelected: selectedFiles.length, - }); - } catch (error) { - s.stop('Failed to select files'); - analytics.capture(WIZARD_INTERACTION, { - action: 'file selection failed', - integration: INTEGRATION_NAME, - error: error instanceof Error ? error.message : 'Unknown error', - }); - return abort('Could not analyze project structure. Please try again.'); - } - - // Read the selected files and enhance them with events - clack.log.info('Files selected for event tracking:'); - selectedFiles.forEach((file, index) => { - clack.log.info(` ${index + 1}. ${file}`); - }); - - const enhancedFiles: Array<{ - filePath: string; - events: Array<{ name: string; description: string }>; - }> = []; - - clack.log.info( - "\nEnhancing files with event tracking. Changes will be applied as they come in. Use your git interface to review new events. Feel free to toss anything you don't like...", - ); - - for (const filePath of selectedFiles) { - const fileSpinner = clack.spinner(); - fileSpinner.start(`Analyzing ${filePath}`); - - try { - const fullPath = path.join(options.installDir, filePath); - const fileContent = await fs.readFile(fullPath, 'utf8'); - - const enhancePrompt = `You are enhancing a REAL production, client-side Next.js file with PostHog analytics. This is NOT an example or tutorial - add events to the ACTUAL code provided. - - - REQUIRED: import posthog from 'posthog-js' - - Track events with: posthog.capture('event-name', { property: 'value' }) - - NEVER import PostHogClient from '@/app/posthog' - - NEVER create functions with 'use server' - - CRITICAL INSTRUCTIONS: - - This is a REAL file from a production codebase - - DO NOT add placeholder comments like "// In a real app..." or "// This is an example..." - - DO NOT modify the existing business logic or add simulation code - - DO NOT add any tutorial-style comments - - ONLY add PostHog event tracking to the existing, real functionality - - DO NOT create wrapper functions around existing functions just to add tracking - - Add tracking code directly inside existing functions where appropriate - - NEVER import new packages or libraries that aren't already used in the file - - ONLY use imports that already exist in the file or the PostHog imports specified - - DO NOT assume any authentication library (Clerk, Auth.js, etc.) is available - - FORBIDDEN - NEVER DO THESE: - - NEVER add 'use client' or 'use server' directives at the top of the file, or in functions - - NEVER define new server actions (functions with "use server") in Client Components - - NEVER create inline "use server" functions in files that have "use client" - - NEVER use useEffect to track page views or component renders - - NEVER track events like "page_viewed", "form_viewed", "component_rendered", "flow_started", "page_opened" etc - - NEVER track that someone simply arrived at or viewed a page - - NEVER change the file's existing client/server architecture - - NEVER add events on component mount or render - only on actual user interactions - - Track events on user interactions like clicks, form submissions, etc. - - Technical Rules: - - This is a client-side file suitable for event tracking - - REQUIRED IMPORT: import posthog from 'posthog-js' - - Use the existing posthog instance for all tracking - - Example: posthog.capture('button-clicked', { buttonId: 'submit' }) - - Focus on tracking user interactions in the UI components - - Track events like button clicks, form submissions, navigation, etc. - - Add 1-2 high-value events that track the ACTUAL user actions in this file - - Use descriptive event names (lowercase-hyphenated) based on what the code ACTUALLY does - - Include properties that capture REAL data from the existing code - - For user identification: ONLY use user data that's already available in the code - - DO NOT add code to fetch user IDs or authentication state if not already available in the file - - Do not change the formatting of the file; only add events - - Do not set timestamps on events; PostHog will do this automatically - - Always return the entire file content, not just the changes - - NEVER add events that correspond to page views; PostHog tracks these automatically - - NEVER INSERT "use client" or "use server" directives - - File path: ${filePath} - File content: - ${fileContent} - - IMPORTANT: If this file only renders UI without any user interactions (no buttons, forms, or actions), - or if the only possible events would be pageview-like (e.g., "form-viewed", "page-opened", "flow-started"), - then SKIP THIS FILE by returning the original content unchanged. We only want to track actual user actions, - not that someone looked at a page. - - Return the enhanced file with PostHog tracking added to the EXISTING functionality. List the events you added.`; - - const response = await query({ - message: enhancePrompt, - model: 'gemini-2.5-pro', - region: cloudRegion, - schema: EnhancedFileSchema, - accessToken, - projectId, - }); - - // Apply changes immediately - if (response.content !== fileContent) { - await updateFile( - { - filePath, - oldContent: fileContent, - newContent: response.content, - }, - options, - ); - - enhancedFiles.push({ - filePath, - events: response.events, - }); - - fileSpinner.stop( - `✓ Enhanced ${filePath} with ${response.events.length} events`, - ); - analytics.capture(WIZARD_INTERACTION, { - action: 'enhanced file', - integration: INTEGRATION_NAME, - filePath, - eventsAdded: response.events.length, - }); - } else { - fileSpinner.stop(`No changes needed for ${filePath}`); - analytics.capture(WIZARD_INTERACTION, { - action: 'file skipped', - integration: INTEGRATION_NAME, - filePath, - reason: 'no events to add', - }); - } - } catch (error) { - fileSpinner.stop(`✗ Failed to enhance ${filePath}`); - debug('Error enhancing file:', error); - analytics.capture(WIZARD_INTERACTION, { - action: 'file enhancement failed', - integration: INTEGRATION_NAME, - filePath, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - - // Generate event tracking report - const generateMarkdown = () => { - let md = `# Event tracking report\n\n`; - md += `This document lists all PostHog events that have been automatically added to your Next.js application.\n\n`; - md += `## Events by File\n\n`; - - enhancedFiles.forEach((file) => { - if (file.events.length > 0) { - md += `### ${file.filePath}\n\n`; - file.events.forEach((event) => { - md += `- **${event.name}**: ${event.description}\n`; - }); - md += `\n`; - } - }); - - md += `\n## Events still awaiting implementation\n`; - md += `- (human: you can fill these in)`; - - md += `\n---\n\n`; - md += `## Next Steps\n\n`; - md += `1. Review the changes made to your files\n`; - md += `2. Test that events are being captured correctly\n`; - md += `3. Create insights and dashboards in PostHog\n`; - md += `4. Make a list of events we missed above. Knock them out yourself, or give this file to an agent.\n\n`; - md += `Learn more about what to measure with PostHog and why: https://posthog.com/docs/new-to-posthog/getting-hogpilled\n`; - return md; - }; - - const markdownContent = generateMarkdown(); - const fileName = 'event-tracking-report.md'; - const filePath = path.join(options.installDir, fileName); - - await fs.writeFile(filePath, markdownContent); - - // Summary - const totalEvents = enhancedFiles.reduce( - (sum, file) => sum + file.events.length, - 0, - ); - - analytics.capture(WIZARD_INTERACTION, { - action: 'event setup completed', - integration: INTEGRATION_NAME, - totalEvents, - filesEnhanced: enhancedFiles.length, - filesProcessed: selectedFiles.length, - }); - - analytics.setTag('event-setup-total-events', totalEvents); - analytics.setTag('event-setup-files-enhanced', enhancedFiles.length); - - clack.outro( - `Success! Added ${chalk.bold( - totalEvents.toString(), - )} events across ${chalk.bold(enhancedFiles.length.toString())} files. - - Event tracking plan saved to: ${chalk.cyan(fileName)} - - Next steps: - 1. Review changes with your favorite git tool - 2. Revert unwanted changes with ${chalk.bold('git checkout ')} - 3. Test that events are being captured in your PostHog project - 4. Create insights in PostHog - `, - ); -} diff --git a/src/nextjs/nextjs-wizard-agent.ts b/src/nextjs/nextjs-wizard-agent.ts index d9b2b7f..082c8d2 100644 --- a/src/nextjs/nextjs-wizard-agent.ts +++ b/src/nextjs/nextjs-wizard-agent.ts @@ -5,6 +5,10 @@ import { enableDebugLogs } from '../utils/debug'; import { runAgentWizard } from '../lib/agent-runner'; import { Integration } from '../lib/constants'; import { getPackageVersion } from '../utils/package-json'; +import { getPackageDotJson } from '../utils/clack-utils'; +import clack from '../utils/clack'; +import chalk from 'chalk'; +import * as semver from 'semver'; import { getNextJsRouter, getNextJsVersionBucket, @@ -15,11 +19,14 @@ import { /** * Next.js framework configuration for the universal agent runner. */ +const MINIMUM_NEXTJS_VERSION = '15.3.0'; + const NEXTJS_AGENT_CONFIG: FrameworkConfig = { metadata: { name: 'Next.js', integration: Integration.nextjs, docsUrl: 'https://posthog.com/docs/libraries/next-js', + unsupportedVersionDocsUrl: 'https://posthog.com/docs/libraries/next-js', abortMessage: 'This wizard uses an LLM agent to intelligently modify your project. Please view the docs to setup Next.js manually instead: https://posthog.com/docs/libraries/next-js', gatherContext: async (options: WizardOptions) => { @@ -94,5 +101,25 @@ export async function runNextjsWizardAgent( enableDebugLogs(); } + // Check Next.js version - agent wizard requires >= 15.3.0 + const packageJson = await getPackageDotJson(options); + const nextVersion = getPackageVersion('next', packageJson); + + if (nextVersion) { + const coercedVersion = semver.coerce(nextVersion); + if (coercedVersion && semver.lt(coercedVersion, MINIMUM_NEXTJS_VERSION)) { + const docsUrl = + NEXTJS_AGENT_CONFIG.metadata.unsupportedVersionDocsUrl ?? + NEXTJS_AGENT_CONFIG.metadata.docsUrl; + + clack.log.warn( + `Sorry: the wizard can't help you with Next.js ${nextVersion}. Upgrade to Next.js ${MINIMUM_NEXTJS_VERSION} or later, or check out the manual setup guide.`, + ); + clack.log.info(`Setup Next.js manually: ${chalk.cyan(docsUrl)}`); + clack.outro('PostHog wizard will see you next time!'); + return; + } + } + await runAgentWizard(NEXTJS_AGENT_CONFIG, options); } diff --git a/src/nextjs/nextjs-wizard.ts b/src/nextjs/nextjs-wizard.ts deleted file mode 100644 index e2aaa92..0000000 --- a/src/nextjs/nextjs-wizard.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* eslint-disable max-lines */ -import { - abort, - askForAIConsent, - confirmContinueIfNoOrDirtyGitRepo, - ensurePackageIsInstalled, - getOrAskForProjectData, - getPackageDotJson, - getPackageManager, - installPackage, - isUsingTypeScript, - printWelcome, -} from '../utils/clack-utils'; -import { getPackageVersion, hasPackageInstalled } from '../utils/package-json'; -import { - getNextJsRouter, - getNextJsRouterName, - getNextJsVersionBucket, - NextJsRouter, -} from './utils'; -import clack from '../utils/clack'; -import { Integration } from '../lib/constants'; -import { - getNextjsAppRouterDocs, - getNextjsPagesRouterDocs, - getModernNextjsDocs, -} from './docs'; -import { analytics } from '../utils/analytics'; -import { - generateFileChangesForIntegration, - getFilesToChange, - getRelevantFilesForIntegration, -} from '../utils/file-utils'; -import type { WizardOptions } from '../utils/types'; -import { askForCloudRegion } from '../utils/clack-utils'; -import { getOutroMessage } from '../lib/messages'; -import { - addEditorRulesStep, - addOrUpdateEnvironmentVariablesStep, - runPrettierStep, - addMCPServerToClientsStep, - uploadEnvironmentVariablesStep, -} from '../steps'; - -import * as semver from 'semver'; - -export async function runNextjsWizard(options: WizardOptions): Promise { - printWelcome({ - wizardName: 'PostHog Next.js wizard', - }); - - const aiConsent = await askForAIConsent(options); - - if (!aiConsent) { - await abort( - 'The Next.js wizard requires AI to get setup right now. Please view the docs to setup Next.js manually instead: https://posthog.com/docs/libraries/next-js', - 0, - ); - } - - const cloudRegion = options.cloudRegion ?? (await askForCloudRegion()); - - const typeScriptDetected = isUsingTypeScript(options); - - await confirmContinueIfNoOrDirtyGitRepo(options); - - const packageJson = await getPackageDotJson(options); - - await ensurePackageIsInstalled(packageJson, 'next', 'Next.js'); - - const nextVersion = getPackageVersion('next', packageJson); - - analytics.setTag('nextjs-version', getNextJsVersionBucket(nextVersion)); - - const { projectApiKey, accessToken, host, projectId } = - await getOrAskForProjectData({ - ...options, - cloudRegion, - }); - - const sdkAlreadyInstalled = hasPackageInstalled('posthog-js', packageJson); - - analytics.setTag('sdk-already-installed', sdkAlreadyInstalled); - - const { packageManager: packageManagerFromInstallStep } = - await installPackage({ - packageName: 'posthog-js', - packageNameDisplayLabel: 'posthog-js', - alreadyInstalled: !!packageJson?.dependencies?.['posthog-js'], - forceInstall: options.forceInstall, - askBeforeUpdating: false, - installDir: options.installDir, - integration: Integration.nextjs, - }); - - await installPackage({ - packageName: 'posthog-node', - packageNameDisplayLabel: 'posthog-node', - packageManager: packageManagerFromInstallStep, - alreadyInstalled: !!packageJson?.dependencies?.['posthog-node'], - forceInstall: options.forceInstall, - askBeforeUpdating: false, - installDir: options.installDir, - integration: Integration.nextjs, - }); - - const relevantFiles = await getRelevantFilesForIntegration({ - installDir: options.installDir, - integration: Integration.nextjs, - }); - - let installationDocumentation; // Documentation for the installation of the PostHog SDK - - if (instrumentationFileAvailable(nextVersion)) { - installationDocumentation = getModernNextjsDocs({ - host, - language: typeScriptDetected ? 'typescript' : 'javascript', - }); - - clack.log.info(`Reviewing PostHog documentation for Next.js`); - } else { - const router = await getNextJsRouter(options); - - installationDocumentation = getInstallationDocumentation({ - router, - host, - language: typeScriptDetected ? 'typescript' : 'javascript', - }); - - clack.log.info( - `Reviewing PostHog documentation for ${getNextJsRouterName(router)}`, - ); - } - - const filesToChange = await getFilesToChange({ - integration: Integration.nextjs, - relevantFiles, - documentation: installationDocumentation, - accessToken, - cloudRegion, - projectId, - }); - - await generateFileChangesForIntegration({ - integration: Integration.nextjs, - filesToChange, - accessToken, - installDir: options.installDir, - documentation: installationDocumentation, - cloudRegion, - projectId, - }); - - const packageManagerForOutro = - packageManagerFromInstallStep ?? (await getPackageManager(options)); - - await runPrettierStep({ - installDir: options.installDir, - integration: Integration.nextjs, - }); - - const { relativeEnvFilePath, addedEnvVariables } = - await addOrUpdateEnvironmentVariablesStep({ - variables: { - NEXT_PUBLIC_POSTHOG_KEY: projectApiKey, - NEXT_PUBLIC_POSTHOG_HOST: host, - }, - installDir: options.installDir, - integration: Integration.nextjs, - }); - - const uploadedEnvVars = await uploadEnvironmentVariablesStep( - { - NEXT_PUBLIC_POSTHOG_KEY: projectApiKey, - NEXT_PUBLIC_POSTHOG_HOST: host, - }, - { - integration: Integration.nextjs, - options, - }, - ); - - const addedEditorRules = await addEditorRulesStep({ - rulesName: 'next-rules.md', - installDir: options.installDir, - integration: Integration.nextjs, - }); - - await addMCPServerToClientsStep({ - cloudRegion, - integration: Integration.nextjs, - }); - - const outroMessage = getOutroMessage({ - options, - integration: Integration.nextjs, - cloudRegion, - addedEditorRules, - packageManager: packageManagerForOutro, - envFileChanged: addedEnvVariables ? relativeEnvFilePath : undefined, - uploadedEnvVars, - }); - - clack.outro(outroMessage); - - clack.outro( - 'Want to try our experimental event instrumentation? Run the wizard again with this argument: npx @posthog/wizard@latest event-setup', - ); - - await analytics.shutdown('success'); -} - -function instrumentationFileAvailable( - nextVersion: string | undefined, -): boolean { - const minimumVersion = '15.3.0'; //instrumentation-client.js|ts was introduced in 15.3 - - if (!nextVersion) { - return false; - } - const coercedNextVersion = semver.coerce(nextVersion); - if (!coercedNextVersion) { - return false; // Unable to parse nextVersion - } - return semver.gte(coercedNextVersion, minimumVersion); -} - -function getInstallationDocumentation({ - router, - host, - language, -}: { - router: NextJsRouter; - host: string; - language: 'typescript' | 'javascript'; -}) { - if (router === NextJsRouter.PAGES_ROUTER) { - return getNextjsPagesRouterDocs({ host, language }); - } - - return getNextjsAppRouterDocs({ host, language }); -} diff --git a/src/run.ts b/src/run.ts index 7716692..2a9faf0 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,15 +1,9 @@ -import { abortIfCancelled, getPackageDotJson } from './utils/clack-utils'; +import { abortIfCancelled } from './utils/clack-utils'; -import { runNextjsWizard } from './nextjs/nextjs-wizard'; import { runNextjsWizardAgent } from './nextjs/nextjs-wizard-agent'; import type { CloudRegion, WizardOptions } from './utils/types'; -import { - getIntegrationDescription, - Integration, - FeatureFlagDefinition, - WizardVariant, -} from './lib/constants'; +import { getIntegrationDescription, Integration } from './lib/constants'; import { readEnvironment } from './utils/environment'; import clack from './utils/clack'; import path from 'path'; @@ -22,8 +16,6 @@ import { runAstroWizard } from './astro/astro-wizard'; import { EventEmitter } from 'events'; import chalk from 'chalk'; import { RateLimitError } from './utils/errors'; -import { getPackageVersion } from './utils/package-json'; -import * as semver from 'semver'; EventEmitter.defaultMaxListeners = 50; @@ -75,7 +67,7 @@ export async function runWizard(argv: Args) { try { switch (integration) { case Integration.nextjs: - await chooseNextjsWizard(wizardOptions); + await runNextjsWizardAgent(wizardOptions); break; case Integration.react: await runReactWizard(wizardOptions); @@ -157,32 +149,3 @@ async function getIntegrationForSetup( return integration; } - -async function chooseNextjsWizard(options: WizardOptions): Promise { - try { - const packageJson = await getPackageDotJson(options); - const nextVersion = getPackageVersion('next', packageJson); - - // If Next.js < 15, use legacy wizard - if (nextVersion) { - const coercedVersion = semver.coerce(nextVersion); - if (coercedVersion && semver.lt(coercedVersion, '15.3.0')) { - await runNextjsWizard(options); - return; - } - } - - // Next.js >= 15 - check feature flag to determine which wizard to use - const flagValue = await analytics.getFeatureFlag( - FeatureFlagDefinition.NextV2, - ); - - if (flagValue === WizardVariant.Agent) { - await runNextjsWizardAgent(options); - } else { - await runNextjsWizard(options); - } - } catch (error) { - await runNextjsWizard(options); - } -}