Skip to content

feat(analytics): add metrics collection, dashboards, and exportable reports#141

Open
LavKumarShakya wants to merge 6 commits into
StabilityNexus:mainfrom
LavKumarShakya:main
Open

feat(analytics): add metrics collection, dashboards, and exportable reports#141
LavKumarShakya wants to merge 6 commits into
StabilityNexus:mainfrom
LavKumarShakya:main

Conversation

@LavKumarShakya

@LavKumarShakya LavKumarShakya commented Dec 13, 2025

Copy link
Copy Markdown

We are Team NULL

Implemented core analytics and reporting features to support data-driven insights.

Work included:

Implemented data collection for key metrics.

Created visualization components to track funding progress.

Built a contributor analysis dashboard for deeper insights.

Developed time-series analytics to analyze trends over time.

Added exportable reports functionality for sharing and offline analysis.

These additions significantly improve observability, transparency, and decision-making capabilities across the platform.

Summary by CodeRabbit

  • New Features

    • Project analytics widget with metrics: funding progress, contributor counts, refund rates, and success likelihood indicators.
    • Platform analytics dashboard with real-time KPIs and trend analysis.
    • Report export functionality supporting CSV, JSON, and HTML formats for projects and platform-wide data.
    • Analytics accessible directly from project details pages via a toggle button.
  • Documentation

    • Added comprehensive analytics guides and integration examples.

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

@coderabbitai

coderabbitai Bot commented Dec 13, 2025

Copy link
Copy Markdown

Walkthrough

This pull request introduces a complete analytics and reporting subsystem for the Benefaction Platform, comprising core metrics calculation functions, reactive Svelte stores with 5-minute caching, export utilities supporting CSV/JSON/HTML formats, seven visualization components, and an analytics dashboard route. Integration into the ProjectDetails page enables per-project analytics viewing. Comprehensive documentation covers usage patterns, API references, and integration examples.

Changes

Cohort / File(s) Summary
Documentation
ANALYTICS_QUICKSTART.md, ANALYTICS_README.md, IMPLEMENTATION_SUMMARY.md, ANALYTICS_UPDATE.md, INTEGRATION_EXAMPLE.md
New documentation files detailing the analytics module features, three-step quickstart guide, component props, usage patterns, integration examples, and migration from the standalone analytics page to project-scoped analytics.
Core Analytics Metrics
src/lib/analytics/metrics.ts
New module exporting metric calculation functions (calculateProjectMetrics, calculateContributorMetrics, calculatePlatformMetrics) and time-series generators (generateFundingTimeSeries, generateContributorTimeSeries) with TypeScript interfaces for ProjectMetrics, ContributorMetrics, PlatformMetrics, and TimeSeriesDataPoint.
Analytics Store & Caching
src/lib/analytics/store.ts
New module exporting Svelte stores (currentBlockHeight, selectedProjectForAnalytics, analyticsCache, projectMetricsMap) and cache management utilities (updateAnalyticsCache, isCacheValid, getPlatformMetrics, getContributorMetrics) with 5-minute TTL.
Export Utilities
src/lib/analytics/export.ts
New module exporting report generation functions (generateProjectReport, generatePlatformReport) and format-specific exporters (exportToCSV, exportToJSON, exportToHTML) plus download orchestration (downloadReport, exportReport). Defines ReportFormat and ReportData types.
Analytics Public API
src/lib/analytics/index.ts
New barrel module re-exporting metrics types and functions, store exports, and export utilities as a unified public API surface.
Chart & Funding Analytics Components
src/lib/components/analytics/Chart.svelte, src/lib/components/analytics/FundingProgress.svelte, src/lib/components/analytics/ProjectAnalytics.svelte
New Svelte components: Chart (configurable line/bar/area visualizations with grid/axes), FundingProgress (funding metrics card with progress bar), and ProjectAnalytics (full-featured campaign analytics widget with overview/details tabs).
Contributor & Platform Analytics Components
src/lib/components/analytics/ContributorDashboard.svelte, src/lib/components/analytics/TimeSeriesAnalytics.svelte, src/lib/components/analytics/PlatformOverview.svelte
New Svelte components: ContributorDashboard (contributor metrics and engagement meter), TimeSeriesAnalytics (dual time-series charts with trend indicators and insights), PlatformOverview (platform-wide KPI cards).
Export & Component Exports
src/lib/components/analytics/ExportReports.svelte, src/lib/components/analytics/index.ts
New ExportReports component (format selection and report export UI) and barrel export module for all analytics components.
ProjectDetails Integration
src/routes/ProjectDetails.svelte
Added analytics toggle button and conditional rendering of ProjectAnalytics component; tracks current block height for metrics.

Sequence Diagram

