Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
33.33% |
6 / 18 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
ExperimentUserDefaultsManager | |
33.33% |
6 / 18 |
|
0.00% |
0 / 5 |
16.67 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
shouldAssignBucket | |
54.55% |
6 / 11 |
|
0.00% |
0 / 1 |
2.38 | |||
isInSample | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSample | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getUserHash | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments; |
4 | |
5 | use MediaWiki\User\CentralId\CentralIdLookup; |
6 | use MediaWiki\User\UserIdentity; |
7 | use 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 | */ |
14 | class 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 | } |