From 1180f27f2e4fa66c96f4d7c42d638b0588fd457d Mon Sep 17 00:00:00 2001 From: John Bickar Date: Thu, 25 Sep 2025 14:53:24 -0700 Subject: [PATCH 1/9] ACHOO-134: Graph of Views/Visits per day --- app/api/acquia/views/route.ts | 38 ++------- app/api/acquia/visits/route.ts | 26 +++---- app/applications/[uuid]/page.tsx | 127 +++++++++++++++++++++++-------- lib/acquia-api.ts | 64 +++++++--------- 4 files changed, 141 insertions(+), 114 deletions(-) diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 72cd719..9b70396 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -6,13 +6,7 @@ export async function GET(request: NextRequest) { const subscriptionUuid = searchParams.get('subscriptionUuid'); const from = searchParams.get('from'); const to = searchParams.get('to'); - /** - console.log('šŸš€ Views by Application API Route called with params:', { - subscriptionUuid, - from, - to, - }); - */ + const granularity = searchParams.get('granularity'); // Get granularity for daily data if (!subscriptionUuid) { console.error('āŒ Missing required parameter: subscriptionUuid'); @@ -22,26 +16,17 @@ export async function GET(request: NextRequest) { ); } - // Update the API service initialization with better error handling if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) { console.error('āŒ Missing required environment variables!'); - console.error('Available env vars:', Object.keys(process.env).filter(k => k.startsWith('ACQUIA'))); return NextResponse.json( - { + { error: 'Server configuration error: missing API credentials', - envCheck: { - ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ? `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : 'missing', - ACQUIA_API_SECRET: process.env.ACQUIA_API_SECRET ? 'present' : 'missing', - ACQUIA_API_BASE_URL: process.env.ACQUIA_API_BASE_URL || 'missing', - ACQUIA_AUTH_BASE_URL: process.env.ACQUIA_AUTH_BASE_URL || 'missing' - } }, { status: 500 } ); } try { - // Update the API service initialization const apiService = new AcquiaApiServiceFixed({ baseUrl: process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api', authUrl: process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api', @@ -53,32 +38,23 @@ export async function GET(request: NextRequest) { // console.log('šŸ“ˆ Views progress:', progress); }); - // console.log('šŸ”§ Using FIXED API Service for views by application (with pagination)'); - const data = await apiService.getViewsDataByApplication( subscriptionUuid, from || undefined, - to || undefined + to || undefined, + granularity || undefined // Pass granularity to the service method ); - // console.log('āœ… Successfully fetched ALL views by application data, total count:', data.length); - return NextResponse.json({ data, totalItems: data.length, message: `Successfully fetched ${data.length} view records across all pages`, }); } catch (error) { - console.error('āŒ API Route Error:', error); - - if (error instanceof Error) { - console.error('šŸ” Error name:', error.name); - console.error('šŸ” Error message:', error.message); - console.error('šŸ” Error stack:', error.stack); - } - + console.error('āŒ API Route Error in /api/acquia/views:', error); + return NextResponse.json( - { + { error: 'Failed to fetch views by application data', details: error instanceof Error ? error.message : 'Unknown error', }, diff --git a/app/api/acquia/visits/route.ts b/app/api/acquia/visits/route.ts index 0e31426..2845963 100644 --- a/app/api/acquia/visits/route.ts +++ b/app/api/acquia/visits/route.ts @@ -6,6 +6,7 @@ export async function GET(request: NextRequest) { const subscriptionUuid = searchParams.get('subscriptionUuid'); const from = searchParams.get('from'); const to = searchParams.get('to'); + const granularity = searchParams.get('granularity'); // Get granularity for daily data /** console.log('šŸš€ Visits by Application API Route called with params:', { @@ -28,7 +29,7 @@ export async function GET(request: NextRequest) { console.error('āŒ Missing required environment variables!'); console.error('Available env vars:', Object.keys(process.env).filter(k => k.startsWith('ACQUIA'))); return NextResponse.json( - { + { error: 'Server configuration error: missing API credentials', envCheck: { ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ? `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : 'missing', @@ -55,26 +56,25 @@ export async function GET(request: NextRequest) { // console.log('šŸ”§ Using FIXED API Service for visits by application (with pagination)'); - const data = await apiService.getVisitsDataByApplication(subscriptionUuid, from || undefined, to || undefined); - + const data = await apiService.getVisitsDataByApplication( + subscriptionUuid, + from || undefined, + to || undefined, + granularity || undefined // Pass granularity to the service method + ); + // console.log('āœ… Successfully fetched ALL visits by application data, total count:', data.length); - + return NextResponse.json({ data, totalItems: data.length, message: `Successfully fetched ${data.length} visit records across all pages` }); } catch (error) { - console.error('āŒ API Route Error:', error); - - if (error instanceof Error) { - console.error('šŸ” Error name:', error.name); - console.error('šŸ” Error message:', error.message); - console.error('šŸ” Error stack:', error.stack); - } - + console.error('āŒ API Route Error in /api/acquia/visits:', error); + return NextResponse.json( - { + { error: 'Failed to fetch visits by application data', details: error instanceof Error ? error.message : 'Unknown error' }, diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index bdbd87b..7ddf5f9 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import CountUpTimer from '@/components/CountUpTimer'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; const DEFAULT_SUBSCRIPTION_UUID = process.env.NEXT_PUBLIC_ACQUIA_SUBSCRIPTION_UUID || ''; interface PageProps { @@ -21,6 +22,8 @@ export default function ApplicationDetailPage({ params }: any) { const [viewsPct, setViewsPct] = useState(0); const [visitsPct, setVisitsPct] = useState(0); const [error, setError] = useState(null); + const [dailyViews, setDailyViews] = useState([]); + const [dailyVisits, setDailyVisits] = useState([]); // Fetch application name on mount or when subscriptionUuid changes useEffect(() => { @@ -51,45 +54,63 @@ export default function ApplicationDetailPage({ params }: any) { if (subscriptionUuid) paramsObj.subscriptionUuid = subscriptionUuid; if (from) paramsObj.from = from; if (to) paramsObj.to = to; - const query = new URLSearchParams(paramsObj).toString(); + + // We only need to fetch daily data, as we can calculate totals from it. + const dailyQuery = new URLSearchParams({ ...paramsObj, granularity: 'day' }).toString(); setLoadingStep('Fetching views and visits...'); - const [viewsRes, visitsRes] = await Promise.all([ - fetch(`/api/acquia/views?${query}`), - fetch(`/api/acquia/visits?${query}`), + const [dailyViewsRes, dailyVisitsRes] = await Promise.all([ + fetch(`/api/acquia/views?${dailyQuery}`), + fetch(`/api/acquia/visits?${dailyQuery}`), ]); - const [viewsRaw, visitsRaw] = await Promise.all([ - viewsRes.ok ? viewsRes.json() : [], - visitsRes.ok ? visitsRes.json() : [], + + const [dailyViewsRaw, dailyVisitsRaw] = await Promise.all([ + dailyViewsRes.ok ? dailyViewsRes.json() : [], + dailyVisitsRes.ok ? dailyVisitsRes.json() : [], ]); - const viewsArr = Array.isArray(viewsRaw) - ? viewsRaw - : viewsRaw && Array.isArray(viewsRaw.data) - ? viewsRaw.data - : []; - const visitsArr = Array.isArray(visitsRaw) - ? visitsRaw - : visitsRaw && Array.isArray(visitsRaw.data) - ? visitsRaw.data - : []; - const appViewsTotal = viewsArr - .filter((v: any) => v.uuid === params.uuid || v.applicationUuid === params.uuid) - .reduce((sum: number, v: any) => sum + (v.views || 0), 0); - - const appVisitsTotal = visitsArr - .filter((v: any) => v.uuid === params.uuid || v.applicationUuid === params.uuid) - .reduce((sum: number, v: any) => sum + (v.visits || 0), 0); - - const totalViews = viewsArr.reduce((sum: number, v: any) => sum + (v.views || 0), 0); - const totalVisits = visitsArr.reduce((sum: number, v: any) => sum + (v.visits || 0), 0); - - setViews(appViewsTotal); - setVisits(appVisitsTotal); - setViewsPct(totalViews ? (appViewsTotal / totalViews) * 100 : 0); - setVisitsPct(totalVisits ? (appVisitsTotal / totalVisits) * 100 : 0); + + // Helper to process and aggregate daily data + const processDailyData = (rawData: any[], metric: 'views' | 'visits') => { + const dailyMap = new Map(); + // Filter for the specific application we are viewing + const appData = (rawData || []).filter((d: any) => d.applicationUuid === params.uuid); + + for (const record of appData) { + // FIX #1: The property is named 'date', not 'timestamp' + const date = record.date.split('T')[0]; + // FIX #2: The value is in either the 'views' or 'visits' property + const value = record[metric] || 0; + dailyMap.set(date, (dailyMap.get(date) || 0) + value); + } + + return Array.from(dailyMap.entries()) + .map(([date, value]) => ({ date, value })) + .sort((a, b) => a.date.localeCompare(b.date)); + }; + + const processedDailyViews = processDailyData(dailyViewsRaw.data, 'views'); + const processedDailyVisits = processDailyData(dailyVisitsRaw.data, 'visits'); + + setDailyViews(processedDailyViews as any); + setDailyVisits(processedDailyVisits as any); + + // Calculate totals for this app from the daily data + const appTotalViews = processedDailyViews.reduce((sum, day) => sum + day.value, 0); + const appTotalVisits = processedDailyVisits.reduce((sum, day) => sum + day.value, 0); + + // Calculate overall totals for percentage calculation + const overallTotalViews = (dailyViewsRaw.data || []).reduce((sum: number, v: any) => sum + (v.views || 0), 0); + const overallTotalVisits = (dailyVisitsRaw.data || []).reduce((sum: number, v: any) => sum + (v.visits || 0), 0); + + setViews(appTotalViews); + setVisits(appTotalVisits); + setViewsPct(overallTotalViews > 0 ? (appTotalViews / overallTotalViews) * 100 : 0); + setVisitsPct(overallTotalVisits > 0 ? (appTotalVisits / overallTotalVisits) * 100 : 0); + setLoadingStep('Complete!'); } catch (err) { setError('Failed to fetch application details.'); + console.error(err); } finally { const endTime = Date.now(); setElapsedTime((endTime - startTime) / 1000); @@ -226,6 +247,48 @@ export default function ApplicationDetailPage({ params }: any) { )} + + {/* Data Display Section */} + {!loading && (views > 0 || visits > 0) && ( +
+ {/* Charts Section */} +
+
+

Daily Views

+ + + + + new Intl.NumberFormat('en-US').format(value as number)} /> + + + + + +
+ +
+