sequenceDiagram
    participant User
    participant ProjectDetails
    participant ProjectAnalytics as ProjectAnalytics<br/>Component
    participant MetricsCalc as Metrics<br/>Calculator
    participant Store as Analytics<br/>Store
    participant ExportUI as ExportReports<br/>Component
    participant ExportMod as Export<br/>Module
    participant Browser as Browser<br/>Download

    User->>ProjectDetails: Click "Show Analytics"
    ProjectDetails->>Store: Get currentBlockHeight
    ProjectDetails->>ProjectAnalytics: Render with project,<br/>currentBlockHeight
    ProjectAnalytics->>MetricsCalc: calculateProjectMetrics(project)
    MetricsCalc-->>ProjectAnalytics: Return ProjectMetrics
    ProjectAnalytics->>ProjectAnalytics: Render overview/details<br/>tabs with metrics
    
    alt User switches to export
        User->>ExportUI: Select export format<br/>(CSV/JSON/HTML)
        ExportUI->>ExportMod: exportReport(data, format)
        ExportMod->>MetricsCalc: Generate report data
        MetricsCalc-->>ExportMod: Return ReportData
        ExportMod->>ExportMod: Format to CSV/JSON/HTML
        ExportMod->>Browser: Trigger download
        Browser-->>User: File downloaded
    end
    
    alt Cached metrics scenario
        User->>Store: getPlatformMetrics()
        Store->>Store: isCacheValid()?
        alt Cache valid
            Store-->>User: Return cached metrics
        else Cache expired
            Store->>MetricsCalc: Recalculate metrics
            MetricsCalc-->>Store: Return fresh metrics
            Store->>Store: Update cache + timestamp
            Store-->>User: Return metrics
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • src/lib/analytics/metrics.ts — Verify metric calculation logic is correct, particularly success_likelihood weighting, time_remaining heuristics, and handling of edge cases (zero contributors, inactive projects)
  • src/lib/analytics/store.ts — Review caching strategy (5-minute TTL validity check, concurrent update scenarios), derived store subscription behavior, and consistency between cache invalidation and store updates
  • src/lib/analytics/export.ts — Examine format-specific serialization (CSV escaping, HTML entity encoding, JSON structure), error handling during export, and memory efficiency for large datasets
  • ProjectDetails.svelte integration — Confirm block height fetching doesn't cause layout shifts, verify analytics button placement doesn't conflict with existing UI, and test state management during navigation
  • Cross-component prop passing — Validate that all 7 UI components correctly receive and use metrics/data props; check for unnecessary re-renders due to reactive dependencies

Possibly related issues

  • Issue #41 (Project Analytics) — This PR implements the complete analytics feature set requested: core metrics calculation modules, Svelte visualization components (FundingProgress, ContributorDashboard, TimeSeriesAnalytics, Chart), export utilities, reactive stores with caching, and integration into the project details view.

Poem

🐰 Metrics hop and numbers bloom,
Charts that light up any room,
Cache it fresh, five minutes true,
CSV, JSON, HTML too—
Analytics hops now, all brand new! 📊

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 title clearly and specifically summarizes the main change: adding analytics features with metrics collection, dashboards, and exportable reports. It aligns well with the substantial analytics module introduction across 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🧹 Nitpick comments (12)
src/lib/analytics/store.ts (1)

24-37: projectMetricsMap is a derived store returning a function — easy to misuse / lose reactivity.
Right now it only “updates” when currentBlockHeight changes, but projects changes aren’t tracked unless every caller re-invokes the returned function manually (and the name suggests it’s a Map, not a function). Consider making this a pure helper (or a derived store over a projects store).

-export const projectMetricsMap = derived(
-    [currentBlockHeight],
-    ([$currentBlockHeight]: [number]) => {
-        return (projects: Project[]): Map<string, ProjectMetrics> => {
-            const metricsMap = new Map<string, ProjectMetrics>();
-            for (const project of projects) {
-                const metrics = calculateProjectMetrics(project, $currentBlockHeight);
-                metricsMap.set(project.project_id, metrics);
-            }
-            return metricsMap;
-        };
-    }
-);
+export function calculateProjectMetricsMap(
+    projects: Project[],
+    currentHeight: number
+): Map<string, ProjectMetrics> {
+    const metricsMap = new Map<string, ProjectMetrics>();
+    for (const project of projects) {
+        metricsMap.set(project.project_id, calculateProjectMetrics(project, currentHeight));
+    }
+    return metricsMap;
+}
src/routes/ProjectDetails.svelte (1)

41-49: Fetch block height lazily (only when analytics is opened) to avoid an unconditional network call.
Right now platform.get_current_height() runs during load() even if the user never clicks “Show Analytics”. Consider:

 let showAnalytics = false;
 let currentBlockHeight = 0;
+let analyticsHeightLoaded = false;

+async function toggleAnalytics() {
+    showAnalytics = !showAnalytics;
+    if (showAnalytics && !analyticsHeightLoaded) {
+        try {
+            currentBlockHeight = await platform.get_current_height();
+            analyticsHeightLoaded = true;
+        } catch (error) {
+            console.error("Error fetching current height:", error);
+        }
+    }
+}
 ...
