/var/www/html/wp-content/plugins/woocommerce/includes/react-admin/class-experimental-abtest.php


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
<?php
/**
 * NOTE: this is a temporary class and can be replaced by jetpack-abtest after
 * https://github.com/Automattic/jetpack/issues/19596 has been fixed.
 *
 * A class that interacts with Explat A/B tests.
 *
 * This class is experimental. It is a fork of the jetpack-abtest package and
 * updated for use with ExPlat. These changes are planned to be contributed
 * back to the upstream Jetpack package. If accepted, this class should then
 * be superseded by the Jetpack class using Composer.
 *
 * This class should not be used externally.
 *
 * @package WooCommerce\Admin
 * @link https://packagist.org/packages/automattic/jetpack-abtest
 */

namespace WooCommerce\Admin;

use 
Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;
use 
Automattic\Jetpack\Connection\Client as Jetpack_Connection_client;
use 
Automattic\WooCommerce\Admin\WCAdminHelper;

/**
 * This class provides an interface to the Explat A/B tests.
 *
 * Usage:
 *
 * $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
 * $allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' );
 * $abtest = new \WooCommerce\Admin\Experimental_Abtest(
 *      $anon_id,
 *      'woocommerce',
 *      $allow_tracking
 * );
 *
 * OR use the helper function:
 *
 * WooCommerce\Admin\Experimental_Abtest::in_treatment('experiment_name');
 *
 *
 * $isTreatment = $abtest->get_variation('your-experiment-name') === 'treatment';
 *
 * @internal This class is experimental and should not be used externally due to planned breaking changes.
 */
final class Experimental_Abtest {

    
/**
     * A variable to hold the tests we fetched, and their variations for the current user.
     *
     * @var array
     */
    
private $tests = array();

    
/**
     * ExPlat Anonymous ID.
     *
     * @var string
     */
    
private $anon_id null;

    
/**
     * ExPlat Platform name.
     *
     * @var string
     */
    
private $platform 'woocommerce';

    
/**
     * Whether trcking consent is given.
     *
     * @var bool
     */
    
private $consent false;

    
/**
     * Request variation as a auth wpcom user or not.
     *
     * @var boolean
     */
    
private $as_auth_wpcom_user false;

    
/**
     * Constructor.
     *
     * @param string $anon_id ExPlat anonymous ID.
     * @param string $platform ExPlat platform name.
     * @param bool   $consent Whether tracking consent is given.
     * @param bool   $as_auth_wpcom_user  Request variation as a auth wp user or not.
     */
    
public function __constructstring $anon_idstring $platformbool $consentbool $as_auth_wpcom_user false ) {
        
$this->anon_id            $anon_id;
        
$this->platform           $platform;
        
$this->consent            $consent;
        
$this->as_auth_wpcom_user $as_auth_wpcom_user;
    }

    
/**
     * Returns true if the current user is in the treatment group of the given experiment.
     *
     * @param string $experiment_name    Name of the experiment.
     * @param bool   $as_auth_wpcom_user Request variation as a auth wp user or not.
     *
     * @return bool True if the user is in the treatment group, false otherwise.
     * @throws \Exception If there is an error retrieving the variation.
     */
    
public static function in_treatmentstring $experiment_namebool $as_auth_wpcom_user false ) {
        
$anon_id        = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_fieldwp_unslash$_COOKIE['tk_ai'] ) ) : '';
        
$allow_tracking 'yes' === get_option'woocommerce_allow_tracking' );
        
$abtest         = new self(
            
$anon_id,
            
'woocommerce',
            
$allow_tracking,
            
$as_auth_wpcom_user
        
);

        return 
$abtest->get_variation$experiment_name ) === 'treatment';
    }

    
/**
     * Returns true if the current user is in the treatment group of the given experiment.
     *
     * If an exception occurs, it will be handled and false will be returned.
     *
     * @param string $experiment_name Name of the experiment.
     * @param bool   $as_auth_wpcom_user Request variation as an auth wp user or not.
     *
     * @return bool True if the user is in the treatment group, false otherwise (including if an exception is thrown).
     */
    
public static function in_treatment_handled_exceptionstring $experiment_namebool $as_auth_wpcom_user false ) {
        try {
            return 
self::in_treatment$experiment_name$as_auth_wpcom_user );
        } catch ( 
\Exception $e ) {
            return 
false;
        }
    }

    
/**
     * 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.
     * @throws \Exception If there is an error retrieving the variation and the environment is not production.
     */
    
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 there was an error retrieving a variation, throw an exception in non-production environments.
        
if ( is_wp_error$variation ) ) {
            if ( 
'production' !== wp_get_environment_type() ) {
                throw new 
\Exception$variation->get_error_message() );
            }
            return 
'control';
        }

        return 
$variation;
    }


    
/**
     * Perform the request for a experiment assignment of a provided A/B test from WP.com.
     *
     * @param array $args Arguments to pass to the request for A/B test.
     * @return array|\WP_Error A/B test variation error on failure.
     */
    
public function request_assignment$args ) {
        
// Request as authenticated wp user.
        
if ( $this->as_auth_wpcom_user && class_existsJetpack_Connection_Manager::class ) ) {
            
$jetpack_connection_manager = new Jetpack_Connection_Manager();
            if ( 
$jetpack_connection_manager->is_user_connected() ) {
                
$response Jetpack_Connection_client::wpcom_json_api_request_as_user(
                    
'/experiments/0.1.0/assignments/' $this->platform,
                    
'2',
                    
$args
                
);
            }
        }

        
// Request as anonymous user.
        
if ( ! isset( $response ) ) {
            if ( ! isset( 
$args['anon_id'] ) || empty( $args['anon_id'] ) ) {
                return new 
\WP_Error'invalid_anon_id''anon_id must be an none empty string.' );
            }

            
$url      add_query_arg(
                
$args,
                
sprintf(
                    
'https://public-api.wordpress.com/wpcom/v2/experiments/0.1.0/assignments/%s',
                    
$this->platform
                
)
            );
            
$response wp_remote_get$url );
        }

        return 
$response;
    }

    
/**
     * 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-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.
        
$args = array(
            
'experiment_name'               => $test_name,
            
'anon_id'                       => rawurlencode$this->anon_id ),
            
'woo_country_code'              => rawurlencodeget_option'woocommerce_default_country''US:CA' ) ),
            
'woo_wcadmin_install_timestamp' => rawurlencodeget_optionWCAdminHelper::WC_ADMIN_TIMESTAMP_OPTION ) ),
        );

        
/**
         * Get additional request args.
         *
         * @since 6.5.0
         */
        
$args     apply_filters'woocommerce_explat_request_args'$args );
        
$response $this->request_assignment$args );

        
// 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 resultsreturned.
        
if ( ! is_array$results ) ) {
            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 ] ?? null;

        
$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;
    }
}