From 056adfe3559c659233fb274b393c61f12e3dfb9d Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Tue, 24 Aug 2021 11:08:24 -0300 Subject: [PATCH 1/3] Add experimental abtest class and use it in WCPayPromotion --- includes/class-experimental-abtest.php | 173 +++++++++++++++++++++++++ src/FeaturePlugin.php | 1 + src/Features/WcPayPromotion/Init.php | 17 ++- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 includes/class-experimental-abtest.php diff --git a/includes/class-experimental-abtest.php b/includes/class-experimental-abtest.php new file mode 100644 index 00000000000..ef2cde6d561 --- /dev/null +++ b/includes/class-experimental-abtest.php @@ -0,0 +1,173 @@ +anon_id = $anon_id; + $this->platform = $platform; + $this->consent = $consent; + } + + /** + * Retrieve the test variation for a provided A/B test. + * + * @param string $test_name Name of the A/B test. + * @return mixed|null A/B test variation, or null on failure. + */ + public function get_variation( $test_name ) { + // Default to the control variation when users haven't consented to tracking. + if ( ! $this->consent ) { + return 'control'; + } + + $variation = $this->fetch_variation( $test_name ); + + // If there was an error retrieving a variation, conceal the error for the consumer. + if ( is_wp_error( $variation ) ) { + return 'control'; + } + + return $variation; + } + + /** + * Fetch and cache the test variation for a provided A/B test from WP.com. + * + * ExPlat returns a null value when the assigned variation is control or + * an assignment has not been set. In these instances, this method returns + * a value of "control". + * + * @param string $test_name Name of the A/B test. + * @return array|\WP_Error A/B test variation, or error on failure. + */ + protected function fetch_variation( $test_name ) { + // Make sure test name exists. + if ( ! $test_name ) { + return new \WP_Error( 'test_name_not_provided', 'A/B test name has not been provided.' ); + } + + // Make sure test name is a valid one. + if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $test_name ) ) { + return new \WP_Error( 'invalid_test_name', 'Invalid A/B test name.' ); + } + + // Return internal-cached test variations. + if ( isset( $this->tests[ $test_name ] ) ) { + return $this->tests[ $test_name ]; + } + + // Return external-cached test variations. + if ( ! empty( get_transient( 'abtest_variation_' . $test_name ) ) ) { + return get_transient( 'abtest_variation_' . $test_name ); + } + + // Make the request to the WP.com API. + $response = $this->request_variation( $test_name ); + + // Bail if there was an error or malformed response. + if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { + return new \WP_Error( 'failed_to_fetch_data', 'Unable to fetch the requested data.' ); + } + + // Decode the results. + $results = json_decode( $response['body'], true ); + + // Bail if there were no results or there is no test variation returned. + if ( ! is_array( $results ) || empty( $results['variations'] ) ) { + return new \WP_Error( 'unexpected_data_format', 'Data was not returned in the expected format.' ); + } + + // Store the variation in our internal cache. + $this->tests[ $test_name ] = $results['variations'][ $test_name ]; + + $variation = $results['variations'][ $test_name ] ?? 'control'; + + // Store the variation in our external cache. + if ( ! empty( $results['ttl'] ) ) { + set_transient( 'abtest_variation_' . $test_name, $variation, $results['ttl'] ); + } + + return $variation; + } + + /** + * Perform the request for a variation of a provided A/B test from WP.com. + * + * @param string $test_name Name of the A/B test. + * @return array|\WP_Error A/B test variation error on failure. + */ + protected function request_variation( $test_name ) { + $args = array( + 'experiment_name' => $test_name, + 'anon_id' => $this->anon_id, + ); + + $url = add_query_arg( + $args, + sprintf( + 'https://public-api.wordpress.com/wpcom/v2/experiments/0.1.0/assignments/%s', + $this->platform + ) + ); + + $get = wp_remote_get( $url ); + + return $get; + } +} + diff --git a/src/FeaturePlugin.php b/src/FeaturePlugin.php index 2642c8c0742..531ab139237 100644 --- a/src/FeaturePlugin.php +++ b/src/FeaturePlugin.php @@ -68,6 +68,7 @@ public function init() { require_once WC_ADMIN_ABSPATH . '/includes/core-functions.php'; require_once WC_ADMIN_ABSPATH . '/includes/feature-config.php'; require_once WC_ADMIN_ABSPATH . '/includes/wc-admin-update-functions.php'; + require_once WC_ADMIN_ABSPATH . '/includes/class-experimental-abtest.php'; register_activation_hook( WC_ADMIN_PLUGIN_FILE, array( $this, 'on_activation' ) ); register_deactivation_hook( WC_ADMIN_PLUGIN_FILE, array( $this, 'on_deactivation' ) ); diff --git a/src/Features/WcPayPromotion/Init.php b/src/Features/WcPayPromotion/Init.php index bfb24655210..097af28db0b 100644 --- a/src/Features/WcPayPromotion/Init.php +++ b/src/Features/WcPayPromotion/Init.php @@ -104,7 +104,22 @@ public static function should_register_pre_install_wc_pay_promoted_gateway() { if ( ! $wc_pay_spec ) { return false; } - return true; + + $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : null; + $allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' ); + $abtest = new \WooCommerce\Admin\Experimental_Abtest( + $anon_id, + 'woocommerce', + $allow_tracking + ); + + $variation_name = $abtest->get_variation( 'woocommerce_wc_pay_promotion_payment_methods_table_' . $wc_pay_spec->version ); + + if ( 'treatment' === $variation_name ) { + return true; + } + + return false; } /** From 219e732423fe4bc6bcd8f8b443e8e1be87a5a5e1 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 9 Sep 2021 09:22:32 -0300 Subject: [PATCH 2/3] Update experiment version data structure --- src/Features/WcPayPromotion/Init.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Features/WcPayPromotion/Init.php b/src/Features/WcPayPromotion/Init.php index 097af28db0b..0990607400c 100644 --- a/src/Features/WcPayPromotion/Init.php +++ b/src/Features/WcPayPromotion/Init.php @@ -101,7 +101,7 @@ public static function should_register_pre_install_wc_pay_promoted_gateway() { } $wc_pay_spec = self::get_wc_pay_promotion_spec(); - if ( ! $wc_pay_spec ) { + if ( ! $wc_pay_spec || ! isset( $wc_pay_spec->additional_info ) ) { return false; } @@ -113,7 +113,7 @@ public static function should_register_pre_install_wc_pay_promoted_gateway() { $allow_tracking ); - $variation_name = $abtest->get_variation( 'woocommerce_wc_pay_promotion_payment_methods_table_' . $wc_pay_spec->version ); + $variation_name = $abtest->get_variation( 'woocommerce_wc_pay_promotion_payment_methods_table_' . $wc_pay_spec->additional_info->experiment_version ); if ( 'treatment' === $variation_name ) { return true; From f28ef29ee6403d32c821717eca95b858cc1de685 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 9 Sep 2021 14:47:16 -0300 Subject: [PATCH 3/3] Address PR feedback --- includes/class-experimental-abtest.php | 3 +++ src/Features/WcPayPromotion/DataSourcePoller.php | 2 +- src/Features/WcPayPromotion/Init.php | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/class-experimental-abtest.php b/includes/class-experimental-abtest.php index ef2cde6d561..e4c7525707b 100644 --- a/includes/class-experimental-abtest.php +++ b/includes/class-experimental-abtest.php @@ -1,5 +1,8 @@ product; + $id = $spec->id; $specs[ $id ] = $spec; } } diff --git a/src/Features/WcPayPromotion/Init.php b/src/Features/WcPayPromotion/Init.php index 0990607400c..8af60c4de51 100644 --- a/src/Features/WcPayPromotion/Init.php +++ b/src/Features/WcPayPromotion/Init.php @@ -101,7 +101,7 @@ public static function should_register_pre_install_wc_pay_promoted_gateway() { } $wc_pay_spec = self::get_wc_pay_promotion_spec(); - if ( ! $wc_pay_spec || ! isset( $wc_pay_spec->additional_info ) ) { + if ( ! $wc_pay_spec || ! isset( $wc_pay_spec->additional_info ) || ! isset( $wc_pay_spec->additional_info->experiment_version ) ) { return false; }