Daily Visits

+ + + + + new Intl.NumberFormat('en-US').format(value as number)} /> + + + + + +
+
+
+ )} ); } \ No newline at end of file diff --git a/lib/acquia-api.ts b/lib/acquia-api.ts index 4d2c318..c54c984 100644 --- a/lib/acquia-api.ts +++ b/lib/acquia-api.ts @@ -109,7 +109,7 @@ class AcquiaApiServiceFixed { // Check if API key appears to be base64 encoded (common issue in some environments) // If it starts with base64-like characters and doesn't look like a UUID, try decoding - if (cleanApiKey && !cleanApiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) && + if (cleanApiKey && !cleanApiKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i) && cleanApiKey.match(/^[A-Za-z0-9+/]+=*$/)) { try { const decodedKey = Buffer.from(cleanApiKey, 'base64').toString('utf-8'); @@ -123,7 +123,7 @@ class AcquiaApiServiceFixed { // console.log('āš ļø Failed to decode suspected base64 API key, using original value'); } } - /** + /** console.log('šŸ” Using cleaned credentials:', { keyLength: cleanApiKey.length, secretLength: cleanApiSecret.length @@ -150,7 +150,7 @@ class AcquiaApiServiceFixed { timeout: this.AUTH_TIMEOUT, validateStatus: () => true, }); - + // console.log('šŸ“„ Basic Auth response status:', response.status); if (response.status === 200 && response.data?.access_token) { return response.data.access_token; @@ -184,7 +184,7 @@ class AcquiaApiServiceFixed { } throw new Error(`Form Parameters failed: ${response.status} - ${JSON.stringify(response.data)}`); }, - + // Method 3: Use correct client ID format (if UUID is in different format) async () => { // console.log('šŸ” Trying with alternate client ID format...'); @@ -251,14 +251,14 @@ class AcquiaApiServiceFixed { }, timeout: this.API_TIMEOUT, }); - + return response; } catch (error) { if (axios.isAxiosError(error)) { if (error.response?.status === 401) { // console.log('šŸ”„ Token expired, retrying...'); this.accessToken = null; - + const newToken = await this.getAccessToken(); return axios.get(fullUrl, { headers: { @@ -269,7 +269,7 @@ class AcquiaApiServiceFixed { }); } } - + throw error; } } @@ -376,7 +376,7 @@ class AcquiaApiServiceFixed { private parseApplicationData(responseData: any, dataType: 'visits' | 'views'): VisitsData[] | ViewsData[] { // console.log('\nšŸ” PARSING ACQUIA API RESPONSE - CORRECT ASSOCIATION'); // console.log('šŸ“Š Response top-level keys:', Object.keys(responseData)); - + if (!responseData._embedded) { console.warn('āš ļø No _embedded found in response'); return []; @@ -403,7 +403,7 @@ class AcquiaApiServiceFixed { let applicationName = ''; let environmentUuids: string[] = []; let environmentNames: string[] = []; - + // console.log(`šŸ“‹ Extracting metadata for item ${itemIndex}...`); // Get application info from metadata.application.uuids[0] @@ -424,7 +424,7 @@ class AcquiaApiServiceFixed { applicationName = item.metadata.application.names[0] || ''; // console.log(` šŸ“ Application name: ${applicationName}`); } - + // If no name found, generate one from UUID if (!applicationName && applicationUuid) { applicationName = `App ${applicationUuid.substring(0, 8)}`; @@ -524,7 +524,7 @@ class AcquiaApiServiceFixed { const totalValue = parsedData.reduce((sum, item) => { return sum + (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views); }, 0); - + const applicationSummary = parsedData.reduce((acc, item) => { const appKey = item.applicationUuid; if (!acc[appKey]) { @@ -561,9 +561,10 @@ class AcquiaApiServiceFixed { dataType: 'visits' | 'views', subscriptionUuid: string, from?: string, - to?: string + to?: string, + granularity?: string // <<< FIX #1: Add granularity parameter here ): Promise { - const cacheKey = generateCacheKey([baseEndpoint, subscriptionUuid, from, to]); + const cacheKey = generateCacheKey([baseEndpoint, subscriptionUuid, from, to, granularity]); // <<< FIX #2: Add granularity to cache key const cachedEntry = cache[cacheKey]; if (cachedEntry && (Date.now() - cachedEntry.timestamp < CACHE_DURATION_MS)) { @@ -580,44 +581,33 @@ class AcquiaApiServiceFixed { let totalPages = 1; let hasMorePages = true; - // Build the filter parameter with corrected date formatting const filterParam = this.buildFilterParam(from, to); - // console.log(`šŸ” Date range requested: ${from} to ${to}`); - // console.log(`šŸ” Filter parameter: ${filterParam}`); while (hasMorePages) { try { const params = new URLSearchParams(); - // Add filter parameter if we have date range if (filterParam) { params.append('filter', filterParam); - // console.log(`šŸ“… Added filter parameter to request`); - } else { - // console.log(`āš ļø No filter parameter - API will return default date range`); } - // Add resolution parameter (day for visits, month for views as per your examples) - // const resolution = dataType === 'visits' ? 'day' : 'month'; - const resolution = 'day'; - params.append('resolution', resolution); - // console.log(`šŸ“Š Using resolution: ${resolution}`); + // <<< FIX #3: Use the granularity parameter if it exists + if (granularity) { + params.append('granularity', granularity); + // console.log(`šŸ“Š Using granularity: ${granularity}`); + } - // Add pagination if needed if (currentPage > 1) { params.append('page', currentPage.toString()); } const fullEndpoint = `${baseEndpoint}?${params.toString()}`; - this.reportProgress({ + this.reportProgress({ step: `Fetching ${dataType} data (page ${currentPage})...`, currentPage, totalPages: totalPages > 1 ? totalPages : undefined, - itemsCollected: allData.length - }); - - // console.log(`šŸ“” Making request to: ${fullEndpoint}`); - // console.log(`šŸ“” Full URL parameters:`, params.toString()); + itemsCollected: allData.length + }); const startTime = Date.now(); const response = await this.makeAuthenticatedRequest(fullEndpoint); @@ -710,16 +700,14 @@ class AcquiaApiServiceFixed { return allData; } - async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { + async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: string, granularity?: string): Promise { // <<< FIX #4: Correctly name the parameter const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/visits-by-application`; - // console.log(`🚶 Fetching visits data with resolution=day for date range: ${from || 'no start'} to ${to || 'no end'}`); - return this.fetchAllPages(baseEndpoint, 'visits', subscriptionUuid, from, to); + return this.fetchAllPages(baseEndpoint, 'visits', subscriptionUuid, from, to, granularity); // <<< FIX #5: Pass granularity through } - async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { + async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string, granularity?: string): Promise { // <<< FIX #4: Correctly name the parameter const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/views-by-application`; - // console.log(`šŸ‘ļø Fetching views data with resolution=month for date range: ${from || 'no start'} to ${to || 'no end'}`); - return this.fetchAllPages(baseEndpoint, 'views', subscriptionUuid, from, to); + return this.fetchAllPages(baseEndpoint, 'views', subscriptionUuid, from, to, granularity); // <<< FIX #5: Pass granularity through } } From 0f0f346ad26174721dbbc51ca7aae491b56f245a Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 11:34:04 -0700 Subject: [PATCH 2/9] fixup! terminology: use resolution instead of granularity --- app/api/acquia/views/route.ts | 4 ++-- app/api/acquia/visits/route.ts | 5 ++--- app/applications/[uuid]/page.tsx | 2 +- lib/acquia-api.ts | 20 ++++++++++---------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 9b70396..42a4b7f 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -6,7 +6,7 @@ export async function GET(request: NextRequest) { const subscriptionUuid = searchParams.get('subscriptionUuid'); const from = searchParams.get('from'); const to = searchParams.get('to'); - const granularity = searchParams.get('granularity'); // Get granularity for daily data + const resolution = searchParams.get('resolution'); // Get granularity for daily data if (!subscriptionUuid) { console.error('āŒ Missing required parameter: subscriptionUuid'); @@ -42,7 +42,7 @@ export async function GET(request: NextRequest) { subscriptionUuid, from || undefined, to || undefined, - granularity || undefined // Pass granularity to the service method + resolution || undefined ); return NextResponse.json({ diff --git a/app/api/acquia/visits/route.ts b/app/api/acquia/visits/route.ts index 2845963..9d29da3 100644 --- a/app/api/acquia/visits/route.ts +++ b/app/api/acquia/visits/route.ts @@ -6,7 +6,7 @@ export async function GET(request: NextRequest) { const subscriptionUuid = searchParams.get('subscriptionUuid'); const from = searchParams.get('from'); const to = searchParams.get('to'); - const granularity = searchParams.get('granularity'); // Get granularity for daily data + const resolution = searchParams.get('resolution'); // Get granularity for daily data /** console.log('šŸš€ Visits by Application API Route called with params:', { @@ -53,14 +53,13 @@ export async function GET(request: NextRequest) { apiService.setProgressCallback((progress) => { // console.log('šŸ“Š Visits progress:', progress); }); - // console.log('šŸ”§ Using FIXED API Service for visits by application (with pagination)'); const data = await apiService.getVisitsDataByApplication( subscriptionUuid, from || undefined, to || undefined, - granularity || undefined // Pass granularity to the service method + resolution || undefined ); // console.log('āœ… Successfully fetched ALL visits by application data, total count:', data.length); diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index 7ddf5f9..b78df90 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -56,7 +56,7 @@ export default function ApplicationDetailPage({ params }: any) { if (to) paramsObj.to = to; // We only need to fetch daily data, as we can calculate totals from it. - const dailyQuery = new URLSearchParams({ ...paramsObj, granularity: 'day' }).toString(); + const dailyQuery = new URLSearchParams({ ...paramsObj, resolution: 'day' }).toString(); setLoadingStep('Fetching views and visits...'); const [dailyViewsRes, dailyVisitsRes] = await Promise.all([ diff --git a/lib/acquia-api.ts b/lib/acquia-api.ts index c54c984..da1ac4e 100644 --- a/lib/acquia-api.ts +++ b/lib/acquia-api.ts @@ -562,9 +562,9 @@ class AcquiaApiServiceFixed { subscriptionUuid: string, from?: string, to?: string, - granularity?: string // <<< FIX #1: Add granularity parameter here + resolution?: string ): Promise { - const cacheKey = generateCacheKey([baseEndpoint, subscriptionUuid, from, to, granularity]); // <<< FIX #2: Add granularity to cache key + const cacheKey = generateCacheKey([baseEndpoint, subscriptionUuid, from, to, resolution]); const cachedEntry = cache[cacheKey]; if (cachedEntry && (Date.now() - cachedEntry.timestamp < CACHE_DURATION_MS)) { @@ -591,10 +591,10 @@ class AcquiaApiServiceFixed { params.append('filter', filterParam); } - // <<< FIX #3: Use the granularity parameter if it exists - if (granularity) { - params.append('granularity', granularity); - // console.log(`šŸ“Š Using granularity: ${granularity}`); + // Use the resolution parameter if it exists + if (resolution) { + params.append('resolution', resolution); + // console.log(`šŸ“Š Using resolution: ${resolution}`); } if (currentPage > 1) { @@ -700,14 +700,14 @@ class AcquiaApiServiceFixed { return allData; } - async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: string, granularity?: string): Promise { // <<< FIX #4: Correctly name the parameter + async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: string, resolution?: string): Promise { const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/visits-by-application`; - return this.fetchAllPages(baseEndpoint, 'visits', subscriptionUuid, from, to, granularity); // <<< FIX #5: Pass granularity through + return this.fetchAllPages(baseEndpoint, 'visits', subscriptionUuid, from, to, resolution); } - async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string, granularity?: string): Promise { // <<< FIX #4: Correctly name the parameter + async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string, resolution?: string): Promise { const baseEndpoint = `/subscriptions/${subscriptionUuid}/metrics/usage/views-by-application`; - return this.fetchAllPages(baseEndpoint, 'views', subscriptionUuid, from, to, granularity); // <<< FIX #5: Pass granularity through + return this.fetchAllPages(baseEndpoint, 'views', subscriptionUuid, from, to, resolution); } } From b623ff683a3f20bb38cc335e05067c3ae54837fd Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 11:51:30 -0700 Subject: [PATCH 3/9] fixup! WIP --- app/api/acquia/views/route.ts | 25 +++++++++++++++++++++++-- app/api/acquia/visits/route.ts | 7 ++++++- lib/acquia-api.ts | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 42a4b7f..2560c45 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -7,7 +7,14 @@ export async function GET(request: NextRequest) { const from = searchParams.get('from'); const to = searchParams.get('to'); const resolution = searchParams.get('resolution'); // Get granularity for daily data - + /** + console.log('šŸš€ Views by Application API Route called with params:', { + subscriptionUuid, + from, + to, + resolution + }); + */ if (!subscriptionUuid) { console.error('āŒ Missing required parameter: subscriptionUuid'); return NextResponse.json( @@ -18,9 +25,16 @@ export async function GET(request: NextRequest) { if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) { console.error('āŒ Missing required environment variables!'); + console.error('Available env vars:', Object.keys(process.env).filter(k => k.startsWith('ACQUIA'))); return NextResponse.json( { error: 'Server configuration error: missing API credentials', + envCheck: { + ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ? `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : 'missing', + ACQUIA_API_SECRET: process.env.ACQUIA_API_SECRET ? 'present' : 'missing', + ACQUIA_API_BASE_URL: process.env.ACQUIA_API_BASE_URL || 'missing', + ACQUIA_AUTH_BASE_URL: process.env.ACQUIA_AUTH_BASE_URL || 'missing' + } }, { status: 500 } ); @@ -44,6 +58,7 @@ export async function GET(request: NextRequest) { to || undefined, resolution || undefined ); + // console.log('āœ… Successfully fetched ALL views by application data, total count:', data.length); return NextResponse.json({ data, @@ -51,7 +66,13 @@ export async function GET(request: NextRequest) { message: `Successfully fetched ${data.length} view records across all pages`, }); } catch (error) { - console.error('āŒ API Route Error in /api/acquia/views:', error); + console.error('āŒ API Route Error:', error); + if (error instanceof Error) { + console.error('šŸ” Error name:', error.name); + console.error('šŸ” Error message:', error.message); + console.error('šŸ” Error stack:', error.stack); + } + return NextResponse.json( { diff --git a/app/api/acquia/visits/route.ts b/app/api/acquia/visits/route.ts index 9d29da3..94104ae 100644 --- a/app/api/acquia/visits/route.ts +++ b/app/api/acquia/visits/route.ts @@ -70,7 +70,12 @@ export async function GET(request: NextRequest) { message: `Successfully fetched ${data.length} visit records across all pages` }); } catch (error) { - console.error('āŒ API Route Error in /api/acquia/visits:', error); + console.error('āŒ API Route Error:', error); + if (error instanceof Error) { + console.error('šŸ” Error name:', error.name); + console.error('šŸ” Error message:', error.message); + console.error('šŸ” Error stack:', error.stack); + } return NextResponse.json( { diff --git a/lib/acquia-api.ts b/lib/acquia-api.ts index da1ac4e..5278f0f 100644 --- a/lib/acquia-api.ts +++ b/lib/acquia-api.ts @@ -586,7 +586,7 @@ class AcquiaApiServiceFixed { while (hasMorePages) { try { const params = new URLSearchParams(); - + // Add filter parameter if we have date range if (filterParam) { params.append('filter', filterParam); } From 7d38530002e5615e65739adb609883f93c41aa65 Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 11:59:33 -0700 Subject: [PATCH 4/9] fixup! explicit typing --- app/applications/[uuid]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index b78df90..86581f7 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -22,8 +22,8 @@ export default function ApplicationDetailPage({ params }: any) { const [viewsPct, setViewsPct] = useState(0); const [visitsPct, setVisitsPct] = useState(0); const [error, setError] = useState(null); - const [dailyViews, setDailyViews] = useState([]); - const [dailyVisits, setDailyVisits] = useState([]); + const [dailyViews, setDailyViews] = useState>([]); + const [dailyVisits, setDailyVisits] = useState>([]); // Fetch application name on mount or when subscriptionUuid changes useEffect(() => { From f04c68b4a35a50177fcf10e1a5f70b64bdc1b6ff Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 12:11:08 -0700 Subject: [PATCH 5/9] fixup! WIP --- app/applications/[uuid]/page.tsx | 60 ++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index 86581f7..5c61404 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -5,11 +5,24 @@ import CountUpTimer from '@/components/CountUpTimer'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; const DEFAULT_SUBSCRIPTION_UUID = process.env.NEXT_PUBLIC_ACQUIA_SUBSCRIPTION_UUID || ''; -interface PageProps { - params: { uuid: string }; + +// Define a type for our chart data points +interface DailyDataPoint { + date: string; + value: number; +} + +// Define a type for the expected API response structure +interface AcquiaApiResponse { + data?: Array<{ + applicationUuid: string; + date: string; + views?: number; + visits?: number; + }>; } -export default function ApplicationDetailPage({ params }: any) { +export default function ApplicationDetailPage({ params }: { params: { uuid: string } }) { const [subscriptionUuid, setSubscriptionUuid] = useState(DEFAULT_SUBSCRIPTION_UUID); const [from, setFrom] = useState(''); const [to, setTo] = useState(''); @@ -22,8 +35,9 @@ export default function ApplicationDetailPage({ params }: any) { const [viewsPct, setViewsPct] = useState(0); const [visitsPct, setVisitsPct] = useState(0); const [error, setError] = useState(null); - const [dailyViews, setDailyViews] = useState>([]); - const [dailyVisits, setDailyVisits] = useState>([]); + // Use the DailyDataPoint interface for state + const [dailyViews, setDailyViews] = useState([]); + const [dailyVisits, setDailyVisits] = useState([]); // Fetch application name on mount or when subscriptionUuid changes useEffect(() => { @@ -55,7 +69,6 @@ export default function ApplicationDetailPage({ params }: any) { if (from) paramsObj.from = from; if (to) paramsObj.to = to; - // We only need to fetch daily data, as we can calculate totals from it. const dailyQuery = new URLSearchParams({ ...paramsObj, resolution: 'day' }).toString(); setLoadingStep('Fetching views and visits...'); @@ -64,21 +77,20 @@ export default function ApplicationDetailPage({ params }: any) { fetch(`/api/acquia/visits?${dailyQuery}`), ]); - const [dailyViewsRaw, dailyVisitsRaw] = await Promise.all([ - dailyViewsRes.ok ? dailyViewsRes.json() : [], - dailyVisitsRes.ok ? dailyVisitsRes.json() : [], + const [dailyViewsRaw, dailyVisitsRaw]: [AcquiaApiResponse, AcquiaApiResponse] = await Promise.all([ + dailyViewsRes.ok ? dailyViewsRes.json() : {}, + dailyVisitsRes.ok ? dailyVisitsRes.json() : {}, ]); - // Helper to process and aggregate daily data - const processDailyData = (rawData: any[], metric: 'views' | 'visits') => { + // Helper to process and aggregate daily data with proper types + const processDailyData = (rawData: AcquiaApiResponse, metric: 'views' | 'visits'): DailyDataPoint[] => { const dailyMap = new Map(); - // Filter for the specific application we are viewing - const appData = (rawData || []).filter((d: any) => d.applicationUuid === params.uuid); + const dataArray = rawData.data || []; + + const appData = dataArray.filter((d) => d.applicationUuid === params.uuid); for (const record of appData) { - // FIX #1: The property is named 'date', not 'timestamp' const date = record.date.split('T')[0]; - // FIX #2: The value is in either the 'views' or 'visits' property const value = record[metric] || 0; dailyMap.set(date, (dailyMap.get(date) || 0) + value); } @@ -88,19 +100,21 @@ export default function ApplicationDetailPage({ params }: any) { .sort((a, b) => a.date.localeCompare(b.date)); }; - const processedDailyViews = processDailyData(dailyViewsRaw.data, 'views'); - const processedDailyVisits = processDailyData(dailyVisitsRaw.data, 'visits'); + const processedDailyViews = processDailyData(dailyViewsRaw, 'views'); + const processedDailyVisits = processDailyData(dailyVisitsRaw, 'visits'); - setDailyViews(processedDailyViews as any); - setDailyVisits(processedDailyVisits as any); + // No type assertion needed here + setDailyViews(processedDailyViews); + setDailyVisits(processedDailyVisits); - // Calculate totals for this app from the daily data const appTotalViews = processedDailyViews.reduce((sum, day) => sum + day.value, 0); const appTotalVisits = processedDailyVisits.reduce((sum, day) => sum + day.value, 0); - // Calculate overall totals for percentage calculation - const overallTotalViews = (dailyViewsRaw.data || []).reduce((sum: number, v: any) => sum + (v.views || 0), 0); - const overallTotalVisits = (dailyVisitsRaw.data || []).reduce((sum: number, v: any) => sum + (v.visits || 0), 0); + const overallViewsData = dailyViewsRaw.data || []; + const overallVisitsData = dailyVisitsRaw.data || []; + + const overallTotalViews = overallViewsData.reduce((sum, v) => sum + (v.views || 0), 0); + const overallTotalVisits = overallVisitsData.reduce((sum, v) => sum + (v.visits || 0), 0); setViews(appTotalViews); setVisits(appTotalVisits); From fc8169f6e8fabb20fa373e17ac27a0483033ebe3 Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 12:12:31 -0700 Subject: [PATCH 6/9] fixup! WIP --- app/api/acquia/views/route.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 2560c45..5bd1f26 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -73,7 +73,6 @@ export async function GET(request: NextRequest) { console.error('šŸ” Error stack:', error.stack); } - return NextResponse.json( { error: 'Failed to fetch views by application data', From d5f4d59055707b7c116e6e1d57a2d918e82a76ac Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 12:41:53 -0700 Subject: [PATCH 7/9] fixup! compilation error --- app/applications/[uuid]/page.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index 5c61404..cca2f72 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -46,7 +46,7 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri setLoadingStep('Fetching application info...'); const res = await fetch(`/api/acquia/applications?subscriptionUuid=${subscriptionUuid}`); const apps = await res.json(); - const app = Array.isArray(apps) ? apps.find((a: any) => a.uuid === params.uuid) : null; + const app = Array.isArray(apps.data) ? apps.data.find((a: any) => a.uuid === params.uuid) : null; setAppName(app ? app.name : ''); } catch { setAppName(''); @@ -103,7 +103,6 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri const processedDailyViews = processDailyData(dailyViewsRaw, 'views'); const processedDailyVisits = processDailyData(dailyVisitsRaw, 'visits'); - // No type assertion needed here setDailyViews(processedDailyViews); setDailyVisits(processedDailyVisits); @@ -243,7 +242,7 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri {error && (
{error}
)} - {!appName ? ( + {!appName && !loading ? (
No application found with UUID: {params.uuid}
) : (
From e3256b51bd6313cca56bb713600e2e0a7c4473a2 Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 12:53:16 -0700 Subject: [PATCH 8/9] fixup! compilation error --- app/applications/[uuid]/page.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/applications/[uuid]/page.tsx b/app/applications/[uuid]/page.tsx index cca2f72..1a32b58 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -22,7 +22,11 @@ interface AcquiaApiResponse { }>; } -export default function ApplicationDetailPage({ params }: { params: { uuid: string } }) { +// Use 'any' in the signature to satisfy the Next.js build process for client components. +export default function ApplicationDetailPage({ params }: any) { + // Re-introduce the type inside the component for type safety. + const typedParams: { uuid: string } = params; + const [subscriptionUuid, setSubscriptionUuid] = useState(DEFAULT_SUBSCRIPTION_UUID); const [from, setFrom] = useState(''); const [to, setTo] = useState(''); @@ -35,7 +39,6 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri const [viewsPct, setViewsPct] = useState(0); const [visitsPct, setVisitsPct] = useState(0); const [error, setError] = useState(null); - // Use the DailyDataPoint interface for state const [dailyViews, setDailyViews] = useState([]); const [dailyVisits, setDailyVisits] = useState([]); @@ -46,7 +49,7 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri setLoadingStep('Fetching application info...'); const res = await fetch(`/api/acquia/applications?subscriptionUuid=${subscriptionUuid}`); const apps = await res.json(); - const app = Array.isArray(apps.data) ? apps.data.find((a: any) => a.uuid === params.uuid) : null; + const app = Array.isArray(apps.data) ? apps.data.find((a: any) => a.uuid === typedParams.uuid) : null; setAppName(app ? app.name : ''); } catch { setAppName(''); @@ -55,7 +58,7 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri } }; if (subscriptionUuid) fetchAppName(); - }, [subscriptionUuid, params.uuid]); + }, [subscriptionUuid, typedParams.uuid]); const fetchAppDetail = async () => { setLoading(true); @@ -87,7 +90,7 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri const dailyMap = new Map(); const dataArray = rawData.data || []; - const appData = dataArray.filter((d) => d.applicationUuid === params.uuid); + const appData = dataArray.filter((d) => d.applicationUuid === typedParams.uuid); for (const record of appData) { const date = record.date.split('T')[0]; @@ -139,7 +142,7 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri color: 'var(--stanford-black)', }}>

- Views and Visits Data for {appName ? appName : {params.uuid}} + Views and Visits Data for {appName ? appName : {typedParams.uuid}}

@@ -243,14 +246,14 @@ export default function ApplicationDetailPage({ params }: { params: { uuid: stri
{error}
)} {!appName && !loading ? ( -
No application found with UUID: {params.uuid}
+
No application found with UUID: {typedParams.uuid}
) : (
Name: {appName}
- UUID: {params.uuid} + UUID: {typedParams.uuid}
Views{from && to ? ` (${from} to ${to})` : ''}: {views.toLocaleString()} ({viewsPct.toFixed(1)}%) From a72c515da239b1ada5d70386f12c34733b1802a4 Mon Sep 17 00:00:00 2001 From: John Bickar Date: Fri, 26 Sep 2025 12:57:41 -0700 Subject: [PATCH 9/9] fixup! WIP --- app/api/acquia/views/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 5bd1f26..0975826 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -58,7 +58,8 @@ export async function GET(request: NextRequest) { to || undefined, resolution || undefined ); - // console.log('āœ… Successfully fetched ALL views by application data, total count:', data.length); + + // console.log('āœ… Successfully fetched ALL views by application data, total count:', data.length); return NextResponse.json({ data,