diff --git a/app/services/analytics.rb b/app/services/analytics.rb index bd7d1e2459a..4016a43c446 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -4,14 +4,14 @@ class Analytics include AnalyticsEvents prepend Idv::AnalyticsEventsEnhancer - attr_reader :user, :request, :sp, :ahoy + attr_reader :user, :request, :sp, :session, :ahoy def initialize(user:, request:, sp:, session:, ahoy: nil) @user = user @request = request @sp = sp - @ahoy = ahoy || Ahoy::Tracker.new(request: request) @session = session + @ahoy = ahoy || Ahoy::Tracker.new(request: request) end def track_event(event, attributes = {}) @@ -27,6 +27,7 @@ def track_event(event, attributes = {}) } analytics_hash.merge!(request_attributes) if request + analytics_hash.merge!(sp_request_attributes) if sp_request_attributes ahoy.track(event, analytics_hash) @@ -42,13 +43,13 @@ def track_event(event, attributes = {}) end def update_session_events_and_paths_visited_for_analytics(event) - @session[:events] ||= {} - @session[:first_event] = !@session[:events].key?(event) - @session[:events][event] = true + session[:events] ||= {} + session[:first_event] = !@session[:events].key?(event) + session[:events][event] = true end def first_event_this_session? - @session[:first_event] + session[:first_event] end def track_mfa_submit_event(attributes) @@ -95,12 +96,43 @@ def browser_attributes end def session_duration - @session[:session_started_at].present? ? Time.zone.now - session_started_at : nil + session[:session_started_at].present? ? Time.zone.now - session_started_at : nil end def session_started_at - value = @session[:session_started_at] + value = session[:session_started_at] return value unless value.is_a?(String) Time.zone.parse(value) end + + def sp_request_attributes + resolved_result = resolved_authn_context_result + return if resolved_result.nil? + + attributes = resolved_result.to_h + attributes[:component_values] = resolved_result.component_values.map do |v| + [v.name.sub('http://idmanagement.gov/ns/assurance/', ''), true] + end.to_h + attributes.reject! { |_key, value| value == false } + attributes.transform_keys! do |key| + key.to_s.chomp('?').to_sym + end + + { sp_request: attributes } + end + + def resolved_authn_context_result + return nil if sp.nil? || session[:sp].blank? + return @resolved_authn_context_result if defined?(@resolved_authn_context_result) + + service_provider = ServiceProvider.find_by(issuer: sp) + + @resolved_authn_context_result = AuthnContextResolver.new( + service_provider:, + vtr: session[:sp][:vtr], + acr_values: session[:sp][:acr_values], + ).resolve + rescue Vot::Parser::ParseException + return + end end diff --git a/spec/services/analytics_spec.rb b/spec/services/analytics_spec.rb index af9b0a66ad8..427ee4e80d3 100644 --- a/spec/services/analytics_spec.rb +++ b/spec/services/analytics_spec.rb @@ -37,13 +37,14 @@ let(:request) { FakeRequest.new } let(:path) { 'fake_path' } let(:success_state) { 'GET|fake_path|Trackable Event' } + let(:session) { {} } subject(:analytics) do Analytics.new( user: current_user, request: request, sp: 'http://localhost:3000', - session: {}, + session: session, ahoy: ahoy, ) end @@ -253,4 +254,114 @@ 'DocumentName' => 'some_name', ) end + + context 'with an SP request vtr saved in the session' do + context 'identity verified' do + let(:session) { { sp: { vtr: ['C1.P1'] } } } + let(:expected_attributes) do + { + sp_request: { + aal2: true, + component_values: { 'C1' => true, 'C2' => true, 'P1' => true }, + identity_proofing: true, + }, + } + end + + it 'includes the sp_request' do + expect(ahoy).to receive(:track). + with('Trackable Event', hash_including(expected_attributes)) + + analytics.track_event('Trackable Event') + end + end + + context 'phishing resistant and requiring biometric comparison' do + let(:session) { { sp: { vtr: ['Ca.Pb'] } } } + let(:expected_attributes) do + { + sp_request: { + aal2: true, + biometric_comparison: true, + component_values: { + 'C1' => true, + 'C2' => true, + 'Ca' => true, + 'P1' => true, + 'Pb' => true, + }, + identity_proofing: true, + phishing_resistant: true, + }, + } + end + + it 'includes the sp_request' do + expect(ahoy).to receive(:track). + with('Trackable Event', hash_including(expected_attributes)) + + analytics.track_event('Trackable Event') + end + end + end + + context 'with SP request acr_values saved in the session' do + context 'legacy IAL1' do + let(:session) { { sp: { acr_values: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF } } } + let(:expected_attributes) do + { + sp_request: { + component_values: { 'ial/1' => true }, + }, + } + end + + it 'includes the sp_request' do + expect(ahoy).to receive(:track). + with('Trackable Event', hash_including(expected_attributes)) + + analytics.track_event('Trackable Event') + end + end + + context 'legacy IAL2' do + let(:session) { { sp: { acr_values: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } } } + let(:expected_attributes) do + { + sp_request: { + aal2: true, + component_values: { 'ial/2' => true }, + identity_proofing: true, + }, + } + end + + it 'includes the sp_request' do + expect(ahoy).to receive(:track). + with('Trackable Event', hash_including(expected_attributes)) + + analytics.track_event('Trackable Event') + end + end + + context 'legacy IALMAX' do + let(:session) { { sp: { acr_values: Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } } } + let(:expected_attributes) do + { + sp_request: { + aal2: true, + component_values: { 'ial/0' => true }, + ialmax: true, + }, + } + end + + it 'includes the sp_request' do + expect(ahoy).to receive(:track). + with('Trackable Event', hash_including(expected_attributes)) + + analytics.track_event('Trackable Event') + end + end + end end diff --git a/spec/services/idv/analytics_events_enhancer_spec.rb b/spec/services/idv/analytics_events_enhancer_spec.rb index 9011b3f6e91..bb0d6b19819 100644 --- a/spec/services/idv/analytics_events_enhancer_spec.rb +++ b/spec/services/idv/analytics_events_enhancer_spec.rb @@ -2,6 +2,8 @@ RSpec.describe Idv::AnalyticsEventsEnhancer do let(:user) { build(:user) } + let(:sp) { nil } + let(:session) { nil } let(:analytics_class) do Class.new(FakeAnalytics) do include AnalyticsEvents @@ -13,12 +15,14 @@ def idv_final(**kwargs) attr_reader :user, :called_kwargs - def initialize(user:) + def initialize(user:, sp:, session:) @user = user + @sp = sp + @session = session end end end - let(:analytics) { analytics_class.new(user: user) } + let(:analytics) { analytics_class.new(user: user, sp: sp, session: session) } it 'includes decorated methods' do expect(analytics.methods).to include(*described_class::DECORATED_METHODS) diff --git a/spec/support/fake_analytics.rb b/spec/support/fake_analytics.rb index 60c18091454..0ada99ed9d9 100644 --- a/spec/support/fake_analytics.rb +++ b/spec/support/fake_analytics.rb @@ -144,9 +144,11 @@ def option_param_names(instance_method) attr_reader :events attr_accessor :user - def initialize(user: AnonymousUser.new) + def initialize(user: AnonymousUser.new, sp: nil, session: nil) @events = Hash.new @user = user + @sp = sp + @session = session end def track_event(event, attributes = {})