diff --git a/app/jobs/reports/ab_tests_report.rb b/app/jobs/reports/ab_tests_report.rb new file mode 100644 index 00000000000..e96618e72cf --- /dev/null +++ b/app/jobs/reports/ab_tests_report.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'reporting/ab_tests_report' + +module Reports + class AbTestsReport < BaseReport + attr_reader :report_date + + def initialize(report_date = nil, *args, **kwargs) + @report_date = report_date + super(*args, **kwargs) + end + + # @param [DateTime] + def perform(report_date) + @report_date = report_date + + report_configs.each do |config| + tables_report(config).deliver_now + end + end + + def tables_report(config) + experiment_name = config.experiment_name + subject = "A/B Tests Report - #{experiment_name} - #{report_date}" + reports = ab_tests_report(config).as_emailable_reports + + ReportMailer.tables_report( + email: config.email, + subject:, + message: subject, + reports:, + attachment_format: :csv, + ) + end + + def ab_tests_report(config) + Reporting::AbTestsReport.new( + queries: config.queries, + time_range: report_date.yesterday..report_date, + ) + end + + private + + def report_configs + AbTests + .all + .values + .select { |ab_test| ab_test.report&.email&.present? && ab_test.active? } + .map(&:report) + end + end +end diff --git a/app/views/report_mailer/tables_report.html.erb b/app/views/report_mailer/tables_report.html.erb index 403db2ce5fa..8bb9fc0df7f 100644 --- a/app/views/report_mailer/tables_report.html.erb +++ b/app/views/report_mailer/tables_report.html.erb @@ -35,7 +35,7 @@ <% row.each do |cell| %> > - <% if cell.is_a?(Float) && report.float_as_percent? %> + <% if cell.is_a?(Float) && report.float_as_percent? && cell.finite? %> <%= number_to_percentage(cell * 100, precision: report.precision || 2) %> <% else %> <%= number_with_delimiter(cell) %> diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 0f3bb7adf49..e8cb3ce8563 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -236,6 +236,12 @@ cron: cron_every_monday, args: -> { [Time.zone.yesterday.end_of_day] }, }, + # Send A/B test reports + ab_tests_report: { + class: 'Reports::AbTestsReport', + cron: cron_24h, + args: -> { [Time.zone.yesterday.end_of_day] }, + }, # Send fraud metrics to Team Judy fraud_metrics_report: { class: 'Reports::FraudMetricsReport', diff --git a/lib/ab_test.rb b/lib/ab_test.rb index 1d7be784b49..3e7a7a84326 100644 --- a/lib/ab_test.rb +++ b/lib/ab_test.rb @@ -3,10 +3,19 @@ class AbTest include ::NewRelic::Agent::MethodTracer - attr_reader :buckets, :experiment_name, :default_bucket, :should_log + attr_reader :buckets, :experiment_name, :default_bucket, :should_log, :report MAX_SHA = (16 ** 64) - 1 + ReportQueryConfig = Struct.new(:title, :query, :row_labels, keyword_init: true).freeze + + ReportConfig = Struct.new(:experiment_name, :email, :queries, keyword_init: true) do + def initialize(queries: [], **) + super + self.queries.map!(&ReportQueryConfig.method(:new)) + end + end.freeze + # @param [Proc,Regexp,string,Boolean,nil] should_log Controls whether bucket data for this # A/B test is logged with specific # events. @@ -20,6 +29,7 @@ def initialize( buckets: {}, should_log: nil, default_bucket: :default, + report: nil, &discriminator ) @buckets = buckets @@ -27,9 +37,11 @@ def initialize( @experiment_name = experiment_name @default_bucket = default_bucket @should_log = should_log + @report = ReportConfig.new(experiment_name:, **report.to_h) if report raise 'invalid bucket data structure' unless valid_bucket_data_structure? ensure_numeric_percentages raise 'bucket percentages exceed 100' unless within_100_percent? + @active = buckets.present? && buckets.values.any?(&:positive?) end # @param [ActionDispatch::Request] request @@ -39,7 +51,7 @@ def initialize( # @param [User] user # @param [Hash] user_session def bucket(request:, service_provider:, session:, user:, user_session:) - return nil if no_percentages? + return nil if !active? discriminator = resolve_discriminator( request:, service_provider:, session:, user:, @@ -71,6 +83,10 @@ def include_in_analytics_event?(event_name) end end + def active? + @active + end + private def resolve_discriminator(user:, **) @@ -81,10 +97,6 @@ def resolve_discriminator(user:, **) end end - def no_percentages? - buckets.empty? || buckets.values.all? { |pct| pct == 0 } - end - def percent(discriminator) Digest::SHA256.hexdigest("#{discriminator}:#{experiment_name}").to_i(16).to_f / MAX_SHA * 100 end diff --git a/lib/reporting/ab_tests_report.rb b/lib/reporting/ab_tests_report.rb new file mode 100644 index 00000000000..8dd45408c69 --- /dev/null +++ b/lib/reporting/ab_tests_report.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'reporting/cloudwatch_client' +require 'reporting/cloudwatch_query_quoting' + +module Reporting + class AbTestsReport + attr_reader :queries, :time_range + + # @param [Array] queries + # @param [Range