From e8754102c7b804a79958688462dd220dd6bfe987 Mon Sep 17 00:00:00 2001 From: Vishakh Date: Fri, 27 Mar 2026 09:41:24 -0400 Subject: [PATCH 1/5] Initial Reddit Ads Integration --- .env.local.example | 13 +++++ app/api/reddit-conversion/route.ts | 85 +++++++++++++++++++++++++++ app/layout.tsx | 8 +++ lib/analytics.ts | 94 +++++++++++++++++++++++++++++- 4 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 app/api/reddit-conversion/route.ts diff --git a/.env.local.example b/.env.local.example index 175bc91..9830782 100644 --- a/.env.local.example +++ b/.env.local.example @@ -105,3 +105,16 @@ NEXT_PUBLIC_SUBSCRIPTION_CACHE_HOURS=1 # ANALYTICS (optional) # ============================================================================= # NEXT_PUBLIC_PLAUSIBLE_DOMAIN=your_domain.com + +# ============================================================================= +# REDDIT ADS - Pixel & Conversions API +# ============================================================================= +# Reddit Pixel ID (embedded in the Reddit Pixel script) +# Get this from: https://ads.reddit.com → Pixels & Events +# Format: a2_... (already set to a2_iom6tk9tutrs in layout.tsx) +REDDIT_PIXEL_ID=a2_iom6tk9tutrs + +# Reddit Conversions API Access Token (for server-side conversion tracking) +# Get this from: https://ads.reddit.com → Settings → API +# Format: Bearer token +REDDIT_CONVERSIONS_API_TOKEN=your_reddit_api_token_here diff --git a/app/api/reddit-conversion/route.ts b/app/api/reddit-conversion/route.ts new file mode 100644 index 0000000..e54528a --- /dev/null +++ b/app/api/reddit-conversion/route.ts @@ -0,0 +1,85 @@ +/** + * Reddit Conversions API endpoint + * + * Server-side conversion tracking for Reddit Ads + * Sends conversion events with match keys for improved attribution + */ + +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const { eventType, metadata } = await request.json(); + + // Get Reddit API credentials from environment + const pixelId = process.env.REDDIT_PIXEL_ID || 'a2_iom6tk9tutrs'; + const accessToken = process.env.REDDIT_CONVERSIONS_API_TOKEN; + + if (!accessToken) { + console.warn('[Reddit Conversion] No access token configured'); + return NextResponse.json( + { error: 'Reddit Conversions API not configured' }, + { status: 500 } + ); + } + + // Extract match keys from request + const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || + request.headers.get('x-real-ip') || + 'unknown'; + const userAgent = request.headers.get('user-agent') || 'unknown'; + + // Build conversion event payload + const payload = { + data: { + events: [ + { + event_at: Date.now(), + action_source: 'web', + type: { + tracking_type: eventType, // 'SignUp' or 'Purchase' + }, + click_id: undefined, // Reddit click ID if available from URL params + user: { + ip_address: ip !== 'unknown' ? ip : undefined, + user_agent: userAgent !== 'unknown' ? userAgent : undefined, + }, + metadata: metadata || undefined, + }, + ], + }, + }; + + // Send to Reddit Conversions API + const response = await fetch( + `https://ads-api.reddit.com/api/v3/pixels/${pixelId}/conversion_events`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[Reddit Conversion] API error:', response.status, errorText); + return NextResponse.json( + { error: 'Failed to send conversion event' }, + { status: response.status } + ); + } + + const result = await response.json(); + return NextResponse.json({ success: true, result }); + + } catch (error) { + console.error('[Reddit Conversion] Error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 2ddb51a..28cb2bb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -73,6 +73,14 @@ export default function RootLayout({ }); `} + {/* Reddit Pixel */} + diff --git a/lib/analytics.ts b/lib/analytics.ts index b9fcfed..7145a08 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -5,7 +5,7 @@ * Privacy-compliant with no PII tracking. */ -// Extend Window interface to include gtag +// Extend Window interface to include gtag and rdt declare global { interface Window { gtag?: ( @@ -14,6 +14,7 @@ declare global { params?: Record ) => void; dataLayer?: any[]; + rdt?: (command: string, ...args: any[]) => void; } } @@ -30,6 +31,48 @@ function trackEvent(eventName: string, params?: Record) { } } +/** + * Safely send an event to Reddit Pixel + */ +function trackRedditEvent(eventName: string, metadata?: Record) { + if (typeof window !== 'undefined' && window.rdt) { + try { + if (metadata) { + window.rdt('track', eventName, metadata); + } else { + window.rdt('track', eventName); + } + } catch (error) { + console.warn('Reddit Pixel tracking failed:', error); + } + } +} + +/** + * Send event to Reddit Conversions API (server-side) + */ +async function trackRedditConversion( + eventType: string, + metadata?: Record +) { + if (typeof window !== 'undefined') { + try { + await fetch('/api/reddit-conversion', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + eventType, + metadata, + }), + }); + } catch (error) { + console.warn('Reddit Conversions API tracking failed:', error); + } + } +} + // ============================================================================ // CORE USER JOURNEY EVENTS (12 total) // ============================================================================ @@ -96,9 +139,22 @@ export function trackAIAnalysisRun() { * User loaded a genotype file (DNA data) */ export function trackGenotypeFileLoaded(fileSize: number, variantCount: number) { - trackEvent('genotype_file_loaded', { + const metadata = { file_size_kb: Math.round(fileSize / 1024), variant_count: variantCount, + }; + + trackEvent('genotype_file_loaded', metadata); + + // Track as SignUp event on Reddit (DNA upload is a key conversion) + const conversionId = `dna_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + trackRedditEvent('SignUp', { + conversionId, + item_count: 1, + }); + trackRedditConversion('SignUp', { + conversion_id: conversionId, + item_count: 1, }); } @@ -175,6 +231,23 @@ export function trackSubscribedWithCreditCard(durationDays: number) { trackEvent('subscribed_credit_card', { duration_days: durationDays, }); + + // Track as Purchase event on Reddit + const conversionId = `sub_cc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const value = (durationDays / 30) * 4.99; // Monthly price is $4.99 + + trackRedditEvent('Purchase', { + conversionId, + currency: 'USD', + value: value, + item_count: 1, + }); + trackRedditConversion('Purchase', { + conversion_id: conversionId, + currency: 'USD', + value: value, + item_count: 1, + }); } /** @@ -184,6 +257,23 @@ export function trackSubscribedWithStablecoin(durationDays: number) { trackEvent('subscribed_stablecoin', { duration_days: durationDays, }); + + // Track as Purchase event on Reddit + const conversionId = `sub_crypto_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const value = (durationDays / 30) * 4.99; // Monthly price is $4.99 + + trackRedditEvent('Purchase', { + conversionId, + currency: 'USD', + value: value, + item_count: 1, + }); + trackRedditConversion('Purchase', { + conversion_id: conversionId, + currency: 'USD', + value: value, + item_count: 1, + }); } /** From 1071b0f80f22abbbb410c2d7a9a4114b8e83e84b Mon Sep 17 00:00:00 2001 From: Vishakh Date: Fri, 27 Mar 2026 09:45:00 -0400 Subject: [PATCH 2/5] Fixed env var --- .env.local.example | 9 +++++---- app/api/reddit-conversion/route.ts | 6 +++--- app/layout.tsx | 16 +++++++++------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.env.local.example b/.env.local.example index 9830782..ec78d73 100644 --- a/.env.local.example +++ b/.env.local.example @@ -109,12 +109,13 @@ NEXT_PUBLIC_SUBSCRIPTION_CACHE_HOURS=1 # ============================================================================= # REDDIT ADS - Pixel & Conversions API # ============================================================================= -# Reddit Pixel ID (embedded in the Reddit Pixel script) +# Reddit Pixel ID (used in both client-side pixel and server-side Conversions API) # Get this from: https://ads.reddit.com → Pixels & Events -# Format: a2_... (already set to a2_iom6tk9tutrs in layout.tsx) -REDDIT_PIXEL_ID=a2_iom6tk9tutrs +# Format: a2_... +# NEXT_PUBLIC_ prefix required for browser access +NEXT_PUBLIC_REDDIT_PIXEL_ID=a2_iom6tk9tutrs -# Reddit Conversions API Access Token (for server-side conversion tracking) +# Reddit Conversions API Access Token (server-side only) # Get this from: https://ads.reddit.com → Settings → API # Format: Bearer token REDDIT_CONVERSIONS_API_TOKEN=your_reddit_api_token_here diff --git a/app/api/reddit-conversion/route.ts b/app/api/reddit-conversion/route.ts index e54528a..2701d46 100644 --- a/app/api/reddit-conversion/route.ts +++ b/app/api/reddit-conversion/route.ts @@ -12,11 +12,11 @@ export async function POST(request: NextRequest) { const { eventType, metadata } = await request.json(); // Get Reddit API credentials from environment - const pixelId = process.env.REDDIT_PIXEL_ID || 'a2_iom6tk9tutrs'; + const pixelId = process.env.NEXT_PUBLIC_REDDIT_PIXEL_ID; const accessToken = process.env.REDDIT_CONVERSIONS_API_TOKEN; - if (!accessToken) { - console.warn('[Reddit Conversion] No access token configured'); + if (!pixelId || !accessToken) { + console.warn('[Reddit Conversion] Reddit API not configured'); return NextResponse.json( { error: 'Reddit Conversions API not configured' }, { status: 500 } diff --git a/app/layout.tsx b/app/layout.tsx index 28cb2bb..241eb28 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -74,13 +74,15 @@ export default function RootLayout({ `} {/* Reddit Pixel */} - + {process.env.NEXT_PUBLIC_REDDIT_PIXEL_ID && ( + + )} From 49445d2b15fa086dac92bf7e9dd71f360950743c Mon Sep 17 00:00:00 2001 From: Vishakh Date: Fri, 27 Mar 2026 09:48:06 -0400 Subject: [PATCH 3/5] Updated README to reflect Reddit Ads integration. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 15ea6ba..8ed38cb 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,10 @@ npm start - `NEXT_PUBLIC_EVM_PAYMENT_WALLET_ADDRESS`: EVM wallet address where users send ETH/USDC payments - `NEXT_PUBLIC_SUBSCRIPTION_CACHE_HOURS`: Cache duration in hours (default: 1) +**Reddit Ads (Optional):** +- `NEXT_PUBLIC_REDDIT_PIXEL_ID`: Reddit Pixel ID for tracking conversions (client + server) +- `REDDIT_CONVERSIONS_API_TOKEN`: Reddit Conversions API access token (server-side only) + See `.env.local.example` for complete configuration details. ### Database Schema From ccc73f3c87aeed6e1e550f02e20b67df20fbbf56 Mon Sep 17 00:00:00 2001 From: Vishakh Date: Fri, 27 Mar 2026 10:06:42 -0400 Subject: [PATCH 4/5] Refactor Reddit conversion payload to exclude undefined fields and simplify user data handling. --- app/api/reddit-conversion/route.ts | 33 ++++++++++++++++-------------- lib/analytics.ts | 2 -- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/api/reddit-conversion/route.ts b/app/api/reddit-conversion/route.ts index 2701d46..5c0c9b0 100644 --- a/app/api/reddit-conversion/route.ts +++ b/app/api/reddit-conversion/route.ts @@ -30,23 +30,26 @@ export async function POST(request: NextRequest) { const userAgent = request.headers.get('user-agent') || 'unknown'; // Build conversion event payload + // Remove undefined fields as Reddit API may reject them + const event: any = { + event_at: Date.now(), + action_source: 'WEBSITE', + type: { + tracking_type: eventType, // 'SignUp' or 'Purchase' + }, + }; + + // Add optional fields only if they have values + const user: any = {}; + if (ip !== 'unknown') user.ip_address = ip; + if (userAgent !== 'unknown') user.user_agent = userAgent; + if (Object.keys(user).length > 0) event.user = user; + + if (metadata) event.metadata = metadata; + const payload = { data: { - events: [ - { - event_at: Date.now(), - action_source: 'web', - type: { - tracking_type: eventType, // 'SignUp' or 'Purchase' - }, - click_id: undefined, // Reddit click ID if available from URL params - user: { - ip_address: ip !== 'unknown' ? ip : undefined, - user_agent: userAgent !== 'unknown' ? userAgent : undefined, - }, - metadata: metadata || undefined, - }, - ], + events: [event], }, }; diff --git a/lib/analytics.ts b/lib/analytics.ts index 7145a08..c24b41f 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -150,11 +150,9 @@ export function trackGenotypeFileLoaded(fileSize: number, variantCount: number) const conversionId = `dna_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; trackRedditEvent('SignUp', { conversionId, - item_count: 1, }); trackRedditConversion('SignUp', { conversion_id: conversionId, - item_count: 1, }); } From 59dd931306b5d566a691878a30a1c1b27c8a2ddf Mon Sep 17 00:00:00 2001 From: Vishakh Date: Fri, 27 Mar 2026 10:25:47 -0400 Subject: [PATCH 5/5] Add Reddit analytics tracking for Explore tab and improve search event tracking - Introduced `trackExploreTabViewed` to monitor Explore tab views. - Enhanced `trackQueryRun` to support optional Reddit tracking. - Updated search tracking to ensure only user-initiated searches are logged. - Changed Reddit conversion event from `SignUp` to `Lead` for DNA uploads. - Refined handling of search filters and user actions to improve analytics accuracy. --- app/page.tsx | 23 +++++++++++++++++++++-- lib/analytics.ts | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 9ab2f52..635bf7d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -26,6 +26,7 @@ import { trackSearch, trackRunAllStarted, trackQueryRun, + trackExploreTabViewed, } from "@/lib/analytics"; type SortOption = "relevance" | "power" | "recent" | "alphabetical"; @@ -212,6 +213,10 @@ function MainContent() { // Track client-side mounting to prevent hydration errors const [mounted, setMounted] = useState(false); + // Track if search change is user-initiated (for Reddit analytics) + const userInitiatedSearchRef = useRef(false); + const previousSearchRef = useRef(''); + useEffect(() => { setMounted(true); }, []); @@ -238,6 +243,13 @@ function MainContent() { } }, []); + // Track Explore tab view + useEffect(() => { + if (activeTab === 'explore' && mounted) { + trackExploreTabViewed(); + } + }, [activeTab, mounted]); + // Persist active tab in dev-mode useEffect(() => { if (isDevModeEnabled()) { @@ -340,6 +352,12 @@ function MainContent() { // Debounce search input to avoid excessive API calls useEffect(() => { + // Check if this is a user-initiated change + if (filters.search !== previousSearchRef.current) { + userInitiatedSearchRef.current = true; + previousSearchRef.current = filters.search; + } + const timer = setTimeout(() => { setDebouncedSearch(filters.search); }, 400); @@ -453,9 +471,10 @@ function MainContent() { const totalLoadTime = endTime - startTime; setLoadTime(totalLoadTime); - // Track search if there's a search query - if (debouncedSearch.trim()) { + // Track search if there's a search query and it was user-initiated + if (debouncedSearch.trim() && userInitiatedSearchRef.current) { trackSearch(debouncedSearch, filteredData.length, totalLoadTime); + userInitiatedSearchRef.current = false; // Reset flag after tracking } // Append results if offset > 0 (Load More), otherwise replace diff --git a/lib/analytics.ts b/lib/analytics.ts index c24b41f..7211029 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -109,13 +109,30 @@ export function trackOnboardingStepViewed(step: string) { }); } +/** + * User viewed the Explore tab + */ +export function trackExploreTabViewed() { + trackEvent('explore_tab_viewed'); + + // Track as ViewContent event on Reddit + trackRedditEvent('ViewContent'); + trackRedditConversion('ViewContent'); +} + /** * User ran a search query to find studies */ -export function trackQueryRun(resultCount: number) { +export function trackQueryRun(resultCount: number, shouldTrackReddit: boolean = false) { trackEvent('query_run', { result_count: resultCount, }); + + // Only track as Search event on Reddit if explicitly requested (user-initiated search) + if (shouldTrackReddit) { + trackRedditEvent('Search'); + trackRedditConversion('Search'); + } } /** @@ -146,12 +163,12 @@ export function trackGenotypeFileLoaded(fileSize: number, variantCount: number) trackEvent('genotype_file_loaded', metadata); - // Track as SignUp event on Reddit (DNA upload is a key conversion) + // Track as Lead event on Reddit (DNA upload is a lead generation action) const conversionId = `dna_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - trackRedditEvent('SignUp', { + trackRedditEvent('Lead', { conversionId, }); - trackRedditConversion('SignUp', { + trackRedditConversion('Lead', { conversion_id: conversionId, }); } @@ -186,6 +203,15 @@ export function trackPremiumSectionViewed() { */ export function trackUserLoggedIn() { trackEvent('user_logged_in'); + + // Track as SignUp event on Reddit + const conversionId = `login_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + trackRedditEvent('SignUp', { + conversionId, + }); + trackRedditConversion('SignUp', { + conversion_id: conversionId, + }); } /** @@ -302,7 +328,8 @@ export function trackFileUploadSuccess(fileSize: number, variantCount: number) { /** @deprecated Use trackQueryRun instead */ export function trackSearch(query: string, resultCount: number, loadTime: number) { - trackQueryRun(resultCount); + // Pass true to indicate this is a user-initiated search (should track on Reddit) + trackQueryRun(resultCount, true); } /** @deprecated Use trackMatchRevealed instead */