diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts index 72cd719..0975826 100644 --- a/app/api/acquia/views/route.ts +++ b/app/api/acquia/views/route.ts @@ -6,14 +6,15 @@ export async function GET(request: NextRequest) { const subscriptionUuid = searchParams.get('subscriptionUuid'); 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( @@ -22,12 +23,11 @@ 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', @@ -41,7 +41,6 @@ export async function GET(request: NextRequest) { } 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,16 +52,15 @@ 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, + resolution || undefined ); // console.log('āœ… Successfully fetched ALL views by application data, total count:', data.length); - + return NextResponse.json({ data, totalItems: data.length, @@ -70,15 +68,14 @@ export async function GET(request: NextRequest) { }); } 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); } - + 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..94104ae 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 resolution = searchParams.get('resolution'); // 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', @@ -52,13 +53,17 @@ 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); - + const data = await apiService.getVisitsDataByApplication( + subscriptionUuid, + from || undefined, + to || undefined, + resolution || undefined + ); + // console.log('āœ… Successfully fetched ALL visits by application data, total count:', data.length); - + return NextResponse.json({ data, totalItems: data.length, @@ -66,15 +71,14 @@ export async function GET(request: NextRequest) { }); } 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); } - + 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..1a32b58 100644 --- a/app/applications/[uuid]/page.tsx +++ b/app/applications/[uuid]/page.tsx @@ -2,13 +2,31 @@ 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 { - 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; + }>; } +// 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(''); @@ -21,6 +39,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(() => { @@ -29,7 +49,7 @@ export default function ApplicationDetailPage({ params }: any) { 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 === typedParams.uuid) : null; setAppName(app ? app.name : ''); } catch { setAppName(''); @@ -38,7 +58,7 @@ export default function ApplicationDetailPage({ params }: any) { } }; if (subscriptionUuid) fetchAppName(); - }, [subscriptionUuid, params.uuid]); + }, [subscriptionUuid, typedParams.uuid]); const fetchAppDetail = async () => { setLoading(true); @@ -51,45 +71,62 @@ 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(); + + const dailyQuery = new URLSearchParams({ ...paramsObj, resolution: '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]: [AcquiaApiResponse, AcquiaApiResponse] = 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 with proper types + const processDailyData = (rawData: AcquiaApiResponse, metric: 'views' | 'visits'): DailyDataPoint[] => { + const dailyMap = new Map(); + const dataArray = rawData.data || []; + + const appData = dataArray.filter((d) => d.applicationUuid === typedParams.uuid); + + for (const record of appData) { + const date = record.date.split('T')[0]; + 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, 'views'); + const processedDailyVisits = processDailyData(dailyVisitsRaw, 'visits'); + + setDailyViews(processedDailyViews); + setDailyVisits(processedDailyVisits); + + const appTotalViews = processedDailyViews.reduce((sum, day) => sum + day.value, 0); + const appTotalVisits = processedDailyVisits.reduce((sum, day) => sum + day.value, 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); + 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); @@ -105,7 +142,7 @@ export default function ApplicationDetailPage({ params }: any) { color: 'var(--stanford-black)', }}>

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

@@ -208,15 +245,15 @@ export default function ApplicationDetailPage({ params }: any) { {error && (
{error}
)} - {!appName ? ( -
No application found with UUID: {params.uuid}
+ {!appName && !loading ? ( +
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)}%) @@ -226,6 +263,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..5278f0f 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, + resolution?: string ): Promise { - const cacheKey = generateCacheKey([baseEndpoint, subscriptionUuid, from, to]); + const cacheKey = generateCacheKey([baseEndpoint, subscriptionUuid, from, to, resolution]); 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}`); + // Use the resolution parameter if it exists + if (resolution) { + params.append('resolution', resolution); + // console.log(`šŸ“Š Using resolution: ${resolution}`); + } - // 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, resolution?: string): Promise { 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, resolution); } - async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string): Promise { + async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: string, resolution?: string): Promise { 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, resolution); } }