Payload Logo
Components,  User interaction

Measuring engagement and creating a report on user events

Author

james

Date Published


Step 1


Add the dependency. This bundles React chart

npm i @nouance/payload-dashboard-analytics


Step 2

Using google analytics and this git repo we add components to dashboard

draw a report on the traffic to the pages, that create the bookings the listing you created.


Case study comming


Users dont know the impact the listing has, so
Prototypes measure the journey creating a report on the user experience. Similar to Figma flow but less files size
draw a report on the traffic to the pages, that create the bookings the listing you created.

package.json

1 "@radix-ui/react-select": "^2.0.0",
2 "@radix-ui/react-slot": "^1.1.1",
3 "@revenuecat/purchases-js": "^0.18.2",
4 "chart.js": "^4.4.9",
5 "class-variance-authority": "^0.7.0",
6 "clsx": "^2.1.1",
7 "date-fns": "^4.1.0",
8@@ -51,6 +52,7 @@
9 "payload-admin-bar": "^1.0.6",
10 "prism-react-renderer": "^2.3.1",
11 "react": "^19.0.0",
12 "react-chartjs-2": "^5.3.0",
13 "react-day-picker": "^8.10.1",
14 "react-dom": "^19.0.0",
15 "react-hook-form": "7.45.4",

taken from google

1import { BetaAnalyticsDataClient } from '@google-analytics/data'
2
3// Enhanced logging for initialization
4console.log('=== GA4 Configuration Debug ===')
5console.log('Environment:', process.env.NODE_ENV)
6console.log('GA4 Property ID:', process.env.GA4_PROPERTY_ID)
7console.log('Service Account JSON Length:', process.env.GOOGLE_SERVICE_ACCOUNT_JSON?.length || 0)
8
9try {
10 if (process.env.GOOGLE_SERVICE_ACCOUNT_JSON) {
11 const serviceAccount = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON)
12 console.log('Service Account Details:')
13 console.log('- Project ID:', serviceAccount.project_id)
14 console.log('- Client Email:', serviceAccount.client_email)
15 console.log('- Private Key Length:', serviceAccount.private_key?.length || 0)
16 } else {
17 console.error('GOOGLE_SERVICE_ACCOUNT_JSON is empty or undefined')
18 }
19} catch (e) {
20 console.error('Error parsing service account JSON:', e)
21 console.error('Raw JSON:', process.env.GOOGLE_SERVICE_ACCOUNT_JSON)
22}
23
24let analyticsDataClient: BetaAnalyticsDataClient | null = null
25
26// Initialize the analytics client
27try {
28 const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON
29 if (!serviceAccountJson) {
30 console.error('GOOGLE_SERVICE_ACCOUNT_JSON environment variable is not set')
31 } else {
32 try {
33 const credentials = JSON.parse(serviceAccountJson)
34 analyticsDataClient = new BetaAnalyticsDataClient({
35 credentials,
36 })
37 console.log('Analytics client initialized successfully')
38 } catch (parseError) {
39 console.error('Error parsing GOOGLE_SERVICE_ACCOUNT_JSON:', parseError)
40 console.error('Service account JSON content:', serviceAccountJson)
41 }
42 }
43} catch (e) {
44 console.error('Error initializing analytics client:', e)
45}
46
47export const getAnalyticsData = async () => {
48 if (!analyticsDataClient) {
49 console.error('Analytics client not initialized. Checking environment variables...')
50 console.error('GA4_PROPERTY_ID:', process.env.GA4_PROPERTY_ID)
51 console.error('GOOGLE_SERVICE_ACCOUNT_JSON exists:', !!process.env.GOOGLE_SERVICE_ACCOUNT_JSON)
52 return {
53 activeUsersNow: 0,
54 total30DayUsers: 0,
55 total30DayViews: 0,
56 historicalData: [],
57 }
58 }
59
60 try {
61 // Get realtime data
62 console.log('Fetching realtime data...')
63 console.log('Using property ID:', process.env.GA4_PROPERTY_ID)
64 const realtimeResponse = await analyticsDataClient.runRealtimeReport({
65 property: `properties/${process.env.GA4_PROPERTY_ID}`,
66 dimensions: [{ name: 'minutesAgo' }],
67 metrics: [{ name: 'activeUsers' }],
68 })
69 console.log('Raw realtime response:', JSON.stringify(realtimeResponse, null, 2))
70
71 // Get historical data
72 console.log('Fetching historical data...')
73 const historicalResponse = await analyticsDataClient.runReport({
74 property: `properties/${process.env.GA4_PROPERTY_ID}`,
75 dateRanges: [
76 {
77 startDate: '30daysAgo',
78 endDate: 'today',
79 },
80 ],
81 dimensions: [{ name: 'date' }],
82 metrics: [{ name: 'totalUsers' }, { name: 'screenPageViews' }],
83 })
84 console.log('Raw historical response:', JSON.stringify(historicalResponse, null, 2))
85
86 // Process realtime data
87 const activeUsersNow = realtimeResponse[0]?.rows?.[0]?.metricValues?.[0]?.value || '0'
88 console.log('Processed activeUsersNow:', activeUsersNow)
89
90 // Process historical data
91 const historicalData =
92 historicalResponse[0]?.rows?.map((row) => {
93 const processedRow = {
94 date: row.dimensionValues?.[0]?.value || '',
95 users: parseInt(row.metricValues?.[0]?.value || '0', 10),
96 views: parseInt(row.metricValues?.[1]?.value || '0', 10),
97 }
98 console.log('Processing row:', processedRow)
99 return processedRow
100 }) || []
101
102 console.log('Processed historical data:', historicalData)
103
104 // Calculate totals
105 const total30DayUsers = historicalData.reduce((sum: number, row) => sum + row.users, 0)
106 const total30DayViews = historicalData.reduce((sum: number, row) => sum + row.views, 0)
107
108 console.log('Calculated totals:', {
109 total30DayUsers,
110 total30DayViews,
111 })
112
113 return {
114 activeUsersNow,
115 total30DayUsers,
116 total30DayViews,
117 historicalData,
118 }
119 } catch (error) {
120 console.error('Error in getAnalyticsData:', error)
121 if (error instanceof Error) {
122 console.error('Error details:', {
123 message: error.message,
124 stack: error.stack,
125 })
126 }
127 return {
128 activeUsersNow: 0,
129 total30DayUsers: 0,
130 total30DayViews: 0,
131 historicalData: [],
132 }
133 }
134}

