feat(analytics): add metrics collection, dashboards, and exportable reports#141
feat(analytics): add metrics collection, dashboards, and exportable reports#141LavKumarShakya wants to merge 6 commits into
Conversation
WalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas requiring extra attention:
Possibly related issues
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (12)
src/lib/analytics/store.ts (1)
24-37:projectMetricsMapis a derived store returning a function — easy to misuse / lose reactivity.
Right now it only “updates” whencurrentBlockHeightchanges, butprojectschanges 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 aprojectsstore).-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 nowplatform.get_current_height()runs duringload()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 usesgetPlatformMetricsandget(...)but doesn’t show imports.
To reduce copy/paste friction, consider making the snippet self-contained (importgetfromsvelte/storeand importgetPlatformMetricsalongside the other store exports).src/lib/components/analytics/ContributorDashboard.svelte (1)
7-23: Extract duplicate formatting logic into a shared utility.The
formatNumberandformatCurrencyfunctions 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
Progresscomponent 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 returnsleftPadding, 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) + 5for x-offset and(plotWidth / data.length) - 10for 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.
onMountis 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 (
2minutes 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", andaria-selectedfor 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
firstValueis 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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/analyticsis 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/analyticsroute + file path references match what’s actually shipped.
The README points tosrc/routes/analytics/+page.svelteand/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.revokeObjectURLafter 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.
| 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" | ||
|
|
There was a problem hiding this comment.
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.
| ```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} | ||
| ``` |
There was a problem hiding this comment.
🧩 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 2Repository: StabilityNexus/BenefactionPlatform-Ergo
Length of output: 4670
🏁 Script executed:
rg "ErgoPlatform" -t ts -t svelte --max-count=10 -B 1 -A 1Repository: 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 10Repository: StabilityNexus/BenefactionPlatform-Ergo
Length of output: 17660
🏁 Script executed:
rg "platform.*Platform" src/lib/common/project.ts -A 5 -B 5Repository: 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.
| 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); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| interface ReportData { | ||
| generated_at: string; | ||
| platform_metrics?: PlatformMetrics; | ||
| project_metrics?: ProjectMetrics[]; | ||
| contributor_metrics?: ContributorMetrics; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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`; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| $: retentionRate = metrics.total_contributions > 0 | ||
| ? ((metrics.active_contributors / metrics.total_contributions) * 100).toFixed(1) | ||
| : "0"; |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| <div class="detail-row"> | ||
| <span class="detail-label">Remaining</span> | ||
| <span class="detail-value">{formatCurrency(metrics.total_goal - metrics.total_raised)}</span> | ||
| </div> |
There was a problem hiding this comment.
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.
| <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.
|
Remove the .md files from the PR also attach a screenshot or video for quick review! |
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
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.