From 9b9ab78b221c39fb76e32de0e9d1fd87937efd30 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Tue, 19 Mar 2024 16:37:05 -0700 Subject: [PATCH 01/11] Add active_profile_idv_level and pending_profile_idv_level to logging --- app/services/idv/analytics_events_enhancer.rb | 12 ++++++ .../idv/analytics_events_enhancer_spec.rb | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index 24124a09655..74dbe18f258 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -134,9 +134,21 @@ def self.should_enhance_method?(method_name) def common_analytics_attributes { proofing_components: proofing_components, + active_profile_idv_level: active_profile&.idv_level, + pending_profile_idv_level: pending_profile&.idv_level, }.compact end + def active_profile + return if !user&.respond_to?(:active_profile) || !user.active_profile + user.active_profile + end + + def pending_profile + return if !user&.respond_to?(:pending_profile) || !user.pending_profile + user.pending_profile + end + def proofing_components return if !user&.respond_to?(:proofing_component) || !user.proofing_component ProofingComponentsLogging.new(user.proofing_component) diff --git a/spec/services/idv/analytics_events_enhancer_spec.rb b/spec/services/idv/analytics_events_enhancer_spec.rb index 9593042b9af..3f77d168d95 100644 --- a/spec/services/idv/analytics_events_enhancer_spec.rb +++ b/spec/services/idv/analytics_events_enhancer_spec.rb @@ -94,4 +94,47 @@ def track_event(_event, **kwargs) end end end + + describe 'active_profile_idv_level' do + context 'without an active profile' do + it 'calls analytics method with original attributes but not active_profile_idv_level' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to match(extra: true) + end + end + + context 'with an active profile' do + let(:user) { create(:user) } + let!(:profile) { create(:profile, :active, user: user) } + + it 'calls analytics method with original attributes and active_profile_idv_level' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to include( + extra: true, + active_profile_idv_level: 'legacy_unsupervised', + ) + end + end + end + + describe 'pending_profile_idv_level' do + context 'without a pending profile' do + it 'calls analytics method with original attributes but not pending_profile_idv_level' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to match(extra: true) + end + end + + context 'with a pending profile' do + let(:user) { create(:user) } + let!(:profile) { create(:profile, :verify_by_mail_pending, user: user) } + + it 'calls analytics method with original attributes and pending_profile_idv_level' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to include( + pending_profile_idv_level: 'legacy_unsupervised', + ) + end + end + end end From 568771e7f347ad140687fd577d9ef3de291d9cba Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 7 Mar 2024 15:08:50 -0800 Subject: [PATCH 02/11] Add profile_history to IdV analytics events Summarize the history of a user's profiles in IdV analytics events changelog: Internal, Identity verification, Include profile metadata in analytics logs --- app/services/idv/analytics_events_enhancer.rb | 10 +++ app/services/idv/profile_logging.rb | 22 ++++++ .../idv/analytics_events_enhancer_spec.rb | 40 ++++++++++ spec/services/idv/profile_logging_spec.rb | 73 +++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 app/services/idv/profile_logging.rb create mode 100644 spec/services/idv/profile_logging_spec.rb diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index 74dbe18f258..9b1f21fa50d 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -136,6 +136,7 @@ def common_analytics_attributes proofing_components: proofing_components, active_profile_idv_level: active_profile&.idv_level, pending_profile_idv_level: pending_profile&.idv_level, + profile_history: profile_history, }.compact end @@ -149,6 +150,15 @@ def pending_profile user.pending_profile end + def profile_history + return if !user&.respond_to?(:profiles) + + (user&.profiles || []). + sort_by { |profile| profile.created_at }. + map { |profile| ProfileLogging.new(profile) }. + presence + end + def proofing_components return if !user&.respond_to?(:proofing_component) || !user.proofing_component ProofingComponentsLogging.new(user.proofing_component) diff --git a/app/services/idv/profile_logging.rb b/app/services/idv/profile_logging.rb new file mode 100644 index 00000000000..5f113451c19 --- /dev/null +++ b/app/services/idv/profile_logging.rb @@ -0,0 +1,22 @@ +module Idv + ProfileLogging = Struct.new(:profile) do + def as_json + profile.slice( + %i[ + id + active + idv_level + created_at + verified_at + activated_at + in_person_verification_pending_at + gpo_verification_pending_at + fraud_review_pending_at + fraud_rejection_at + fraud_pending_reason + deactivation_reason + ], + ).compact + end + end +end diff --git a/spec/services/idv/analytics_events_enhancer_spec.rb b/spec/services/idv/analytics_events_enhancer_spec.rb index 3f77d168d95..4e0fc345d1f 100644 --- a/spec/services/idv/analytics_events_enhancer_spec.rb +++ b/spec/services/idv/analytics_events_enhancer_spec.rb @@ -137,4 +137,44 @@ def track_event(_event, **kwargs) end end end + + describe 'profile_history' do + let(:profiles) { nil } + + context 'user has no profiles' do + it 'logs an empty array' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to eq(extra: true) + end + end + + context 'user has profiles' do + let(:user) { create(:user) } + let!(:profiles) do + [ + create(:profile, :active, user:, created_at: 10.days.ago), + create(:profile, :verify_by_mail_pending, user:, created_at: 11.days.ago), + ] + end + + it 'logs Profiles in created_at order' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to include(:profile_history) + expect(analytics.called_kwargs[:profile_history].map { |h| h.profile.id }).to eql( + [ + profiles.last.id, + profiles.first.id, + ], + ) + end + + it 'logs Profiles using ProfileLogging' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).to include( + active_profile_idv_level: 'legacy_unsupervised', + profile_history: all(be_instance_of(Idv::ProfileLogging)), + ) + end + end + end end diff --git a/spec/services/idv/profile_logging_spec.rb b/spec/services/idv/profile_logging_spec.rb new file mode 100644 index 00000000000..0eac24f159f --- /dev/null +++ b/spec/services/idv/profile_logging_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +RSpec.describe Idv::ProfileLogging do + describe '#as_json' do + context 'active profile' do + let(:profile) { create(:profile, :active) } + + it 'returns relevant attributes with nil values omitted' do + expect(described_class.new(profile).as_json).to eql( + profile.slice(%i[id active activated_at created_at idv_level verified_at]), + ) + end + end + + context 'gpo pending profile' do + let(:profile) { create(:profile, :verify_by_mail_pending) } + it 'returns relevant attributes with nil values omitted' do + expect(described_class.new(profile).as_json).to eql( + profile.slice( + %i[id active created_at idv_level + gpo_verification_pending_at], + ), + ) + end + end + + context 'in person pending profile' do + let(:profile) { create(:profile, :in_person_verification_pending) } + it 'returns relevant attributes with nil values omitted' do + expect(described_class.new(profile).as_json).to eql( + profile.slice( + %i[id active created_at idv_level + in_person_verification_pending_at], + ), + ) + end + end + + context 'fraud review pending' do + let(:profile) { create(:profile, :fraud_review_pending) } + it 'returns relevant attributes with nil values omitted' do + expect(described_class.new(profile).as_json).to eql( + profile.slice( + %i[id active created_at idv_level + fraud_pending_reason fraud_review_pending_at], + ), + ) + end + end + + context 'fraud rejection' do + let(:profile) { create(:profile, :fraud_rejection) } + it 'returns relevant attributes with nil values omitted' do + expect(described_class.new(profile).as_json).to eql( + profile.slice( + %i[id active created_at idv_level fraud_pending_reason fraud_rejection_at], + ), + ) + end + end + + context 'password reset profile' do + let(:profile) { create(:profile, :password_reset) } + it 'returns relevant attributes with nil values omitted' do + expect(described_class.new(profile).as_json).to eql( + profile.slice( + %i[id active created_at idv_level deactivation_reason], + ), + ) + end + end + end +end From f3d8149be0f8a077fb94a0ece935f80bf52bb70f Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 20 Mar 2024 12:26:40 -0700 Subject: [PATCH 03/11] Rework active_profile / pending_profile a little Just return the levels, and use .presence to ensure falsey values are nil so they're compacted properly --- app/services/idv/analytics_events_enhancer.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index 9b1f21fa50d..928b7e511df 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -134,20 +134,18 @@ def self.should_enhance_method?(method_name) def common_analytics_attributes { proofing_components: proofing_components, - active_profile_idv_level: active_profile&.idv_level, - pending_profile_idv_level: pending_profile&.idv_level, + active_profile_idv_level: active_profile_idv_level.presence, + pending_profile_idv_level: pending_profile_idv_level.presence, profile_history: profile_history, }.compact end - def active_profile - return if !user&.respond_to?(:active_profile) || !user.active_profile - user.active_profile + def active_profile_idv_level + user&.respond_to?(:active_profile) && user&.active_profile&.idv_level end - def pending_profile - return if !user&.respond_to?(:pending_profile) || !user.pending_profile - user.pending_profile + def pending_profile_idv_level + user&.respond_to?(:pending_profile) && user&.pending_profile&.idv_level end def profile_history From a773b46f442aeec188aa4fdf640cf1268fcb008c Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 20 Mar 2024 16:52:01 -0700 Subject: [PATCH 04/11] Only send profile_history to certain methods It's a lot of data and we want to keep average event payload size low. --- app/services/idv/analytics_events_enhancer.rb | 49 ++++++++++++++----- .../idv/analytics_events_enhancer_spec.rb | 21 ++++++++ 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index 928b7e511df..188d381d165 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -84,7 +84,6 @@ module AnalyticsEventsEnhancer idv_in_person_usps_proofing_results_job_unexpected_response idv_in_person_usps_proofing_results_job_user_sent_to_fraud_review idv_in_person_usps_request_enroll_exception - idv_intro_visit idv_ipp_deactivated_for_never_visiting_post_office idv_link_sent_capture_doc_polling_complete idv_link_sent_capture_doc_polling_started @@ -109,6 +108,20 @@ module AnalyticsEventsEnhancer idv_warning_shown ].to_set.freeze + STANDARD_ARGUMENTS = %i[ + proofing_components + active_profile_idv_level + pending_profile_idv_level + ].freeze + + METHODS_WITH_PROFILE_HISTORY = %i[ + idv_doc_auth_verify_proofing_results + idv_intro_visit + idv_final + idv_please_call_visited + idv_start_over + ].to_set.freeze + def self.included(_mod) raise 'this mixin is intended to be prepended, not included' end @@ -117,7 +130,7 @@ def self.prepended(mod) mod.instance_methods.each do |method_name| if should_enhance_method?(method_name) mod.define_method method_name do |**kwargs| - super(**kwargs, **common_analytics_attributes) + super(**kwargs, **analytics_attributes(method_name)) end end end @@ -125,19 +138,32 @@ def self.prepended(mod) def self.should_enhance_method?(method_name) return false if IGNORED_METHODS.include?(method_name) - method_name.start_with?('idv_') end + def self.extra_args_for_method(method_name) + return [] unless should_enhance_method?(method_name) + + args = STANDARD_ARGUMENTS + + if METHODS_WITH_PROFILE_HISTORY.include?(method_name) + args = [ + *args, + :profile_history, + ] + end + + args + end + private - def common_analytics_attributes - { - proofing_components: proofing_components, - active_profile_idv_level: active_profile_idv_level.presence, - pending_profile_idv_level: pending_profile_idv_level.presence, - profile_history: profile_history, - }.compact + def analytics_attributes(method_name) + AnalyticsEventsEnhancer.extra_args_for_method(method_name). + index_with do |arg_name| + send(arg_name.to_s).presence + end. + compact end def active_profile_idv_level @@ -153,8 +179,7 @@ def profile_history (user&.profiles || []). sort_by { |profile| profile.created_at }. - map { |profile| ProfileLogging.new(profile) }. - presence + map { |profile| ProfileLogging.new(profile) } end def proofing_components diff --git a/spec/services/idv/analytics_events_enhancer_spec.rb b/spec/services/idv/analytics_events_enhancer_spec.rb index 4e0fc345d1f..874e23f4069 100644 --- a/spec/services/idv/analytics_events_enhancer_spec.rb +++ b/spec/services/idv/analytics_events_enhancer_spec.rb @@ -140,6 +140,18 @@ def track_event(_event, **kwargs) describe 'profile_history' do let(:profiles) { nil } + let(:include_profile_history?) { true } + + before do + if include_profile_history? + allow( + stub_const( + 'Idv::AnalyticsEventsEnhancer::METHODS_WITH_PROFILE_HISTORY', + %i[idv_test_method], + ), + ) + end + end context 'user has no profiles' do it 'logs an empty array' do @@ -175,6 +187,15 @@ def track_event(_event, **kwargs) profile_history: all(be_instance_of(Idv::ProfileLogging)), ) end + + context 'method is not opted into profile_history' do + let(:include_profile_history?) { false } + + it 'does not log profile_history' do + analytics.idv_test_method(extra: true) + expect(analytics.called_kwargs).not_to include(:profile_history) + end + end end end end From a0a6db330d0e14e70b35fa9f2d674eaa6329aa49 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Fri, 22 Mar 2024 13:40:42 -0700 Subject: [PATCH 05/11] Add arguments to analytics_events.rb Adding the following to covered methods: - active_profile_idv_level - pending_profile_idv_level - profile_history Also making sure proofing_components is documented appropriately. --- app/services/analytics_events.rb | 314 +++++++++++++++++++++++++++++-- 1 file changed, 296 insertions(+), 18 deletions(-) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 004bc82bd22..0c05b2125cc 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -808,24 +808,44 @@ def idv_barcode_warning_retake_photos_clicked(liveness_checking_required:, **ext # @param [String] step the step that the user was on when they clicked cancel # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user confirmed their choice to cancel going through IDV - def idv_cancellation_confirmed(step:, proofing_components: nil, **extra) + def idv_cancellation_confirmed( + step:, + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: cancellation confirmed', step: step, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [String] step the step that the user was on when they clicked cancel # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user chose to go back instead of cancel IDV - def idv_cancellation_go_back(step:, proofing_components: nil, **extra) + def idv_cancellation_go_back( + step:, + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: cancellation go back', step: step, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -834,11 +854,15 @@ def idv_cancellation_go_back(step:, proofing_components: nil, **extra) # @param [String] request_came_from the controller and action from the # source such as "users/sessions#new" # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user clicked cancel during IDV (presented with an option to go back or confirm) def idv_cancellation_visited( step:, request_came_from:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -846,6 +870,8 @@ def idv_cancellation_visited( step: step, request_came_from: request_came_from, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -1227,6 +1253,8 @@ def idv_doc_auth_welcome_visited(**extra) # @param [Boolean] in_person_verification_pending # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # @identity.idp.previous_event_name IdV: review info visited def idv_enter_password_submitted( success:, @@ -1236,6 +1264,8 @@ def idv_enter_password_submitted( in_person_verification_pending:, deactivation_reason: nil, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -1247,6 +1277,8 @@ def idv_enter_password_submitted( in_person_verification_pending: in_person_verification_pending, fraud_rejection: fraud_rejection, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -1254,18 +1286,24 @@ def idv_enter_password_submitted( # @param [Idv::ProofingComponentsLogging] proofing_components User's # current proofing components # @param [String] address_verification_method The method (phone or gpo) being + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # used to verify the user's identity # User visited IDV password confirm page # @identity.idp.previous_event_name IdV: review info visited def idv_enter_password_visited( proofing_components: nil, address_verification_method: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( :idv_enter_password_visited, address_verification_method: address_verification_method, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -1302,6 +1340,9 @@ def idv_exit_optional_questions( # @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton # @param [Boolean] in_person_verification_pending Profile is awaiting in person verificaiton # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. + # @param [Array,nil] profile_history Array of user's profiles (oldest to newest). # @see Reporting::IdentityVerificationReport#query This event is used by the identity verification # report. Changes here should be reflected there. # Tracks the last step of IDV, indicates the user successfully proofed @@ -1313,6 +1354,9 @@ def idv_final( in_person_verification_pending:, deactivation_reason: nil, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + profile_history: nil, **extra ) track_event( @@ -1324,26 +1368,47 @@ def idv_final( in_person_verification_pending: in_person_verification_pending, deactivation_reason: deactivation_reason, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, + profile_history: profile_history, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # User visited forgot password page - def idv_forgot_password(proofing_components: nil, **extra) + def idv_forgot_password( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: forgot password visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # User confirmed forgot password - def idv_forgot_password_confirmed(proofing_components: nil, **extra) + def idv_forgot_password_confirmed( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: forgot password confirmed', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -1473,6 +1538,8 @@ def idv_front_image_clicked( # and now in hours # @param [Integer] phone_step_attempts Number of attempts at phone step before requesting letter # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # GPO letter was enqueued and the time at which it was enqueued def idv_gpo_address_letter_enqueued( enqueued_at:, @@ -1481,6 +1548,8 @@ def idv_gpo_address_letter_enqueued( hours_since_first_letter:, phone_step_attempts:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -1491,6 +1560,8 @@ def idv_gpo_address_letter_enqueued( hours_since_first_letter: hours_since_first_letter, phone_step_attempts: phone_step_attempts, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -1501,6 +1572,8 @@ def idv_gpo_address_letter_enqueued( # and now in hours # @param [Integer] phone_step_attempts Number of attempts at phone step before requesting letter # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # GPO letter was requested def idv_gpo_address_letter_requested( resend:, @@ -1508,6 +1581,8 @@ def idv_gpo_address_letter_requested( hours_since_first_letter:, phone_step_attempts:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -1517,6 +1592,8 @@ def idv_gpo_address_letter_requested( hours_since_first_letter:, phone_step_attempts:, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -1962,12 +2039,18 @@ def idv_in_person_ready_to_verify_sp_link_clicked(**extra) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user visited the "ready to verify" page for the in person proofing flow def idv_in_person_ready_to_verify_visit(proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra) track_event( 'IdV: in person ready to verify visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2402,8 +2485,25 @@ def idv_in_person_usps_request_enroll_exception( end # User visits IdV - def idv_intro_visit(**extra) - track_event('IdV: intro visited', **extra) + # @param [Hash,nil] proofing_components User's proofing components. + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. + # @param [Array,nil] profile_history Array of user's profiles (oldest to newest). + def idv_intro_visit( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + profile_history: nil, + **extra + ) + track_event( + 'IdV: intro visited', + proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, + profile_history: profile_history, + **extra, + ) end # @param [String] enrollment_id @@ -2421,11 +2521,20 @@ def idv_ipp_deactivated_for_never_visiting_post_office( # The user visited the "letter enqueued" page shown during the verify by mail flow # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # @identity.idp.previous_event_name IdV: come back later visited - def idv_letter_enqueued_visit(proofing_components: nil, **extra) + def idv_letter_enqueued_visit( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: letter enqueued visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2518,12 +2627,22 @@ def idv_not_verified_visited(**extra) # key creation # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components # @param [boolean] checked whether the user checked or un-checked + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # the box with this click - def idv_personal_key_acknowledgment_toggled(checked:, proofing_components:, **extra) + def idv_personal_key_acknowledgment_toggled( + checked:, + proofing_components:, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: personal key acknowledgment toggled', checked: checked, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2531,10 +2650,19 @@ def idv_personal_key_acknowledgment_toggled(checked:, proofing_components:, **ex # A user has downloaded their personal key. This event is no longer emitted. # @identity.idp.previous_event_name IdV: download personal key # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_personal_key_downloaded(proofing_components: nil, **extra) + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. + def idv_personal_key_downloaded( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: personal key downloaded', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2544,6 +2672,8 @@ def idv_personal_key_downloaded(proofing_components: nil, **extra) # @param [Boolean] fraud_review_pending Profile is under review for fraud # @param [Boolean] fraud_rejection Profile is rejected due to fraud # @param [Boolean] in_person_verification_pending Profile is pending in-person verification + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # User submitted IDV personal key page def idv_personal_key_submitted( fraud_review_pending:, @@ -2551,6 +2681,8 @@ def idv_personal_key_submitted( in_person_verification_pending:, proofing_components: nil, deactivation_reason: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2560,16 +2692,27 @@ def idv_personal_key_submitted( fraud_review_pending: fraud_review_pending, fraud_rejection: fraud_rejection, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # User visited IDV personal key page - def idv_personal_key_visited(proofing_components: nil, **extra) + def idv_personal_key_visited( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: personal key visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2578,12 +2721,16 @@ def idv_personal_key_visited(proofing_components: nil, **extra) # @param [Hash] errors # @param ["sms", "voice"] otp_delivery_preference # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user submitted their phone on the phone confirmation page def idv_phone_confirmation_form_submitted( success:, otp_delivery_preference:, errors:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2592,36 +2739,65 @@ def idv_phone_confirmation_form_submitted( errors: errors, otp_delivery_preference: otp_delivery_preference, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user was rate limited for submitting too many OTPs during the IDV phone step - def idv_phone_confirmation_otp_rate_limit_attempts(proofing_components: nil, **extra) + def idv_phone_confirmation_otp_rate_limit_attempts( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'Idv: Phone OTP attempts rate limited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user was locked out for hitting the phone OTP rate limit during IDV - def idv_phone_confirmation_otp_rate_limit_locked_out(proofing_components: nil, **extra) + def idv_phone_confirmation_otp_rate_limit_locked_out( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'Idv: Phone OTP rate limited user', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user was rate limited for requesting too many OTPs during the IDV phone step - def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extra) + def idv_phone_confirmation_otp_rate_limit_sends( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'Idv: Phone OTP sends rate limited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2634,6 +2810,8 @@ def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extr # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt # @param [Hash] telephony_response response from Telephony gem # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user resent an OTP during the IDV phone step def idv_phone_confirmation_otp_resent( success:, @@ -2644,6 +2822,8 @@ def idv_phone_confirmation_otp_resent( rate_limit_exceeded:, telephony_response:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2656,6 +2836,8 @@ def idv_phone_confirmation_otp_resent( rate_limit_exceeded: rate_limit_exceeded, telephony_response: telephony_response, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2670,6 +2852,8 @@ def idv_phone_confirmation_otp_resent( # @param [Hash] telephony_response response from Telephony gem # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The user requested an OTP to confirm their phone during the IDV phone step def idv_phone_confirmation_otp_sent( success:, @@ -2682,6 +2866,8 @@ def idv_phone_confirmation_otp_sent( telephony_response:, adapter:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2696,6 +2882,8 @@ def idv_phone_confirmation_otp_sent( telephony_response: telephony_response, adapter: adapter, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2707,6 +2895,8 @@ def idv_phone_confirmation_otp_sent( # @param [Integer] second_factor_attempts_count number of attempts to confirm this phone # @param [Time, nil] second_factor_locked_at timestamp when the phone was locked out # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # When a user attempts to confirm posession of a new phone number during the IDV process def idv_phone_confirmation_otp_submitted( success:, @@ -2716,6 +2906,8 @@ def idv_phone_confirmation_otp_submitted( second_factor_attempts_count:, second_factor_locked_at:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2727,16 +2919,27 @@ def idv_phone_confirmation_otp_submitted( second_factor_attempts_count: second_factor_attempts_count, second_factor_locked_at: second_factor_locked_at, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # When a user visits the page to confirm posession of a new phone number during the IDV process - def idv_phone_confirmation_otp_visit(proofing_components: nil, **extra) + def idv_phone_confirmation_otp_visit( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: phone confirmation otp visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2744,11 +2947,15 @@ def idv_phone_confirmation_otp_visit(proofing_components: nil, **extra) # @param [Boolean] success # @param [Hash] errors # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The vendor finished the process of confirming the users phone def idv_phone_confirmation_vendor_submitted( success:, errors:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2756,6 +2963,8 @@ def idv_phone_confirmation_vendor_submitted( success: success, errors: errors, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2765,12 +2974,16 @@ def idv_phone_confirmation_vendor_submitted( # @param [Integer] remaining_submit_attempts number of submit attempts remaining # (previously called "remaining_attempts") # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # When a user gets an error during the phone finder flow of IDV def idv_phone_error_visited( type:, proofing_components: nil, limiter_expires_at: nil, remaining_submit_attempts: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2782,15 +2995,26 @@ def idv_phone_error_visited( remaining_submit_attempts: remaining_submit_attempts, **extra, }.compact, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # User visited idv phone of record - def idv_phone_of_record_visited(proofing_components: nil, **extra) + def idv_phone_of_record_visited( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: phone of record visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2800,12 +3024,16 @@ def idv_phone_of_record_visited(proofing_components: nil, **extra) # @param [Hash] errors # @param [Hash] error_details # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. def idv_phone_otp_delivery_selection_submitted( success:, otp_delivery_preference:, proofing_components: nil, errors: nil, error_details: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **extra ) track_event( @@ -2818,15 +3046,26 @@ def idv_phone_otp_delivery_selection_submitted( proofing_components: proofing_components, **extra, }.compact, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # User visited idv phone OTP delivery selection - def idv_phone_otp_delivery_selection_visit(proofing_components: nil, **extra) + def idv_phone_otp_delivery_selection_visit( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: Phone OTP delivery Selection Visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2845,21 +3084,42 @@ def idv_phone_use_different(step:, proofing_components: nil, **extra) # @identity.idp.previous_event_name IdV: Verify setup errors visited # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. + # @param [Array,nil] profile_history Array of user's profiles (oldest to newest). # Tracks when the user reaches the verify please call page after failing proofing - def idv_please_call_visited(proofing_components: nil, **extra) + def idv_please_call_visited( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + profile_history: nil, + **extra + ) track_event( 'IdV: Verify please call visited', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, + profile_history: profile_history, **extra, ) end # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # The system encountered an error and the proofing results are missing - def idv_proofing_resolution_result_missing(proofing_components: nil, **extra) + def idv_proofing_resolution_result_missing( + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + **extra + ) track_event( 'IdV: proofing resolution result missing', proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, **extra, ) end @@ -2993,6 +3253,9 @@ def idv_selfie_image_added( # @param [String] source # @param [String] use_alternate_sdk # @param [Boolean] liveness_checking_required + # @param [Hash,nil] proofing_components User's proofing components. + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. def idv_selfie_image_clicked( acuant_sdk_upgrade_a_b_testing_enabled:, acuant_version:, @@ -3001,6 +3264,9 @@ def idv_selfie_image_clicked( source:, use_alternate_sdk:, liveness_checking_required: nil, + proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, **_extra ) track_event( @@ -3012,6 +3278,9 @@ def idv_selfie_image_clicked( source: source, use_alternate_sdk: use_alternate_sdk, liveness_checking_required: liveness_checking_required, + proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, ) end # rubocop:enable Naming/VariableName,Naming/MethodParameterName @@ -3035,11 +3304,17 @@ def idv_session_error_visited( # @param [String] step # @param [String] location # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String,nil] active_profile_idv_level ID verification level of user's active profile. + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. + # @param [Array,nil] profile_history Array of user's profiles (oldest to newest). # User started over idv def idv_start_over( step:, location:, proofing_components: nil, + active_profile_idv_level: nil, + pending_profile_idv_level: nil, + profile_history: nil, **extra ) track_event( @@ -3047,6 +3322,9 @@ def idv_start_over( step: step, location: location, proofing_components: proofing_components, + active_profile_idv_level: active_profile_idv_level, + pending_profile_idv_level: pending_profile_idv_level, + profile_history: profile_history, **extra, ) end From e0a4eeceeda7f34337ff4076359e7b49b090cfa6 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Fri, 22 Mar 2024 16:51:34 -0700 Subject: [PATCH 06/11] Update analytics feature spec Sprinkle all these new args around a little. --- spec/features/idv/analytics_spec.rb | 108 ++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 15 deletions(-) diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index baff68f6e93..c0a46501b95 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -37,7 +37,11 @@ let(:happy_path_events) do { - 'IdV: intro visited' => {}, + 'IdV: intro visited' => { + active_profile_idv_level: nil, pending_profile_idv_level: nil, + profile_history: nil, + proofing_components: nil + }, 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, skip_hybrid_handoff: nil, lexisnexis_instant_verify_workflow_ab_test_bucket: :default }, @@ -96,49 +100,62 @@ }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp visited' => { - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: nil, pending_profile_idv_level: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, + profile_history: match_array(kind_of(Idv::ProfileLogging)), proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { address_verification_method: 'phone', encrypted_profiles_missing: false, in_person_verification_pending: false, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key acknowledgment toggled' => { checked: true, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, } @@ -146,7 +163,11 @@ let(:happy_hybrid_path_events) do { - 'IdV: intro visited' => {}, + 'IdV: intro visited' => { + active_profile_idv_level: nil, pending_profile_idv_level: nil, + profile_history: nil, + proofing_components: nil + }, 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, skip_hybrid_handoff: nil, lexisnexis_instant_verify_workflow_ab_test_bucket: :default }, @@ -205,49 +226,62 @@ }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: true, area_code: '202', country_code: 'US', phone_fingerprint: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp visited' => { - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: nil, pending_profile_idv_level: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, + profile_history: match_array(kind_of(Idv::ProfileLogging)), proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { address_verification_method: 'phone', encrypted_profiles_missing: false, in_person_verification_pending: false, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key acknowledgment toggled' => { checked: true, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, } @@ -255,7 +289,11 @@ let(:gpo_path_events) do { - 'IdV: intro visited' => {}, + 'IdV: intro visited' => { + active_profile_idv_level: nil, pending_profile_idv_level: nil, + profile_history: nil, + proofing_components: nil + }, 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, skip_hybrid_handoff: nil, lexisnexis_instant_verify_workflow_ab_test_bucket: :default }, @@ -311,10 +349,12 @@ }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: USPS address letter requested' => { resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: request letter visited' => { @@ -322,22 +362,29 @@ }, :idv_enter_password_visited => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, + # NOTE: pending_profile_idv_level should be set here, a nil value is cached for current_user.pending_profile. + active_profile_idv_level: nil, pending_profile_idv_level: nil, + profile_history: match_array(kind_of(Idv::ProfileLogging)), proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, 'IdV: letter enqueued visited' => { - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' }, + active_profile_idv_level: nil, pending_profile_idv_level: 'legacy_unsupervised', + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } }, } end @@ -420,51 +467,65 @@ }, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation otp visited' => { - proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' }, + active_profile_idv_level: nil, pending_profile_idv_level: nil, + proofing_components: { address_check: 'lexis_nexis_address', document_check: 'usps', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', source_check: 'aamva' } }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, address_verification_method: 'phone', + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: true, deactivation_reason: nil, + # NOTE: pending_profile_idv_level should be set here, a nil value is cached for current_user.pending_profile. + active_profile_idv_level: nil, pending_profile_idv_level: nil, + profile_history: match_array(kind_of(Idv::ProfileLogging)), proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { in_person_verification_pending: true, address_verification_method: 'phone', encrypted_profiles_missing: false, - proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: nil, pending_profile_idv_level: 'legacy_in_person', + proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key acknowledgment toggled' => { checked: true, - proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: nil, pending_profile_idv_level: 'legacy_in_person', + proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: true, deactivation_reason: nil, + active_profile_idv_level: nil, pending_profile_idv_level: 'legacy_in_person', proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: in person ready to verify visited' => { - proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: nil, pending_profile_idv_level: 'legacy_in_person', + proofing_components: { document_check: 'usps', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: user clicked what to bring link on ready to verify page' => {}, 'IdV: user clicked sp link on ready to verify page' => {}, @@ -473,7 +534,11 @@ let(:happy_mobile_selfie_path_events) do { - 'IdV: intro visited' => {}, + 'IdV: intro visited' => { + active_profile_idv_level: nil, pending_profile_idv_level: nil, + profile_history: nil, + proofing_components: nil + }, 'IdV: doc auth welcome visited' => { step: 'welcome', analytics_id: 'Doc Auth', irs_reproofing: false, skip_hybrid_handoff: anything, lexisnexis_instant_verify_workflow_ab_test_bucket: :default }, @@ -535,49 +600,62 @@ }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, otp_delivery_preference: 'sms', + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp visited' => { - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: nil, pending_profile_idv_level: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: phone confirmation otp submitted' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, code_expired: false, code_matches: true, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_visited => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, + active_profile_idv_level: nil, pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, lexisnexis_instant_verify_workflow_ab_test_bucket: :default, skip_hybrid_handoff: anything, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, + profile_history: match_array(kind_of(Idv::ProfileLogging)), proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key visited' => { address_verification_method: 'phone', in_person_verification_pending: false, encrypted_profiles_missing: false, + active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key acknowledgment toggled' => { checked: true, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, + active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, + proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, + active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } }, } From 4f204846fd69397f4247aa9b0d3e73c8783ff88c Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Tue, 12 Mar 2024 16:01:27 -0700 Subject: [PATCH 07/11] Lint that arguments added by AnalyticsEventsEnhancer are present in AnalyticsEvents Check each method on AnalyticsEvents that is "enhanced" by AnalyticsEventsEnhancer: 1. Includes the enhancements in its args list 2. Passes those to track_event --- .rubocop.yml | 1 + lib/linters/enhanced_idv_events_linter.rb | 199 ++++++++++++++ .../enhanced_idv_events_linter_spec.rb | 254 ++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 lib/linters/enhanced_idv_events_linter.rb create mode 100644 spec/lib/linters/enhanced_idv_events_linter_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index b11c5735c6f..5e31f451e91 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,7 @@ require: - rubocop-rspec - rubocop-performance - ./lib/linters/analytics_event_name_linter.rb + - ./lib/linters/enhanced_idv_events_linter.rb - ./lib/linters/localized_validation_message_linter.rb - ./lib/linters/image_size_linter.rb - ./lib/linters/mail_later_linter.rb diff --git a/lib/linters/enhanced_idv_events_linter.rb b/lib/linters/enhanced_idv_events_linter.rb new file mode 100644 index 00000000000..c94f2faa6b6 --- /dev/null +++ b/lib/linters/enhanced_idv_events_linter.rb @@ -0,0 +1,199 @@ +module RuboCop + module Cop + module IdentityIdp + class EnhancedIdvEventsLinter < RuboCop::Cop::Base + extend AutoCorrector + include MultilineElementLineBreaks + + RESTRICT_ON_SEND = [:track_event] + + ENHANCED_ARGS = [:proofing_components].freeze + + def on_send(track_event_send) + method = track_event_send.each_ancestor(:def).first + return if !should_check_method?(method.method_name) + + ENHANCED_ARGS.each do |arg_name| + check_arg_present_on_event_method(arg_name, method) + check_arg_present_in_track_event_send(arg_name, track_event_send) + end + end + + private + + def check_arg_present_in_track_event_send(arg_name, track_event_send) + # We expect there is a hash that includes arg_name + hash_arg = track_event_send.each_descendant.find do |node| + next unless node.hash_type? + node.pairs.any? { |pair| pair.key.type == :sym && pair.key.value == arg_name } + end + + return if hash_arg + + message = "#{arg_name} is missing from track_event call." + add_offense(track_event_send, message:) do |corrector| + correct_arg_missing_from_track_event_send(arg_name, track_event_send, corrector) + end + end + + def check_arg_present_on_event_method(arg_name, method) + arg = method.arguments.find { |a| a.name == arg_name } + return if arg + + add_offense(method, message: "Method is missing #{arg_name} argument.") do |corrector| + correct_arg_missing_from_event_method(arg_name, method, corrector) + end + end + + def correct_arg_missing_from_event_method(arg_name, method, corrector) + arg = method.arguments.find { |a| a.kwarg_type? && a.name == arg_name } + return if arg + + kwrest = method.arguments.find { |a| a.kwrestarg_type? } + return if !kwrest + + new_arg = "#{arg_name}: nil" + + if all_on_same_line?(method.arguments) + indent = indentation_for_node(method) + + proposed_line_length = [ + indent.length, + method.method_name.length + 2, + method.arguments.map { |arg| arg.source }.join(', ').length, + new_arg.length + 1, + ].sum + + if proposed_line_length > max_line_length + make_method_args_multiline(corrector, method) + corrector.insert_before(kwrest, "\n#{indent} #{new_arg},") + else + corrector.insert_before(kwrest, "#{new_arg}, ") + end + else + indent = indentation_for_node(kwrest) + corrector.insert_before(kwrest, "#{new_arg},\n#{indent}") + end + end + + def correct_arg_missing_from_track_event_send(arg_name, track_event_send, corrector) + hash_arg = track_event_send.arguments.find { |arg| arg.hash_type? } + return unless hash_arg + + return if hash_arg.pairs.any? { |pair| pair.key.value == arg_name } + + # Put the reference into the hash, before the splat + kwsplat = hash_arg.children.find { |child| child.kwsplat_type? } + return unless kwsplat + + new_arg = "#{arg_name}: #{arg_name}," + if all_on_same_line?(track_event_send.arguments) + indent = indentation_for_node(track_event_send) + + # We might need to convert this to a multi-line invocation + proposed_line_length = [ + indent.length, + track_event_send.method_name.length + 2, + track_event_send.arguments.map { |arg| arg.source }.join(', ').length, + new_arg.length + 1, + ].sum + + if proposed_line_length > max_line_length + make_send_multiline(corrector, track_event_send) + corrector.insert_before(kwsplat, "\n#{indent} #{new_arg}") + else + corrector.insert_before(kwsplat, "#{new_arg} ") + end + else + # Assume that we are doing 1 arg per line + indent = indentation_for_node(kwsplat) + corrector.insert_before(kwsplat, "#{new_arg}\n#{indent}") + end + end + + def events_enhancer_class + # Idv::AnalyticsEventsEnhancer keeps its own record of which + # methods it wants to "enhance". + + # Force the class to be reloaded (this supports LSP-type use cases + # where the rubocop process may be long-lived.) + idv = begin + Object.const_get(:Idv) + rescue + nil + end + + idv&.send(:remove_const, 'AnalyticsEventsEnhancer') + + file = File.expand_path( + '../../app/services/idv/analytics_events_enhancer.rb', + __dir__, + ) + + load(file) + + ::Idv::AnalyticsEventsEnhancer + end + + def make_method_args_multiline(corrector, method) + indent = indentation_for_node(method) + arg_indent = "\n#{indent} " + paren_indent = "\n#{indent}" + + method.arguments.each do |arg| + remove_whitespace_before(arg, corrector) + corrector.insert_before(arg, arg_indent) + end + + corrector.insert_before( + method.arguments.source_range.with( + begin_pos: method.arguments.source_range.end_pos - 1, + ), + paren_indent, + ) + end + + def make_send_multiline(corrector, send) + indent = indentation_for_node(send) + arg_indent = "\n#{indent} " + paren_indent = "\n#{indent}" + + send.arguments.each do |arg| + if arg.hash_type? + arg.children.each do |child| + remove_whitespace_before(child, corrector) + corrector.insert_before(child, arg_indent) + end + else + remove_whitespace_before(arg, corrector) + corrector.insert_before(arg, arg_indent) + end + end + + corrector.insert_before(send.loc.end, paren_indent) + end + + def indentation_for_node(node) + source_line = processed_source.lines[node.loc.line - 1] + /^(?\s*)/.match(source_line)[:indentation] + end + + def max_line_length + config.for_cop('Layout/LineLength')['Max'] || 100 + end + + def remove_whitespace_before(node, corrector) + char_before = node.source_range.with( + begin_pos: node.source_range.begin_pos - 1, + end_pos: node.source_range.begin_pos, + ) + corrector.remove_preceding(node, 1) if /\s/.match?(char_before.source) + end + + def should_check_method?(method_name) + events_enhancer_class&.should_enhance_method?(method_name) + end + end + end + end +end diff --git a/spec/lib/linters/enhanced_idv_events_linter_spec.rb b/spec/lib/linters/enhanced_idv_events_linter_spec.rb new file mode 100644 index 00000000000..a2c487ccc09 --- /dev/null +++ b/spec/lib/linters/enhanced_idv_events_linter_spec.rb @@ -0,0 +1,254 @@ +require 'rubocop' +require 'rubocop/rspec/cop_helper' +require 'rubocop/rspec/expect_offense' + +require_relative '../../../lib/linters/enhanced_idv_events_linter' + +RSpec.describe RuboCop::Cop::IdentityIdp::EnhancedIdvEventsLinter do + include CopHelper + include RuboCop::RSpec::ExpectOffense + + let(:config) { RuboCop::Config.new } + let(:cop) { RuboCop::Cop::IdentityIdp::EnhancedIdvEventsLinter.new(config) } + + it 'does not register an offense for non-idv methods' do + expect_no_offenses(<<~RUBY) + module AnalyticsEvents + def a_non_idv_method + track_event(:a_non_idv_method) + end + end + RUBY + end + + it 'does not register an offense for ignored methods' do + expect_no_offenses(<<~RUBY) + module AnalyticsEvents + def idv_acuant_sdk_loaded + track_event(:idv_acuant_sdk_loaded) + end + end + RUBY + end + + it 'registers an offense when an idv_ is missing proofing_components' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method + ^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event(:idv_my_method) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + end + + it 'registers an offense when an idv_ method is missing proofing_components with **extra' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method(**extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event(:idv_my_method, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method(proofing_components: nil, **extra) + track_event(:idv_my_method, proofing_components: proofing_components, **extra) + end + end + RUBY + end + + it 'registers offense when idv_ method missing proofing_components with **extra and other args' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method(other:, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event(:idv_my_method, other: other, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method(other:, proofing_components: nil, **extra) + track_event(:idv_my_method, other: other, proofing_components: proofing_components, **extra) + end + end + RUBY + end + + it 'does not register an offense when an proofing_components present on track_event call' do + expect_no_offenses(<<~RUBY) + module AnalyticsEvents + def idv_my_method(proofing_components: nil, **extra) + track_event(:idv_my_method, proofing_components: proofing_components, **extra) + end + end + RUBY + end + + it 'registers an offense when an proofing_components is missing from track_event call' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method(proofing_components: nil, **extra) + track_event(:idv_my_method, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method(proofing_components: nil, **extra) + track_event(:idv_my_method, proofing_components: proofing_components, **extra) + end + end + RUBY + end + + it 'can put the track_event arg on its own line if needed' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method(proofing_components: nil, **extra) + track_event( + ^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + :idv_my_method, + **extra, + ) + end + end + RUBY + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method(proofing_components: nil, **extra) + track_event( + :idv_my_method, + proofing_components: proofing_components, + **extra, + ) + end + end + RUBY + end + + it 'can put the method arg on its own line if needed' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method( + ^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + foo:, + bar:, + **extra + ) + track_event(:idv_my_method, proofing_components: proofing_components, **extra) + end + end + RUBY + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method( + foo:, + bar:, + proofing_components: nil, + **extra + ) + track_event(:idv_my_method, proofing_components: proofing_components, **extra) + end + end + RUBY + end + + it 'handles track_event calls with a hash arg' do + expect_no_offenses(<<~RUBY) + module AnalyticsEvents + def idv_my_method( + type:, + proofing_components: nil, + limiter_expires_at: nil, + remaining_submit_attempts: nil, + **extra + ) + track_event( + 'IdV: phone error visited', + { + type: type, + proofing_components: proofing_components, + limiter_expires_at: limiter_expires_at, + remaining_submit_attempts: remaining_submit_attempts, + **extra, + }.compact, + ) + end + end + RUBY + end + + it 'breaks track_event calls across lines' do + allow(cop).to receive(:max_line_length).and_return(100) + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method_that_is_really_really_long(**extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event(:idv_my_method_that_is_really_really_long, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method_that_is_really_really_long(proofing_components: nil, **extra) + track_event( + :idv_my_method_that_is_really_really_long, + proofing_components: proofing_components, + **extra + ) + end + end + RUBY + end + + it 'can break method definitions across lines' do + allow(cop).to receive(:max_line_length).and_return(100) + + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method_that_is_really_really_long(step_name:, remaining_submit_attempts:, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event( + ^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + :idv_my_method_that_is_really_really_long, + step_name: step_name, + remaining_submit_attempts: remaining_submit_attempts, + **extra, + ) + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method_that_is_really_really_long( + step_name:, + remaining_submit_attempts:, + proofing_components: nil, + **extra + ) + track_event( + :idv_my_method_that_is_really_really_long, + step_name: step_name, + remaining_submit_attempts: remaining_submit_attempts, + proofing_components: proofing_components, + **extra, + ) + end + end + RUBY + end +end From 6c08eaff7329985b668b6e2ed71e963488561abc Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 14 Mar 2024 10:54:55 -0700 Subject: [PATCH 08/11] Check that "enhanced" event args have @param docs Look for "@param" documentation for e.g. proofing_components. Autocorrect to a "TODO" --- lib/linters/enhanced_idv_events_linter.rb | 37 ++++++++++++ .../enhanced_idv_events_linter_spec.rb | 59 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/lib/linters/enhanced_idv_events_linter.rb b/lib/linters/enhanced_idv_events_linter.rb index c94f2faa6b6..4cfb8f9a3bf 100644 --- a/lib/linters/enhanced_idv_events_linter.rb +++ b/lib/linters/enhanced_idv_events_linter.rb @@ -16,11 +16,44 @@ def on_send(track_event_send) ENHANCED_ARGS.each do |arg_name| check_arg_present_on_event_method(arg_name, method) check_arg_present_in_track_event_send(arg_name, track_event_send) + check_arg_has_docs(arg_name, method) end end private + def check_arg_has_docs(arg_name, method) + pattern = Regexp.new("# @param \\[.+\\] #{arg_name}") + has_docs = preceding_lines(method).any? do |line| + line.is_a?(Parser::Source::Comment) && pattern.match?(line.text) + end + + return if has_docs + + add_offense( + method, + message: "Missing @param documentation comment for #{arg_name}", + ) do |corrector| + last_param_line = preceding_lines(method).reverse.find do |line| + line.is_a?(Parser::Source::Comment) && /@param/.match?(line.text) + end + + comment = "# @param [Object] #{arg_name} TODO: Write doc comment" + indent = indentation_for_node(method) + if last_param_line + corrector.insert_after( + last_param_line, + "\n#{indent}#{comment}", + ) + else + corrector.insert_before( + method, + "#{comment}\n#{indent}", + ) + end + end + end + def check_arg_present_in_track_event_send(arg_name, track_event_send) # We expect there is a hash that includes arg_name hash_arg = track_event_send.each_descendant.find do |node| @@ -182,6 +215,10 @@ def max_line_length config.for_cop('Layout/LineLength')['Max'] || 100 end + def preceding_lines(node) + processed_source.ast_with_comments[node].select { |line| line.loc.line < node.loc.line } + end + def remove_whitespace_before(node, corrector) char_before = node.source_range.with( begin_pos: node.source_range.begin_pos - 1, diff --git a/spec/lib/linters/enhanced_idv_events_linter_spec.rb b/spec/lib/linters/enhanced_idv_events_linter_spec.rb index a2c487ccc09..c2dd1b45535 100644 --- a/spec/lib/linters/enhanced_idv_events_linter_spec.rb +++ b/spec/lib/linters/enhanced_idv_events_linter_spec.rb @@ -10,6 +10,15 @@ let(:config) { RuboCop::Config.new } let(:cop) { RuboCop::Cop::IdentityIdp::EnhancedIdvEventsLinter.new(config) } + let(:check_param_docs) { false } + + before do + if !check_param_docs + # Most of these tests don't involve the @param doc linter, so + # unwire it here. + allow(cop).to receive(:check_arg_has_docs).and_return(nil) + end + end it 'does not register an offense for non-idv methods' do expect_no_offenses(<<~RUBY) @@ -251,4 +260,54 @@ def idv_my_method_that_is_really_really_long( end RUBY end + + describe 'parameter documentation' do + let(:check_param_docs) { true } + + it 'can add parameter documentation for proofing_components' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method(other:, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event(:idv_my_method, other: other, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + # @param [Object] proofing_components TODO: Write doc comment + def idv_my_method(other:, proofing_components: nil, **extra) + track_event(:idv_my_method, other: other, proofing_components: proofing_components, **extra) + end + end + RUBY + end + + it 'can add parameter documentation for proofing_components with param docs already there' do + expect_offense(<<~RUBY) + module AnalyticsEvents + # @param [String] other Some param + # Logs an event for my method + def idv_my_method(other:, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + track_event(:idv_my_method, other: other, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + # @param [String] other Some param + # @param [Object] proofing_components TODO: Write doc comment + # Logs an event for my method + def idv_my_method(other:, proofing_components: nil, **extra) + track_event(:idv_my_method, other: other, proofing_components: proofing_components, **extra) + end + end + RUBY + end + end end From 04ff22213d40a2c847082297a57e83a9cb35c531 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 20 Mar 2024 16:12:26 -0700 Subject: [PATCH 09/11] Handle cases where **extra not passed to track_event --- lib/linters/enhanced_idv_events_linter.rb | 130 +++++++++++++----- .../enhanced_idv_events_linter_spec.rb | 44 ++++++ 2 files changed, 141 insertions(+), 33 deletions(-) diff --git a/lib/linters/enhanced_idv_events_linter.rb b/lib/linters/enhanced_idv_events_linter.rb index 4cfb8f9a3bf..6dc4f9f5b06 100644 --- a/lib/linters/enhanced_idv_events_linter.rb +++ b/lib/linters/enhanced_idv_events_linter.rb @@ -7,7 +7,12 @@ class EnhancedIdvEventsLinter < RuboCop::Cop::Base RESTRICT_ON_SEND = [:track_event] - ENHANCED_ARGS = [:proofing_components].freeze + ENHANCED_ARGS = [ + :proofing_components, + :active_profile_idv_level, + :pending_profile_idv_level, + :profile_history, + ].freeze def on_send(track_event_send) method = track_event_send.each_ancestor(:def).first @@ -22,6 +27,71 @@ def on_send(track_event_send) private + def add_argument_to_send( + arg_name:, + arg_value:, + corrector:, + send: + ) + hash_arg = send.arguments.find { |arg| arg.hash_type? } + return if hash_arg&.pairs&.any? { |pair| pair.key.value == arg_name } + + # Put the reference into the hash, before the splat + # If there is no splat, add it to the end of the arg list + kwsplat = hash_arg&.children&.find { |child| child.kwsplat_type? } + do_insert = nil + + arg_and_value = "#{arg_name}: #{arg_value}" + + if kwsplat + do_insert = ->(on_new_line:) { + newline_before = whitespace_before(kwsplat).include?("\n") + + to_insert = if on_new_line && newline_before + "#{arg_and_value},\n#{indentation_for_node(send)} " + elsif on_new_line + "\n#{indentation_for_node(send)} #{arg_and_value}," + else + "#{arg_and_value}, " + end + + corrector.insert_before(kwsplat, to_insert) + } + else + last_arg = send.arguments.last + do_insert = ->(on_new_line:) { + to_insert = if on_new_line + ",\n#{indentation_for_node(send)} #{arg_and_value}" + else + ", #{arg_and_value}" + end + + corrector.insert_after(last_arg, to_insert) + } + end + + if all_on_same_line?(send.arguments) + indent = indentation_for_node(send) + + # We might need to convert this to a multi-line invocation + proposed_line_length = [ + indent.length, + send.method_name.length + 2, # method() + send.arguments.map { |arg| arg.source }.join(', ').length, + arg_and_value.length + 1, # arg: value, + ].sum + + if proposed_line_length > max_line_length + make_send_multiline(corrector, send) + do_insert.call(on_new_line: true) + else + do_insert.call(on_new_line: false) + end + else + do_insert.call(on_new_line: true) + end + end + def check_arg_has_docs(arg_name, method) pattern = Regexp.new("# @param \\[.+\\] #{arg_name}") has_docs = preceding_lines(method).any? do |line| @@ -110,38 +180,12 @@ def correct_arg_missing_from_event_method(arg_name, method, corrector) end def correct_arg_missing_from_track_event_send(arg_name, track_event_send, corrector) - hash_arg = track_event_send.arguments.find { |arg| arg.hash_type? } - return unless hash_arg - - return if hash_arg.pairs.any? { |pair| pair.key.value == arg_name } - - # Put the reference into the hash, before the splat - kwsplat = hash_arg.children.find { |child| child.kwsplat_type? } - return unless kwsplat - - new_arg = "#{arg_name}: #{arg_name}," - if all_on_same_line?(track_event_send.arguments) - indent = indentation_for_node(track_event_send) - - # We might need to convert this to a multi-line invocation - proposed_line_length = [ - indent.length, - track_event_send.method_name.length + 2, - track_event_send.arguments.map { |arg| arg.source }.join(', ').length, - new_arg.length + 1, - ].sum - - if proposed_line_length > max_line_length - make_send_multiline(corrector, track_event_send) - corrector.insert_before(kwsplat, "\n#{indent} #{new_arg}") - else - corrector.insert_before(kwsplat, "#{new_arg} ") - end - else - # Assume that we are doing 1 arg per line - indent = indentation_for_node(kwsplat) - corrector.insert_before(kwsplat, "#{new_arg}\n#{indent}") - end + add_argument_to_send( + arg_name:, + arg_value: arg_name, + send: track_event_send, + corrector:, + ) end def events_enhancer_class @@ -230,6 +274,26 @@ def remove_whitespace_before(node, corrector) def should_check_method?(method_name) events_enhancer_class&.should_enhance_method?(method_name) end + + def whitespace_before(node) + len = 1 + result = '' + + loop do + begin_pos = node.source_range.begin_pos - len + end_pos = node.source_range.begin_pos + + return result if begin_pos < 0 + + range = node.source_range.with(begin_pos:, end_pos:) + + next_result = range.source + return result if /[^\s]/.match?(next_result) + + result = next_result + len += 1 + end + end end end end diff --git a/spec/lib/linters/enhanced_idv_events_linter_spec.rb b/spec/lib/linters/enhanced_idv_events_linter_spec.rb index c2dd1b45535..6a4635f98f8 100644 --- a/spec/lib/linters/enhanced_idv_events_linter_spec.rb +++ b/spec/lib/linters/enhanced_idv_events_linter_spec.rb @@ -18,6 +18,8 @@ # unwire it here. allow(cop).to receive(:check_arg_has_docs).and_return(nil) end + + stub_const("#{described_class.name}::ENHANCED_ARGS", [:proofing_components]) end it 'does not register an offense for non-idv methods' do @@ -198,6 +200,48 @@ def idv_my_method( RUBY end + it 'handles track_event calls without **extra' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_my_method( + ^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing proofing_components argument. + type:, + limiter_expires_at: nil, + remaining_submit_attempts: nil, + **_extra + ) + track_event( + ^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: proofing_components is missing from track_event call. + 'IdV: phone error visited', + type: type, + limiter_expires_at: limiter_expires_at, + remaining_submit_attempts: remaining_submit_attempts, + ) + end + end + RUBY + + expect_correction(<<~RUBY) + module AnalyticsEvents + def idv_my_method( + type:, + limiter_expires_at: nil, + remaining_submit_attempts: nil, + proofing_components: nil, + **_extra + ) + track_event( + 'IdV: phone error visited', + type: type, + limiter_expires_at: limiter_expires_at, + remaining_submit_attempts: remaining_submit_attempts, + proofing_components: proofing_components, + ) + end + end + RUBY + end + it 'breaks track_event calls across lines' do allow(cop).to receive(:max_line_length).and_return(100) expect_offense(<<~RUBY) From e36d7c72def65989d2175ef3a6b72fe474c2d994 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Wed, 13 Mar 2024 12:16:12 -0700 Subject: [PATCH 10/11] Turn on the EnhancedIdvEventsLinter --- .rubocop.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 5e31f451e91..31214bea194 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -62,6 +62,11 @@ IdentityIdp/UrlOptionsLinter: Exclude: - 'spec/**/*.rb' +IdentityIdp/EnhancedIdvEventsLinter: + Enabled: true + Include: + - 'app/services/analytics_events.rb' + IdentityIdp/ErrorsAddLinter: Enabled: true Exclude: From dabec7bb8decd96fdcd31f00702e272f5b7d267c Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Fri, 22 Mar 2024 13:28:08 -0700 Subject: [PATCH 11/11] Update how linter sources args for a method Delegate entirely Idv::AnalyticsEventsEnhancer rather than duplicating arg lists --- lib/linters/enhanced_idv_events_linter.rb | 19 ++---- .../enhanced_idv_events_linter_spec.rb | 63 ++++++++++++------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/lib/linters/enhanced_idv_events_linter.rb b/lib/linters/enhanced_idv_events_linter.rb index 6dc4f9f5b06..6b815e207e3 100644 --- a/lib/linters/enhanced_idv_events_linter.rb +++ b/lib/linters/enhanced_idv_events_linter.rb @@ -7,18 +7,11 @@ class EnhancedIdvEventsLinter < RuboCop::Cop::Base RESTRICT_ON_SEND = [:track_event] - ENHANCED_ARGS = [ - :proofing_components, - :active_profile_idv_level, - :pending_profile_idv_level, - :profile_history, - ].freeze - def on_send(track_event_send) method = track_event_send.each_ancestor(:def).first - return if !should_check_method?(method.method_name) + args = extra_args_for_method(method.method_name) - ENHANCED_ARGS.each do |arg_name| + args.each do |arg_name| check_arg_present_on_event_method(arg_name, method) check_arg_present_in_track_event_send(arg_name, track_event_send) check_arg_has_docs(arg_name, method) @@ -212,6 +205,10 @@ def events_enhancer_class ::Idv::AnalyticsEventsEnhancer end + def extra_args_for_method(method_name) + events_enhancer_class.extra_args_for_method(method_name) + end + def make_method_args_multiline(corrector, method) indent = indentation_for_node(method) arg_indent = "\n#{indent} " @@ -271,10 +268,6 @@ def remove_whitespace_before(node, corrector) corrector.remove_preceding(node, 1) if /\s/.match?(char_before.source) end - def should_check_method?(method_name) - events_enhancer_class&.should_enhance_method?(method_name) - end - def whitespace_before(node) len = 1 result = '' diff --git a/spec/lib/linters/enhanced_idv_events_linter_spec.rb b/spec/lib/linters/enhanced_idv_events_linter_spec.rb index 6a4635f98f8..5ff4029a5e0 100644 --- a/spec/lib/linters/enhanced_idv_events_linter_spec.rb +++ b/spec/lib/linters/enhanced_idv_events_linter_spec.rb @@ -19,27 +19,11 @@ allow(cop).to receive(:check_arg_has_docs).and_return(nil) end - stub_const("#{described_class.name}::ENHANCED_ARGS", [:proofing_components]) - end - - it 'does not register an offense for non-idv methods' do - expect_no_offenses(<<~RUBY) - module AnalyticsEvents - def a_non_idv_method - track_event(:a_non_idv_method) - end - end - RUBY - end - - it 'does not register an offense for ignored methods' do - expect_no_offenses(<<~RUBY) - module AnalyticsEvents - def idv_acuant_sdk_loaded - track_event(:idv_acuant_sdk_loaded) - end - end - RUBY + allow(cop).to receive(:extra_args_for_method).and_return( + [ + :proofing_components, + ], + ) end it 'registers an offense when an idv_ is missing proofing_components' do @@ -354,4 +338,41 @@ def idv_my_method(other:, proofing_components: nil, **extra) RUBY end end + + describe 'profile_history' do + context 'method should not receive profile_history' do + it 'registers no offense' do + expect_no_offenses(<<~RUBY) + module AnalyticsEvents + def idv_final(proofing_components:, **extra) + track_event(:idv_final, proofing_components: proofing_components, **extra) + end + end + RUBY + end + end + + context 'method should receive profile_history' do + before do + allow(cop).to receive(:extra_args_for_method).and_return( + %i[ + proofing_components + profile_history + ], + ) + end + + it 'registers offence when method that should receive profile_history does not' do + expect_offense(<<~RUBY) + module AnalyticsEvents + def idv_final(proofing_components:, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: Method is missing profile_history argument. + track_event(:idv_final, proofing_components: proofing_components, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/EnhancedIdvEventsLinter: profile_history is missing from track_event call. + end + end + RUBY + end + end + end end