Analytics VTO Page
Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 31/10/2025
Overview
The Analytics VTO page provides comprehensive analytics and reporting for Virtual Try-On (VTO) features across multiple channels. It's a sophisticated data aggregation and visualization layer with:
- Multi-level filtering (service types, glass models, variants, date ranges, multiple catalogues)
- Intelligent caching with deduplication and concurrent fetch prevention
- Firebase consumption tracking (shows data read costs)
- Dynamic data enrichment with subcollections
- PDF report generation with selective sections (Overview, Device Analytics, Variants Details)
- Three visualization components (Overview card, Catalogue time focus table, Device analytics)
Key characteristics:
- Smart default date range (last 7 days)
- Multi-catalogue support (single or bulk selection)
- Service type filtering (APP, 3D VIEWER, WEB VTO, or ALL)
- Glass model & variant drill-down (extracts specific product data)
- Non-blocking Firestore enrichment (graceful fallback on errors)
- Ref-based fetch deduplication (prevents duplicate requests)
- Responsive UI (different layouts for mobile/tablet)
Location: /src/pages/Analytics.js
Architecture Overview
Analytics VTO Page
├─ Redux Layer
│ ├─ filterOptions (glass, variant)
│ ├─ selectedCatalog (single)
│ ├─ selectedCatalogs (multiple)
│ ├─ glassList (available glass models)
│ └─ listaCatalogues (all catalogues)
│
├─ State Management (Local)
│ ├─ dateRange [startDate, endDate]
│ ├─ typeData (service types: "all" | ["app", "3d", "WebVto"])
│ ├─ aggregateList (raw Firebase data)
│ ├─ filteredAggList (filtered for UI)
│ ├─ loading, downloadLoading states
│ └─ reportData (for report generation)
│
├─ Caching Layer
│ ├─ aggregateCache (Map with cacheKey → data)
│ ├─ ongoingFetches (Map with cacheKey → Promise)
│ ├─ snapshotRef (Firestore read tracker)
│ └─ Cache invalidation logic
│
├─ Data Pipeline
│ ├─ Fetch aggregated data from Firebase
│ ├─ Enrich with subcollections (parallel)
│ ├─ Apply multi-level filters
│ └─ Update filtered list
│
├─ UI Sections
│ ├─ Header (with "Create your report" button)
│ ├─ Filters (Filters component)
│ ├─ Overview Card
│ ├─ Catalogue Time Focus Table
│ ├─ Device Analytics
│ └─ Report Modal (date range, catalogues, sections)
│
└─ Report Generation
├─ Modal for report configuration
├─ Catalogue multi-select
├─ Date range validation
├─ Section selection (checkboxes)
└─ PDF download via API
State Management & Redux Integration
Redux Selectors
const filterOptions = useSelector((state) => state.analytics.filterOptions);
const { glass, variant } = filterOptions; // Currently selected filters
const selectedCatalog = useSelector(
(state) => state.catalogues.selectedCatalog
);
const selectedCatalogs = useSelector(
(state) => state.catalogues.selectedCatalogs || []
);
const glassList = useSelector((state) => state.analytics.glassList);
const catalogsList = useSelector((state) => state.catalogues.listaCatalogues);
State structure:
// analytics.filterOptions
{
glass: "product-id-123" | "", // Selected glass model (product reference)
variant: "variant-id-456" | "" // Selected variant (variant reference)
}
// catalogues.selectedCatalog
{
id: string,
nameCatalog: string
}
// catalogues.selectedCatalogs (array for multi-select)
[
{ id: "cat-1", nameCatalog: "Catalogue 1" },
{ id: "cat-2", nameCatalog: "Catalogue 2" },
// or
[{ id: "all", nameCatalog: "All Catalogues" }] // Special "all" selector
]
// analytics.glassList
[
{ id: "glass-1", nome_modello: "Model A" },
{ id: "glass-2", nome_modello: "Model B" },
...
]
// catalogues.listaCatalogues (all available catalogues)
["cat-1", "cat-2", "cat-3", ...] // Array of catalogue IDs
Redux Actions Used
import { setAnalyticsFilterOptions } from "../redux/analytics/actions";
import {
setSelectedCatalogue,
setSelectedCatalogues,
} from "../redux/Catalogues/actions";
// Usage:
dispatch(setAnalyticsFilterOptions({ glass: "", variant: "" }));
dispatch(setSelectedCatalogue({ id: "all", nameCatalog: "All Catalogues" }));
dispatch(setSelectedCatalogues([{ id: "all", nameCatalog: "All Catalogues" }]));
Local Component State
const [loading, setLoading] = useState(false); // Fetch loading
const [downloadLoading, setDownloadLoading] = useState(false); // Report download
const [dateRange, setDateRange] = useState(defaultDateRange); // 7 days ago to today
const [typeData, setTypeData] = useState("all"); // Service type filter
const [aggregateList, setAggregateList] = useState([]); // Raw Firebase data
const [filteredAggList, setFilteredAggList] = useState([]); // Filtered data for UI
const [overviewData, setOverviewData] = useState(); // Overview metrics
const [reportData, setReportData] = useState({}); // Report metadata
const [selectedValue, setSelectedValue] = useState(null); // Filter UI state
Default Date Range
const endDate = dayjs().endOf("day"); // Today at 23:59:59
const startDate = dayjs().subtract(7, "day").startOf("day"); // 7 days ago at 00:00:00
const defaultDateRange = [startDate, endDate];
Format: [dayjs object, dayjs object]
Caching System (Sophisticated)
Problem Statement
- Multiple users might select same date range + catalogues simultaneously
- Rapidly switching between date ranges causes redundant Firebase calls
- Without deduplication, identical requests fire multiple times
Solution: Triple-Layer Caching
Layer 1: Cache Key Generation
const catalogIdsKey = [...allCatalogIds].sort().join("-"); // "cat-1-cat-2-cat-3"
const cacheKey = `${catalogIdsKey}-${dateRange[0].format(
"YYYY-MM-DD"
)}-${dateRange[1].format("YYYY-MM-DD")}`;
// Result: "cat-1-cat-2-cat-3-2025-10-22-2025-10-29"
Guarantees consistency: Same catalogues + dates always produce same key (sorted to prevent "cat-1-cat-2" vs "cat-2-cat-1")
Layer 2: Check Persistent Cache
if (aggregateCache.current.has(cacheKey)) {
console.log(`📦 Using cached data (key: ${cacheKey.substring(0, 50)}...)`);
setAggregateList(aggregateCache.current.get(cacheKey));
setLoading(false);
return; // No Firebase call needed
}
Layer 3: Check In-Flight Requests
if (ongoingFetches.current.has(cacheKey)) {
console.log(
`⏳ Fetch already in progress, waiting... (key: ${cacheKey.substring(
0,
50
)}...)`
);
const data = await ongoingFetches.current.get(cacheKey); // Await same Promise
setAggregateList(data);
setLoading(false);
return;
}
Benefits: If two components request same data simultaneously, the second waits for the first's Promise
Full Fetch Flow with Caching
const fetchAggregate = async () => {
let cacheKey = null;
try {
// 1. Take Firestore consumption snapshot
snapshotRef.current = tracker.snapshot();
setLoading(true);
// 2. Determine which catalogues to fetch
let allCatalogIds;
if (selectedCatalogs.length === 1 && selectedCatalogs[0].id === "all") {
// If "All Catalogues" is selected, use full catalogsList
allCatalogIds = catalogsList || [];
} else {
// Otherwise, use explicitly selected catalogue IDs
allCatalogIds = selectedCatalogs.map((catalog) => catalog.id);
}
if (allCatalogIds.length === 0) {
setAggregateList([]);
setLoading(false);
return;
}
// 3. Generate cache key (sorted for consistency)
const catalogIdsKey = [...allCatalogIds].sort().join("-");
cacheKey = `${catalogIdsKey}-${dateRange[0].format(
"YYYY-MM-DD"
)}-${dateRange[1].format("YYYY-MM-DD")}`;
// 4. Check persistent cache
if (aggregateCache.current.has(cacheKey)) {
setAggregateList(aggregateCache.current.get(cacheKey));
setLoading(false);
return;
}
// 5. Check for in-flight requests (deduplication)
if (ongoingFetches.current.has(cacheKey)) {
const data = await ongoingFetches.current.get(cacheKey);
setAggregateList(data);
setLoading(false);
return;
}
// 6. Format dates for Firebase query
const startDate = dateRange[0].format("YYYY/MM/DD");
const endDate = dateRange[1].format("YYYY/MM/DD");
console.log(
`🔥 Firebase call in progress (key: ${cacheKey.substring(
0,
50
)}...)`
);
// 7. Create fetch Promise with enrichment
const fetchPromise = (async () => {
// Fetch raw aggregate data
const aggregateData = await getAggregatedData(
allCatalogIds,
startDate,
endDate
);
if (!aggregateData || aggregateData.length === 0) {
return [];
}
// Store raw data in cache
aggregateCache.current.set(cacheKey, aggregateData);
// Attempt enrichment (non-blocking)
try {
const enrichedData = await enrichDataWithSubcollections(
aggregateData
);
aggregateCache.current.set(cacheKey, enrichedData); // Update cache with enriched
return enrichedData;
} catch (enrichError) {
console.error("Error enriching data:", enrichError);
return aggregateData; // Fallback to raw if enrichment fails
}
})();
// 8. Register Promise to prevent concurrent fetches
ongoingFetches.current.set(cacheKey, fetchPromise);
// 9. Await and set data
try {
const finalData = await fetchPromise;
setAggregateList(finalData);
} catch (error) {
messageApi.error("Error retrieving aggregate data");
setAggregateList([]);
}
} finally {
// 10. Clean up in-flight tracking
if (cacheKey) {
ongoingFetches.current.delete(cacheKey);
}
setLoading(false);
// 11. Show Firestore consumption diff
if (snapshotRef.current) {
tracker.showDiff(
snapshotRef.current,
"Firebase Consumption - Analytics"
);
}
}
};
Firebase Consumption Tracking
Purpose
Monitor and display Firestore read/write costs during analytics operations
Implementation
import tracker from "../services/firebase/firebaseTracker";
// On component mount: start tracking session
useEffect(() => {
tracker.start("Analytics");
return () => {
tracker.end();
};
}, []);
// Before each fetch: capture snapshot
snapshotRef.current = tracker.snapshot();
// After fetch completes: show consumption diff
tracker.showDiff(snapshotRef.current, "Firebase Consumption - Analytics");
Output example:
Firebase Consumption - Analytics
─────────────────────────────────
Reads before: 125
Reads after: 145
Diff: +20 reads
Writes before: 5
Writes after: 5
Diff: +0 writes
Multi-Level Filtering System
Filter 1: Service Type Filter
Input: Radio/checkbox selection
const [typeData, setTypeData] = useState("all");
// User selects service type
const handleServiceChanged = (value) => {
setTypeData(value);
};
Possible values:
"all"(string, legacy support)["all"](array including "all")["app"](single service)["app", "3d", "WebVto"](multiple services)
Conversion function:
const getServiceType = (service) => {
switch (service) {
case "app":
return "APP";
case "3d":
return "3D VIEWER";
case "WebVto":
return "WEB VTO";
case "all":
return "ALL";
default:
return "";
}
};
Why: Firebase stores "APP", "3D VIEWER", "WEB VTO" (uppercase with spaces), so we convert user input
Filter 2: Glass Model Filter
Input: Redux state
const { glass, variant } = useSelector(
(state) => state.analytics.filterOptions
);
When glass is selected:
- Filters
aggregateListto only sessions containing the selected glass product - Extracts metrics for that specific glass from
list_productsarray - Updates session properties dynamically:
if (value.productRef === glass) {
session.avgEngagementTime = getMinSecObject(value.averageEngagementTime);
session.avgSessionTime = getMinSecObject(value.averageSessionTime);
session.averageSessionsPerUser = value.averageSessionsPerUser;
session.newUsersCount = value.newUsersCount;
session.totalSessions = value.numberOfSessions;
session.totalUniqueUsers = value.numberOfUniqueUsers;
}
Filter 3: Variant Filter
Input: Redux state
const { variant } = useSelector((state) => state.analytics.filterOptions);
When variant is selected:
- Filters
aggregateListto only sessions containing the selected variant - Extracts metrics from
list_variantsarray - Updates session properties the same way as glass filter
Filter 4: Date Range Filter
Input: DatePicker component
const [dateRange, setDateRange] = useState(defaultDateRange);
const onChangeDate = (dates) => {
if (dates) {
setDateRange([dates[0], dates[1]]);
} else {
setDateRange(defaultDateRange);
}
};
Used in: Cache key generation, Firebase query, report date range
Filter 5: Catalogue Filter
Input: Redux multi-select
const selectedCatalogs = useSelector(
(state) => state.catalogues.selectedCatalogs || []
);
Logic:
let allCatalogIds;
if (selectedCatalogs.length === 1 && selectedCatalogs[0].id === "all") {
// "All Catalogues" selected → use full catalogsList
allCatalogIds = catalogsList || [];
} else {
// Specific catalogues selected → use their IDs
allCatalogIds = selectedCatalogs.map((catalog) => catalog.id);
}
Combined Filter Application
Effect that applies all filters:
useEffect(() => {
let filteredAggs =
aggregateList?.filter((session) => {
// 1. SERVICE TYPE FILTER
if (Array.isArray(typeData)) {
if (typeData.includes("all")) {
// 'all' included → don't filter by service
} else {
// Check if session service matches any selected
const serviceMatches = typeData.some(
(service) => session.service === getServiceType(service)
);
if (!serviceMatches) return false; // Exclude session
}
} else {
// Backward compatibility: typeData is string
if (
typeData !== "all" &&
session.service !== getServiceType(typeData)
) {
return false;
}
}
let result = true;
// 2. GLASS MODEL FILTER
const gData = session.list_products;
if (
!!glass &&
gData &&
!gData.some((value) => {
if (value.productRef === glass) {
// Extract metrics for selected glass
session.avgEngagementTime = getMinSecObject(
value.averageEngagementTime
);
session.avgSessionTime = getMinSecObject(
value.averageSessionTime
);
session.averageSessionsPerUser =
value.averageSessionsPerUser;
session.newUsersCount = value.newUsersCount;
session.totalSessions = value.numberOfSessions;
session.totalUniqueUsers = value.numberOfUniqueUsers;
}
return value.productRef === glass;
})
) {
result = false; // No matching glass product
}
// 3. VARIANT FILTER
const vData = session.list_variants;
if (
!!variant &&
vData &&
!vData.some((value) => {
if (value.variantRef === variant) {
// Extract metrics for selected variant
session.avgEngagementTime = getMinSecObject(
value.averageEngagementTime
);
session.avgSessionTime = getMinSecObject(
value.averageSessionTime
);
session.averageSessionsPerUser =
value.averageSessionsPerUser;
session.newUsersCount = value.newUsersCount;
session.totalSessions = value.numberOfSessions;
session.totalUniqueUsers = value.numberOfUniqueUsers;
}
return value.variantRef === variant;
})
) {
result = false; // No matching variant
}
return result;
}) ?? [];
// 4. Add reference arrays for drill-down
filteredAggs =
filteredAggs.map((session) => {
const updatedSession = { ...session };
if (!!glass) {
updatedSession.productRefs = [glass]; // For detail views
}
if (!!variant) {
updatedSession.variantRefs = [variant];
}
return updatedSession;
}) ?? [];
setFilteredAggList(filteredAggs);
}, [glass, variant, typeData, aggregateList]);
Dependency array: Triggers whenever any filter changes OR new aggregateList loaded
Data Enrichment Pipeline
Enrichment Purpose
Raw Firebase data lacks nested subcollection details. Enrichment fetches:
- Product details (name, brand, etc.)
- Variant details
- User information
- Session metrics breakdown
Enrichment Process
// In fetchAggregate:
try {
const enrichedData = await enrichDataWithSubcollections(aggregateData);
aggregateCache.current.set(cacheKey, enrichedData);
return enrichedData;
} catch (enrichError) {
console.error("Error enriching data:", enrichError);
return aggregateData; // Fallback to raw
}
Non-blocking design: If enrichment fails, UI still displays raw data
Report Generation Modal
Modal States
const [showModal, setShowModal] = useState(false);
const [catalogProfileData, setCatalogProfileData] = useState([]);
const [allCatalogsSelected, setAllCatalogsSelected] = useState(false);
const [reportSelectedCatalogs, setReportSelectedCatalogs] = useState([]);
const [startDateReport, setStartDateReport] = useState("");
const [endDateReport, setEndDateReport] = useState("");
const [error, setError] = useState("");
const [reportSections, setReportSections] = useState({
overview: false,
deviceAnalytics: false,
variantsDetails: false,
});
Modal Load Workflow
Triggers when: showModal state becomes true
useEffect(() => {
if (showModal) {
// 1. Fetch catalogue data from Firestore
const loadCatalogData = async () => {
try {
if (!catalogsList || catalogsList.length === 0) {
setCatalogProfileData([]);
return;
}
// Fetch full details for each catalogue
const promises = catalogsList.map(async (id) => {
try {
const docRef = await getDoc(doc(db, "Catalogues", id));
if (docRef.exists()) {
const catalogData = docRef.data();
// Filter: exclude Gateway catalogues
if (catalogData.isGatewayCatalog === true) {
return null;
}
return {
id: docRef.id,
nome_brand:
catalogData.nameCatalog ||
"Unnamed Catalog",
};
}
return null;
} catch (error) {
return null; // Silently skip failures
}
});
const results = await Promise.all(promises);
const validCatalogs = results
.filter(Boolean) // Remove nulls
.sort((a, b) =>
(a.nome_brand || "").localeCompare(b.nome_brand || "")
);
setCatalogProfileData(validCatalogs);
// 2. Pre-select all catalogues
const allCatalogIds = validCatalogs.map((cat) => cat.id);
setReportSelectedCatalogs(allCatalogIds);
setAllCatalogsSelected(true);
} catch (error) {
messageApi.error("Error loading catalogs data");
setCatalogProfileData([]);
}
};
loadCatalogData();
// 3. Initialize date range from current UI
setStartDateReport(dateRange[0].format("YYYY-MM-DD"));
setEndDateReport(dateRange[1].format("YYYY-MM-DD"));
// 4. Initialize report sections (all selected by default)
setReportSections({
overview: true,
variantsDetails: true,
deviceAnalytics: true,
});
}
}, [showModal, catalogsList, dateRange]);
Gateway Catalogue Filtering
// Exclude Gateway catalogues from report (VTO-specific analytics only)
if (catalogData.isGatewayCatalog === true) {
return null; // Skip this catalogue
}
Date Validation
Start Date:
const handleStartDateChange = (date) => {
if (!date) {
setStartDateReport(null);
setError("");
return;
}
const value = date.format("YYYY-MM-DD");
if (endDateReport && dayjs(value).isAfter(dayjs(endDateReport))) {
setError("Start date cannot be later than end date.");
} else {
setStartDateReport(value);
setError("");
}
};
End Date:
const handleEndDateChange = (date) => {
if (!date) {
setEndDateReport(null);
setError("");
return;
}
const value = date.format("YYYY-MM-DD");
if (startDateReport && dayjs(value).isBefore(dayjs(startDateReport))) {
setError("End date cannot be earlier than start date.");
} else {
setEndDateReport(value);
setError("");
}
};
Report Sections Selection
Handler:
const handleReportSectionChange = (e) => {
const { name, checked } = e.target;
if (name === "all") {
// Toggle all sections
setReportSections({
overview: checked,
variantsDetails: checked,
deviceAnalytics: checked,
});
} else {
// Toggle specific section
setReportSections((prev) => ({
...prev,
[name]: checked,
}));
}
};
Validation:
const isAllSectionsSelected = () => {
return Object.values(reportSections).every((value) => value === true);
};
const isAnySectionSelected = () => {
return Object.values(reportSections).some((value) => value === true);
};
const isFormValid = () => {
return startDateReport && endDateReport && !error && isAnySectionSelected();
};
Download Handler
Workflow:
const handleDownload = async () => {
setDownloadLoading(true);
try {
const startDate = startDateReport;
const endDate = endDateReport;
const selectedCatalogues = reportSelectedCatalogs;
// 1. Format dates for Firebase (with startOf/endOf for consistency)
const formattedStartDate = dayjs(startDate)
.startOf("day")
.format("YYYY/MM/DD");
const formattedEndDate = dayjs(endDate)
.endOf("day")
.format("YYYY/MM/DD");
// 2. Determine catalogues to include
let cataloguesToUse;
if (
selectedCatalogues.length === 0 ||
(selectedCatalogues.length === catalogProfileData.length &&
allCatalogsSelected)
) {
// All catalogues → use full catalogsList
cataloguesToUse = catalogsList || [];
} else {
// Specific selection → use selected IDs
cataloguesToUse = selectedCatalogues;
}
// 3. Fetch aggregate data with formatted dates
const aggregateData = await getAggregatedData(
cataloguesToUse,
formattedStartDate,
formattedEndDate
);
// 4. Enrich with subcollections (non-blocking)
let enrichedData = aggregateData;
try {
enrichedData = await enrichDataWithSubcollections(aggregateData);
} catch (enrichError) {
console.error("Error enriching data:", enrichError);
// Continue with raw data
}
// 5. Generate report with selected sections
const success = await generateReportWithSections(
startDate,
endDate,
selectedCatalogues,
enrichedData,
reportSections // Pass which sections to include
);
if (success) {
messageApi.success("Report successfully generated!");
}
} catch (error) {
console.error("Error during report download:", error);
messageApi.error("Error generating the report.");
} finally {
setDownloadLoading(false);
setShowModal(false); // Close modal after download
}
};
Filter Clear Handlers
Handler 1: Clear All Filters + Dates
const handleClearAllFilters = () => {
// 1. Reset Redux filter options
dispatch(
setAnalyticsFilterOptions({
glass: "",
variant: "",
})
);
// 2. Reset date range to default
setDateRange(defaultDateRange);
// 3. Reset service type
setTypeData(["all"]);
setSelectedValue(null);
// 4. Reset catalogues to "All"
const allCatalog = { id: "all", nameCatalog: "All Catalogues" };
dispatch(setSelectedCatalogue(allCatalog));
dispatch(setSelectedCatalogues([allCatalog]));
// 5. Force refresh
setRefresh((r) => !r);
};
Handler 2: Clear Filters but Keep Dates
const handleClearFilter = () => {
// Same as above but don't reset dateRange or serve as alternative lighter reset
dispatch(
setAnalyticsFilterOptions({
glass: "",
variant: "",
})
);
setTypeData(["all"]);
setSelectedValue(null);
const allCatalog = { id: "all", nameCatalog: "All Catalogues" };
dispatch(setSelectedCatalogue(allCatalog));
dispatch(setSelectedCatalogues([allCatalog]));
setRefresh((r) => !r);
};
Report Data Preparation
Data Accumulated During View
async function getDataAnalyticsOverview(data) {
try {
const {
totalSession,
totalUser,
totalNewUser,
totalAvgTime,
totalAvgEndTime,
totalAvgSpU,
..._data
} = data;
const startDate = dateRange[0].format("YYYY/MM/DD HH:mm:ss");
const endDate = dateRange[1].format("YYYY/MM/DD HH:mm:ss");
setReportData((prevReportData) => ({
...prevReportData,
catalog: selectedCatalog.nameCatalog,
typeData,
startDate,
endDate,
totalSession,
totalUser,
totalNewUser,
totalAvgEndTime,
totalAvgSpU,
}));
setOverviewData(_data);
} catch (error) {
// Error handling
}
}
Glass Model in Report
useEffect(() => {
let selectedModel = "";
if (glass) {
selectedModel = glassList?.find((modello) => modello.id === glass);
}
setReportData({
model: selectedModel?.nome_modello ?? "All",
});
}, [glass, filterOptions, glassList]);
UI Components Integration
AnalyticsOverviewCard
<AnalyticsOverviewCard
loading={loading}
aggregateList={filteredAggList} // Receives filtered data
getData={getDataAnalyticsOverview} // Callback to populate reportData
/>
Displays: Total sessions, users, new users, avg time, avg sessions per user
AnalyticsTable
<AnalyticsTable
loading={loading}
aggregateList={filteredAggList} // Filtered by all layers
/>
Displays: Catalogue time focus table (drill-down by catalogue, glass, variant)
DeviceAnalytics
<DeviceAnalytics
loading={loading}
sessionList={filteredSessionList} // Note: different data source
aggregateList={filteredAggList}
onResetFilters={handleClearFilter}
/>
Displays: Device, browser, OS analytics with pie charts/breakdowns
Responsive Design
const isTabletOrMobile = useMediaQuery({ query: "(max-width: 768px)" });
// Hide header button on mobile
<HeaderComponent
...
isVisible={!isTabletOrMobile}
...
/>
Performance Optimization Patterns
Pattern 1: Cache Deduplication
Problem: Multiple rapid requests for same data
Solution: Store cacheKey → Promise and await instead of refetch
Pattern 2: Non-Blocking Enrichment
Problem: Enrichment might fail (network timeout, permission denied)
Solution: Wrap in try/catch, fallback to raw data, don't block UI
try {
enrichedData = await enrichDataWithSubcollections(aggregateData);
} catch (enrichError) {
return aggregateData; // Fallback
}
Pattern 3: Firestore Consumption Tracking
Problem: Unknown data costs in production
Solution: Snapshot before/after operations, display diffs
Pattern 4: Ref-Based Tracking
const aggregateCache = useRef(new Map()); // Persists across renders
const ongoingFetches = useRef(new Map()); // Tracks in-flight requests
const snapshotRef = useRef(null); // Holds Firestore snapshot
Benefits: Not reset on re-renders, perfect for concurrent request prevention
Pattern 5: useEffect Dependencies
useEffect(() => {
fetchAggregate();
}, [selectedCatalogs, dateRange, refresh]); // Only 3 critical dependencies
Avoids: Unnecessary fetches on unrelated state changes
Data Structures
Aggregate Data Structure
{
// Session metadata
id: string,
catalogRef: string,
sessionDate: Date | timestamp,
// Overall metrics
totalSessions: number,
totalUniqueUsers: number,
totalNewUsers: number,
averageSessionTime: number, // Milliseconds
averageEngagementTime: number,
averageSessionsPerUser: number,
// Service type
service: "APP" | "3D VIEWER" | "WEB VTO",
// Nested products
list_products: [{
productRef: string,
productName: string,
numberOfSessions: number,
numberOfUniqueUsers: number,
newUsersCount: number,
averageSessionTime: number,
averageEngagementTime: number,
averageSessionsPerUser: number
}],
// Nested variants
list_variants: [{
variantRef: string,
variantName: string,
numberOfSessions: number,
numberOfUniqueUsers: number,
newUsersCount: number,
averageSessionTime: number,
averageEngagementTime: number,
averageSessionsPerUser: number
}]
}
Catalogue Structure
{
id: string,
nameCatalog: string,
isGatewayCatalog: boolean, // true → excluded from VTO analytics
// ... other fields
}
Error Handling & Edge Cases
Edge Case 1: No Catalogues Selected
if (allCatalogIds.length === 0) {
setAggregateList([]);
setLoading(false);
return;
}
Impact: Shows empty table
Edge Case 2: "All Catalogues" Special Handling
if (selectedCatalogs.length === 1 && selectedCatalogs[0].id === "all") {
allCatalogIds = catalogsList || []; // Use full list
}
Why: Special "all" item isn't a real catalogue ID
Edge Case 3: Enrichment Failure
try {
enrichedData = await enrichDataWithSubcollections(aggregateData);
} catch (enrichError) {
console.error("Error enriching data:", enrichError);
return aggregateData; // Continue with raw
}
Impact: UI still works, just without nested details
Edge Case 4: Gateway Catalogues in Report
if (catalogData.isGatewayCatalog === true) {
return null; // Exclude from report modal
}
Why: Gateway analytics use different page
Edge Case 5: Invalid Date Range in Report
const isFormValid = () => {
return startDateReport && endDateReport && !error && isAnySectionSelected();
};
Disables download if dates invalid
Edge Case 6: No Sections Selected in Report
const isAnySectionSelected = () => {
return Object.values(reportSections).some((value) => value === true);
};
Requires at least one section
Edge Case 7: Concurrent Fetch with Different Keys
Scenario: User quickly changes date range twice
Handling:
// First fetch starts with key1
ongoingFetches.current.set(key1, promise1);
// Second fetch starts with key2 (different date)
ongoingFetches.current.set(key2, promise2);
// Both requests are independent, both tracked
Utilities Used
getMinSecObject(milliseconds)
Purpose: Convert milliseconds to readable "MM:SS" format
Usage: Display average engagement time as "05:30" instead of "330000"
enrichDataWithSubcollections(data)
Purpose: Fetch nested product/variant details from Firestore
Returns: Same structure with additional detail fields enriched
generateReportWithSections(startDate, endDate, catalogues, data, sections)
Purpose: Generate PDF with selected sections only
Parameters:
sections: { overview: boolean, deviceAnalytics: boolean, variantsDetails: boolean }
Related Files & Architecture
Analytics components:
/src/pages/Analytics.js(main page)/src/components/Analytics/AnalyticsOverviewCard.js/src/components/Analytics/AnalyticsTable.js/src/components/Analytics/DeviceAnalytics.js/src/components/Analytics/Filters.js
API services:
/src/services/api/analyticsApi.js(getAggregatedData, generateReportWithSections)/src/services/data/analyticsData.js(enrichDataWithSubcollections)
Utilities:
/src/services/utils/analyticsUtils.js(getMinSecObject, etc.)/src/services/firebase/firebaseTracker.js(consumption tracking)
Redux:
/src/redux/analytics/actions.js(setAnalyticsFilterOptions)/src/redux/Catalogues/actions.js(catalogue selection)
Firestore:
Cataloguescollection- Session aggregated data (from custom analytics)