diff --git a/Gemfile b/Gemfile
index b6defce7400..f4eea5be00c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -44,7 +44,7 @@ gem 'msgpack', '~> 1.6'
gem 'maxminddb'
gem 'multiset'
gem 'net-sftp'
-gem 'newrelic_rpm', '~> 8.0'
+gem 'newrelic_rpm', '~> 9.0'
gem 'puma', '~> 5.6.7'
gem 'pg'
gem 'phonelib'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7d2825e26d2..ebac2028a6d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -424,7 +424,7 @@ GEM
net-smtp (0.3.3)
net-protocol
net-ssh (6.1.0)
- newrelic_rpm (8.15.0)
+ newrelic_rpm (9.5.0)
nio4r (2.5.9)
nokogiri (1.14.5)
mini_portile2 (~> 2.8.0)
@@ -770,7 +770,7 @@ DEPENDENCIES
msgpack (~> 1.6)
multiset
net-sftp
- newrelic_rpm (~> 8.0)
+ newrelic_rpm (~> 9.0)
nokogiri (~> 1.14.0)
pg
pg_query
diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb
index 4f3b8422c85..1af2ce6edbc 100644
--- a/app/controllers/concerns/idv/verify_info_concern.rb
+++ b/app/controllers/concerns/idv/verify_info_concern.rb
@@ -29,7 +29,7 @@ def shared_update
# TEMPORARY DEBUGGING
logger.info("ResolutionJobDebug: user_uuid=#{current_user.uuid} old=#{pii[:ssn].present?} new=#{idv_session.ssn.present?} controller=#{self.class.name}")
# rubocop:enable Layout/LineLength
- pii[:ssn] ||= idv_session.ssn # Required for proof_resolution job
+ pii[:ssn] = idv_session.ssn # Required for proof_resolution job
Idv::Agent.new(pii).proof_resolution(
document_capture_session,
should_proof_state_id: should_use_aamva?(pii),
@@ -74,9 +74,8 @@ def resolution_rate_limiter
end
def ssn_rate_limiter
- ssn = idv_session.ssn || pii[:ssn]
@ssn_rate_limiter ||= RateLimiter.new(
- target: Pii::Fingerprinter.fingerprint(ssn),
+ target: Pii::Fingerprinter.fingerprint(idv_session.ssn),
rate_limit_type: :proof_ssn,
)
end
@@ -306,19 +305,18 @@ def log_idv_verification_submitted_event(success: false, failure_reason: nil)
last_name: pii_from_doc[:last_name],
date_of_birth: pii_from_doc[:dob],
address: pii_from_doc[:address1],
- ssn: idv_session.ssn || pii_from_doc[:ssn],
+ ssn: idv_session.ssn,
failure_reason: failure_reason,
)
end
def check_ssn
- ssn = idv_session.ssn || pii[:ssn]
- Idv::SsnForm.new(current_user).submit(ssn: ssn)
+ Idv::SsnForm.new(current_user).submit(ssn: idv_session.ssn)
end
def move_applicant_to_idv_session
idv_session.applicant = pii
- idv_session.applicant[:ssn] ||= idv_session.ssn
+ idv_session.applicant[:ssn] = idv_session.ssn
idv_session.applicant['uuid'] = current_user.uuid
delete_pii
end
diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb
index 44ce2834e9d..157b6d8ab8b 100644
--- a/app/controllers/concerns/idv_step_concern.rb
+++ b/app/controllers/concerns/idv_step_concern.rb
@@ -52,7 +52,7 @@ def flow_path
private
def confirm_ssn_step_complete
- return if pii.present? && (idv_session.ssn.present? || pii[:ssn].present?)
+ return if pii.present? && idv_session.ssn.present?
redirect_to prev_url
end
diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb
index d5b3a9ae4e7..8ada2c10913 100644
--- a/app/controllers/concerns/rate_limit_concern.rb
+++ b/app/controllers/concerns/rate_limit_concern.rb
@@ -64,9 +64,7 @@ def idv_attempter_rate_limited?(rate_limit_type)
end
def pii_ssn
- return unless defined?(flow_session) && defined?(idv_session) && user_session
- pii_from_doc_ssn = idv_session&.ssn || flow_session[:pii_from_doc]&.[](:ssn)
- return pii_from_doc_ssn if pii_from_doc_ssn
- flow_session[:pii_from_user]&.[](:ssn)
+ return unless defined?(idv_session) && user_session
+ idv_session&.ssn
end
end
diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb
index 813663c3272..4c885a62985 100644
--- a/app/controllers/frontend_log_controller.rb
+++ b/app/controllers/frontend_log_controller.rb
@@ -11,6 +11,7 @@ class FrontendLogController < ApplicationController
# Please try to keep this list alphabetical as well!
# rubocop:disable Layout/LineLength
EVENT_MAP = {
+ 'Frontend Error' => FrontendErrorLogger.method(:track_error),
'IdV: consent checkbox toggled' => :idv_consent_checkbox_toggled,
'IdV: download personal key' => :idv_personal_key_downloaded,
'IdV: location submitted' => :idv_in_person_location_submitted,
@@ -26,11 +27,10 @@ class FrontendLogController < ApplicationController
'IdV: user clicked what to bring link on ready to verify page' => :idv_in_person_ready_to_verify_what_to_bring_link_clicked,
'IdV: verify in person troubleshooting option clicked' => :idv_verify_in_person_troubleshooting_option_clicked,
'Multi-Factor Authentication: download backup code' => :multi_factor_auth_backup_code_download,
- 'Show Password button clicked' => :show_password_button_clicked,
'Sign In: IdV requirements accordion clicked' => :sign_in_idv_requirements_accordion_clicked,
'User prompted before navigation' => :user_prompted_before_navigation,
'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page,
- }.transform_values { |method| AnalyticsEvents.instance_method(method) }.freeze
+ }.freeze
# rubocop:enable Layout/LineLength
def create
diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb
index d7782d08a55..95376bf6c19 100644
--- a/app/controllers/idv/by_mail/enter_code_controller.rb
+++ b/app/controllers/idv/by_mail/enter_code_controller.rb
@@ -1,155 +1,157 @@
-module Idv::ByMail
- class EnterCodeController < ApplicationController
- include IdvSession
- include Idv::StepIndicatorConcern
- include FraudReviewConcern
-
- prepend_before_action :note_if_user_did_not_receive_letter
- before_action :confirm_two_factor_authenticated
- before_action :confirm_verification_needed
-
- def index
- # GPO reminder emails include an "I did not receive my letter!" link that results in
- # slightly different copy on this screen.
- @user_did_not_receive_letter = !!params[:did_not_receive_letter]
-
- analytics.idv_verify_by_mail_enter_code_visited(
- source: if @user_did_not_receive_letter then 'gpo_reminder_email' end,
- )
-
- if rate_limiter.limited?
- render_rate_limited
- return
- end
-
- gpo_mail = Idv::GpoMail.new(current_user)
- @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii)
- @code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code?
-
- @should_prompt_user_to_request_another_letter =
- FeatureManagement.gpo_verification_enabled? &&
- !gpo_mail.mail_spammed? &&
- !gpo_mail.profile_too_old?
+module Idv
+ module ByMail
+ class EnterCodeController < ApplicationController
+ include IdvSession
+ include Idv::StepIndicatorConcern
+ include FraudReviewConcern
+
+ prepend_before_action :note_if_user_did_not_receive_letter
+ before_action :confirm_two_factor_authenticated
+ before_action :confirm_verification_needed
+
+ def index
+ # GPO reminder emails include an "I did not receive my letter!" link that results in
+ # slightly different copy on this screen.
+ @user_did_not_receive_letter = !!params[:did_not_receive_letter]
+
+ analytics.idv_verify_by_mail_enter_code_visited(
+ source: if @user_did_not_receive_letter then 'gpo_reminder_email' end,
+ )
- if pii_locked?
- redirect_to capture_password_url
- else
- render :index
+ if rate_limiter.limited?
+ render_rate_limited
+ return
+ end
+
+ gpo_mail = Idv::GpoMail.new(current_user)
+ @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii)
+ @code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code?
+
+ @should_prompt_user_to_request_another_letter =
+ FeatureManagement.gpo_verification_enabled? &&
+ !gpo_mail.mail_spammed? &&
+ !gpo_mail.profile_too_old?
+
+ if pii_locked?
+ redirect_to capture_password_url
+ else
+ render :index
+ end
end
- end
- def pii
- Pii::Cacher.new(current_user, user_session).fetch
- end
-
- def create
- if rate_limiter.limited?
- render_rate_limited
- return
+ def pii
+ Pii::Cacher.new(current_user, user_session).fetch
end
- rate_limiter.increment!
- @gpo_verify_form = build_gpo_verify_form
+ def create
+ if rate_limiter.limited?
+ render_rate_limited
+ return
+ end
+ rate_limiter.increment!
- result = @gpo_verify_form.submit
- analytics.idv_verify_by_mail_enter_code_submitted(**result.to_h)
- irs_attempts_api_tracker.idv_gpo_verification_submitted(
- success: result.success?,
- failure_reason: irs_attempts_api_tracker.parse_failure_reason(result),
- )
+ @gpo_verify_form = build_gpo_verify_form
- if !result.success?
- flash[:error] = @gpo_verify_form.errors.first.message
- redirect_to idv_verify_by_mail_enter_code_url
- return
- end
+ result = @gpo_verify_form.submit
+ analytics.idv_verify_by_mail_enter_code_submitted(**result.to_h)
+ irs_attempts_api_tracker.idv_gpo_verification_submitted(
+ success: result.success?,
+ failure_reason: irs_attempts_api_tracker.parse_failure_reason(result),
+ )
- prepare_for_personal_key
+ if !result.success?
+ flash[:error] = @gpo_verify_form.errors.first.message
+ redirect_to idv_verify_by_mail_enter_code_url
+ return
+ end
- redirect_to idv_personal_key_url
- end
+ prepare_for_personal_key
- private
+ redirect_to idv_personal_key_url
+ end
- def pending_in_person_enrollment?
- return false unless IdentityConfig.store.in_person_proofing_enabled
- current_user.pending_in_person_enrollment.present?
- end
+ private
- def account_not_ready_to_be_activated?
- fraud_check_failed? || pending_in_person_enrollment?
- end
+ def pending_in_person_enrollment?
+ return false unless IdentityConfig.store.in_person_proofing_enabled
+ current_user.pending_in_person_enrollment.present?
+ end
- def note_if_user_did_not_receive_letter
- if !current_user && params[:did_not_receive_letter]
- # Stash that the user didn't receive their letter.
- # Once the authentication process completes, they'll be redirected to complete their
- # GPO verification...
- session[:gpo_user_did_not_receive_letter] = true
+ def account_not_ready_to_be_activated?
+ fraud_check_failed? || pending_in_person_enrollment?
end
- if current_user && session.delete(:gpo_user_did_not_receive_letter)
- # ...and we can pick things up here.
- redirect_to idv_verify_by_mail_enter_code_path(did_not_receive_letter: 1)
+ def note_if_user_did_not_receive_letter
+ if !current_user && params[:did_not_receive_letter]
+ # Stash that the user didn't receive their letter.
+ # Once the authentication process completes, they'll be redirected to complete their
+ # GPO verification...
+ session[:gpo_user_did_not_receive_letter] = true
+ end
+
+ if current_user && session.delete(:gpo_user_did_not_receive_letter)
+ # ...and we can pick things up here.
+ redirect_to idv_verify_by_mail_enter_code_path(did_not_receive_letter: 1)
+ end
end
- end
- def prepare_for_personal_key
- unless account_not_ready_to_be_activated?
- event, _disavowal_token = create_user_event(:account_verified)
+ def prepare_for_personal_key
+ unless account_not_ready_to_be_activated?
+ event, _disavowal_token = create_user_event(:account_verified)
+
+ UserAlerts::AlertUserAboutAccountVerified.call(
+ user: current_user,
+ date_time: event.created_at,
+ sp_name: decorated_session.sp_name,
+ )
+ flash[:success] = t('account.index.verification.success')
+ end
+
+ idv_session.address_verification_mechanism = 'gpo'
+ idv_session.address_confirmed!
+ end
- UserAlerts::AlertUserAboutAccountVerified.call(
+ def rate_limiter
+ @rate_limiter ||= RateLimiter.new(
user: current_user,
- date_time: event.created_at,
- sp_name: decorated_session.sp_name,
+ rate_limit_type: :verify_gpo_key,
)
- flash[:success] = t('account.index.verification.success')
end
- idv_session.address_verification_mechanism = 'gpo'
- idv_session.address_confirmed!
- end
-
- def rate_limiter
- @rate_limiter ||= RateLimiter.new(
- user: current_user,
- rate_limit_type: :verify_gpo_key,
- )
- end
-
- def render_rate_limited
- irs_attempts_api_tracker.idv_gpo_verification_rate_limited
- analytics.rate_limit_reached(
- limiter_type: :verify_gpo_key,
- )
+ def render_rate_limited
+ irs_attempts_api_tracker.idv_gpo_verification_rate_limited
+ analytics.rate_limit_reached(
+ limiter_type: :verify_gpo_key,
+ )
- @expires_at = rate_limiter.expires_at
- render :rate_limited
- end
+ @expires_at = rate_limiter.expires_at
+ render :rate_limited
+ end
- def build_gpo_verify_form
- GpoVerifyForm.new(
- user: current_user,
- pii: pii,
- otp: params_otp,
- )
- end
+ def build_gpo_verify_form
+ GpoVerifyForm.new(
+ user: current_user,
+ pii: pii,
+ otp: params_otp,
+ )
+ end
- def params_otp
- params.require(:gpo_verify_form).permit(:otp)[:otp]
- end
+ def params_otp
+ params.require(:gpo_verify_form).permit(:otp)[:otp]
+ end
- def confirm_verification_needed
- return if current_user.gpo_verification_pending_profile?
- redirect_to account_url
- end
+ def confirm_verification_needed
+ return if current_user.gpo_verification_pending_profile?
+ redirect_to account_url
+ end
- def threatmetrix_enabled?
- FeatureManagement.proofing_device_profiling_decisioning_enabled?
- end
+ def threatmetrix_enabled?
+ FeatureManagement.proofing_device_profiling_decisioning_enabled?
+ end
- def pii_locked?
- !Pii::Cacher.new(current_user, user_session).exists_in_session?
+ def pii_locked?
+ !Pii::Cacher.new(current_user, user_session).exists_in_session?
+ end
end
end
end
diff --git a/app/controllers/idv/by_mail/letter_enqueued_controller.rb b/app/controllers/idv/by_mail/letter_enqueued_controller.rb
index 2a9bf4fb720..44823646216 100644
--- a/app/controllers/idv/by_mail/letter_enqueued_controller.rb
+++ b/app/controllers/idv/by_mail/letter_enqueued_controller.rb
@@ -1,19 +1,21 @@
-module Idv::ByMail
- class LetterEnqueuedController < ApplicationController
- include IdvSession
- include Idv::StepIndicatorConcern
+module Idv
+ module ByMail
+ class LetterEnqueuedController < ApplicationController
+ include IdvSession
+ include Idv::StepIndicatorConcern
- before_action :confirm_two_factor_authenticated
- before_action :confirm_user_needs_gpo_confirmation
+ before_action :confirm_two_factor_authenticated
+ before_action :confirm_user_needs_gpo_confirmation
- def show
- analytics.idv_letter_enqueued_visit
- end
+ def show
+ analytics.idv_letter_enqueued_visit
+ end
- private
+ private
- def confirm_user_needs_gpo_confirmation
- redirect_to account_url unless current_user.gpo_verification_pending_profile?
+ def confirm_user_needs_gpo_confirmation
+ redirect_to account_url unless current_user.gpo_verification_pending_profile?
+ end
end
end
end
diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb
index 6871c080375..fff9877c0f8 100644
--- a/app/controllers/idv/by_mail/request_letter_controller.rb
+++ b/app/controllers/idv/by_mail/request_letter_controller.rb
@@ -1,130 +1,131 @@
-module Idv::ByMail
- class RequestLetterController < ApplicationController
- include IdvSession
- include Idv::StepIndicatorConcern
- include Idv::AbTestAnalyticsConcern
-
- before_action :confirm_two_factor_authenticated
- before_action :confirm_idv_needed
- before_action :confirm_user_completed_idv_profile_step
- before_action :confirm_mail_not_spammed
- before_action :confirm_profile_not_too_old
-
- def index
- @presenter = RequestLetterPresenter.new(current_user, url_options)
- @step_indicator_current_step = step_indicator_current_step
- Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
- call(:usps_address, :view, true)
- analytics.idv_request_letter_visited(
- letter_already_sent: @presenter.resend_requested?,
- )
- end
+module Idv
+ module ByMail
+ class RequestLetterController < ApplicationController
+ include IdvSession
+ include Idv::StepIndicatorConcern
+ include Idv::AbTestAnalyticsConcern
+
+ before_action :confirm_two_factor_authenticated
+ before_action :confirm_idv_needed
+ before_action :confirm_user_completed_idv_profile_step
+ before_action :confirm_mail_not_spammed
+ before_action :confirm_profile_not_too_old
+
+ def index
+ @presenter = RequestLetterPresenter.new(current_user, url_options)
+ @step_indicator_current_step = step_indicator_current_step
+ Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
+ call(:usps_address, :view, true)
+ analytics.idv_request_letter_visited(
+ letter_already_sent: @presenter.resend_requested?,
+ )
+ end
- def create
- update_tracking
- idv_session.address_verification_mechanism = :gpo
-
- if resend_requested? && pii_locked?
- redirect_to capture_password_url
- elsif resend_requested?
- resend_letter
- flash[:success] = t('idv.messages.gpo.another_letter_on_the_way')
- redirect_to idv_letter_enqueued_url
- else
- redirect_to idv_review_url
+ def create
+ update_tracking
+ idv_session.address_verification_mechanism = :gpo
+
+ if resend_requested? && pii_locked?
+ redirect_to capture_password_url
+ elsif resend_requested?
+ resend_letter
+ flash[:success] = t('idv.messages.gpo.another_letter_on_the_way')
+ redirect_to idv_letter_enqueued_url
+ else
+ redirect_to idv_review_url
+ end
end
- end
- def gpo_mail_service
- @gpo_mail_service ||= Idv::GpoMail.new(current_user)
- end
+ def gpo_mail_service
+ @gpo_mail_service ||= Idv::GpoMail.new(current_user)
+ end
- private
+ private
- def confirm_profile_not_too_old
- redirect_to idv_path if gpo_mail_service.profile_too_old?
- end
+ def confirm_profile_not_too_old
+ redirect_to idv_path if gpo_mail_service.profile_too_old?
+ end
- def step_indicator_current_step
- if resend_requested?
- :get_a_letter
- else
- :verify_phone_or_address
+ def step_indicator_current_step
+ if resend_requested?
+ :get_a_letter
+ else
+ :verify_phone_or_address
+ end
end
- end
- def update_tracking
- Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
- call(:usps_letter_sent, :update, true)
-
- analytics.idv_gpo_address_letter_requested(
- resend: resend_requested?,
- first_letter_requested_at: first_letter_requested_at,
- hours_since_first_letter:
- gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
- phone_step_attempts: gpo_mail_service.phone_step_attempts,
- **ab_test_analytics_buckets,
- )
- irs_attempts_api_tracker.idv_gpo_letter_requested(resend: resend_requested?)
- create_user_event(:gpo_mail_sent, current_user)
-
- ProofingComponent.find_or_create_by(user: current_user).update(address_check: 'gpo_letter')
- end
+ def update_tracking
+ Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
+ call(:usps_letter_sent, :update, true)
+
+ analytics.idv_gpo_address_letter_requested(
+ resend: resend_requested?,
+ first_letter_requested_at: first_letter_requested_at,
+ hours_since_first_letter:
+ gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
+ phone_step_attempts: gpo_mail_service.phone_step_attempts,
+ **ab_test_analytics_buckets,
+ )
+ irs_attempts_api_tracker.idv_gpo_letter_requested(resend: resend_requested?)
+ create_user_event(:gpo_mail_sent, current_user)
+
+ ProofingComponent.find_or_create_by(user: current_user).update(address_check: 'gpo_letter')
+ end
- def resend_requested?
- current_user.gpo_verification_pending_profile?
- end
+ def resend_requested?
+ current_user.gpo_verification_pending_profile?
+ end
- def first_letter_requested_at
- current_user.gpo_verification_pending_profile&.gpo_verification_pending_at
- end
+ def first_letter_requested_at
+ current_user.gpo_verification_pending_profile&.gpo_verification_pending_at
+ end
- def confirm_mail_not_spammed
- redirect_to idv_review_url if idv_session.address_mechanism_chosen? &&
- gpo_mail_service.mail_spammed?
- end
+ def confirm_mail_not_spammed
+ redirect_to idv_review_url if gpo_mail_service.mail_spammed?
+ end
- def confirm_user_completed_idv_profile_step
- # If the user has a pending profile, they may have completed idv in a
- # different session and need a letter resent now
- return if current_user.gpo_verification_pending_profile?
- return if idv_session.verify_info_step_complete?
+ def confirm_user_completed_idv_profile_step
+ # If the user has a pending profile, they may have completed idv in a
+ # different session and need a letter resent now
+ return if current_user.gpo_verification_pending_profile?
+ return if idv_session.verify_info_step_complete?
- redirect_to idv_verify_info_url
- end
+ redirect_to idv_verify_info_url
+ end
- def resend_letter
- analytics.idv_gpo_address_letter_enqueued(
- enqueued_at: Time.zone.now,
- resend: true,
- first_letter_requested_at: first_letter_requested_at,
- hours_since_first_letter:
- gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
- phone_step_attempts: gpo_mail_service.phone_step_attempts,
- **ab_test_analytics_buckets,
- )
- confirmation_maker = confirmation_maker_perform
- send_reminder
- return unless FeatureManagement.reveal_gpo_code?
- session[:last_gpo_confirmation_code] = confirmation_maker.otp
- end
+ def resend_letter
+ analytics.idv_gpo_address_letter_enqueued(
+ enqueued_at: Time.zone.now,
+ resend: true,
+ first_letter_requested_at: first_letter_requested_at,
+ hours_since_first_letter:
+ gpo_mail_service.hours_since_first_letter(first_letter_requested_at),
+ phone_step_attempts: gpo_mail_service.phone_step_attempts,
+ **ab_test_analytics_buckets,
+ )
+ confirmation_maker = confirmation_maker_perform
+ send_reminder
+ return unless FeatureManagement.reveal_gpo_code?
+ session[:last_gpo_confirmation_code] = confirmation_maker.otp
+ end
- def confirmation_maker_perform
- confirmation_maker = GpoConfirmationMaker.new(
- pii: Pii::Cacher.new(current_user, user_session).fetch,
- service_provider: current_sp,
- profile: current_user.pending_profile,
- )
- confirmation_maker.perform
- confirmation_maker
- end
+ def confirmation_maker_perform
+ confirmation_maker = GpoConfirmationMaker.new(
+ pii: Pii::Cacher.new(current_user, user_session).fetch,
+ service_provider: current_sp,
+ profile: current_user.pending_profile,
+ )
+ confirmation_maker.perform
+ confirmation_maker
+ end
- def send_reminder
- current_user.send_email_to_all_addresses(:letter_reminder)
- end
+ def send_reminder
+ current_user.send_email_to_all_addresses(:letter_reminder)
+ end
- def pii_locked?
- !Pii::Cacher.new(current_user, user_session).exists_in_session?
+ def pii_locked?
+ !Pii::Cacher.new(current_user, user_session).exists_in_session?
+ end
end
end
end
diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb
index 8a717ba6968..6b9dad37590 100644
--- a/app/controllers/idv/in_person/ssn_controller.rb
+++ b/app/controllers/idv/in_person/ssn_controller.rb
@@ -14,8 +14,7 @@ class SsnController < ApplicationController
attr_accessor :error_message
def show
- incoming_ssn = idv_session.ssn || flow_session.dig(:pii_from_user, :ssn)
- @ssn_form = Idv::SsnFormatForm.new(current_user, incoming_ssn)
+ @ssn_form = Idv::SsnFormatForm.new(current_user, idv_session.ssn)
analytics.idv_doc_auth_redo_ssn_submitted(**analytics_arguments) if updating_ssn?
analytics.idv_doc_auth_ssn_visited(**analytics_arguments)
@@ -28,8 +27,7 @@ def show
def update
@error_message = nil
- incoming_ssn = idv_session.ssn || flow_session.dig(:pii_from_user, :ssn)
- @ssn_form = Idv::SsnFormatForm.new(current_user, incoming_ssn)
+ @ssn_form = Idv::SsnFormatForm.new(current_user, idv_session.ssn)
ssn = params.require(:doc_auth).permit(:ssn)
form_response = @ssn_form.submit(ssn)
@@ -42,7 +40,6 @@ def update
)
if form_response.success?
- flow_session[:pii_from_user][:ssn] = params[:doc_auth][:ssn]
idv_session.ssn = params[:doc_auth][:ssn]
idv_session.invalidate_steps_after_ssn!
redirect_to idv_in_person_verify_info_url
@@ -70,7 +67,7 @@ def flow_path
end
def confirm_repeat_ssn
- return if !idv_session.ssn && !pii_from_user[:ssn]
+ return if !idv_session.ssn
return if request.referer == idv_in_person_verify_info_url
redirect_to idv_in_person_verify_info_url
end
@@ -86,7 +83,7 @@ def analytics_arguments
end
def updating_ssn?
- idv_session.ssn.present? || flow_session.dig(:pii_from_user, :ssn).present?
+ idv_session.ssn.present?
end
def confirm_in_person_address_step_complete
diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb
index e2853047e3c..6ce2fc38a91 100644
--- a/app/controllers/idv/in_person/verify_info_controller.rb
+++ b/app/controllers/idv/in_person/verify_info_controller.rb
@@ -12,7 +12,7 @@ class VerifyInfoController < ApplicationController
def show
@step_indicator_steps = step_indicator_steps
- @ssn = idv_session.ssn || flow_session[:pii_from_user][:ssn]
+ @ssn = idv_session.ssn
@capture_secondary_id_enabled = capture_secondary_id_enabled
analytics.idv_doc_auth_verify_visited(**analytics_arguments)
diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb
index 4dd98df5847..d7b9ca201c3 100644
--- a/app/controllers/idv/session_errors_controller.rb
+++ b/app/controllers/idv/session_errors_controller.rb
@@ -39,9 +39,9 @@ def failure
def ssn_failure
rate_limiter = nil
- if ssn_from_doc
+ if idv_session&.ssn
rate_limiter = RateLimiter.new(
- target: Pii::Fingerprinter.fingerprint(ssn_from_doc),
+ target: Pii::Fingerprinter.fingerprint(idv_session.ssn),
rate_limit_type: :proof_ssn,
)
@expires_at = rate_limiter.expires_at
@@ -59,10 +59,6 @@ def rate_limited
private
- def ssn_from_doc
- idv_session&.ssn || user_session&.dig('idv/doc_auth', :pii_from_doc, :ssn)
- end
-
def confirm_two_factor_authenticated_or_user_id_in_session
return if session[:doc_capture_user_id].present?
diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb
index ab41a5b0df5..f0e64000d91 100644
--- a/app/controllers/idv/ssn_controller.rb
+++ b/app/controllers/idv/ssn_controller.rb
@@ -13,8 +13,7 @@ class SsnController < ApplicationController
attr_accessor :error_message
def show
- incoming_ssn = idv_session.ssn || flow_session.dig(:pii_from_doc, :ssn)
- @ssn_form = Idv::SsnFormatForm.new(current_user, incoming_ssn)
+ @ssn_form = Idv::SsnFormatForm.new(current_user, idv_session.ssn)
analytics.idv_doc_auth_redo_ssn_submitted(**analytics_arguments) if @ssn_form.updating_ssn?
analytics.idv_doc_auth_ssn_visited(**analytics_arguments)
@@ -28,8 +27,7 @@ def show
def update
@error_message = nil
- incoming_ssn = idv_session.ssn || flow_session.dig(:pii_from_doc, :ssn)
- @ssn_form = Idv::SsnFormatForm.new(current_user, incoming_ssn)
+ @ssn_form = Idv::SsnFormatForm.new(current_user, idv_session.ssn)
form_response = @ssn_form.submit(params.require(:doc_auth).permit(:ssn))
analytics.idv_doc_auth_ssn_submitted(
@@ -40,7 +38,6 @@ def update
)
if form_response.success?
- flow_session[:pii_from_doc][:ssn] = params[:doc_auth][:ssn]
idv_session.ssn = params[:doc_auth][:ssn]
idv_session.invalidate_steps_after_ssn!
redirect_to next_url
@@ -53,7 +50,7 @@ def update
private
def confirm_repeat_ssn
- return if !idv_session.ssn && !pii_from_doc[:ssn]
+ return if !idv_session.ssn
return if request.referer == idv_verify_info_url
redirect_to idv_verify_info_url
diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb
index 833143b4cb7..cd4f7e1c340 100644
--- a/app/controllers/idv/verify_info_controller.rb
+++ b/app/controllers/idv/verify_info_controller.rb
@@ -11,7 +11,7 @@ class VerifyInfoController < ApplicationController
def show
@step_indicator_steps = step_indicator_steps
- @ssn = idv_session.ssn || pii_from_doc[:ssn]
+ @ssn = idv_session.ssn
analytics.idv_doc_auth_verify_visited(**analytics_arguments)
Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]).
diff --git a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
index 7c496cafde8..0edb08b8fb1 100644
--- a/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
+++ b/app/controllers/two_factor_authentication/webauthn_verification_controller.rb
@@ -15,7 +15,10 @@ def show
def confirm
result = form.submit
analytics.track_mfa_submit_event(
- result.to_h.merge(analytics_properties),
+ **result.to_h,
+ **analytics_properties,
+ multi_factor_auth_method_created_at:
+ webauthn_configuration_or_latest.created_at.strftime('%s%L'),
)
if analytics_properties[:multi_factor_auth_method] == 'webauthn_platform'
@@ -95,7 +98,7 @@ def save_challenge_in_session
end
def credentials
- MfaContext.new(current_user).webauthn_configurations.
+ webauthn_configurations.
select { |configuration| configuration.platform_authenticator? == platform_authenticator? }.
map do |configuration|
{ id: configuration.credential_id, transports: configuration.transports }
@@ -138,5 +141,13 @@ def check_sp_required_mfa
def platform_authenticator?
params[:platform].to_s == 'true'
end
+
+ def webauthn_configuration_or_latest
+ form.webauthn_configuration || webauthn_configurations.first
+ end
+
+ def webauthn_configurations
+ MfaContext.new(current_user).webauthn_configurations.order(created_at: :desc)
+ end
end
end
diff --git a/app/controllers/users/delete_controller.rb b/app/controllers/users/delete_controller.rb
index 5402f5f809b..ca76540140d 100644
--- a/app/controllers/users/delete_controller.rb
+++ b/app/controllers/users/delete_controller.rb
@@ -1,7 +1,10 @@
module Users
class DeleteController < ApplicationController
+ include ReauthenticationRequiredConcern
+
before_action :confirm_two_factor_authenticated
before_action :confirm_current_password, only: [:delete]
+ before_action :confirm_recently_authenticated_2fa
def show
analytics.account_delete_visited
diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb
index b2da5680250..5600061a3eb 100644
--- a/app/helpers/script_helper.rb
+++ b/app/helpers/script_helper.rb
@@ -19,11 +19,11 @@ def javascript_packs_tag_once(*names, prepend: false)
alias_method :enqueue_component_scripts, :javascript_packs_tag_once
def render_javascript_pack_once_tags(*names)
- javascript_packs_tag_once(*names) if names.present?
- if @scripts && (sources = AssetSources.get_sources(*@scripts)).present?
+ names = names.presence || @scripts
+ if names && (sources = AssetSources.get_sources(*names)).present?
safe_join(
[
- javascript_assets_tag(*@scripts),
+ javascript_assets_tag(*names),
*sources.map do |source|
javascript_include_tag(
source,
diff --git a/app/javascript/packages/analytics/index.spec.ts b/app/javascript/packages/analytics/index.spec.ts
index e206e138ff4..8befbf1763d 100644
--- a/app/javascript/packages/analytics/index.spec.ts
+++ b/app/javascript/packages/analytics/index.spec.ts
@@ -1,5 +1,5 @@
import { trackEvent, trackError } from '@18f/identity-analytics';
-import { usePropertyValue, useSandbox } from '@18f/identity-test-helpers';
+import { useSandbox } from '@18f/identity-test-helpers';
import type { SinonStub } from 'sinon';
describe('trackEvent', () => {
@@ -84,20 +84,28 @@ describe('trackEvent', () => {
});
describe('trackError', () => {
- it('is a noop', () => {
- trackError(new Error('Oops!'));
+ const sandbox = useSandbox();
+ const endpoint = '/log';
+
+ beforeEach(() => {
+ sandbox.stub(global.navigator, 'sendBeacon').returns(true);
+ document.body.innerHTML = ``;
});
- context('with newrelic agent present', () => {
- const sandbox = useSandbox();
- const noticeError = sandbox.stub();
- usePropertyValue(globalThis as any, 'newrelic', { noticeError });
+ it('tracks event', async () => {
+ trackError(new Error('Oops!'));
- it('notices error in newrelic', () => {
- const error = new Error('Oops!');
- trackError(error);
+ expect(global.navigator.sendBeacon).to.have.been.calledOnce();
- expect(noticeError).to.have.been.calledWith(error);
- });
+ const [actualEndpoint, data] = (global.navigator.sendBeacon as SinonStub).firstCall.args;
+ expect(actualEndpoint).to.eql(endpoint);
+
+ const { event, payload } = JSON.parse(await data.text());
+ const { name, message, stack } = payload;
+
+ expect(event).to.equal('Frontend Error');
+ expect(name).to.equal('Error');
+ expect(message).to.equal('Oops!');
+ expect(stack).to.be.a('string');
});
});
diff --git a/app/javascript/packages/analytics/index.ts b/app/javascript/packages/analytics/index.ts
index 1f0f43d4c0f..cf4cf11d26b 100644
--- a/app/javascript/packages/analytics/index.ts
+++ b/app/javascript/packages/analytics/index.ts
@@ -1,11 +1,6 @@
-import type { noticeError } from 'newrelic';
import { getConfigValue } from '@18f/identity-config';
-type NewRelicAgent = { noticeError: typeof noticeError };
-
-interface NewRelicGlobals {
- newrelic?: NewRelicAgent;
-}
+export { default as isTrackableErrorEvent } from './is-trackable-error-event';
/**
* Logs an event.
@@ -30,6 +25,5 @@ export function trackEvent(event: string, payload?: object) {
*
* @param error Error object.
*/
-export function trackError(error: Error) {
- (globalThis as typeof globalThis & NewRelicGlobals).newrelic?.noticeError(error);
-}
+export const trackError = ({ name, message, stack }: Error) =>
+ trackEvent('Frontend Error', { name, message, stack });
diff --git a/app/javascript/packages/analytics/is-trackable-error-event.spec.ts b/app/javascript/packages/analytics/is-trackable-error-event.spec.ts
new file mode 100644
index 00000000000..7b7b53ec5cb
--- /dev/null
+++ b/app/javascript/packages/analytics/is-trackable-error-event.spec.ts
@@ -0,0 +1,37 @@
+import isTrackableErrorEvent from './is-trackable-error-event';
+
+describe('isTrackableErrorEvent', () => {
+ context('with filename not present on event', () => {
+ const event = new ErrorEvent('error');
+
+ it('returns false', () => {
+ expect(isTrackableErrorEvent(event)).to.be.false();
+ });
+ });
+
+ context('with filename as an invalid url', () => {
+ const event = new ErrorEvent('error', { filename: '.' });
+
+ it('returns false', () => {
+ expect(isTrackableErrorEvent(event)).to.be.false();
+ });
+ });
+
+ context('with filename from a different host', () => {
+ const event = new ErrorEvent('error', { filename: 'http://different.example.com/foo.js' });
+
+ it('returns false', () => {
+ expect(isTrackableErrorEvent(event)).to.be.false();
+ });
+ });
+
+ context('with filename from the same host', () => {
+ const event = new ErrorEvent('error', {
+ filename: new URL('foo.js', window.location.origin).toString(),
+ });
+
+ it('returns true', () => {
+ expect(isTrackableErrorEvent(event)).to.be.true();
+ });
+ });
+});
diff --git a/app/javascript/packages/analytics/is-trackable-error-event.ts b/app/javascript/packages/analytics/is-trackable-error-event.ts
new file mode 100644
index 00000000000..b5e10cb519e
--- /dev/null
+++ b/app/javascript/packages/analytics/is-trackable-error-event.ts
@@ -0,0 +1,9 @@
+function isTrackableErrorEvent(event: ErrorEvent): boolean {
+ try {
+ return new URL(event.filename).host === window.location.host;
+ } catch {
+ return false;
+ }
+}
+
+export default isTrackableErrorEvent;
diff --git a/app/javascript/packages/i18n/.gitignore b/app/javascript/packages/i18n/.gitignore
new file mode 100644
index 00000000000..849ddff3b7e
--- /dev/null
+++ b/app/javascript/packages/i18n/.gitignore
@@ -0,0 +1 @@
+dist/
diff --git a/app/javascript/packages/i18n/CHANGELOG.md b/app/javascript/packages/i18n/CHANGELOG.md
new file mode 100644
index 00000000000..fddaf37c74b
--- /dev/null
+++ b/app/javascript/packages/i18n/CHANGELOG.md
@@ -0,0 +1,5 @@
+# `Change Log`
+
+## 1.0.1
+
+- Initial release
diff --git a/app/javascript/packages/i18n/LICENSE.md b/app/javascript/packages/i18n/LICENSE.md
new file mode 100644
index 00000000000..a9eb4a921cb
--- /dev/null
+++ b/app/javascript/packages/i18n/LICENSE.md
@@ -0,0 +1,21 @@
+# License
+
+As a work of the [United States government](https://www.usa.gov/), this project is in the public domain within the United States of America.
+
+Additionally, we waive copyright and related rights in the work worldwide through the CC0 1.0 Universal public domain dedication.
+
+## CC0 1.0 Universal Summary
+
+This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
+
+### No Copyright
+
+The person who associated a work with this deed has dedicated the work to the public domain by waiving all of their rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
+
+You can copy, modify, distribute, and perform the work, even for commercial purposes, all without asking permission.
+
+### Other Information
+
+In no way are the patent or trademark rights of any person affected by CC0, nor are the rights that other persons may have in the work or in how the work is used, such as publicity or privacy rights.
+
+Unless expressly stated otherwise, the person who associated a work with this deed makes no warranties about the work, and disclaims liability for all uses of the work, to the fullest extent permitted by applicable law. When using or citing the work, you should not imply endorsement by the author or the affirmer.
diff --git a/app/javascript/packages/i18n/babel.config.json b/app/javascript/packages/i18n/babel.config.json
new file mode 100644
index 00000000000..6526ba5735a
--- /dev/null
+++ b/app/javascript/packages/i18n/babel.config.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../../../babel.config.js",
+ "presets": [["@babel/preset-env", { "modules": false }]]
+}
diff --git a/app/javascript/packages/i18n/package.json b/app/javascript/packages/i18n/package.json
index 95f64304fc6..0b07c95f6f9 100644
--- a/app/javascript/packages/i18n/package.json
+++ b/app/javascript/packages/i18n/package.json
@@ -1,5 +1,28 @@
{
"name": "@18f/identity-i18n",
- "private": true,
- "version": "1.0.0"
+ "private": false,
+ "version": "1.0.1",
+ "type": "module",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "prepublishOnly": "babel index.ts --out-dir dist --extensions '.ts,.js'"
+ },
+ "exports": {
+ ".": {
+ "source": "./index.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "license": "CC0-1.0",
+ "bugs": {
+ "url": "https://github.com/18f/identity-idp/issues"
+ },
+ "homepage": "https://github.com/18f/identity-idp",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/18f/identity-idp.git",
+ "directory": "app/javascript/packages/i18n"
+ }
}
diff --git a/app/javascript/packages/password-confirmation/password-confirmation-element.spec.ts b/app/javascript/packages/password-confirmation/password-confirmation-element.spec.ts
index ad841565a13..fcbe92eb92d 100644
--- a/app/javascript/packages/password-confirmation/password-confirmation-element.spec.ts
+++ b/app/javascript/packages/password-confirmation/password-confirmation-element.spec.ts
@@ -1,7 +1,5 @@
import userEvent from '@testing-library/user-event';
import { getByLabelText, waitFor } from '@testing-library/dom';
-import { useSandbox } from '@18f/identity-test-helpers';
-import * as analytics from '@18f/identity-analytics';
import './password-confirmation-element';
import type PasswordConfirmationElement from './password-confirmation-element';
@@ -10,7 +8,6 @@ describe('PasswordConfirmationElement', () => {
let input1: HTMLInputElement;
let input2: HTMLInputElement;
let idCounter = 0;
- const sandbox = useSandbox();
function createElement() {
element = document.createElement('lg-password-confirmation') as PasswordConfirmationElement;
@@ -53,17 +50,6 @@ describe('PasswordConfirmationElement', () => {
expect(input1.type).to.equal('text');
});
- it('logs an event when clicking the Show Password button', async () => {
- sandbox.stub(analytics, 'trackEvent');
- const toggle = getByLabelText(element, 'Show password') as HTMLInputElement;
-
- await userEvent.click(toggle);
-
- expect(analytics.trackEvent).to.have.been.calledWith('Show Password button clicked', {
- path: window.location.pathname,
- });
- });
-
describe('Password validation', () => {
it('validates passwords in both directions', async () => {
await userEvent.type(input1, 'salty pickles');
diff --git a/app/javascript/packages/password-confirmation/password-confirmation-element.ts b/app/javascript/packages/password-confirmation/password-confirmation-element.ts
index d63ea158c2c..13e2f38137d 100644
--- a/app/javascript/packages/password-confirmation/password-confirmation-element.ts
+++ b/app/javascript/packages/password-confirmation/password-confirmation-element.ts
@@ -1,10 +1,8 @@
-import { trackEvent } from '@18f/identity-analytics';
import { t } from '@18f/identity-i18n';
class PasswordConfirmationElement extends HTMLElement {
connectedCallback() {
this.toggle.addEventListener('change', () => this.setInputType());
- this.toggle.addEventListener('click', () => this.trackToggleEvent());
this.input.addEventListener('input', () => this.validatePassword());
this.inputConfirmation.addEventListener('input', () => this.validatePassword());
this.setInputType();
@@ -37,10 +35,6 @@ class PasswordConfirmationElement extends HTMLElement {
this.inputConfirmation.type = checked;
}
- trackToggleEvent() {
- trackEvent('Show Password button clicked', { path: window.location.pathname });
- }
-
validatePassword() {
const password = this.input.value;
const confirmation = this.inputConfirmation.value;
diff --git a/app/javascript/packages/password-toggle/password-toggle-element.spec.ts b/app/javascript/packages/password-toggle/password-toggle-element.spec.ts
index e2dd0229f68..bb3ec06f2ba 100644
--- a/app/javascript/packages/password-toggle/password-toggle-element.spec.ts
+++ b/app/javascript/packages/password-toggle/password-toggle-element.spec.ts
@@ -1,13 +1,10 @@
import userEvent from '@testing-library/user-event';
import { getByLabelText } from '@testing-library/dom';
-import { useSandbox } from '@18f/identity-test-helpers';
-import * as analytics from '@18f/identity-analytics';
import './password-toggle-element';
import type PasswordToggleElement from './password-toggle-element';
describe('PasswordToggleElement', () => {
let idCounter = 0;
- const sandbox = useSandbox();
function createElement() {
const element = document.createElement('lg-password-toggle') as PasswordToggleElement;
@@ -48,16 +45,4 @@ describe('PasswordToggleElement', () => {
expect(input.type).to.equal('text');
});
-
- it('logs an event when clicking the Show Password button', async () => {
- sandbox.stub(analytics, 'trackEvent');
- const element = createElement();
- const toggle = getByLabelText(element, 'Show password') as HTMLInputElement;
-
- await userEvent.click(toggle);
-
- expect(analytics.trackEvent).to.have.been.calledWith('Show Password button clicked', {
- path: window.location.pathname,
- });
- });
});
diff --git a/app/javascript/packages/password-toggle/password-toggle-element.ts b/app/javascript/packages/password-toggle/password-toggle-element.ts
index 815267d2b39..3405deb6cd1 100644
--- a/app/javascript/packages/password-toggle/password-toggle-element.ts
+++ b/app/javascript/packages/password-toggle/password-toggle-element.ts
@@ -1,9 +1,6 @@
-import { trackEvent } from '@18f/identity-analytics';
-
class PasswordToggleElement extends HTMLElement {
connectedCallback() {
this.toggle.addEventListener('change', () => this.setInputType());
- this.toggle.addEventListener('click', () => this.trackToggleEvent());
this.setInputType();
}
@@ -24,10 +21,6 @@ class PasswordToggleElement extends HTMLElement {
setInputType() {
this.input.type = this.toggle.checked ? 'text' : 'password';
}
-
- trackToggleEvent() {
- trackEvent('Show Password button clicked', { path: window.location.pathname });
- }
}
declare global {
diff --git a/app/javascript/packs/track-errors.ts b/app/javascript/packs/track-errors.ts
new file mode 100644
index 00000000000..47dbeef4154
--- /dev/null
+++ b/app/javascript/packs/track-errors.ts
@@ -0,0 +1,14 @@
+import { trackError, isTrackableErrorEvent } from '@18f/identity-analytics';
+
+export interface WindowWithInitialErrors extends Window {
+ _e: ErrorEvent[];
+}
+
+declare let window: WindowWithInitialErrors;
+
+const { _e: initialErrors } = window;
+
+const handleErrorEvent = (event: ErrorEvent) =>
+ isTrackableErrorEvent(event) && trackError(event.error);
+initialErrors.forEach(handleErrorEvent);
+window.addEventListener('error', handleErrorEvent);
diff --git a/app/presenters/idv/by_mail/request_letter_presenter.rb b/app/presenters/idv/by_mail/request_letter_presenter.rb
index ee96ad76d7a..c95619606dd 100644
--- a/app/presenters/idv/by_mail/request_letter_presenter.rb
+++ b/app/presenters/idv/by_mail/request_letter_presenter.rb
@@ -1,51 +1,53 @@
-module Idv::ByMail
- class RequestLetterPresenter
- include Rails.application.routes.url_helpers
+module Idv
+ module ByMail
+ class RequestLetterPresenter
+ include Rails.application.routes.url_helpers
- attr_reader :current_user, :url_options
+ attr_reader :current_user, :url_options
- def initialize(current_user, url_options)
- @current_user = current_user
- @url_options = url_options
- end
+ def initialize(current_user, url_options)
+ @current_user = current_user
+ @url_options = url_options
+ end
- def title
- resend_requested? ? I18n.t('idv.titles.mail.resend') : I18n.t('idv.titles.mail.verify')
- end
+ def title
+ resend_requested? ? I18n.t('idv.titles.mail.resend') : I18n.t('idv.titles.mail.verify')
+ end
- def button
- resend_requested? ? I18n.t('idv.buttons.mail.resend') : I18n.t('idv.buttons.mail.send')
- end
+ def button
+ resend_requested? ? I18n.t('idv.buttons.mail.resend') : I18n.t('idv.buttons.mail.send')
+ end
- def fallback_back_path
- return idv_verify_info_path if OutageStatus.new.any_phone_vendor_outage?
- user_needs_address_otp_verification? ? idv_verify_by_mail_enter_code_path : idv_phone_path
- end
+ def fallback_back_path
+ return idv_verify_info_path if OutageStatus.new.any_phone_vendor_outage?
+ user_needs_address_otp_verification? ? idv_verify_by_mail_enter_code_path : idv_phone_path
+ end
- def resend_requested?
- current_user.gpo_verification_pending_profile?
- end
+ def resend_requested?
+ current_user.gpo_verification_pending_profile?
+ end
- def back_or_cancel_partial
- if FeatureManagement.idv_by_mail_only?
- 'idv/doc_auth/cancel'
- else
- 'idv/shared/back'
+ def back_or_cancel_partial
+ if FeatureManagement.idv_by_mail_only?
+ 'idv/doc_auth/cancel'
+ else
+ 'idv/shared/back'
+ end
end
- end
- def back_or_cancel_parameters
- if FeatureManagement.idv_by_mail_only?
- { step: 'gpo' }
- else
- { fallback_path: fallback_back_path }
+ def back_or_cancel_parameters
+ if FeatureManagement.idv_by_mail_only?
+ { step: 'gpo' }
+ else
+ { fallback_path: fallback_back_path }
+ end
end
- end
- private
+ private
- def user_needs_address_otp_verification?
- current_user.pending_profile?
+ def user_needs_address_otp_verification?
+ current_user.pending_profile?
+ end
end
end
end
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index de0670f125a..ff4370f3215 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -573,6 +573,14 @@ def fraud_review_rejected(
)
end
+ # An uncaught error occurred in frontend JavaScript
+ # @param [String] name
+ # @param [String] message
+ # @param [String] stack
+ def frontend_error(name:, message:, stack: nil, **extra)
+ track_event('Frontend Error', name:, message:, stack:, **extra)
+ end
+
# @param [Boolean] success
# @param [Boolean] address_edited
# @param [Hash] pii_like_keypaths
@@ -3724,12 +3732,6 @@ def session_total_duration_timeout
track_event('User Maximum Session Length Exceeded')
end
- # Tracks if a user clicks the "Show Password button"
- # @param [String] path URL path where the click occurred
- def show_password_button_clicked(path:, **extra)
- track_event('Show Password Button Clicked', path: path, **extra)
- end
-
# Tracks if a user clicks the "You will also need" accordion on the homepage
def sign_in_idv_requirements_accordion_clicked
track_event('Sign In: IdV requirements accordion clicked')
diff --git a/app/services/frontend_error_logger.rb b/app/services/frontend_error_logger.rb
new file mode 100644
index 00000000000..fe3b61ea373
--- /dev/null
+++ b/app/services/frontend_error_logger.rb
@@ -0,0 +1,11 @@
+class FrontendErrorLogger
+ class FrontendError < StandardError; end
+
+ def self.track_error(name:, message:, stack:)
+ NewRelic::Agent.notice_error(
+ FrontendError.new,
+ expected: true,
+ custom_params: { frontend_error: { name:, message:, stack: } },
+ )
+ end
+end
diff --git a/app/services/frontend_logger.rb b/app/services/frontend_logger.rb
index 6155253e9a1..1e5034d003a 100644
--- a/app/services/frontend_logger.rb
+++ b/app/services/frontend_logger.rb
@@ -1,14 +1,27 @@
class FrontendLogger
attr_reader :analytics, :event_map
+ # @param [Analytics] analytics
+ # @param [Hash{String=>Symbol,#call}] event_map map of string event names to method names
+ # on Analytics, or a custom implementation that's callable (like a Proc or Method)
def initialize(analytics:, event_map:)
@analytics = analytics
@event_map = event_map
end
+ # Logs an event and converts the payload to the correct keyword args
+ # @param [String] name
+ # @param [Hash] attributes payload with string keys
def track_event(name, attributes)
- if (analytics_method = event_map[name])
- analytics.send(analytics_method.name, **hash_from_method_kwargs(attributes, analytics_method))
+ analytics_method = event_map[name]
+
+ if analytics_method.is_a?(Symbol)
+ analytics.send(
+ analytics_method,
+ **hash_from_kwargs(attributes, AnalyticsEvents.instance_method(analytics_method)),
+ )
+ elsif analytics_method.respond_to?(:call)
+ analytics_method.call(**hash_from_kwargs(attributes, analytics_method))
else
analytics.track_event("Frontend: #{name}", attributes)
end
@@ -16,12 +29,17 @@ def track_event(name, attributes)
private
- def hash_from_method_kwargs(hash, method)
- method_kwargs(method).index_with { |key| hash[key.to_s] }
+ # @param [Hash] hash
+ # @param [Proc,Method] callable
+ # @return [Hash]
+ def hash_from_kwargs(hash, callable)
+ kwargs(callable).index_with { |key| hash[key.to_s] }
end
- def method_kwargs(method)
- method.
+ # @param [Proc,Method] callable
+ # @return [Array] the names of the kwargs for the callable (both optional and required)
+ def kwargs(callable)
+ callable.
parameters.
map { |type, name| name if [:key, :keyreq].include?(type) }.
compact
diff --git a/app/services/idv/gpo_mail.rb b/app/services/idv/gpo_mail.rb
index ed1d082784c..381657c1131 100644
--- a/app/services/idv/gpo_mail.rb
+++ b/app/services/idv/gpo_mail.rb
@@ -1,15 +1,11 @@
module Idv
class GpoMail
- MAX_MAIL_EVENTS = IdentityConfig.store.max_mail_events
- MAIL_EVENTS_WINDOW_DAYS = IdentityConfig.store.max_mail_events_window_in_days
-
def initialize(current_user)
@current_user = current_user
end
def mail_spammed?
- return false if user_mail_events.empty?
- max_events? && updated_within_last_month?
+ too_many_letter_requests_within_window? || last_letter_request_too_recent?
end
def profile_too_old?
@@ -37,21 +33,48 @@ def hours_since_first_letter(first_letter_requested_at)
private
+ def window_limit_enabled?
+ IdentityConfig.store.max_mail_events != 0 &&
+ IdentityConfig.store.max_mail_events_window_in_days != 0
+ end
+
+ def last_not_too_recent_enabled?
+ IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours != 0
+ end
+
attr_reader :current_user
- def user_mail_events
- @user_mail_events ||= current_user.events.
- gpo_mail_sent.
- order('updated_at DESC').
- limit(MAX_MAIL_EVENTS)
+ def too_many_letter_requests_within_window?
+ return false unless window_limit_enabled?
+
+ number_of_letter_requests_within(
+ IdentityConfig.store.max_mail_events_window_in_days.days,
+ maximum: IdentityConfig.store.max_mail_events,
+ ) >= IdentityConfig.store.max_mail_events
end
- def max_events?
- user_mail_events.size == MAX_MAIL_EVENTS
+ def last_letter_request_too_recent?
+ return false unless last_not_too_recent_enabled?
+ return false unless current_user.gpo_verification_pending_profile?
+
+ number_of_letter_requests_within(
+ IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours.hours,
+ maximum: 1,
+ for_profile: current_user.gpo_verification_pending_profile,
+ ) > 0
end
- def updated_within_last_month?
- user_mail_events.last.updated_at > MAIL_EVENTS_WINDOW_DAYS.days.ago
+ def number_of_letter_requests_within(time_window, maximum:, for_profile: nil)
+ profile_query_conditions = { user: current_user }
+ profile_query_conditions[:id] = for_profile.id if for_profile
+
+ GpoConfirmationCode.joins(:profile).
+ where(
+ updated_at: (time_window.ago..),
+ profile: profile_query_conditions,
+ ).
+ limit(maximum).
+ count
end
end
end
diff --git a/app/services/rate_limiter.rb b/app/services/rate_limiter.rb
index 6e7a0f8b656..f278b178fe3 100644
--- a/app/services/rate_limiter.rb
+++ b/app/services/rate_limiter.rb
@@ -48,7 +48,7 @@ def attempted_at
end
def expires_at
- return Time.zone.now if attempted_at.blank?
+ return nil if attempted_at.blank?
attempted_at + RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes
end
@@ -59,6 +59,7 @@ def remaining_count
end
def expired?
+ return nil if expires_at.nil?
expires_at <= Time.zone.now
end
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index 2c21d28ea3e..a103815d8f8 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -55,8 +55,9 @@
) %>
- <% if BrowserSupport.supported?(request.user_agent) && IdentityConfig.store.newrelic_browser_key.present? && IdentityConfig.store.newrelic_browser_app_id.present? %>
- <%= render 'shared/newrelic/browser_instrumentation' %>
+ <%# Prelude script for error tracking (see `track-errors`) %>
+ <%= javascript_tag(nonce: true) do %>
+ _e=[],addEventListener("error",(e)=>_e.push(e));
<% end %>
<%= yield(:head) if content_for?(:head) %>
@@ -109,6 +110,7 @@
false,
) %>
<%= javascript_packs_tag_once('application', prepend: true) %>
+ <%= javascript_packs_tag_once('track-errors') if BrowserSupport.supported?(request.user_agent) %>
<%= render_javascript_pack_once_tags %>
<%= render 'shared/dap_analytics' if IdentityConfig.store.participate_in_dap && !session_with_trust? %>
diff --git a/app/views/shared/newrelic/_browser_instrumentation.html.erb b/app/views/shared/newrelic/_browser_instrumentation.html.erb
deleted file mode 100644
index 8940160b924..00000000000
--- a/app/views/shared/newrelic/_browser_instrumentation.html.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-<%= javascript_tag(nonce: true) do %>
- window.NREUM||(NREUM={}),__nr_require=function(t,e,n){function r(n){if(!e[n]){var o=e[n]={exports:{}};t[n][0].call(o.exports,function(e){var o=t[n][1][e];return r(o||e)},o,o.exports)}return e[n].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;o0&&(d-=1)}),c.on("internal-error",function(t){i("ierr",[t,(new Date).getTime(),!0])})},{}],3:[function(t,e,n){t("loader").features.ins=!0},{}],4:[function(t,e,n){function r(t){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var o=t("ee"),i=t("handle"),a=t(8),c=t(7),s="learResourceTimings",f="addEventListener",u="resourcetimingbufferfull",d="bstResource",l="resource",p="-start",h="-end",m="fn"+p,w="fn"+h,v="bstTimer",y="pushState";t("loader").features.stn=!0,t(6);var g=NREUM.o.EV;o.on(m,function(t,e){var n=t[0];n instanceof g&&(this.bstStart=Date.now())}),o.on(w,function(t,e){var n=t[0];n instanceof g&&i("bst",[n,e,this.bstStart,Date.now()])}),a.on(m,function(t,e,n){this.bstStart=Date.now(),this.bstType=n}),a.on(w,function(t,e){i(v,[e,this.bstStart,Date.now(),this.bstType])}),c.on(m,function(){this.bstStart=Date.now()}),c.on(w,function(t,e){i(v,[e,this.bstStart,Date.now(),"requestAnimationFrame"])}),o.on(y+p,function(t){this.time=Date.now(),this.startPath=location.pathname+location.hash}),o.on(y+h,function(t){i("bstHist",[location.pathname+location.hash,this.startPath,this.time])}),f in window.performance&&(window.performance["c"+s]?window.performance[f](u,function(t){i(d,[window.performance.getEntriesByType(l)]),window.performance["c"+s]()},!1):window.performance[f]("webkit"+u,function(t){i(d,[window.performance.getEntriesByType(l)]),window.performance["webkitC"+s]()},!1)),document[f]("scroll",r,!1),document[f]("keypress",r,!1),document[f]("click",r,!1)}},{}],5:[function(t,e,n){function r(t){for(var e=t;e&&!e.hasOwnProperty(u);)e=Object.getPrototypeOf(e);e&&o(e)}function o(t){c.inPlace(t,[u,d],"-",i)}function i(t,e){return t[1]}var a=t("ee").get("events"),c=t(17)(a,!0),s=t("gos"),f=XMLHttpRequest,u="addEventListener",d="removeEventListener";e.exports=a,"getPrototypeOf"in Object?(r(document),r(window),r(f.prototype)):f.prototype.hasOwnProperty(u)&&(o(window),o(f.prototype)),a.on(u+"-start",function(t,e){var n=t[1],r=s(n,"nr@wrapped",function(){function t(){if("function"==typeof n.handleEvent)return n.handleEvent.apply(n,arguments)}var e={object:t,"function":n}[typeof n];return e?c(e,"fn-",null,e.name||"anonymous"):n});this.wrapped=t[1]=r}),a.on(d+"-start",function(t){t[1]=this.wrapped||t[1]})},{}],6:[function(t,e,n){var r=t("ee").get("history"),o=t(17)(r);e.exports=r,o.inPlace(window.history,["pushState","replaceState"],"-")},{}],7:[function(t,e,n){var r=t("ee").get("raf"),o=t(17)(r),i="equestAnimationFrame";e.exports=r,o.inPlace(window,["r"+i,"mozR"+i,"webkitR"+i,"msR"+i],"raf-"),r.on("raf-start",function(t){t[0]=o(t[0],"fn-")})},{}],8:[function(t,e,n){function r(t,e,n){t[0]=a(t[0],"fn-",null,n)}function o(t,e,n){this.method=n,this.timerDuration="number"==typeof t[1]?t[1]:0,t[0]=a(t[0],"fn-",this,n)}var i=t("ee").get("timer"),a=t(17)(i),c="setTimeout",s="setInterval",f="clearTimeout",u="-start",d="-";e.exports=i,a.inPlace(window,[c,"setImmediate"],c+d),a.inPlace(window,[s],s+d),a.inPlace(window,[f,"clearImmediate"],f+d),i.on(s+u,r),i.on(c+u,o)},{}],9:[function(t,e,n){function r(t,e){d.inPlace(e,["onreadystatechange"],"fn-",c)}function o(){var t=this,e=u.context(t);t.readyState>3&&!e.resolved&&(e.resolved=!0,u.emit("xhr-resolved",[],t)),d.inPlace(t,w,"fn-",c)}function i(t){v.push(t),h&&(g=-g,b.data=g)}function a(){for(var t=0;t34||p<10)||window.opera||t.addEventListener("progress",function(t){e.lastSize=t.loaded},!1)}),f.on("open-xhr-start",function(t){this.params={method:t[0]},i(this,t[1]),this.metrics={}}),f.on("open-xhr-end",function(t,e){"loader_config"in NREUM&&"xpid"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader("X-NewRelic-ID",NREUM.loader_config.xpid)}),f.on("send-xhr-start",function(t,e){var n=this.metrics,r=t[0],o=this;if(n&&r){var i=h(r);i&&(n.txSize=i)}this.startTime=(new Date).getTime(),this.listener=function(t){try{"abort"===t.type&&(o.params.aborted=!0),("load"!==t.type||o.called===o.totalCbs&&(o.onloadCalled||"function"!=typeof e.onload))&&o.end(e)}catch(n){try{f.emit("internal-error",[n])}catch(r){}}};for(var a=0;a",applicationID:"<%= IdentityConfig.store.newrelic_browser_app_id %>",sa:1}
-<% end %>
diff --git a/config/application.yml.default b/config/application.yml.default
index 63d9d0941d4..4f23277751d 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -199,6 +199,7 @@ max_mail_events_window_in_days: 30
max_phone_numbers_per_account: 5
max_piv_cac_per_account: 2
min_password_score: 3
+minimum_wait_before_another_usps_letter_in_hours: 24
multi_region_kms_migration_jobs_enabled: true
mx_timeout: 3
otp_delivery_blocklist_maxretry: 10
@@ -376,8 +377,6 @@ development:
logins_per_ip_limit: 5
logo_upload_enabled: true
max_bad_passwords: 5
- newrelic_browser_app_id: ''
- newrelic_browser_key: ''
newrelic_license_key: ''
nonessential_email_banlist: '["banned_email@gmail.com"]'
otp_delivery_blocklist_findtime: 5
@@ -447,8 +446,6 @@ production:
logins_per_ip_limit: 20
logins_per_ip_period: 20
logins_per_ip_track_only_mode: true
- newrelic_browser_app_id: ''
- newrelic_browser_key: ''
newrelic_license_key: ''
nonessential_email_banlist: '[]'
otp_delivery_blocklist_findtime: 5
@@ -519,8 +516,6 @@ test:
max_bad_passwords: 5
max_mail_events: 2
otp_min_attempts_remaining_warning_count: 1
- newrelic_browser_app_id: ''
- newrelic_browser_key: ''
newrelic_license_key: ''
nonessential_email_banlist: '["banned_email@gmail.com"]'
otp_delivery_blocklist_findtime: 1
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 0a09da1bec6..4758b264cb3 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -2,7 +2,7 @@
# rubocop:disable Metrics/BlockLength
Rails.application.config.content_security_policy do |policy|
- connect_src = ["'self'", '*.nr-data.net']
+ connect_src = ["'self'"]
font_src = [:self, :data, IdentityConfig.store.asset_host.presence].compact
@@ -15,14 +15,8 @@
"https://s3.#{IdentityConfig.store.aws_region}.amazonaws.com",
].select(&:present?)
- script_src = [
- :self,
- 'js-agent.newrelic.com',
- '*.nr-data.net',
- IdentityConfig.store.asset_host.presence,
- ].compact
-
- script_src = [:self, :unsafe_eval] if !Rails.env.production?
+ script_src = [:self, IdentityConfig.store.asset_host.presence].compact
+ script_src << :unsafe_eval if !Rails.env.production?
style_src = [:self, IdentityConfig.store.asset_host.presence].compact
diff --git a/config/newrelic.yml b/config/newrelic.yml
index 08269ea96e0..9e2ab1f3b30 100644
--- a/config/newrelic.yml
+++ b/config/newrelic.yml
@@ -21,7 +21,7 @@ production:
error_collector:
enabled: true
capture_source: true
- ignore_errors: "<%= %w[
+ ignore_classes: "<%= %w[
ActionController::BadRequest
ActionController::ParameterMissing
ActionController::RoutingError
diff --git a/config/routes.rb b/config/routes.rb
index ad4ff4c4ed3..6b4fdebabde 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -388,28 +388,27 @@
get '/in_person/:step' => 'in_person#show', as: :in_person_step
put '/in_person/:step' => 'in_person#update'
- get '/by_mail' => 'by_mail/enter_code#index', as: :verify_by_mail_enter_code
- post '/by_mail' => 'by_mail/enter_code#create'
+ get '/by_mail/enter_code' => 'by_mail/enter_code#index', as: :verify_by_mail_enter_code
+ post '/by_mail/enter_code' => 'by_mail/enter_code#create'
get '/by_mail/confirm_start_over' => 'confirm_start_over#index',
as: :confirm_start_over
if FeatureManagement.gpo_verification_enabled?
- get '/usps' => 'by_mail/request_letter#index', as: :request_letter
- put '/usps' => 'by_mail/request_letter#create'
-
- # These will be made the new "official" routes in a future commit
- get '/by_mail/request_letter' => 'by_mail/request_letter#index'
+ get '/by_mail/request_letter' => 'by_mail/request_letter#index', as: :request_letter
put '/by_mail/request_letter' => 'by_mail/request_letter#create'
+
+ # Temporary routes + redirects supporting GPO route renaming
+ get '/usps' => redirect('/verify/by_mail/request_letter')
+ put '/usps' => 'by_mail/request_letter#create'
end
- get '/come_back_later' => 'by_mail/letter_enqueued#show', as: :letter_enqueued
+ get '/by_mail/letter_enqueued' => 'by_mail/letter_enqueued#show', as: :letter_enqueued
- # BEGIN temporary routes in preparation for renaming the GPO routes
- # These will allow old instances to serve requests for new routes during the 50/50
- # state when new routes are deployed.
- get '/by_mail/letter_enqueued' => 'by_mail/letter_enqueued#show'
- get '/by_mail/enter_code' => 'by_mail/enter_code#index'
- post '/by_mail/enter_code' => 'by_mail/enter_code#create'
+ # BEGIN temporary routes & redirects supporting GPO route renaming
+ get '/come_back_later' => redirect('/verify/by_mail/letter_enqueued')
+
+ get '/by_mail' => redirect('/verify/by_mail/enter_code')
+ post '/by_mail' => 'by_mail/enter_code#create'
# END temporary routes
end
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index ab4bfce0cc9..ceed0812342 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -307,10 +307,9 @@ def self.build_store(config_map)
config.add(:max_phone_numbers_per_account, type: :integer)
config.add(:max_piv_cac_per_account, type: :integer)
config.add(:min_password_score, type: :integer)
+ config.add(:minimum_wait_before_another_usps_letter_in_hours, type: :integer)
config.add(:multi_region_kms_migration_jobs_enabled, type: :boolean)
config.add(:mx_timeout, type: :integer)
- config.add(:newrelic_browser_app_id, type: :string)
- config.add(:newrelic_browser_key, type: :string)
config.add(:newrelic_license_key, type: :string)
config.add(:nonessential_email_banlist, type: :json)
config.add(:otp_delivery_blocklist_findtime, type: :integer)
diff --git a/lib/reporting/identity_verification_report.rb b/lib/reporting/identity_verification_report.rb
index 16c9f5963a8..b7d7d93d16d 100644
--- a/lib/reporting/identity_verification_report.rb
+++ b/lib/reporting/identity_verification_report.rb
@@ -18,8 +18,11 @@ class IdentityVerificationReport
module Events
IDV_DOC_AUTH_WELCOME = 'IdV: doc auth welcome visited'
+ IDV_DOC_AUTH_WELCOME_SUBMITTED = 'IdV: doc auth welcome submitted'
IDV_DOC_AUTH_GETTING_STARTED = 'IdV: doc auth getting_started visited'
IDV_DOC_AUTH_IMAGE_UPLOAD = 'IdV: doc auth image upload vendor submitted'
+ IDV_DOC_AUTH_VERIFY_RESULTS = 'IdV: doc auth verify proofing results'
+ IDV_PHONE_FINDER_RESULTS = 'IdV: phone confirmation vendor'
IDV_FINAL_RESOLUTION = 'IdV: final resolution'
GPO_VERIFICATION_SUBMITTED = 'IdV: enter verify by mail code submitted'
GPO_VERIFICATION_SUBMITTED_OLD = 'IdV: GPO verification submitted'
@@ -35,6 +38,10 @@ module Results
IDV_FINAL_RESOLUTION_FRAUD_REVIEW = 'IdV: final resolution - Fraud Review Pending'
IDV_FINAL_RESOLUTION_GPO = 'IdV: final resolution - GPO Pending'
IDV_FINAL_RESOLUTION_IN_PERSON = 'IdV: final resolution - In Person Proofing'
+
+ IDV_REJECT_DOC_AUTH = 'IdV Reject: Doc Auth'
+ IDV_REJECT_VERIFY = 'IdV Reject: Verify'
+ IDV_REJECT_PHONE_FINDER = 'IdV Reject: Phone Finder'
end
# @param [Array] issuers
@@ -45,7 +52,8 @@ def initialize(
verbose: false,
progress: false,
slice: 3.hours,
- threads: 5
+ threads: 5,
+ data: nil
)
@issuers = issuers
@time_range = time_range
@@ -53,6 +61,7 @@ def initialize(
@progress = progress
@slice = slice
@threads = threads
+ @data = data
end
def verbose?
@@ -72,6 +81,7 @@ def to_csv
csv << ['Metric', '# of Users']
csv << []
csv << ['Started IdV Verification', idv_started]
+ csv << ['Submitted welcome page', idv_doc_auth_welcome_submitted]
csv << ['Images uploaded', idv_doc_auth_image_vendor_submitted]
csv << []
csv << ['Workflow completed', idv_final_resolution]
@@ -88,39 +98,54 @@ def to_csv
end
end
+ # @param [Reporting::IdentityVerificationReport] other
+ # @return [Reporting::IdentityVerificationReport]
+ def merge(other)
+ self.class.new(
+ issuers: (Array(issuers) + Array(other.issuers)).uniq,
+ time_range: Range.new(
+ [time_range.begin, other.time_range.begin].min,
+ [time_range.end, other.time_range.end].max,
+ ),
+ data: data.merge(other.data) do |_event, old_user_ids, new_user_ids|
+ old_user_ids + new_user_ids
+ end,
+ )
+ end
+
def idv_final_resolution
- data[Events::IDV_FINAL_RESOLUTION].to_i
+ data[Events::IDV_FINAL_RESOLUTION].count
end
def idv_final_resolution_verified
- data[Results::IDV_FINAL_RESOLUTION_VERIFIED].to_i
+ data[Results::IDV_FINAL_RESOLUTION_VERIFIED].count
end
def idv_final_resolution_gpo
- data[Results::IDV_FINAL_RESOLUTION_GPO].to_i
+ data[Results::IDV_FINAL_RESOLUTION_GPO].count
end
def idv_final_resolution_in_person
- data[Results::IDV_FINAL_RESOLUTION_IN_PERSON].to_i
+ data[Results::IDV_FINAL_RESOLUTION_IN_PERSON].count
end
def idv_final_resolution_fraud_review
- data[Results::IDV_FINAL_RESOLUTION_FRAUD_REVIEW].to_i
+ data[Results::IDV_FINAL_RESOLUTION_FRAUD_REVIEW].count
end
def idv_final_resolution_total_pending
- idv_final_resolution - idv_final_resolution_verified
+ @idv_final_resolution_total_pending ||=
+ (data[Events::IDV_FINAL_RESOLUTION] - data[Results::IDV_FINAL_RESOLUTION_VERIFIED]).count
end
def gpo_verification_submitted
- [
- data[Events::GPO_VERIFICATION_SUBMITTED].to_i,
- data[Events::GPO_VERIFICATION_SUBMITTED_OLD].to_i,
- ].sum
+ @gpo_verification_submitted ||= (
+ data[Events::GPO_VERIFICATION_SUBMITTED] +
+ data[Events::GPO_VERIFICATION_SUBMITTED_OLD]).count
end
def usps_enrollment_status_updated
- data[Events::USPS_ENROLLMENT_STATUS_UPDATED].to_i
+ data[Events::USPS_ENROLLMENT_STATUS_UPDATED].count
end
def successfully_verified_users
@@ -128,16 +153,29 @@ def successfully_verified_users
end
def idv_started
- [
- data[Events::IDV_DOC_AUTH_WELCOME].to_i,
- data[Events::IDV_DOC_AUTH_GETTING_STARTED].to_i,
- ].sum
+ @idv_started ||=
+ (data[Events::IDV_DOC_AUTH_WELCOME] + data[Events::IDV_DOC_AUTH_GETTING_STARTED]).count
end
def idv_doc_auth_image_vendor_submitted
- data[Events::IDV_DOC_AUTH_IMAGE_UPLOAD].to_i
+ data[Events::IDV_DOC_AUTH_IMAGE_UPLOAD].count
end
+ def idv_doc_auth_welcome_submitted
+ data[Events::IDV_DOC_AUTH_WELCOME_SUBMITTED].count
+ end
+
+ def idv_doc_auth_rejected
+ @idv_doc_auth_rejected ||= (
+ data[Results::IDV_REJECT_DOC_AUTH] +
+ data[Results::IDV_REJECT_VERIFY] +
+ data[Results::IDV_REJECT_PHONE_FINDER] -
+ data[Results::IDV_FINAL_RESOLUTION_VERIFIED] -
+ data[Results::IDV_FINAL_RESOLUTION_IN_PERSON]
+ ).count
+ end
+
+ # rubocop:disable Layout/LineLength
# Turns query results into a hash keyed by event name, values are a count of unique users
# for that event
# @return [Hash>]
@@ -150,27 +188,31 @@ def data
# IDEA: maybe there's a block form if this we can do that yields results as it loads them
# to go slightly faster
fetch_results.each do |row|
- event_users[row['name']] << row['user_id']
-
- if row['name'] == Events::IDV_FINAL_RESOLUTION
- if row['identity_verified'] == '1'
- event_users[Results::IDV_FINAL_RESOLUTION_VERIFIED] << row['user_id']
- end
- if row['gpo_verification_pending'] == '1'
- event_users[Results::IDV_FINAL_RESOLUTION_GPO] << row['user_id']
- end
- if row['in_person_verification_pending'] == '1'
- event_users[Results::IDV_FINAL_RESOLUTION_IN_PERSON] << row['user_id']
- end
- if row['fraud_review_pending'] == '1'
- event_users[Results::IDV_FINAL_RESOLUTION_FRAUD_REVIEW] << row['user_id']
- end
+ event = row['name']
+ user_id = row['user_id']
+ success = row['success']
+
+ event_users[event] << user_id
+
+ case event
+ when Events::IDV_FINAL_RESOLUTION
+ event_users[Results::IDV_FINAL_RESOLUTION_VERIFIED] << user_id if row['identity_verified'] == '1'
+ event_users[Results::IDV_FINAL_RESOLUTION_GPO] << user_id if row['gpo_verification_pending'] == '1'
+ event_users[Results::IDV_FINAL_RESOLUTION_IN_PERSON] << user_id if row['in_person_verification_pending'] == '1'
+ event_users[Results::IDV_FINAL_RESOLUTION_FRAUD_REVIEW] << user_id if row['fraud_review_pending'] == '1'
+ when Events::IDV_DOC_AUTH_IMAGE_UPLOAD
+ event_users[Results::IDV_REJECT_DOC_AUTH] << user_id if row['doc_auth_failed_non_fraud'] == '1'
+ when Events::IDV_DOC_AUTH_VERIFY_RESULTS
+ event_users[Results::IDV_REJECT_VERIFY] << user_id if success == '0'
+ when Events::IDV_PHONE_FINDER_RESULTS
+ event_users[Results::IDV_REJECT_PHONE_FINDER] << user_id if success == '0'
end
end
- event_users.transform_values(&:count)
+ event_users
end
end
+ # rubocop:enable Layout/LineLength
def fetch_results
cloudwatch_client.fetch(query:, from: time_range.begin, to: time_range.end)
@@ -194,6 +236,7 @@ def query
fields
name
, properties.user_id AS user_id
+ , coalesce(properties.event_properties.success, 0) AS success
#{issuers.present? ? '| filter properties.service_provider IN %{issuers}' : ''}
| filter name in %{event_names}
| filter (name = %{usps_enrollment_status_updated} and properties.event_properties.passed = 1)
@@ -205,6 +248,7 @@ def query
, coalesce(properties.event_properties.gpo_verification_pending, 0) AS gpo_verification_pending
, coalesce(properties.event_properties.in_person_verification_pending, 0) AS in_person_verification_pending
, ispresent(properties.event_properties.deactivation_reason) AS has_other_deactivation_reason
+ , properties.event_properties.success = '0' AND properties.event_properties.doc_auth_result NOT IN ['Failed', 'Attention'] AS doc_auth_failed_non_fraud
| fields
!fraud_review_pending and !gpo_verification_pending and !in_person_verification_pending and !has_other_deactivation_reason AS identity_verified
| limit 10000
diff --git a/lib/reporting/proofing_rate_report.rb b/lib/reporting/proofing_rate_report.rb
new file mode 100644
index 00000000000..cad7b303c04
--- /dev/null
+++ b/lib/reporting/proofing_rate_report.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'csv'
+require 'reporting/identity_verification_report'
+require 'reporting/unknown_progress_bar'
+
+module Reporting
+ class ProofingRateReport
+ DATE_INTERVALS = [30, 60, 90].freeze
+
+ attr_reader :end_date
+
+ def initialize(end_date:, verbose: false, progress: false)
+ @end_date = end_date.in_time_zone('UTC')
+ @verbose = verbose
+ @progress = progress
+ end
+
+ def verbose?
+ @verbose
+ end
+
+ def progress?
+ @progress
+ end
+
+ # rubocop:disable Layout/LineLength
+ def as_csv
+ csv = []
+
+ csv << ['Metric', *DATE_INTERVALS.map { |days| "Trailing #{days}d" }]
+
+ csv << ['Start Date', *reports.map(&:time_range).map(&:begin)]
+ csv << ['End Date', *reports.map(&:time_range).map(&:end)]
+
+ csv << ['IDV Started', *reports.map(&:idv_started)]
+ csv << ['Welcome Submitted', *reports.map(&:idv_doc_auth_welcome_submitted)]
+ csv << ['Image Submitted', *reports.map(&:idv_doc_auth_image_vendor_submitted)]
+ csv << ['Successfully Verified', *reports.map(&:successfully_verified_users)]
+ csv << ['IDV Rejected', *reports.map(&:idv_doc_auth_rejected)]
+
+ csv << ['Blanket Proofing Rate (IDV Started to Successfully Verified)', *blanket_proofing_rates(reports)]
+ csv << ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', *intent_proofing_rates(reports)]
+ csv << ['Actual Proofing Rate (Image Submitted to Successfully Verified)', *actual_proofing_rates(reports)]
+ csv << ['Industry Proofing Rate (Verified minus IDV Rejected)', *industry_proofing_rates(reports)]
+
+ csv
+ end
+ # rubocop:enable Layout/LineLength
+
+ def to_csv
+ CSV.generate do |csv|
+ as_csv.each do |row|
+ csv << row
+ end
+ end
+ end
+
+ def reports
+ @reports ||= begin
+ threads = [0, *DATE_INTERVALS].each_cons(2).map do |slice_end, slice_start|
+ Thread.new do
+ Reporting::IdentityVerificationReport.new(
+ issuers: nil, # all issuers
+ time_range: Range.new(
+ (end_date - slice_start.days).beginning_of_day,
+ (end_date - slice_end.days).beginning_of_day,
+ ),
+ progress: false,
+ verbose: verbose?,
+ ).tap(&:data)
+ end
+ end
+
+ reports = Reporting::UnknownProgressBar.wrap(show_bar: progress?) do
+ threads.map(&:value)
+ end
+
+ reports.reduce([]) do |acc, report|
+ if acc.empty?
+ acc << report
+ else
+ acc << report.merge(acc.last)
+ end
+ end
+ end
+ end
+
+ # @param [Array] reports
+ # @return [Array]
+ def blanket_proofing_rates(reports)
+ reports.map do |report|
+ report.successfully_verified_users.to_f / report.idv_started
+ end
+ end
+
+ # @param [Array] reports
+ # @return [Array]
+ def intent_proofing_rates(reports)
+ reports.map do |report|
+ report.successfully_verified_users.to_f / report.idv_doc_auth_welcome_submitted
+ end
+ end
+
+ # @param [Array] reports
+ # @return [Array]
+ def actual_proofing_rates(reports)
+ reports.map do |report|
+ report.successfully_verified_users.to_f / report.idv_doc_auth_image_vendor_submitted
+ end
+ end
+
+ # @param [Array] reports
+ # @return [Array]
+ def industry_proofing_rates(reports)
+ reports.map do |report|
+ report.successfully_verified_users.to_f / (
+ report.successfully_verified_users + report.idv_doc_auth_rejected
+ )
+ end
+ end
+ end
+end
+
+# rubocop:disable Rails/Output
+if __FILE__ == $PROGRAM_NAME
+ end_date = if ARGV.first.match?(/\d{4}-\d{1,2}-\d{1,2}/)
+ Date.parse(ARGV.first)
+ else
+ ActiveSupport::TimeZone['UTC'].today
+ end
+ progress = !ARGV.include?('--no-progress')
+ verbose = ARGV.include?('--verbose')
+
+ puts Reporting::ProofingRateReport.new(
+ end_date: end_date,
+ progress: progress,
+ verbose: verbose,
+ ).to_csv
+end
+# rubocop:enable Rails/Output
diff --git a/package.json b/package.json
index d19219ed7f2..c4bc066c474 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"zxcvbn": "4.4.2"
},
"devDependencies": {
+ "@babel/cli": "^7.22.15",
"@testing-library/dom": "^8.18.1",
"@testing-library/react": "^11.2.2",
"@testing-library/react-hooks": "^3.7.0",
@@ -55,7 +56,6 @@
"@types/grecaptcha": "^3.0.4",
"@types/intl-tel-input": "^17.0.6",
"@types/mocha": "^10.0.0",
- "@types/newrelic": "^7.0.3",
"@types/node": "^20.2.5",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
diff --git a/spec/controllers/concerns/rate_limit_concern_spec.rb b/spec/controllers/concerns/rate_limit_concern_spec.rb
index 25bc2ee6482..f8e3b8b29b7 100644
--- a/spec/controllers/concerns/rate_limit_concern_spec.rb
+++ b/spec/controllers/concerns/rate_limit_concern_spec.rb
@@ -89,16 +89,6 @@ def update
).increment_to_limited!
end
- context 'ssn is in flow session' do
- it 'redirects to proof_ssn rate limited error page' do
- flow_session = { pii_from_doc: { ssn: ssn } }
- allow(subject).to receive(:flow_session).and_return(flow_session)
- get :show
-
- expect(response).to redirect_to idv_session_errors_ssn_failure_url
- end
- end
-
context 'ssn is in idv_session' do
it 'redirects to proof_ssn rate limited error page' do
subject.idv_session.ssn = ssn
diff --git a/spec/controllers/frontend_log_controller_spec.rb b/spec/controllers/frontend_log_controller_spec.rb
index 8eda1f642f5..7a983b7265d 100644
--- a/spec/controllers/frontend_log_controller_spec.rb
+++ b/spec/controllers/frontend_log_controller_spec.rb
@@ -170,6 +170,39 @@
expect(json[:success]).to eq(true)
end
end
+
+ context 'for an error event' do
+ let(:params) do
+ {
+ 'event' => 'Frontend Error',
+ 'payload' => {
+ 'name' => 'name',
+ 'message' => 'message',
+ 'stack' => 'stack',
+ },
+ }
+ end
+
+ it 'notices the error to NewRelic instead of analytics logger' do
+ expect(fake_analytics).not_to receive(:track_event)
+ expect(NewRelic::Agent).to receive(:notice_error).with(
+ FrontendErrorLogger::FrontendError.new,
+ custom_params: {
+ frontend_error: {
+ name: 'name',
+ message: 'message',
+ stack: 'stack',
+ },
+ },
+ expected: true,
+ )
+
+ action
+
+ expect(response).to have_http_status(:ok)
+ expect(json[:success]).to eq(true)
+ end
+ end
end
context 'anonymous user with session-associated user id' do
diff --git a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb
index bc3450d6afd..89e0af9e1c4 100644
--- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb
+++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb
@@ -30,10 +30,14 @@
if user
stub_sign_in(user)
pending_user = stub_user_with_pending_profile(user)
+ creation_time =
+ (IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours + 1).hours.ago
create(
:gpo_confirmation_code,
profile: pending_profile,
otp_fingerprint: Pii::Fingerprinter.fingerprint(otp),
+ created_at: creation_time,
+ updated_at: creation_time,
)
allow(pending_user).to receive(:gpo_verification_pending_profile?).
and_return(has_pending_profile)
diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb
index a5ad306f80b..1bf95e7713f 100644
--- a/spec/controllers/idv/in_person/ssn_controller_spec.rb
+++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb
@@ -81,31 +81,6 @@
expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil)
end
- context 'with an ssn in flow_session' do
- let(:referer) { idv_in_person_step_url(step: :address) }
- before do
- flow_session[:pii_from_user][:ssn] = ssn
- request.env['HTTP_REFERER'] = referer
- end
-
- context 'referer is not verify_info' do
- it 'redirects to verify_info' do
- get :show
-
- expect(response).to redirect_to(idv_in_person_verify_info_url)
- end
- end
-
- context 'referer is verify_info' do
- let(:referer) { idv_in_person_verify_info_url }
- it 'does not redirect' do
- get :show
-
- expect(response).to render_template :show
- end
- end
- end
-
context 'with an ssn in idv_session' do
let(:referer) { idv_in_person_step_url(step: :address) }
before do
@@ -163,12 +138,6 @@
put :update, params: params
end
- it 'merges ssn into pii session value' do
- put :update, params: params
-
- expect(flow_session[:pii_from_user][:ssn]).to eq(ssn)
- end
-
it 'adds ssn to idv_session' do
put :update, params: params
@@ -190,7 +159,7 @@
end
it 'does not change threatmetrix_session_id when updating ssn' do
- flow_session[:pii_from_user][:ssn] = ssn
+ subject.idv_session.ssn = ssn
put :update, params: params
session_id = subject.idv_session.threatmetrix_session_id
subject.threatmetrix_view_variables
diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb
index 3cd9a7773dd..eeaca608221 100644
--- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb
@@ -20,6 +20,7 @@
before do
allow(subject).to receive(:flow_session).and_return(flow_session)
stub_sign_in(user)
+ subject.idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID[:ssn]
allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args)
end
diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb
index 82c9da477c2..95e4e9da7e6 100644
--- a/spec/controllers/idv/session_errors_controller_spec.rb
+++ b/spec/controllers/idv/session_errors_controller_spec.rb
@@ -160,7 +160,7 @@
let(:user) { create(:user) }
before do
- RateLimiter.new(rate_limit_type: :proof_address, user: user).increment!
+ RateLimiter.new(rate_limit_type: :idv_resolution, user: user).increment!
end
it 'assigns remaining count' do
@@ -180,7 +180,7 @@
'IdV: session error visited',
hash_including(
type: action.to_s,
- attempts_remaining: 5,
+ attempts_remaining: IdentityConfig.store.idv_max_attempts - 1,
),
)
response
@@ -236,7 +236,7 @@
let(:user) { create(:user) }
before do
- RateLimiter.new(rate_limit_type: :proof_address, user: user).increment_to_limited!
+ RateLimiter.new(rate_limit_type: :idv_resolution, user: user).increment_to_limited!
end
it 'assigns expiration time' do
@@ -258,7 +258,7 @@
'IdV: session error visited',
hash_including(
type: action.to_s,
- attempts_remaining: 5,
+ attempts_remaining: 0,
),
)
get action
@@ -285,7 +285,7 @@
rate_limit_type: :proof_ssn,
target: Pii::Fingerprinter.fingerprint(ssn),
).increment_to_limited!
- controller.user_session['idv/doc_auth'] = { pii_from_doc: { ssn: ssn } }
+ allow(idv_session).to receive(:ssn).and_return(ssn)
end
it 'assigns expiration time' do
@@ -304,30 +304,6 @@
)
get action
end
-
- context 'with ssn in idv_session' do
- before do
- controller.user_session['idv/doc_auth'] = {}
- allow(idv_session).to receive(:ssn).and_return(ssn)
- end
-
- it 'assigns expiration time' do
- get action
-
- expect(assigns(:expires_at)).not_to eq(Time.zone.now)
- end
-
- it 'logs an event with attempts remaining' do
- expect(@analytics).to receive(:track_event).with(
- 'IdV: session error visited',
- hash_including(
- type: 'ssn_failure',
- attempts_remaining: 0,
- ),
- )
- get action
- end
- end
end
end
diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb
index e64e04f9f0c..d16a33c2bcd 100644
--- a/spec/controllers/idv/ssn_controller_spec.rb
+++ b/spec/controllers/idv/ssn_controller_spec.rb
@@ -90,31 +90,6 @@
expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil)
end
- context 'with an ssn in flow_session' do
- let(:referer) { idv_document_capture_url }
- before do
- flow_session[:pii_from_doc][:ssn] = ssn
- request.env['HTTP_REFERER'] = referer
- end
-
- context 'referer is not verify_info' do
- it 'redirects to verify_info' do
- get :show
-
- expect(response).to redirect_to(idv_verify_info_url)
- end
- end
-
- context 'referer is verify_info' do
- let(:referer) { idv_verify_info_url }
- it 'does not redirect' do
- get :show
-
- expect(response).to render_template :show
- end
- end
- end
-
context 'with an ssn in idv_session' do
let(:referer) { idv_document_capture_url }
before do
@@ -178,12 +153,6 @@
}.merge(ab_test_args)
end
- it 'merges ssn into pii session value' do
- put :update, params: params
-
- expect(flow_session[:pii_from_doc][:ssn]).to eq(ssn)
- end
-
it 'updates idv_session.ssn' do
expect { put :update, params: params }.to change { subject.idv_session.ssn }.
from(nil).to(ssn)
@@ -199,7 +168,7 @@
end
it 'redirects to the verify info controller if a user is updating their SSN' do
- flow_session[:pii_from_doc][:ssn] = ssn
+ subject.idv_session.ssn = ssn
flow_session[:pii_from_doc][:state] = 'PR'
put :update, params: params
@@ -226,7 +195,7 @@
end
it 'does not change threatmetrix_session_id when updating ssn' do
- flow_session[:pii_from_doc][:ssn] = ssn
+ subject.idv_session.ssn = ssn
put :update, params: params
session_id = subject.idv_session.threatmetrix_session_id
subject.threatmetrix_view_variables
diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb
index 8e52b8a21a1..97cd1a4f177 100644
--- a/spec/controllers/idv/verify_info_controller_spec.rb
+++ b/spec/controllers/idv/verify_info_controller_spec.rb
@@ -4,7 +4,7 @@
include IdvHelper
let(:flow_session) do
- { pii_from_doc: Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.dup }
+ { pii_from_doc: Idp::Constants::MOCK_IDV_APPLICANT.dup }
end
let(:user) { create(:user) }
@@ -26,6 +26,7 @@
stub_attempts_tracker
stub_idv_steps_before_verify_step(user)
subject.idv_session.flow_path = 'standard'
+ subject.idv_session.ssn = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn]
subject.user_session['idv/doc_auth'] = flow_session
allow(subject).to receive(:ab_test_analytics_buckets).and_return(ab_test_args)
end
@@ -64,7 +65,7 @@
}.merge(ab_test_args)
end
- it 'renders the show template (with ssn in flow_session)' do
+ it 'renders the show template' do
get :show
expect(response).to render_template :show
@@ -114,7 +115,6 @@
end
it 'redirects to ssn controller when ssn info is missing' do
- flow_session[:pii_from_doc][:ssn] = nil
subject.idv_session.ssn = nil
get :show
@@ -122,15 +122,6 @@
expect(response).to redirect_to(idv_ssn_url)
end
- it 'renders show when ssn is in idv_session' do
- subject.idv_session.ssn = flow_session[:pii_from_doc][:ssn]
- flow_session[:pii_from_doc][:ssn] = nil
-
- get :show
-
- expect(response).to render_template :show
- end
-
context 'when the user is ssn rate limited' do
before do
RateLimiter.new(
@@ -141,16 +132,7 @@
).increment_to_limited!
end
- it 'redirects to ssn failure url with ssn in flow session' do
- get :show
-
- expect(response).to redirect_to idv_session_errors_ssn_failure_url
- end
-
- it 'redirects to ssn failure url with ssn in idv_session' do
- subject.idv_session.ssn = flow_session[:pii_from_doc][:ssn]
- flow_session[:pii_from_doc][:ssn] = nil
-
+ it 'redirects to ssn failure url' do
get :show
expect(response).to redirect_to idv_session_errors_ssn_failure_url
diff --git a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
index d3275c56731..cdf70de6e9a 100644
--- a/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
+++ b/spec/controllers/two_factor_authentication/webauthn_verification_controller_spec.rb
@@ -73,11 +73,14 @@
end
context 'with multiple webauthn configured' do
- let!(:webauthn_platform_configuration) do
- create(:webauthn_configuration, :platform_authenticator, user:)
+ let!(:first_webauthn_platform_configuration) do
+ create(:webauthn_configuration, :platform_authenticator, user:, created_at: 2.days.ago)
+ end
+ let!(:second_webauthn_platform_configuration) do
+ create(:webauthn_configuration, :platform_authenticator, user:, created_at: 1.day.ago)
end
- it 'filters credentials based on requested authenticator attachment' do
+ it 'filters credentials based on requested attachment, sorted descending by date' do
get :show
expect(assigns(:presenter).credentials).to eq(
@@ -91,8 +94,14 @@
expect(assigns(:presenter).credentials).to eq(
[
- id: webauthn_platform_configuration.credential_id,
- transports: webauthn_platform_configuration.transports,
+ {
+ id: second_webauthn_platform_configuration.credential_id,
+ transports: second_webauthn_platform_configuration.transports,
+ },
+ {
+ id: first_webauthn_platform_configuration.credential_id,
+ transports: first_webauthn_platform_configuration.transports,
+ },
],
)
end
@@ -209,10 +218,10 @@
let(:webauthn_error) { 'NotAllowedError' }
let(:params) do
{
- authenticator_data: authenticator_data,
- client_data_json: verification_client_data_json,
- signature: signature,
- credential_id: credential_id,
+ authenticator_data: '',
+ client_data_json: '',
+ signature: '',
+ credential_id: '',
platform: true,
webauthn_error: webauthn_error,
}
@@ -223,17 +232,17 @@
end
let(:view_context) { ActionController::Base.new.view_context }
+ let!(:first_webauthn_platform_configuration) do
+ create(:webauthn_configuration, :platform_authenticator, user:, created_at: 2.days.ago)
+ end
+ let!(:second_webauthn_platform_configuration) do
+ create(:webauthn_configuration, :platform_authenticator, user:, created_at: 1.day.ago)
+ end
+
before do
allow_any_instance_of(TwoFactorAuthCode::WebauthnAuthenticationPresenter).
to receive(:multiple_factors_enabled?).
and_return(true)
- create(
- :webauthn_configuration,
- user: controller.current_user,
- credential_id: credential_id,
- credential_public_key: credential_public_key,
- platform_authenticator: true,
- )
end
it 'redirects to webauthn show page' do
@@ -256,13 +265,19 @@
it 'logs an event with error details' do
expect(@analytics).to receive(:track_mfa_submit_event).with(
- hash_including(
- success: false,
- error_details: { webauthn_error: [webauthn_error] },
- context: UserSessionContext::AUTHENTICATION_CONTEXT,
- multi_factor_auth_method: 'webauthn_platform',
- webauthn_configuration_id: controller.current_user.webauthn_configurations.first.id,
- ),
+ success: false,
+ error_details: {
+ authenticator_data: [:blank],
+ client_data_json: [:blank],
+ signature: [:blank],
+ webauthn_configuration: [:blank],
+ webauthn_error: [webauthn_error],
+ },
+ context: UserSessionContext::AUTHENTICATION_CONTEXT,
+ multi_factor_auth_method: 'webauthn_platform',
+ multi_factor_auth_method_created_at:
+ second_webauthn_platform_configuration.created_at.strftime('%s%L'),
+ webauthn_configuration_id: nil,
)
patch :confirm, params: params
diff --git a/spec/controllers/users/delete_controller_spec.rb b/spec/controllers/users/delete_controller_spec.rb
index 8cab2b1ffd5..304e10b611d 100644
--- a/spec/controllers/users/delete_controller_spec.rb
+++ b/spec/controllers/users/delete_controller_spec.rb
@@ -1,6 +1,16 @@
require 'rails_helper'
RSpec.describe Users::DeleteController do
+ describe 'before_actions' do
+ it 'includes authentication before_action' do
+ expect(subject).to have_actions(
+ :before,
+ :confirm_two_factor_authenticated,
+ :confirm_recently_authenticated_2fa,
+ )
+ end
+ end
+
describe '#show' do
it 'shows and logs a visit' do
stub_analytics
diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb
index 9eba17c66e0..4c88b222624 100644
--- a/spec/features/idv/end_to_end_idv_spec.rb
+++ b/spec/features/idv/end_to_end_idv_spec.rb
@@ -38,6 +38,7 @@
validate_phone_page
try_to_skip_ahead_from_phone
+ visit_by_mail_and_return
complete_otp_verification_page(user)
validate_review_page
@@ -315,6 +316,12 @@ def try_to_skip_ahead_from_phone
expect(page).to have_current_path(idv_phone_path)
end
+ def visit_by_mail_and_return
+ enter_gpo_flow
+ click_doc_auth_back_link
+ expect(page).to have_current_path(idv_phone_path)
+ end
+
def try_to_go_back_from_document_capture
visit(idv_agreement_path)
expect(page).to have_current_path(idv_document_capture_path)
diff --git a/spec/features/idv/steps/gpo_step_spec.rb b/spec/features/idv/steps/gpo_step_spec.rb
index 68e05f6c909..48e80fc2b55 100644
--- a/spec/features/idv/steps/gpo_step_spec.rb
+++ b/spec/features/idv/steps/gpo_step_spec.rb
@@ -4,6 +4,17 @@
include IdvStepHelper
include OidcAuthHelper
+ let(:minimum_wait_for_letter) { 24 }
+ let(:days_passed) { max_days_before_resend_disabled + 1 }
+ let(:max_days_before_resend_disabled) { 30 }
+
+ before do
+ allow(IdentityConfig.store).to receive(:minimum_wait_before_another_usps_letter_in_hours).
+ and_return(minimum_wait_for_letter)
+ allow(IdentityConfig.store).to receive(:gpo_max_profile_age_to_send_letter_in_days).
+ and_return(max_days_before_resend_disabled)
+ end
+
it 'redirects to and completes the review step when the user chooses to verify by letter', :js do
start_idv_from_sp
complete_idv_steps_before_gpo_step
@@ -16,51 +27,74 @@
expect(page).to have_content(t('idv.messages.gpo.letter_on_the_way'))
end
- it 'allows the user to go back', :js do
- start_idv_from_sp
- complete_idv_steps_before_gpo_step
-
- click_doc_auth_back_link
-
- expect(page).to have_current_path(idv_phone_path)
- end
-
context 'the user has sent a letter but not verified an OTP' do
let(:user) { user_with_2fa }
- it 'allows the user to resend a letter and redirects to the come back later step', :js do
- complete_idv_and_return_to_gpo_step
-
- # Confirm that we show the correct content on
- # the GPO page for users requesting re-send
- expect(page).to have_content(t('idv.titles.mail.resend'))
- expect(page).to have_content(t('idv.messages.gpo.resend_timeframe'))
- expect(page).to have_content(t('idv.messages.gpo.resend_code_warning'))
- expect(page).to have_content(t('idv.buttons.mail.resend'))
- expect(page).to_not have_content(t('idv.messages.gpo.info_alert'))
-
- expect { click_on t('idv.buttons.mail.resend') }.
- to change { GpoConfirmation.count }.from(1).to(2)
- expect_user_to_be_unverified(user)
-
- expect(page).to have_content(t('idv.messages.gpo.another_letter_on_the_way'))
- expect(page).to have_content(t('idv.titles.come_back_later'))
- expect(page).to have_current_path(idv_letter_enqueued_path)
+ it 'if not rate limited, allow user to resend letter & redirect to letter enqueued step', :js do
+ complete_idv_by_mail_and_sign_out
- # Confirm that user cannot visit other IdV pages while unverified
- visit idv_agreement_path
- expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
- visit idv_ssn_url
- expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
- visit idv_verify_info_url
- expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
-
- # complete verification: end to end gpo test
- complete_gpo_verification(user)
+ # rate-limited because too little time has passed
+ sign_in_live_with_2fa(user)
+ confirm_rate_limited
+ sign_out
+
+ # still rate-limited because too little time has passed
+ travel_to((minimum_wait_for_letter - 1).hours.from_now) do
+ sign_in_live_with_2fa(user)
+ confirm_rate_limited
+ sign_out
+ end
- expect(user.identity_verified?).to be(true)
+ # will be rate-limted after expiration
+ travel_to(days_passed.days.from_now) do
+ sign_in_live_with_2fa(user)
+ confirm_rate_limited
+ sign_out
+ # Clear MFA SMS message from the future to allow re-logging in with test helper
+ Telephony::Test::Message.clear_messages
+ end
- expect(page).to_not have_content(t('account.index.verification.reactivate_button'))
+ # can re-request after the waiting period
+ travel_to((minimum_wait_for_letter + 1).hours.from_now) do
+ sign_in_live_with_2fa(user)
+ click_on t('idv.messages.gpo.resend')
+
+ # Confirm that we show the correct content on
+ # the GPO page for users requesting re-send
+ expect(page).to have_content(t('idv.titles.mail.resend'))
+ expect(page).to have_content(t('idv.messages.gpo.resend_timeframe'))
+ expect(page).to have_content(t('idv.messages.gpo.resend_code_warning'))
+ expect(page).to have_content(t('idv.buttons.mail.resend'))
+ expect(page).to_not have_content(t('idv.messages.gpo.info_alert'))
+
+ # Ensure user can go back from this page
+ click_doc_auth_back_link
+ expect(page).to have_content(t('idv.gpo.title'))
+ expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
+ expect_user_to_be_unverified(user)
+ click_on t('idv.messages.gpo.resend')
+
+ # And then actually ask for a resend
+ expect { click_on t('idv.buttons.mail.resend') }.
+ to change { GpoConfirmation.count }.from(1).to(2)
+ expect_user_to_be_unverified(user)
+ expect(page).to have_content(t('idv.messages.gpo.another_letter_on_the_way'))
+ expect(page).to have_content(t('idv.titles.come_back_later'))
+ expect(page).to have_current_path(idv_letter_enqueued_path)
+
+ # Confirm that user cannot visit other IdV pages while unverified
+ visit idv_agreement_path
+ expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
+ visit idv_ssn_url
+ expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
+ visit idv_verify_info_url
+ expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
+
+ # complete verification: end to end gpo test
+ complete_gpo_verification(user)
+ expect(user.identity_verified?).to be(true)
+ expect(page).to_not have_content(t('account.index.verification.reactivate_button'))
+ end
end
context 'logged in with PIV/CAC and no password' do
@@ -80,59 +114,13 @@
end
end
- context 'too much time has passed', :js do
- let(:days_passed) { 31 }
- let(:max_days_before_resend_disabled) { 30 }
-
- before do
- allow(IdentityConfig.store).to receive(:gpo_max_profile_age_to_send_letter_in_days).
- and_return(max_days_before_resend_disabled)
- end
-
- it 'does not present the user the option to to resend', :js do
- complete_idv_and_sign_out
- travel_to(days_passed.days.from_now) do
- sign_in_live_with_2fa(user)
- expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
- expect(page).not_to have_css('.usa-button', text: t('idv.buttons.mail.resend'))
- end
- end
-
- it 'does not allow the user to go to the resend page manually' do
- complete_idv_and_sign_out
- travel_to(days_passed.days.from_now) do
- sign_in_live_with_2fa(user)
- visit idv_request_letter_path
- expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
- expect(page).not_to have_css('.usa-button', text: t('idv.buttons.mail.resend'))
- end
- end
- end
-
- it 'allows the user to return to gpo otp confirmation', :js do
- complete_idv_and_return_to_gpo_step
- click_doc_auth_back_link
-
- expect(page).to have_content(t('idv.gpo.title'))
- expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
- expect_user_to_be_unverified(user)
- end
-
- def complete_idv_and_sign_out
+ def complete_idv_by_mail_and_sign_out
start_idv_from_sp
complete_idv_steps_before_gpo_step(user)
click_on t('idv.buttons.mail.send')
fill_in 'Password', with: user_password
click_continue
- visit root_path
- click_on t('idv.gpo.return_to_profile')
- first(:button, t('links.sign_out')).click
- end
-
- def complete_idv_and_return_to_gpo_step
- complete_idv_and_sign_out
- sign_in_live_with_2fa(user)
- click_on t('idv.messages.gpo.resend')
+ sign_out
end
def expect_user_to_be_unverified(user)
@@ -144,6 +132,24 @@ def expect_user_to_be_unverified(user)
expect(profile.active?).to eq false
expect(profile.gpo_verification_pending?).to eq true
end
+
+ def sign_out
+ visit sign_out_url
+ end
+
+ def confirm_rate_limited
+ expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
+ expect(page).not_to have_link(
+ t('idv.gpo.did_not_receive_letter.intro.request_new_letter_link'),
+ )
+ # does not allow the user to go to the resend page manually
+ visit idv_request_letter_path
+
+ expect(page).to have_current_path(idv_verify_by_mail_enter_code_path)
+ expect(page).not_to have_link(
+ t('idv.gpo.did_not_receive_letter.intro.request_new_letter_link'),
+ )
+ end
end
context 'GPO verified user has reset their password and needs to re-verify with GPO again', :js do
diff --git a/spec/features/saml/ial2_sso_spec.rb b/spec/features/saml/ial2_sso_spec.rb
index 748b3e111a0..acf814f1716 100644
--- a/spec/features/saml/ial2_sso_spec.rb
+++ b/spec/features/saml/ial2_sso_spec.rb
@@ -84,10 +84,10 @@ def sign_out_user
)
end
- context 'having previously selected USPS verification', js: true do
+ context 'immediately after selecting USPS verification', js: true do
let(:phone_confirmed) { false }
- context 'provides an option to send another letter' do
+ context 'does not provide an option to send another letter' do
it 'without signing out' do
user = create(:user, :fully_registered)
@@ -99,16 +99,7 @@ def sign_out_user
click_link(t('account.index.verification.reactivate_button'))
expect(current_path).to eq idv_verify_by_mail_enter_code_path
-
- click_link(t('idv.messages.gpo.resend'))
-
- expect(user.events.account_verified.size).to be(0)
- expect(current_path).to eq(idv_request_letter_path)
-
- click_button(t('idv.buttons.mail.resend'))
-
- expect(user.events.gpo_mail_sent.size).to eq 2
- expect(current_path).to eq(idv_letter_enqueued_path)
+ expect(page).not_to have_link(t('idv.messages.gpo.resend'))
end
it 'after signing out' do
@@ -121,16 +112,33 @@ def sign_out_user
sign_in_live_with_2fa(user)
expect(current_path).to eq idv_verify_by_mail_enter_code_path
+ expect(page).not_to have_link(t('idv.messages.gpo.resend'))
+ end
+ end
+ end
- click_link(t('idv.messages.gpo.resend'))
-
- expect(user.events.account_verified.size).to be(0)
- expect(current_path).to eq(idv_request_letter_path)
+ context 'having previously selected USPS verification', js: true do
+ let(:phone_confirmed) { false }
- click_button(t('idv.buttons.mail.resend'))
+ it 'provides an option to send another letter' do
+ user = create(:user, :fully_registered)
- expect(current_path).to eq(idv_letter_enqueued_path)
+ travel_to(2.days.ago) do
+ perform_id_verification_with_gpo_without_confirming_code(user)
end
+
+ sign_in_live_with_2fa(user)
+
+ expect(current_path).to eq idv_verify_by_mail_enter_code_path
+
+ click_link(t('idv.messages.gpo.resend'))
+
+ expect(user.events.account_verified.size).to be(0)
+ expect(current_path).to eq(idv_request_letter_path)
+
+ click_button(t('idv.buttons.mail.resend'))
+
+ expect(current_path).to eq(idv_letter_enqueued_path)
end
end
end
diff --git a/spec/lib/reporting/identity_verification_report_spec.rb b/spec/lib/reporting/identity_verification_report_spec.rb
index 49c899bcfd3..384de3a6bcf 100644
--- a/spec/lib/reporting/identity_verification_report_spec.rb
+++ b/spec/lib/reporting/identity_verification_report_spec.rb
@@ -14,33 +14,43 @@
cloudwatch_client = double(
'Reporting::CloudwatchClient',
fetch: [
- # Online verification user
+ # Online verification user (failed each vendor once, then suceeded once)
{ 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user1', 'name' => 'IdV: doc auth image upload vendor submitted' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth verify proofing results', 'success' => '0' },
+ { 'user_id' => 'user1', 'name' => 'IdV: doc auth verify proofing results', 'success' => '1' },
+ { 'user_id' => 'user1', 'name' => 'IdV: phone confirmation vendor', 'success' => '0' },
+ { 'user_id' => 'user1', 'name' => 'IdV: phone confirmation vendor', 'success' => '1' },
{ 'user_id' => 'user1', 'name' => 'IdV: final resolution', 'identity_verified' => '1' },
# Letter requested user (incomplete)
{ 'user_id' => 'user2', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user2', 'name' => 'IdV: doc auth image upload vendor submitted' },
+ { 'user_id' => 'user2', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user2', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
{ 'user_id' => 'user2', 'name' => 'IdV: final resolution', 'gpo_verification_pending' => '1' },
- # Fraud review user (incomplet)
+ # Fraud review user (incomplete)
{ 'user_id' => 'user3', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user3', 'name' => 'IdV: doc auth image upload vendor submitted' },
+ { 'user_id' => 'user3', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user3', 'name' => 'IdV: doc auth image upload vendor submitted', 'success' => '1' },
{ 'user_id' => 'user3', 'name' => 'IdV: final resolution', 'fraud_review_pending' => '1' },
# Success through address confirmation user
{ 'user_id' => 'user4', 'name' => 'IdV: GPO verification submitted' },
- # Success through in-person verification
+ # Success through in-person verification, failed doc auth (rejected)
{ 'user_id' => 'user5', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user5', 'name' => 'IdV: doc auth image upload vendor submitted' },
+ { 'user_id' => 'user5', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user5', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' },
{ 'user_id' => 'user5', 'name' => 'IdV: final resolution', 'in_person_verification_pending' => '1' },
{ 'user_id' => 'user5', 'name' => 'GetUspsProofingResultsJob: Enrollment status updated' },
# Incomplete user
{ 'user_id' => 'user6', 'name' => 'IdV: doc auth welcome visited' },
- { 'user_id' => 'user6', 'name' => 'IdV: doc auth image upload vendor submitted' },
+ { 'user_id' => 'user6', 'name' => 'IdV: doc auth welcome submitted' },
+ { 'user_id' => 'user6', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' },
],
)
@@ -60,6 +70,7 @@
['Metric', '# of Users'],
[],
['Started IdV Verification', '5'],
+ ['Submitted welcome page', '5'],
['Images uploaded', '5'],
[],
['Workflow completed', '4'],
@@ -85,17 +96,67 @@
describe '#data' do
it 'counts unique users per event as a hash' do
- expect(report.data).to eq(
+ expect(report.data.transform_values(&:count)).to eq(
+ # events
+ 'GetUspsProofingResultsJob: Enrollment status updated' => 1,
'IdV: doc auth image upload vendor submitted' => 5,
+ 'IdV: doc auth verify proofing results' => 1,
+ 'IdV: doc auth welcome submitted' => 5,
'IdV: doc auth welcome visited' => 5,
'IdV: final resolution' => 4,
+ 'IdV: GPO verification submitted' => 1,
+ 'IdV: phone confirmation vendor' => 1,
+
+ # results
+ 'IdV: final resolution - Fraud Review Pending' => 1,
'IdV: final resolution - GPO Pending' => 1,
'IdV: final resolution - In Person Proofing' => 1,
- 'IdV: final resolution - Fraud Review Pending' => 1,
'IdV: final resolution - Verified' => 1,
- 'IdV: GPO verification submitted' => 1,
- 'GetUspsProofingResultsJob: Enrollment status updated' => 1,
+ 'IdV Reject: Doc Auth' => 3,
+ 'IdV Reject: Phone Finder' => 1,
+ 'IdV Reject: Verify' => 1,
+ )
+ end
+ end
+
+ describe '#idv_doc_auth_rejected' do
+ it 'is the number of users who failed proofing and never passed' do
+ expect(report.idv_doc_auth_rejected).to eq(1)
+ end
+ end
+
+ describe '#merge', :freeze_time do
+ it 'makes a new instance with merged data' do
+ report1 = Reporting::IdentityVerificationReport.new(
+ time_range: 4.days.ago..3.days.ago,
+ issuers: %w[a],
+ )
+ allow(report1).to receive(:data).and_return(
+ 'IdV: doc auth image upload vendor submitted' => %w[a b].to_set,
+ 'IdV: final resolution' => %w[a].to_set,
+ )
+
+ report2 = Reporting::IdentityVerificationReport.new(
+ time_range: 2.days.ago..1.day.ago,
+ issuers: %w[b],
+ )
+ allow(report2).to receive(:data).and_return(
+ 'IdV: doc auth image upload vendor submitted' => %w[b c].to_set,
+ 'IdV: final resolution' => %w[c].to_set,
)
+
+ merged = report1.merge(report2)
+
+ aggregate_failures do
+ expect(merged.time_range).to eq(4.days.ago..1.day.ago)
+
+ expect(merged.issuers).to eq(%w[a b])
+
+ expect(merged.data).to eq(
+ 'IdV: doc auth image upload vendor submitted' => %w[a b c].to_set,
+ 'IdV: final resolution' => %w[a c].to_set,
+ )
+ end
end
end
diff --git a/spec/lib/reporting/proofing_rate_report_spec.rb b/spec/lib/reporting/proofing_rate_report_spec.rb
new file mode 100644
index 00000000000..66bbdfe088c
--- /dev/null
+++ b/spec/lib/reporting/proofing_rate_report_spec.rb
@@ -0,0 +1,122 @@
+require 'rails_helper'
+require 'reporting/proofing_rate_report'
+
+RSpec.describe Reporting::ProofingRateReport do
+ let(:end_date) { Date.new(2022, 1, 1) }
+
+ subject(:report) do
+ Reporting::ProofingRateReport.new(end_date: end_date)
+ end
+
+ describe '#as_csv' do
+ before do
+ allow(report).to receive(:reports).and_return(
+ [
+ instance_double(
+ 'Reporting::IdentityVerificationReport',
+ idv_started: 4,
+ idv_doc_auth_welcome_submitted: 3,
+ idv_doc_auth_image_vendor_submitted: 2,
+ successfully_verified_users: 1,
+ idv_doc_auth_rejected: 1,
+ time_range: (end_date - 30.days)..end_date,
+ ),
+ instance_double(
+ 'Reporting::IdentityVerificationReport',
+ idv_started: 5,
+ idv_doc_auth_welcome_submitted: 4,
+ idv_doc_auth_image_vendor_submitted: 3,
+ successfully_verified_users: 2,
+ idv_doc_auth_rejected: 1,
+ time_range: (end_date - 60.days)..end_date,
+ ),
+ instance_double(
+ 'Reporting::IdentityVerificationReport',
+ idv_started: 6,
+ idv_doc_auth_welcome_submitted: 5,
+ idv_doc_auth_image_vendor_submitted: 4,
+ successfully_verified_users: 3,
+ idv_doc_auth_rejected: 1,
+ time_range: (end_date - 90.days)..end_date,
+ ),
+ ],
+ )
+ end
+
+ it 'renders a report with 30, 60, 90 day numbers' do
+ # rubocop:disable Layout/LineLength
+ expected_csv = [
+ ['Metric', 'Trailing 30d', 'Trailing 60d', 'Trailing 90d'],
+ ['Start Date', Date.new(2021, 12, 2), Date.new(2021, 11, 2), Date.new(2021, 10, 3)],
+ ['End Date', Date.new(2022, 1, 1), Date.new(2022, 1, 1), Date.new(2022, 1, 1)],
+ ['IDV Started', 4, 5, 6],
+ ['Welcome Submitted', 3, 4, 5],
+ ['Image Submitted', 2, 3, 4],
+ ['Successfully Verified', 1, 2, 3],
+ ['IDV Rejected', 1, 1, 1],
+ ['Blanket Proofing Rate (IDV Started to Successfully Verified)', 1.0 / 4, 2.0 / 5, 3.0 / 6],
+ ['Intent Proofing Rate (Welcome Submitted to Successfully Verified)', 1.0 / 3, 2.0 / 4, 3.0 / 5],
+ ['Actual Proofing Rate (Image Submitted to Successfully Verified)', 1.0 / 2, 2.0 / 3, 3.0 / 4],
+ ['Industry Proofing Rate (Verified minus IDV Rejected)', 1.0 / 2, 2.0 / 3, 3.0 / 4],
+ ]
+ # rubocop:enable Layout/LineLength
+
+ aggregate_failures do
+ report.as_csv.zip(expected_csv).each do |actual, expected|
+ expect(actual).to eq(expected)
+ end
+ end
+ end
+ end
+
+ describe '#reports' do
+ before do
+ stub_const('Reporting::CloudwatchClient::DEFAULT_WAIT_DURATION', 0)
+
+ query_id = SecureRandom.hex
+
+ Aws.config[:cloudwatchlogs] = {
+ stub_responses: {
+ start_query: { query_id: query_id },
+ get_query_results: {
+ status: 'Complete',
+ results: [],
+ },
+ },
+ }
+ end
+
+ it 'calls IdentityVerificationReport with separate slices, but merges them' do
+ allow(Reporting::IdentityVerificationReport).to receive(:new).and_call_original
+
+ expect(report.reports.map(&:time_range)).to eq(
+ [
+ (end_date - 30.days)..end_date,
+ (end_date - 60.days)..end_date,
+ (end_date - 90.days)..end_date,
+ ],
+ )
+
+ expect(Reporting::IdentityVerificationReport).to have_received(:new).with(
+ time_range: (end_date - 30.days)..end_date,
+ issuers: nil,
+ verbose: false,
+ progress: false,
+ ).once
+
+ expect(Reporting::IdentityVerificationReport).to have_received(:new).with(
+ time_range: (end_date - 60.days)..(end_date - 30.days),
+ issuers: nil,
+ verbose: false,
+ progress: false,
+ ).once
+
+ expect(Reporting::IdentityVerificationReport).to have_received(:new).with(
+ time_range: (end_date - 90.days)..(end_date - 60.days),
+ issuers: nil,
+ verbose: false,
+ progress: false,
+ ).once
+ end
+ end
+end
diff --git a/spec/presenters/idv/by_mail/request_letter_presenter_spec.rb b/spec/presenters/idv/by_mail/request_letter_presenter_spec.rb
index 6f2d9ce0cb4..547e54e3db6 100644
--- a/spec/presenters/idv/by_mail/request_letter_presenter_spec.rb
+++ b/spec/presenters/idv/by_mail/request_letter_presenter_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe Idv::ByMail::RequestLetterPresenter do
+ include Rails.application.routes.url_helpers
+
let(:user) { create(:user) }
subject(:decorator) do
@@ -67,13 +69,13 @@
context 'when the user has a pending profile' do
it 'returns the verify account path' do
create(:profile, user: user, gpo_verification_pending_at: 1.day.ago)
- expect(subject.fallback_back_path).to eq('/verify/by_mail')
+ expect(subject.fallback_back_path).to eq(idv_verify_by_mail_enter_code_path)
end
end
context 'when the user does not have a pending profile' do
it 'returns the idv phone path' do
- expect(subject.fallback_back_path).to eq('/verify/phone')
+ expect(subject.fallback_back_path).to eq(idv_phone_path)
end
end
end
diff --git a/spec/requests/csp_spec.rb b/spec/requests/csp_spec.rb
index 80b38faa197..54e6305809f 100644
--- a/spec/requests/csp_spec.rb
+++ b/spec/requests/csp_spec.rb
@@ -11,9 +11,7 @@
expect(content_security_policy['default-src']).to eq("'self'")
expect(content_security_policy['base-uri']).to eq("'self'")
expect(content_security_policy['child-src']).to eq("'self'")
- expect(content_security_policy['connect-src']).to eq(
- "'self' *.nr-data.net",
- )
+ expect(content_security_policy['connect-src']).to eq("'self'")
expect(content_security_policy['font-src']).to eq("'self' data:")
expect(content_security_policy['form-action']).to eq(
"'self' http://localhost:7654 https://example.com http://www.example.com",
@@ -39,9 +37,7 @@
expect(content_security_policy['default-src']).to eq("'self'")
expect(content_security_policy['base-uri']).to eq("'self'")
expect(content_security_policy['child-src']).to eq("'self'")
- expect(content_security_policy['connect-src']).to eq(
- "'self' *.nr-data.net",
- )
+ expect(content_security_policy['connect-src']).to eq("'self'")
expect(content_security_policy['font-src']).to eq("'self' data:")
expect(content_security_policy['form-action']).to eq("'self'")
expect(content_security_policy['img-src']).to eq(
diff --git a/spec/services/frontend_error_logger_spec.rb b/spec/services/frontend_error_logger_spec.rb
new file mode 100644
index 00000000000..0203f0b457e
--- /dev/null
+++ b/spec/services/frontend_error_logger_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.describe FrontendErrorLogger do
+ describe '.track_event' do
+ it 'notices an expected error to NewRelic with custom parameters' do
+ expect(NewRelic::Agent).to receive(:notice_error).with(
+ kind_of(FrontendErrorLogger::FrontendError),
+ expected: true,
+ custom_params: {
+ frontend_error: {
+ name: 'name',
+ message: 'message',
+ stack: 'stack',
+ },
+ },
+ )
+
+ FrontendErrorLogger.track_error(name: 'name', message: 'message', stack: 'stack')
+ end
+ end
+end
diff --git a/spec/services/frontend_logger_spec.rb b/spec/services/frontend_logger_spec.rb
index 01bffa7ce85..577e7c5aa03 100644
--- a/spec/services/frontend_logger_spec.rb
+++ b/spec/services/frontend_logger_spec.rb
@@ -16,7 +16,10 @@ def example_method_handler(ok:, **rest)
let(:event_map) do
{
- 'method' => ExampleAnalyticsEvents.instance_method(:example_method_handler),
+ 'method' => analytics.method(:example_method_handler),
+ 'proc' => lambda do |ok:, other:|
+ analytics.track_event('some customized event', 'ok' => ok, 'other' => other, 'custom' => 1)
+ end,
}
end
let(:logger) { described_class.new(analytics: analytics, event_map: event_map) }
@@ -46,5 +49,17 @@ def example_method_handler(ok:, **rest)
expect(analytics).to have_logged_event('example', ok: true, rest: {})
end
end
+
+ context 'with proc handler' do
+ let(:name) { 'proc' }
+
+ it 'calls the method and passes analytics and attributes' do
+ call
+
+ expect(analytics).to have_logged_event(
+ 'some customized event', 'ok' => true, 'other' => true, 'custom' => 1
+ )
+ end
+ end
end
end
diff --git a/spec/services/idv/gpo_mail_spec.rb b/spec/services/idv/gpo_mail_spec.rb
index 24ab2063dca..953f1fae559 100644
--- a/spec/services/idv/gpo_mail_spec.rb
+++ b/spec/services/idv/gpo_mail_spec.rb
@@ -3,76 +3,104 @@
RSpec.describe Idv::GpoMail do
let(:user) { create(:user) }
let(:subject) { Idv::GpoMail.new(user) }
+ let(:max_letter_request_events) { 2 }
+ let(:letter_request_events_window_days) { 30 }
+ let(:minimum_wait_before_another_usps_letter_in_hours) { 24 }
+
+ before do
+ allow(IdentityConfig.store).to receive(:max_mail_events).
+ and_return(max_letter_request_events)
+ allow(IdentityConfig.store).to receive(:max_mail_events_window_in_days).
+ and_return(letter_request_events_window_days)
+ allow(IdentityConfig.store).to receive(:minimum_wait_before_another_usps_letter_in_hours).
+ and_return(minimum_wait_before_another_usps_letter_in_hours)
+ end
describe '#mail_spammed?' do
- context 'when no mail has been sent' do
+ context 'when no letters have been requested' do
it 'returns false' do
expect(subject.mail_spammed?).to eq false
end
end
- context 'when the amount of sent mail is lower than the allowed maximum' do
- it 'returns false' do
- event_create(event_type: :gpo_mail_sent, user: user)
+ context 'when too many letters have been requested within the limiting window' do
+ before do
+ enqueue_gpo_letter_for(user, at_time: 4.days.ago)
+ enqueue_gpo_letter_for(user, at_time: 3.days.ago)
+ enqueue_gpo_letter_for(user, at_time: 2.days.ago)
+ end
- expect(subject.mail_spammed?).to eq false
+ it 'is true' do
+ expect(subject.mail_spammed?).to eq true
+ end
+
+ context 'but the window limit is disabled due to a 0 window size' do
+ let(:letter_request_events_window_days) { 0 }
+
+ it 'is false' do
+ expect(subject.mail_spammed?).to eq false
+ end
+ end
+
+ context 'but the window limit is disabled due to a 0 window count' do
+ let(:max_letter_request_events) { 0 }
+
+ it 'is false' do
+ expect(subject.mail_spammed?).to eq false
+ end
end
end
- context 'when too much mail has been sent' do
- it 'returns true if the oldest event was within the last month' do
- event_create(event_type: :gpo_mail_sent, user: user, updated_at: 2.weeks.ago)
- event_create(event_type: :gpo_mail_sent, user: user, updated_at: 1.week.ago)
+ context 'when a letter has been requested too recently' do
+ before do
+ enqueue_gpo_letter_for(user)
+ end
+ it 'is true' do
expect(subject.mail_spammed?).to eq true
end
- it 'returns false if the oldest event was more than a month ago' do
- event_create(event_type: :gpo_mail_sent, user: user, updated_at: 2.weeks.ago)
- event_create(event_type: :gpo_mail_sent, user: user, updated_at: 2.months.ago)
+ context 'but the too-recent limit is disabled' do
+ let(:minimum_wait_before_another_usps_letter_in_hours) { 0 }
- expect(subject.mail_spammed?).to eq false
+ it 'is false' do
+ expect(subject.mail_spammed?).to eq false
+ end
end
- end
- context 'when MAX_MAIL_EVENTS or MAIL_EVENTS_WINDOW_DAYS are zero' do
- it 'returns false' do
- stub_const 'Idv::GpoMail::MAX_MAIL_EVENTS', 0
- stub_const 'Idv::GpoMail::MAIL_EVENTS_WINDOW_DAYS', 0
+ context 'but the letter is not attached to their pending profile' do
+ # This can happen if the user resets their password while a GPO
+ # letter is pending.
- expect(subject.mail_spammed?).to eq false
+ before do
+ user.gpo_verification_pending_profile.update(
+ gpo_verification_pending_at: nil,
+ )
+ end
+
+ it 'returns false' do
+ expect(subject.mail_spammed?).to be false
+ end
end
end
end
- def event_create(hash)
- uuid = 'foo'
- remote_ip = '127.0.0.1'
- user = hash[:user]
- event = hash[:event_type]
- now = Time.zone.now
- updated_at = hash[:updated_at] || now
- device = Device.find_by(user_id: user.id, cookie_uuid: uuid)
- if device
- device.last_used_at = now
- device.last_ip = remote_ip
- device.save
- else
- last_login_at = Time.zone.now
- device = Device.create(
- user_id: user.id,
- user_agent: '',
- cookie_uuid: uuid,
- last_used_at: last_login_at,
- last_ip: remote_ip,
- )
- end
- Event.create(
- user_id: user.id,
- device_id: device.id,
- ip: remote_ip,
- event_type: event,
- created_at: updated_at, updated_at: updated_at
+ def enqueue_gpo_letter_for(user, at_time: Time.zone.now)
+ profile = create(
+ :profile,
+ user: user,
+ gpo_verification_pending_at: at_time,
+ )
+
+ GpoConfirmationMaker.new(
+ pii: {},
+ service_provider: nil,
+ profile: profile,
+ ).perform
+
+ profile.gpo_confirmation_codes.last.update(
+ created_at: at_time,
+ updated_at: at_time,
)
end
end
diff --git a/spec/services/rate_limiter_spec.rb b/spec/services/rate_limiter_spec.rb
index 9a4d629e454..dae6a7297f9 100644
--- a/spec/services/rate_limiter_spec.rb
+++ b/spec/services/rate_limiter_spec.rb
@@ -131,10 +131,8 @@
let(:rate_limiter) { RateLimiter.new(target: '1', rate_limit_type: rate_limit_type) }
context 'without having attempted' do
- it 'returns current time' do
- freeze_time do
- expect(rate_limiter.expires_at).to eq(Time.zone.now)
- end
+ it 'returns nil' do
+ expect(rate_limiter.expires_at).to eq(nil)
end
end
diff --git a/spec/views/layouts/application.html.erb_spec.rb b/spec/views/layouts/application.html.erb_spec.rb
index b4799cdb66d..cb25c4db0e5 100644
--- a/spec/views/layouts/application.html.erb_spec.rb
+++ b/spec/views/layouts/application.html.erb_spec.rb
@@ -164,26 +164,29 @@
end
end
- context 'when new relic browser key and app id are present' do
- it 'it render the new relic javascript' do
- allow(IdentityConfig.store).to receive(:newrelic_browser_key).and_return('foo')
- allow(IdentityConfig.store).to receive(:newrelic_browser_app_id).and_return('foo')
- allow(BrowserSupport).to receive(:supported?).and_return(true)
+ describe 'javascript error tracking' do
+ context 'when browser is unsupported' do
+ before do
+ allow(BrowserSupport).to receive(:supported?).and_return(false)
+ end
- render
+ it 'does not render error tracking script' do
+ render
- expect(view).to render_template(partial: 'shared/newrelic/_browser_instrumentation')
+ expect(rendered).not_to have_css('script[src$="track-errors.js"]', visible: :all)
+ end
end
- end
- context 'when new relic browser key and app id are not present' do
- it 'it does not render the new relic javascript' do
- allow(IdentityConfig.store).to receive(:newrelic_browser_key).and_return('')
- allow(IdentityConfig.store).to receive(:newrelic_browser_app_id).and_return('')
+ context 'when browser is supported' do
+ before do
+ allow(BrowserSupport).to receive(:supported?).and_return(true)
+ end
- render
+ it 'renders error tracking script' do
+ render
- expect(view).to_not render_template(partial: 'shared/newrelic/_browser_instrumentation')
+ expect(rendered).to have_css('script[src$="track-errors.js"]', visible: :all)
+ end
end
end
end
diff --git a/webpack.config.js b/webpack.config.js
index 82e3296ca06..787ad2a5703 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -49,7 +49,7 @@ module.exports = /** @type {import('webpack').Configuration} */ ({
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'],
- conditionNames: ['source', 'import', 'require', 'node'],
+ conditionNames: ['source', '...'],
},
module: {
rules: [
diff --git a/yarn.lock b/yarn.lock
index bb4a58e1944..0100b355516 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -27,6 +27,22 @@
"@jridgewell/gen-mapping" "^0.1.0"
"@jridgewell/trace-mapping" "^0.3.9"
+"@babel/cli@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.22.15.tgz#22ed82d76745a43caa60a89917bedb7c9b5bd145"
+ integrity sha512-prtg5f6zCERIaECeTZzd2fMtVjlfjhUcO+fBLQ6DXXdq5FljN+excVitJ2nogsusdf31LeqkjAfXZ7Xq+HmN8g==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.17"
+ commander "^4.0.1"
+ convert-source-map "^1.1.0"
+ fs-readdir-recursive "^1.1.0"
+ glob "^7.2.0"
+ make-dir "^2.1.0"
+ slash "^2.0.0"
+ optionalDependencies:
+ "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3"
+ chokidar "^3.4.0"
+
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
@@ -1115,10 +1131,10 @@
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
-"@jridgewell/resolve-uri@^3.0.3":
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
- integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
version "1.1.2"
@@ -1133,18 +1149,18 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@jridgewell/sourcemap-codec@^1.4.10":
- version "1.4.14"
- resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
- integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
-"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
- version "0.3.14"
- resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
- integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
+"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
+ integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==
dependencies:
- "@jridgewell/resolve-uri" "^3.0.3"
- "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
@@ -1173,6 +1189,11 @@
strict-event-emitter "^0.2.4"
web-encoding "^1.1.5"
+"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
+ version "2.1.8-no-fsevents.3"
+ resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"
+ integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -1490,11 +1511,6 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
-"@types/newrelic@^7.0.3":
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/@types/newrelic/-/newrelic-7.0.3.tgz#07ae3d0175f712e0fcaef69564dbe06b6039febb"
- integrity sha512-MAaZYpJ9HEumg8MR4OFNDu8Cy4LnNc89aZ4pqXcBiQyo8SaQVv0sPj2JzzR1rQ/Rk86WL9vLjrDjuEg2vDMimg==
-
"@types/node@*", "@types/node@^20.2.5":
version "20.2.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb"
@@ -2388,7 +2404,7 @@ check-error@^1.0.2:
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
-chokidar@3.5.3, chokidar@^3.4.2, chokidar@^3.5.3:
+chokidar@3.5.3, chokidar@^3.4.0, chokidar@^3.4.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -2518,6 +2534,11 @@ commander@^2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+commander@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
+ integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+
commander@^7.0.0, commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
@@ -2575,7 +2596,7 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-convert-source-map@^1.7.0:
+convert-source-map@^1.1.0, convert-source-map@^1.7.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
@@ -3577,6 +3598,11 @@ fs-monkey@^1.0.3:
resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3"
integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==
+fs-readdir-recursive@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
+ integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -3675,7 +3701,7 @@ glob@7.2.0:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.1.3:
+glob@^7.1.3, glob@^7.2.0:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -5987,6 +6013,11 @@ sinon@^14.0.0:
nise "^5.1.1"
supports-color "^7.2.0"
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"