Skip to main content
Version: 1.0.0

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 aggregateList to only sessions containing the selected glass product
  • Extracts metrics for that specific glass from list_products array
  • 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 aggregateList to only sessions containing the selected variant
  • Extracts metrics from list_variants array
  • 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

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,
});

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 }

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:

  • Catalogues collection
  • Session aggregated data (from custom analytics)