feat: add optional amplitude tracking

This commit is contained in:
veganbeef 2025-05-22 12:17:49 -07:00
parent 3fcd2f6e52
commit 7df556740d
No known key found for this signature in database
6 changed files with 72 additions and 2 deletions

View File

@ -76,7 +76,7 @@ export async function init() {
{ {
type: 'confirm', type: 'confirm',
name: 'useNeynar', 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' + 'Benefits of using Neynar in your mini app:\n' +
'- Pre-configured webhook handling (no setup required)\n' + '- Pre-configured webhook handling (no setup required)\n' +
'- Automatic mini app analytics in your dev portal\n' + '- Automatic mini app analytics in your dev portal\n' +
@ -252,6 +252,17 @@ export async function init() {
]); ]);
answers.useTunnel = hostingAnswer.useTunnel; 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 projectName = answers.projectName;
const projectDirName = projectName.replace(/\s+/g, '-').toLowerCase(); const projectDirName = projectName.replace(/\s+/g, '-').toLowerCase();
const projectPath = path.join(process.cwd(), projectDirName); 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_PRIMARY_CATEGORY="${answers.primaryCategory}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_TAGS="${answers.tags.join(',')}"`); 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_FRAME_BUTTON_TEXT="${answers.buttonText}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_ANALYTICS_ENABLED="${answers.enableAnalytics}"`);
fs.appendFileSync(envPath, `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`); fs.appendFileSync(envPath, `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`);
if (useNeynar && neynarApiKey && neynarClientId) { if (useNeynar && neynarApiKey && neynarClientId) {
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`); fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);

View File

@ -1,6 +1,6 @@
{ {
"name": "@neynar/create-farcaster-mini-app", "name": "@neynar/create-farcaster-mini-app",
"version": "1.2.28", "version": "1.2.29",
"type": "module", "type": "module",
"private": false, "private": false,
"access": "public", "access": "public",

View File

@ -355,6 +355,9 @@ async function main() {
`NEXT_PUBLIC_FRAME_TAGS="${process.env.NEXT_PUBLIC_FRAME_TAGS || ''}"`, `NEXT_PUBLIC_FRAME_TAGS="${process.env.NEXT_PUBLIC_FRAME_TAGS || ''}"`,
`NEXT_PUBLIC_FRAME_BUTTON_TEXT="${buttonText}"`, `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) // Neynar configuration (if it exists in current env)
...(process.env.NEYNAR_API_KEY ? ...(process.env.NEYNAR_API_KEY ?
[`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []),

View File

@ -121,6 +121,7 @@ async function loadEnvLocal() {
'NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY', 'NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY',
'NEXT_PUBLIC_FRAME_TAGS', 'NEXT_PUBLIC_FRAME_TAGS',
'NEXT_PUBLIC_FRAME_BUTTON_TEXT', 'NEXT_PUBLIC_FRAME_BUTTON_TEXT',
'NEXT_PUBLIC_ANALYTICS_ENABLED',
'NEYNAR_API_KEY', 'NEYNAR_API_KEY',
'NEYNAR_CLIENT_ID' 'NEYNAR_CLIENT_ID'
]; ];

View File

@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from "react";
import sdk, { type Context, type FrameNotificationDetails, AddMiniApp } from "@farcaster/frame-sdk"; import sdk, { type Context, type FrameNotificationDetails, AddMiniApp } from "@farcaster/frame-sdk";
import { createStore } from "mipd"; import { createStore } from "mipd";
import React from "react"; import React from "react";
import { logEvent } from "../../lib/amplitude";
interface FrameContextType { interface FrameContextType {
isSDKLoaded: boolean; isSDKLoaded: boolean;
@ -72,36 +73,54 @@ export function useFrame() {
setContext(context); setContext(context);
setIsSDKLoaded(true); 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 // Set up event listeners
sdk.on("frameAdded", ({ notificationDetails }) => { sdk.on("frameAdded", ({ notificationDetails }) => {
console.log("Frame added", notificationDetails); console.log("Frame added", notificationDetails);
setAdded(true); setAdded(true);
setNotificationDetails(notificationDetails ?? null); setNotificationDetails(notificationDetails ?? null);
setLastEvent("Frame added"); setLastEvent("Frame added");
logEvent("Frame Added", amplitudeBaseEvent, amplitudeUserId);
}); });
sdk.on("frameAddRejected", ({ reason }) => { sdk.on("frameAddRejected", ({ reason }) => {
console.log("Frame add rejected", reason); console.log("Frame add rejected", reason);
setAdded(false); setAdded(false);
setLastEvent(`Frame add rejected: ${reason}`); setLastEvent(`Frame add rejected: ${reason}`);
logEvent("Frame Add Rejected", amplitudeBaseEvent, amplitudeUserId);
}); });
sdk.on("frameRemoved", () => { sdk.on("frameRemoved", () => {
console.log("Frame removed"); console.log("Frame removed");
setAdded(false); setAdded(false);
setLastEvent("Frame removed"); setLastEvent("Frame removed");
logEvent("Frame Removed", amplitudeBaseEvent, amplitudeUserId);
}); });
sdk.on("notificationsEnabled", ({ notificationDetails }) => { sdk.on("notificationsEnabled", ({ notificationDetails }) => {
console.log("Notifications enabled", notificationDetails); console.log("Notifications enabled", notificationDetails);
setNotificationDetails(notificationDetails ?? null); setNotificationDetails(notificationDetails ?? null);
setLastEvent("Notifications enabled"); setLastEvent("Notifications enabled");
logEvent("Notifications Enabled", amplitudeBaseEvent, amplitudeUserId);
}); });
sdk.on("notificationsDisabled", () => { sdk.on("notificationsDisabled", () => {
console.log("Notifications disabled"); console.log("Notifications disabled");
setNotificationDetails(null); setNotificationDetails(null);
setLastEvent("Notifications disabled"); setLastEvent("Notifications disabled");
logEvent("Notifications Disabled", amplitudeBaseEvent, amplitudeUserId);
}); });
sdk.on("primaryButtonClicked", () => { sdk.on("primaryButtonClicked", () => {

34
src/lib/amplitude.ts Normal file
View File

@ -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<string, any> = {},
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);
});
}