|_ 📄 package.json
|_ 📁 src
| |_ 📁 AnalyticsUtilties
| | | |_ 📄 ga4.ts
| |_ 📁 app/(payload)/admin
| | | |_ 📄 importMap.js.ts
|_ 📁 componets
| |_ 📁 AnalyticsDashboard
| | |_ 📄 AnalyticsDashboard.tsx
| |_ 📄 GoogleAnalytics.tsx
|_ 📄 payload.config.ts

1'use client'
2import React, { useEffect, useState } from 'react'
3import { Line } from 'react-chartjs-2'
4import {
5 Chart as ChartJS,
6 CategoryScale,
7 LinearScale,
8 PointElement,
9 LineElement,
10 Title,
11 Tooltip,
12 Legend,
13} from 'chart.js'
14
15ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend)
16
17interface AnalyticsData {
18 activeUsersNow?: number
19 total30DayUsers?: number
20 total30DayViews?: number
21 historicalData?: Array<{
22 date: string
23 users: number
24 views: number
25 }>
26}
27
28const AnalyticsDashboard: React.FC = () => {
29 const [data, setData] = useState<AnalyticsData | null>(null)
30 const [loading, setLoading] = useState(true)
31 const [error, setError] = useState<string | null>(null)
32
33 // fallback data
34 const fallbackData: AnalyticsData = {
35 historicalData: [
36 { date: '2025-03-15', users: 4, views: 5 },
37 { date: '2025-03-16', users: 5, views: 6 },
38 { date: '2025-03-17', users: 6, views: 7 },
39 { date: '2025-03-18', users: 2, views: 3 },
40 { date: '2025-03-19', users: 1, views: 2 },
41 { date: '2025-03-20', users: 10, views: 15 },
42 { date: '2025-03-21', users: 8, views: 12 },
43 { date: '2025-03-22', users: 11, views: 13 },
44 { date: '2025-03-23', users: 7, views: 9 },
45 { date: '2025-03-24', users: 4, views: 5 },
46 { date: '2025-03-25', users: 13, views: 15 },
47 { date: '2025-03-26', users: 14, views: 16 },
48 { date: '2025-03-27', users: 12, views: 14 },
49 { date: '2025-03-28', users: 16, views: 18 },
50 { date: '2025-03-29', users: 13, views: 15 },
51 { date: '2025-03-30', users: 14, views: 17 },
52 { date: '2025-03-31', users: 3, views: 4 },
53 { date: '2025-04-01', users: 20, views: 24 },
54 ],
55 }
56
57 const fetchAnalytics = async () => {
58 try {
59 const response = await fetch('/api/analytics', {
60 headers: { 'Content-Type': 'application/json' },
61 })
62
63 if (!response.ok) {
64 throw new Error(`HTTP error! status: ${response.status}`)
65 }
66
67 return await response.json()
68 } catch (err) {
69 console.error('Fetch error:', err)
70 throw err instanceof Error ? err : new Error('Failed to fetch analytics')
71 }
72 }
73
74 useEffect(() => {
75 const loadData = async () => {
76 try {
77 const analyticsData = await fetchAnalytics()
78
79 // Check if the data is empty or invalid
80 const isEmptyData =
81 !analyticsData ||
82 (analyticsData.activeUsersNow === 0 &&
83 analyticsData.total30DayUsers === 0 &&
84 (!analyticsData.historicalData || analyticsData.historicalData.length === 0))
85
86 setData(isEmptyData ? fallbackData : analyticsData)
87 setError(null)
88 } catch (err) {
89 console.error('Error loading analytics:', err)
90 setData(fallbackData) // Use fallback data on error
91 setError(err instanceof Error ? err.message : 'Failed to fetch analytics')
92 } finally {
93 setLoading(false)
94 }
95 }
96
97 loadData()
98 // const interval = setInterval(loadData, 120000)
99 const interval = setInterval(loadData, 8000)
100 return () => clearInterval(interval)
101 }, [])
102
103 const Card = ({
104 title,
105 value,
106 style = {},
107 }: {
108 title: string
109 value: number
110 style?: React.CSSProperties
111 }) => {
112 const [hovered, setHovered] = useState(false)
113
114 return (
115 <div
116 style={{
117 padding: '1.5rem',
118 backgroundColor: 'var(--theme-elevation-50)',
119 borderRadius: '8px',
120 boxShadow: hovered ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 6px rgba(0,0,0,0.08)',
121 border: hovered ? '1px solid var(--theme-elevation-200)' : '1px solid transparent',
122 transition: 'all 0.2s ease-in-out',
123 cursor: 'pointer',
124 display: 'flex',
125 flexDirection: 'column',
126 justifyContent: 'space-between',
127 minHeight: '120px',
128 ...style,
129 }}
130 onMouseEnter={() => setHovered(true)}
131 onMouseLeave={() => setHovered(false)}
132 >
133 <h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 600 }}>{title}</h3>
134 <p style={{ fontSize: '2rem', fontWeight: 'bold', margin: '0.5rem 0 0' }}>{value}</p>
135 </div>
136 )
137 }
138
139 // Determine which data to use (fallback if no data available)
140 const displayData = data || fallbackData
141 const chartData = displayData.historicalData || []
142
143 return (
144 <div style={{ margin: '2rem 0', padding: '0 1rem' }}>
145 <h2 style={{ fontSize: '1.8rem', marginBottom: '1rem' }}>📊 Google Analytics Overview</h2>
146
147 {loading && <div>Loading analytics data...</div>}
148 {error && (
149 <div
150 style={{
151 color: 'var(--theme-error-500)',
152 backgroundColor: 'var(--theme-error-50)',
153 padding: '1rem',
154 borderRadius: '4px',
155 marginBottom: '1rem',
156 }}
157 >
158 {error} (using fallback data)
159 </div>
160 )}
161
162 <div
163 style={{
164 display: 'grid',
165 marginTop: '2rem',
166 gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))',
167 gap: '1.5rem',
168 marginBottom: '2rem',
169 }}
170 >
171 <Card title="Active Users Now" value={displayData.activeUsersNow || 0} />
172 <Card title="Total Users 30 Days" value={displayData.total30DayUsers || 0} />
173 <Card title="Total Views 30 Days" value={displayData.total30DayViews || 0} />
174 </div>
175
176 <div
177 style={{
178 padding: '2rem 1rem',
179 backgroundColor: 'var(--theme-elevation-25)',
180 borderRadius: '8px',
181 boxShadow: '0 2px 6px rgba(0,0,0,0.08)',
182 }}
183 >
184 <h3 style={{ marginTop: 0, marginBottom: '1rem' }}>📈 User Activity 30 Days</h3>
185
186 {chartData.length > 0 ? (
187 <div style={{ width: '100%', height: '400px' }}>
188 <Line
189 data={{
190 labels: chartData.map((item) => item.date),
191 datasets: [
192 {
193 label: 'Users',
194 data: chartData.map((item) => item.users),
195 borderColor: 'rgb(75, 192, 192)',
196 backgroundColor: 'rgba(75, 192, 192, 0.2)',
197 tension: 0.3,
198 fill: true,
199 },
200 {
201 label: 'Views',
202 data: chartData.map((item) => item.views),
203 borderColor: 'rgb(53, 162, 235)',
204 backgroundColor: 'rgba(53, 162, 235, 0.2)',
205 tension: 0.3,
206 fill: true,
207 },
208 ],
209 }}
210 options={{
211 responsive: true,
212 maintainAspectRatio: false,
213 plugins: {
214 legend: {
215 position: 'top',
216 },
217 tooltip: {
218 mode: 'index',
219 intersect: false,
220 },
221 },
222 scales: {
223 x: {
224 grid: {
225 display: false,
226 },
227 },
228 y: {
229 beginAtZero: true,
230 },
231 },
232 }}
233 />
234 </div>
235 ) : (
236 <p>No data available for the selected period.</p>
237 )}
238 </div>
239 </div>
240 )
241}
242
243export default AnalyticsDashboard
244

