-
Notifications
You must be signed in to change notification settings - Fork 5
Feature/metrics/rich text links #787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
024f596
WIP: initial rake task for rich text link QA
rsmithlal d5ecc75
WIP: content link data type and rich text link metric
rsmithlal 9038c08
Merge branch 'main' into feature/metrics/rich-text-links
rsmithlal ee7821b
Merge branch 'main' into feature/metrics/rich-text-links
rsmithlal 4993dfd
Merge branch 'main' into feature/metrics/rich-text-links
rsmithlal 3d1076f
Merge branch 'main' into feature/metrics/rich-text-links
rsmithlal e01bed8
Rubocop fixes
rsmithlal ce919ec
Rubocop fixes
rsmithlal 067c5bf
Rubocop fixes
rsmithlal 9f436a5
Rubocop fixes
rsmithlal 44c6d45
Add RichText link checker functionality and related specs
rsmithlal 81e586c
Add migration for better_together_metrics_rich_text_links and update …
rsmithlal be96825
Rubocop fixes
rsmithlal cec750e
Refactor RichText link handling and improve service documentation
rsmithlal b61f8d8
Refactor link processing logic in RichTextLinkIdentifier for clarity …
rsmithlal 8156153
Rubocop fixes
rsmithlal fe419af
Add Link Checker report functionality with associated views, mailer, …
rsmithlal f04af71
Add scheduling and testing for daily link checker report functionality
rsmithlal 583e4f7
Add Sidekiq scheduling for link checker and event reminder jobs, alon…
rsmithlal 8e5911e
Implement Link Checker Reports functionality with CRUD operations, vi…
rsmithlal 635d2c0
Rubocop fixes
rsmithlal a449930
Refactor tab navigation for metrics reports to improve accessibility …
rsmithlal 8ec3b13
Refactor RSpec tests for ReportPORO and LinkCheckerReportsController …
rsmithlal ef124d9
Rubocop fixes
rsmithlal 53f873a
Add migrations for rich text link associations and metrics link check…
rsmithlal 19f4164
Refactor RichTextLinkCheckerQueueJob to use BetterTogether::Content::…
rsmithlal d48eddc
Refactor RichTextLink and related migrations to improve link handling…
rsmithlal 148885f
Add link checker report translations to English, Spanish, and French …
rsmithlal 1e20be2
Improve error handling assertions in HttpLinkChecker spec
rsmithlal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
app/jobs/better_together/metrics/external_link_checker_job.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'net/http' | ||
| require 'uri' | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| class ExternalLinkCheckerJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| def perform(link_id) | ||
| link = BetterTogether::Content::Link.find(link_id) | ||
| uri = URI.parse(link.url) | ||
| response = http_head(uri) | ||
|
|
||
| link.update!(last_checked_at: Time.current, latest_status_code: response.code.to_s, valid_link: response.is_a?(Net::HTTPSuccess)) | ||
| rescue StandardError => e | ||
| link.update!(last_checked_at: Time.current, latest_status_code: nil, valid_link: false, error_message: e.message) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def http_head(uri) | ||
| Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5, read_timeout: 5) do |http| | ||
| request = Net::HTTP::Head.new(uri.request_uri) | ||
| http.request(request) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
31 changes: 31 additions & 0 deletions
31
app/jobs/better_together/metrics/internal_link_checker_job.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'net/http' | ||
| require 'uri' | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| class InternalLinkCheckerJob < ApplicationJob | ||
| queue_as :default | ||
|
|
||
| def perform(link_id) | ||
| link = BetterTogether::Content::Link.find(link_id) | ||
| uri = URI.parse(link.url) | ||
| response = http_head(uri) | ||
|
|
||
| link.update!(last_checked_at: Time.current, latest_status_code: response.code.to_s, valid_link: response.is_a?(Net::HTTPSuccess)) | ||
| rescue StandardError => e | ||
| link.update!(last_checked_at: Time.current, latest_status_code: nil, valid_link: false, error_message: e.message) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def http_head(uri) | ||
| Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 5, read_timeout: 5) do |http| | ||
| request = Net::HTTP::Head.new(uri.request_uri) | ||
| http.request(request) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
20 changes: 20 additions & 0 deletions
20
app/jobs/better_together/metrics/rich_text_external_link_checker_queue_job.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| # Queues jobs that check external links found inside ActionText rich content. | ||
| # Subclasses of RichTextLinkCheckerQueueJob should implement the specifics | ||
| # for how individual link check jobs are performed. | ||
| class RichTextExternalLinkCheckerQueueJob < RichTextLinkCheckerQueueJob | ||
| protected | ||
|
|
||
| def model_collection | ||
| super.where(link_type: 'external') | ||
| end | ||
|
|
||
| def child_job_class | ||
| BetterTogether::Metrics::ExternalLinkCheckerJob | ||
| end | ||
| end | ||
| end | ||
| end |
24 changes: 24 additions & 0 deletions
24
app/jobs/better_together/metrics/rich_text_internal_link_checker_queue_job.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| # Queues jobs that check internal links found inside ActionText rich content. | ||
| # This job narrows the collection to internal links and may delay processing | ||
| # to reduce immediate load on the application. | ||
| class RichTextInternalLinkCheckerQueueJob < RichTextLinkCheckerQueueJob | ||
| protected | ||
|
|
||
| def model_collection | ||
| super.where(link_type: 'internal') | ||
| end | ||
|
|
||
| def queue_delay | ||
| 5.minutes | ||
| end | ||
|
|
||
| def child_job_class | ||
| BetterTogether::Metrics::InternalLinkCheckerJob | ||
| end | ||
| end | ||
| end | ||
| end |
63 changes: 63 additions & 0 deletions
63
app/jobs/better_together/metrics/rich_text_link_checker_queue_job.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| # Base queueing job that distributes RichText link check work across hosts. | ||
| # It groups RichText links by host and schedules child jobs with delays to | ||
| # avoid overloading external hosts or the application. | ||
| class RichTextLinkCheckerQueueJob < MetricsJob | ||
| def perform | ||
| records_size = model_collection.size | ||
| return if records_size.zero? | ||
|
|
||
| # Define the total time window for each host (e.g., 1 hour in seconds) | ||
| time_window = 3600 | ||
|
|
||
| records_by_host.each do |host, link_count| | ||
| next if link_count.zero? | ||
|
|
||
| delay_between_requests = time_window / link_count.to_f | ||
| queue_jobs_for_host(host, delay_between_requests) | ||
| end | ||
| end | ||
|
|
||
| def records_by_host | ||
| model_collection.group(:host) | ||
| .order('count_all DESC') | ||
| .count | ||
| end | ||
|
|
||
| protected | ||
|
|
||
| def model_class | ||
| BetterTogether::Metrics::RichTextLink | ||
| end | ||
|
|
||
| def model_collection | ||
| model_class.where(valid_link: true) | ||
| .where(last_checked_at: [nil, last_checked_lt..]) | ||
| end | ||
|
|
||
| def queue_jobs_for_host(host, delay_between_requests) | ||
| links_for_host = model_collection.where(host: host) | ||
| links_for_host.each_with_index do |link, index| | ||
| schedule_time = Time.current + (delay_between_requests * index).seconds | ||
| child_job_class.set(wait_until: schedule_time).perform_later(link.id) | ||
| end | ||
| end | ||
|
|
||
| def child_job_class | ||
| # Define this in subclasses (e.g., InternalLinkCheckerJob, ExternalLinkCheckerJob) | ||
| raise NotImplementedError, 'Subclasses must implement `child_job_class`' | ||
| end | ||
|
|
||
| def last_checked_lt | ||
| Time.current - last_checked_threshold | ||
| end | ||
|
|
||
| def last_checked_threshold | ||
| 14.days | ||
| end | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| module Content | ||
| # Represents a persisted link discovered in rich content. Stores metadata | ||
| # about the link (host, scheme, validity) and associates to RichText | ||
| # metrics records. | ||
| class Link < ApplicationRecord | ||
| has_many :rich_text_links, class_name: 'BetterTogether::Metrics::RichTextLink', inverse_of: :link | ||
| has_many :rich_texts, through: :rich_text_links | ||
| has_many :rich_text_records, through: :rich_text_links | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| # Namespace helper for links-related tables. Ensures a consistent | ||
| # table name prefix for models placed under BetterTogether::Links. | ||
| module Links | ||
| def self.table_name_prefix | ||
| 'better_together_links_' | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| # Tracks occurrences of links found inside ActionText rich content and | ||
| # associates them with the original Link record and owning rich text. | ||
| class RichTextLink < ApplicationRecord | ||
| belongs_to :link, class_name: 'BetterTogether::Content::Link' | ||
| belongs_to :rich_text, class_name: 'ActionText::RichText' | ||
| belongs_to :rich_text_record, polymorphic: true | ||
|
|
||
| accepts_nested_attributes_for :link, reject_if: ->(attributes) { attributes['url'].blank? }, allow_destroy: false | ||
| end | ||
| end | ||
| end |
101 changes: 101 additions & 0 deletions
101
app/services/better_together/metrics/rich_text_link_identifier.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module BetterTogether | ||
| module Metrics | ||
| # Service to scan ActionText::RichText records, extract links, and persist | ||
| # both the link metadata (BetterTogether::Content::Link) and the join | ||
| # records (BetterTogether::Metrics::RichTextLink). | ||
| # | ||
| # Usage: | ||
| # BetterTogether::Metrics::RichTextLinkIdentifier.call | ||
| class RichTextLinkIdentifier | ||
| def self.call(rich_texts: nil) | ||
| new(rich_texts: rich_texts).call | ||
| end | ||
|
|
||
| def initialize(rich_texts: nil) | ||
| @rich_texts = rich_texts | ||
| end | ||
|
|
||
| def call | ||
| texts = rich_texts || ActionText::RichText.includes(:record).where.not(body: nil) | ||
| valid_count = 0 | ||
| invalid_count = 0 | ||
|
|
||
| texts.find_each do |rt| | ||
| links = extract_links(rt) | ||
| next if links.empty? | ||
|
|
||
| links.each_with_index do |link, index| | ||
| uri = parse_uri(link) | ||
| if uri.nil? || (uri.host.nil? && uri.scheme.nil?) | ||
| create_invalid(rt, index, link, 'undetermined') | ||
| invalid_count += 1 | ||
| next | ||
| end | ||
|
|
||
| # Create or find the canonical Link record | ||
| bt_link = BetterTogether::Content::Link.find_or_initialize_by(url: link) | ||
| bt_link.host ||= uri.host | ||
| bt_link.scheme ||= uri.scheme | ||
rsmithlal marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| bt_link.external = (uri.host.present? && (rt_platform_host != uri.host)) | ||
| bt_link.save! if bt_link.changed? | ||
|
|
||
| # Create or update the rich text link join record | ||
| attrs = { | ||
| link_id: bt_link.id, | ||
| rich_text_id: rt.id, | ||
| rich_text_record_id: rt.record_id, | ||
| rich_text_record_type: rt.record_type, | ||
| position: index, | ||
| locale: rt.locale | ||
| } | ||
|
|
||
| BetterTogether::Metrics::RichTextLink.find_or_create_by!(attrs) | ||
| valid_count += 1 | ||
| rescue URI::InvalidURIError | ||
| create_invalid(rt, index, link, 'invalid_uri') | ||
| invalid_count += 1 | ||
| end | ||
| end | ||
|
|
||
| { valid: valid_count, invalid: invalid_count } | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :rich_texts | ||
|
|
||
| def extract_links(rt) | ||
| # ActionText stores HTML; use the body helper to extract hrefs | ||
| rt.body.links.uniq | ||
| rescue StandardError | ||
| [] | ||
| end | ||
|
|
||
| def parse_uri(link) | ||
| URI.parse(link) | ||
| end | ||
|
|
||
| def create_invalid(rt, index, link, invalid_type) | ||
| BetterTogether::Metrics::RichTextLink.create!( | ||
| rich_text_id: rt.id, | ||
| rich_text_record_id: rt.record_id, | ||
| rich_text_record_type: rt.record_type, | ||
| position: index, | ||
| locale: rt.locale, | ||
| link: BetterTogether::Content::Link.create!(url: link, valid_link: false, error_message: invalid_type) | ||
| ) | ||
| end | ||
|
|
||
| def rt_platform_host | ||
| @rt_platform_host ||= begin | ||
| host_platform = BetterTogether::Platform.host.first | ||
| URI(host_platform.url).host | ||
| rescue StandardError | ||
| nil | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
21 changes: 21 additions & 0 deletions
21
db/migrate/20241124181740_create_better_together_content_links.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Migration to create the persistent links table used by the | ||
| # BetterTogether rich text link metrics system. | ||
| class CreateBetterTogetherContentLinks < ActiveRecord::Migration[7.1] | ||
| # rubocop:disable Metrics/MethodLength | ||
| def change | ||
| create_bt_table :links, prefix: :better_together_content do |t| | ||
| t.string :link_type, null: false, index: true | ||
| t.string :url, null: false, index: true | ||
| t.string :scheme | ||
| t.string :host, index: true | ||
| t.boolean :external, index: true | ||
| t.boolean :valid_link, index: true | ||
| t.datetime :last_checked_at, index: true | ||
| t.string :latest_status_code, index: true | ||
| t.text :error_message | ||
| end | ||
| end | ||
| # rubocop:enable Metrics/MethodLength | ||
| end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.