Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.33% covered (danger)
33.33%
6 / 18
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExperimentUserDefaultsManager
33.33% covered (danger)
33.33%
6 / 18
0.00% covered (danger)
0.00%
0 / 5
16.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 shouldAssignBucket
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
2.38
 isInSample
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSample
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getUserHash
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments;
4
5use MediaWiki\User\CentralId\CentralIdLookup;
6use MediaWiki\User\UserIdentity;
7use Psr\Log\LoggerInterface;
8
9/**
10 * Service for assigning experiment / variant to users using configured
11 * variants in conditional user options.
12 * @see https://www.mediawiki.org/wiki/Manual:$wgConditionalUserOptions
13 */
14class ExperimentUserDefaultsManager {
15
16    public const CUCOND_BUCKET_BY_USER_ID = 'user-bucket-growth';
17
18    private LoggerInterface $logger;
19
20    /**
21     * CentralIdLookup must be provided as a callback function to avoid circular dependency
22     * @var callable
23     */
24    private $centralIdLookupCallback;
25
26    public function __construct( LoggerInterface $logger, callable $centralIdLookupCallback ) {
27        $this->logger = $logger;
28        $this->centralIdLookupCallback = $centralIdLookupCallback;
29    }
30
31    /**
32     * @param UserIdentity $userIdentity The user being evaluated
33     * @param string $experimentName The name of the experiment
34     * @param array $args An array with the condition arguments.
35     * @return bool Whether the user option being evaluated (bucket) should be returned as the default
36     */
37    public function shouldAssignBucket( UserIdentity $userIdentity, string $experimentName, array $args ): bool {
38        $centralIdLookupCallback = $this->centralIdLookupCallback;
39        /** @var CentralIdLookup */
40        $centralIdLookup = $centralIdLookupCallback();
41        $userCentralId = $centralIdLookup->centralIdFromLocalUser( $userIdentity );
42        if ( $userCentralId === 0 ) {
43            // CentralIdLookup is documented to return a zero on failure
44            // TODO: Increase severity back to error, once it stops happening so frequently (T380271)
45            $this->logger->debug( __METHOD__ . ' failed to get a central user ID', [
46                'exception' => new \RuntimeException,
47                'userName' => $userIdentity->getName(),
48            ] );
49            // No point in hashing an error code
50            return false;
51        }
52        $sample = $this->getSample( $userCentralId, $experimentName );
53        return $this->isInSample( $args[0], $sample );
54    }
55
56    /**
57     * @param int $bucketRatio The probability rate for the bucket to be assigned
58     * @param int $sample The sample to test against
59     * @return bool Whether the sample is within the bucket ratio
60     */
61    private function isInSample( int $bucketRatio, int $sample ): bool {
62        return $sample < $bucketRatio;
63    }
64
65    private function getSample( int $userCentralId, string $experimentName ): int {
66        $floatHash = $this->getUserHash( $userCentralId, $experimentName );
67        return (int)( fmod( $floatHash, 1 ) * 100 );
68    }
69
70    /**
71     * Get hash of a user ID as a float between 0.0 (inclusive) and 1.0 (non-inclusive)
72     * concatenated with an experiment name.
73     *
74     * Originally taken from MediaWiki\Extension\MetricsPlatform\UserSplitter\UserSplitterInstrumentation.
75     *
76     * @param int $userId The user's id to hash
77     * @param string $experimentName The name of the experiment the bucketing applies
78     * @return float Float between 0.0 (inclusive) and 1.0 (non-inclusive) representing a user's hash
79     */
80    private function getUserHash( int $userId, string $experimentName ): float {
81        $userIdExperimentName = $userId . $experimentName;
82        return intval( substr( md5( $userIdExperimentName ), 0, 6 ), 16 ) / ( 0xffffff + 1 );
83    }
84
85}