-<Button class="analytics-btn" on:click={() => showAnalytics = !showAnalytics}>
+<Button class="analytics-btn" on:click={toggleAnalytics}>

Optionally, render a small “loading height…” state in the analytics area when currentBlockHeight === 0 / not loaded.

Also applies to: 597-608, 805-823

ANALYTICS_README.md (1)

248-267: Snippet uses getPlatformMetrics and get(...) but doesn’t show imports.
To reduce copy/paste friction, consider making the snippet self-contained (import get from svelte/store and import getPlatformMetrics alongside the other store exports).

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

7-23: Extract duplicate formatting logic into a shared utility.

The formatNumber and formatCurrency functions share identical thresholding logic (1M, 1K). The only difference is the decimal precision for values below 1K.

Consider creating a shared helper:

+function formatWithPrecision(value: number, precision: number): string {
+    if (value >= 1_000_000) {
+        return `${(value / 1_000_000).toFixed(2)}M`;
+    } else if (value >= 1_000) {
+        return `${(value / 1_000).toFixed(2)}K`;
+    }
+    return value.toFixed(precision);
+}
+
-function formatNumber(value: number): string {
-    if (value >= 1_000_000) {
-        return `${(value / 1_000_000).toFixed(2)}M`;
-    } else if (value >= 1_000) {
-        return `${(value / 1_000).toFixed(2)}K`;
-    }
-    return value.toFixed(0);
-}
+function formatNumber(value: number): string {
+    return formatWithPrecision(value, 0);
+}

-function formatCurrency(value: number): string {
-    if (value >= 1_000_000) {
-        return `${(value / 1_000_000).toFixed(2)}M`;
-    } else if (value >= 1_000) {
-        return `${(value / 1_000).toFixed(2)}K`;
-    }
-    return value.toFixed(2);
-}
+function formatCurrency(value: number): string {
+    return formatWithPrecision(value, 2);
+}
src/lib/components/analytics/ExportReports.svelte (1)

42-44: Replace blocking alert with user-friendly error notification.

Using alert() for error notification provides poor UX—it blocks the entire UI and looks unprofessional. Consider using a toast notification system or inline error message.

+let errorMessage = "";
+
 async function handleExport() {
     isExporting = true;
+    errorMessage = "";
     
     try {
         // ... export logic ...
     } catch (error) {
         console.error("Export failed:", error);
-        alert("Failed to export report. Please try again.");
+        errorMessage = "Failed to export report. Please try again.";
     } finally {
         isExporting = false;
     }
 }

Then display the error in the template:

