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 |