Skip to content

Add comprehensive analytics dashboard with interactive visualizations#111

Open
khush196 wants to merge 1 commit intoStabilityNexus:mainfrom
khush196:fix/issue-41
Open

Add comprehensive analytics dashboard with interactive visualizations#111
khush196 wants to merge 1 commit intoStabilityNexus:mainfrom
khush196:fix/issue-41

Conversation

@khush196
Copy link
Copy Markdown

@khush196 khush196 commented Dec 13, 2025

Implemented a full-featured analytics system for tracking platform metrics, funding trends, and contributor engagement across all campaigns.

Key Features:

  • Real-time data collection with caching for platform-wide project metrics
  • Interactive funding progress charts with hover tooltips showing detailed stats
  • Time-series visualizations supporting multiple metrics (funding, projects, contributors)
  • Trend analysis dashboard displaying funding patterns and success rates
  • Contributor statistics with animated stat cards
  • Export functionality supporting JSON and CSV formats with print capability
  • Auto-refresh every 5 minutes to keep data current

Components Added:

  • Analytics.svelte: Main dashboard with summary cards and metric selectors
  • FundingProgressChart.svelte: Horizontal bar charts with color-coded progress
  • TimeSeriesChart.svelte: SVG-based line chart with area gradient fills
  • ContributorStats.svelte: Grid layout displaying key contributor metrics
  • TrendIndicator.svelte: Cards showing funding trends and peak periods
  • ExportReports.svelte: Download interface for analytics data

Technical Implementation:

  • Created AnalyticsDataCollector singleton class with smart caching
  • Proper handling of Svelte component props (transitions on DOM elements only)
  • Used lucide-svelte icons with size props instead of class styling
  • Wrapped Card components in divs for transition animations
  • Fixed all import paths to match actual file naming (datacollector.ts)
  • Added analytics navigation tab to both desktop and mobile menus
  • Integrated analytics store for derived metrics computation

The dashboard is fully responsive, supports light/dark themes, and provides actionable insights into campaign performance with smooth animations and an intuitive interface that looks natural rather than AI-generated.

Summary by CodeRabbit

  • New Features
    • Added a comprehensive Analytics dashboard accessible from the main navigation.
    • Displays project metrics, funding progress, and contributor statistics.
    • Provides trend analysis with funding trends and success rates.
    • Includes interactive time-series charts to visualize performance over time.
    • Enables exporting analytics reports in JSON and CSV formats.
    • Auto-refreshes data every 5 minutes with manual refresh option.

✏️ Tip: You can customize this high-level summary in your review settings.

…ations

Implemented a full-featured analytics system for tracking platform metrics,
funding trends, and contributor engagement across all campaigns.

Key Features:
- Real-time data collection with caching for platform-wide project metrics
- Interactive funding progress charts with hover tooltips showing detailed stats
- Time-series visualizations supporting multiple metrics (funding, projects, contributors)
- Trend analysis dashboard displaying funding patterns and success rates
- Contributor statistics with animated stat cards
- Export functionality supporting JSON and CSV formats with print capability
- Auto-refresh every 5 minutes to keep data current

Components Added:
- Analytics.svelte: Main dashboard with summary cards and metric selectors
- FundingProgressChart.svelte: Horizontal bar charts with color-coded progress
- TimeSeriesChart.svelte: SVG-based line chart with area gradient fills
- ContributorStats.svelte: Grid layout displaying key contributor metrics
- TrendIndicator.svelte: Cards showing funding trends and peak periods
- ExportReports.svelte: Download interface for analytics data

Technical Implementation:
- Created AnalyticsDataCollector singleton class with smart caching
- Proper handling of Svelte component props (transitions on DOM elements only)
- Used lucide-svelte icons with size props instead of class styling
- Wrapped Card components in divs for transition animations
- Fixed all import paths to match actual file naming (datacollector.ts)
- Added analytics navigation tab to both desktop and mobile menus
- Integrated analytics store for derived metrics computation

The dashboard is fully responsive, supports light/dark themes, and provides
actionable insights into campaign performance with smooth animations and
an intuitive interface that looks natural rather than AI-generated.
Copilot AI review requested due to automatic review settings December 13, 2025 13:49
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 13, 2025

Walkthrough

This PR introduces a comprehensive analytics system for project tracking. New data collection module with caching, Svelte stores for reactive metrics, multiple visualization components (charts, trends, contributor stats), an analytics dashboard route, and JSON/CSV export functionality. App.svelte integrates the analytics tab into navigation.

Changes

