Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 30 additions & 49 deletions app/api/events/[slug]/track-view/route.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,50 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { AnalyticsService } from '@/lib/services/analytics-service'

// Force Node.js runtime for API routes
export const runtime = 'nodejs'

/**
* POST /api/events/[slug]/track-view
* Track a view for an event with session-based deduplication
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const supabase = await createClient()
const { slug } = await params

// Get event by slug
const { data: event, error: eventError } = await supabase
.from('events')
.select('id, company_id, views')
.eq('slug', slug)
.single()

if (eventError || !event) {
return NextResponse.json(
{ error: 'Event not found' },
{ status: 404 }
)
}

// Increment view count
const { error: updateError } = await supabase
.from('events')
.update({ views: (event.views || 0) + 1 })
.eq('id', event.id)
// Get session ID from request body (generated on client)
const body = await request.json()
const sessionId = body.sessionId

if (updateError) {
console.error('Error updating view count:', updateError)
if (!sessionId) {
return NextResponse.json(
{ error: 'Failed to track view' },
{ status: 500 }
{ error: 'Session ID is required' },
{ status: 400 }
)
}

// Update company analytics if event has a company
if (event.company_id) {
const today = new Date().toISOString().split('T')[0]
// Check if this session has already viewed this event
// We rely on client-side sessionStorage to prevent duplicates
// The client will only send the request once per session

// Track the view
await AnalyticsService.trackEventView(slug)

// Upsert daily analytics
const { error: analyticsError } = await supabase.rpc(
'increment_company_analytics',
{
p_company_id: event.company_id,
p_date: today,
p_field: 'total_views',
p_increment: 1
}
)

if (analyticsError) {
console.error('Error updating company analytics:', analyticsError)
// Don't fail the request if analytics update fails
}
}

return NextResponse.json({ success: true })
return NextResponse.json(
{ success: true, message: 'View tracked successfully' },
{ status: 200 }
)
} catch (error) {
console.error('Error tracking view:', error)
console.error('Error tracking event view:', error)

// Don't fail the request if view tracking fails
// Just log the error and return success
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
{ success: true, message: 'View tracking skipped' },
{ status: 200 }
)
}
}
2 changes: 2 additions & 0 deletions app/events/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ export default function EventPage() {
} = useRegistrationStatus('event', event?.id?.toString() || '')

// Track analytics
console.log('[EventPage] About to call useAnalyticsTracking with slug:', slug)
const { trackClick } = useAnalyticsTracking({
eventSlug: slug,
trackView: true,
})
console.log('[EventPage] useAnalyticsTracking returned:', { trackClick })

useEffect(() => {
const fetchEvent = async () => {
Expand Down
98 changes: 90 additions & 8 deletions hooks/useAnalyticsTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ interface UseAnalyticsTrackingOptions {
trackClick?: boolean
}

// Helper to get or create session ID
const getSessionId = () => {
if (typeof window === 'undefined') return null

let sessionId = sessionStorage.getItem('analytics_session_id')
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
sessionStorage.setItem('analytics_session_id', sessionId)
}
return sessionId
}

// Helper to check if already viewed in this session
const hasViewedInSession = (slug: string, type: 'event' | 'hackathon'): boolean => {
if (typeof window === 'undefined') return false

const key = `viewed_${type}s`
const viewed = JSON.parse(sessionStorage.getItem(key) || '[]')
return viewed.includes(slug)
}

// Helper to mark as viewed in this session
const markAsViewed = (slug: string, type: 'event' | 'hackathon'): void => {
if (typeof window === 'undefined') return

const key = `viewed_${type}s`
const viewed = JSON.parse(sessionStorage.getItem(key) || '[]')
if (!viewed.includes(slug)) {
viewed.push(slug)
sessionStorage.setItem(key, JSON.stringify(viewed))
}
}

export function useAnalyticsTracking({
eventSlug,
hackathonId,
Expand All @@ -16,30 +49,79 @@ export function useAnalyticsTracking({
const viewTracked = useRef(false)
const clickTracked = useRef(false)

// Track view on mount
console.log('[Analytics Hook] Initialized with:', { eventSlug, hackathonId, trackView, trackClick })

// Track view on mount with session-based deduplication
useEffect(() => {
if (!trackView || viewTracked.current) return
console.log('[Analytics Hook] useEffect triggered', { trackView, viewTracked: viewTracked.current })
if (!trackView || viewTracked.current) {
console.log('[Analytics Hook] Skipping - trackView:', trackView, 'viewTracked:', viewTracked.current)
return
}

const trackViewAsync = async () => {
try {
const sessionId = getSessionId()
if (!sessionId) {
console.log('[Analytics] No session ID available')
return
}

if (eventSlug) {
await fetch(`/api/events/${eventSlug}/track-view`, {
console.log('[Analytics] Tracking view for event:', eventSlug)

// Check if already viewed in this session
if (hasViewedInSession(eventSlug, 'event')) {
console.log('[Analytics] Event already viewed in this session')
viewTracked.current = true
return
}

console.log('[Analytics] Sending track-view request...')
const response = await fetch(`/api/events/${eventSlug}/track-view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ sessionId }),
})
viewTracked.current = true

console.log('[Analytics] Track-view response:', response.status, response.ok)

if (response.ok) {
markAsViewed(eventSlug, 'event')
viewTracked.current = true
console.log('[Analytics] View tracked successfully')
} else {
console.error('[Analytics] Failed to track view:', await response.text())
}
} else if (hackathonId) {
await fetch(`/api/hackathons/${hackathonId}/track-view`, {
// Check if already viewed in this session
if (hasViewedInSession(hackathonId, 'hackathon')) {
viewTracked.current = true
return
}

const response = await fetch(`/api/hackathons/${hackathonId}/track-view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ sessionId }),
})
viewTracked.current = true

if (response.ok) {
markAsViewed(hackathonId, 'hackathon')
viewTracked.current = true
}
}
} catch (error) {
console.error('Error tracking view:', error)
}
}

// Track view after a short delay to avoid tracking bots
const timer = setTimeout(trackViewAsync, 1000)
// Track view after a short delay to avoid tracking bots and ensure real engagement
const timer = setTimeout(trackViewAsync, 2000)

return () => clearTimeout(timer)
}, [eventSlug, hackathonId, trackView])
Expand Down
63 changes: 63 additions & 0 deletions hooks/useViewTracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useRef } from 'react'

/**
* Hook to track event views with session-based deduplication
* Only tracks once per session per event
*/
export function useViewTracking(eventSlug: string, enabled: boolean = true) {
const hasTracked = useRef(false)

useEffect(() => {
// Don't track if disabled or already tracked
if (!enabled || hasTracked.current || !eventSlug) {
return
}

// Generate or get session ID
const getSessionId = () => {
let sessionId = sessionStorage.getItem('view_session_id')
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
sessionStorage.setItem('view_session_id', sessionId)
}
return sessionId
}

// Check if this event was already viewed in this session
const viewedEvents = JSON.parse(sessionStorage.getItem('viewed_events') || '[]')
if (viewedEvents.includes(eventSlug)) {
hasTracked.current = true
return
}

// Track the view
const trackView = async () => {
try {
const sessionId = getSessionId()

const response = await fetch(`/api/events/${eventSlug}/track-view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ sessionId }),
})

if (response.ok) {
// Mark as viewed in this session
viewedEvents.push(eventSlug)
sessionStorage.setItem('viewed_events', JSON.stringify(viewedEvents))
hasTracked.current = true
}
} catch (error) {
console.error('Failed to track view:', error)
// Silently fail - don't disrupt user experience
}
}

// Track after a short delay to ensure the user actually viewed the page
const timer = setTimeout(trackView, 2000) // 2 second delay

return () => clearTimeout(timer)
}, [eventSlug, enabled])
}
48 changes: 36 additions & 12 deletions lib/services/analytics-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,53 @@ export class AnalyticsService {
static async trackEventView(eventSlug: string): Promise<void> {
const supabase = await createClient()

// First, try to get the event (only approved events should be viewable publicly)
const { data: event, error: eventError } = await supabase
.from('events')
.select('id, company_id, views')
.select('id, company_id, views, approval_status')
.eq('slug', eventSlug)
.eq('approval_status', 'approved') // Only track views for approved events
.single()

if (eventError || !event) {
if (eventError) {
console.error('Error fetching event for view tracking:', eventError)
throw new Error('Event not found')
}

// Increment view count
await supabase
.from('events')
.update({ views: (event.views || 0) + 1 })
.eq('id', event.id)
if (!event) {
throw new Error('Event not found')
}

// Increment view count using RPC function if available, otherwise direct update
const { error: rpcError } = await supabase.rpc('increment_event_views', {
event_id: event.id,
})

if (rpcError) {
// Fallback to direct update if RPC doesn't exist
const { error: updateError } = await supabase
.from('events')
.update({ views: (event.views || 0) + 1 })
.eq('id', event.id)

if (updateError) {
console.error('Error updating event views:', updateError)
// Don't throw - we don't want to fail the request if view tracking fails
}
}

// Update company analytics if event has a company
if (event.company_id) {
await this.incrementCompanyAnalytics(
event.company_id,
'total_views',
1
)
try {
await this.incrementCompanyAnalytics(
event.company_id,
'total_views',
1
)
} catch (error) {
console.error('Error updating company analytics:', error)
// Don't throw - we don't want to fail the request
}
}
}

Expand Down
Loading