{#if errorMessage}
    <div class="error-banner">
        {errorMessage}
    </div>
{/if}
src/lib/components/analytics/FundingProgress.svelte (1)

3-3: Remove unused Progress component import.

The Progress component is imported but never used in the template.

-import Progress from "$lib/components/ui/progress/progress.svelte";
src/lib/components/analytics/Chart.svelte (2)

24-27: Improve single data point handling.

When data.length === 1, the function returns leftPadding, placing the single point at the left edge. It would be more visually centered to place it in the middle of the plot area.

 function getX(index: number): number {
     if (data.length <= 1) {
-        return leftPadding;
+        return leftPadding + plotWidth / 2;
     }
     return leftPadding + (index / (data.length - 1)) * plotWidth;
 }

114-117: Clarify bar chart width calculation.

The bar positioning uses (plotWidth / data.length / 2) + 5 for x-offset and (plotWidth / data.length) - 10 for width. The asymmetric padding (5 on one side, 10 total) is unclear and may cause bars to appear off-center.

Consider symmetric padding:

+{@const barWidth = (plotWidth / data.length) - 10}
+{@const barOffset = 5}
 <rect
-    x={getX(i) - (plotWidth / data.length / 2) + 5}
+    x={getX(i) - barWidth / 2}
     y={getY(point.value)}
-    width={(plotWidth / data.length) - 10}
+    width={barWidth}
     height={getY(minValue) - getY(point.value)}
     fill={color}
     opacity="0.8"
 />
src/lib/components/analytics/ProjectAnalytics.svelte (3)

2-2: Remove unused import.

onMount is imported but never used in this component.

 <script lang="ts">
-    import { onMount } from 'svelte';
     import { type Project } from '$lib/common/project';

28-35: Consider extracting block duration constant to avoid magic numbers.

The time calculation uses magic numbers (2 minutes per block, 2 * 60 * 1000). Extract these to a shared constant for consistency and maintainability.

+    const MS_PER_BLOCK = 2 * 60 * 1000; // 2 minutes per block
+    
     function formatTime(blocks: number): string {
-        const minutes = blocks * 2;
+        const minutes = blocks * (MS_PER_BLOCK / 60000);
         if (minutes < 60) return `${minutes}m`;
         const hours = Math.floor(minutes / 60);
         if (hours < 24) return `${hours}h`;
         const days = Math.floor(hours / 24);
         return `${days}d`;
     }

And update line 183:

-                        {metrics.time_remaining && metrics.time_remaining > 0 ? formatTime(Math.floor(metrics.time_remaining / (2 * 60 * 1000))) : 'Ended'}
+                        {metrics.time_remaining && metrics.time_remaining > 0 ? formatTime(Math.floor(metrics.time_remaining / MS_PER_BLOCK)) : 'Ended'}

Also applies to: 181-184


48-65: Improve tab accessibility with ARIA attributes.

The tab switcher lacks ARIA attributes for screen reader support. Consider adding role="tablist", role="tab", and aria-selected for better accessibility.

-        <div class="tab-switcher">
+        <div class="tab-switcher" role="tablist">
             <button 
                 class="tab-btn"
                 class:active={activeTab === 'overview'}
                 on:click={() => activeTab = 'overview'}
+                role="tab"
+                aria-selected={activeTab === 'overview'}
             >
                 <BarChart3 size={16} />
                 Overview
             </button>
             <button 
                 class="tab-btn"
                 class:active={activeTab === 'details'}
                 on:click={() => activeTab = 'details'}
+                role="tab"
+                aria-selected={activeTab === 'details'}
             >
                 <Target size={16} />
                 Details
             </button>
         </div>
src/lib/components/analytics/TimeSeriesAnalytics.svelte (1)

18-40: Reconsider trend calculation when first value is zero.

When firstValue is 0, returning { direction: "up", percentage: 100 } is arbitrary and potentially misleading. If funding went from 0 to 1000, reporting "100% up" understates the growth. Consider returning "N/A" or calculating based on absolute change.

     function calculateTrend(data: TimeSeriesDataPoint[]): { direction: "up" | "down" | "stable", percentage: number } {
         if (data.length < 2) {
             return { direction: "stable", percentage: 0 };
         }
         
         const firstValue = data[0].value;
         const lastValue = data[data.length - 1].value;
         
         if (firstValue === 0) {
-            return { direction: "up", percentage: 100 };
+            // Can't calculate percentage change from zero; use absolute indicator
+            return { direction: lastValue > 0 ? "up" : "stable", percentage: lastValue > 0 ? 100 : 0 };
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e1ac3b and 2794f25.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • ANALYTICS_QUICKSTART.md (1 hunks)
  • ANALYTICS_README.md (1 hunks)
  • ANALYTICS_UPDATE.md (1 hunks)
  • IMPLEMENTATION_SUMMARY.md (1 hunks)
  • INTEGRATION_EXAMPLE.md (1 hunks)
  • src/lib/analytics/export.ts (1 hunks)
  • src/lib/analytics/index.ts (1 hunks)
  • src/lib/analytics/metrics.ts (1 hunks)
  • src/lib/analytics/store.ts (1 hunks)
  • src/lib/components/analytics/Chart.svelte (1 hunks)
  • src/lib/components/analytics/ContributorDashboard.svelte (1 hunks)
  • src/lib/components/analytics/ExportReports.svelte (1 hunks)
  • src/lib/components/analytics/FundingProgress.svelte (1 hunks)
  • src/lib/components/analytics/PlatformOverview.svelte (1 hunks)
  • src/lib/components/analytics/ProjectAnalytics.svelte (1 hunks)
  • src/lib/components/analytics/TimeSeriesAnalytics.svelte (1 hunks)
  • src/lib/components/analytics/index.ts (1 hunks)
  • src/routes/ProjectDetails.svelte (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/lib/analytics/store.ts (2)
src/lib/common/project.ts (1)
  • Project (60-85)
src/lib/analytics/metrics.ts (6)
  • ProjectMetrics (3-16)
  • calculateProjectMetrics (48-105)
  • PlatformMetrics (34-43)
  • ContributorMetrics (18-26)
  • calculatePlatformMetrics (146-200)
  • calculateContributorMetrics (110-141)
src/lib/analytics/export.ts (2)
src/lib/analytics/metrics.ts (6)
  • PlatformMetrics (34-43)
  • ProjectMetrics (3-16)
  • ContributorMetrics (18-26)
  • calculateProjectMetrics (48-105)
  • calculatePlatformMetrics (146-200)
  • calculateContributorMetrics (110-141)
src/lib/common/project.ts (1)
  • Project (60-85)
src/lib/analytics/metrics.ts (1)
src/lib/common/project.ts (1)
  • Project (60-85)
🪛 markdownlint-cli2 (0.18.1)
ANALYTICS_QUICKSTART.md

10-10: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


11-11: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


16-16: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


18-18: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


20-20: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


26-26: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


27-27: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


28-28: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)

🔇 Additional comments (9)
ANALYTICS_QUICKSTART.md (1)

5-10: Verify /analytics is still a valid route.
Quickstart instructs users to open /analytics; please confirm the route exists on this branch and won’t 404 (or update the doc to the new entry point).

ANALYTICS_README.md (1)

50-52: Verify /analytics route + file path references match what’s actually shipped.
The README points to src/routes/analytics/+page.svelte and /analytics; please confirm those exist and are intended to remain public entry points.

Also applies to: 287-297

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

48-90: [rewritten comment]
[classification tag]

src/lib/components/analytics/index.ts (1)

1-10: LGTM!

The barrel export pattern is correctly implemented, consolidating all analytics component exports for convenient imports.

ANALYTICS_UPDATE.md (1)

1-89: LGTM!

The documentation clearly explains the analytics integration changes, including what was removed, what was added, and how to use the new per-campaign analytics feature.

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

1-53: Well-structured time series analytics component.

The reactive computations, trend calculations, and fallback handling are implemented correctly. The component properly guards against edge cases like empty arrays and zero denominators.

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

322-338: Clean download implementation.

The download function properly manages the blob URL lifecycle by calling URL.revokeObjectURL after the download is triggered, preventing memory leaks.

IMPLEMENTATION_SUMMARY.md (1)

1-265: Comprehensive implementation documentation.

The summary provides clear documentation of the analytics subsystem including file structure, features, usage examples, and integration points. The note about simulated historical data (line 256) appropriately sets expectations for production use.

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

1-40: Well-organized barrel exports.

The module correctly separates type exports from runtime exports and provides a clean public API surface for the analytics subsystem.

Comment thread ANALYTICS_QUICKSTART.md Outdated
Comment on lines +12 to +57
The dashboard will automatically load and display:
- Platform overview with key metrics
- Project performance tracking
- Contributor analysis
- Trend visualizations
- Export options

### Step 2: Explore the Tabs

**Overview Tab**: See platform-wide statistics
- Total projects (active, completed, failed)
- Total funds raised
- Contributor counts
- Success rates

**Projects Tab**: View individual project performance
- Funding progress bars
- Contribution statistics
- Time remaining
- Success likelihood

**Contributors Tab**: Analyze contributor behavior
- Active vs. total contributors
- Average contribution sizes
- Refund rates
- Engagement metrics

**Trends Tab**: Examine time-series data
- Select a project to view trends
- Funding growth over time
- Contributor acquisition

**Export Tab**: Download reports
- Choose format (CSV, JSON, HTML)
- Download platform or project reports
- Share with stakeholders

### Step 3: Export Reports

1. Click the "Export" tab
2. Select your preferred format:
- **CSV**: Open in Excel/Google Sheets
- **JSON**: Use programmatically
- **HTML**: View in browser or print
3. Click "Download Report"

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 | 🟡 Minor

Fix markdownlint MD007 list indentation warnings.
This file currently fails markdownlint-cli2 (MD007 ul-indent). Please re-indent the affected unordered lists to match the repo’s lint config.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

14-14: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


15-15: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


16-16: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


18-18: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


19-19: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


20-20: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


22-22: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


26-26: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


27-27: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


28-28: Unordered list indentation
Expected: 4; Actual: 7

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)

🤖 Prompt for AI Agents
In ANALYTICS_QUICKSTART.md around lines 12 to 57, several unordered lists are
mis-indented and trigger markdownlint MD007 (ul-indent); fix by normalizing list
indentation to the repo's markdownlint config (use consistent 2-space or 4-space
indent as configured) so nested bullets (e.g., items under "The dashboard will
automatically load...", each Tab section bullet lists, and the Export step
sublists) align uniformly; ensure top-level bullets start at column 1 (no extra
leading spaces) and nested bullets are indented exactly one level per nesting,
and update all list items in this range to that consistent indentation.

Comment thread ANALYTICS_QUICKSTART.md Outdated
Comment on lines +62 to +87
```svelte
<script lang="ts">
import { PlatformOverview } from '$lib/components/analytics';
import { getPlatformMetrics } from '$lib/analytics';
import { projects } from '$lib/common/store';

let currentHeight = 0;

// Fetch current height
onMount(async () => {
if ($projects.data.size > 0) {
const firstProject = Array.from($projects.data.values())[0];
currentHeight = await firstProject.platform.get_current_height();
}
});

$: allProjects = Array.from($projects.data.values());
$: platformMetrics = allProjects.length > 0
? getPlatformMetrics(allProjects, currentHeight)
: null;
</script>

{#if platformMetrics}
<PlatformOverview metrics={platformMetrics} />
{/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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "ProjectDetails.svelte" --exec cat -n {} \;

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 50395


🏁 Script executed:

rg "get_current_height" --max-count=20 -B 2 -A 2

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 4670


🏁 Script executed:

rg "ErgoPlatform" -t ts -t svelte --max-count=10 -B 1 -A 1

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 113


🏁 Script executed:

cat -n ANALYTICS_QUICKSTART.md | sed -n '50,95p'

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 1644


🏁 Script executed:

rg "type Project|interface Project" --max-count=5 -A 10

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 17660


🏁 Script executed:

rg "platform.*Platform" src/lib/common/project.ts -A 5 -B 5

Repository: StabilityNexus/BenefactionPlatform-Ergo

Length of output: 376


Add missing onMount import to the Svelte script.

The code snippet is missing import { onMount } from 'svelte'; at the top of the <script> block since onMount is used on line 71.

 <script lang="ts">
+    import { onMount } from 'svelte';
     import { PlatformOverview } from '$lib/components/analytics';

The height API usage firstProject.platform.get_current_height() is correct and matches the established pattern in the codebase (e.g., INTEGRATION_EXAMPLE.md and src/lib/common/project.ts use the same pattern).

🤖 Prompt for AI Agents
In ANALYTICS_QUICKSTART.md around lines 62 to 87, the Svelte script uses onMount
but forgets to import it; add "import { onMount } from 'svelte';" to the script
imports (alongside the existing imports) so the onMount call works; keep the
rest of the snippet unchanged.

Comment thread INTEGRATION_EXAMPLE.md Outdated
Comment on lines +18 to +27
onMount(async () => {
if (projects.length > 0) {
try {
blockHeight = await projects[0].platform.get_current_height();
updateAnalyticsCache(projects, blockHeight);
} catch (error) {
console.error("Failed to load analytics:", error);
}
}
});

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

Address potential race condition in onMount.

The onMount callback checks projects.length > 0, but since projects is derived from a reactive store ($projectsStore.data), it may be empty when onMount first executes and populate later. This means analytics initialization could be skipped entirely.

Use a reactive statement instead to trigger analytics updates whenever projects become available:

-onMount(async () => {
-    if (projects.length > 0) {
-        try {
-            blockHeight = await projects[0].platform.get_current_height();
-            updateAnalyticsCache(projects, blockHeight);
-        } catch (error) {
-            console.error("Failed to load analytics:", error);
-        }
-    }
-});
+$: if (projects.length > 0) {
+    (async () => {
+        try {
+            blockHeight = await projects[0].platform.get_current_height();
+            updateAnalyticsCache(projects, blockHeight);
+        } catch (error) {
+            console.error("Failed to load analytics:", error);
+        }
+    })();
+}
📝 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
onMount(async () => {
if (projects.length > 0) {
try {
blockHeight = await projects[0].platform.get_current_height();
updateAnalyticsCache(projects, blockHeight);
} catch (error) {
console.error("Failed to load analytics:", error);
}
}
});
$: if (projects.length > 0) {
(async () => {
try {
blockHeight = await projects[0].platform.get_current_height();
updateAnalyticsCache(projects, blockHeight);
} catch (error) {
console.error("Failed to load analytics:", error);
}
})();
}
🤖 Prompt for AI Agents
In INTEGRATION_EXAMPLE.md around lines 18 to 27, the onMount block checks
projects.length but may run before the reactive projects store populates,
causing analytics to be skipped; replace that one-time onMount check with a
reactive statement that watches the projects store (or $projectsStore.data) and
runs the same async logic whenever projects becomes non-empty: when
projects.length > 0, await projects[0].platform.get_current_height(), call
updateAnalyticsCache(projects, blockHeight), and handle errors in the same
catch, ensuring the logic runs on future updates rather than only on initial
mount.

Comment on lines +7 to +12
interface ReportData {
generated_at: string;
platform_metrics?: PlatformMetrics;
project_metrics?: ProjectMetrics[];
contributor_metrics?: ContributorMetrics;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Export ReportData interface for consumer type safety.

ReportData is used as return type for generateProjectReport and generatePlatformReport, but it's not exported. Consumers won't be able to properly type variables holding report data.

-interface ReportData {
+export interface ReportData {
     generated_at: string;
     platform_metrics?: PlatformMetrics;
     project_metrics?: ProjectMetrics[];
     contributor_metrics?: ContributorMetrics;
 }

Also update src/lib/analytics/index.ts to re-export it:

export type { ReportFormat, ReportData } from './export';
🤖 Prompt for AI Agents
In src/lib/analytics/export.ts around lines 7 to 12, the ReportData interface is
declared but not exported; update the file to export the interface (e.g., add
export before interface) so callers can import its type for
generateProjectReport/generatePlatformReport return values. Also update
src/lib/analytics/index.ts to re-export the type by adding a line to export
ReportData alongside ReportFormat (e.g., export type { ReportFormat, ReportData
} from './export';) so downstream consumers can import the ReportData type from
the analytics barrel.

Comment on lines +82 to +84
for (const pm of data.project_metrics) {
csv += `"${pm.project_id}","${pm.title.replace(/"/g, '""')}",${pm.funding_progress},${pm.total_raised},${pm.total_goal},${pm.contributor_count},${pm.average_contribution},${pm.refund_rate},${pm.exchange_activity},${pm.time_remaining || "N/A"},${pm.is_active},${pm.success_likelihood}\n`;
}

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

