diff --git a/bin/init.js b/bin/init.js index a916fc5..90575ff 100644 --- a/bin/init.js +++ b/bin/init.js @@ -76,7 +76,7 @@ export async function init() { { type: 'confirm', name: 'useNeynar', - message: '🪐 Neynar is an API that makes it easy to build on Farcaster.\n\n' + + message: `🪐 ${purple}${bright}${italic}Neynar is an API that makes it easy to build on Farcaster.${reset}\n\n` + 'Benefits of using Neynar in your mini app:\n' + '- Pre-configured webhook handling (no setup required)\n' + '- Automatic mini app analytics in your dev portal\n' + @@ -252,6 +252,17 @@ export async function init() { ]); answers.useTunnel = hostingAnswer.useTunnel; + // Ask about analytics opt-out + const analyticsAnswer = await inquirer.prompt([ + { + type: 'confirm', + name: 'enableAnalytics', + message: 'Would you like to help improve Neynar products by sharing usage data from your mini app?', + default: true + } + ]); + answers.enableAnalytics = analyticsAnswer.enableAnalytics; + const projectName = answers.projectName; const projectDirName = projectName.replace(/\s+/g, '-').toLowerCase(); const projectPath = path.join(process.cwd(), projectDirName); @@ -374,6 +385,8 @@ export async function init() { fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_PRIMARY_CATEGORY="${answers.primaryCategory}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_TAGS="${answers.tags.join(',')}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`); + fs.appendFileSync(envPath, `\nNEXT_PUBLIC_ANALYTICS_ENABLED="${answers.enableAnalytics}"`); + fs.appendFileSync(envPath, `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`); if (useNeynar && neynarApiKey && neynarClientId) { fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`); diff --git a/package.json b/package.json index 1109c71..ea208f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.2.28", + "version": "1.2.29", "type": "module", "private": false, "access": "public", diff --git a/scripts/build.js b/scripts/build.js index 2ea2932..8dc054a 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -355,6 +355,9 @@ async function main() { `NEXT_PUBLIC_FRAME_TAGS="${process.env.NEXT_PUBLIC_FRAME_TAGS || ''}"`, `NEXT_PUBLIC_FRAME_BUTTON_TEXT="${buttonText}"`, + // Analytics + `NEXT_PUBLIC_ANALYTICS_ENABLED="${process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false'}"`, + // Neynar configuration (if it exists in current env) ...(process.env.NEYNAR_API_KEY ? [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), diff --git a/scripts/deploy.js b/scripts/deploy.js index b5dd3e5..4ebaab8 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -121,6 +121,7 @@ async function loadEnvLocal() { 'NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY', 'NEXT_PUBLIC_FRAME_TAGS', 'NEXT_PUBLIC_FRAME_BUTTON_TEXT', + 'NEXT_PUBLIC_ANALYTICS_ENABLED', 'NEYNAR_API_KEY', 'NEYNAR_CLIENT_ID' ]; diff --git a/src/components/providers/FrameProvider.tsx b/src/components/providers/FrameProvider.tsx index 07db4c9..a4d29d9 100644 --- a/src/components/providers/FrameProvider.tsx +++ b/src/components/providers/FrameProvider.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from "react"; import sdk, { type Context, type FrameNotificationDetails, AddMiniApp } from "@farcaster/frame-sdk"; import { createStore } from "mipd"; import React from "react"; +import { logEvent } from "../../lib/amplitude"; interface FrameContextType { isSDKLoaded: boolean; @@ -72,36 +73,54 @@ export function useFrame() { setContext(context); setIsSDKLoaded(true); + const amplitudeBaseEvent = { + fid: context.user.fid, + username: context.user.username, + clientFid: context.client.clientFid, + }; + const amplitudeUserId = `${context.user.fid}-${context.client.clientFid}`; + + logEvent("Frame Opened", { + ...amplitudeBaseEvent, + location: context.location, + added: context.client.added, + }, amplitudeUserId); + // Set up event listeners sdk.on("frameAdded", ({ notificationDetails }) => { console.log("Frame added", notificationDetails); setAdded(true); setNotificationDetails(notificationDetails ?? null); setLastEvent("Frame added"); + logEvent("Frame Added", amplitudeBaseEvent, amplitudeUserId); }); sdk.on("frameAddRejected", ({ reason }) => { console.log("Frame add rejected", reason); setAdded(false); setLastEvent(`Frame add rejected: ${reason}`); + logEvent("Frame Add Rejected", amplitudeBaseEvent, amplitudeUserId); }); sdk.on("frameRemoved", () => { console.log("Frame removed"); setAdded(false); setLastEvent("Frame removed"); + logEvent("Frame Removed", amplitudeBaseEvent, amplitudeUserId); }); sdk.on("notificationsEnabled", ({ notificationDetails }) => { console.log("Notifications enabled", notificationDetails); setNotificationDetails(notificationDetails ?? null); setLastEvent("Notifications enabled"); + logEvent("Notifications Enabled", amplitudeBaseEvent, amplitudeUserId); }); sdk.on("notificationsDisabled", () => { console.log("Notifications disabled"); setNotificationDetails(null); setLastEvent("Notifications disabled"); + logEvent("Notifications Disabled", amplitudeBaseEvent, amplitudeUserId); }); sdk.on("primaryButtonClicked", () => { diff --git a/src/lib/amplitude.ts b/src/lib/amplitude.ts new file mode 100644 index 0000000..4dbca45 --- /dev/null +++ b/src/lib/amplitude.ts @@ -0,0 +1,34 @@ +import { APP_URL } from "./constants"; + +// Amplitude tracking -- only runs if configured via the CLI or in the .env file +export function logEvent( + eventType: string, + eventProperties: Record = {}, + deviceId: string | null = null +) { + if (process.env.NEXT_PUBLIC_ANALYTICS_ENABLED?.toLowerCase() !== 'true' || process.env.NODE_ENV !== "production") { + return; + } + + const event = { + event_type: eventType, + api_key: '0c4fe46171b9bb8eca2ca61eb71f2e19', + time: Date.now(), + user_id: APP_URL, + ...(deviceId && { device_id: deviceId }), + ...(Object.keys(eventProperties).length && { event_properties: eventProperties }) + }; + + fetch('https://api2.amplitude.com/2/httpapi', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key: '0c4fe46171b9bb8eca2ca61eb71f2e19', + events: [event] + }) + }).catch(error => { + console.error('Amplitude tracking error:', error); + }); +} \ No newline at end of file