From 7350663364981d524a7c09514627df4e8f543820 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 3 Feb 2025 12:16:42 -0500 Subject: [PATCH 01/16] LG-15559: Add support for A/B tests daily reporting changelog: Internal, A/B Tests, Add support for A/B tests daily reporting --- app/jobs/reports/ab_tests_report.rb | 54 +++++++++++ config/initializers/job_configurations.rb | 6 ++ lib/ab_test.rb | 15 +-- lib/reporting/ab_tests_report.rb | 95 +++++++++++++++++++ spec/jobs/reports/ab_tests_report_spec.rb | 94 ++++++++++++++++++ spec/lib/ab_test_spec.rb | 41 +++++++- spec/lib/reporting/ab_tests_report_spec.rb | 93 ++++++++++++++++++ .../mailers/previews/report_mailer_preview.rb | 47 ++++++++- 8 files changed, 433 insertions(+), 12 deletions(-) create mode 100644 app/jobs/reports/ab_tests_report.rb create mode 100644 lib/reporting/ab_tests_report.rb create mode 100644 spec/jobs/reports/ab_tests_report_spec.rb create mode 100644 spec/lib/reporting/ab_tests_report_spec.rb diff --git a/app/jobs/reports/ab_tests_report.rb b/app/jobs/reports/ab_tests_report.rb new file mode 100644 index 00000000000..7661a7f21ea --- /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 { |ab_test| ab_test.report.merge(experiment_name: ab_test.experiment_name) } + end + end +end 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..4ed1a24a510 100644 --- a/lib/ab_test.rb +++ b/lib/ab_test.rb @@ -3,7 +3,7 @@ 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 @@ -20,6 +20,7 @@ def initialize( buckets: {}, should_log: nil, default_bucket: :default, + report: nil, &discriminator ) @buckets = buckets @@ -27,6 +28,7 @@ def initialize( @experiment_name = experiment_name @default_bucket = default_bucket @should_log = should_log + @report = report raise 'invalid bucket data structure' unless valid_bucket_data_structure? ensure_numeric_percentages raise 'bucket percentages exceed 100' unless within_100_percent? @@ -39,7 +41,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 +73,11 @@ def include_in_analytics_event?(event_name) end end + def active? + return @active if defined?(@active) + @active = buckets.present? && buckets.values.any?(&:positive?) + end + private def resolve_discriminator(user:, **) @@ -81,10 +88,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..36146c635a3 --- /dev/null +++ b/lib/reporting/ab_tests_report.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +begin + require 'reporting/cloudwatch_client' + require 'reporting/cloudwatch_query_quoting' + require 'reporting/command_line_options' +rescue LoadError => e + warn 'could not load paths, try running with "bundle exec rails runner"' + raise e +end + +module Reporting + class AbTestsReport + attr_reader :queries, :time_range + + # @param [Array] queries + # @param [Range