Mitigate CSV injection vulnerability.

User-provided fields like title could contain formula injection payloads (e.g., =CMD|'/C calc'!A0). When opened in Excel, these execute as formulas. Prefix cells starting with =, +, -, @, \t, or \r with a single quote.

+function sanitizeCSVValue(value: string): string {
+    // Prevent CSV injection by prefixing dangerous characters
+    if (/^[=+\-@\t\r]/.test(value)) {
+        return `'${value}`;
+    }
+    return value;
+}
+
         for (const pm of data.project_metrics) {
-            csv += `"${pm.project_id}","${pm.title.replace(/"/g, '""')}",${pm.funding_progress},${pm.total_raised},${pm.total_goal},${pm.contributor_count},${pm.average_contribution},${pm.refund_rate},${pm.exchange_activity},${pm.time_remaining || "N/A"},${pm.is_active},${pm.success_likelihood}\n`;
+            csv += `"${sanitizeCSVValue(pm.project_id)}","${sanitizeCSVValue(pm.title.replace(/"/g, '""'))}",${pm.funding_progress},${pm.total_raised},${pm.total_goal},${pm.contributor_count},${pm.average_contribution},${pm.refund_rate},${pm.exchange_activity},${pm.time_remaining || "N/A"},${pm.is_active},${pm.success_likelihood}\n`;
         }
