293 lines
8.2 KiB
JavaScript
293 lines
8.2 KiB
JavaScript
// src/services/googleAnalyticsService.js
|
|
class GoogleAnalyticsService {
|
|
constructor() {
|
|
this.baseUrl = "/api/analytics"; // This matches your NGINX config
|
|
}
|
|
|
|
async getBasicMetrics({ startDate = "7daysAgo" } = {}) {
|
|
try {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/metrics?startDate=${startDate}`,
|
|
{
|
|
credentials: "include",
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch metrics");
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result?.data) {
|
|
throw new Error("No data received");
|
|
}
|
|
|
|
return this.processMetricsData(result.data);
|
|
} catch (error) {
|
|
console.error("Failed to fetch basic metrics:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getRealTimeBasicData() {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/realtime/basic`, {
|
|
credentials: "include",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch basic realtime data");
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result?.data) {
|
|
throw new Error("No data received");
|
|
}
|
|
|
|
const processed = this.processRealTimeBasicData(result.data);
|
|
return {
|
|
...processed,
|
|
lastUpdated: result.lastUpdated || new Date().toISOString(),
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to fetch basic realtime data:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getRealTimeDetailedData() {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/realtime/detailed`, {
|
|
credentials: "include",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch detailed realtime data");
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result?.data) {
|
|
throw new Error("No data received");
|
|
}
|
|
|
|
const processed = this.processRealTimeDetailedData(result.data);
|
|
return {
|
|
...processed,
|
|
lastUpdated: result.lastUpdated || new Date().toISOString(),
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to fetch detailed realtime data:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getUserBehavior(timeRange = "30") {
|
|
try {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/user-behavior?timeRange=${timeRange}`,
|
|
{
|
|
credentials: "include",
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to fetch user behavior");
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log("Raw user behavior response:", result);
|
|
|
|
if (!result?.success) {
|
|
throw new Error("Invalid response structure");
|
|
}
|
|
|
|
// Handle both data structures
|
|
const rawData = result.data?.data || result.data;
|
|
|
|
// Try to access the data differently based on the structure
|
|
const pageResponse = rawData?.pageResponse || rawData?.reports?.[0];
|
|
const deviceResponse = rawData?.deviceResponse || rawData?.reports?.[1];
|
|
const sourceResponse = rawData?.sourceResponse || rawData?.reports?.[2];
|
|
|
|
console.log("Extracted responses:", {
|
|
pageResponse,
|
|
deviceResponse,
|
|
sourceResponse,
|
|
});
|
|
|
|
const processed = {
|
|
success: true,
|
|
data: {
|
|
pageData: {
|
|
pageData: this.processPageData(pageResponse),
|
|
deviceData: this.processDeviceData(deviceResponse),
|
|
},
|
|
sourceData: this.processSourceData(sourceResponse),
|
|
},
|
|
};
|
|
|
|
console.log("Final processed data:", processed);
|
|
return processed;
|
|
} catch (error) {
|
|
console.error("Failed to fetch user behavior:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
processMetricsData(data) {
|
|
if (!data?.rows) {
|
|
console.log("No rows found in data");
|
|
return [];
|
|
}
|
|
|
|
return data.rows.map((row) => ({
|
|
date: row.dimensionValues[0].value,
|
|
activeUsers: parseInt(row.metricValues[0].value),
|
|
newUsers: parseInt(row.metricValues[1].value),
|
|
avgSessionDuration: parseFloat(row.metricValues[2].value),
|
|
pageViews: parseInt(row.metricValues[3].value),
|
|
bounceRate: parseFloat(row.metricValues[4].value) * 100,
|
|
conversions: parseInt(row.metricValues[5].value),
|
|
}));
|
|
}
|
|
|
|
processRealTimeBasicData(data) {
|
|
const last30MinUsers = parseInt(
|
|
data.userResponse?.rows?.[0]?.metricValues?.[0]?.value || 0
|
|
);
|
|
const last5MinUsers = parseInt(
|
|
data.fiveMinResponse?.rows?.[0]?.metricValues?.[0]?.value || 0
|
|
);
|
|
|
|
const byMinute = Array.from({ length: 30 }, (_, i) => {
|
|
const matchingRow = data.timeSeriesResponse?.rows?.find(
|
|
(row) => parseInt(row.dimensionValues[0].value) === i
|
|
);
|
|
const users = matchingRow
|
|
? parseInt(matchingRow.metricValues[0].value)
|
|
: 0;
|
|
const timestamp = new Date(Date.now() - i * 60000);
|
|
return {
|
|
minute: -i,
|
|
users,
|
|
timestamp: timestamp.toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}),
|
|
};
|
|
}).reverse();
|
|
|
|
const tokenQuota = data.quotaInfo
|
|
? {
|
|
projectHourly: data.quotaInfo.projectHourly || {},
|
|
daily: data.quotaInfo.daily || {},
|
|
serverErrors: data.quotaInfo.serverErrors || {},
|
|
thresholdedRequests: data.quotaInfo.thresholdedRequests || {},
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
last30MinUsers,
|
|
last5MinUsers,
|
|
byMinute,
|
|
tokenQuota,
|
|
};
|
|
}
|
|
|
|
processRealTimeDetailedData(data) {
|
|
return {
|
|
currentPages:
|
|
data.pageResponse?.rows?.map((row) => ({
|
|
page: row.dimensionValues[0].value,
|
|
views: parseInt(row.metricValues[0].value),
|
|
})) || [],
|
|
|
|
sources:
|
|
data.sourceResponse?.rows?.map((row) => ({
|
|
device: row.dimensionValues[0].value,
|
|
users: parseInt(row.metricValues[0].value),
|
|
})) || [],
|
|
|
|
recentEvents:
|
|
data.eventResponse?.rows
|
|
?.filter(
|
|
(row) =>
|
|
!["session_start", "(other)"].includes(
|
|
row.dimensionValues[0].value
|
|
)
|
|
)
|
|
.map((row) => ({
|
|
event: row.dimensionValues[0].value,
|
|
count: parseInt(row.metricValues[0].value),
|
|
})) || [],
|
|
};
|
|
}
|
|
|
|
processPageData(data) {
|
|
console.log("Processing page data input:", data);
|
|
if (!data?.rows) {
|
|
console.log("No rows in page data");
|
|
return [];
|
|
}
|
|
|
|
const processed = data.rows.map((row) => ({
|
|
path: row.dimensionValues[0].value || "Unknown",
|
|
pageViews: parseInt(row.metricValues[0].value || 0),
|
|
avgSessionDuration: parseFloat(row.metricValues[1].value || 0),
|
|
bounceRate: parseFloat(row.metricValues[2].value || 0) * 100,
|
|
engagedSessions: parseInt(row.metricValues[3].value || 0),
|
|
}));
|
|
console.log("Processed page data:", processed);
|
|
return processed;
|
|
}
|
|
processDeviceData(data) {
|
|
console.log("Processing device data input:", data);
|
|
if (!data?.rows) {
|
|
console.log("No rows in device data");
|
|
return [];
|
|
}
|
|
|
|
const processed = data.rows
|
|
.filter((row) => {
|
|
const device = (row.dimensionValues[0].value || "").toLowerCase();
|
|
return ["desktop", "mobile", "tablet"].includes(device);
|
|
})
|
|
.map((row) => {
|
|
const device = row.dimensionValues[0].value || "Unknown";
|
|
return {
|
|
device:
|
|
device.charAt(0).toUpperCase() + device.slice(1).toLowerCase(),
|
|
pageViews: parseInt(row.metricValues[0].value || 0),
|
|
sessions: parseInt(row.metricValues[1].value || 0),
|
|
};
|
|
})
|
|
.sort((a, b) => b.pageViews - a.pageViews);
|
|
|
|
console.log("Processed device data:", processed);
|
|
return processed;
|
|
}
|
|
processSourceData(data) {
|
|
console.log("Processing source data input:", data);
|
|
if (!data?.rows) {
|
|
console.log("No rows in source data");
|
|
return [];
|
|
}
|
|
|
|
const processed = data.rows.map((row) => ({
|
|
source: row.dimensionValues[0].value || "Unknown",
|
|
sessions: parseInt(row.metricValues[0].value || 0),
|
|
conversions: parseInt(row.metricValues[1].value || 0),
|
|
}));
|
|
console.log("Processed source data:", processed);
|
|
return processed;
|
|
}
|
|
}
|
|
// Create a single instance
|
|
const service = new GoogleAnalyticsService();
|
|
|
|
// Export both the instance and the class
|
|
export { service as googleAnalyticsService, GoogleAnalyticsService };
|