-
Notifications
You must be signed in to change notification settings - Fork 166
LG-15394 Add tooling for an SP proofing events by UUID report #11787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
6b2d5bb
LG-15394 Add tooling for an SP proofing events by UUID report
jmhooper ac224f6
Test cleanup
jmhooper 6247883
tweak wording
jmhooper 106ab13
Modify the job to use a JSON config
jmhooper b337d20
Add the job to the schedules with an empty default config
jmhooper 414d1b9
remove command line options
jmhooper c61063d
get events after the first 10k
jmhooper a6aad0b
Batch lookup agency UUIDs
jmhooper 40d7b05
fix a CW syntax error
jmhooper File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'reporting/sp_proofing_events_by_uuid' | ||
|
|
||
| module Reports | ||
| class SpProofingEventsByUuid < BaseReport | ||
| attr_accessor :report_date | ||
|
|
||
| def perform(report_date) | ||
| return unless IdentityConfig.store.s3_reports_enabled | ||
|
|
||
| self.report_date = report_date | ||
|
|
||
| IdentityConfig.store.sp_proofing_events_by_uuid_report_configs.each do |report_config| | ||
| send_report(report_config) | ||
| end | ||
| end | ||
|
|
||
| def send_report(report_config) | ||
| return unless IdentityConfig.store.s3_reports_enabled | ||
| issuers = report_config['issuers'] | ||
| agency_abbreviation = report_config['agency_abbreviation'] | ||
| emails = report_config['emails'] | ||
|
|
||
| agency_report_nane = "#{agency_abbreviation.downcase}_proofing_events_by_uuid" | ||
| agency_report_title = "#{agency_abbreviation} Proofing Events By UUID" | ||
|
|
||
| report_maker = build_report_maker( | ||
| issuers:, | ||
| agency_abbreviation:, | ||
| time_range: report_date.to_date.weeks_ago(1).all_week(:sunday), | ||
| ) | ||
|
|
||
| csv = report_maker.to_csv | ||
|
|
||
| save_report(agency_report_nane, csv, extension: 'csv') | ||
|
|
||
| if emails.blank? | ||
| Rails.logger.warn "No email addresses received - #{agency_report_title} NOT SENT" | ||
| return false | ||
| end | ||
|
|
||
| email_message = <<~HTML.html_safe # rubocop:disable Rails/OutputSafety | ||
| <h2>#{agency_report_title}</h2> | ||
| HTML | ||
|
|
||
| emails.each do |email| | ||
| ReportMailer.tables_report( | ||
| email: email, | ||
| subject: "#{agency_report_title} - #{report_date.to_date}", | ||
| reports: report_maker.as_emailable_reports, | ||
| message: email_message, | ||
| attachment_format: :csv, | ||
| ).deliver_now | ||
| end | ||
| end | ||
|
|
||
| def build_report_maker(issuers:, agency_abbreviation:, time_range:) | ||
| Reporting::SpProofingEventsByUuid.new(issuers:, agency_abbreviation:, time_range:) | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'reporting/cloudwatch_client' | ||
| require 'reporting/cloudwatch_query_quoting' | ||
|
|
||
| module Reporting | ||
| class SpProofingEventsByUuid | ||
| attr_reader :issuers, :agency_abbreviation, :time_range | ||
|
|
||
| def initialize( | ||
| issuers:, | ||
| agency_abbreviation:, | ||
| time_range:, | ||
| verbose: false, | ||
| progress: false, | ||
| cloudwatch_client: nil | ||
| ) | ||
| @issuers = issuers | ||
| @agency_abbreviation = agency_abbreviation | ||
| @time_range = time_range | ||
| @verbose = verbose | ||
| @progress = progress | ||
| @cloudwatch_client = cloudwatch_client | ||
| end | ||
|
|
||
| def verbose? | ||
| @verbose | ||
| end | ||
|
|
||
| def progress? | ||
| @progress | ||
| end | ||
|
|
||
| def query(after_row:) | ||
| base_query = <<~QUERY | ||
| filter properties.service_provider in #{issuers.inspect} or | ||
| (name = "IdV: enter verify by mail code submitted" and properties.event_properties.initiating_service_provider in #{issuers.inspect}) | ||
| | filter name in [ | ||
| "IdV: doc auth welcome visited", | ||
| "IdV: doc auth document_capture visited", | ||
| "Frontend: IdV: front image added", | ||
| "Frontend: IdV: back image added", | ||
| "idv_selfie_image_added", | ||
| "IdV: doc auth image upload vendor submitted", | ||
| "IdV: doc auth ssn submitted", | ||
| "IdV: doc auth verify proofing results", | ||
| "IdV: phone confirmation form", | ||
| "IdV: phone confirmation vendor", | ||
| "IdV: final resolution", | ||
| "IdV: enter verify by mail code submitted", | ||
| "Fraud: Profile review passed", | ||
| "Fraud: Profile review rejected", | ||
| "User registration: agency handoff visited", | ||
| "SP redirect initiated" | ||
| ] | ||
|
|
||
| | fields coalesce(name = "Fraud: Profile review passed" and properties.event_properties.success, 0) * properties.event_properties.profile_age_in_seconds as fraud_review_profile_age_in_seconds, | ||
| coalesce(name = "IdV: enter verify by mail code submitted" and properties.event_properties.success and !properties.event_properties.pending_in_person_enrollment and !properties.event_properties.fraud_check_failed, 0) * properties.event_properties.profile_age_in_seconds as gpo_profile_age_in_seconds, | ||
| fraud_review_profile_age_in_seconds + gpo_profile_age_in_seconds as profile_age_in_seconds | ||
|
|
||
| | stats sum(name = "IdV: doc auth welcome visited") > 0 as workflow_started, | ||
| sum(name = "IdV: doc auth document_capture visited") > 0 as doc_auth_started, | ||
| sum(name = "Frontend: IdV: front image added") > 0 and sum(name = "Frontend: IdV: back image added") > 0 as document_captured, | ||
| sum(name = "idv_selfie_image_added") > 0 as selfie_captured, | ||
| sum(name = "IdV: doc auth image upload vendor submitted" and properties.event_properties.success) > 0 as doc_auth_passed, | ||
| sum(name = "IdV: doc auth ssn submitted") > 0 as ssn_submitted, | ||
| sum(name = "IdV: doc auth verify proofing results") > 0 as personal_info_submitted, | ||
| sum(name = "IdV: doc auth verify proofing results" and properties.event_properties.success) > 0 as personal_info_verified, | ||
| sum(name = "IdV: phone confirmation form") > 0 as phone_submitted, | ||
| sum(name = "IdV: phone confirmation vendor" and properties.event_properties.success) > 0 as phone_verified, | ||
| sum(name = "IdV: final resolution") > 0 as online_workflow_completed, | ||
| sum(name = "IdV: final resolution" and !properties.event_properties.gpo_verification_pending and !properties.event_properties.in_person_verification_pending and !coalesce(properties.event_properties.fraud_pending_reason, 0)) > 0 as verified_in_band, | ||
| sum(name = "IdV: enter verify by mail code submitted" and properties.event_properties.success and !properties.event_properties.pending_in_person_enrollment and !properties.event_properties.fraud_check_failed) > 0 as verified_by_mail, | ||
| sum(name = "Fraud: Profile review passed" and properties.event_properties.success) > 0 as verified_fraud_review, | ||
| max(profile_age_in_seconds) as out_of_band_verification_pending_seconds, | ||
| sum(name = "User registration: agency handoff visited" and properties.event_properties.ial2) > 0 as agency_handoff, | ||
| sum(name = "SP redirect initiated" and properties.event_properties.ial == 2) > 0 as sp_redirect, | ||
| toMillis(min(@timestamp)) as first_event | ||
| by properties.user_id as login_uuid | ||
| | filter workflow_started > 0 or verified_by_mail > 0 or verified_fraud_review > 0 | ||
| | limit 10000 | ||
| | sort first_event asc | ||
| QUERY | ||
| return base_query if after_row.nil? | ||
|
|
||
| base_query + " | filter first_event > #{after_row['first_event']}" | ||
| end | ||
|
|
||
| def as_csv | ||
| csv = [] | ||
| csv << ['Date Range', "#{time_range.begin.to_date} - #{time_range.end.to_date}"] | ||
| csv << csv_header | ||
| data.each do |result_row| | ||
| csv << result_row | ||
| end | ||
| csv.compact | ||
| end | ||
|
|
||
| def to_csv | ||
| CSV.generate do |csv| | ||
| as_csv.each do |row| | ||
| csv << row | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def as_emailable_reports | ||
| [ | ||
| EmailableReport.new( | ||
| title: "#{agency_abbreviation} Proofing Events By UUID", | ||
| table: as_csv, | ||
| filename: "#{agency_abbreviation.downcase}_proofing_events_by_uuid", | ||
| ), | ||
| ] | ||
| end | ||
|
|
||
| def csv_header | ||
| [ | ||
| 'UUID', | ||
| 'Workflow Started', | ||
| 'Documnet Capture Started', | ||
| 'Document Captured', | ||
| 'Selfie Captured', | ||
| 'Document Authentication Passed', | ||
| 'SSN Submitted', | ||
| 'Personal Information Submitted', | ||
| 'Personal Information Verified', | ||
| 'Phone Submitted', | ||
| 'Phone Verified', | ||
| 'Verification Workflow Complete', | ||
| 'Identity Verified for In-Band Users', | ||
| 'Identity Verified for Verify-By-Mail Users', | ||
| 'Identity Verified for Fraud Review Users', | ||
| 'Out-of-Band Verification Pending Seconds', | ||
| 'Agency Handoff Visited', | ||
| 'Agency Handoff Submitted', | ||
| ] | ||
| end | ||
|
|
||
| def data | ||
| return @data if defined? @data | ||
|
|
||
| login_uuid_data ||= fetch_results.map do |result_row| | ||
| process_result_row(result_row) | ||
| end | ||
| login_uuid_to_agency_uuid_map = build_uuid_map(login_uuid_data.map(&:first)) | ||
|
|
||
| @data = login_uuid_data.map do |row| | ||
| login_uuid, *row_data = row | ||
| agency_uuid = login_uuid_to_agency_uuid_map[login_uuid] | ||
| next if agency_uuid.nil? | ||
| [agency_uuid, *row_data] | ||
| end.compact | ||
| end | ||
|
|
||
| def process_result_row(result_row) | ||
| [ | ||
| result_row['login_uuid'], | ||
| result_row['workflow_started'] == '1', | ||
| result_row['doc_auth_started'] == '1', | ||
| result_row['document_captured'] == '1', | ||
| result_row['selfie_captured'] == '1', | ||
| result_row['doc_auth_passed'] == '1', | ||
| result_row['ssn_submitted'] == '1', | ||
| result_row['personal_info_submitted'] == '1', | ||
| result_row['personal_info_verified'] == '1', | ||
| result_row['phone_submitted'] == '1', | ||
| result_row['phone_verified'] == '1', | ||
| result_row['online_workflow_completed'] == '1', | ||
| result_row['verified_in_band'] == '1', | ||
| result_row['verified_by_mail'] == '1', | ||
| result_row['verified_fraud_review'] == '1', | ||
| result_row['out_of_band_verification_pending_seconds'].to_i, | ||
| result_row['agency_handoff'] == '1', | ||
| result_row['sp_redirect'] == '1', | ||
| ] | ||
| end | ||
|
|
||
| # rubocop:disable Rails/FindEach | ||
| # Use of `find` instead of `find_each` here is safe since we are already batching the UUIDs | ||
| # that go into the query | ||
| def build_uuid_map(uuids) | ||
| uuid_map = Hash.new | ||
|
|
||
| uuids.each_slice(1000) do |uuid_slice| | ||
| AgencyIdentity.joins(:user).where( | ||
| agency:, | ||
| users: { uuid: uuid_slice }, | ||
| ).each do |agency_identity| | ||
| uuid_map[agency_identity.user.uuid] = agency_identity.uuid | ||
| end | ||
| end | ||
|
|
||
| uuid_map | ||
| end | ||
| # rubocop:enable Rails/FindEach | ||
|
|
||
| def agency | ||
| @agency ||= begin | ||
| record = Agency.find_by(abbreviation: agency_abbreviation) | ||
| raise "Unable to find agency with abbreviation: #{agency_abbreviation}" if record.nil? | ||
| record | ||
| end | ||
| end | ||
|
|
||
| def fetch_results(after_row: nil) | ||
| results = cloudwatch_client.fetch( | ||
| query: query(after_row:), | ||
| from: time_range.begin.beginning_of_day, | ||
| to: time_range.end.end_of_day, | ||
| ) | ||
| return results if results.count < 10000 | ||
| results + fetch_results(after_row: results.last) | ||
| end | ||
|
|
||
| def cloudwatch_client | ||
| @cloudwatch_client ||= Reporting::CloudwatchClient.new( | ||
| num_threads: 1, | ||
| ensure_complete_logs: false, | ||
jmhooper marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| slice_interval: 100.years, | ||
| progress: progress?, | ||
| logger: verbose? ? Logger.new(STDERR) : nil, | ||
| ) | ||
| end | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will leave out deleted users, right? should we put like some sort of deleted placeholder?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that for this report we are okay leaving out deleted users.