📝 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
for (const pm of data.project_metrics) {
csv += `"${pm.project_id}","${pm.title.replace(/"/g, '""')}",${pm.funding_progress},${pm.total_raised},${pm.total_goal},${pm.contributor_count},${pm.average_contribution},${pm.refund_rate},${pm.exchange_activity},${pm.time_remaining || "N/A"},${pm.is_active},${pm.success_likelihood}\n`;
}
function sanitizeCSVValue(value: string): string {
// Prevent CSV injection by prefixing dangerous characters
if (/^[=+\-@\t\r]/.test(value)) {
return `'${value}`;
}
return value;
}
for (const pm of data.project_metrics) {
csv += `"${sanitizeCSVValue(pm.project_id)}","${sanitizeCSVValue(pm.title.replace(/"/g, '""'))}",${pm.funding_progress},${pm.total_raised},${pm.total_goal},${pm.contributor_count},${pm.average_contribution},${pm.refund_rate},${pm.exchange_activity},${pm.time_remaining || "N/A"},${pm.is_active},${pm.success_likelihood}\n`;
}
🤖 Prompt for AI Agents
In src/lib/analytics/export.ts around lines 82 to 84, the CSV row construction
uses user-provided strings (e.g., title) and is vulnerable to CSV/Excel formula
injection; before wrapping fields in quotes, escape existing double-quotes as
is, and if a string field begins with any of the characters =, +, -, @, tab (\t)
or carriage return (\r), prefix that field with a single quote to neutralize
formulas; apply this sanitization to all user-controlled string columns (title
and any other string fields), then proceed to quote the value and build the CSV
row as before.

Comment on lines +34 to +41
function formatValue(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1)}M`;
} else if (value >= 1_000) {
return `${(value / 1_000).toFixed(1)}K`;
}
return value.toFixed(0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicate formatValue utility.

Yet another duplicate formatting function. Consolidate with the other formatters in a shared utility module.

🤖 Prompt for AI Agents
In src/lib/components/analytics/Chart.svelte around lines 34 to 41, the local
formatValue function duplicates formatting logic present elsewhere; extract and
reuse a shared utility. Remove the function from Chart.svelte and import a
single exported formatter (e.g., formatNumber or formatValue) from the existing
shared utils module (or create one under src/lib/utils/formatters.ts if none
exists), update all call sites in this file to use the imported function, and
run a project-wide search to replace other duplicates with the shared utility to
avoid future divergence.

Comment on lines +25 to +27
$: retentionRate = metrics.total_contributions > 0
? ((metrics.active_contributors / metrics.total_contributions) * 100).toFixed(1)
: "0";

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

Fix incorrect retention rate calculation.

The retention rate divides active_contributors by total_contributions, which compares counts of people to counts of transactions. Retention rate should compare active contributors to total unique contributors.

Apply this fix:

 $: retentionRate = metrics.total_contributions > 0 
-    ? ((metrics.active_contributors / metrics.total_contributions) * 100).toFixed(1)
+    ? ((metrics.active_contributors / metrics.total_unique_contributors) * 100).toFixed(1)
     : "0";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/lib/components/analytics/ContributorDashboard.svelte around lines 25 to
27, the retention rate calculation incorrectly divides active_contributors by
total_contributions; change it to divide active_contributors by
total_contributors (i.e., total unique contributors) and keep the same
zero-check and formatting: check that metrics.total_contributors > 0, compute
(metrics.active_contributors / metrics.total_contributors) * 100, and call
toFixed(1) (or return "0" when zero) so the value represents percentage of
retained contributors correctly.

Comment on lines +12 to +19
function formatCurrency(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(2)}M`;
} else if (value >= 1_000) {
return `${(value / 1_000).toFixed(2)}K`;
}
return value.toFixed(2);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Eliminate duplicate formatCurrency function.