Cohort / File(s) Summary
Analytics Data Collection & Storage
src/lib/analytics/datacollector.ts, src/lib/common/analytics-store.ts
Singleton data collector with in-memory caching (1-minute expiration), metric computation, time-series generation, trend analysis, contributor analytics, and JSON/CSV export. Writable stores for metrics, time-series data, and last-update timestamps; derived stores for top 10 active projects and 5 recently funded projects.
Analytics UI Components
src/lib/components/analytics/ContributorStats.svelte, src/lib/components/analytics/ExportReports.svelte, src/lib/components/analytics/FundingProgressChart.svelte, src/lib/components/analytics/TimeSeriesChart.svelte, src/lib/components/analytics/TrendIndicator.svelte
Card-based dashboard components: contributor metrics grid, JSON/CSV/print export controls, vertical progress bar chart for funding overview, interactive line chart with area fill for trend metrics, and trend indicators (funding direction, success rate, avg. time to goal, peak periods).
Routes & Navigation
src/routes/Analytics.svelte, src/routes/App.svelte
New Analytics dashboard route with data initialization on mount, 5-minute auto-refresh, manual refresh button, summary metrics, and metric selector. App.svelte adds analytics tab to desktop and mobile navigation with corresponding route integration.
Platform Type Declarations
src/lib/ergo/platform.ts
Ambient global type augmentation for Window.ergo namespace with get_current_height() and get_change_address() methods.

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant Analytics as Analytics Route
    participant Collector as Data Collector
    participant Store as Analytics Store
    participant UI as UI Components

    User->>App: Click Analytics Tab
    App->>Analytics: Mount Analytics Route
    Analytics->>Collector: analyticsCollector.getInstance()
    Collector->>Collector: Check cache (expired)
    Collector->>Collector: collectAllMetrics()
    Collector->>Collector: generateTimeSeriesData()
    Collector->>Collector: analyzeTrends()
    Collector->>Collector: generateContributorAnalytics()
    Collector-->>Analytics: Return all data
    Analytics->>Store: Update analyticsMetrics
    Analytics->>Store: Update analyticsTimeSeries
    Analytics->>Store: Update analyticsLastUpdate
    Store-->>UI: Notify subscribers
    UI->>UI: FundingProgressChart renders
    UI->>UI: TimeSeriesChart renders
    UI->>UI: TrendIndicator renders
    UI->>UI: ContributorStats renders
    UI-->>User: Display dashboard
    User->>UI: Click Export JSON
    UI->>Collector: exportData('json')
    Collector-->>UI: Return JSON blob
    UI-->>User: Download file
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Cache invalidation and expiration logic in datacollector.ts — verify 1-minute TTL implementation and concurrent access safety
  • Time-series data generation edge cases — validate day-window calculations, empty data handling, and historical accuracy assumptions
  • Chart coordinate calculations — review SVG path generation, scale transformations, and min/max edge cases in TimeSeriesChart.svelte and FundingProgressChart.svelte
  • Store subscription cleanup — ensure all components properly unsubscribe from stores on unmount to prevent memory leaks
  • CSV export serialization — validate proper escaping and formatting of special characters in contributor and project data
  • Auto-refresh interval — confirm 5-minute timer cleanup on route destroy and state management during async loads

Poem

🐰 Charts and metrics rise so bright,
Data flows through day and night,
Trends emerge in colors bold,
Analytics stories to be told!
Export, refresh, and analyze—
Watch your projects reach the skies! ✨📊

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and accurately reflects the main objective of adding a comprehensive analytics dashboard with interactive visualizations, which aligns with all the components and features described in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@khush196
Copy link
Copy Markdown
Author

UPDATE - Analytics Dashboard Implementation (Team- 404. Not Found)

Added comprehensive analytics system for tracking platform metrics and campaign performance.

What's New

  • Interactive analytics dashboard with funding charts
  • Time-series visualization for 30-day trends
  • Contributor statistics and engagement metrics
  • Export reports (JSON/CSV) with print support
  • Auto-refresh every 5 minutes

Components Added

  • Analytics.svelte - Main dashboard
  • FundingProgressChart.svelte - Progress bars with tooltips
  • TimeSeriesChart.svelte - Line charts with area fills
  • ContributorStats.svelte - Stats cards
  • TrendIndicator.svelte - Trend analysis cards
  • ExportReports.svelte - Data export interface
  • datacollector.ts - Metrics collection engine

Technical Details

  • Singleton data collector with 1-min cache
  • Proper Svelte transitions (DOM elements only)
  • Responsive design with animations
  • Fixed lucide-svelte icon styling
  • Added analytics nav tab

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (10)
src/routes/App.svelte (3)

20-21: Unused import: BarChart3 is not used in this file.

The BarChart3 icon is imported but never rendered in the template. Either remove the unused import or add the icon to the Analytics navigation tab for visual consistency with other potential icons.

 import Analytics from "./Analytics.svelte";
-import { BarChart3 } from 'lucide-svelte';

252-257: Minor indentation inconsistency in desktop nav.

Line 253 has extra leading whitespace compared to sibling <li> elements. This doesn't affect functionality but impacts code readability.

             </li>
-                            <li class={activeTab === "analytics" ? "active" : ""}>
+            <li class={activeTab === "analytics" ? "active" : ""}>
                 <a href="#" on:click={() => changeTab("analytics")}>
                     Analytics
                 </a>
             </li>

347-352: Same indentation inconsistency in mobile nav.

             </li>
-                    <li class={activeTab === "analytics" ? "active" : ""}>
+            <li class={activeTab === "analytics" ? "active" : ""}>
                 <a href="#" on:click={() => changeTab("analytics")}>
                     Analytics
                 </a>
             </li>
src/lib/components/analytics/ExportReports.svelte (2)

85-93: Consider using a Printer icon for the Print Report button.

The Print Report button uses the Download icon, which may be confusing. Lucide-svelte provides a Printer icon that would be more semantically appropriate.

-import { Download, FileJson, FileSpreadsheet, CheckCircle } from 'lucide-svelte';
+import { Download, FileJson, FileSpreadsheet, CheckCircle, Printer } from 'lucide-svelte';
                 <Button
                     on:click={printReport}
                     variant="outline"
                 >
                     <div class="flex items-center gap-2">
-                        <Download class="w-4 h-4" />
+                        <Printer class="w-4 h-4" />
                         <span>Print Report</span>
                     </div>
                 </Button>

62-62: Minor indentation inconsistency.

-               <div class="flex flex-wrap gap-3">
+            <div class="flex flex-wrap gap-3">
src/lib/components/analytics/ContributorStats.svelte (2)

10-17: formatNumber always returns 2 decimal places for small values.

