Skip to content

Commit

Permalink
[VACMS-19451]Alternative Banners fetch banner data from graphql to db (
Browse files Browse the repository at this point in the history
…#19511)

* add initial thougnts/notes

* create banner job to pull and update db, only fetching banner data

* cleanup notes and variable names

* Move fetching out of main job into banner services for building and updating

* Further refinement of builder/updater/job process

* get builder creating banners using parsed response data

* params refinement

* rubocop cleanup

* separate banner profiles from updater

* adjust scope to simplify queries a notch for easier understanding

* add error response to controller when path is not provided for #by_path

* remove no longer used #enabled?

* cleanup updater

* cleanup requirements

* appease the rubocop

* destroy any lingering banners that are no longer being provided

* fixup query and vamc model naming

* pluck > map

* remove job to be handled with VAMC-19452

* remove leftover banner spec and adjust #by_path testing in appropriate banner spec

* add logging to builder and updater

* add updater spec

* add spec for builder

* update settings used, appease the rubocop

* avoid alternative_banners wording

* missed a spec

* rebasing added the job too soon

* return when rendering error

* 422 is more accurate than 400

* [VAMC-19452]Update Alternative Banners DB every 10min with sidekiq job (#19550)

* adjust updater to return error when parsing failed, introduce job to work with updater

* avoid alternative_banners wording

* add job to 10m rotation

* add flipper to enable the update all job

* make linter happy

* be more specific when re-raising error

* update updater spec to include error

* Use Job in periodic_jobs.rb

Co-authored-by: Eli Selkin <[email protected]>

---------

Co-authored-by: Eli Selkin <[email protected]>
  • Loading branch information
SnowboardTechie and eselkin authored Dec 2, 2024
1 parent 1415707 commit 4d6a935
Show file tree
Hide file tree
Showing 15 changed files with 498 additions and 26 deletions.
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1566,7 +1566,6 @@ spec/models/accredited_organization_spec.rb @department-of-veterans-affairs/accr
spec/models/async_transaction/base_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
spec/models/async_transaction/va_profile @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
spec/models/async_transaction/vet360 @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
spec/models/banner_spec.rb @department-of-veterans-affairs/vfs-facilities-frontend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
spec/models/bgs_dependents @department-of-veterans-affairs/benefits-dependents-management @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
spec/models/decision_review_notification_audit_log_spec.rb @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
spec/models/dependents_application_spec.rb @department-of-veterans-affairs/vfs-authenticated-experience-backend @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group
Expand Down
15 changes: 10 additions & 5 deletions app/controllers/v0/banners_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ class BannersController < ApplicationController
skip_before_action :authenticate

def by_path
path = params[:path]
# Default to 'full_width_banner_alert' banner (bundle) type.
banner_type = params.fetch(:type, 'full_width_banner_alert')
banners = []
banners = Banner.by_path_and_type(path, banner_type) if path && banner_type
response = { banners: banners, path: path, banner_type: banner_type }
render json: response
return render json: { error: 'Path parameter is required' }, status: :unprocessable_entity if path.blank?

banners = Banner.where(entity_bundle: banner_type).by_path(path)
render json: { banners:, path:, banner_type: }
end

private

def path
params[:path]
end
end
end
3 changes: 3 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,9 @@ features:
show_yellow_ribbon_table:
actor_type: user
description: Used to show yellow ribbon table in Comparison Tool
banner_update_alternative_banners:
actor_type: user
description: Used to toggle the DB updating of alternative banners
banner_use_alternative_banners:
actor_type: user
description: Used to toggle use of alternative banners.
Expand Down
3 changes: 3 additions & 0 deletions lib/periodic_jobs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
# Checks status of Flipper features expected to be enabled and alerts to Slack if any are not enabled
mgr.register('0 2,9,16 * * 1-5', 'AppealsApi::FlipperStatusAlert')

# Update alternative Banners data every 10 minutes
mgr.register('*/10 * * * *', 'Banners::UpdateAllJob')

# Update static data cache
mgr.register('0 0 * * *', 'Crm::TopicsDataJob')

Expand Down
10 changes: 3 additions & 7 deletions modules/banners/app/models/banner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class Banner < ApplicationRecord
validates :find_facilities_cta, inclusion: { in: [true, false] }
validates :limit_subpage_inheritance, inclusion: { in: [true, false] }

# Returns banners for a given path and banner bundle type.
scope :by_path_and_type, lambda { |path, type|
scope :by_path, lambda { |path|
normalized_path = path.sub(%r{^/?}, '')

# Direct path matches.
Expand Down Expand Up @@ -48,10 +47,7 @@ class Banner < ApplicationRecord
].to_json)
.where(limit_subpage_inheritance: false)

# Bundle type match
type_condition = where(entity_bundle: type)

# Combine conditions with the "AND" for type, and "OR" between exact paths and subpage matches
type_condition.and(exact_path_conditions.or(subpage_condition))
# Look for both exact paths and subpage matches
exact_path_conditions.or(subpage_condition)
}
end
56 changes: 56 additions & 0 deletions modules/banners/app/sidekiq/banners/update_all_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Banners
class UpdateAllJob
include Sidekiq::Job

STATSD_KEY_PREFIX = 'banners.sidekiq.update_all_banners'

sidekiq_options retry: 5

sidekiq_retries_exhausted do |msg, _ex|
job_id = msg['jid']
job_class = msg['class']
error_class = msg['error_class']
error_message = msg['error_message']

StatsD.increment("#{STATSD_KEY_PREFIX}.exhausted")

message = "#{job_class} retries exhausted"
Rails.logger.error(message, { job_id:, error_class:, error_message: })
rescue => e
Rails.logger.error(
"Failure in #{job_class}#sidekiq_retries_exhausted",
{
messaged_content: e.message,
job_id:,
pre_exhaustion_failure: {
error_class:,
error_message:
}
}
)

raise e
end

def perform
return unless enabled?

Banners.update_all
rescue Banners::Updater::BannerDataFetchError => e
StatsD.increment("#{STATSD_KEY_PREFIX}.banner_data_fetch_error")
Rails.logger.error(
'Banner data fetch failed',
{ error_message: e.message, error_class: e.class.name }
)
raise e # Re-raise to trigger Sidekiq retries
end

private

def enabled?
Flipper.enabled?(:banner_update_alternative_banners)
end
end
end
69 changes: 69 additions & 0 deletions modules/banners/config/vamcs_graphql_query.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
query bannerAlerts {
nodeQuery(
limit: 500,
filter: {conditions: [
{field: "status", value: "1", operator: EQUAL},
{field: "type", value: "full_width_banner_alert"},
{field: "field_banner_alert_vamcs", operator: IS_NOT_NULL}
]}
) {
count
entities {
... on NodeFullWidthBannerAlert {
title
fieldBody {
processed
}
entityId
fieldAlertType
fieldAlertDismissable
fieldAlertFindFacilitiesCta
fieldAlertOperatingStatusCta
fieldAlertEmailUpdatesButton
fieldAlertInheritanceSubpages
fieldOperatingStatusSendemail
fieldLastSavedByAnEditor
fieldBannerAlertSituationinfo {
processed
}
fieldSituationUpdates {
entity {
... on ParagraphSituationUpdate {
fieldWysiwyg {
processed
}
}
}
}
fieldBannerAlertVamcs {
entity {
... on NodeVamcOperatingStatusAndAlerts {
title
entityUrl {
path
}
fieldOffice {
entity {
... on NodeHealthCareRegionPage {
fieldVamcEhrSystem
title
entityUrl {
path
}
}
}
}
}
}
}
fieldAdministration {
entity{
... on TaxonomyTermAdministration {
entityId
}
}
}
}
}
}
}
10 changes: 9 additions & 1 deletion modules/banners/lib/banners.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# frozen_string_literal: true

require 'banners/builder'
require 'banners/engine'
require 'banners/updater'

module Banners
# Your code goes here...
def self.build(banner_props)
Builder.perform(banner_props)
end

def self.update_all
Updater.perform
end
end
41 changes: 41 additions & 0 deletions modules/banners/lib/banners/builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Banners
class Builder
STATSD_KEY_PREFIX = 'banners.builder'

def self.perform(banner_data)
banner = new(banner_data).banner

if banner.update(banner_data)
log_success(banner_data[:entity_id])
banner
else
log_failure(banner_data[:entity_id])
false
end
end

def initialize(banner_data)
@banner_data = banner_data
end

def banner
@banner ||= Banner.find_or_initialize_by(entity_id: banner_data[:entity_id])
end

attr_reader :banner_data

private

def self.log_failure(entity_id)
StatsD.increment("#{STATSD_KEY_PREFIX}.failure", tags: ["entitiy_id:#{entity_id}"])
end

def self.log_success(entity_id)
StatsD.increment("#{STATSD_KEY_PREFIX}.success", tags: ["entitiy_id:#{entity_id}"])
end

private_class_method :log_failure, :log_success
end
end
24 changes: 24 additions & 0 deletions modules/banners/lib/banners/profile/vamc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Banners
module Profile
class Vamc
# Converts the GraphQL response into a hash that can be used to create or update a VAMC banner
def self.parsed_banner(graphql_banner_response)
{
entity_id: graphql_banner_response['entityId'],
headline: graphql_banner_response['title'],
alert_type: graphql_banner_response['fieldAlertType'],
entity_bundle: 'full_width_banner_alert',
content: graphql_banner_response['fieldBody']['processed'],
context: graphql_banner_response['fieldBannerAlertVamcs'],
show_close: graphql_banner_response['fieldAlertDismissable'],
operating_status_cta: graphql_banner_response['fieldAlertOperatingStatusCta'],
email_updates_button: graphql_banner_response['fieldAlertEmailUpdatesButton'],
find_facilities_cta: graphql_banner_response['fieldAlertFindFacilitiesCta'],
limit_subpage_inheritance: graphql_banner_response['fieldAlertLimitSubpageInheritance'] || false
}
end
end
end
end
80 changes: 80 additions & 0 deletions modules/banners/lib/banners/updater.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

require 'banners/builder'
require 'banners/profile/vamc'

module Banners
class Updater
STATSD_KEY_PREFIX = 'banners.updater'

class BannerDataFetchError < StandardError; end

# If banners are to be added in the future, include them here by adding
# another #update_{type_of}_banners method for #perform to call
def self.perform
banner_updater = new
banner_updater.update_vamc_banners
end

def update_vamc_banners
if vamcs_banner_data.all? { |banner_data| Builder.perform(Profile::Vamc.parsed_banner(banner_data)) }
destroy_missing_banners(vamcs_banner_data.pluck('entityId'))
log_success('vamc')
true
else
log_failure('vamc')
false
end
end

private

def connection
@connection ||= Faraday.new(Settings.banners.drupal_url, faraday_options) do |faraday|
faraday.request :url_encoded
faraday.request :authorization, :basic, Settings.banners.drupal_username,
Settings.banners.drupal_password
faraday.adapter faraday_adapter
end
end

def destroy_missing_banners(entity_ids_to_keep)
Banner.where.not(entity_id: entity_ids_to_keep).destroy_all
end

def faraday_adapter
Rails.env.production? ? Faraday.default_adapter : :net_http_socks
end

def faraday_options
options = { ssl: { verify: false } }
options[:proxy] = { uri: URI.parse('socks://localhost:2001') } unless Rails.env.production?
options
end

def log_failure(banner_type)
StatsD.increment("#{STATSD_KEY_PREFIX}.failure", tags: ["banner_type:#{banner_type}"])
end

def log_success(banner_type)
StatsD.increment("#{STATSD_KEY_PREFIX}.success", tags: ["banner_type:#{banner_type}"])
end

def vamcs_banner_data
banner_graphql_query = Rails.root.join('modules', 'banners', 'config', 'vamcs_graphql_query.txt')
body = { query: File.read(banner_graphql_query) }

response = connection.post do |req|
req.path = 'graphql'
req.body = body.to_json
req.options.timeout = 300
end

begin
JSON.parse(response.body).dig('data', 'nodeQuery', 'entities')
rescue JSON::ParserError => e
raise BannerDataFetchError, "Failed to parse VAMC banner data: #{e.message}"
end
end
end
end
Loading

0 comments on commit 4d6a935

Please sign in to comment.