mirror of
https://github.com/we-promise/sure.git
synced 2026-04-08 14:54:49 +00:00
* Add shared sync statistics collection and provider sync summary UI - Introduced `SyncStats::Collector` concern to centralize sync statistics logic, including account, transaction, holdings, and health stats collection. - Added collapsible `ProviderSyncSummary` component for displaying sync summaries across providers. - Updated syncers (e.g., `LunchflowItem::Syncer`) to use the shared collector methods for consistent stats calculation. - Added rake tasks under `dev:sync_stats` for testing and development purposes, including fake stats generation with optional issues. - Enhanced provider-specific views to include sync summaries using the new shared component. * Refactor `ProviderSyncSummary` to improve maintainability - Extracted `severity_color_class` to simplify severity-to-CSS mapping. - Replaced `holdings_label` with `holdings_label_key` for streamlined localization. - Updated locale file to separate `found` and `processed` translations for clarity. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
199 lines
7.1 KiB
Ruby
199 lines
7.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# SyncStats::Collector provides shared methods for collecting sync statistics
|
|
# across different provider syncers.
|
|
#
|
|
# This concern standardizes the stat collection interface so all providers
|
|
# can report consistent sync summaries.
|
|
#
|
|
# @example Include in a syncer class
|
|
# class PlaidItem::Syncer
|
|
# include SyncStats::Collector
|
|
#
|
|
# def perform_sync(sync)
|
|
# # ... sync logic ...
|
|
# collect_setup_stats(sync, provider_accounts: plaid_item.plaid_accounts)
|
|
# collect_transaction_stats(sync, account_ids: account_ids, source: "plaid")
|
|
# # ...
|
|
# end
|
|
# end
|
|
#
|
|
module SyncStats
|
|
module Collector
|
|
extend ActiveSupport::Concern
|
|
|
|
# Collects account setup statistics (total, linked, unlinked counts).
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
# @param provider_accounts [ActiveRecord::Relation] The provider accounts (e.g., SimplefinAccount, PlaidAccount)
|
|
# @param linked_check [Proc, nil] Optional proc to check if an account is linked. If nil, uses default logic.
|
|
# @return [Hash] The setup stats that were collected
|
|
def collect_setup_stats(sync, provider_accounts:, linked_check: nil)
|
|
return {} unless sync.respond_to?(:sync_stats)
|
|
|
|
total_accounts = provider_accounts.count
|
|
|
|
# Count linked accounts - either via custom check or default association check
|
|
linked_count = if linked_check
|
|
provider_accounts.count { |pa| linked_check.call(pa) }
|
|
else
|
|
# Default: check for current_account method or account association
|
|
provider_accounts.count do |pa|
|
|
(pa.respond_to?(:current_account) && pa.current_account.present?) ||
|
|
(pa.respond_to?(:account) && pa.account.present?)
|
|
end
|
|
end
|
|
|
|
unlinked_count = total_accounts - linked_count
|
|
|
|
setup_stats = {
|
|
"total_accounts" => total_accounts,
|
|
"linked_accounts" => linked_count,
|
|
"unlinked_accounts" => unlinked_count
|
|
}
|
|
|
|
merge_sync_stats(sync, setup_stats)
|
|
setup_stats
|
|
end
|
|
|
|
# Collects transaction statistics (imported, updated, seen, skipped).
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
# @param account_ids [Array<String>] The account IDs to count transactions for
|
|
# @param source [String] The transaction source (e.g., "simplefin", "plaid")
|
|
# @param window_start [Time, nil] Start of the sync window (defaults to sync.created_at or 30 minutes ago)
|
|
# @param window_end [Time, nil] End of the sync window (defaults to Time.current)
|
|
# @return [Hash] The transaction stats that were collected
|
|
def collect_transaction_stats(sync, account_ids:, source:, window_start: nil, window_end: nil)
|
|
return {} unless sync.respond_to?(:sync_stats)
|
|
return {} if account_ids.empty?
|
|
|
|
window_start ||= sync.created_at || 30.minutes.ago
|
|
window_end ||= Time.current
|
|
|
|
tx_scope = Entry.where(account_id: account_ids, source: source, entryable_type: "Transaction")
|
|
tx_imported = tx_scope.where(created_at: window_start..window_end).count
|
|
tx_updated = tx_scope.where(updated_at: window_start..window_end)
|
|
.where.not(created_at: window_start..window_end).count
|
|
tx_seen = tx_imported + tx_updated
|
|
|
|
tx_stats = {
|
|
"tx_imported" => tx_imported,
|
|
"tx_updated" => tx_updated,
|
|
"tx_seen" => tx_seen,
|
|
"window_start" => window_start.iso8601,
|
|
"window_end" => window_end.iso8601
|
|
}
|
|
|
|
merge_sync_stats(sync, tx_stats)
|
|
tx_stats
|
|
end
|
|
|
|
# Collects holdings statistics.
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
# @param holdings_count [Integer] The number of holdings found/processed
|
|
# @param label [String] The label for the stat ("found" or "processed")
|
|
# @return [Hash] The holdings stats that were collected
|
|
def collect_holdings_stats(sync, holdings_count:, label: "found")
|
|
return {} unless sync.respond_to?(:sync_stats)
|
|
|
|
key = label == "processed" ? "holdings_processed" : "holdings_found"
|
|
holdings_stats = { key => holdings_count }
|
|
|
|
merge_sync_stats(sync, holdings_stats)
|
|
holdings_stats
|
|
end
|
|
|
|
# Collects health/error statistics.
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
# @param errors [Array<Hash>, nil] Array of error objects with :message and optional :category
|
|
# @param rate_limited [Boolean] Whether the sync was rate limited
|
|
# @param rate_limited_at [Time, nil] When rate limiting occurred
|
|
# @return [Hash] The health stats that were collected
|
|
def collect_health_stats(sync, errors: nil, rate_limited: false, rate_limited_at: nil)
|
|
return {} unless sync.respond_to?(:sync_stats)
|
|
|
|
health_stats = {
|
|
"import_started" => true
|
|
}
|
|
|
|
if errors.present?
|
|
health_stats["errors"] = errors.map do |e|
|
|
e.is_a?(Hash) ? e.stringify_keys : { "message" => e.to_s }
|
|
end
|
|
health_stats["total_errors"] = errors.size
|
|
else
|
|
health_stats["total_errors"] = 0
|
|
end
|
|
|
|
if rate_limited
|
|
health_stats["rate_limited"] = true
|
|
health_stats["rate_limited_at"] = rate_limited_at&.iso8601
|
|
end
|
|
|
|
merge_sync_stats(sync, health_stats)
|
|
health_stats
|
|
end
|
|
|
|
# Collects data quality warnings and notices.
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
# @param warnings [Integer] Number of data warnings
|
|
# @param notices [Integer] Number of notices
|
|
# @param details [Array<Hash>] Array of detail objects with :message and optional :severity
|
|
# @return [Hash] The data quality stats that were collected
|
|
def collect_data_quality_stats(sync, warnings: 0, notices: 0, details: [])
|
|
return {} unless sync.respond_to?(:sync_stats)
|
|
|
|
quality_stats = {
|
|
"data_warnings" => warnings,
|
|
"notices" => notices
|
|
}
|
|
|
|
if details.present?
|
|
quality_stats["data_quality_details"] = details.map do |d|
|
|
d.is_a?(Hash) ? d.stringify_keys : { "message" => d.to_s, "severity" => "info" }
|
|
end
|
|
end
|
|
|
|
merge_sync_stats(sync, quality_stats)
|
|
quality_stats
|
|
end
|
|
|
|
# Marks the sync as having started import (used for health indicator).
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
def mark_import_started(sync)
|
|
return unless sync.respond_to?(:sync_stats)
|
|
|
|
merge_sync_stats(sync, { "import_started" => true })
|
|
end
|
|
|
|
# Clears previous sync stats (useful at start of sync).
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
def clear_sync_stats(sync)
|
|
return unless sync.respond_to?(:sync_stats)
|
|
|
|
sync.update!(sync_stats: { "cleared_at" => Time.current.iso8601 })
|
|
end
|
|
|
|
private
|
|
|
|
# Merges new stats into the existing sync_stats hash.
|
|
#
|
|
# @param sync [Sync] The sync record to update
|
|
# @param new_stats [Hash] The new stats to merge
|
|
def merge_sync_stats(sync, new_stats)
|
|
return unless sync.respond_to?(:sync_stats)
|
|
|
|
existing = sync.sync_stats || {}
|
|
sync.update!(sync_stats: existing.merge(new_stats))
|
|
rescue => e
|
|
Rails.logger.warn("SyncStats::Collector#merge_sync_stats failed: #{e.class} - #{e.message}")
|
|
end
|
|
end
|
|
end
|