Show the traffic to the listing

|_ 📄 package.json
|_ 📁 src
| |_ 📁 AnalyticsUtilties
| | | |_ 📄 ga4.ts
| |_ 📁 app/(payload)/admin
| | | |_ 📄 importMap.js.ts
|_ 📁 componets
| |_ 📁 AnalyticsDashboard
| | |_ 📄 AnalyticsDashboard.tsx
| |_ 📄 GoogleAnalytics.tsx
|_ 📄 payload.config.ts

1
2import React from 'react'
3import Script from 'next/script'
4const GoogleAnalytics = () => {
5 console.log('Google Analytics ID:', process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS)
6 return (
7 <>
8 <Script
9 strategy="lazyOnload"
10 src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS}`}
11 />
12 <Script id="google-analytics" strategy="lazyOnload">
13 {`
14 window.dataLayer = window.dataLayer || [];
15 function gtag(){dataLayer.push(arguments);}
16 gtag('js', new Date());
17 gtag('config', '${process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS}', {
18 page_path: window.location.pathname,
19 });
20 `}
21 </Script>
22 </>
23 )
24}
25export default GoogleAnalytics

register it with the payload config replaing the BeforelLogin

1export default buildConfig({
2 admin: {
3 components: {
4 afterDashboard: ['@/components/AnalyticsDashboardData/AnalyticsDashboard'],
5 // The `BeforeLogin` component renders a message that you see while logging into your admin panel.
6 // Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
7 beforeLogin: ['@/components/BeforeLogin'],
Insurance,  CX,  Service design

Subscription payment and Sharing the policy using a protected route with a paywall to ensure privacy of users the payment is intended for