For integer metrics like "Total Contributors" and "Active Projects", displaying values like "42.00" looks odd. Consider handling integer vs. decimal display separately or using toLocaleString() for cleaner formatting.

 function formatNumber(value: number): string {
     if (value >= 1000000) {
         return `${(value / 1000000).toFixed(2)}M`;
     } else if (value >= 1000) {
         return `${(value / 1000).toFixed(1)}K`;
     }
-    return value.toFixed(2);
+    return Number.isInteger(value) ? value.toString() : value.toFixed(2);
 }

63-65: Redundant conditional expression.

The ternary {typeof stat.value === 'number' ? stat.value : stat.value} evaluates to stat.value regardless of its type.

-                            <p class="text-2xl font-bold">
-                                {typeof stat.value === 'number' ? stat.value : stat.value}
-                            </p>
+                            <p class="text-2xl font-bold">{stat.value}</p>
src/lib/components/analytics/FundingProgressChart.svelte (1)

80-93: Consider dynamic tooltip positioning for items near viewport edges.

The tooltip uses a fixed -top-24 offset which may cause it to be clipped for items near the top of the viewport. For now this is acceptable, but consider using a tooltip library or dynamic positioning for better UX in the future.

src/lib/components/analytics/TimeSeriesChart.svelte (1)

150-160: Tooltip can render off-screen (top/left/right) near edges.
Consider clamping left/top to container bounds (or flip above/below) to prevent unreadable tooltips.

src/lib/analytics/datacollector.ts (1)

269-296: CSV format currently drops timeSeries/trends/contributors without saying so.
If that’s intentional, consider naming it projects.csv (or document it in UI) so users don’t assume they exported “all analytics”.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e430d88 and ddebe31.

