diff --git a/app/existing/[repoUrl]/repo-scan-client.tsx b/app/existing/[repoUrl]/repo-scan-client.tsx index e66989e..5a22947 100644 --- a/app/existing/[repoUrl]/repo-scan-client.tsx +++ b/app/existing/[repoUrl]/repo-scan-client.tsx @@ -14,6 +14,8 @@ import { generateFromRepoScan } from "@/lib/scan-generate" import FinalOutputView from "@/components/final-output-view" import RepoScanLoader from "@/components/repo-scan-loader" import type { GeneratedFileResult } from "@/types/output" +import { track } from "@/lib/mixpanel" +import { ANALYTICS_EVENTS } from "@/lib/analytics-events" const buildQuery = (url: string) => `/api/scan-repo?url=${encodeURIComponent(url)}` const CONVENTIONS_DOC_URL = @@ -110,23 +112,27 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps) })) }, [scanResult]) - const handleStartScan = () => { - if (!repoUrlForScan) { - return - } - - setHasConfirmed(true) - setScanToken((token) => token + 1) + const handleStartScan = () => { + if (!repoUrlForScan) { + return } - const handleRetryScan = () => { - if (!repoUrlForScan) { - return - } + setHasConfirmed(true) + setScanToken((token) => token + 1) + + track(ANALYTICS_EVENTS.REPO_SCAN_START, { repo: repoUrlForScan }) + } - setScanToken((token) => token + 1) + const handleRetryScan = () => { + if (!repoUrlForScan) { + return } + setScanToken((token) => token + 1) + + track(ANALYTICS_EVENTS.REPO_SCAN_RETRY, { repo: repoUrlForScan }) + } + const warnings = scanResult?.warnings ?? [] const stackMeta = scanResult?.conventions ?? null const detectedStackId = stackMeta?.stack ?? null diff --git a/app/existing/existing-repo-entry-client.tsx b/app/existing/existing-repo-entry-client.tsx index ec785ca..a149990 100644 --- a/app/existing/existing-repo-entry-client.tsx +++ b/app/existing/existing-repo-entry-client.tsx @@ -8,6 +8,8 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { normalizeGitHubRepoInput } from "@/lib/github" +import { track } from "@/lib/mixpanel" +import { ANALYTICS_EVENTS } from "@/lib/analytics-events" export function ExistingRepoEntryClient() { const router = useRouter() @@ -30,6 +32,11 @@ export function ExistingRepoEntryClient() { const encoded = encodeURIComponent(normalized) + track(ANALYTICS_EVENTS.REPO_ANALYZE_SUBMIT, { + inputProvided: value.length > 0, + url: normalized, + }) + router.push(`/existing/${encoded}`) } diff --git a/app/new/stack/stack-summary-page.tsx b/app/new/stack/stack-summary-page.tsx index 80ab27c..f5eb0f1 100644 --- a/app/new/stack/stack-summary-page.tsx +++ b/app/new/stack/stack-summary-page.tsx @@ -26,6 +26,8 @@ import type { } from "@/types/wizard" import type { GeneratedFileResult } from "@/types/output" import { WizardEditAnswerDialog } from "@/components/wizard-edit-answer-dialog" +import { track } from "@/lib/mixpanel" +import { ANALYTICS_EVENTS } from "@/lib/analytics-events" const fileOptions = getFileOptions() const fileSummaryQuestion = getFileSummaryQuestion() @@ -210,6 +212,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { }, [wizardSteps]) const handleEditClick = (questionId: string) => { + track(ANALYTICS_EVENTS.SUMMARY_EDIT_OPEN, { questionId }) setEditingQuestionId(questionId) } @@ -226,6 +229,7 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { const trimmed = submittedValue.trim() const nextFreeText: FreeTextResponses = (() => { if (trimmed.length === 0) { + track(ANALYTICS_EVENTS.SUMMARY_EDIT_FREE_TEXT_CLEARED, { questionId: question.id }) if (!(question.id in freeTextResponses)) { return { ...freeTextResponses } } @@ -235,6 +239,10 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { return next } + track(ANALYTICS_EVENTS.SUMMARY_EDIT_FREE_TEXT_SAVED, { + questionId: question.id, + length: trimmed.length, + }) return { ...freeTextResponses, [question.id]: trimmed, @@ -314,6 +322,13 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) { }) } + track(ANALYTICS_EVENTS.SUMMARY_EDIT_ANSWER_SELECTED, { + questionId: question.id, + answerValue: answer.value, + answerLabel: answer.label, + allowMultiple: question.allowMultiple ?? false, + }) + if (!question.allowMultiple) { handleCloseEdit() } diff --git a/components/instructions-wizard.tsx b/components/instructions-wizard.tsx index 6952470..e8ea2f3 100644 --- a/components/instructions-wizard.tsx +++ b/components/instructions-wizard.tsx @@ -28,6 +28,8 @@ import { import { persistWizardState, clearWizardState } from "@/lib/wizard-storage" import { WizardAnswerGrid } from "./wizard-answer-grid" import { WizardConfirmationDialog } from "./wizard-confirmation-dialog" +import { track } from "@/lib/mixpanel" +import { ANALYTICS_EVENTS } from "@/lib/analytics-events" const suffixSteps = getSuffixSteps() const buildStepSignature = (step: WizardStep) => `${step.id}::${step.questions.map((question) => question.id).join("|")}` @@ -559,6 +561,16 @@ export function InstructionsWizard({ return } + // Track answer selection + track(ANALYTICS_EVENTS.WIZARD_ANSWER_SELECTED, { + questionId: currentQuestion.id, + answerValue: answer.value, + answerLabel: answer.label, + allowMultiple: currentQuestion.allowMultiple ?? false, + isDefault: Boolean(answer.isDefault), + stepId: currentStep?.id ?? null, + }) + void handleQuestionAnswerSelection(currentQuestion, answer) } @@ -704,6 +716,11 @@ export function InstructionsWizard({ return } + track(ANALYTICS_EVENTS.WIZARD_FREE_TEXT_SAVED, { + questionId: currentQuestion.id, + length: currentFreeTextValue.trim().length, + }) + commitFreeTextValue(currentQuestion, currentFreeTextValue) } @@ -712,6 +729,10 @@ export function InstructionsWizard({ return } + track(ANALYTICS_EVENTS.WIZARD_FREE_TEXT_CLEARED, { + questionId: currentQuestion.id, + }) + commitFreeTextValue(currentQuestion, "", { allowAutoAdvance: false }) } @@ -733,6 +754,14 @@ export function InstructionsWizard({ const isStackQuestion = currentQuestion.id === STACK_QUESTION_ID + // Track default use + track(ANALYTICS_EVENTS.WIZARD_USE_DEFAULT, { + questionId: currentQuestion.id, + answerValue: defaultAnswer.value, + answerLabel: defaultAnswer.label, + isStackQuestion, + }) + if (isStackQuestion) { await loadStackQuestions(defaultAnswer.value, defaultAnswer.label, { skipFastTrackPrompt: autoStartAfterStackSelection, @@ -769,6 +798,7 @@ export function InstructionsWizard({ } const requestResetWizard = () => { + track(ANALYTICS_EVENTS.WIZARD_RESET) setPendingConfirmation("reset") } diff --git a/lib/analytics-events.ts b/lib/analytics-events.ts index b9f69d3..61fe905 100644 --- a/lib/analytics-events.ts +++ b/lib/analytics-events.ts @@ -1,6 +1,22 @@ export const ANALYTICS_EVENTS = { PAGE_VIEW: "Page View", CREATE_INSTRUCTIONS_FILE: "Create My Instructions File", + // Wizard interactions + WIZARD_ANSWER_SELECTED: "Wizard Answer Selected", + WIZARD_USE_DEFAULT: "Wizard Use Default", + WIZARD_FREE_TEXT_SAVED: "Wizard Free Text Saved", + WIZARD_FREE_TEXT_CLEARED: "Wizard Free Text Cleared", + WIZARD_RESET: "Wizard Reset", + // Summary dialog interactions + SUMMARY_EDIT_OPEN: "Summary Edit Open", + SUMMARY_EDIT_ANSWER_SELECTED: "Summary Edit Answer Selected", + SUMMARY_EDIT_FREE_TEXT_SAVED: "Summary Edit Free Text Saved", + SUMMARY_EDIT_FREE_TEXT_CLEARED: "Summary Edit Free Text Cleared", + // Existing repo / scan interactions + REPO_ANALYZE_SUBMIT: "Existing Repo Analyze Submit", + REPO_SCAN_START: "Repo Scan Start", + REPO_SCAN_RETRY: "Repo Scan Retry", + REPO_SCAN_GENERATE_FILE: "Repo Scan Generate File", } as const export type AnalyticsEvent = diff --git a/lib/scan-generate.ts b/lib/scan-generate.ts index e89d3e9..1750ffc 100644 --- a/lib/scan-generate.ts +++ b/lib/scan-generate.ts @@ -4,6 +4,8 @@ import { getFileOptions } from "@/lib/wizard-config" import { getMimeTypeForFormat } from "@/lib/wizard-utils" import type { RepoScanSummary } from "@/types/repo-scan" import type { GeneratedFileResult } from "@/types/output" +import { track } from "@/lib/mixpanel" +import { ANALYTICS_EVENTS } from "@/lib/analytics-events" const fileOptions = getFileOptions() @@ -15,6 +17,12 @@ export async function generateFromRepoScan( ): Promise { const selected = fileOptions.find((f) => f.id === outputFileId) || null + track(ANALYTICS_EVENTS.REPO_SCAN_GENERATE_FILE, { + outputFile: outputFileId, + stack: scan.conventions?.stack ?? null, + language: scan.language ?? null, + }) + const res = await fetch(`/api/scan-generate/${encodeURIComponent(outputFileId)}`, { method: "POST", headers: {