The formatCurrency function is duplicated across multiple components (ContributorDashboard, FundingProgress, and others). Extract it to a shared utility module as suggested in the PlatformOverview review.

🤖 Prompt for AI Agents
In src/lib/components/analytics/FundingProgress.svelte around lines 12 to 19,
the local formatCurrency function is duplicated across multiple components;
extract it into a shared utility (e.g., src/lib/utils/format.ts or similar),
export a named formatCurrency function, update FundingProgress.svelte to import
and use that shared function, and remove the local implementation; ensure other
components (ContributorDashboard, PlatformOverview, etc.) are updated to import
the same utility to eliminate duplicates and run tests/linters to confirm no
unused imports remain.

Comment on lines +7 to +14
function formatNumber(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(2)}M`;
} else if (value >= 1_000) {
return `${(value / 1_000).toFixed(2)}K`;
}
return value.toFixed(0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consolidate duplicate formatting utilities.

The formatNumber function is duplicated across multiple analytics components (ContributorDashboard, PlatformOverview, and likely others). This violates DRY principles and makes maintenance harder.

Extract shared formatting functions to a common utility module:

// src/lib/analytics/formatters.ts
export function formatNumber(value: number): string {
    if (value >= 1_000_000) {
        return `${(value / 1_000_000).toFixed(2)}M`;
    } else if (value >= 1_000) {
        return `${(value / 1_000).toFixed(2)}K`;
    }
    return value.toFixed(0);
}

export function formatCurrency(value: number): string {
    if (value >= 1_000_000) {
        return `${(value / 1_000_000).toFixed(2)}M`;
    } else if (value >= 1_000) {
        return `${(value / 1_000).toFixed(2)}K`;
    }
    return value.toFixed(2);
}

Then import in all components:

+import { formatNumber } from "$lib/analytics/formatters";
-function formatNumber(value: number): string {
-    if (value >= 1_000_000) {
-        return `${(value / 1_000_000).toFixed(2)}M`;
-    } else if (value >= 1_000) {
-        return `${(value / 1_000).toFixed(2)}K`;
-    }
-    return value.toFixed(0);
-}
🤖 Prompt for AI Agents
In src/lib/components/analytics/PlatformOverview.svelte around lines 7 to 14,
the local formatNumber duplication should be removed and replaced with a shared
utility; extract formatNumber (and optionally formatCurrency) into a new module
(e.g., src/lib/analytics/formatters.ts) that exports the formatting functions,
update this component to import formatNumber from that module, and remove the
local implementation so all analytics components use the centralized utility for
consistent, maintainable formatting.

Comment on lines +162 to +165
<div class="detail-row">
<span class="detail-label">Remaining</span>
<span class="detail-value">{formatCurrency(metrics.total_goal - metrics.total_raised)}</span>
</div>

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 | 🟡 Minor

Handle overfunded scenario gracefully.

If total_raised exceeds total_goal, this displays a negative remaining amount. Consider capping at zero or showing an "Overfunded" indicator.

             <div class="detail-row">
                 <span class="detail-label">Remaining</span>
-                <span class="detail-value">{formatCurrency(metrics.total_goal - metrics.total_raised)}</span>
+                <span class="detail-value">
+                    {metrics.total_raised >= metrics.total_goal 
+                        ? 'Fully Funded' 
+                        : formatCurrency(metrics.total_goal - metrics.total_raised)}
+                </span>
             </div>
📝 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
<div class="detail-row">
<span class="detail-label">Remaining</span>
<span class="detail-value">{formatCurrency(metrics.total_goal - metrics.total_raised)}</span>
</div>
<div class="detail-row">
<span class="detail-label">Remaining</span>
<span class="detail-value">
{metrics.total_raised >= metrics.total_goal
? 'Fully Funded'
: formatCurrency(metrics.total_goal - metrics.total_raised)}
</span>
</div>
🤖 Prompt for AI Agents
In src/lib/components/analytics/ProjectAnalytics.svelte around lines 162 to 165,
the remaining amount can become negative when metrics.total_raised >
metrics.total_goal; change the rendering to compute a safe remaining value and
display either the capped zero amount or an "Overfunded" indicator: compute
remaining = metrics.total_goal - metrics.total_raised, if remaining <= 0 render
a localized "Overfunded" label (optionally with the overfunded amount formatted,
e.g. formatCurrency(Math.abs(remaining))), otherwise render
formatCurrency(remaining); ensure this logic is placed in the component script
and the template uses the computed value/condition.

@AdityaGupta20871

Copy link
Copy Markdown
Contributor

Remove the .md files from the PR also attach a screenshot or video for quick review!

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.

3 participants