Set up drop in replacement routes for data coming from acot connection that previously came from klaviyo

This commit is contained in:
2025-05-30 09:39:20 -04:00
parent 9db11531d6
commit 21185e23cf
12 changed files with 1686 additions and 275 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback, memo } from "react";
import axios from "axios";
import { acotService } from "@/services/acotService";
import {
Card,
CardContent,
@@ -175,10 +176,8 @@ const MiniSalesChart = ({ className = "" }) => {
try {
setProjectionLoading(true);
const response = await axios.get("/api/klaviyo/events/projection", {
params: { timeRange: "last30days" }
});
setProjection(response.data);
const response = await acotService.getProjection({ timeRange: "last30days" });
setProjection(response);
} catch (error) {
console.error("Error loading projection:", error);
} finally {
@@ -191,21 +190,19 @@ const MiniSalesChart = ({ className = "" }) => {
setLoading(true);
setError(null);
const response = await axios.get("/api/klaviyo/events/stats/details", {
params: {
timeRange: "last30days",
metric: "revenue",
daily: true,
},
const response = await acotService.getStatsDetails({
timeRange: "last30days",
metric: "revenue",
daily: true,
});
if (!response.data) {
throw new Error("Invalid response format");
}
const stats = Array.isArray(response.data)
? response.data
: response.data.stats || [];
const stats = Array.isArray(response)
? response
: response.stats || [];
const processedData = processData(stats);

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback, memo } from "react";
import axios from "axios";
import { acotService } from "@/services/acotService";
import {
Card,
CardContent,
@@ -307,13 +308,11 @@ const MiniStatCards = ({
const params =
timeRange === "custom" ? { startDate, endDate } : { timeRange };
const response = await axios.get("/api/klaviyo/events/stats", {
params,
});
const response = await acotService.getStats(params);
if (!isMounted) return;
setStats(response.data.stats);
setStats(response.stats);
setLastUpdate(DateTime.now().setZone("America/New_York"));
setError(null);
} catch (error) {
@@ -345,12 +344,10 @@ const MiniStatCards = ({
setProjectionLoading(true);
const params =
timeRange === "custom" ? { startDate, endDate } : { timeRange };
const response = await axios.get("/api/klaviyo/events/projection", {
params,
});
const response = await acotService.getProjection(params);
if (!isMounted) return;
setProjection(response.data);
setProjection(response);
} catch (error) {
console.error("Error loading projection:", error);
} finally {
@@ -373,16 +370,12 @@ const MiniStatCards = ({
const interval = setInterval(async () => {
try {
const [statsResponse, projectionResponse] = await Promise.all([
axios.get("/api/klaviyo/events/stats", {
params: { timeRange: "today" },
}),
axios.get("/api/klaviyo/events/projection", {
params: { timeRange: "today" },
}),
acotService.getStats({ timeRange: "today" }),
acotService.getProjection({ timeRange: "today" }),
]);
setStats(statsResponse.data.stats);
setProjection(projectionResponse.data);
setStats(statsResponse.stats);
setProjection(projectionResponse);
setLastUpdate(DateTime.now().setZone("America/New_York"));
} catch (error) {
console.error("Error auto-refreshing stats:", error);
@@ -399,15 +392,13 @@ const MiniStatCards = ({
setDetailDataLoading((prev) => ({ ...prev, [metric]: true }));
try {
const response = await axios.get("/api/klaviyo/events/stats/details", {
params: {
timeRange: "last30days",
metric,
daily: true,
},
});
const response = await acotService.getStatsDetails({
timeRange: "last30days",
metric,
daily: true,
});
setDetailData((prev) => ({ ...prev, [metric]: response.data.stats }));
setDetailData((prev) => ({ ...prev, [metric]: response.stats }));
} catch (error) {
console.error(`Error fetching detail data for ${metric}:`, error);
} finally {
@@ -424,13 +415,23 @@ const MiniStatCards = ({
}
}, [selectedMetric, fetchDetailData]);
// Add preload effect
// Add preload effect with throttling
useEffect(() => {
// Preload all detail data when component mounts
const metrics = ["revenue", "orders", "average_order", "shipping"];
metrics.forEach((metric) => {
fetchDetailData(metric);
});
// Preload detail data with throttling to avoid overwhelming the server
const preloadData = async () => {
const metrics = ["revenue", "orders", "average_order", "shipping"];
for (const metric of metrics) {
try {
await fetchDetailData(metric);
// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 25));
} catch (error) {
console.error(`Error preloading ${metric}:`, error);
}
}
};
preloadData();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (loading && !stats) {

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import { acotService } from "@/services/acotService";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Loader2, ArrowUpDown, AlertCircle, Package, Settings2, Search, X } from "lucide-react";
@@ -57,10 +58,8 @@ const ProductGrid = ({
setLoading(true);
setError(null);
const response = await axios.get("/api/klaviyo/events/products", {
params: { timeRange: selectedTimeRange },
});
setProducts(response.data.stats.products.list || []);
const response = await acotService.getProducts({ timeRange: selectedTimeRange });
setProducts(response.stats.products.list || []);
} catch (error) {
console.error("Error fetching products:", error);
setError(error.message);

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useMemo, useCallback, memo } from "react";
import axios from "axios";
import { acotService } from "@/services/acotService";
import {
Card,
CardContent,
@@ -550,10 +551,8 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
try {
setProjectionLoading(true);
const response = await axios.get("/api/klaviyo/events/projection", {
params,
});
setProjection(response.data);
const response = await acotService.getProjection(params);
setProjection(response);
} catch (error) {
console.error("Error loading projection:", error);
} finally {
@@ -568,22 +567,20 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
setError(null);
// Fetch data
const response = await axios.get("/api/klaviyo/events/stats/details", {
params: {
...params,
metric: "revenue",
daily: true,
},
const response = await acotService.getStatsDetails({
...params,
metric: "revenue",
daily: true,
});
if (!response.data) {
if (!response.stats) {
throw new Error("Invalid response format");
}
// Process the data
const currentStats = Array.isArray(response.data)
? response.data
: response.data.stats || [];
const currentStats = Array.isArray(response.stats)
? response.stats
: [];
// Process the data directly without remapping
const processedData = processData(currentStats);
@@ -614,20 +611,15 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
[fetchData]
);
// Initial load effect
useEffect(() => {
fetchData({ timeRange: selectedTimeRange });
}, [selectedTimeRange, fetchData]);
// Auto-refresh effect for 'today' view
// Initial load and auto-refresh effect
useEffect(() => {
let intervalId = null;
if (selectedTimeRange === "today") {
// Initial fetch
fetchData({ timeRange: "today" });
// Initial fetch
fetchData({ timeRange: selectedTimeRange });
// Set up interval
// Set up auto-refresh only for 'today' view
if (selectedTimeRange === "today") {
intervalId = setInterval(() => {
fetchData({ timeRange: "today" });
}, 60000);

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, useCallback, Suspense, memo } from "react";
import axios from "axios";
import { acotService } from "@/services/acotService";
import {
Card,
CardContent,
@@ -1415,10 +1416,8 @@ const StatCards = ({
// For metrics that need the full stats
if (["shipping", "brands_categories"].includes(metric)) {
const response = await axios.get("/api/klaviyo/events/stats", {
params,
});
const data = [response.data.stats];
const response = await acotService.getStats(params);
const data = [response.stats];
setCacheData(detailTimeRange, metric, data);
setDetailData((prev) => ({ ...prev, [metric]: data }));
setError(null);
@@ -1427,16 +1426,11 @@ const StatCards = ({
// For order types (pre_orders, local_pickup, on_hold)
if (["pre_orders", "local_pickup", "on_hold"].includes(metric)) {
const response = await axios.get(
"/api/klaviyo/events/stats/details",
{
params: {
...params,
orderType: orderType,
},
}
);
const data = response.data.stats;
const response = await acotService.getStatsDetails({
...params,
orderType: orderType,
});
const data = response.stats;
setCacheData(detailTimeRange, metric, data);
setDetailData((prev) => ({ ...prev, [metric]: data }));
setError(null);
@@ -1445,17 +1439,12 @@ const StatCards = ({
// For refunds and cancellations
if (["refunds", "cancellations"].includes(metric)) {
const response = await axios.get(
"/api/klaviyo/events/stats/details",
{
params: {
...params,
eventType:
metric === "refunds" ? "PAYMENT_REFUNDED" : "CANCELED_ORDER",
},
}
);
const data = response.data.stats;
const response = await acotService.getStatsDetails({
...params,
eventType:
metric === "refunds" ? "PAYMENT_REFUNDED" : "CANCELED_ORDER",
});
const data = response.stats;
// Transform the data to match the expected format
const transformedData = data.map((day) => ({
@@ -1487,16 +1476,11 @@ const StatCards = ({
// For order range
if (metric === "order_range") {
const response = await axios.get(
"/api/klaviyo/events/stats/details",
{
params: {
...params,
eventType: "PLACED_ORDER",
},
}
);
const data = response.data.stats;
const response = await acotService.getStatsDetails({
...params,
eventType: "PLACED_ORDER",
});
const data = response.stats;
console.log("Fetched order range data:", data);
setCacheData(detailTimeRange, metric, data);
setDetailData((prev) => ({ ...prev, [metric]: data }));
@@ -1505,10 +1489,8 @@ const StatCards = ({
}
// For all other metrics
const response = await axios.get("/api/klaviyo/events/stats/details", {
params,
});
const data = response.data.stats;
const response = await acotService.getStatsDetails(params);
const data = response.stats;
setCacheData(detailTimeRange, metric, data);
setDetailData((prev) => ({ ...prev, [metric]: data }));
setError(null);
@@ -1531,8 +1513,8 @@ const StatCards = ({
]
);
// Corrected preloadDetailData function
const preloadDetailData = useCallback(() => {
// Throttled preloadDetailData function to avoid overwhelming the server
const preloadDetailData = useCallback(async () => {
const metrics = [
"revenue",
"orders",
@@ -1545,11 +1527,22 @@ const StatCards = ({
"on_hold",
];
return Promise.all(
metrics.map((metric) => fetchDetailData(metric, metric))
).catch((error) => {
console.error("Error during detail data preload:", error);
});
// Process metrics in batches of 3 to avoid overwhelming the connection pool
const batchSize = 3;
for (let i = 0; i < metrics.length; i += batchSize) {
const batch = metrics.slice(i, i + batchSize);
try {
await Promise.all(
batch.map((metric) => fetchDetailData(metric, metric))
);
// Small delay between batches to prevent overwhelming the server
if (i + batchSize < metrics.length) {
await new Promise(resolve => setTimeout(resolve, 50));
}
} catch (error) {
console.error(`Error during detail data preload batch ${i / batchSize + 1}:`, error);
}
}
}, [fetchDetailData]);
// Move trend calculation functions inside the component
@@ -1630,14 +1623,12 @@ const StatCards = ({
const params =
timeRange === "custom" ? { startDate, endDate } : { timeRange };
const response = await axios.get("/api/klaviyo/events/stats", {
params,
});
const response = await acotService.getStats(params);
if (!isMounted) return;
setDateRange(response.data.timeRange);
setStats(response.data.stats);
setDateRange(response.timeRange);
setStats(response.stats);
setLastUpdate(DateTime.now().setZone("America/New_York"));
setError(null);
@@ -1674,12 +1665,10 @@ const StatCards = ({
const params =
timeRange === "custom" ? { startDate, endDate } : { timeRange };
const response = await axios.get("/api/klaviyo/events/projection", {
params,
});
const response = await acotService.getProjection(params);
if (!isMounted) return;
setProjection(response.data);
setProjection(response);
} catch (error) {
console.error("Error loading projection:", error);
} finally {
@@ -1702,16 +1691,12 @@ const StatCards = ({
const interval = setInterval(async () => {
try {
const [statsResponse, projectionResponse] = await Promise.all([
axios.get("/api/klaviyo/events/stats", {
params: { timeRange: "today" },
}),
axios.get("/api/klaviyo/events/projection", {
params: { timeRange: "today" },
}),
acotService.getStats({ timeRange: "today" }),
acotService.getProjection({ timeRange: "today" }),
]);
setStats(statsResponse.data.stats);
setProjection(projectionResponse.data);
setStats(statsResponse.stats);
setProjection(projectionResponse);
setLastUpdate(DateTime.now().setZone("America/New_York"));
} catch (error) {
console.error("Error auto-refreshing stats:", error);

View File

@@ -0,0 +1,176 @@
import axios from 'axios';
// Use the proxy in development, direct URL in production
const ACOT_BASE_URL = process.env.NODE_ENV === 'development'
? '' // Use proxy in development (which now points to production)
: (process.env.REACT_APP_ACOT_API_URL || 'https://dashboard.kent.pw');
const acotApi = axios.create({
baseURL: ACOT_BASE_URL,
timeout: 30000,
});
// Request deduplication cache
const requestCache = new Map();
// Periodic cache cleanup (every 5 minutes)
setInterval(() => {
const now = Date.now();
const maxAge = 5 * 60 * 1000; // 5 minutes
for (const [key, value] of requestCache.entries()) {
if (value.timestamp && now - value.timestamp > maxAge) {
requestCache.delete(key);
}
}
if (requestCache.size > 0) {
console.log(`[ACOT API] Cache cleanup: ${requestCache.size} entries remaining`);
}
}, 5 * 60 * 1000);
// Retry function for timeout errors
const retryRequest = async (requestFn, maxRetries = 2, delay = 1000) => {
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
return await requestFn();
} catch (error) {
const isTimeout = error.code === 'ECONNABORTED' || error.message.includes('timeout');
const isLastAttempt = attempt === maxRetries + 1;
if (isTimeout && !isLastAttempt) {
console.log(`[ACOT API] Timeout on attempt ${attempt}, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 1.5; // Exponential backoff
continue;
}
throw error;
}
}
};
// Request deduplication function
const deduplicatedRequest = async (cacheKey, requestFn, cacheDuration = 5000) => {
// Check if we have a pending request for this key
if (requestCache.has(cacheKey)) {
const cached = requestCache.get(cacheKey);
// If it's a pending promise, return it
if (cached.promise) {
console.log(`[ACOT API] Deduplicating request: ${cacheKey}`);
return cached.promise;
}
// If it's cached data and still fresh, return it
if (cached.data && Date.now() - cached.timestamp < cacheDuration) {
console.log(`[ACOT API] Using cached data: ${cacheKey}`);
return cached.data;
}
}
// Create new request
const promise = requestFn().then(data => {
// Cache the result
requestCache.set(cacheKey, {
data,
timestamp: Date.now(),
promise: null
});
return data;
}).catch(error => {
// Remove from cache on error
requestCache.delete(cacheKey);
throw error;
});
// Cache the promise while it's pending
requestCache.set(cacheKey, {
promise,
timestamp: Date.now(),
data: null
});
return promise;
};
// Add request interceptor for logging
acotApi.interceptors.request.use(
(config) => {
console.log(`[ACOT API] ${config.method?.toUpperCase()} ${config.url}`, config.params);
return config;
},
(error) => {
console.error('[ACOT API] Request error:', error);
return Promise.reject(error);
}
);
// Add response interceptor for logging
acotApi.interceptors.response.use(
(response) => {
console.log(`[ACOT API] Response ${response.status}:`, response.data);
return response;
},
(error) => {
console.error('[ACOT API] Response error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
// Cleanup function to clear cache
const clearCache = () => {
requestCache.clear();
console.log('[ACOT API] Request cache cleared');
};
export const acotService = {
// Get main stats - replaces klaviyo events/stats
getStats: async (params) => {
const cacheKey = `stats_${JSON.stringify(params)}`;
return deduplicatedRequest(cacheKey, () =>
retryRequest(async () => {
const response = await acotApi.get('/api/acot/events/stats', { params });
return response.data;
})
);
},
// Get detailed stats - replaces klaviyo events/stats/details
getStatsDetails: async (params) => {
const cacheKey = `details_${JSON.stringify(params)}`;
return deduplicatedRequest(cacheKey, () =>
retryRequest(async () => {
const response = await acotApi.get('/api/acot/events/stats/details', { params });
return response.data;
})
);
},
// Get products data - replaces klaviyo events/products
getProducts: async (params) => {
const cacheKey = `products_${JSON.stringify(params)}`;
return deduplicatedRequest(cacheKey, () =>
retryRequest(async () => {
const response = await acotApi.get('/api/acot/events/products', { params });
return response.data;
})
);
},
// Get projections - replaces klaviyo events/projection
getProjection: async (params) => {
const cacheKey = `projection_${JSON.stringify(params)}`;
return deduplicatedRequest(cacheKey, () =>
retryRequest(async () => {
const response = await acotApi.get('/api/acot/events/projection', { params });
return response.data;
})
);
},
// Utility functions
clearCache,
};
export default acotService;

View File

@@ -31,6 +31,42 @@ export default defineConfig(({ mode }) => {
host: "0.0.0.0",
port: 3000,
proxy: {
"/api/acot": {
target: "https://dashboard.kent.pw",
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/api\/acot/, "/api/acot"),
configure: (proxy, _options) => {
proxy.on("error", (err, req, res) => {
console.error("ACOT proxy error:", err);
res.writeHead(500, {
"Content-Type": "application/json",
});
res.end(
JSON.stringify({
error: "Proxy Error",
message: err.message,
details: err.stack
})
);
});
proxy.on("proxyReq", (proxyReq, req, _res) => {
console.log("Outgoing ACOT request:", {
method: req.method,
url: req.url,
path: proxyReq.path,
headers: proxyReq.getHeaders(),
});
});
proxy.on("proxyRes", (proxyRes, req, _res) => {
console.log("ACOT proxy response:", {
statusCode: proxyRes.statusCode,
url: req.url,
headers: proxyRes.headers,
});
});
},
},
"/api/klaviyo": {
target: "https://dashboard.kent.pw",
changeOrigin: true,
@@ -203,42 +239,6 @@ export default defineConfig(({ mode }) => {
});
});
},
},
"/api/acot": {
target: "https://dashboard.kent.pw",
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api\/acot/, "/api/acot"),
configure: (proxy, _options) => {
proxy.on("error", (err, req, res) => {
console.error("ACOT proxy error:", err);
res.writeHead(500, {
"Content-Type": "application/json",
});
res.end(
JSON.stringify({
error: "Proxy Error",
message: err.message,
details: err.stack
})
);
});
proxy.on("proxyReq", (proxyReq, req, _res) => {
console.log("Outgoing ACOT request:", {
method: req.method,
url: req.url,
path: proxyReq.path,
headers: proxyReq.getHeaders(),
});
});
proxy.on("proxyRes", (proxyRes, req, _res) => {
console.log("ACOT proxy response:", {
statusCode: proxyRes.statusCode,
url: req.url,
headers: proxyRes.headers,
});
});
},
}
},
},