source_monitor 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +132 -0
- data/CHANGELOG.md +66 -0
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +411 -0
- data/MIT-LICENSE +20 -0
- data/README.md +108 -0
- data/Rakefile +8 -0
- data/app/assets/builds/.keep +0 -0
- data/app/assets/config/source_monitor_manifest.js +4 -0
- data/app/assets/images/source_monitor/.keep +0 -0
- data/app/assets/javascripts/source_monitor/application.js +20 -0
- data/app/assets/javascripts/source_monitor/controllers/async_submit_controller.js +36 -0
- data/app/assets/javascripts/source_monitor/controllers/dropdown_controller.js +109 -0
- data/app/assets/javascripts/source_monitor/controllers/modal_controller.js +56 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +53 -0
- data/app/assets/javascripts/source_monitor/turbo_actions.js +13 -0
- data/app/assets/stylesheets/source_monitor/application.tailwind.css +13 -0
- data/app/assets/svgs/source_monitor/.keep +0 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/source_monitor/sanitizes_search_params.rb +81 -0
- data/app/controllers/source_monitor/application_controller.rb +62 -0
- data/app/controllers/source_monitor/dashboard_controller.rb +27 -0
- data/app/controllers/source_monitor/fetch_logs_controller.rb +9 -0
- data/app/controllers/source_monitor/health_controller.rb +10 -0
- data/app/controllers/source_monitor/items_controller.rb +116 -0
- data/app/controllers/source_monitor/logs_controller.rb +15 -0
- data/app/controllers/source_monitor/scrape_logs_controller.rb +9 -0
- data/app/controllers/source_monitor/source_bulk_scrapes_controller.rb +35 -0
- data/app/controllers/source_monitor/source_fetches_controller.rb +22 -0
- data/app/controllers/source_monitor/source_health_checks_controller.rb +34 -0
- data/app/controllers/source_monitor/source_health_resets_controller.rb +27 -0
- data/app/controllers/source_monitor/source_retries_controller.rb +22 -0
- data/app/controllers/source_monitor/source_turbo_responses.rb +115 -0
- data/app/controllers/source_monitor/sources_controller.rb +179 -0
- data/app/helpers/source_monitor/application_helper.rb +327 -0
- data/app/jobs/source_monitor/application_job.rb +13 -0
- data/app/jobs/source_monitor/fetch_feed_job.rb +117 -0
- data/app/jobs/source_monitor/item_cleanup_job.rb +48 -0
- data/app/jobs/source_monitor/log_cleanup_job.rb +47 -0
- data/app/jobs/source_monitor/schedule_fetches_job.rb +29 -0
- data/app/jobs/source_monitor/scrape_item_job.rb +47 -0
- data/app/jobs/source_monitor/source_health_check_job.rb +77 -0
- data/app/mailers/source_monitor/application_mailer.rb +17 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/concerns/source_monitor/loggable.rb +18 -0
- data/app/models/source_monitor/application_record.rb +5 -0
- data/app/models/source_monitor/fetch_log.rb +31 -0
- data/app/models/source_monitor/health_check_log.rb +28 -0
- data/app/models/source_monitor/item.rb +102 -0
- data/app/models/source_monitor/item_content.rb +11 -0
- data/app/models/source_monitor/log_entry.rb +56 -0
- data/app/models/source_monitor/scrape_log.rb +31 -0
- data/app/models/source_monitor/source.rb +115 -0
- data/app/views/layouts/source_monitor/application.html.erb +54 -0
- data/app/views/source_monitor/dashboard/_fetch_schedule.html.erb +90 -0
- data/app/views/source_monitor/dashboard/_job_metrics.html.erb +82 -0
- data/app/views/source_monitor/dashboard/_recent_activity.html.erb +39 -0
- data/app/views/source_monitor/dashboard/_stat_card.html.erb +6 -0
- data/app/views/source_monitor/dashboard/_stats.html.erb +9 -0
- data/app/views/source_monitor/dashboard/index.html.erb +48 -0
- data/app/views/source_monitor/fetch_logs/show.html.erb +90 -0
- data/app/views/source_monitor/items/_details.html.erb +234 -0
- data/app/views/source_monitor/items/_details_wrapper.html.erb +3 -0
- data/app/views/source_monitor/items/index.html.erb +147 -0
- data/app/views/source_monitor/items/show.html.erb +3 -0
- data/app/views/source_monitor/logs/index.html.erb +208 -0
- data/app/views/source_monitor/scrape_logs/show.html.erb +73 -0
- data/app/views/source_monitor/shared/_toast.html.erb +34 -0
- data/app/views/source_monitor/sources/_bulk_scrape_form.html.erb +64 -0
- data/app/views/source_monitor/sources/_bulk_scrape_modal.html.erb +53 -0
- data/app/views/source_monitor/sources/_details.html.erb +302 -0
- data/app/views/source_monitor/sources/_details_wrapper.html.erb +3 -0
- data/app/views/source_monitor/sources/_empty_state_row.html.erb +5 -0
- data/app/views/source_monitor/sources/_fetch_interval_heatmap.html.erb +46 -0
- data/app/views/source_monitor/sources/_form.html.erb +143 -0
- data/app/views/source_monitor/sources/_health_status_badge.html.erb +46 -0
- data/app/views/source_monitor/sources/_row.html.erb +102 -0
- data/app/views/source_monitor/sources/edit.html.erb +28 -0
- data/app/views/source_monitor/sources/index.html.erb +153 -0
- data/app/views/source_monitor/sources/new.html.erb +22 -0
- data/app/views/source_monitor/sources/show.html.erb +3 -0
- data/config/coverage_baseline.json +2010 -0
- data/config/initializers/feedjira.rb +19 -0
- data/config/routes.rb +18 -0
- data/config/tailwind.config.js +17 -0
- data/db/migrate/20241008120000_create_source_monitor_sources.rb +40 -0
- data/db/migrate/20241008121000_create_source_monitor_items.rb +44 -0
- data/db/migrate/20241008122000_create_source_monitor_fetch_logs.rb +32 -0
- data/db/migrate/20241008123000_create_source_monitor_scrape_logs.rb +25 -0
- data/db/migrate/20251008183000_change_fetch_interval_to_minutes.rb +23 -0
- data/db/migrate/20251009090000_create_source_monitor_item_contents.rb +38 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +5 -0
- data/db/migrate/20251010090000_add_adaptive_fetching_toggle_to_sources.rb +7 -0
- data/db/migrate/20251010123000_add_deleted_at_to_source_monitor_items.rb +8 -0
- data/db/migrate/20251010153000_add_type_to_source_monitor_sources.rb +8 -0
- data/db/migrate/20251010154500_add_fetch_status_to_source_monitor_sources.rb +9 -0
- data/db/migrate/20251010160000_create_solid_cable_messages.rb +16 -0
- data/db/migrate/20251011090000_add_fetch_retry_state_to_sources.rb +14 -0
- data/db/migrate/20251012090000_add_health_fields_to_sources.rb +17 -0
- data/db/migrate/20251012100000_optimize_source_monitor_database_performance.rb +13 -0
- data/db/migrate/20251014064947_add_not_null_constraints_to_items.rb +30 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +29 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +18 -0
- data/db/migrate/20251015100000_create_source_monitor_log_entries.rb +89 -0
- data/db/migrate/20251022100000_create_source_monitor_health_check_logs.rb +22 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +29 -0
- data/docs/configuration.md +170 -0
- data/docs/deployment.md +63 -0
- data/docs/gh-cli-workflow.md +44 -0
- data/docs/installation.md +144 -0
- data/docs/troubleshooting.md +76 -0
- data/eslint.config.mjs +27 -0
- data/lib/generators/source_monitor/install/install_generator.rb +59 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +155 -0
- data/lib/source_monitor/analytics/source_activity_rates.rb +53 -0
- data/lib/source_monitor/analytics/source_fetch_interval_distribution.rb +57 -0
- data/lib/source_monitor/analytics/sources_index_metrics.rb +92 -0
- data/lib/source_monitor/assets/bundler.rb +49 -0
- data/lib/source_monitor/assets.rb +6 -0
- data/lib/source_monitor/configuration.rb +654 -0
- data/lib/source_monitor/dashboard/queries.rb +356 -0
- data/lib/source_monitor/dashboard/quick_action.rb +7 -0
- data/lib/source_monitor/dashboard/quick_actions_presenter.rb +26 -0
- data/lib/source_monitor/dashboard/recent_activity.rb +30 -0
- data/lib/source_monitor/dashboard/recent_activity_presenter.rb +77 -0
- data/lib/source_monitor/dashboard/turbo_broadcaster.rb +87 -0
- data/lib/source_monitor/dashboard/upcoming_fetch_schedule.rb +126 -0
- data/lib/source_monitor/engine.rb +107 -0
- data/lib/source_monitor/events.rb +110 -0
- data/lib/source_monitor/feedjira_extensions.rb +103 -0
- data/lib/source_monitor/fetching/advisory_lock.rb +54 -0
- data/lib/source_monitor/fetching/completion/event_publisher.rb +22 -0
- data/lib/source_monitor/fetching/completion/follow_up_handler.rb +37 -0
- data/lib/source_monitor/fetching/completion/retention_handler.rb +30 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +627 -0
- data/lib/source_monitor/fetching/fetch_error.rb +88 -0
- data/lib/source_monitor/fetching/fetch_runner.rb +142 -0
- data/lib/source_monitor/fetching/retry_policy.rb +85 -0
- data/lib/source_monitor/fetching/stalled_fetch_reconciler.rb +146 -0
- data/lib/source_monitor/health/source_health_check.rb +100 -0
- data/lib/source_monitor/health/source_health_monitor.rb +210 -0
- data/lib/source_monitor/health/source_health_reset.rb +68 -0
- data/lib/source_monitor/health.rb +46 -0
- data/lib/source_monitor/http.rb +85 -0
- data/lib/source_monitor/instrumentation.rb +52 -0
- data/lib/source_monitor/items/item_creator.rb +601 -0
- data/lib/source_monitor/items/retention_pruner.rb +146 -0
- data/lib/source_monitor/items/retention_strategies/destroy.rb +26 -0
- data/lib/source_monitor/items/retention_strategies/soft_delete.rb +50 -0
- data/lib/source_monitor/items/retention_strategies.rb +9 -0
- data/lib/source_monitor/jobs/cleanup_options.rb +85 -0
- data/lib/source_monitor/jobs/fetch_failure_subscriber.rb +129 -0
- data/lib/source_monitor/jobs/solid_queue_metrics.rb +199 -0
- data/lib/source_monitor/jobs/visibility.rb +133 -0
- data/lib/source_monitor/logs/entry_sync.rb +69 -0
- data/lib/source_monitor/logs/filter_set.rb +163 -0
- data/lib/source_monitor/logs/query.rb +81 -0
- data/lib/source_monitor/logs/table_presenter.rb +161 -0
- data/lib/source_monitor/metrics.rb +77 -0
- data/lib/source_monitor/model_extensions.rb +109 -0
- data/lib/source_monitor/models/sanitizable.rb +76 -0
- data/lib/source_monitor/models/url_normalizable.rb +84 -0
- data/lib/source_monitor/pagination/paginator.rb +90 -0
- data/lib/source_monitor/realtime/adapter.rb +97 -0
- data/lib/source_monitor/realtime/broadcaster.rb +237 -0
- data/lib/source_monitor/realtime.rb +17 -0
- data/lib/source_monitor/release/changelog.rb +59 -0
- data/lib/source_monitor/release/runner.rb +73 -0
- data/lib/source_monitor/scheduler.rb +82 -0
- data/lib/source_monitor/scrapers/base.rb +105 -0
- data/lib/source_monitor/scrapers/fetchers/http_fetcher.rb +97 -0
- data/lib/source_monitor/scrapers/parsers/readability_parser.rb +101 -0
- data/lib/source_monitor/scrapers/readability.rb +156 -0
- data/lib/source_monitor/scraping/bulk_result_presenter.rb +85 -0
- data/lib/source_monitor/scraping/bulk_source_scraper.rb +233 -0
- data/lib/source_monitor/scraping/enqueuer.rb +125 -0
- data/lib/source_monitor/scraping/item_scraper/adapter_resolver.rb +44 -0
- data/lib/source_monitor/scraping/item_scraper/persistence.rb +189 -0
- data/lib/source_monitor/scraping/item_scraper.rb +84 -0
- data/lib/source_monitor/scraping/scheduler.rb +43 -0
- data/lib/source_monitor/scraping/state.rb +79 -0
- data/lib/source_monitor/security/authentication.rb +85 -0
- data/lib/source_monitor/security/parameter_sanitizer.rb +42 -0
- data/lib/source_monitor/sources/turbo_stream_presenter.rb +54 -0
- data/lib/source_monitor/turbo_streams/stream_responder.rb +95 -0
- data/lib/source_monitor/version.rb +3 -0
- data/lib/source_monitor.rb +149 -0
- data/lib/tasks/recover_stalled_fetches.rake +16 -0
- data/lib/tasks/source_monitor_assets.rake +28 -0
- data/lib/tasks/source_monitor_tasks.rake +29 -0
- data/lib/tasks/test_smoke.rake +12 -0
- data/package-lock.json +3997 -0
- data/package.json +29 -0
- data/postcss.config.js +6 -0
- data/source_monitor.gemspec +46 -0
- data/stylelint.config.js +12 -0
- metadata +469 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SourceMonitor engine configuration.
|
|
4
|
+
#
|
|
5
|
+
# These values default to conservative settings that work for most hosts.
|
|
6
|
+
# Tweak them here instead of monkey-patching the engine so upgrades remain easy.
|
|
7
|
+
SourceMonitor.configure do |config|
|
|
8
|
+
# Namespace used when deriving queue names and instrumentation keys. If your
|
|
9
|
+
# host app already prefixes queues (via ActiveJob.queue_name_prefix), this
|
|
10
|
+
# string is automatically combined with that prefix.
|
|
11
|
+
config.queue_namespace = "source_monitor"
|
|
12
|
+
|
|
13
|
+
# Dedicated queue names for fetching and scraping jobs. Solid Queue will use
|
|
14
|
+
# these names when dispatching work; ensure they match entries in
|
|
15
|
+
# config/solid_queue.yml (or your chosen Active Job backend).
|
|
16
|
+
config.fetch_queue_name = "#{config.queue_namespace}_fetch"
|
|
17
|
+
config.scrape_queue_name = "#{config.queue_namespace}_scrape"
|
|
18
|
+
|
|
19
|
+
# Recommended worker concurrency for each queue when using Solid Queue.
|
|
20
|
+
# Adjust to fit the workload and infrastructure available in the host app.
|
|
21
|
+
config.fetch_queue_concurrency = 2
|
|
22
|
+
config.scrape_queue_concurrency = 2
|
|
23
|
+
|
|
24
|
+
# Solid Queue executes recurring "command" tasks via a job class. Override
|
|
25
|
+
# this when host apps need additional context or instrumentation around
|
|
26
|
+
# recurring commands.
|
|
27
|
+
# config.recurring_command_job_class = "MyRecurringCommandJob"
|
|
28
|
+
|
|
29
|
+
# SourceMonitor assumes Solid Queue handles persistence. Run
|
|
30
|
+
# `bin/rails solid_queue:install` (dedicated queue database) or copy the
|
|
31
|
+
# engine's Solid Queue migration into your app so Mission Control and the
|
|
32
|
+
# dashboard can surface live queue metrics.
|
|
33
|
+
|
|
34
|
+
# Toggle SourceMonitor's lightweight queue visibility layer. When enabled (the
|
|
35
|
+
# default), the dashboard shows queue depth and last-run timestamps sourced
|
|
36
|
+
# from ActiveSupport::Notifications.
|
|
37
|
+
config.job_metrics_enabled = true
|
|
38
|
+
|
|
39
|
+
# Mission Control integration is optional. Flip this to true to surface an
|
|
40
|
+
# "Open Mission Control" link on the SourceMonitor dashboard.
|
|
41
|
+
config.mission_control_enabled = false
|
|
42
|
+
|
|
43
|
+
# Provide a String path ("/mission_control"), a route helper
|
|
44
|
+
# (-> { Rails.application.routes.url_helpers.mission_control_jobs_path }),
|
|
45
|
+
# or nil if you prefer not to link directly. This is only referenced when
|
|
46
|
+
# mission_control_enabled is true. Ensure the host routes mount Mission
|
|
47
|
+
# Control when supplying a path, for example:
|
|
48
|
+
# # Gemfile: gem "mission_control-jobs"
|
|
49
|
+
# # config/routes.rb:
|
|
50
|
+
# # mount MissionControl::Jobs::Engine, at: "/mission_control"
|
|
51
|
+
# # config.mission_control_dashboard_path = "/mission_control"
|
|
52
|
+
config.mission_control_dashboard_path = nil
|
|
53
|
+
|
|
54
|
+
# ---- Authentication -----------------------------------------------------
|
|
55
|
+
# Hook SourceMonitor into your host application's authentication stack. Each
|
|
56
|
+
# handler can be a Symbol (invoked on the controller) or a callable that will
|
|
57
|
+
# receive the controller instance.
|
|
58
|
+
# config.authentication.authenticate_with :authenticate_admin!
|
|
59
|
+
# config.authentication.authorize_with -> { authorize!(:manage, :source_monitor) }
|
|
60
|
+
# config.authentication.current_user_method = :current_admin
|
|
61
|
+
# config.authentication.user_signed_in_method = :admin_signed_in?
|
|
62
|
+
|
|
63
|
+
# ---- HTTP client -------------------------------------------------------
|
|
64
|
+
# Tune the Faraday client SourceMonitor uses for fetches/scrapes.
|
|
65
|
+
config.http.timeout = 15
|
|
66
|
+
config.http.open_timeout = 5
|
|
67
|
+
config.http.max_redirects = 5
|
|
68
|
+
# config.http.proxy = ENV["SOURCE_MONITOR_HTTP_PROXY"]
|
|
69
|
+
# config.http.retry_max = 4
|
|
70
|
+
# config.http.retry_interval = 0.5
|
|
71
|
+
# config.http.retry_backoff_factor = 2
|
|
72
|
+
# config.http.retry_statuses = [429, 500, 502, 503, 504]
|
|
73
|
+
# Merge extra default headers (User-Agent defaults to SourceMonitor/version).
|
|
74
|
+
# config.http.headers = { "X-Request-ID" => -> { SecureRandom.uuid } }
|
|
75
|
+
|
|
76
|
+
# ---- Adaptive fetch scheduling ----------------------------------------
|
|
77
|
+
# Control how quickly sources speed up or back off when adaptive fetching
|
|
78
|
+
# is enabled. Times are in minutes; factors must be positive numbers.
|
|
79
|
+
# config.fetching.min_interval_minutes = 5 # lower bound (default: 5 minutes)
|
|
80
|
+
# config.fetching.max_interval_minutes = 1440 # upper bound (default: 24 hours)
|
|
81
|
+
# config.fetching.increase_factor = 1.25 # multiplier when no new items
|
|
82
|
+
# config.fetching.decrease_factor = 0.75 # multiplier when new items arrive
|
|
83
|
+
# config.fetching.failure_increase_factor = 1.5 # multiplier on errors/timeouts
|
|
84
|
+
# config.fetching.jitter_percent = 0.1 # random jitter (0 disables jitter)
|
|
85
|
+
|
|
86
|
+
# ---- Source health monitoring ---------------------------------------
|
|
87
|
+
# Tune how many fetches SourceMonitor evaluates when determining health
|
|
88
|
+
# status, as well as thresholds for warnings and automatic pauses.
|
|
89
|
+
config.health.window_size = 20
|
|
90
|
+
config.health.healthy_threshold = 0.8
|
|
91
|
+
config.health.warning_threshold = 0.5
|
|
92
|
+
config.health.auto_pause_threshold = 0.2
|
|
93
|
+
config.health.auto_resume_threshold = 0.6
|
|
94
|
+
config.health.auto_pause_cooldown_minutes = 60
|
|
95
|
+
|
|
96
|
+
# ---- Scraper adapters --------------------------------------------------
|
|
97
|
+
# Register additional scraper adapters or override built-ins. Adapters must
|
|
98
|
+
# inherit from SourceMonitor::Scrapers::Base.
|
|
99
|
+
# config.scrapers.register(:custom, "MyApp::Scrapers::CustomAdapter")
|
|
100
|
+
|
|
101
|
+
# ---- Retention defaults ------------------------------------------------
|
|
102
|
+
# Sources inherit these values when they leave retention fields blank.
|
|
103
|
+
config.retention.items_retention_days = nil
|
|
104
|
+
config.retention.max_items = nil
|
|
105
|
+
# config.retention.strategy = :destroy # or :soft_delete
|
|
106
|
+
|
|
107
|
+
# ---- Scraping controls -------------------------------------------------
|
|
108
|
+
# Limit how many scrapes can be in-flight per source and cap the size of
|
|
109
|
+
# bulk enqueue operations. Set to nil to disable a limit.
|
|
110
|
+
# config.scraping.max_in_flight_per_source = 25
|
|
111
|
+
# config.scraping.max_bulk_batch_size = 100
|
|
112
|
+
|
|
113
|
+
# ---- Event callbacks ---------------------------------------------------
|
|
114
|
+
# Integrate with host workflows by responding to engine events. Handlers
|
|
115
|
+
# receive a single event object with helpful context. For example:
|
|
116
|
+
#
|
|
117
|
+
# config.events.after_item_created do |event|
|
|
118
|
+
# NewItemNotifier.publish(event.item, source: event.source)
|
|
119
|
+
# end
|
|
120
|
+
#
|
|
121
|
+
# config.events.after_fetch_completed do |event|
|
|
122
|
+
# Rails.logger.info "Fetch for #{event.source.name} finished with #{event.status}"
|
|
123
|
+
# end
|
|
124
|
+
#
|
|
125
|
+
# Register item processors to run after each entry is processed. These are
|
|
126
|
+
# ideal for lightweight normalization or denormalized writes.
|
|
127
|
+
# config.events.register_item_processor ->(context) { ItemIndexer.index(context.item) }
|
|
128
|
+
|
|
129
|
+
# ---- Model extensions --------------------------------------------------
|
|
130
|
+
# Host applications can extend SourceMonitor models without monkey patches.
|
|
131
|
+
# Table names default to "sourcemon_*"; override when multi-tenancy or
|
|
132
|
+
# legacy naming requires a different prefix.
|
|
133
|
+
# config.models.table_name_prefix = "sourcemon_"
|
|
134
|
+
#
|
|
135
|
+
# Include extension concerns to add associations, scopes, or helper methods.
|
|
136
|
+
# config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
|
|
137
|
+
#
|
|
138
|
+
# Register custom validations using a method name or a callable. Both forms
|
|
139
|
+
# run within the model instance context, so you can reuse helpers defined in
|
|
140
|
+
# your concerns.
|
|
141
|
+
# config.models.source.validate :enforce_custom_rules
|
|
142
|
+
# config.models.source.validate ->(record) { record.errors.add(:base, "custom error") }
|
|
143
|
+
|
|
144
|
+
# ---- Realtime adapter -------------------------------------------------
|
|
145
|
+
# Choose the Action Cable backend powering Turbo Streams. Solid Cable keeps
|
|
146
|
+
# everything in the primary database so Redis is no longer required. Switch
|
|
147
|
+
# to :redis if the host already runs a Redis cluster.
|
|
148
|
+
config.realtime.adapter = :solid_cable
|
|
149
|
+
# config.realtime.redis_url = ENV.fetch("SOURCE_MONITOR_REDIS_URL", nil)
|
|
150
|
+
# config.realtime.solid_cable.polling_interval = "0.05.seconds"
|
|
151
|
+
# config.realtime.solid_cable.message_retention = "12.hours"
|
|
152
|
+
# config.realtime.solid_cable.connects_to = {
|
|
153
|
+
# database: { writing: :cable }
|
|
154
|
+
# }
|
|
155
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
module Analytics
|
|
5
|
+
class SourceActivityRates
|
|
6
|
+
DEFAULT_LOOKBACK = 14.days
|
|
7
|
+
|
|
8
|
+
def initialize(scope: SourceMonitor::Source.all, lookback: DEFAULT_LOOKBACK, now: Time.current)
|
|
9
|
+
@scope = scope
|
|
10
|
+
@lookback = lookback
|
|
11
|
+
@now = now
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def per_source_rates
|
|
15
|
+
return {} if source_ids.empty?
|
|
16
|
+
|
|
17
|
+
counts = SourceMonitor::Item
|
|
18
|
+
.where(source_id: source_ids)
|
|
19
|
+
.where("created_at >= ?", window_start)
|
|
20
|
+
.group(:source_id)
|
|
21
|
+
.count
|
|
22
|
+
|
|
23
|
+
days = [ lookback.in_days, 1 ].max
|
|
24
|
+
|
|
25
|
+
counts.transform_values { |count| count.to_f / days }.tap do |results|
|
|
26
|
+
source_ids.each { |source_id| results[source_id] ||= 0.0 }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.rate_for(source, lookback: DEFAULT_LOOKBACK, now: Time.current)
|
|
31
|
+
return 0.0 unless source
|
|
32
|
+
|
|
33
|
+
new(scope: Array(source), lookback:, now:).per_source_rates[source.id] || 0.0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :scope, :lookback, :now
|
|
39
|
+
|
|
40
|
+
def source_ids
|
|
41
|
+
@source_ids ||= if scope.respond_to?(:pluck)
|
|
42
|
+
scope.pluck(:id)
|
|
43
|
+
else
|
|
44
|
+
Array(scope).map { |record| record.respond_to?(:id) ? record.id : record }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def window_start
|
|
49
|
+
now - lookback
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
module Analytics
|
|
5
|
+
class SourceFetchIntervalDistribution
|
|
6
|
+
Bucket = Struct.new(:label, :min, :max, :count, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
BUCKETS = [
|
|
9
|
+
{ label: "5-30 min", min: 5, max: 30 },
|
|
10
|
+
{ label: "30-60 min", min: 30, max: 60 },
|
|
11
|
+
{ label: "60-120 min", min: 60, max: 120 },
|
|
12
|
+
{ label: "120-240 min", min: 120, max: 240 },
|
|
13
|
+
{ label: "240-480 min", min: 240, max: 480 },
|
|
14
|
+
{ label: "480+ min", min: 480, max: nil }
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(scope: SourceMonitor::Source.all)
|
|
18
|
+
@scope = scope
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def buckets
|
|
22
|
+
@buckets ||= build_buckets
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :scope
|
|
28
|
+
|
|
29
|
+
def build_buckets
|
|
30
|
+
values = scope.pluck(:fetch_interval_minutes).compact
|
|
31
|
+
counts = Hash.new(0)
|
|
32
|
+
|
|
33
|
+
values.each do |value|
|
|
34
|
+
bucket = bucket_for(value)
|
|
35
|
+
counts[bucket_key(bucket)] += 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
BUCKETS.map do |definition|
|
|
39
|
+
bucket = definition.merge(count: counts[bucket_key(definition)] || 0)
|
|
40
|
+
Bucket.new(**bucket)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def bucket_for(value)
|
|
45
|
+
BUCKETS.find do |definition|
|
|
46
|
+
min = definition[:min] || 0
|
|
47
|
+
max = definition[:max]
|
|
48
|
+
value >= min && (max.nil? || value < max)
|
|
49
|
+
end || BUCKETS.first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bucket_key(definition)
|
|
53
|
+
[ definition[:min], definition[:max] ]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
module Analytics
|
|
5
|
+
class SourcesIndexMetrics
|
|
6
|
+
FETCH_INTERVAL_KEYS = %w[
|
|
7
|
+
fetch_interval_minutes_gteq
|
|
8
|
+
fetch_interval_minutes_lt
|
|
9
|
+
fetch_interval_minutes_lteq
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :search_params
|
|
13
|
+
|
|
14
|
+
def initialize(base_scope:, result_scope:, search_params:, lookback: SourceActivityRates::DEFAULT_LOOKBACK, now: Time.current)
|
|
15
|
+
@base_scope = base_scope
|
|
16
|
+
@result_scope = result_scope
|
|
17
|
+
@search_params = (search_params || {}).dup
|
|
18
|
+
@lookback = lookback
|
|
19
|
+
@now = now
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch_interval_distribution
|
|
23
|
+
@fetch_interval_distribution ||= SourceFetchIntervalDistribution.new(scope: distribution_scope).buckets
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def selected_fetch_interval_bucket
|
|
27
|
+
filter = fetch_interval_filter
|
|
28
|
+
return if filter.blank?
|
|
29
|
+
|
|
30
|
+
fetch_interval_distribution.find do |bucket|
|
|
31
|
+
min_match = filter[:min].present? ? filter[:min].to_i == bucket.min.to_i : bucket.min.nil?
|
|
32
|
+
max_match = if bucket.max.nil?
|
|
33
|
+
filter[:max].nil?
|
|
34
|
+
else
|
|
35
|
+
filter[:max].present? && filter[:max].to_i == bucket.max.to_i
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
min_match && max_match
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def item_activity_rates
|
|
43
|
+
@item_activity_rates ||= SourceActivityRates.new(scope: result_scope, lookback:, now:).per_source_rates
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch_interval_filter
|
|
47
|
+
min = integer_param(search_params["fetch_interval_minutes_gteq"])
|
|
48
|
+
max = integer_param(search_params["fetch_interval_minutes_lt"]) || integer_param(search_params["fetch_interval_minutes_lteq"])
|
|
49
|
+
return if min.nil? && max.nil?
|
|
50
|
+
|
|
51
|
+
{ min:, max: }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :base_scope, :result_scope, :lookback, :now
|
|
57
|
+
|
|
58
|
+
def distribution_scope
|
|
59
|
+
@distribution_scope ||= begin
|
|
60
|
+
filtered_params = search_params.except(*FETCH_INTERVAL_KEYS)
|
|
61
|
+
|
|
62
|
+
if filtered_params.present? && base_scope.respond_to?(:ransack)
|
|
63
|
+
base_scope.ransack(filtered_params).result
|
|
64
|
+
else
|
|
65
|
+
base_scope
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def integer_param(value)
|
|
71
|
+
return if value.blank?
|
|
72
|
+
|
|
73
|
+
sanitized = SourceMonitor::Security::ParameterSanitizer.sanitize(value.to_s)
|
|
74
|
+
cleaned = sanitized.strip
|
|
75
|
+
return if cleaned.blank?
|
|
76
|
+
|
|
77
|
+
Integer(cleaned)
|
|
78
|
+
rescue ArgumentError, TypeError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def distribution_source_ids
|
|
83
|
+
scope = distribution_scope
|
|
84
|
+
if scope.respond_to?(:pluck)
|
|
85
|
+
scope.pluck(:id)
|
|
86
|
+
else
|
|
87
|
+
Array(scope).map { |record| record.respond_to?(:id) ? record.id : record }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module SourceMonitor
|
|
2
|
+
module Assets
|
|
3
|
+
module Bundler
|
|
4
|
+
MissingBuildError = Class.new(StandardError)
|
|
5
|
+
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def build!
|
|
9
|
+
run_script!("build")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_css!
|
|
13
|
+
run_script!("build:css")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_js!
|
|
17
|
+
run_script!("build:js")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def verify!
|
|
21
|
+
missing = build_artifacts.reject(&:exist?)
|
|
22
|
+
|
|
23
|
+
if missing.any?
|
|
24
|
+
relative_paths = missing.map { |path| path.relative_path_from(engine_root) }
|
|
25
|
+
raise MissingBuildError,
|
|
26
|
+
"SourceMonitor asset build artifacts missing: #{relative_paths.join(', ')}. Run `npm run build` in the engine root to regenerate."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_artifacts
|
|
33
|
+
[
|
|
34
|
+
engine_root.join("app/assets/builds/source_monitor/application.css"),
|
|
35
|
+
engine_root.join("app/assets/builds/source_monitor/application.js")
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run_script!(script)
|
|
40
|
+
command = [ "npm", "run", script ]
|
|
41
|
+
system({ "BUNDLE_GEMFILE" => engine_root.join("Gemfile").to_s }, *command, chdir: engine_root.to_s, exception: true)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def engine_root
|
|
45
|
+
SourceMonitor::Engine.root
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|