From 663482006477bd8c7e239e8aea3fa961eca682dd Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 13:42:53 -0700
Subject: [PATCH 1/9] Remove LoadingSpinner
---
components/Dashboard.tsx | 5 ++---
components/LoadingSpinner.tsx | 9 ---------
2 files changed, 2 insertions(+), 12 deletions(-)
delete mode 100644 components/LoadingSpinner.tsx
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
index 8773e75..e90a7ae 100644
--- a/components/Dashboard.tsx
+++ b/components/Dashboard.tsx
@@ -4,9 +4,8 @@ import React, { useState, useEffect } from 'react';
import { VisitsData, ViewsData, Application } from '@/lib/acquia-api-fixed';
import VisitsPieChart from './VisitsPieChart';
import ViewsPieChart from './ViewsPieChart';
-import SimpleVisitsBarChart from './SimpleVisitsBarChart'; // Use this
-import SimpleViewsBarChart from './SimpleViewsBarChart'; // Use this
-import LoadingSpinner from './LoadingSpinner';
+import SimpleVisitsBarChart from './SimpleVisitsBarChart';
+import SimpleViewsBarChart from './SimpleViewsBarChart';
import CountUpTimer from './CountUpTimer';
import DataTable from './DataTable';
diff --git a/components/LoadingSpinner.tsx b/components/LoadingSpinner.tsx
deleted file mode 100644
index a9f1930..0000000
--- a/components/LoadingSpinner.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-const LoadingSpinner: React.FC = () => {
- return (
-
- );
-};
-
-export default LoadingSpinner;
\ No newline at end of file
From 670bb61dd9e97615c2079e5cb97b61ee6d8216ec Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 13:46:08 -0700
Subject: [PATCH 2/9] fixup! Cleanup
---
app/api/acquia/applications/route.ts | 2 +-
app/api/acquia/views/route.ts | 2 +-
app/api/acquia/visits/route.ts | 2 +-
app/api/debug-application/route.ts | 106 ----
app/api/debug-auth/route.ts | 69 ---
app/api/debug-env-source/route.ts | 52 --
app/api/debug-raw-response/route.ts | 92 ----
app/api/debug-raw-structure/route.ts | 224 ---------
app/api/test-auth/route.ts | 20 -
app/api/test-date-format/route.ts | 109 -----
app/api/test-date-params/route.ts | 47 --
app/api/test-fixed-auth/route.ts | 36 --
components/Dashboard.tsx | 14 +-
lib/acquia-api-fixed.ts | 682 --------------------------
lib/acquia-api.ts | 708 +++++++++++++++++++++++----
15 files changed, 607 insertions(+), 1558 deletions(-)
delete mode 100644 app/api/debug-application/route.ts
delete mode 100644 app/api/debug-auth/route.ts
delete mode 100644 app/api/debug-env-source/route.ts
delete mode 100644 app/api/debug-raw-response/route.ts
delete mode 100644 app/api/debug-raw-structure/route.ts
delete mode 100644 app/api/test-auth/route.ts
delete mode 100644 app/api/test-date-format/route.ts
delete mode 100644 app/api/test-date-params/route.ts
delete mode 100644 app/api/test-fixed-auth/route.ts
delete mode 100644 lib/acquia-api-fixed.ts
diff --git a/app/api/acquia/applications/route.ts b/app/api/acquia/applications/route.ts
index 3a230e4..14086a3 100644
--- a/app/api/acquia/applications/route.ts
+++ b/app/api/acquia/applications/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
+import AcquiaApiServiceFixed from '@/lib/acquia-api';
export async function GET(request: NextRequest) {
console.log('๐ Applications API Route called');
diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts
index 43622c7..143c515 100644
--- a/app/api/acquia/views/route.ts
+++ b/app/api/acquia/views/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
+import AcquiaApiServiceFixed from '@/lib/acquia-api';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
diff --git a/app/api/acquia/visits/route.ts b/app/api/acquia/visits/route.ts
index ce8d315..169c337 100644
--- a/app/api/acquia/visits/route.ts
+++ b/app/api/acquia/visits/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
+import AcquiaApiServiceFixed from '@/lib/acquia-api';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
diff --git a/app/api/debug-application/route.ts b/app/api/debug-application/route.ts
deleted file mode 100644
index d884535..0000000
--- a/app/api/debug-application/route.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
-
-export async function GET(request: NextRequest) {
- const searchParams = request.nextUrl.searchParams;
- const subscriptionUuid = searchParams.get('subscriptionUuid');
- const applicationUuid = searchParams.get('applicationUuid');
- const endpoint = searchParams.get('endpoint') || 'views'; // 'visits' or 'views'
-
- if (!subscriptionUuid) {
- return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 });
- }
-
- if (!process.env.ACQUIA_API_SECRET) {
- return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
- }
-
- try {
- 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',
- apiKey: process.env.ACQUIA_API_KEY!,
- apiSecret: process.env.ACQUIA_API_SECRET!,
- });
-
- // Get the access token
- const token = await apiService.getAccessToken();
- const baseUrl = process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api';
- const fullUrl = `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application`;
-
- console.log('๐ Making debug request to:', fullUrl);
-
- const response = await fetch(fullUrl, {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Accept': '*/*',
- },
- });
-
- const rawData = await response.json();
-
- // Extract applications and their details
- const applications: any[] = [];
-
- if (rawData._embedded?.metadata) {
- rawData._embedded.metadata.forEach((metaItem: any, index: number) => {
- if (metaItem.uuids && Array.isArray(metaItem.uuids)) {
- metaItem.uuids.forEach((appUuid: string, appIndex: number) => {
- const appInfo = {
- uuid: appUuid,
- name: metaItem.names?.[appIndex] || `App ${appUuid.substring(0, 8)}`,
- metadataIndex: index,
- environments: [],
- totalDatapoints: 0,
- sampleDatapoints: [] as any[]
- };
-
- // Add environment info if available
- if (metaItem.ids && Array.isArray(metaItem.ids)) {
- metaItem.ids.forEach((envId: string, envIndex: number) => {
- appInfo.environments.push({
- id: envId,
- name: metaItem.environmentNames?.[envIndex] || metaItem.environment_names?.[envIndex] || `Env ${envId.substring(0, 8)}`
- });
- });
- }
-
- applications.push(appInfo);
- });
- }
- });
- }
-
- // Count datapoints and get samples
- if (rawData._embedded?.datapoints) {
- applications.forEach(app => {
- app.totalDatapoints = rawData._embedded.datapoints.length;
- app.sampleDatapoints = rawData._embedded.datapoints.slice(0, 5);
- });
- }
-
- // Find the specific application if requested
- let specificApplication = null;
- if (applicationUuid) {
- specificApplication = applications.find(app => app.uuid === applicationUuid);
- }
-
- return NextResponse.json({
- url: fullUrl,
- status: response.status,
- totalApplications: applications.length,
- totalDatapoints: rawData._embedded?.datapoints?.length || 0,
- applications: applications,
- specificApplication: specificApplication,
- searchedFor: applicationUuid,
- rawMetadata: rawData._embedded?.metadata || [],
- sampleDatapoints: rawData._embedded?.datapoints?.slice(0, 10) || []
- });
-
- } catch (error) {
- return NextResponse.json({
- error: 'Debug request failed',
- details: error instanceof Error ? error.message : String(error)
- }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/app/api/debug-auth/route.ts b/app/api/debug-auth/route.ts
deleted file mode 100644
index 5520fb0..0000000
--- a/app/api/debug-auth/route.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- // Get credentials from environment
- const apiKey = process.env.ACQUIA_API_KEY;
- const apiSecret = process.env.ACQUIA_API_SECRET;
- const authUrl = process.env.ACQUIA_AUTH_BASE_URL || 'https://accounts.acquia.com/api';
-
- // Check if credentials exist
- if (!apiKey || !apiSecret) {
- return NextResponse.json({
- error: 'Missing API credentials',
- envVars: {
- ACQUIA_API_KEY: apiKey ? `${apiKey.substring(0, 5)}...` : 'not set',
- ACQUIA_API_SECRET: apiSecret ? `${apiSecret.substring(0, 5)}...` : 'not set',
- ACQUIA_AUTH_BASE_URL: authUrl
- }
- }, { status: 400 });
- }
-
- try {
- // Attempt authentication with debug info
- const credentials = Buffer.from(`${apiKey}:${apiSecret}`).toString('base64');
- console.log('๐ Encoded credentials (first 10 chars):', credentials.substring(0, 10) + '...');
-
- const formData = new URLSearchParams();
- formData.append('grant_type', 'client_credentials');
-
- // Try authentication
- const response = await fetch(`${authUrl}/auth/oauth/token`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': '*/*',
- 'Authorization': `Basic ${credentials}`
- },
- body: 'grant_type=client_credentials'
- });
-
- const data = await response.json();
-
- if (response.ok) {
- return NextResponse.json({
- success: true,
- message: 'Authentication successful',
- token_type: data.token_type,
- expires_in: data.expires_in,
- token_preview: data.access_token ? `${data.access_token.substring(0, 10)}...` : null
- });
- } else {
- return NextResponse.json({
- error: 'Authentication failed',
- status: response.status,
- data,
- request_details: {
- url: `${authUrl}/auth/oauth/token`,
- api_key_preview: apiKey.substring(0, 5) + '...',
- api_key_length: apiKey.length,
- credentials_preview: credentials.substring(0, 10) + '...'
- }
- }, { status: response.status });
- }
- } catch (error) {
- return NextResponse.json({
- error: 'Failed to authenticate',
- message: error instanceof Error ? error.message : String(error)
- }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/app/api/debug-env-source/route.ts b/app/api/debug-env-source/route.ts
deleted file mode 100644
index 9ddb8a1..0000000
--- a/app/api/debug-env-source/route.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import fs from 'fs';
-import path from 'path';
-
-export async function GET(request: NextRequest) {
- const projectRoot = process.cwd();
-
- // Check for all possible env files
- const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
- const foundFiles: Record = {};
-
- for (const file of envFiles) {
- const filePath = path.join(projectRoot, file);
- try {
- if (fs.existsSync(filePath)) {
- const content = fs.readFileSync(filePath, 'utf8');
- const lines = content.split('\n');
- const acquiaLines = lines.filter(line =>
- line.trim().startsWith('ACQUIA_API_KEY') ||
- line.trim().startsWith('ACQUIA_API_SECRET')
- );
-
- foundFiles[file] = {
- exists: true,
- acquiaLines: acquiaLines,
- fullContent: content.substring(0, 500) + (content.length > 500 ? '...' : '')
- };
- } else {
- foundFiles[file] = { exists: false };
- }
- } catch (error) {
- foundFiles[file] = {
- exists: false,
- error: error instanceof Error ? error.message : String(error)
- };
- }
- }
-
- return NextResponse.json({
- message: 'Environment Files Debug',
- project_root: projectRoot,
- node_env: process.env.NODE_ENV,
- current_values: {
- ACQUIA_API_KEY: process.env.ACQUIA_API_KEY,
- ACQUIA_API_SECRET: process.env.ACQUIA_API_SECRET ? '[hidden]' : undefined,
- ACQUIA_API_KEY_LENGTH: process.env.ACQUIA_API_KEY?.length,
- },
- env_files_found: foundFiles,
- expected_api_key: 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3',
- values_match: process.env.ACQUIA_API_KEY === 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'
- });
-}
\ No newline at end of file
diff --git a/app/api/debug-raw-response/route.ts b/app/api/debug-raw-response/route.ts
deleted file mode 100644
index 6ea27a4..0000000
--- a/app/api/debug-raw-response/route.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
-
-export async function GET(request: NextRequest) {
- const searchParams = request.nextUrl.searchParams;
- const subscriptionUuid = searchParams.get('subscriptionUuid');
- const endpoint = searchParams.get('endpoint') || 'visits'; // 'visits' or 'views'
-
- if (!subscriptionUuid) {
- return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 });
- }
-
- if (!process.env.ACQUIA_API_SECRET) {
- return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
- }
-
- try {
- 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',
- apiKey: process.env.ACQUIA_API_KEY!,
- apiSecret: process.env.ACQUIA_API_SECRET!,
- });
-
- // Get the access token using the public method
- const token = await apiService.getAccessToken();
- const baseUrl = process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api';
- const fullUrl = `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application`;
-
- console.log('๐ Making debug request to:', fullUrl);
-
- const response = await fetch(fullUrl, {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Accept': '*/*',
- },
- });
-
- const rawData = await response.json();
-
- return NextResponse.json({
- url: fullUrl,
- status: response.status,
- headers: Object.fromEntries(response.headers.entries()),
- rawResponse: rawData,
- responseKeys: Object.keys(rawData),
- embeddedKeys: rawData._embedded ? Object.keys(rawData._embedded) : null,
- dataStructureAnalysis: analyzeDataStructure(rawData)
- });
-
- } catch (error) {
- return NextResponse.json({
- error: 'Debug request failed',
- details: error instanceof Error ? error.message : String(error)
- }, { status: 500 });
- }
-}
-
-function analyzeDataStructure(data: any, path = ''): any {
- if (Array.isArray(data)) {
- return {
- type: 'array',
- length: data.length,
- firstItem: data.length > 0 ? analyzeDataStructure(data[0], `${path}[0]`) : null,
- sampleItems: data.slice(0, 3).map((item, i) => analyzeDataStructure(item, `${path}[${i}]`))
- };
- } else if (data && typeof data === 'object') {
- const keys = Object.keys(data);
- const analysis: any = {
- type: 'object',
- keys: keys,
- keyCount: keys.length
- };
-
- // Analyze first few keys
- keys.slice(0, 5).forEach(key => {
- analysis[key] = analyzeDataStructure(data[key], `${path}.${key}`);
- });
-
- return analysis;
- } else {
- return {
- type: typeof data,
- value: data
- };
- }
-}
-
-// Also export POST method if you want to test with POST requests
-export async function POST(request: NextRequest) {
- return NextResponse.json({ error: 'Use GET method' }, { status: 405 });
-}
diff --git a/app/api/debug-raw-structure/route.ts b/app/api/debug-raw-structure/route.ts
deleted file mode 100644
index 8e361bb..0000000
--- a/app/api/debug-raw-structure/route.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
-
-// Add proper handling for circular references in JSON
-function safeStringify(obj: any): string {
- const seen = new WeakSet();
- return JSON.stringify(obj, (key, value) => {
- if (typeof value === 'object' && value !== null) {
- if (seen.has(value)) {
- return '[Circular Reference]';
- }
- seen.add(value);
- }
- return value;
- }, 2);
-}
-
-export async function GET(request: NextRequest) {
- const searchParams = request.nextUrl.searchParams;
- const subscriptionUuid = searchParams.get('subscriptionUuid');
- const endpoint = searchParams.get('endpoint') || 'views';
- const from = searchParams.get('from');
- const to = searchParams.get('to');
-
- if (!subscriptionUuid) {
- return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 });
- }
-
- if (!process.env.ACQUIA_API_SECRET) {
- return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
- }
-
- try {
- 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',
- apiKey: process.env.ACQUIA_API_KEY!,
- apiSecret: process.env.ACQUIA_API_SECRET!,
- });
-
- const token = await apiService.getAccessToken();
- const baseUrl = process.env.ACQUIA_API_BASE_URL || 'https://cloud.acquia.com/api';
-
- // Build URL with date parameters if provided
- const params = new URLSearchParams();
- if (from) params.append('from', from);
- if (to) params.append('to', to);
-
- const fullUrl = `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application${params.toString() ? `?${params.toString()}` : ''}`;
-
- console.log('๐ Making comprehensive debug request to:', fullUrl);
-
- const response = await fetch(fullUrl, {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Accept': '*/*',
- },
- });
-
- const rawData = await response.json();
-
- // Comprehensive analysis function
- function analyzeStructure(obj: any, path: string = '', maxDepth: number = 5): any {
- if (maxDepth <= 0) return { type: typeof obj, note: 'max_depth_reached' };
-
- if (obj === null) return { type: 'null' };
- if (obj === undefined) return { type: 'undefined' };
-
- if (Array.isArray(obj)) {
- return {
- type: 'array',
- length: obj.length,
- isEmpty: obj.length === 0,
- firstItems: obj.slice(0, 3).map((item, i) => ({
- index: i,
- analysis: analyzeStructure(item, `${path}[${i}]`, maxDepth - 1)
- })),
- allItemTypes: [...new Set(obj.map(item => Array.isArray(item) ? 'array' : typeof item))]
- };
- }
-
- if (typeof obj === 'object') {
- const keys = Object.keys(obj);
- const analysis: any = {
- type: 'object',
- keyCount: keys.length,
- isEmpty: keys.length === 0,
- keys: keys
- };
-
- // Analyze each key
- keys.forEach(key => {
- analysis[key] = analyzeStructure(obj[key], `${path}.${key}`, maxDepth - 1);
- });
-
- return analysis;
- }
-
- return {
- type: typeof obj,
- value: typeof obj === 'string' ? obj.substring(0, 100) + (obj.length > 100 ? '...' : '') : obj
- };
- }
-
- const structureAnalysis = analyzeStructure(rawData);
-
- // Look for any arrays or data structures that might contain our data
- function findArraysAndData(obj: any, path: string = ''): any[] {
- const results: any[] = [];
-
- if (Array.isArray(obj)) {
- results.push({
- path,
- type: 'array',
- length: obj.length,
- sample: obj.slice(0, 2)
- });
- } else if (obj && typeof obj === 'object') {
- Object.keys(obj).forEach(key => {
- const fullPath = path ? `${path}.${key}` : key;
- results.push(...findArraysAndData(obj[key], fullPath));
- });
- }
-
- return results;
- }
-
- const foundArrays = findArraysAndData(rawData);
-
- // Check for any UUID-like strings anywhere in the response
- function findUUIDs(obj: any, path: string = ''): any[] {
- const results: any[] = [];
- const uuidPattern = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi;
-
- if (typeof obj === 'string' && uuidPattern.test(obj)) {
- results.push({ path, value: obj });
- } else if (Array.isArray(obj)) {
- obj.forEach((item, index) => {
- results.push(...findUUIDs(item, `${path}[${index}]`));
- });
- } else if (obj && typeof obj === 'object') {
- Object.keys(obj).forEach(key => {
- const fullPath = path ? `${path}.${key}` : key;
- results.push(...findUUIDs(obj[key], fullPath));
- });
- }
-
- return results;
- }
-
- const foundUUIDs = findUUIDs(rawData);
-
- // Check for any numeric data that might be metrics
- function findNumericData(obj: any, path: string = ''): any[] {
- const results: any[] = [];
-
- if (typeof obj === 'number') {
- results.push({ path, value: obj });
- } else if (typeof obj === 'string' && !isNaN(Number(obj))) {
- results.push({ path, value: obj, note: 'numeric_string' });
- } else if (Array.isArray(obj)) {
- obj.forEach((item, index) => {
- results.push(...findNumericData(item, `${path}[${index}]`));
- });
- } else if (obj && typeof obj === 'object') {
- Object.keys(obj).forEach(key => {
- const fullPath = path ? `${path}.${key}` : key;
- results.push(...findNumericData(obj[key], fullPath));
- });
- }
-
- return results;
- }
-
- const foundNumbers = findNumericData(rawData);
-
- const responseData = {
- request: {
- url: fullUrl,
- method: 'GET',
- hasDateRange: !!(from || to),
- dateRange: { from, to }
- },
- response: {
- status: response.status,
- statusText: response.statusText,
- headers: Object.fromEntries(response.headers.entries())
- },
- analysis: {
- topLevelKeys: Object.keys(rawData),
- structureAnalysis,
- foundArrays,
- foundUUIDs,
- foundNumbers: foundNumbers.slice(0, 20), // Limit to first 20 to avoid huge responses
- totalNumericValues: foundNumbers.length
- },
- rawResponse: rawData,
- searchTargets: {
- lookingFor: 'datapoints, metadata, applications with UUIDs',
- targetUUID: '3e02ea73-76fa-4a88-91d7-3476aca3cf07'
- }
- };
-
- return new NextResponse(safeStringify(responseData), {
- status: 200,
- headers: {
- 'Content-Type': 'application/json'
- }
- });
-
- } catch (error) {
- const errorResponse = {
- error: 'Debug request failed',
- details: error instanceof Error ? error.message : String(error)
- };
-
- return new NextResponse(safeStringify(errorResponse), {
- status: 500,
- headers: {
- 'Content-Type': 'application/json'
- }
- });
- }
-}
diff --git a/app/api/test-auth/route.ts b/app/api/test-auth/route.ts
deleted file mode 100644
index ec582aa..0000000
--- a/app/api/test-auth/route.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import { testAcquiaAuth } from '@/lib/test-auth';
-
-export async function GET(request: NextRequest) {
- console.log('๐งช Test authentication endpoint called');
-
- if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) {
- return NextResponse.json(
- { error: 'Missing API credentials in environment variables' },
- { status: 500 }
- );
- }
-
- const result = await testAcquiaAuth(
- process.env.ACQUIA_API_KEY,
- process.env.ACQUIA_API_SECRET
- );
-
- return NextResponse.json(result);
-}
\ No newline at end of file
diff --git a/app/api/test-date-format/route.ts b/app/api/test-date-format/route.ts
deleted file mode 100644
index e940aa4..0000000
--- a/app/api/test-date-format/route.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
-
-export async function GET(request: NextRequest) {
- const searchParams = request.nextUrl.searchParams;
- const subscriptionUuid = searchParams.get('subscriptionUuid');
- const from = searchParams.get('from');
- const to = searchParams.get('to');
- const endpoint = searchParams.get('endpoint') || 'views';
-
- if (!subscriptionUuid) {
- return NextResponse.json({ error: 'subscriptionUuid required' }, { status: 400 });
- }
-
- try {
- 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',
- apiKey: process.env.ACQUIA_API_KEY!,
- apiSecret: process.env.ACQUIA_API_SECRET!,
- });
-
- const token = await apiService.getAccessToken();
-
- // Test different URL formats
- const baseUrl = `https://cloud.acquia.com/api/subscriptions/${subscriptionUuid}/metrics/usage/${endpoint}-by-application`;
-
- const testUrls = [];
-
- if (from && to) {
- // Format 1: Our current format
- const filter1 = `from=${from}T00:00:00.000Z,to=${to}T23:59:59.000Z`;
- testUrls.push({
- name: 'Current format',
- url: `${baseUrl}?filter=${encodeURIComponent(filter1)}&resolution=day`,
- filter: filter1
- });
-
- // Format 2: Your working example format
- const filter2 = `from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z`;
- testUrls.push({
- name: 'Working example format',
- url: `${baseUrl}?filter=${encodeURIComponent(filter2)}&resolution=day`,
- filter: filter2
- });
- } else {
- // No date filter
- testUrls.push({
- name: 'No date filter',
- url: `${baseUrl}?resolution=day`,
- filter: 'none'
- });
- }
-
- const results = [];
-
- for (const testUrl of testUrls) {
- try {
- console.log(`Testing: ${testUrl.url}`);
-
- const response = await fetch(testUrl.url, {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Accept': '*/*',
- },
- });
-
- const data = await response.json();
-
- const itemCount = data._embedded?.items?.length || 0;
- let dateRange = 'no data';
-
- if (itemCount > 0 && data._embedded.items[0].datapoints?.length > 0) {
- const datapoints = data._embedded.items[0].datapoints;
- const firstDate = Array.isArray(datapoints[0]) ? datapoints[0][0] : 'unknown';
- const lastDate = Array.isArray(datapoints[datapoints.length - 1]) ? datapoints[datapoints.length - 1][0] : 'unknown';
- dateRange = `${firstDate} to ${lastDate}`;
- }
-
- results.push({
- ...testUrl,
- status: response.status,
- itemCount,
- dateRange,
- success: response.status === 200
- });
- } catch (error) {
- results.push({
- ...testUrl,
- status: 'error',
- error: error instanceof Error ? error.message : String(error),
- success: false
- });
- }
- }
-
- return NextResponse.json({
- message: 'Date format testing results',
- requestedRange: { from, to },
- results
- });
-
- } catch (error) {
- return NextResponse.json({
- error: 'Test failed',
- details: error instanceof Error ? error.message : String(error)
- }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/app/api/test-date-params/route.ts b/app/api/test-date-params/route.ts
deleted file mode 100644
index 28320ff..0000000
--- a/app/api/test-date-params/route.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- const searchParams = request.nextUrl.searchParams;
- const from = searchParams.get('from');
- const to = searchParams.get('to');
- const subscriptionUuid = searchParams.get('subscriptionUuid');
-
- // Test the exact URL format
- const baseUrl = 'https://cloud.acquia.com/api';
-
- // Build filter exactly as the API expects
- let filterParts: string[] = [];
-
- if (from) {
- const fromDate = from.includes('T') ? from : `${from}T00:00:00.000Z`;
- filterParts.push(`from=${fromDate}`);
- }
-
- if (to) {
- const toDate = to.includes('T') ? to : `${to}T23:59:59.000Z`;
- filterParts.push(`to=${toDate}`);
- }
-
- const filterParam = filterParts.join(',');
-
- const testUrls = {
- visits: `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/visits-by-application?filter=${encodeURIComponent(filterParam)}&resolution=day`,
- views: `${baseUrl}/subscriptions/${subscriptionUuid}/metrics/usage/views-by-application?filter=${encodeURIComponent(filterParam)}&resolution=month`,
-
- // Compare with your working examples
- yourExampleViews: `${baseUrl}/subscriptions/0bc0f7c5-b96a-43c9-b74f-3dd3810a5245/metrics/usage/views-by-application?filter=from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z&resolution=month`,
- yourExampleVisits: `${baseUrl}/subscriptions/0bc0f7c5-b96a-43c9-b74f-3dd3810a5245/metrics/usage/visits-by-application?filter=from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z&resolution=day`
- };
-
- return NextResponse.json({
- input: { from, to, subscriptionUuid },
- filterParam,
- encodedFilter: encodeURIComponent(filterParam),
- testUrls,
- comparison: {
- ourFormat: filterParam,
- expectedFormat: 'from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z',
- matches: filterParam === 'from=2025-04-01T00:00:00.000Z,to=2025-04-30T23:59:59.000Z'
- }
- });
-}
\ No newline at end of file
diff --git a/app/api/test-fixed-auth/route.ts b/app/api/test-fixed-auth/route.ts
deleted file mode 100644
index 07a5979..0000000
--- a/app/api/test-fixed-auth/route.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import AcquiaApiServiceFixed from '@/lib/acquia-api-fixed';
-
-export async function GET(request: NextRequest) {
- console.log('๐งช Testing with FIXED API service...');
-
- if (!process.env.ACQUIA_API_SECRET) {
- return NextResponse.json({ error: 'Missing API secret' }, { status: 500 });
- }
-
- try {
- 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',
- apiKey: process.env.ACQUIA_API_KEY!, // This will be ignored and replaced with correct value
- apiSecret: process.env.ACQUIA_API_SECRET!,
- });
-
- // Just test authentication
- console.log('Testing authentication only...');
-
- return NextResponse.json({
- success: true,
- message: 'Fixed API service created successfully',
- forced_api_key: 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'
- });
-
- } catch (error) {
- console.error('โ Fixed API service error:', error);
-
- return NextResponse.json({
- error: 'Failed with fixed API service',
- details: error instanceof Error ? error.message : 'Unknown error'
- }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
index e90a7ae..2a652e8 100644
--- a/components/Dashboard.tsx
+++ b/components/Dashboard.tsx
@@ -1,7 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
-import { VisitsData, ViewsData, Application } from '@/lib/acquia-api-fixed';
+import { VisitsData, ViewsData, Application } from '@/lib/acquia-api';
import VisitsPieChart from './VisitsPieChart';
import ViewsPieChart from './ViewsPieChart';
import SimpleVisitsBarChart from './SimpleVisitsBarChart';
@@ -330,18 +330,6 @@ const Dashboard: React.FC = () => {
(Note that it can take several minutes to fetch data from the Acquia API.)
-
-
- Debug Environment Variables
-
-
diff --git a/lib/acquia-api-fixed.ts b/lib/acquia-api-fixed.ts
deleted file mode 100644
index ebc1e5f..0000000
--- a/lib/acquia-api-fixed.ts
+++ /dev/null
@@ -1,682 +0,0 @@
-import axios from 'axios';
-
-export interface VisitsData {
- applicationUuid: string;
- applicationName?: string;
- environmentUuid?: string;
- environmentName?: string;
- visits: number;
- date: string;
-}
-
-export interface ViewsData {
- applicationUuid: string;
- applicationName?: string;
- environmentUuid?: string;
- environmentName?: string;
- views: number;
- date: string;
-}
-
-export interface Application {
- uuid: string;
- name: string;
- subscription?: {
- uuid: string;
- name: string;
- };
- environments?: {
- uuid: string;
- name: string;
- }[];
-}
-
-export interface AcquiaApiConfig {
- baseUrl: string;
- authUrl: string;
- apiKey: string;
- apiSecret: string;
-}
-
-export interface FetchProgress {
- step: string;
- currentPage?: number;
- totalPages?: number;
- itemsCollected?: number;
-}
-
-class AcquiaApiServiceFixed {
- private config: AcquiaApiConfig;
- private accessToken: string | null = null;
- private readonly AUTH_TIMEOUT = 120000;
- private readonly API_TIMEOUT = 120000;
- private progressCallback?: (progress: FetchProgress) => void;
-
- constructor(config: AcquiaApiConfig) {
- this.config = config;
-
- console.log('๐ง Initializing Acquia API Service...');
- }
- setProgressCallback(callback: (progress: FetchProgress) => void) {
- this.progressCallback = callback;
- }
-
- private reportProgress(progress: FetchProgress) {
- if (this.progressCallback) {
- this.progressCallback(progress);
- }
- console.log('๐ Progress:', progress);
- }
-
- private async getAccessToken(): Promise {
- if (this.accessToken) {
- return this.accessToken;
- }
-
- // Debug credentials
- console.log('๐ Debug API Key:', {
- value: this.config.apiKey ? `${this.config.apiKey.substring(0, 8)}...` : 'missing',
- length: this.config.apiKey?.length || 0,
- hasQuotes: this.config.apiKey?.startsWith('"') && this.config.apiKey?.endsWith('"')
- });
-
- console.log('๐ Debug API Secret:', {
- preview: this.config.apiSecret ? `${this.config.apiSecret.substring(0, 8)}...` : 'missing',
- length: this.config.apiSecret?.length || 0,
- hasQuotes: this.config.apiSecret?.startsWith('"') && this.config.apiSecret?.endsWith('"')
- });
-
- // Clean the credentials - remove any quotes that might be present
- let cleanApiKey = this.config.apiKey.replace(/^"|"$/g, '').trim();
- const cleanApiSecret = this.config.apiSecret.replace(/^"|"$/g, '').trim();
-
- // 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) &&
- cleanApiKey.match(/^[A-Za-z0-9+/]+=*$/)) {
- try {
- const decodedKey = Buffer.from(cleanApiKey, 'base64').toString('utf-8');
- // Check if decoded value looks like a UUID
- if (decodedKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
- console.log('๐ง Detected base64-encoded API key, using decoded value');
- cleanApiKey = decodedKey;
- }
- } catch (error) {
- // If decoding fails, use original value
- console.log('โ ๏ธ Failed to decode suspected base64 API key, using original value');
- }
- }
-
- console.log('๐ Using cleaned credentials:', {
- keyLength: cleanApiKey.length,
- secretLength: cleanApiSecret.length
- });
-
- const authUrl = `${this.config.authUrl}/auth/oauth/token`;
-
- // Try different authentication methods
- const authMethods = [
- // Method 1: Basic Auth
- async () => {
- console.log('๐ Trying Basic Auth method...');
- const credentials = Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64');
- const response = await axios({
- method: 'POST',
- url: authUrl,
- headers: {
- 'Authorization': `Basic ${credentials}`,
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': '*/*',
- },
- data: 'grant_type=client_credentials',
- 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;
- }
- throw new Error(`Basic Auth failed: ${response.status} - ${JSON.stringify(response.data)}`);
- },
-
- // Method 2: Form parameters
- async () => {
- console.log('๐ Trying Form Parameters method...');
- const formData = new URLSearchParams();
- formData.append('grant_type', 'client_credentials');
- formData.append('client_id', cleanApiKey);
- formData.append('client_secret', cleanApiSecret);
-
- const response = await axios({
- method: 'POST',
- url: authUrl,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': '*/*',
- },
- data: formData.toString(),
- timeout: this.AUTH_TIMEOUT,
- validateStatus: () => true,
- });
-
- console.log('๐ฅ Form Parameters response status:', response.status);
- if (response.status === 200 && response.data?.access_token) {
- return response.data.access_token;
- }
- 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...');
-
- // Try with a UUID format if the key is not already in UUID format
- const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey);
- const clientId = isUuid
- ? cleanApiKey
- : 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'; // fallback to known working UUID
-
- const formData = new URLSearchParams();
- formData.append('grant_type', 'client_credentials');
- formData.append('client_id', clientId);
- formData.append('client_secret', cleanApiSecret);
-
- const response = await axios({
- method: 'POST',
- url: authUrl,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': '*/*',
- },
- data: formData.toString(),
- timeout: this.AUTH_TIMEOUT,
- validateStatus: () => true,
- });
-
- console.log('๐ฅ Alternate client ID response status:', response.status);
- if (response.status === 200 && response.data?.access_token) {
- return response.data.access_token;
- }
- throw new Error(`Alternate client ID failed: ${response.status} - ${JSON.stringify(response.data)}`);
- }
- ];
-
- // Try each authentication method
- let lastError: Error | null = null;
- for (const method of authMethods) {
- try {
- const token = await method();
- this.accessToken = token;
- console.log('โ
Successfully authenticated!');
- return token;
- } catch (error) {
- lastError = error instanceof Error ? error : new Error(String(error));
- console.warn('โ ๏ธ Auth method failed:', lastError.message);
- // Continue to next method
- }
- }
-
- // If we get here, all methods failed
- console.error('โ All authentication methods failed');
- throw lastError || new Error('Failed to authenticate with Acquia API');
- }
-
- private async makeAuthenticatedRequest(endpoint: string) {
- const token = await this.getAccessToken();
- const fullUrl = `${this.config.baseUrl}${endpoint}`;
- try {
- const response = await axios.get(fullUrl, {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Accept': '*/*',
- },
- 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: {
- 'Authorization': `Bearer ${newToken}`,
- 'Accept': '*/*',
- },
- timeout: this.API_TIMEOUT,
- });
- }
- }
-
- throw error;
- }
- }
-
- async getApplications(): Promise {
- try {
- console.log(`๐ Fetching all applications`);
-
- const response = await this.makeAuthenticatedRequest('/applications');
-
- console.log('โ
Applications API Response Status:', response.status);
-
- let applications: Application[] = [];
-
- if (response.data._embedded?.items) {
- applications = response.data._embedded.items.map((item: any) => ({
- uuid: item.uuid,
- name: item.name || `App ${item.uuid.substring(0, 8)}`,
- subscription: item.subscription ? {
- uuid: item.subscription.uuid,
- name: item.subscription.name
- } : undefined,
- environments: item._embedded?.environments?.map((env: any) => ({
- uuid: env.uuid,
- name: env.name
- }))
- }));
-
- console.log(`โ
Extracted ${applications.length} applications`);
- } else {
- console.warn('โ ๏ธ No applications found in response');
- }
-
- return applications;
- } catch (error) {
- console.error('โ Error fetching applications:', error);
- throw error;
- }
- }
-
- private buildFilterParam(from?: string, to?: string): string {
- if (!from && !to) {
- console.log('๐
No date range specified, API will return default data');
- return '';
- }
-
- console.log(`๐
Building filter for date range: ${from} to ${to}`);
-
- // Convert YYYY-MM-DD format to the exact format the API expects
- const formatDateForApi = (dateStr: string, isEndDate: boolean = false): string => {
- // If it's already in the correct ISO format, return as-is
- if (dateStr.includes('T') && dateStr.includes('Z')) {
- return dateStr;
- }
-
- // Convert YYYY-MM-DD to the exact format Acquia expects
- let isoDate: string;
- if (dateStr.includes('T')) {
- // Already has time component, just ensure it ends with Z
- isoDate = dateStr.endsWith('Z') ? dateStr : `${dateStr}Z`;
- } else {
- // Simple date format, add appropriate time
- if (isEndDate) {
- // For end date, use end of day
- isoDate = `${dateStr}T23:59:59.000Z`;
- } else {
- // For start date, use beginning of day
- isoDate = `${dateStr}T00:00:00.000Z`;
- }
- }
-
- console.log(`๐
Formatted ${dateStr} (end=${isEndDate}) -> ${isoDate}`);
- return isoDate;
- };
-
- let filterParts: string[] = [];
-
- if (from) {
- const fromDate = formatDateForApi(from, false);
- filterParts.push(`from=${fromDate}`);
- }
-
- if (to) {
- const toDate = formatDateForApi(to, true);
- filterParts.push(`to=${toDate}`);
- }
-
- const filterString = filterParts.join(',');
- console.log(`๐
Final filter parameter: ${filterString}`);
- return filterString;
- }
-
- 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 [];
- }
-
- console.log('๐ _embedded keys:', Object.keys(responseData._embedded));
-
- if (!responseData._embedded.items || !Array.isArray(responseData._embedded.items)) {
- console.warn('โ ๏ธ No _embedded.items array found in response');
- return [];
- }
-
- const items = responseData._embedded.items;
- console.log(`๐ Found ${items.length} items in _embedded.items`);
-
- const parsedVisitsData: VisitsData[] = [];
- const parsedViewsData: ViewsData[] = [];
-
- items.forEach((item: any, itemIndex: number) => {
- console.log(`\n๐ข === PROCESSING ITEM ${itemIndex} (One Application) ===`);
- console.log(`๐ Item structure: hasDatapoints=${!!item.datapoints}, datapointsCount=${item.datapoints?.length || 0}, hasMetadata=${!!item.metadata}, metadataKeys=${item.metadata ? JSON.stringify(Object.keys(item.metadata)) : '[]'}`);
- // FIRST: Extract the application metadata for this entire item
- let applicationUuid = '';
- let applicationName = '';
- let environmentUuids: string[] = [];
- let environmentNames: string[] = [];
-
- console.log(`๐ Extracting metadata for item ${itemIndex}...`);
-
- // Get application info from metadata.application.uuids[0]
- if (item.metadata?.application?.uuids && Array.isArray(item.metadata.application.uuids)) {
- applicationUuid = item.metadata.application.uuids[0] || '';
- console.log(` ๐ Application UUID: ${applicationUuid}`);
- } else {
- console.log(` โ No application UUID found in metadata for item ${itemIndex}`);
- if (item.metadata) {
- console.log(` ๐ Available metadata: ${JSON.stringify(item.metadata, null, 2)}`);
- } else {
- console.log(` ๐ No metadata available`);
- }
- }
-
- // Get application name from metadata.application.names[0]
- if (item.metadata?.application?.names && Array.isArray(item.metadata.application.names)) {
- 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)}`;
- console.log(` ๐ Generated application name: ${applicationName}`);
- }
-
- // Get environment info if available
- if (item.metadata?.environment) {
- if (item.metadata.environment.uuids && Array.isArray(item.metadata.environment.uuids)) {
- environmentUuids = item.metadata.environment.uuids;
- console.log(` ๐ Environment UUIDs (${environmentUuids.length}): ${JSON.stringify(environmentUuids)}`);
- }
-
- if (item.metadata.environment.names && Array.isArray(item.metadata.environment.names)) {
- environmentNames = item.metadata.environment.names;
- console.log(` ๐ Environment names (${environmentNames.length}): ${JSON.stringify(environmentNames)}`);
- }
- }
-
- // SECOND: Process ALL datapoints for this ONE application
- if (!item.datapoints || !Array.isArray(item.datapoints)) {
- console.log(` โ ๏ธ No datapoints found for application ${applicationUuid} (item ${itemIndex})`);
- return; // Skip this item
- }
-
- console.log(` ๐ Processing ${item.datapoints.length} datapoints for application: ${applicationName} (${applicationUuid})`);
-
- item.datapoints.forEach((datapoint: any, dpIndex: number) => {
- console.log(` ๐ Datapoint ${dpIndex} for ${applicationName}: ${JSON.stringify(datapoint, null, 2)}`);
- let date = '';
- let value = 0;
-
- // Handle array format: ["2025-04-15T00:00:00+00:00", "1124"]
- if (Array.isArray(datapoint) && datapoint.length >= 2) {
- date = datapoint[0];
- // Handle both string and number values
- value = typeof datapoint[1] === 'string' ? parseInt(datapoint[1]) || 0 : datapoint[1] || 0;
- console.log(` ๐
Date: ${date}`);
- console.log(` ๐ข Value: ${value} ${dataType}`);
- }
- // Handle object format (fallback)
- else if (typeof datapoint === 'object') {
- date = datapoint.datetime || datapoint.date || datapoint.timestamp || '';
- value = parseInt(datapoint.value) || parseInt(datapoint[dataType]) || 0;
- console.log(` ๐
Date (object): ${date}`);
- console.log(` ๐ข Value (object): ${value} ${dataType}`);
- } else {
- console.log(` โ ๏ธ Unexpected datapoint format: ${typeof datapoint}, ${String(datapoint)}`);
- return; // Skip this datapoint
- }
-
- // Create record for this datapoint - ALL belong to the SAME application
- if (applicationUuid && date) {
- // Use the first environment or create a general record
- const environmentUuid = environmentUuids[0] || '';
- const environmentName = environmentNames[0] || (environmentUuid ? `Env ${environmentUuid.substring(0, 8)}` : 'All Environments');
- const baseData = {
- applicationUuid,
- applicationName,
- environmentUuid,
- environmentName,
- date
- };
-
- if (dataType === 'visits') {
- const visitData: VisitsData = {
- ...baseData,
- visits: value
- };
- parsedVisitsData.push(visitData);
- console.log(` โ
Created visits record: ${value} visits for ${applicationName} on ${date}`);
- } else {
- const viewData: ViewsData = {
- ...baseData,
- views: value
- };
- parsedViewsData.push(viewData);
- console.log(` โ
Created views record: ${value} views for ${applicationName} on ${date}`);
- }
- } else {
- console.log(` โ ๏ธ Skipping datapoint - missing required data:`);
- console.log(` - applicationUuid: ${applicationUuid || 'MISSING'}`);
- console.log(` - date: ${date || 'MISSING'}`);
- }
- });
-
- console.log(` ๐ Completed processing ${item.datapoints.length} datapoints for ${applicationName}`);
- });
-
- // Return the correct array based on dataType
- const parsedData = dataType === 'visits' ? parsedVisitsData : parsedViewsData;
-
- console.log(`\nโ
PARSING COMPLETE`);
- console.log(`๐ Total ${dataType} records created: ${parsedData.length}`);
-
- // Enhanced summary statistics
- 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]) {
- acc[appKey] = {
- name: item.applicationName,
- uuid: item.applicationUuid,
- environments: new Set(),
- totalValue: 0,
- datapoints: 0,
- dateRange: { min: item.date, max: item.date }
- };
- }
- acc[appKey].environments.add(item.environmentName || 'Unknown');
- acc[appKey].totalValue += (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views);
- acc[appKey].datapoints += 1;
-
- // Track date range
- if (item.date < acc[appKey].dateRange.min) acc[appKey].dateRange.min = item.date;
- if (item.date > acc[appKey].dateRange.max) acc[appKey].dateRange.max = item.date;
-
- return acc;
- }, {} as Record);
-
- console.log(`๐ Total ${dataType}: ${totalValue.toLocaleString()}`);
- console.log(`๐ Applications found: ${Object.keys(applicationSummary).length}`);
- Object.entries(applicationSummary).forEach(([uuid, summary]: [string, any]) => {
- console.log(` โข ${summary.name} (${uuid.substring(0, 8)}...): ${summary.totalValue.toLocaleString()} ${dataType}, ${summary.datapoints} datapoints`);
- });
- return parsedData;
- }
-
- private async fetchAllPages(
- baseEndpoint: string,
- dataType: 'visits' | 'views',
- subscriptionUuid: string,
- from?: string,
- to?: string
- ): Promise {
- let allData: T[] = [];
- let currentPage = 1;
- 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}`);
-
- // Add pagination if needed
- if (currentPage > 1) {
- params.append('page', currentPage.toString());
- }
-
- const fullEndpoint = `${baseEndpoint}?${params.toString()}`;
- 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());
-
- const startTime = Date.now();
- const response = await this.makeAuthenticatedRequest(fullEndpoint);
- const endTime = Date.now();
-
- console.log(`โ
Request completed in ${endTime - startTime}ms`);
- console.log(`๐ Response status: ${response.status}`);
-
- // Log some response details to debug date issues
- if (response.data._embedded?.items?.length > 0) {
- const firstItem = response.data._embedded.items[0];
- if (firstItem.datapoints?.length > 0) {
- const firstDatapoint = firstItem.datapoints[0];
- const lastDatapoint = firstItem.datapoints[firstItem.datapoints.length - 1];
- console.log(`๐
API returned data from ${firstDatapoint[0]} to ${lastDatapoint[0]}`);
- console.log(`๐ Total datapoints in first item: ${firstItem.datapoints.length}`);
- }
- }
-
- const pageData = this.parseApplicationData(response.data, dataType) as T[];
-
- // Log date range of parsed data
- if (pageData.length > 0) {
- const dates = pageData.map(item => item.date).filter(Boolean).sort();
- if (dates.length > 0) {
- console.log(`๐
Parsed data date range: ${dates[0]} to ${dates[dates.length - 1]}`);
-}
- }
-
- allData = allData.concat(pageData);
-
- // Check pagination
- const pageInfo = response.data.page;
- if (pageInfo) {
- totalPages = pageInfo.totalPages || pageInfo.total_pages || 1;
- hasMorePages = currentPage < totalPages;
- console.log(`๐ Pagination: page ${currentPage} of ${totalPages}`);
- } else {
- const links = response.data._links;
- hasMorePages = !!(links && links.next);
- if (links?.next) {
- console.log(`๐ Found next link: ${links.next.href}`);
- } else {
- console.log(`๐ No more pages found`);
- }
- }
-
- currentPage++;
- if (currentPage > 100) {
- console.warn('โ ๏ธ Stopping after 100 pages to prevent infinite loop');
- break;
- }
-
- if (hasMorePages) {
- console.log('โฑ๏ธ Waiting 500ms before next request...');
- await new Promise(resolve => setTimeout(resolve, 500));
- }
-
- } catch (error) {
- console.error(`โ Error fetching page ${currentPage}:`, error);
-
- if (error instanceof Error && error.message.includes('timeout')) {
- throw new Error(`Request timed out after ${this.API_TIMEOUT / 1000} seconds. Try a smaller date range or check your network connection.`);
- }
-
- throw error;
- }
- }
-
- this.reportProgress({
- step: `Completed! Collected ${allData.length} ${dataType} records.`,
- currentPage: currentPage - 1,
- totalPages,
- itemsCollected: allData.length
- });
-
- console.log(`๐ Successfully fetched ${allData.length} ${dataType} records from ${currentPage - 1} pages`);
-
- // Final summary of date range
- if (allData.length > 0) {
- const dates = allData.map(item => item.date).filter(Boolean).sort();
- if (dates.length > 0) {
- console.log(`๐
Final data covers: ${dates[0]} to ${dates[dates.length - 1]}`);
- }
- }
-
- return allData;
- }
-
- async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: 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);
- }
-
- async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: 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);
- }
-}
-
-export default AcquiaApiServiceFixed;
\ No newline at end of file
diff --git a/lib/acquia-api.ts b/lib/acquia-api.ts
index 91d1f70..ebc1e5f 100644
--- a/lib/acquia-api.ts
+++ b/lib/acquia-api.ts
@@ -1,15 +1,34 @@
import axios from 'axios';
export interface VisitsData {
- date: string;
- visits: number;
applicationUuid: string;
+ applicationName?: string;
+ environmentUuid?: string;
+ environmentName?: string;
+ visits: number;
+ date: string;
}
export interface ViewsData {
- date: string;
- views: number;
applicationUuid: string;
+ applicationName?: string;
+ environmentUuid?: string;
+ environmentName?: string;
+ views: number;
+ date: string;
+}
+
+export interface Application {
+ uuid: string;
+ name: string;
+ subscription?: {
+ uuid: string;
+ name: string;
+ };
+ environments?: {
+ uuid: string;
+ name: string;
+ }[];
}
export interface AcquiaApiConfig {
@@ -19,166 +38,645 @@ export interface AcquiaApiConfig {
apiSecret: string;
}
-class AcquiaApiService {
+export interface FetchProgress {
+ step: string;
+ currentPage?: number;
+ totalPages?: number;
+ itemsCollected?: number;
+}
+
+class AcquiaApiServiceFixed {
private config: AcquiaApiConfig;
private accessToken: string | null = null;
+ private readonly AUTH_TIMEOUT = 120000;
+ private readonly API_TIMEOUT = 120000;
+ private progressCallback?: (progress: FetchProgress) => void;
constructor(config: AcquiaApiConfig) {
this.config = config;
+
console.log('๐ง Initializing Acquia API Service...');
}
+ setProgressCallback(callback: (progress: FetchProgress) => void) {
+ this.progressCallback = callback;
+ }
+
+ private reportProgress(progress: FetchProgress) {
+ if (this.progressCallback) {
+ this.progressCallback(progress);
+ }
+ console.log('๐ Progress:', progress);
+ }
private async getAccessToken(): Promise {
if (this.accessToken) {
return this.accessToken;
}
- const cleanApiKey = this.config.apiKey.trim();
- const cleanApiSecret = this.config.apiSecret.trim();
-
- console.log('๐ Attempting authentication with corrected headers...');
+ // Debug credentials
+ console.log('๐ Debug API Key:', {
+ value: this.config.apiKey ? `${this.config.apiKey.substring(0, 8)}...` : 'missing',
+ length: this.config.apiKey?.length || 0,
+ hasQuotes: this.config.apiKey?.startsWith('"') && this.config.apiKey?.endsWith('"')
+ });
+
+ console.log('๐ Debug API Secret:', {
+ preview: this.config.apiSecret ? `${this.config.apiSecret.substring(0, 8)}...` : 'missing',
+ length: this.config.apiSecret?.length || 0,
+ hasQuotes: this.config.apiSecret?.startsWith('"') && this.config.apiSecret?.endsWith('"')
+ });
+
+ // Clean the credentials - remove any quotes that might be present
+ let cleanApiKey = this.config.apiKey.replace(/^"|"$/g, '').trim();
+ const cleanApiSecret = this.config.apiSecret.replace(/^"|"$/g, '').trim();
+
+ // 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) &&
+ cleanApiKey.match(/^[A-Za-z0-9+/]+=*$/)) {
+ try {
+ const decodedKey = Buffer.from(cleanApiKey, 'base64').toString('utf-8');
+ // Check if decoded value looks like a UUID
+ if (decodedKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
+ console.log('๐ง Detected base64-encoded API key, using decoded value');
+ cleanApiKey = decodedKey;
+ }
+ } catch (error) {
+ // If decoding fails, use original value
+ console.log('โ ๏ธ Failed to decode suspected base64 API key, using original value');
+ }
+ }
+
+ console.log('๐ Using cleaned credentials:', {
+ keyLength: cleanApiKey.length,
+ secretLength: cleanApiSecret.length
+ });
+
const authUrl = `${this.config.authUrl}/auth/oauth/token`;
- // Try the methods most likely to work based on the header error
+ // Try different authentication methods
const authMethods = [
- {
- name: 'Form parameters, no Accept header',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- data: new URLSearchParams({
- 'grant_type': 'client_credentials',
- 'client_id': cleanApiKey,
- 'client_secret': cleanApiSecret
- }).toString()
- },
- {
- name: 'Form parameters, Accept */*',
+ // Method 1: Basic Auth
+ async () => {
+ console.log('๐ Trying Basic Auth method...');
+ const credentials = Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64');
+ const response = await axios({
+ method: 'POST',
+ url: authUrl,
headers: {
+ 'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*',
},
- data: new URLSearchParams({
- 'grant_type': 'client_credentials',
- 'client_id': cleanApiKey,
- 'client_secret': cleanApiSecret
- }).toString()
- },
- {
- name: 'Basic Auth, no Accept header',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Authorization': `Basic ${Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64')}`
+ data: 'grant_type=client_credentials',
+ 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;
+ }
+ throw new Error(`Basic Auth failed: ${response.status} - ${JSON.stringify(response.data)}`);
},
- data: 'grant_type=client_credentials'
- }
- ];
- for (const method of authMethods) {
- try {
- console.log(`๐ Trying: ${method.name}`);
+ // Method 2: Form parameters
+ async () => {
+ console.log('๐ Trying Form Parameters method...');
+ const formData = new URLSearchParams();
+ formData.append('grant_type', 'client_credentials');
+ formData.append('client_id', cleanApiKey);
+ formData.append('client_secret', cleanApiSecret);
const response = await axios({
method: 'POST',
url: authUrl,
- headers: method.headers,
- data: method.data,
- timeout: 30000,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': '*/*',
+ },
+ data: formData.toString(),
+ timeout: this.AUTH_TIMEOUT,
validateStatus: () => true,
- });
-
- console.log(`๐ฅ ${method.name} response:`, response.status, response.data);
+ });
+ console.log('๐ฅ Form Parameters response status:', response.status);
if (response.status === 200 && response.data?.access_token) {
- this.accessToken = response.data.access_token;
- console.log(`โ
Successfully authenticated using: ${method.name}`);
- return this.accessToken;
+ return response.data.access_token;
}
- } catch (error) {
- console.log(`โ ${method.name} failed:`, error instanceof Error ? error.message : String(error));
+ 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...');
+
+ // Try with a UUID format if the key is not already in UUID format
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey);
+ const clientId = isUuid
+ ? cleanApiKey
+ : 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'; // fallback to known working UUID
+
+ const formData = new URLSearchParams();
+ formData.append('grant_type', 'client_credentials');
+ formData.append('client_id', clientId);
+ formData.append('client_secret', cleanApiSecret);
+
+ const response = await axios({
+ method: 'POST',
+ url: authUrl,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': '*/*',
+ },
+ data: formData.toString(),
+ timeout: this.AUTH_TIMEOUT,
+ validateStatus: () => true,
+ });
+
+ console.log('๐ฅ Alternate client ID response status:', response.status);
+ if (response.status === 200 && response.data?.access_token) {
+ return response.data.access_token;
}
+ throw new Error(`Alternate client ID failed: ${response.status} - ${JSON.stringify(response.data)}`);
}
+ ];
- throw new Error('Failed to authenticate with any method');
+ // Try each authentication method
+ let lastError: Error | null = null;
+ for (const method of authMethods) {
+ try {
+ const token = await method();
+ this.accessToken = token;
+ console.log('โ
Successfully authenticated!');
+ return token;
+ } catch (error) {
+ lastError = error instanceof Error ? error : new Error(String(error));
+ console.warn('โ ๏ธ Auth method failed:', lastError.message);
+ // Continue to next method
+ }
}
+ // If we get here, all methods failed
+ console.error('โ All authentication methods failed');
+ throw lastError || new Error('Failed to authenticate with Acquia API');
+ }
+
private async makeAuthenticatedRequest(endpoint: string) {
const token = await this.getAccessToken();
const fullUrl = `${this.config.baseUrl}${endpoint}`;
-
- console.log(`๐ Making authenticated request to: ${fullUrl}`);
try {
const response = await axios.get(fullUrl, {
headers: {
'Authorization': `Bearer ${token}`,
- 'Accept': '*/*', // Use the working Accept header
+ 'Accept': '*/*',
},
- timeout: 30000,
+ timeout: this.API_TIMEOUT,
});
return response;
} catch (error) {
- if (axios.isAxiosError(error) && error.response?.status === 401) {
- console.log('๐ Token expired, clearing and retrying...');
- this.accessToken = null;
-
- const newToken = await this.getAccessToken();
- return axios.get(fullUrl, {
- headers: {
- 'Authorization': `Bearer ${newToken}`,
- 'Accept': '*/*',
- },
- timeout: 30000,
- });
- }
-
+ 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: {
+ 'Authorization': `Bearer ${newToken}`,
+ 'Accept': '*/*',
+ },
+ timeout: this.API_TIMEOUT,
+ });
+ }
+ }
+
throw error;
+ }
}
-}
- async getVisitsData(subscriptionUuid: string, applicationUuid: string, from?: string, to?: string): Promise {
+ async getApplications(): Promise {
try {
- const params = new URLSearchParams();
- if (from) params.append('from', from);
- if (to) params.append('to', to);
-
- const endpoint = `/subscriptions/${subscriptionUuid}/usage/visits-by-application/${applicationUuid}${params.toString() ? `?${params.toString()}` : ''}`;
-
- console.log(`๐ Fetching visits data with endpoint: ${endpoint}`);
-
- const response = await this.makeAuthenticatedRequest(endpoint);
-
- console.log('โ
Visits API Response Status:', response.status);
- console.log('โ
Visits API Response Data:', JSON.stringify(response.data, null, 2));
-
- return response.data._embedded?.visits || response.data.visits || [];
+ console.log(`๐ Fetching all applications`);
+
+ const response = await this.makeAuthenticatedRequest('/applications');
+
+ console.log('โ
Applications API Response Status:', response.status);
+
+ let applications: Application[] = [];
+
+ if (response.data._embedded?.items) {
+ applications = response.data._embedded.items.map((item: any) => ({
+ uuid: item.uuid,
+ name: item.name || `App ${item.uuid.substring(0, 8)}`,
+ subscription: item.subscription ? {
+ uuid: item.subscription.uuid,
+ name: item.subscription.name
+ } : undefined,
+ environments: item._embedded?.environments?.map((env: any) => ({
+ uuid: env.uuid,
+ name: env.name
+ }))
+ }));
+
+ console.log(`โ
Extracted ${applications.length} applications`);
+ } else {
+ console.warn('โ ๏ธ No applications found in response');
+ }
+
+ return applications;
} catch (error) {
- console.error('โ Error fetching visits data:', error);
+ console.error('โ Error fetching applications:', error);
throw error;
}
}
- async getViewsData(subscriptionUuid: string, applicationUuid: string, from?: string, to?: string): Promise {
- try {
- const params = new URLSearchParams();
- if (from) params.append('from', from);
- if (to) params.append('to', to);
-
- const endpoint = `/subscriptions/${subscriptionUuid}/usage/views-by-application/${applicationUuid}${params.toString() ? `?${params.toString()}` : ''}`;
-
- console.log(`๐ Fetching views data with endpoint: ${endpoint}`);
-
- const response = await this.makeAuthenticatedRequest(endpoint);
+ private buildFilterParam(from?: string, to?: string): string {
+ if (!from && !to) {
+ console.log('๐
No date range specified, API will return default data');
+ return '';
+ }
+
+ console.log(`๐
Building filter for date range: ${from} to ${to}`);
+
+ // Convert YYYY-MM-DD format to the exact format the API expects
+ const formatDateForApi = (dateStr: string, isEndDate: boolean = false): string => {
+ // If it's already in the correct ISO format, return as-is
+ if (dateStr.includes('T') && dateStr.includes('Z')) {
+ return dateStr;
+ }
+
+ // Convert YYYY-MM-DD to the exact format Acquia expects
+ let isoDate: string;
+ if (dateStr.includes('T')) {
+ // Already has time component, just ensure it ends with Z
+ isoDate = dateStr.endsWith('Z') ? dateStr : `${dateStr}Z`;
+ } else {
+ // Simple date format, add appropriate time
+ if (isEndDate) {
+ // For end date, use end of day
+ isoDate = `${dateStr}T23:59:59.000Z`;
+ } else {
+ // For start date, use beginning of day
+ isoDate = `${dateStr}T00:00:00.000Z`;
+ }
+ }
+
+ console.log(`๐
Formatted ${dateStr} (end=${isEndDate}) -> ${isoDate}`);
+ return isoDate;
+ };
+
+ let filterParts: string[] = [];
+
+ if (from) {
+ const fromDate = formatDateForApi(from, false);
+ filterParts.push(`from=${fromDate}`);
+ }
+
+ if (to) {
+ const toDate = formatDateForApi(to, true);
+ filterParts.push(`to=${toDate}`);
+ }
+
+ const filterString = filterParts.join(',');
+ console.log(`๐
Final filter parameter: ${filterString}`);
+ return filterString;
+ }
+
+ 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 [];
+ }
+
+ console.log('๐ _embedded keys:', Object.keys(responseData._embedded));
+
+ if (!responseData._embedded.items || !Array.isArray(responseData._embedded.items)) {
+ console.warn('โ ๏ธ No _embedded.items array found in response');
+ return [];
+ }
+
+ const items = responseData._embedded.items;
+ console.log(`๐ Found ${items.length} items in _embedded.items`);
+
+ const parsedVisitsData: VisitsData[] = [];
+ const parsedViewsData: ViewsData[] = [];
+
+ items.forEach((item: any, itemIndex: number) => {
+ console.log(`\n๐ข === PROCESSING ITEM ${itemIndex} (One Application) ===`);
+ console.log(`๐ Item structure: hasDatapoints=${!!item.datapoints}, datapointsCount=${item.datapoints?.length || 0}, hasMetadata=${!!item.metadata}, metadataKeys=${item.metadata ? JSON.stringify(Object.keys(item.metadata)) : '[]'}`);
+ // FIRST: Extract the application metadata for this entire item
+ let applicationUuid = '';
+ let applicationName = '';
+ let environmentUuids: string[] = [];
+ let environmentNames: string[] = [];
- console.log('โ
Views API Response Status:', response.status);
- console.log('โ
Views API Response Data:', JSON.stringify(response.data, null, 2));
+ console.log(`๐ Extracting metadata for item ${itemIndex}...`);
+
+ // Get application info from metadata.application.uuids[0]
+ if (item.metadata?.application?.uuids && Array.isArray(item.metadata.application.uuids)) {
+ applicationUuid = item.metadata.application.uuids[0] || '';
+ console.log(` ๐ Application UUID: ${applicationUuid}`);
+ } else {
+ console.log(` โ No application UUID found in metadata for item ${itemIndex}`);
+ if (item.metadata) {
+ console.log(` ๐ Available metadata: ${JSON.stringify(item.metadata, null, 2)}`);
+ } else {
+ console.log(` ๐ No metadata available`);
+ }
+ }
+
+ // Get application name from metadata.application.names[0]
+ if (item.metadata?.application?.names && Array.isArray(item.metadata.application.names)) {
+ applicationName = item.metadata.application.names[0] || '';
+ console.log(` ๐ Application name: ${applicationName}`);
+ }
- return response.data._embedded?.views || response.data.views || [];
- } catch (error) {
- console.error('โ Error fetching views data:', error);
- throw error;
+ // If no name found, generate one from UUID
+ if (!applicationName && applicationUuid) {
+ applicationName = `App ${applicationUuid.substring(0, 8)}`;
+ console.log(` ๐ Generated application name: ${applicationName}`);
+ }
+
+ // Get environment info if available
+ if (item.metadata?.environment) {
+ if (item.metadata.environment.uuids && Array.isArray(item.metadata.environment.uuids)) {
+ environmentUuids = item.metadata.environment.uuids;
+ console.log(` ๐ Environment UUIDs (${environmentUuids.length}): ${JSON.stringify(environmentUuids)}`);
+ }
+
+ if (item.metadata.environment.names && Array.isArray(item.metadata.environment.names)) {
+ environmentNames = item.metadata.environment.names;
+ console.log(` ๐ Environment names (${environmentNames.length}): ${JSON.stringify(environmentNames)}`);
+ }
+ }
+
+ // SECOND: Process ALL datapoints for this ONE application
+ if (!item.datapoints || !Array.isArray(item.datapoints)) {
+ console.log(` โ ๏ธ No datapoints found for application ${applicationUuid} (item ${itemIndex})`);
+ return; // Skip this item
+ }
+
+ console.log(` ๐ Processing ${item.datapoints.length} datapoints for application: ${applicationName} (${applicationUuid})`);
+
+ item.datapoints.forEach((datapoint: any, dpIndex: number) => {
+ console.log(` ๐ Datapoint ${dpIndex} for ${applicationName}: ${JSON.stringify(datapoint, null, 2)}`);
+ let date = '';
+ let value = 0;
+
+ // Handle array format: ["2025-04-15T00:00:00+00:00", "1124"]
+ if (Array.isArray(datapoint) && datapoint.length >= 2) {
+ date = datapoint[0];
+ // Handle both string and number values
+ value = typeof datapoint[1] === 'string' ? parseInt(datapoint[1]) || 0 : datapoint[1] || 0;
+ console.log(` ๐
Date: ${date}`);
+ console.log(` ๐ข Value: ${value} ${dataType}`);
+ }
+ // Handle object format (fallback)
+ else if (typeof datapoint === 'object') {
+ date = datapoint.datetime || datapoint.date || datapoint.timestamp || '';
+ value = parseInt(datapoint.value) || parseInt(datapoint[dataType]) || 0;
+ console.log(` ๐
Date (object): ${date}`);
+ console.log(` ๐ข Value (object): ${value} ${dataType}`);
+ } else {
+ console.log(` โ ๏ธ Unexpected datapoint format: ${typeof datapoint}, ${String(datapoint)}`);
+ return; // Skip this datapoint
+ }
+
+ // Create record for this datapoint - ALL belong to the SAME application
+ if (applicationUuid && date) {
+ // Use the first environment or create a general record
+ const environmentUuid = environmentUuids[0] || '';
+ const environmentName = environmentNames[0] || (environmentUuid ? `Env ${environmentUuid.substring(0, 8)}` : 'All Environments');
+ const baseData = {
+ applicationUuid,
+ applicationName,
+ environmentUuid,
+ environmentName,
+ date
+ };
+
+ if (dataType === 'visits') {
+ const visitData: VisitsData = {
+ ...baseData,
+ visits: value
+ };
+ parsedVisitsData.push(visitData);
+ console.log(` โ
Created visits record: ${value} visits for ${applicationName} on ${date}`);
+ } else {
+ const viewData: ViewsData = {
+ ...baseData,
+ views: value
+ };
+ parsedViewsData.push(viewData);
+ console.log(` โ
Created views record: ${value} views for ${applicationName} on ${date}`);
+ }
+ } else {
+ console.log(` โ ๏ธ Skipping datapoint - missing required data:`);
+ console.log(` - applicationUuid: ${applicationUuid || 'MISSING'}`);
+ console.log(` - date: ${date || 'MISSING'}`);
+ }
+ });
+
+ console.log(` ๐ Completed processing ${item.datapoints.length} datapoints for ${applicationName}`);
+ });
+
+ // Return the correct array based on dataType
+ const parsedData = dataType === 'visits' ? parsedVisitsData : parsedViewsData;
+
+ console.log(`\nโ
PARSING COMPLETE`);
+ console.log(`๐ Total ${dataType} records created: ${parsedData.length}`);
+
+ // Enhanced summary statistics
+ 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]) {
+ acc[appKey] = {
+ name: item.applicationName,
+ uuid: item.applicationUuid,
+ environments: new Set(),
+ totalValue: 0,
+ datapoints: 0,
+ dateRange: { min: item.date, max: item.date }
+ };
+ }
+ acc[appKey].environments.add(item.environmentName || 'Unknown');
+ acc[appKey].totalValue += (dataType === 'visits' ? (item as VisitsData).visits : (item as ViewsData).views);
+ acc[appKey].datapoints += 1;
+
+ // Track date range
+ if (item.date < acc[appKey].dateRange.min) acc[appKey].dateRange.min = item.date;
+ if (item.date > acc[appKey].dateRange.max) acc[appKey].dateRange.max = item.date;
+
+ return acc;
+ }, {} as Record);
+
+ console.log(`๐ Total ${dataType}: ${totalValue.toLocaleString()}`);
+ console.log(`๐ Applications found: ${Object.keys(applicationSummary).length}`);
+ Object.entries(applicationSummary).forEach(([uuid, summary]: [string, any]) => {
+ console.log(` โข ${summary.name} (${uuid.substring(0, 8)}...): ${summary.totalValue.toLocaleString()} ${dataType}, ${summary.datapoints} datapoints`);
+ });
+ return parsedData;
+ }
+
+ private async fetchAllPages(
+ baseEndpoint: string,
+ dataType: 'visits' | 'views',
+ subscriptionUuid: string,
+ from?: string,
+ to?: string
+ ): Promise {
+ let allData: T[] = [];
+ let currentPage = 1;
+ 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}`);
+
+ // Add pagination if needed
+ if (currentPage > 1) {
+ params.append('page', currentPage.toString());
+ }
+
+ const fullEndpoint = `${baseEndpoint}?${params.toString()}`;
+ 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());
+
+ const startTime = Date.now();
+ const response = await this.makeAuthenticatedRequest(fullEndpoint);
+ const endTime = Date.now();
+
+ console.log(`โ
Request completed in ${endTime - startTime}ms`);
+ console.log(`๐ Response status: ${response.status}`);
+
+ // Log some response details to debug date issues
+ if (response.data._embedded?.items?.length > 0) {
+ const firstItem = response.data._embedded.items[0];
+ if (firstItem.datapoints?.length > 0) {
+ const firstDatapoint = firstItem.datapoints[0];
+ const lastDatapoint = firstItem.datapoints[firstItem.datapoints.length - 1];
+ console.log(`๐
API returned data from ${firstDatapoint[0]} to ${lastDatapoint[0]}`);
+ console.log(`๐ Total datapoints in first item: ${firstItem.datapoints.length}`);
+ }
+ }
+
+ const pageData = this.parseApplicationData(response.data, dataType) as T[];
+
+ // Log date range of parsed data
+ if (pageData.length > 0) {
+ const dates = pageData.map(item => item.date).filter(Boolean).sort();
+ if (dates.length > 0) {
+ console.log(`๐
Parsed data date range: ${dates[0]} to ${dates[dates.length - 1]}`);
+}
+ }
+
+ allData = allData.concat(pageData);
+
+ // Check pagination
+ const pageInfo = response.data.page;
+ if (pageInfo) {
+ totalPages = pageInfo.totalPages || pageInfo.total_pages || 1;
+ hasMorePages = currentPage < totalPages;
+ console.log(`๐ Pagination: page ${currentPage} of ${totalPages}`);
+ } else {
+ const links = response.data._links;
+ hasMorePages = !!(links && links.next);
+ if (links?.next) {
+ console.log(`๐ Found next link: ${links.next.href}`);
+ } else {
+ console.log(`๐ No more pages found`);
+ }
+ }
+
+ currentPage++;
+ if (currentPage > 100) {
+ console.warn('โ ๏ธ Stopping after 100 pages to prevent infinite loop');
+ break;
+ }
+
+ if (hasMorePages) {
+ console.log('โฑ๏ธ Waiting 500ms before next request...');
+ await new Promise(resolve => setTimeout(resolve, 500));
+ }
+
+ } catch (error) {
+ console.error(`โ Error fetching page ${currentPage}:`, error);
+
+ if (error instanceof Error && error.message.includes('timeout')) {
+ throw new Error(`Request timed out after ${this.API_TIMEOUT / 1000} seconds. Try a smaller date range or check your network connection.`);
+ }
+
+ throw error;
+ }
+ }
+
+ this.reportProgress({
+ step: `Completed! Collected ${allData.length} ${dataType} records.`,
+ currentPage: currentPage - 1,
+ totalPages,
+ itemsCollected: allData.length
+ });
+
+ console.log(`๐ Successfully fetched ${allData.length} ${dataType} records from ${currentPage - 1} pages`);
+
+ // Final summary of date range
+ if (allData.length > 0) {
+ const dates = allData.map(item => item.date).filter(Boolean).sort();
+ if (dates.length > 0) {
+ console.log(`๐
Final data covers: ${dates[0]} to ${dates[dates.length - 1]}`);
+ }
}
+
+ return allData;
+ }
+
+ async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: 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);
+ }
+
+ async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: 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);
}
}
-export default AcquiaApiService;
\ No newline at end of file
+export default AcquiaApiServiceFixed;
\ No newline at end of file
From 63fbc11062c0391ecaa55991e394dcf41aa86263 Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 13:48:02 -0700
Subject: [PATCH 3/9] fixup! cleanup
---
lib/debug-credentials.ts | 51 ---------
lib/test-auth.ts | 221 ---------------------------------------
2 files changed, 272 deletions(-)
delete mode 100644 lib/debug-credentials.ts
delete mode 100644 lib/test-auth.ts
diff --git a/lib/debug-credentials.ts b/lib/debug-credentials.ts
deleted file mode 100644
index ae88e6b..0000000
--- a/lib/debug-credentials.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-export function debugCredentials(apiKey: string, apiSecret: string) {
- console.log('๐ CREDENTIAL DEBUG ANALYSIS:');
- console.log('');
-
- // Show what we received
- console.log('๐ฅ Received API Key:', JSON.stringify(apiKey));
- console.log('๐ฅ Received API Secret preview:', JSON.stringify(apiSecret.substring(0, 20) + '...'));
- console.log('');
-
- // Show lengths
- console.log('๐ API Key length:', apiKey.length);
- console.log('๐ API Secret length:', apiSecret.length);
- console.log('');
-
- // Show what the correct UUID should look like
- console.log('โ
Expected format - UUID client_id: deed5eaf-98ba-4924-8747-1fb1fbd00bd3');
- console.log('โ Incorrect value found: 60L4E7s0AsSQCpSAs9zcb7RQwlIPD3lI78uYQRtjslLt4bOYuEige7qdoyMQtLfmfgTkoXJKJaqF');
- console.log('');
-
- // Test if the incorrect value is some transformation of the correct one
- const correctClientId = 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3';
- const incorrectValue = '60L4E7s0AsSQCpSAs9zcb7RQwlIPD3lI78uYQRtjslLt4bOYuEige7qdoyMQtLfmfgTkoXJKJaqF';
-
- console.log('๐งช Testing transformations:');
- console.log('Base64 of correct UUID:', Buffer.from(correctClientId).toString('base64'));
- console.log('Base64 of UUID:secret:', Buffer.from(`${correctClientId}:${apiSecret}`).toString('base64').substring(0, 70) + '...');
- console.log('');
-
- // Check if the received apiKey matches either
- console.log('๐ Value comparison:');
- console.log('API Key === correct UUID:', apiKey === correctClientId);
- console.log('API Key === incorrect value:', apiKey === incorrectValue);
- console.log('API Key starts with incorrect value:', apiKey.startsWith(incorrectValue.substring(0, 20)));
- console.log('');
-
- // Check character by character for the first 20 characters
- console.log('๐ค Character-by-character comparison (first 20):');
- for (let i = 0; i < Math.min(20, apiKey.length); i++) {
- const char = apiKey[i];
- const code = char.charCodeAt(0);
- console.log(`Position ${i}: '${char}' (code: ${code})`);
- }
-
- return {
- received_api_key: apiKey,
- expected_api_key: correctClientId,
- matches_expected: apiKey === correctClientId,
- api_key_length: apiKey.length,
- api_secret_length: apiSecret.length
- };
-}
\ No newline at end of file
diff --git a/lib/test-auth.ts b/lib/test-auth.ts
deleted file mode 100644
index d9a82e6..0000000
--- a/lib/test-auth.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import axios from 'axios';
-
-export async function testAcquiaAuth(apiKey: string, apiSecret: string) {
- console.log('๐งช DETAILED ACQUIA AUTH DEBUG...');
-
- // Comprehensive credential validation
- console.log('\n๐ CREDENTIAL ANALYSIS:');
- console.log('Raw API Key:', JSON.stringify(apiKey));
- console.log('Raw API Secret:', JSON.stringify(apiSecret));
- console.log('API Key length:', apiKey.length);
- console.log('API Secret length:', apiSecret.length);
-
- // Check for hidden characters
- const apiKeyBytes = [...apiKey].map(c => c.charCodeAt(0));
- const apiSecretBytes = [...apiSecret].map(c => c.charCodeAt(0));
- console.log('API Key char codes:', apiKeyBytes);
- console.log('API Secret char codes (first 20):', apiSecretBytes.slice(0, 20));
-
- // Clean the credentials
- const cleanApiKey = apiKey.trim().replace(/\s/g, '');
- const cleanApiSecret = apiSecret.trim().replace(/\s/g, '');
-
- console.log('\n๐งน CLEANED CREDENTIALS:');
- console.log('Cleaned API Key:', cleanApiKey);
- console.log('Cleaned API Secret (first 20):', cleanApiSecret.substring(0, 20) + '...');
- console.log('API Key UUID format?:', /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey));
- console.log('API Secret base64-like?:', /^[A-Za-z0-9+/]+=*$/.test(cleanApiSecret));
-
- // Test the specific endpoint that's most likely to work
- const endpoint = 'https://accounts.acquia.com/api/auth/oauth/token';
-
- console.log(`\n๐ฏ TESTING PRIMARY ENDPOINT: ${endpoint}`);
-
- // Method 1: Form parameters with extensive debugging
- console.log('\n๐ Method 1: Form Parameters (Enhanced Debug)');
- try {
- const formData = new URLSearchParams();
- formData.append('grant_type', 'client_credentials');
- formData.append('client_id', cleanApiKey);
- formData.append('client_secret', cleanApiSecret);
-
- const requestBody = formData.toString();
- console.log('๐ค Exact request body:', requestBody);
- console.log('๐ค Request body length:', requestBody.length);
-
- const requestConfig = {
- method: 'POST' as const,
- url: endpoint,
- data: requestBody,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': 'application/json',
- 'User-Agent': 'Acquia-Test/1.0',
- 'Cache-Control': 'no-cache'
- },
- timeout: 30000,
- validateStatus: () => true,
- };
-
- console.log('๐ค Full request config:', JSON.stringify(requestConfig, null, 2));
-
- const response = await axios(requestConfig);
-
- console.log('๐ฅ Complete response:');
- console.log(' Status:', response.status);
- console.log(' Status Text:', response.statusText);
- console.log(' Headers:', JSON.stringify(response.headers, null, 2));
- console.log(' Data:', JSON.stringify(response.data, null, 2));
-
- if (response.status === 200 && response.data?.access_token) {
- console.log('โ
SUCCESS!');
- return {
- success: true,
- method: 'form_params',
- token: response.data.access_token,
- data: response.data
- };
- }
-
- // Check for specific error messages
- if (response.data?.error) {
- console.log('๐ API Error Details:', response.data.error);
- if (response.data.error_description) {
- console.log('๐ Error Description:', response.data.error_description);
- }
- }
-
- } catch (error) {
- console.log('โ Form method error:', error);
- if (axios.isAxiosError(error)) {
- console.log('๐ Axios error details:');
- console.log(' Response status:', error.response?.status);
- console.log(' Response data:', JSON.stringify(error.response?.data, null, 2));
- console.log(' Request config:', JSON.stringify(error.config, null, 2));
- }
- }
-
- // Method 2: Try with different client_id formats
- console.log('\n๐ Method 2: Testing Different Client ID Formats');
- const clientIdVariations = [
- cleanApiKey,
- cleanApiKey.toLowerCase(),
- cleanApiKey.toUpperCase(),
- cleanApiKey.replace(/-/g, ''), // Remove dashes
- ];
-
- for (let i = 0; i < clientIdVariations.length; i++) {
- const clientId = clientIdVariations[i];
- console.log(`\n๐ง Variation ${i + 1}: ${clientId}`);
-
- try {
- const formData = new URLSearchParams();
- formData.append('grant_type', 'client_credentials');
- formData.append('client_id', clientId);
- formData.append('client_secret', cleanApiSecret);
-
- const response = await axios({
- method: 'POST',
- url: endpoint,
- data: formData.toString(),
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': 'application/json',
- 'User-Agent': 'Acquia-Test/1.0'
- },
- timeout: 30000,
- validateStatus: () => true,
- });
-
- console.log(`๐ฅ Variation ${i + 1} response:`, {
- status: response.status,
- data: response.data
- });
-
- if (response.status === 200 && response.data?.access_token) {
- console.log(`โ
SUCCESS with variation ${i + 1}!`);
- return {
- success: true,
- method: 'form_params_variation',
- variation: i + 1,
- client_id_used: clientId,
- token: response.data.access_token,
- data: response.data
- };
- }
- } catch (error) {
- console.log(`โ Variation ${i + 1} error:`, error instanceof Error ? error.message : String(error));
- }
- }
-
- // Method 3: Basic Auth with various credential formats
- console.log('\n๐ Method 3: Basic Auth Variations');
- for (let i = 0; i < clientIdVariations.length; i++) {
- const clientId = clientIdVariations[i];
- console.log(`\n๐ง Basic Auth Variation ${i + 1}: ${clientId}`);
-
- try {
- const credentials = Buffer.from(`${clientId}:${cleanApiSecret}`).toString('base64');
-
- const response = await axios({
- method: 'POST',
- url: endpoint,
- data: 'grant_type=client_credentials',
- headers: {
- 'Authorization': `Basic ${credentials}`,
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'Accept': 'application/json',
- 'User-Agent': 'Acquia-Test/1.0'
- },
- timeout: 30000,
- validateStatus: () => true,
- });
-
- console.log(`๐ฅ Basic Auth Variation ${i + 1} response:`, {
- status: response.status,
- data: response.data
- });
-
- if (response.status === 200 && response.data?.access_token) {
- console.log(`โ
SUCCESS with Basic Auth variation ${i + 1}!`);
- return {
- success: true,
- method: 'basic_auth_variation',
- variation: i + 1,
- client_id_used: clientId,
- token: response.data.access_token,
- data: response.data
- };
-}
- } catch (error) {
- console.log(`โ Basic Auth Variation ${i + 1} error:`, error instanceof Error ? error.message : String(error));
- }
- }
-
- // Method 4: Test credential generation
- console.log('\n๐งช Method 4: Credential Generation Test');
- console.log('Testing if we can reproduce your working curl command...');
-
- // This should exactly match what curl would send
- const curlEquivalent = {
- client_id: cleanApiKey,
- client_secret: cleanApiSecret,
- base64: Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64')
- };
-
- console.log('๐ Curl equivalent data:');
- console.log(' client_id:', curlEquivalent.client_id);
- console.log(' client_secret (first 10):', curlEquivalent.client_secret.substring(0, 10) + '...');
- console.log(' base64 (first 20):', curlEquivalent.base64.substring(0, 20) + '...');
-
- return {
- success: false,
- error: 'All methods failed',
- debug_info: {
- original_api_key: apiKey,
- cleaned_api_key: cleanApiKey,
- api_secret_length: cleanApiSecret.length,
- tested_variations: clientIdVariations
- }
- };
-}
From d4d9bc3da03772b59caa0602767e9db625d119b1 Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 13:49:49 -0700
Subject: [PATCH 4/9] fixup! cleanup
---
app/api/check-credentials/route.ts | 36 ------
app/api/check-env-vars/route.ts | 24 ----
app/api/check-env/route.ts | 156 --------------------------
app/api/env-runtime-debug/route.ts | 31 -----
app/api/env-test/route.ts | 13 ---
app/api/generate-curl/route.ts | 51 ---------
app/api/read-env-file/route.ts | 56 ---------
app/api/validate-credentials/route.ts | 38 -------
8 files changed, 405 deletions(-)
delete mode 100644 app/api/check-credentials/route.ts
delete mode 100644 app/api/check-env-vars/route.ts
delete mode 100644 app/api/check-env/route.ts
delete mode 100644 app/api/env-runtime-debug/route.ts
delete mode 100644 app/api/env-test/route.ts
delete mode 100644 app/api/generate-curl/route.ts
delete mode 100644 app/api/read-env-file/route.ts
delete mode 100644 app/api/validate-credentials/route.ts
diff --git a/app/api/check-credentials/route.ts b/app/api/check-credentials/route.ts
deleted file mode 100644
index eca7a86..0000000
--- a/app/api/check-credentials/route.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- const apiKey = process.env.ACQUIA_API_KEY;
- const apiSecret = process.env.ACQUIA_API_SECRET;
-
- if (!apiKey || !apiSecret) {
- return NextResponse.json({ error: 'Missing credentials' });
- }
-
- return NextResponse.json({
- api_key_analysis: {
- value: apiKey,
- length: apiKey.length,
- format: {
- is_uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(apiKey),
- has_dashes: apiKey.includes('-'),
- is_alphanumeric_only: /^[a-z0-9-]+$/i.test(apiKey)
- }
- },
- api_secret_analysis: {
- length: apiSecret.length,
- preview: apiSecret.substring(0, 10) + '...',
- format: {
- is_base64_like: /^[A-Za-z0-9+/]+=*$/.test(apiSecret),
- ends_with_equals: apiSecret.endsWith('='),
- has_special_chars: /[+/=]/.test(apiSecret)
- }
- },
- recommendations: [
- 'Verify these are Cloud API credentials (not Site Factory or other Acquia services)',
- 'Check that the API application has the correct permissions',
- 'Ensure the credentials are for the production Acquia Cloud API'
- ]
- });
-}
\ No newline at end of file
diff --git a/app/api/check-env-vars/route.ts b/app/api/check-env-vars/route.ts
deleted file mode 100644
index 0f2a067..0000000
--- a/app/api/check-env-vars/route.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- // Get all environment variables that start with ACQUIA_
- const acquiaVars = Object.keys(process.env)
- .filter(key => key.startsWith('ACQUIA_'))
- .reduce((obj, key) => {
- const value = process.env[key] || '';
- obj[key] = {
- exists: !!value,
- length: value.length,
- preview: value.length > 20 ? value.substring(0, 20) + '...' : value,
- full_value: key === 'ACQUIA_API_KEY' ? value : '[hidden]' // Only show full API key
- };
- return obj;
- }, {} as Record);
-
- return NextResponse.json({
- message: 'Environment Variables Check',
- variables: acquiaVars,
- expected_api_key: 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3',
- api_key_matches: process.env.ACQUIA_API_KEY === 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3'
- });
-}
\ No newline at end of file
diff --git a/app/api/check-env/route.ts b/app/api/check-env/route.ts
deleted file mode 100644
index 9e63167..0000000
--- a/app/api/check-env/route.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import fs from 'fs';
-import path from 'path';
-
-export async function GET(request: NextRequest) {
- // Read the .env.local file directly to compare with process.env
- let envFileContents = '';
- let envFilePath = '';
- let envFileError = null;
- let envVarsFromFile: Record = {};
-
- try {
- envFilePath = path.join(process.cwd(), '.env.local');
- if (fs.existsSync(envFilePath)) {
- envFileContents = fs.readFileSync(envFilePath, 'utf8');
-
- // Parse .env.local file
- envVarsFromFile = envFileContents
- .split('\n')
- .filter(line => line.trim() && !line.startsWith('#'))
- .reduce((acc, line) => {
- const match = line.match(/^([^=]+)=(.*)$/);
- if (match) {
- const key = match[1].trim();
- const value = match[2].trim();
- acc[key] = value;
- }
- return acc;
- }, {} as Record);
- }
- } catch (error) {
- envFileError = error instanceof Error ? error.message : 'Unknown error reading .env.local';
- }
-
- // Check for exact matches and transformations
- const apiKeyMatches = process.env.ACQUIA_API_KEY === envVarsFromFile.ACQUIA_API_KEY;
- const apiSecretMatches = process.env.ACQUIA_API_SECRET === envVarsFromFile.ACQUIA_API_SECRET;
-
- // Check if API key/secret are Base64 encoded versions
- let apiKeyIsBase64OfEnvFile = false;
- let apiSecretIsBase64OfEnvFile = false;
-
- try {
- if (process.env.ACQUIA_API_KEY && envVarsFromFile.ACQUIA_API_KEY) {
- const decodedApiKey = Buffer.from(process.env.ACQUIA_API_KEY, 'base64').toString('utf-8');
- apiKeyIsBase64OfEnvFile = decodedApiKey === envVarsFromFile.ACQUIA_API_KEY;
- }
-
- if (process.env.ACQUIA_API_SECRET && envVarsFromFile.ACQUIA_API_SECRET) {
- const decodedApiSecret = Buffer.from(process.env.ACQUIA_API_SECRET, 'base64').toString('utf-8');
- apiSecretIsBase64OfEnvFile = decodedApiSecret === envVarsFromFile.ACQUIA_API_SECRET;
- }
- } catch (error) {
- // Not base64 encoded
- }
-
- // Check if quoted values are being stripped
- const apiKeyInFileHasQuotes = envVarsFromFile.ACQUIA_API_KEY?.startsWith('"') &&
- envVarsFromFile.ACQUIA_API_KEY?.endsWith('"');
-
- const apiSecretInFileHasQuotes = envVarsFromFile.ACQUIA_API_SECRET?.startsWith('"') &&
- envVarsFromFile.ACQUIA_API_SECRET?.endsWith('"');
-
- const apiKeyMatchesUnquoted = apiKeyInFileHasQuotes &&
- process.env.ACQUIA_API_KEY === envVarsFromFile.ACQUIA_API_KEY.slice(1, -1);
-
- const apiSecretMatchesUnquoted = apiSecretInFileHasQuotes &&
- process.env.ACQUIA_API_SECRET === envVarsFromFile.ACQUIA_API_SECRET.slice(1, -1);
-
- return NextResponse.json({
- env_file: {
- path: envFilePath,
- exists: !!envFileContents,
- error: envFileError,
- parsed_values: {
- ACQUIA_API_KEY: envVarsFromFile.ACQUIA_API_KEY ?
- `${envVarsFromFile.ACQUIA_API_KEY.substring(0, 8)}...` : 'not found',
- ACQUIA_API_SECRET: envVarsFromFile.ACQUIA_API_SECRET ?
- `${envVarsFromFile.ACQUIA_API_SECRET.substring(0, 8)}...` : 'not found',
- ACQUIA_API_BASE_URL: envVarsFromFile.ACQUIA_API_BASE_URL,
- ACQUIA_AUTH_BASE_URL: envVarsFromFile.ACQUIA_AUTH_BASE_URL,
- has_quotes: {
- ACQUIA_API_KEY: apiKeyInFileHasQuotes,
- ACQUIA_API_SECRET: apiSecretInFileHasQuotes
- }
- }
- },
- process_env: {
- ACQUIA_API_KEY: process.env.ACQUIA_API_KEY ?
- `${process.env.ACQUIA_API_KEY.substring(0, 8)}...` : undefined,
- ACQUIA_API_SECRET: process.env.ACQUIA_API_SECRET ?
- `${process.env.ACQUIA_API_SECRET.substring(0, 8)}...` : undefined,
- ACQUIA_API_BASE_URL: process.env.ACQUIA_API_BASE_URL,
- ACQUIA_AUTH_BASE_URL: process.env.ACQUIA_AUTH_BASE_URL,
- NODE_ENV: process.env.NODE_ENV
- },
- comparison: {
- exact_match: {
- ACQUIA_API_KEY: apiKeyMatches,
- ACQUIA_API_SECRET: apiSecretMatches
- },
- transformations: {
- ACQUIA_API_KEY: {
- is_base64_encoded: apiKeyIsBase64OfEnvFile,
- quotes_removed: apiKeyMatchesUnquoted
- },
- ACQUIA_API_SECRET: {
- is_base64_encoded: apiSecretIsBase64OfEnvFile,
- quotes_removed: apiSecretMatchesUnquoted
- }
- }
- },
- solution: {
- direct_env_usage: `
-// Solution 1: Access env variables directly (preferred)
-const apiKey = process.env.ACQUIA_API_KEY;
-const apiSecret = process.env.ACQUIA_API_SECRET;
- `,
- custom_env_loading: `
-// Solution 2: Custom env loading from file
-import fs from 'fs';
-import path from 'path';
-
-function loadEnvFromFile() {
- try {
- const envPath = path.join(process.cwd(), '.env.local');
- const envContent = fs.readFileSync(envPath, 'utf8');
- const envVars = {};
-
- envContent.split('\\n').forEach(line => {
- const parts = line.match(/^([^=]+)=(.*)$/);
- if (parts) {
- const key = parts[1].trim();
- let value = parts[2].trim();
- // Remove quotes if present
- if (value.startsWith('"') && value.endsWith('"')) {
- value = value.slice(1, -1);
- }
- envVars[key] = value;
- }
- });
-
- return envVars;
- } catch (e) {
- console.error('Failed to load .env.local file');
- return {};
- }
-}
-
-const envVars = loadEnvFromFile();
-const apiKey = envVars.ACQUIA_API_KEY;
-const apiSecret = envVars.ACQUIA_API_SECRET;
- `
- }
- });
-}
\ No newline at end of file
diff --git a/app/api/env-runtime-debug/route.ts b/app/api/env-runtime-debug/route.ts
deleted file mode 100644
index 3613e3e..0000000
--- a/app/api/env-runtime-debug/route.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- // Capture the current state
- const currentApiKey = process.env.ACQUIA_API_KEY;
-
- // Check if we can manually set it
- const originalValue = process.env.ACQUIA_API_KEY;
- process.env.ACQUIA_API_KEY = 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3';
- const afterManualSet = process.env.ACQUIA_API_KEY;
-
- // Restore original (for safety)
- process.env.ACQUIA_API_KEY = originalValue;
-
- return NextResponse.json({
- message: 'Runtime Environment Debug',
- original_value: originalValue,
- after_manual_set: afterManualSet,
- manual_set_worked: afterManualSet === 'deed5eaf-98ba-4924-8747-1fb1fbd00bd3',
-
- // Check for any weird process.env behavior
- process_env_keys: Object.keys(process.env).filter(k => k.includes('ACQUIA')),
-
- // Check if there are any descriptor issues
- api_key_descriptor: Object.getOwnPropertyDescriptor(process.env, 'ACQUIA_API_KEY'),
-
- // Check the type
- api_key_type: typeof process.env.ACQUIA_API_KEY,
- api_key_constructor: process.env.ACQUIA_API_KEY?.constructor?.name,
- });
-}
\ No newline at end of file
diff --git a/app/api/env-test/route.ts b/app/api/env-test/route.ts
deleted file mode 100644
index a9eff5c..0000000
--- a/app/api/env-test/route.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- return NextResponse.json({
- node_env: process.env.NODE_ENV,
- has_api_key: !!process.env.ACQUIA_API_KEY,
- has_api_secret: !!process.env.ACQUIA_API_SECRET,
- api_key_length: process.env.ACQUIA_API_KEY?.length || 0,
- api_secret_length: process.env.ACQUIA_API_SECRET?.length || 0,
- api_key_preview: process.env.ACQUIA_API_KEY?.substring(0, 8) + '...' || 'undefined',
- all_env_keys: Object.keys(process.env).filter(key => key.startsWith('ACQUIA_')),
- });
-}
\ No newline at end of file
diff --git a/app/api/generate-curl/route.ts b/app/api/generate-curl/route.ts
deleted file mode 100644
index 9cfcacc..0000000
--- a/app/api/generate-curl/route.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) {
- return NextResponse.json(
- { error: 'Missing API credentials in environment variables' },
- { status: 500 }
- );
- }
-
- const apiKey = process.env.ACQUIA_API_KEY;
- const apiSecret = process.env.ACQUIA_API_SECRET;
-
- // Generate curl commands for testing
- const credentials = Buffer.from(`${apiKey}:${apiSecret}`).toString('base64');
-
- const curlCommands = {
- basic_auth_accounts: `curl -X POST "https://accounts.acquia.com/api/auth/oauth/token" \\
- -H "Content-Type: application/x-www-form-urlencoded" \\
- -H "Authorization: Basic ${credentials}" \\
- -H "Accept: application/json" \\
- -d "grant_type=client_credentials"`,
-
- form_params_accounts: `curl -X POST "https://accounts.acquia.com/api/auth/oauth/token" \\
- -H "Content-Type: application/x-www-form-urlencoded" \\
- -H "Accept: application/json" \\
- -d "grant_type=client_credentials&client_id=${apiKey}&client_secret=${apiSecret}"`,
-
- basic_auth_cloud: `curl -X POST "https://cloud.acquia.com/api/auth/oauth/token" \\
- -H "Content-Type: application/x-www-form-urlencoded" \\
- -H "Authorization: Basic ${credentials}" \\
- -H "Accept: application/json" \\
- -d "grant_type=client_credentials"`,
-
- form_params_cloud: `curl -X POST "https://cloud.acquia.com/api/auth/oauth/token" \\
- -H "Content-Type: application/x-www-form-urlencoded" \\
- -H "Accept: application/json" \\
- -d "grant_type=client_credentials&client_id=${apiKey}&client_secret=${apiSecret}"`
- };
-
- return NextResponse.json({
- message: 'Test these curl commands in your terminal to see which one works',
- credentials_info: {
- api_key: apiKey,
- api_secret_length: apiSecret.length,
- api_secret_preview: apiSecret.substring(0, 10) + '...',
- base64_credentials_preview: credentials.substring(0, 20) + '...'
- },
- curl_commands: curlCommands
- });
-}
\ No newline at end of file
diff --git a/app/api/read-env-file/route.ts b/app/api/read-env-file/route.ts
deleted file mode 100644
index b8a839e..0000000
--- a/app/api/read-env-file/route.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-import fs from 'fs';
-import path from 'path';
-
-export async function GET(request: NextRequest) {
- try {
- const envLocalPath = path.join(process.cwd(), '.env.local');
-
- if (!fs.existsSync(envLocalPath)) {
- return NextResponse.json({
- error: '.env.local file not found',
- path: envLocalPath
- });
- }
-
- const content = fs.readFileSync(envLocalPath, 'utf8');
- const lines = content.split('\n');
-
- // Find the ACQUIA_API_KEY line
- const apiKeyLine = lines.find(line => line.trim().startsWith('ACQUIA_API_KEY'));
- const apiSecretLine = lines.find(line => line.trim().startsWith('ACQUIA_API_SECRET'));
-
- // Parse the values
- let fileApiKey = '';
- let fileApiSecret = '';
-
- if (apiKeyLine) {
- const match = apiKeyLine.match(/ACQUIA_API_KEY\s*=\s*(.+)/);
- fileApiKey = match ? match[1].trim() : '';
- }
-
- if (apiSecretLine) {
- const match = apiSecretLine.match(/ACQUIA_API_SECRET\s*=\s*(.+)/);
- fileApiSecret = match ? match[1].trim() : '';
- }
-
- return NextResponse.json({
- message: 'Direct .env.local file read',
- file_path: envLocalPath,
- file_exists: true,
- api_key_line: apiKeyLine,
- api_secret_line: apiSecretLine ? apiSecretLine.substring(0, 50) + '...' : undefined,
- parsed_api_key: fileApiKey,
- parsed_api_secret_preview: fileApiSecret ? fileApiSecret.substring(0, 20) + '...' : '',
- process_env_api_key: process.env.ACQUIA_API_KEY,
- values_match: fileApiKey === process.env.ACQUIA_API_KEY,
- file_content_preview: content.substring(0, 300) + (content.length > 300 ? '...' : '')
- });
-
- } catch (error) {
- return NextResponse.json({
- error: 'Failed to read .env.local file',
- details: error instanceof Error ? error.message : String(error)
- });
- }
-}
\ No newline at end of file
diff --git a/app/api/validate-credentials/route.ts b/app/api/validate-credentials/route.ts
deleted file mode 100644
index b1a97fc..0000000
--- a/app/api/validate-credentials/route.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server';
-
-export async function GET(request: NextRequest) {
- const apiKey = process.env.ACQUIA_API_KEY;
- const apiSecret = process.env.ACQUIA_API_SECRET;
-
- if (!apiKey || !apiSecret) {
- return NextResponse.json({
- error: 'Missing credentials',
- found_api_key: !!apiKey,
- found_api_secret: !!apiSecret
- });
- }
-
- // Detailed credential analysis
- const analysis = {
- api_key: {
- value: apiKey,
- length: apiKey.length,
- has_whitespace: /\s/.test(apiKey),
- is_uuid_format: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(apiKey),
- char_codes: Array.from(apiKey).map(c => c.charCodeAt(0)),
- },
- api_secret: {
- value_preview: apiSecret.substring(0, 20) + '...',
- length: apiSecret.length,
- has_whitespace: /\s/.test(apiSecret),
- is_base64_like: /^[A-Za-z0-9+/]+=*$/.test(apiSecret),
- ends_with_equals: apiSecret.endsWith('='),
- char_codes_sample: Array.from(apiSecret.substring(0, 20)).map(c => c.charCodeAt(0)),
- }
- };
-
- return NextResponse.json({
- message: 'Credential Analysis',
- analysis
- });
-}
\ No newline at end of file
From 3d5019fada491a13864435594d97e6c99e845351 Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 14:27:47 -0700
Subject: [PATCH 5/9] fixup! cleanup
---
components/Dashboard.tsx | 48 ++++++------
lib/acquia-api.ts | 153 ++++++++++++++++++++-------------------
2 files changed, 101 insertions(+), 100 deletions(-)
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
index 2a652e8..3250113 100644
--- a/components/Dashboard.tsx
+++ b/components/Dashboard.tsx
@@ -335,6 +335,30 @@ const Dashboard: React.FC = () => {
{/* Data Display Section */}
+ {/* Data Tables Section */}
+
+ ({
+ rank: index + 1,
+ name: app.name,
+ value: app.views,
+ uuid: app.uuid,
+ }))}
+ valueLabel="Views"
+ />
+ ({
+ rank: index + 1,
+ name: app.name,
+ value: app.visits,
+ uuid: app.uuid,
+ }))}
+ valueLabel="Visits"
+ />
+
+
{/* Views Section */}
@@ -352,18 +376,6 @@ const Dashboard: React.FC = () => {
({ name: app.name, value: app.views, uuid: app.uuid }))} />
-
- ({
- rank: index + 1,
- name: app.name,
- value: app.views,
- uuid: app.uuid,
- }))}
- valueLabel="Views"
- />
-
{/* Visits Section */}
@@ -382,18 +394,6 @@ const Dashboard: React.FC = () => {
({ name: app.name, value: app.visits, uuid: app.uuid }))} />
-
- ({
- rank: index + 1,
- name: app.name,
- value: app.visits,
- uuid: app.uuid,
- }))}
- valueLabel="Visits"
- />
-
{loading && (
diff --git a/lib/acquia-api.ts b/lib/acquia-api.ts
index ebc1e5f..4c12d44 100644
--- a/lib/acquia-api.ts
+++ b/lib/acquia-api.ts
@@ -55,7 +55,7 @@ class AcquiaApiServiceFixed {
constructor(config: AcquiaApiConfig) {
this.config = config;
- console.log('๐ง Initializing Acquia API Service...');
+ // console.log('๐ง Initializing Acquia API Service...');
}
setProgressCallback(callback: (progress: FetchProgress) => void) {
this.progressCallback = callback;
@@ -65,7 +65,7 @@ class AcquiaApiServiceFixed {
if (this.progressCallback) {
this.progressCallback(progress);
}
- console.log('๐ Progress:', progress);
+ // console.log('๐ Progress:', progress);
}
private async getAccessToken(): Promise {
@@ -74,7 +74,7 @@ class AcquiaApiServiceFixed {
}
// Debug credentials
- console.log('๐ Debug API Key:', {
+ /* console.log('๐ Debug API Key:', {
value: this.config.apiKey ? `${this.config.apiKey.substring(0, 8)}...` : 'missing',
length: this.config.apiKey?.length || 0,
hasQuotes: this.config.apiKey?.startsWith('"') && this.config.apiKey?.endsWith('"')
@@ -84,7 +84,7 @@ class AcquiaApiServiceFixed {
preview: this.config.apiSecret ? `${this.config.apiSecret.substring(0, 8)}...` : 'missing',
length: this.config.apiSecret?.length || 0,
hasQuotes: this.config.apiSecret?.startsWith('"') && this.config.apiSecret?.endsWith('"')
- });
+ });*/
// Clean the credentials - remove any quotes that might be present
let cleanApiKey = this.config.apiKey.replace(/^"|"$/g, '').trim();
@@ -98,19 +98,20 @@ class AcquiaApiServiceFixed {
const decodedKey = Buffer.from(cleanApiKey, 'base64').toString('utf-8');
// Check if decoded value looks like a UUID
if (decodedKey.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
- console.log('๐ง Detected base64-encoded API key, using decoded value');
+ // console.log('๐ง Detected base64-encoded API key, using decoded value');
cleanApiKey = decodedKey;
}
} catch (error) {
// If decoding fails, use original value
- console.log('โ ๏ธ Failed to decode suspected base64 API key, using original value');
+ // console.log('โ ๏ธ Failed to decode suspected base64 API key, using original value');
}
}
-
+ /**
console.log('๐ Using cleaned credentials:', {
keyLength: cleanApiKey.length,
secretLength: cleanApiSecret.length
});
+ */
const authUrl = `${this.config.authUrl}/auth/oauth/token`;
@@ -118,7 +119,7 @@ class AcquiaApiServiceFixed {
const authMethods = [
// Method 1: Basic Auth
async () => {
- console.log('๐ Trying Basic Auth method...');
+ // console.log('๐ Trying Basic Auth method...');
const credentials = Buffer.from(`${cleanApiKey}:${cleanApiSecret}`).toString('base64');
const response = await axios({
method: 'POST',
@@ -133,7 +134,7 @@ class AcquiaApiServiceFixed {
validateStatus: () => true,
});
- console.log('๐ฅ Basic Auth response status:', response.status);
+ // console.log('๐ฅ Basic Auth response status:', response.status);
if (response.status === 200 && response.data?.access_token) {
return response.data.access_token;
}
@@ -142,7 +143,7 @@ class AcquiaApiServiceFixed {
// Method 2: Form parameters
async () => {
- console.log('๐ Trying Form Parameters method...');
+ // console.log('๐ Trying Form Parameters method...');
const formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', cleanApiKey);
@@ -160,7 +161,7 @@ class AcquiaApiServiceFixed {
validateStatus: () => true,
});
- console.log('๐ฅ Form Parameters response status:', response.status);
+ // console.log('๐ฅ Form Parameters response status:', response.status);
if (response.status === 200 && response.data?.access_token) {
return response.data.access_token;
}
@@ -169,7 +170,7 @@ class AcquiaApiServiceFixed {
// Method 3: Use correct client ID format (if UUID is in different format)
async () => {
- console.log('๐ Trying with alternate client ID format...');
+ // console.log('๐ Trying with alternate client ID format...');
// Try with a UUID format if the key is not already in UUID format
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleanApiKey);
@@ -194,7 +195,7 @@ class AcquiaApiServiceFixed {
validateStatus: () => true,
});
- console.log('๐ฅ Alternate client ID response status:', response.status);
+ // console.log('๐ฅ Alternate client ID response status:', response.status);
if (response.status === 200 && response.data?.access_token) {
return response.data.access_token;
}
@@ -208,7 +209,7 @@ class AcquiaApiServiceFixed {
try {
const token = await method();
this.accessToken = token;
- console.log('โ
Successfully authenticated!');
+ // console.log('โ
Successfully authenticated!');
return token;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
@@ -238,7 +239,7 @@ class AcquiaApiServiceFixed {
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
- console.log('๐ Token expired, retrying...');
+ // console.log('๐ Token expired, retrying...');
this.accessToken = null;
const newToken = await this.getAccessToken();
@@ -258,11 +259,11 @@ class AcquiaApiServiceFixed {
async getApplications(): Promise {
try {
- console.log(`๐ Fetching all applications`);
+ // console.log(`๐ Fetching all applications`);
const response = await this.makeAuthenticatedRequest('/applications');
- console.log('โ
Applications API Response Status:', response.status);
+ // console.log('โ
Applications API Response Status:', response.status);
let applications: Application[] = [];
@@ -280,7 +281,7 @@ class AcquiaApiServiceFixed {
}))
}));
- console.log(`โ
Extracted ${applications.length} applications`);
+ // console.log(`โ
Extracted ${applications.length} applications`);
} else {
console.warn('โ ๏ธ No applications found in response');
}
@@ -294,11 +295,11 @@ class AcquiaApiServiceFixed {
private buildFilterParam(from?: string, to?: string): string {
if (!from && !to) {
- console.log('๐
No date range specified, API will return default data');
+ // console.log('๐
No date range specified, API will return default data');
return '';
}
- console.log(`๐
Building filter for date range: ${from} to ${to}`);
+ // console.log(`๐
Building filter for date range: ${from} to ${to}`);
// Convert YYYY-MM-DD format to the exact format the API expects
const formatDateForApi = (dateStr: string, isEndDate: boolean = false): string => {
@@ -323,7 +324,7 @@ class AcquiaApiServiceFixed {
}
}
- console.log(`๐
Formatted ${dateStr} (end=${isEndDate}) -> ${isoDate}`);
+ // console.log(`๐
Formatted ${dateStr} (end=${isEndDate}) -> ${isoDate}`);
return isoDate;
};
@@ -340,20 +341,20 @@ class AcquiaApiServiceFixed {
}
const filterString = filterParts.join(',');
- console.log(`๐
Final filter parameter: ${filterString}`);
+ // console.log(`๐
Final filter parameter: ${filterString}`);
return filterString;
}
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));
+ // 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 [];
}
- console.log('๐ _embedded keys:', Object.keys(responseData._embedded));
+ // console.log('๐ _embedded keys:', Object.keys(responseData._embedded));
if (!responseData._embedded.items || !Array.isArray(responseData._embedded.items)) {
console.warn('โ ๏ธ No _embedded.items array found in response');
@@ -361,70 +362,70 @@ class AcquiaApiServiceFixed {
}
const items = responseData._embedded.items;
- console.log(`๐ Found ${items.length} items in _embedded.items`);
+ // console.log(`๐ Found ${items.length} items in _embedded.items`);
const parsedVisitsData: VisitsData[] = [];
const parsedViewsData: ViewsData[] = [];
items.forEach((item: any, itemIndex: number) => {
- console.log(`\n๐ข === PROCESSING ITEM ${itemIndex} (One Application) ===`);
- console.log(`๐ Item structure: hasDatapoints=${!!item.datapoints}, datapointsCount=${item.datapoints?.length || 0}, hasMetadata=${!!item.metadata}, metadataKeys=${item.metadata ? JSON.stringify(Object.keys(item.metadata)) : '[]'}`);
+ // console.log(`\n๐ข === PROCESSING ITEM ${itemIndex} (One Application) ===`);
+ // console.log(`๐ Item structure: hasDatapoints=${!!item.datapoints}, datapointsCount=${item.datapoints?.length || 0}, hasMetadata=${!!item.metadata}, metadataKeys=${item.metadata ? JSON.stringify(Object.keys(item.metadata)) : '[]'}`);
// FIRST: Extract the application metadata for this entire item
let applicationUuid = '';
let applicationName = '';
let environmentUuids: string[] = [];
let environmentNames: string[] = [];
- console.log(`๐ Extracting metadata for item ${itemIndex}...`);
+ // console.log(`๐ Extracting metadata for item ${itemIndex}...`);
// Get application info from metadata.application.uuids[0]
if (item.metadata?.application?.uuids && Array.isArray(item.metadata.application.uuids)) {
applicationUuid = item.metadata.application.uuids[0] || '';
- console.log(` ๐ Application UUID: ${applicationUuid}`);
+ // console.log(` ๐ Application UUID: ${applicationUuid}`);
} else {
- console.log(` โ No application UUID found in metadata for item ${itemIndex}`);
+ // console.log(` โ No application UUID found in metadata for item ${itemIndex}`);
if (item.metadata) {
- console.log(` ๐ Available metadata: ${JSON.stringify(item.metadata, null, 2)}`);
+ // console.log(` ๐ Available metadata: ${JSON.stringify(item.metadata, null, 2)}`);
} else {
- console.log(` ๐ No metadata available`);
+ // console.log(` ๐ No metadata available`);
}
}
// Get application name from metadata.application.names[0]
if (item.metadata?.application?.names && Array.isArray(item.metadata.application.names)) {
applicationName = item.metadata.application.names[0] || '';
- console.log(` ๐ Application name: ${applicationName}`);
+ // console.log(` ๐ Application name: ${applicationName}`);
}
// If no name found, generate one from UUID
if (!applicationName && applicationUuid) {
applicationName = `App ${applicationUuid.substring(0, 8)}`;
- console.log(` ๐ Generated application name: ${applicationName}`);
+ // console.log(` ๐ Generated application name: ${applicationName}`);
}
// Get environment info if available
if (item.metadata?.environment) {
if (item.metadata.environment.uuids && Array.isArray(item.metadata.environment.uuids)) {
environmentUuids = item.metadata.environment.uuids;
- console.log(` ๐ Environment UUIDs (${environmentUuids.length}): ${JSON.stringify(environmentUuids)}`);
+ // console.log(` ๐ Environment UUIDs (${environmentUuids.length}): ${JSON.stringify(environmentUuids)}`);
}
if (item.metadata.environment.names && Array.isArray(item.metadata.environment.names)) {
environmentNames = item.metadata.environment.names;
- console.log(` ๐ Environment names (${environmentNames.length}): ${JSON.stringify(environmentNames)}`);
+ // console.log(` ๐ Environment names (${environmentNames.length}): ${JSON.stringify(environmentNames)}`);
}
}
// SECOND: Process ALL datapoints for this ONE application
if (!item.datapoints || !Array.isArray(item.datapoints)) {
- console.log(` โ ๏ธ No datapoints found for application ${applicationUuid} (item ${itemIndex})`);
+ // console.log(` โ ๏ธ No datapoints found for application ${applicationUuid} (item ${itemIndex})`);
return; // Skip this item
}
- console.log(` ๐ Processing ${item.datapoints.length} datapoints for application: ${applicationName} (${applicationUuid})`);
+ // console.log(` ๐ Processing ${item.datapoints.length} datapoints for application: ${applicationName} (${applicationUuid})`);
item.datapoints.forEach((datapoint: any, dpIndex: number) => {
- console.log(` ๐ Datapoint ${dpIndex} for ${applicationName}: ${JSON.stringify(datapoint, null, 2)}`);
+ // console.log(` ๐ Datapoint ${dpIndex} for ${applicationName}: ${JSON.stringify(datapoint, null, 2)}`);
let date = '';
let value = 0;
@@ -433,17 +434,17 @@ class AcquiaApiServiceFixed {
date = datapoint[0];
// Handle both string and number values
value = typeof datapoint[1] === 'string' ? parseInt(datapoint[1]) || 0 : datapoint[1] || 0;
- console.log(` ๐
Date: ${date}`);
- console.log(` ๐ข Value: ${value} ${dataType}`);
+ // console.log(` ๐
Date: ${date}`);
+ // console.log(` ๐ข Value: ${value} ${dataType}`);
}
// Handle object format (fallback)
else if (typeof datapoint === 'object') {
date = datapoint.datetime || datapoint.date || datapoint.timestamp || '';
value = parseInt(datapoint.value) || parseInt(datapoint[dataType]) || 0;
- console.log(` ๐
Date (object): ${date}`);
- console.log(` ๐ข Value (object): ${value} ${dataType}`);
+ // console.log(` ๐
Date (object): ${date}`);
+ // console.log(` ๐ข Value (object): ${value} ${dataType}`);
} else {
- console.log(` โ ๏ธ Unexpected datapoint format: ${typeof datapoint}, ${String(datapoint)}`);
+ // console.log(` โ ๏ธ Unexpected datapoint format: ${typeof datapoint}, ${String(datapoint)}`);
return; // Skip this datapoint
}
@@ -466,30 +467,30 @@ class AcquiaApiServiceFixed {
visits: value
};
parsedVisitsData.push(visitData);
- console.log(` โ
Created visits record: ${value} visits for ${applicationName} on ${date}`);
+ // console.log(` โ
Created visits record: ${value} visits for ${applicationName} on ${date}`);
} else {
const viewData: ViewsData = {
...baseData,
views: value
};
parsedViewsData.push(viewData);
- console.log(` โ
Created views record: ${value} views for ${applicationName} on ${date}`);
+ // console.log(` โ
Created views record: ${value} views for ${applicationName} on ${date}`);
}
} else {
- console.log(` โ ๏ธ Skipping datapoint - missing required data:`);
- console.log(` - applicationUuid: ${applicationUuid || 'MISSING'}`);
- console.log(` - date: ${date || 'MISSING'}`);
+ // console.log(` โ ๏ธ Skipping datapoint - missing required data:`);
+ // console.log(` - applicationUuid: ${applicationUuid || 'MISSING'}`);
+ // console.log(` - date: ${date || 'MISSING'}`);
}
});
- console.log(` ๐ Completed processing ${item.datapoints.length} datapoints for ${applicationName}`);
+ // console.log(` ๐ Completed processing ${item.datapoints.length} datapoints for ${applicationName}`);
});
// Return the correct array based on dataType
const parsedData = dataType === 'visits' ? parsedVisitsData : parsedViewsData;
- console.log(`\nโ
PARSING COMPLETE`);
- console.log(`๐ Total ${dataType} records created: ${parsedData.length}`);
+ // console.log(`\nโ
PARSING COMPLETE`);
+ // console.log(`๐ Total ${dataType} records created: ${parsedData.length}`);
// Enhanced summary statistics
const totalValue = parsedData.reduce((sum, item) => {
@@ -519,10 +520,10 @@ class AcquiaApiServiceFixed {
return acc;
}, {} as Record);
- console.log(`๐ Total ${dataType}: ${totalValue.toLocaleString()}`);
- console.log(`๐ Applications found: ${Object.keys(applicationSummary).length}`);
+ // console.log(`๐ Total ${dataType}: ${totalValue.toLocaleString()}`);
+ // console.log(`๐ Applications found: ${Object.keys(applicationSummary).length}`);
Object.entries(applicationSummary).forEach(([uuid, summary]: [string, any]) => {
- console.log(` โข ${summary.name} (${uuid.substring(0, 8)}...): ${summary.totalValue.toLocaleString()} ${dataType}, ${summary.datapoints} datapoints`);
+ // console.log(` โข ${summary.name} (${uuid.substring(0, 8)}...): ${summary.totalValue.toLocaleString()} ${dataType}, ${summary.datapoints} datapoints`);
});
return parsedData;
}
@@ -541,8 +542,8 @@ class AcquiaApiServiceFixed {
// 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}`);
+ // console.log(`๐ Date range requested: ${from} to ${to}`);
+ // console.log(`๐ Filter parameter: ${filterParam}`);
while (hasMorePages) {
try {
@@ -551,16 +552,16 @@ class AcquiaApiServiceFixed {
// Add filter parameter if we have date range
if (filterParam) {
params.append('filter', filterParam);
- console.log(`๐
Added filter parameter to request`);
+ // console.log(`๐
Added filter parameter to request`);
} else {
- console.log(`โ ๏ธ No filter parameter - API will return default date range`);
+ // 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}`);
+ // console.log(`๐ Using resolution: ${resolution}`);
// Add pagination if needed
if (currentPage > 1) {
@@ -575,15 +576,15 @@ class AcquiaApiServiceFixed {
itemsCollected: allData.length
});
- console.log(`๐ก Making request to: ${fullEndpoint}`);
- console.log(`๐ก Full URL parameters:`, params.toString());
+ // console.log(`๐ก Making request to: ${fullEndpoint}`);
+ // console.log(`๐ก Full URL parameters:`, params.toString());
const startTime = Date.now();
const response = await this.makeAuthenticatedRequest(fullEndpoint);
const endTime = Date.now();
- console.log(`โ
Request completed in ${endTime - startTime}ms`);
- console.log(`๐ Response status: ${response.status}`);
+ // console.log(`โ
Request completed in ${endTime - startTime}ms`);
+ // console.log(`๐ Response status: ${response.status}`);
// Log some response details to debug date issues
if (response.data._embedded?.items?.length > 0) {
@@ -591,8 +592,8 @@ class AcquiaApiServiceFixed {
if (firstItem.datapoints?.length > 0) {
const firstDatapoint = firstItem.datapoints[0];
const lastDatapoint = firstItem.datapoints[firstItem.datapoints.length - 1];
- console.log(`๐
API returned data from ${firstDatapoint[0]} to ${lastDatapoint[0]}`);
- console.log(`๐ Total datapoints in first item: ${firstItem.datapoints.length}`);
+ // console.log(`๐
API returned data from ${firstDatapoint[0]} to ${lastDatapoint[0]}`);
+ // console.log(`๐ Total datapoints in first item: ${firstItem.datapoints.length}`);
}
}
@@ -602,7 +603,7 @@ class AcquiaApiServiceFixed {
if (pageData.length > 0) {
const dates = pageData.map(item => item.date).filter(Boolean).sort();
if (dates.length > 0) {
- console.log(`๐
Parsed data date range: ${dates[0]} to ${dates[dates.length - 1]}`);
+ // console.log(`๐
Parsed data date range: ${dates[0]} to ${dates[dates.length - 1]}`);
}
}
@@ -613,14 +614,14 @@ class AcquiaApiServiceFixed {
if (pageInfo) {
totalPages = pageInfo.totalPages || pageInfo.total_pages || 1;
hasMorePages = currentPage < totalPages;
- console.log(`๐ Pagination: page ${currentPage} of ${totalPages}`);
+ // console.log(`๐ Pagination: page ${currentPage} of ${totalPages}`);
} else {
const links = response.data._links;
hasMorePages = !!(links && links.next);
if (links?.next) {
- console.log(`๐ Found next link: ${links.next.href}`);
+ // console.log(`๐ Found next link: ${links.next.href}`);
} else {
- console.log(`๐ No more pages found`);
+ // console.log(`๐ No more pages found`);
}
}
@@ -631,7 +632,7 @@ class AcquiaApiServiceFixed {
}
if (hasMorePages) {
- console.log('โฑ๏ธ Waiting 500ms before next request...');
+ // console.log('โฑ๏ธ Waiting 500ms before next request...');
await new Promise(resolve => setTimeout(resolve, 500));
}
@@ -653,13 +654,13 @@ class AcquiaApiServiceFixed {
itemsCollected: allData.length
});
- console.log(`๐ Successfully fetched ${allData.length} ${dataType} records from ${currentPage - 1} pages`);
+ // console.log(`๐ Successfully fetched ${allData.length} ${dataType} records from ${currentPage - 1} pages`);
// Final summary of date range
if (allData.length > 0) {
const dates = allData.map(item => item.date).filter(Boolean).sort();
if (dates.length > 0) {
- console.log(`๐
Final data covers: ${dates[0]} to ${dates[dates.length - 1]}`);
+ // console.log(`๐
Final data covers: ${dates[0]} to ${dates[dates.length - 1]}`);
}
}
@@ -668,13 +669,13 @@ class AcquiaApiServiceFixed {
async getVisitsDataByApplication(subscriptionUuid: string, from?: string, to?: 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'}`);
+ // 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);
}
async getViewsDataByApplication(subscriptionUuid: string, from?: string, to?: 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'}`);
+ // 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);
}
}
From a0e744d3c5abd1190c91b0fd941b39bae0a6382f Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 14:32:16 -0700
Subject: [PATCH 6/9] fixup! comment out console.log messages
---
app/api/acquia/applications/route.ts | 6 +++---
app/api/acquia/views/route.ts | 9 +++++----
app/api/acquia/visits/route.ts | 8 +++++---
components/Dashboard.tsx | 12 ++++++------
components/VisitsPieChart.tsx | 4 ++--
5 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/app/api/acquia/applications/route.ts b/app/api/acquia/applications/route.ts
index 14086a3..808b927 100644
--- a/app/api/acquia/applications/route.ts
+++ b/app/api/acquia/applications/route.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import AcquiaApiServiceFixed from '@/lib/acquia-api';
export async function GET(request: NextRequest) {
- console.log('๐ Applications API Route called');
+ // console.log('๐ Applications API Route called');
// Update the API service initialization with better error handling
if (!process.env.ACQUIA_API_KEY || !process.env.ACQUIA_API_SECRET) {
@@ -31,11 +31,11 @@ export async function GET(request: NextRequest) {
apiSecret: process.env.ACQUIA_API_SECRET!,
});
- console.log('๐ง Using FIXED API Service for applications');
+ // console.log('๐ง Using FIXED API Service for applications');
const applications = await apiService.getApplications();
- console.log('โ
Successfully fetched applications data, count:', applications.length);
+ // console.log('โ
Successfully fetched applications data, count:', applications.length);
return NextResponse.json(applications);
} catch (error) {
diff --git a/app/api/acquia/views/route.ts b/app/api/acquia/views/route.ts
index 143c515..72cd719 100644
--- a/app/api/acquia/views/route.ts
+++ b/app/api/acquia/views/route.ts
@@ -6,12 +6,13 @@ 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,
});
+ */
if (!subscriptionUuid) {
console.error('โ Missing required parameter: subscriptionUuid');
@@ -49,10 +50,10 @@ export async function GET(request: NextRequest) {
});
apiService.setProgressCallback((progress) => {
- console.log('๐ Views progress:', progress);
+ // console.log('๐ Views progress:', progress);
});
- console.log('๐ง Using FIXED API Service for views by application (with pagination)');
+ // console.log('๐ง Using FIXED API Service for views by application (with pagination)');
const data = await apiService.getViewsDataByApplication(
subscriptionUuid,
@@ -60,7 +61,7 @@ export async function GET(request: NextRequest) {
to || 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,
diff --git a/app/api/acquia/visits/route.ts b/app/api/acquia/visits/route.ts
index 169c337..0e31426 100644
--- a/app/api/acquia/visits/route.ts
+++ b/app/api/acquia/visits/route.ts
@@ -7,11 +7,13 @@ export async function GET(request: NextRequest) {
const from = searchParams.get('from');
const to = searchParams.get('to');
+ /**
console.log('๐ Visits by Application API Route called with params:', {
subscriptionUuid,
from,
to
});
+ */
if (!subscriptionUuid) {
console.error('โ Missing required parameter: subscriptionUuid');
@@ -48,14 +50,14 @@ export async function GET(request: NextRequest) {
});
apiService.setProgressCallback((progress) => {
- console.log('๐ Visits progress:', progress);
+ // console.log('๐ Visits progress:', progress);
});
- console.log('๐ง Using FIXED API Service for visits by application (with pagination)');
+ // console.log('๐ง Using FIXED API Service for visits by application (with pagination)');
const data = await apiService.getVisitsDataByApplication(subscriptionUuid, from || undefined, to || undefined);
- console.log('โ
Successfully fetched ALL visits by application data, total count:', data.length);
+ // console.log('โ
Successfully fetched ALL visits by application data, total count:', data.length);
return NextResponse.json({
data,
diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx
index 3250113..96c1944 100644
--- a/components/Dashboard.tsx
+++ b/components/Dashboard.tsx
@@ -37,7 +37,7 @@ const Dashboard: React.FC = () => {
}
const apps = await response.json();
- console.log('๐ฑ Fetched applications:', apps.length);
+ // console.log('๐ฑ Fetched applications:', apps.length);
setApplications(apps);
@@ -48,7 +48,7 @@ const Dashboard: React.FC = () => {
});
setApplicationMap(appMap);
- console.log('๐ฑ Created application map with', Object.keys(appMap).length, 'entries');
+ // console.log('๐ฑ Created application map with', Object.keys(appMap).length, 'entries');
} catch (error) {
console.error('Error fetching applications:', error);
@@ -60,7 +60,7 @@ const Dashboard: React.FC = () => {
try {
const response = await fetch('/api/check-env');
const data = await response.json();
- console.log('Environment variables check:', data);
+ // console.log('Environment variables check:', data);
alert(`API Key in .env.local: ${data.env_file.parsed_values.ACQUIA_API_KEY}\nAPI Key in process.env: ${data.process_env.ACQUIA_API_KEY}\nExact match: ${data.comparison.exact_match.ACQUIA_API_KEY}`);
} catch (error) {
console.error('Error checking environment variables:', error);
@@ -94,7 +94,7 @@ const Dashboard: React.FC = () => {
...(dateTo && { to: dateTo }),
});
- console.log('๐ Fetching data with params:', { subscriptionUuid, dateFrom, dateTo });
+ // console.log('๐ Fetching data with params:', { subscriptionUuid, dateFrom, dateTo });
// Fetch visits data
setLoadingStep('Fetching visits data from Acquia API...');
@@ -107,7 +107,7 @@ const Dashboard: React.FC = () => {
}
const visitsResult = await visitsResponse.json();
- console.log('๐ Received visits result with length:', Array.isArray(visitsResult) ? visitsResult.length : 'not an array');
+ // console.log('๐ Received visits result with length:', Array.isArray(visitsResult) ? visitsResult.length : 'not an array');
// Fetch views data
setLoadingStep('Fetching views data from Acquia API...');
const viewsResponse = await fetch(`/api/acquia/views?${params}`);
@@ -119,7 +119,7 @@ const Dashboard: React.FC = () => {
}
const viewsResult = await viewsResponse.json();
- console.log('๐ Received views result with length:', Array.isArray(viewsResult) ? viewsResult.length : 'not an array');
+ // console.log('๐ Received views result with length:', Array.isArray(viewsResult) ? viewsResult.length : 'not an array');
setLoadingStep('Processing data...');
// Handle different response formats
diff --git a/components/VisitsPieChart.tsx b/components/VisitsPieChart.tsx
index 9b28c8c..ffd281a 100644
--- a/components/VisitsPieChart.tsx
+++ b/components/VisitsPieChart.tsx
@@ -72,7 +72,7 @@ const VisitsPieChart: React.FC = ({ data }) => {
useEffect(() => {
if (!isMounted || !data) return;
- console.log('๐ฏ VisitsPieChart receiving pre-summarized data:', data.length, 'records');
+ // console.log('๐ฏ VisitsPieChart receiving pre-summarized data:', data.length, 'records');
try {
// Data is already summarized. We just add colors and sort.
@@ -89,7 +89,7 @@ const VisitsPieChart: React.FC = ({ data }) => {
const total = filteredData.reduce((sum, item) => sum + item.value, 0);
- console.log(`๐ฏ Prepared pie chart data: ${filteredData.length} applications, ${total.toLocaleString()} total visits`);
+ // console.log(`๐ฏ Prepared pie chart data: ${filteredData.length} applications, ${total.toLocaleString()} total visits`);
setChartData(filteredData);
setTotalVisits(total);
From 47ec1fc5ecce47f89a0841be51c590fafe2b6c4c Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 14:38:58 -0700
Subject: [PATCH 7/9] fixup! WIP
---
README.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 3d036a8..bf6ff5a 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,14 @@
-# churro
+# CHURRO
Cloud Hosting Usage Reporting with Recurring Output
## Usage
-1. Add your Acquia API key and secret to a file called `.env.local` at the root of the repository directory:
+1. Add your Acquia API key, API secret, subscription UUID, and your Views/Visits entitlements to a file called `.env.local` at the root of the repository directory:
```
ACQUIA_API_KEY=
ACQUIA_API_SECRET=
NEXT_PUBLIC_ACQUIA_SUBSCRIPTION_UUID=
NEXT_PUBLIC_ACQUIA_MONTHLY_VIEWS_ENTITLEMENT=
NEXT_PUBLIC_ACQUIA_MONTHLY_VISITS_ENTITLEMENT=
-
```
(no quotes)
From 7c99b2a8f93bb59842d356f6673442b0cd14a708 Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 14:40:25 -0700
Subject: [PATCH 8/9] fixup! remove unused components
---
components/SimpleVisitsPieChart.tsx | 40 ----------------
components/TestChart.tsx | 39 ---------------
components/ViewsBarChart.tsx | 74 -----------------------------
components/VisitsBarChart.tsx | 74 -----------------------------
4 files changed, 227 deletions(-)
delete mode 100644 components/SimpleVisitsPieChart.tsx
delete mode 100644 components/TestChart.tsx
delete mode 100644 components/ViewsBarChart.tsx
delete mode 100644 components/VisitsBarChart.tsx
diff --git a/components/SimpleVisitsPieChart.tsx b/components/SimpleVisitsPieChart.tsx
deleted file mode 100644
index d7fca64..0000000
--- a/components/SimpleVisitsPieChart.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
-
-interface SimpleVisitsPieChartProps {
- data: { name: string; value: number }[];
-}
-
-const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'];
-
-const SimpleVisitsPieChart: React.FC = ({ data }) => {
- console.log('SimpleVisitsPieChart data:', data);
-
- return (
-
-
- Simple Visits Pie Chart
-
-
-
- `${name}: ${value}`}
- >
- {data.map((entry, index) => (
- |
- ))}
-
-
-
-
- );
-};
-
-export default SimpleVisitsPieChart;
\ No newline at end of file
diff --git a/components/TestChart.tsx b/components/TestChart.tsx
deleted file mode 100644
index f0fdbc3..0000000
--- a/components/TestChart.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-'use client';
-
-import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
-
-const TestChart = () => {
- const data = [
- { name: 'Group A', value: 400 },
- { name: 'Group B', value: 300 },
- { name: 'Group C', value: 300 },
- { name: 'Group D', value: 200 },
- ];
-
- const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042'];
-
- return (
-
-
Test Chart
-
-
-
- {data.map((entry, index) => (
- |
- ))}
-
-
-
-
- );
-};
-
-export default TestChart;
\ No newline at end of file
diff --git a/components/ViewsBarChart.tsx b/components/ViewsBarChart.tsx
deleted file mode 100644
index 16009bc..0000000
--- a/components/ViewsBarChart.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
-
-interface SummarizedData {
- name: string;
- value: number;
- uuid: string;
-}
-
-interface ViewsBarChartProps {
- data: SummarizedData[];
-}
-
-const COLORS = ['#8884D8', '#82CA9D', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA'];
-
-const ViewsBarChart: React.FC = ({ data }) => {
- const [chartData, setChartData] = useState([]);
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- useEffect(() => {
- if (!isMounted || !data) return;
-
- try {
- // Data is already summarized. We just filter and sort it.
- const filteredData = [...data]
- .filter(item => item.value > 0)
- .sort((a, b) => b.value - a.value);
-
- setChartData(filteredData);
-
- } catch (error) {
- console.error('โ Error preparing views bar chart data:', error);
- setChartData([]);
- }
- }, [data, isMounted]);
-
- if (!isMounted) {
- return ;
- }
-
- if (chartData.length === 0) {
- return (
-
-
No views data to display
-
- );
- }
-
- return (
-
-
-
-
-
-
- [value.toLocaleString(), 'Views']} cursor={{fill: 'rgba(206, 206, 206, 0.2)'}} />
-
- {chartData.map((entry, index) => (
- |
- ))}
-
-
-
-
- );
-};
-
-export default ViewsBarChart;
\ No newline at end of file
diff --git a/components/VisitsBarChart.tsx b/components/VisitsBarChart.tsx
deleted file mode 100644
index 08d2681..0000000
--- a/components/VisitsBarChart.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
-
-interface SummarizedData {
- name: string;
- value: number;
- uuid: string;
-}
-
-interface VisitsBarChartProps {
- data: SummarizedData[];
-}
-
-const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA'];
-
-const VisitsBarChart: React.FC = ({ data }) => {
- const [chartData, setChartData] = useState([]);
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- useEffect(() => {
- if (!isMounted || !data) return;
-
- try {
- // Data is already summarized. We just filter and sort it.
- const filteredData = [...data]
- .filter(item => item.value > 0)
- .sort((a, b) => b.value - a.value);
-
- setChartData(filteredData);
-
- } catch (error) {
- console.error('โ Error preparing visits bar chart data:', error);
- setChartData([]);
- }
- }, [data, isMounted]);
-
- if (!isMounted) {
- return ;
- }
-
- if (chartData.length === 0) {
- return (
-
-
No visits data to display
-
- );
- }
-
- return (
-
-
-
-
-
-
- [value.toLocaleString(), 'Visits']} cursor={{fill: 'rgba(206, 206, 206, 0.2)'}} />
-
- {chartData.map((entry, index) => (
- |
- ))}
-
-
-
-
- );
-};
-
-export default VisitsBarChart;
\ No newline at end of file
From 2e6d5e7680675c946a9768d3760e11a3dcf2415c Mon Sep 17 00:00:00 2001
From: John Bickar
Date: Wed, 3 Sep 2025 14:43:04 -0700
Subject: [PATCH 9/9] fixup! Add github default documents
---
.github/ISSUE_TEMPLATE/bug_report.md | 35 ++++++++++++++
.github/ISSUE_TEMPLATE/feature_request.md | 17 +++++++
.github/pull_request_template.md | 58 +++++++++++++++++++++++
.github/workflows/release.yml | 18 +++++++
4 files changed, 128 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
create mode 100644 .github/pull_request_template.md
create mode 100644 .github/workflows/release.yml
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..b735373
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,35 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..066b2d9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..a0453ac
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,58 @@
+# NOT READY FOR REVIEW
+- (Edit the above to reflect status)
+
+# Summary
+- TL;DR - what's this PR for?
+
+# Review By (Date)
+- When does this need to be reviewed by?
+
+# Criticality
+- How critical is this PR on a 1-10 scale? Also see [Severity Assessment](https://stanfordits.atlassian.net/browse/D8CORE-1705).
+- E.g., it affects one site, or every site and product?
+
+# Review Tasks
+
+## Setup tasks and/or behavior to test
+
+1. Check out this branch
+2. Navigate to...
+3. Verify...
+
+## Front End Validation
+- [ ] Is the markup using the appropriate semantic tags and passes HTML validation?
+- [ ] Cross-browser testing has been performed?
+- [ ] Automated accessibility scans performed?
+- [ ] Manual accessibility tests performed?
+- [ ] Design is approved by @ user?
+
+## Backend / Functional Validation
+### Code
+- [ ] Are the naming conventions following our standards?
+- [ ] Does the code have sufficient inline comments?
+- [ ] Is there anything in this code that would be hidden or hard to discover through the UI?
+- [ ] Are there any [code smells](https://blog.codinghorror.com/code-smells/)?
+- [ ] Are tests provided? eg (unit, behat, or codeception)
+
+### Code security
+- [ ] Are all [forms properly sanitized](https://www.drupal.org/docs/8/security/drupal-8-sanitizing-output)?
+- [ ] Any obvious [security flaws or new areas for attack](https://www.drupal.org/docs/8/security)?
+
+## General
+- [ ] Is there anything included in this PR that is not related to the problem it is trying to solve?
+- [ ] Is the approach to the problem appropriate?
+
+# Affected Projects or Products
+- Does this PR impact any particular projects, products, or modules?
+
+# Associated Issues and/or People
+- JIRA ticket(s)
+- Other PRs
+- Any other contextual information that might be helpful (e.g., description of a bug that this PR fixes, new functionality that it adds, etc.)
+- Anyone who should be notified? (`@mention` them here)
+
+# Resources
+- [SiteImprove](https://siteimprove.stanford.edu)
+- [Accessibility Manual Test Script](https://docs.google.com/document/d/1ZXJ9RIUNXsS674ow9j3qJ2g1OAkCjmqMXl0Gs8XHEPQ/edit?usp=sharing)
+- [HTML Validator](https://validator.w3.org/)
+- [Browserstack](https://live.browserstack.com/dashboard)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..dc42231
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,18 @@
+# .github/workflows/release.yml
+name: Release
+
+on:
+ pull_request:
+ types: closed
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Tag
+ uses: K-Phoen/semver-release-action@master
+ with:
+ release_branch: master
+ tag_format: "%major%.%minor%.%patch%"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}