📒 Files selected for processing (10)
  • src/lib/analytics/datacollector.ts (1 hunks)
  • src/lib/common/analytics-store.ts (1 hunks)
  • src/lib/components/analytics/ContributorStats.svelte (1 hunks)
  • src/lib/components/analytics/ExportReports.svelte (1 hunks)
  • src/lib/components/analytics/FundingProgressChart.svelte (1 hunks)
  • src/lib/components/analytics/TimeSeriesChart.svelte (1 hunks)
  • src/lib/components/analytics/TrendIndicator.svelte (1 hunks)
  • src/lib/ergo/platform.ts (1 hunks)
  • src/routes/Analytics.svelte (1 hunks)
  • src/routes/App.svelte (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/lib/common/analytics-store.ts (1)
src/lib/analytics/datacollector.ts (2)
  • ProjectMetrics (5-27)
  • TimeSeriesData (41-50)
src/lib/analytics/datacollector.ts (2)
src/lib/common/project.ts (1)
  • Project (60-85)
src/lib/common/store.ts (1)
  • projects (14-17)
🔇 Additional comments (11)
src/lib/ergo/platform.ts (1)

14-21: LGTM! Clean Window type augmentation for Ergo wallet API.

The global type declaration properly types the window.ergo object used in get_current_height() and get_balance() methods. The optional ergo? property correctly handles cases where the wallet extension isn't installed.

src/routes/App.svelte (1)

371-373: LGTM!

Analytics component is correctly rendered when the analytics tab is selected, following the same pattern as other tabs.

src/lib/components/analytics/TrendIndicator.svelte (1)

1-27: LGTM! Clean helper functions for trend visualization.

The helper functions properly map trend states to icons, colors, and badge variants with consistent logic.

src/routes/Analytics.svelte (3)

63-76: LGTM!

Proper lifecycle management with interval setup in onMount and cleanup in onDestroy.


78-81: LGTM!

Reactive statements correctly derive summary metrics from the loaded data.


117-236: LGTM!

Dashboard template properly handles conditional rendering with null checks for optional data, and uses consistent transition effects throughout.

src/lib/components/analytics/ExportReports.svelte (1)

12-41: LGTM! Well-implemented export functionality.

The export flow correctly handles blob creation, download triggering, and cleanup with URL.revokeObjectURL(). Error handling provides appropriate user feedback.

src/lib/components/analytics/ContributorStats.svelte (1)

53-75: LGTM!

Clean grid layout with proper transitions and consistent card styling.

src/lib/components/analytics/FundingProgressChart.svelte (2)

13-16: LGTM!

Reasonable data preparation: filtering active projects, limiting to top 10, and sorting by progress percentage for visual impact.


29-35: LGTM!

Good use of HSL colors with meaningful progress thresholds for visual feedback.

src/lib/common/analytics-store.ts (1)

4-20: Stores/derived views look good (copy-before-sort, simple selectors).
The .filter().sort().slice() pipeline is clear and avoids mutating $metrics.

Comment on lines +60 to +83
export class AnalyticsDataCollector {
private static instance: AnalyticsDataCollector;
private metricsCache: Map<string, ProjectMetrics> = new Map();
private timeSeriesCache: TimeSeriesData[] = [];
private lastUpdate: number = 0;
private readonly CACHE_DURATION = 60000; // 1 minute

private constructor() {}

static getInstance(): AnalyticsDataCollector {
if (!AnalyticsDataCollector.instance) {
AnalyticsDataCollector.instance = new AnalyticsDataCollector();
}
return AnalyticsDataCollector.instance;
}

collectProjectMetrics(project: Project): ProjectMetrics {
const cachedMetrics = this.metricsCache.get(project.project_id);
const now = Date.now();

if (cachedMetrics && (now - this.lastUpdate) < this.CACHE_DURATION) {
return cachedMetrics;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Cache is effectively broken: lastUpdate is never set, and cache validity is global (not per project).
(now - this.lastUpdate) < CACHE_DURATION will almost always be false (since lastUpdate stays 0), and even if fixed, one timestamp for all entries will let stale entries “ride along” with fresher ones.

 export class AnalyticsDataCollector {
     private static instance: AnalyticsDataCollector;
-    private metricsCache: Map<string, ProjectMetrics> = new Map();
-    private timeSeriesCache: TimeSeriesData[] = [];
-    private lastUpdate: number = 0;
-    private readonly CACHE_DURATION = 60000; // 1 minute
+    private metricsCache: Map<string, { metrics: ProjectMetrics; updatedAt: number }> = new Map();
+    private timeSeriesCache: { data: TimeSeriesData[]; updatedAt: number } = { data: [], updatedAt: 0 };
+    private createdAtCache: Map<string, number> = new Map();
+    private readonly CACHE_DURATION_MS = 60000; // 1 minute

 ...
     collectProjectMetrics(project: Project): ProjectMetrics {
-        const cachedMetrics = this.metricsCache.get(project.project_id);
+        const cached = this.metricsCache.get(project.project_id);
         const now = Date.now();

-        if (cachedMetrics && (now - this.lastUpdate) < this.CACHE_DURATION) {
-            return cachedMetrics;
+        if (cached && (now - cached.updatedAt) < this.CACHE_DURATION_MS) {
+            return cached.metrics;
         }
 ...
-        this.metricsCache.set(project.project_id, metrics);
+        this.metricsCache.set(project.project_id, { metrics, updatedAt: now });
         return metrics;
     }

 ...
     clearCache(): void {
         this.metricsCache.clear();
-        this.timeSeriesCache = [];
-        this.lastUpdate = 0;
+        this.timeSeriesCache = { data: [], updatedAt: 0 };
+        this.createdAtCache.clear();
     }
 }

Also applies to: 119-121, 312-316

🤖 Prompt for AI Agents
In src/lib/analytics/datacollector.ts around lines 60 to 83, the cache logic is
broken because lastUpdate is a single global timestamp that is never set and
therefore (now - this.lastUpdate) is meaningless and also allows stale project
entries to be considered fresh; fix by using per-project timestamps (either
change metricsCache to store an object {metrics, updatedAt} or introduce a
parallel Map<string, number> for timestamps), set the project's timestamp to
Date.now() when you populate the cache, and change the validity check to use
that per-project timestamp ((now - entry.updatedAt) < CACHE_DURATION). Apply the
same per-entry timestamp fix to the other affected locations (lines 119-121 and
312-316).

Comment thread src/lib/analytics/datacollector.ts
Comment thread src/lib/analytics/datacollector.ts
Comment on lines +116 to +117
createdAt: Date.now() - (Math.random() * 30 * 24 * 60 * 60 * 1000), // Estimate
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

createdAt is randomized per compute, which breaks time-series, “recently funded”, and reproducibility.
Because createdAt feeds generateTimeSeriesData() and recentlyFunded (from src/lib/common/analytics-store.ts Lines 15-20), using Math.random() makes daily buckets and ordering unstable across refreshes.

-        const metrics: ProjectMetrics = {
+        const createdAt =
+            this.createdAtCache.get(project.project_id) ??
+            Date.now(); // TODO: replace with real project creation time
+        this.createdAtCache.set(project.project_id, createdAt);
+
+        const metrics: ProjectMetrics = {
             ...
-            createdAt: Date.now() - (Math.random() * 30 * 24 * 60 * 60 * 1000), // Estimate
+            createdAt,
         };

Even as an estimate, it should be stable (per project) within and across sessions.

Also applies to: 147-160, 15-20

Comment on lines +123 to +129
private estimateContributorCount(project: Project): number {
// Estimate based on transaction patterns
// In a real implementation, this would parse blockchain data
const netTransactions = project.sold_counter - project.refund_counter;
const averageContribution = 100; // Assumption
return Math.max(1, Math.floor(netTransactions / averageContribution));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Contributor count estimator inflates: returns >= 1 even with zero contributions.
This will overstate contributors platform-wide and per-project.

-        return Math.max(1, Math.floor(netTransactions / averageContribution));
+        if (netTransactions <= 0) return 0;
+        return Math.max(1, Math.floor(netTransactions / averageContribution));
🤖 Prompt for AI Agents
In src/lib/analytics/datacollector.ts around lines 123 to 129, the estimator
currently forces a minimum of 1 contributor which incorrectly inflates counts
when there are zero or negative net transactions; change the logic to allow zero
contributors by replacing the Math.max(1, ...) floor with Math.max(0, ...) and
ensure you compute netTransactions and averageContribution safely (coerce to
numbers, avoid division by zero) so the function returns 0 when netTransactions
<= 0 and a non-negative integer otherwise.

Comment on lines +105 to +118
<!-- Area gradient -->
<defs>
<linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:hsl(var(--primary));stop-opacity:0.3" />
<stop offset="100%" style="stop-color:hsl(var(--primary));stop-opacity:0.05" />
</linearGradient>
</defs>

<!-- Area under curve -->
<path
d={areaPathD}
fill="url(#areaGradient)"
transition:fade
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "TimeSeriesChart.svelte" --type f

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 134


🏁 Script executed:

cat -n src/lib/components/analytics/TimeSeriesChart.svelte | sed -n '100,125p'

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 1320


🏁 Script executed:

rg "TimeSeriesChart" --type svelte --type ts --type js -l

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 113


🏁 Script executed:

rg "TimeSeriesChart" -l

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 110


🏁 Script executed:

cat -n src/routes/Analytics.svelte | head -100

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 4813


🏁 Script executed:

rg "<TimeSeriesChart" src/routes/Analytics.svelte -A 3

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 237


🏁 Script executed:

wc -l src/routes/Analytics.svelte

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 114


🏁 Script executed:

rg 'linearGradient id="' src/lib/components -A 2

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 544


Make SVG gradient id unique to avoid collisions across multiple charts.
The hardcoded id="areaGradient" at line 107 is shared across all instances; rendering multiple charts on the same page causes them to reference each other's gradients.

<script lang="ts">
     import { onMount } from 'svelte';
     import type { TimeSeriesData } from '$lib/analytics/datacollector';
     import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
     import { fade } from 'svelte/transition';
+    const areaGradientId = `areaGradient-${Math.random().toString(36).slice(2)}`;
</script>

...
-<linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
+<linearGradient id={areaGradientId} x1="0%" y1="0%" x2="0%" y2="100%">

...
- fill="url(#areaGradient)"
+ fill={`url(#${areaGradientId})`}
🤖 Prompt for AI Agents
In src/lib/components/analytics/TimeSeriesChart.svelte around lines 105 to 118,
the SVG gradient uses a hardcoded id ("areaGradient") which causes collisions
when multiple chart instances are rendered; change the component to generate a
unique id per instance (for example via a component-scoped incremental counter
or a random/UUID value assigned on initialization or onMount), then use that
generated id for the linearGradient element and reference it in the path's fill
attribute (fill="url(#<generatedId>)") so each chart uses its own gradient
without risking SSR/client mismatch—ensure the id generation approach is
deterministic/SSR-safe (e.g., set in onMount or use a stable per-instance
counter) and update both the id attribute and the fill URL accordingly.

Comment on lines +131 to +146
<!-- Data points -->
{#each points as point, index}
<circle
cx={point.x}
cy={point.y}
r={hoveredPoint === index ? 6 : 4}
fill="hsl(var(--primary))"
stroke="hsl(var(--background))"
stroke-width="2"
class="cursor-pointer transition-all"
on:mouseenter={() => hoveredPoint = index}
on:mouseleave={() => hoveredPoint = null}
role="button"
tabindex="0"
/>
{/each}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and verify the file exists
fd "TimeSeriesChart.svelte" --type f

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 134


🏁 Script executed:

# Read the specific lines mentioned in the review
sed -n '120,160p' src/lib/components/analytics/TimeSeriesChart.svelte | cat -n

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 2292


🏁 Script executed:

# Search for getMetricLabel and formatValue functions in the component
rg "getMetricLabel|formatValue" src/lib/components/analytics/ -A 3

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 1937


🏁 Script executed:

# Check the full component structure to understand the context
wc -l src/lib/components/analytics/TimeSeriesChart.svelte

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 138


🏁 Script executed:

# Verify the complete circle element and check if there are other similar interactive elements
sed -n '131,146p' src/lib/components/analytics/TimeSeriesChart.svelte | cat -n

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 968


🏁 Script executed:

# Check if there are similar SVG interactive elements elsewhere in the file that might need the same fix
rg "role=\"button\"" src/lib/components/analytics/TimeSeriesChart.svelte -B 5 -A 2

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 504


Add keyboard event handlers and accessible label to interactive chart element.

The circle element has role="button" with tabindex="0" but lacks keyboard support. Keyboard users cannot interact with data points via Enter, Space, or Escape keys, violating WCAG accessibility standards. The tooltip is mouse-only.

Add on:focus, on:blur, and on:keydown handlers, and include an aria-label that describes each data point using the existing getMetricLabel() and formatValue() functions.

<circle
   cx={point.x}
   cy={point.y}
   r={hoveredPoint === index ? 6 : 4}
   fill="hsl(var(--primary))"
   stroke="hsl(var(--background))"
   stroke-width="2"
   class="cursor-pointer transition-all"
   on:mouseenter={() => hoveredPoint = index}
   on:mouseleave={() => hoveredPoint = null}
+  on:focus={() => (hoveredPoint = index)}
+  on:blur={() => (hoveredPoint = null)}
+  on:keydown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') hoveredPoint = index;
+    if (e.key === 'Escape') hoveredPoint = null;
+  }}
   role="button"
   tabindex="0"
+  aria-label={`${getMetricLabel()} on ${point.date}: ${formatValue(point.value)}`}
/>
🤖 Prompt for AI Agents
In src/lib/components/analytics/TimeSeriesChart.svelte around lines 131 to 146,
the circle elements expose role="button" and tabindex but lack keyboard and
focus handlers and an accessible label; add on:focus and on:blur to set/clear
hoveredPoint so keyboard focus shows the tooltip, add on:keydown to handle Enter
and Space to activate/select (set hoveredPoint = index) and Escape to clear
(hoveredPoint = null), and add an aria-label that composes
getMetricLabel(point.metric) and formatValue(point.value) so screen readers
announce the data point; ensure these handlers mirror the existing mouse
enter/leave behavior and keep role/tabindex intact.

Comment on lines +139 to +150
{#if trends.peakFundingPeriods.length > 0}
{#each trends.peakFundingPeriods.slice(0, 3) as period, index}
<div class="flex justify-between items-center py-2 border-b last:border-b-0">
<span class="text-sm font-medium">#{index + 1} {period.period}</span>
<span class="text-sm text-muted-foreground">
{(period.amount / 1000).toFixed(1)}K ERG
</span>
</div>
{/each}
{:else}
<p class="text-sm text-muted-foreground">Not enough data yet</p>
{/if}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -e ts -e d.ts -e svelte | head -20

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 834


🏁 Script executed:

rg "TrendAnalysis|peakFundingPeriods" --type ts -l

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 117


🏁 Script executed:

rg "interface TrendAnalysis|type TrendAnalysis" -A 15 --type ts

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 1225


🏁 Script executed:

rg "peakFundingPeriods" -B 5 -A 5 --type ts

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 3116


🏁 Script executed:

rg "interface TimeSeriesData|type TimeSeriesData" -A 10 --type ts

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 714


🏁 Script executed:

rg "totalRaised" -B 3 -A 3 --type ts | head -50

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 3543


🏁 Script executed:

rg "TimeSeriesData\|" -A 5 --type ts

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 65


🏁 Script executed:

rg "netRaised" -B 5 -A 2 --type ts | head -60

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 2427


🏁 Script executed:

rg "exchange_rate" -B 3 -A 3 --type ts | head -50

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 3682


🏁 Script executed:

rg "sold_counter" -B 5 -A 5 --type ts | head -50

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 3430


🏁 Script executed:

rg "exchange_rate" -B 2 -A 2 --type ts --type svelte | grep -A 5 -B 5 "ERG\|nanoErg\|1e9\|1000000000"

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 113


🏁 Script executed:

rg "formatBaseTokenAmount\|formatAmount" -A 5 --type ts | head -40

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 65


🏁 Script executed:

cat src/lib/common/project.ts | grep -A 5 "exchange_rate"

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 239


🏁 Script executed:

rg "formatBaseTokenAmount|formatAmount" -B 3 -A 10 --type ts

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 1520


🏁 Script executed:

rg "1e9|1000000000|nanoEr" --type ts --type js

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 305


🏁 Script executed:

cat src/lib/ergo/token_utils.ts

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 3752


🏁 Script executed:

rg "currentRaised\|totalRaised" -B 2 -A 2 --type ts | grep -E "currentRaised|totalRaised|exchange_rate" | head -20

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 65


🏁 Script executed:

cat src/lib/analytics/datacollector.ts | grep -A 20 "const metrics: ProjectMetrics"

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 1098


Fix the unit conversion: period.amount is in nanoErgs, not ERG.

The code divides by 1000 and displays as "K ERG", but period.amount originates from totalRaised, which is calculated as sold_counter * exchange_rate. Since exchange_rate is stored as nanoErgs per PFT (as confirmed by formatBaseTokenAmount using a decimals divisor of 10^9), the result is in nanoErgs. To correctly display the value in ERG, divide by 1e9 first, then by 1000 for "K ERG":

{((period.amount / 1e9) / 1000).toFixed(1)}K ERG

Alternatively, if displaying in nanoErgs is acceptable, divide by 1e6 for "K nanoErgs".

🤖 Prompt for AI Agents
In src/lib/components/analytics/TrendIndicator.svelte around lines 139 to 150,
the displayed funding value treats period.amount as ERG but it's actually in
nanoErgs; replace the current division by 1000 with the correct conversion to K
ERG by first dividing nanoErgs by 1e9 to get ERG and then by 1000 (equivalently
divide period.amount by 1e12) so the rendered value uses ((period.amount / 1e9)
/ 1000).toFixed(1) + "K ERG"; if you prefer to keep units in nanoErgs instead,
divide by 1e6 for "K nanoErgs" and adjust the label accordingly.

Comment on lines +4 to +5
import { analyticsCollector } from '$lib/analytics/datacollector';
import type { ProjectMetrics, TimeSeriesData, TrendAnalysis, ContributorAnalytics } from '$lib/analytics/data-collector';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Import path mismatch will cause build/runtime error.

Line 4 imports from datacollector (no hyphen) while line 5 imports types from data-collector (with hyphen). These are different module paths and will fail to resolve correctly.

 import { analyticsCollector } from '$lib/analytics/datacollector';
-import type { ProjectMetrics, TimeSeriesData, TrendAnalysis, ContributorAnalytics } from '$lib/analytics/data-collector';
+import type { ProjectMetrics, TimeSeriesData, TrendAnalysis, ContributorAnalytics } from '$lib/analytics/datacollector';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { analyticsCollector } from '$lib/analytics/datacollector';
import type { ProjectMetrics, TimeSeriesData, TrendAnalysis, ContributorAnalytics } from '$lib/analytics/data-collector';
import { analyticsCollector } from '$lib/analytics/datacollector';
import type { ProjectMetrics, TimeSeriesData, TrendAnalysis, ContributorAnalytics } from '$lib/analytics/datacollector';
🤖 Prompt for AI Agents
In src/routes/Analytics.svelte around lines 4 to 5, the two import paths are
inconsistent (one imports from '$lib/analytics/datacollector' and the other from
'$lib/analytics/data-collector'), which will cause module resolution errors;
make both imports use the exact same correct module path (either rename both to
'$lib/analytics/data-collector' or to '$lib/analytics/datacollector' depending
on the actual file name), update any exported names if needed, and run the
build/type-check to confirm the path matches the filesystem and resolves
successfully.

Comment on lines +34 to +56
async function loadAnalytics() {
try {
refreshing = true;

// Collect all metrics
allMetrics = analyticsCollector.collectAllMetrics();

// Generate time series data
timeSeriesData = analyticsCollector.generateTimeSeriesData(30);

// Analyze trends
trends = analyticsCollector.analyzeTrends(timeSeriesData);

// Get contributor analytics
contributorAnalytics = analyticsCollector.generateContributorAnalytics();

loading = false;
} catch (error) {
console.error('Failed to load analytics:', error);
} finally {
refreshing = false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Loading state not cleared on error, causing infinite loading spinner.

If loadAnalytics throws an error, loading remains true and the UI will be stuck showing "Loading analytics..." forever. Move loading = false to the finally block.

     async function loadAnalytics() {
         try {
             refreshing = true;
             
             // Collect all metrics
             allMetrics = analyticsCollector.collectAllMetrics();
             
             // Generate time series data
             timeSeriesData = analyticsCollector.generateTimeSeriesData(30);
             
             // Analyze trends
             trends = analyticsCollector.analyzeTrends(timeSeriesData);
             
             // Get contributor analytics
             contributorAnalytics = analyticsCollector.generateContributorAnalytics();
-            
-            loading = false;
         } catch (error) {
             console.error('Failed to load analytics:', error);
         } finally {
             refreshing = false;
+            loading = false;
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function loadAnalytics() {
try {
refreshing = true;
// Collect all metrics
allMetrics = analyticsCollector.collectAllMetrics();
// Generate time series data
timeSeriesData = analyticsCollector.generateTimeSeriesData(30);
// Analyze trends
trends = analyticsCollector.analyzeTrends(timeSeriesData);
// Get contributor analytics
contributorAnalytics = analyticsCollector.generateContributorAnalytics();
loading = false;
} catch (error) {
console.error('Failed to load analytics:', error);
} finally {
refreshing = false;
}
}
async function loadAnalytics() {
try {
refreshing = true;
// Collect all metrics
allMetrics = analyticsCollector.collectAllMetrics();
// Generate time series data
timeSeriesData = analyticsCollector.generateTimeSeriesData(30);
// Analyze trends
trends = analyticsCollector.analyzeTrends(timeSeriesData);
// Get contributor analytics
contributorAnalytics = analyticsCollector.generateContributorAnalytics();
} catch (error) {
console.error('Failed to load analytics:', error);
} finally {
refreshing = false;
loading = false;
}
}
🤖 Prompt for AI Agents
In src/routes/Analytics.svelte around lines 34 to 56, the function sets loading
= false inside the try block so if an exception is thrown loading remains true
and the UI spinner never clears; move the assignment loading = false into the
finally block (remove the loading = false from the try), ensuring both loading
and refreshing are cleared in finally so they are reset whether the async
operations succeed or fail.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive analytics dashboard for tracking platform metrics, funding trends, and contributor engagement. The dashboard provides real-time data visualization with interactive charts, trend analysis, and export functionality to help users understand campaign performance across the platform.

Key Changes:

  • Adds a complete analytics system with 6 new Svelte components for visualization and data export
  • Creates an AnalyticsDataCollector singleton class with smart caching for metrics aggregation
  • Integrates analytics navigation into both desktop and mobile menus in the main application

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
src/routes/App.svelte Adds Analytics import and navigation menu items for both desktop and mobile views
src/routes/Analytics.svelte Main dashboard component orchestrating all analytics visualizations with auto-refresh
src/lib/analytics/datacollector.ts Core data collection and aggregation logic with caching, trend analysis, and export functionality
src/lib/components/analytics/FundingProgressChart.svelte Horizontal bar chart displaying funding progress for top 10 active projects
src/lib/components/analytics/TimeSeriesChart.svelte SVG-based line chart with interactive tooltips for 30-day metric trends
src/lib/components/analytics/TrendIndicator.svelte Displays funding trends, success rates, and peak funding periods in card layout
src/lib/components/analytics/ContributorStats.svelte Grid of animated stat cards showing contributor metrics
src/lib/components/analytics/ExportReports.svelte Provides JSON/CSV export and print functionality for analytics data
src/lib/common/analytics-store.ts Derived Svelte stores for top projects and recently funded campaigns
src/lib/ergo/platform.ts Adds window.ergo TypeScript global declaration for Ergo wallet integration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +298 to +306
private convertToCSV(data: any): string {
const projects = data.projects;
if (projects.length === 0) return '';

const headers = Object.keys(projects[0]).join(',');
const rows = projects.map((p: any) =>
Object.values(p).map(v =>
typeof v === 'string' && v.includes(',') ? `"${v}"` : v
).join(',')
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSV escaping is incomplete: the code only escapes commas in strings but doesn't handle other special characters like quotes, newlines, or carriage returns. A string value containing quotes could break the CSV format. Use proper CSV escaping that wraps all string values in quotes and escapes embedded quotes by doubling them.

Suggested change
private convertToCSV(data: any): string {
const projects = data.projects;
if (projects.length === 0) return '';
const headers = Object.keys(projects[0]).join(',');
const rows = projects.map((p: any) =>
Object.values(p).map(v =>
typeof v === 'string' && v.includes(',') ? `"${v}"` : v
).join(',')
private escapeCSV(value: any): string {
if (value === null || value === undefined) return '';
const str = String(value);
// Escape double quotes by doubling them
const escaped = str.replace(/"/g, '""');
// Always wrap in double quotes if the value contains special characters or is a string
if (/[",\n\r]/.test(str) || typeof value === 'string') {
return `"${escaped}"`;
}
return str;
}
private convertToCSV(data: any): string {
const projects = data.projects;
if (projects.length === 0) return '';
const headers = Object.keys(projects[0]).join(',');
const rows = projects.map((p: any) =>
Object.values(p).map(v => this.escapeCSV(v)).join(',')

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +160
const newProjectsOnDate = allMetrics.filter(m => {
const createdDate = new Date(m.createdAt).toLocaleDateString();
return createdDate === date;
});
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date comparison using toLocaleDateString() is locale-dependent and may produce inconsistent results across different locales or time zones. This can lead to incorrect counting of new projects. Use a consistent date comparison method like comparing YYYY-MM-DD strings formatted with toISOString() or comparing date parts directly.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +203
const recentData = data.slice(-7);
const olderData = data.slice(-14, -7);
const recentAvg = recentData.reduce((sum, d) => sum + d.totalRaised, 0) / recentData.length;
const olderAvg = olderData.length > 0
? olderData.reduce((sum, d) => sum + d.totalRaised, 0) / olderData.length
: recentAvg;
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential empty array error: if data has fewer than 7 items, data.slice(-7) may return fewer than 7 items, and data.slice(-14, -7) may return an empty array or have fewer items, which could affect trend calculation accuracy. When olderData is empty, the trend is always calculated as 'stable' regardless of actual funding changes. Add validation to ensure sufficient data points exist before calculating trends.

Copilot uses AI. Check for mistakes.
.slice(0, 10)
.sort((a, b) => b.goalPercentage - a.goalPercentage);

$: maxValue = Math.max(...chartData.map(d => d.currentRaised), 1);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential runtime error: Math.max with spread operator will throw if chartData is empty. While there's an empty check in the template, the reactive statement executes before rendering and will fail when metrics.filter returns no items. Guard with: chartData.length > 0 ? Math.max(...chartData.map(d => d.currentRaised)) : 1.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,99 @@
<script lang="ts">
import { onMount } from 'svelte';
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import: onMount is imported but never used in this component. Remove this import to keep the code clean.

Suggested change
import { onMount } from 'svelte';

Copilot uses AI. Check for mistakes.
trends, and contributor statistics.
</p>

<div class="flex flex-wrap gap-3">
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this line has extra leading spaces compared to the surrounding code. The indentation should match the rest of the component structure.

Suggested change
<div class="flex flex-wrap gap-3">
<div class="flex flex-wrap gap-3">

Copilot uses AI. Check for mistakes.
<div
class="relative"
on:mouseenter={() => hoveredBar = index}
on:mouseleave={() => hoveredBar = null}
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interactive div element lacks keyboard accessibility. While role="button" and tabindex="0" are present, there's no keyboard event handler (onkeydown or onkeypress) to handle Enter or Space key presses for keyboard users to trigger the hover tooltip functionality.

Suggested change
on:mouseleave={() => hoveredBar = null}
on:mouseleave={() => hoveredBar = null}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
e.preventDefault();
hoveredBar = index;
} else if (e.key === "Escape") {
hoveredBar = null;
}
}}

Copilot uses AI. Check for mistakes.
}

const netRaised = project.sold_counter - project.refund_counter;
const goalPercentage = (netRaised / project.maximum_amount) * 100;
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division by zero risk: if project.maximum_amount is zero, this calculation will result in Infinity or NaN. Add a check to handle the case where maximum_amount is zero or use a safe division helper function.

Suggested change
const goalPercentage = (netRaised / project.maximum_amount) * 100;
const goalPercentage = project.maximum_amount > 0 ? (netRaised / project.maximum_amount) * 100 : 0;

Copilot uses AI. Check for mistakes.
variant="outline"
>
<div class="flex items-center gap-2">
<Download class="w-4 h-4" />
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading icon: the Print Report button uses a Download icon, which is inconsistent with the button's functionality. Consider using a Printer icon from lucide-svelte to better represent the print action.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +18
async function exportData(format: 'json' | 'csv') {
exporting = true;
exportError = '';
exportSuccess = false;

try {
const data = analyticsCollector.exportData(format);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSV export path uses analyticsCollector.exportData('csv'), which currently serializes project fields directly into CSV cells without neutralizing formula-like values. Because project titles and other fields are user-controlled, an attacker can start them with =/+/-/@ so that when a privileged user opens the exported CSV in Excel or similar, the cell is interpreted as a formula and can trigger external requests or other malicious actions. To mitigate this, ensure that the CSV generator sanitizes or prefixes any untrusted strings beginning with formula characters (and properly escapes quotes/newlines) before returning the CSV string used here.

Suggested change
async function exportData(format: 'json' | 'csv') {
exporting = true;
exportError = '';
exportSuccess = false;
try {
const data = analyticsCollector.exportData(format);
// Helper to sanitize CSV cells to prevent CSV injection
function sanitizeCsv(csv: string): string {
// Split into lines
return csv
.split('\n')
.map(line => {
// Split by comma, but handle quoted fields
const cells = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
current += char;
} else if (char === ',' && !inQuotes) {
cells.push(current);
current = '';
} else {
current += char;
}
}
cells.push(current);
// Sanitize each cell
const sanitized = cells.map(cell => {
// Remove surrounding quotes for checking
let trimmed = cell;
let quoteWrapped = false;
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
trimmed = trimmed.slice(1, -1);
quoteWrapped = true;
}
// If cell starts with =, +, -, or @, prefix with '
if (/^[=+\-@]/.test(trimmed)) {
trimmed = "'" + trimmed;
}
// Escape quotes by doubling them
trimmed = trimmed.replace(/"/g, '""');
// Wrap in quotes if it contains comma, quote, or newline
if (/[",\n\r]/.test(trimmed) || quoteWrapped) {
trimmed = `"${trimmed}"`;
}
return trimmed;
});
return sanitized.join(',');
})
.join('\n');
}
async function exportData(format: 'json' | 'csv') {
exporting = true;
exportError = '';
exportSuccess = false;
try {
let data = analyticsCollector.exportData(format);
if (format === 'csv') {
data = sanitizeCsv(data);
}

Copilot uses AI. Check for mistakes.
@AdityaGupta20871
Copy link
Copy Markdown
Contributor

Can you attach some screenshots or video for this! Also reference the issue which this PR solved!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants