Skip to content

Day-bucketing/aggregation logic is copy-pasted independently across QuickStats, HistoryChart, and ActivityChart — fixing #29 in History won't fix Dashboard's identical copy #47

Description

@abayomicornelius

Problem

The exact same "bucket transactions by day and sum sent/received/fees" logic is independently written out, by hand, three separate times — in dashboard/QuickStats.tsx, pages/History.tsx's HistoryChart, and pages/Dashboard.tsx's ActivityChart — instead of existing once as a shared utility.

pages/Dashboard.tsx's ActivityChart (7-day window):

// src/pages/Dashboard.tsx:28-46
const days: Record<string, { sent: number; received: number }> = {}
const now = Date.now()
for (let i = 6; i >= 0; i--) {
  const d = new Date(now - i * 86_400_000)
  const key = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
  days[key] = { sent: 0, received: 0 }
}
transactions.forEach((tx) => {
  const key = new Date(tx.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
  if (days[key]) {
    if (tx.direction === 'sent') days[key].sent += parseFloat(tx.amount || '0')
    else days[key].received += parseFloat(tx.amount || '0')
  }
})

pages/History.tsx's HistoryChart (30-day window) — the subject of the already-filed year-boundary collision bug (#29 in this repo):

// src/pages/History.tsx:27-45
const days: Record<string, { sent: number; received: number; fees: number }> = {}
const now = Date.now()
for (let i = 29; i >= 0; i--) {
  const d = new Date(now - i * 86_400_000)
  const key = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
  days[key] = { sent: 0, received: 0, fees: 0 }
}
transactions.forEach((tx) => {
  const key = new Date(tx.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
  if (days[key]) { ... }
})

These two blocks are the same algorithm with the same month/day-only (no year) key format, the same if (days[key]) truthy-check pattern, and the same reliance on the local timezone via toLocaleDateString.

Why it matters

#29 in this repo, already filed, fixes (or will fix) the year-boundary bucket-collision bug for History.tsx's copy specifically. But Dashboard.tsx's ActivityChart has the exact same month/day-only key construction (d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), no year) and will still silently merge two same-calendar-day-different-year transactions into one bucket after #29 is fixed, because it's an entirely independent copy of the logic, not a shared function. Fixing the bug in one file does nothing for the other two. The same is true for any future fix to the bucketing approach in general (e.g. switching to UTC-based bucketing, or fixing the "average fee should only count sent transactions" issue filed separately in this batch, which also affects QuickStats.tsx and History.tsx's HistorySummary independently) — every fix has to be applied three times, in three files, and it's easy to catch two of the three and miss the third, exactly as would happen here.

This is the same "same problem solved three independent, drifting ways" shape as the already-filed isValidStellarAddress/truncateAddress/formatAmount duplication (#18) and the formatDate duplication (#28) in this repo, but for the app's transaction-aggregation/analytics logic specifically, which is materially more complex (and therefore more bug-prone) than a one-line formatter.

Reproduction

  • grep -n "toLocaleDateString('en-US', { month: 'short', day: 'numeric'" src/pages/Dashboard.tsx src/pages/History.tsx — two independent matches, same format string, same lack of year.
  • Apply the fix described in issue History page's 30-day volume chart buckets transactions by month/day only, silently merging entries across year boundaries #29 (add year to the bucket key) only to src/pages/History.tsx. Feed Dashboard.tsx's ActivityChart two transactions a year apart on the same calendar day (well within its own 7-day-window fetch of useRecentTransactions(50), which can include transactions from any time in the past depending on account history) — they still silently merge into a single bar, because the fix was never applied to ActivityChart's own copy.

Suggested fix

  • Extract a single shared function, e.g. bucketTransactionsByDay(transactions, windowDays, { includeFees }) in src/lib/ or src/utils/, that both Dashboard.tsx and History.tsx (and QuickStats.tsx's totals, which do the same "reduce sent/received from a transaction list" work without the day-bucketing part) call into, so any correctness fix — the year-boundary bug, timezone handling, or the sent-vs-received fee-averaging bug — only needs to be made once and benefits all three call sites simultaneously.

Additional Notes

  • src/pages/Dashboard.tsx:24-52 (ActivityChart), src/pages/History.tsx:23-52 (HistoryChart), src/components/dashboard/QuickStats.tsx:8-29 (same-shape sent/received/fee reduction, no day-bucketing but same underlying duplicated pattern).
  • Directly compounds with issue History page's 30-day volume chart buckets transactions by month/day only, silently merging entries across year boundaries #29 in this repo (fixing the year-collision bug there does not fix Dashboard.tsx's independent copy) and with the "Average Fee includes received transactions' fees" issue filed separately in this batch (same duplication shape, different specific defect).
  • Testing strategy: once extracted, a single test suite against the shared bucketing function (year-boundary fixture, timezone fixture, sent-only-fee fixture) covers all three current call sites at once — today, each would need its own duplicate test suite to get the same coverage, and none currently has one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingenhancementNew feature or requestvery hardVery difficult / senior-level bounty issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions