Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 222 |
|
0.00% |
0 / 17 |
CRAP | |
0.00% |
0 / 1 |
WikimediaEventsHooks | |
0.00% |
0 / 222 |
|
0.00% |
0 / 17 |
6642 | |
0.00% |
0 / 1 |
onBeforePageDisplay | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onXAnalyticsSetHeader | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
onPageSaveComplete | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
210 | |||
getModuleConfig | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
getModuleFile | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
272 | |||
onListDefinedTags | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
onChangeTagsListActive | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialSearchGoResult | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onSpecialSearchResults | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onRecentChange_save | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onRecentChangeSaveCrossWikiUpload | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
onRecentChangeSaveEditCampaign | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
onResourceLoaderRegisterModules | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
onArticleViewHeader | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
onMakeGlobalVariablesScript | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
shouldSchemaEditAttemptStepOversample | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onBeforeInitialize | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | namespace WikimediaEvents; |
6 | |
7 | use Article; |
8 | use ExtensionRegistry; |
9 | use IContextSource; |
10 | use ISearchResultSet; |
11 | use MediaWiki\Actions\ActionEntryPoint; |
12 | use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook; |
13 | use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook; |
14 | use MediaWiki\Config\Config; |
15 | use MediaWiki\Deferred\DeferredUpdates; |
16 | use MediaWiki\Hook\BeforeInitializeHook; |
17 | use MediaWiki\Hook\BeforePageDisplayHook; |
18 | use MediaWiki\Hook\MakeGlobalVariablesScriptHook; |
19 | use MediaWiki\Hook\RecentChange_saveHook; |
20 | use MediaWiki\Hook\SpecialSearchGoResultHook; |
21 | use MediaWiki\Hook\SpecialSearchResultsHook; |
22 | use MediaWiki\MediaWikiServices; |
23 | use MediaWiki\Output\OutputPage; |
24 | use MediaWiki\Page\Hook\ArticleViewHeaderHook; |
25 | use MediaWiki\Parser\ParserOutput; |
26 | use MediaWiki\Request\WebRequest; |
27 | use MediaWiki\ResourceLoader as RL; |
28 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook; |
29 | use MediaWiki\ResourceLoader\ResourceLoader; |
30 | use MediaWiki\Revision\RevisionRecord; |
31 | use MediaWiki\Storage\EditResult; |
32 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
33 | use MediaWiki\Title\Title; |
34 | use MediaWiki\User\User; |
35 | use MediaWiki\User\UserIdentity; |
36 | use MediaWiki\WikiMap\WikiMap; |
37 | use MobileContext; |
38 | use RecentChange; |
39 | use RequestContext; |
40 | use Skin; |
41 | use WikimediaEvents\Hooks\HookRunner; |
42 | use WikiPage; |
43 | |
44 | /** |
45 | * Hooks used for Wikimedia-related logging |
46 | * |
47 | * @author Ori Livneh <ori@wikimedia.org> |
48 | * @author Matthew Flaschen <mflaschen@wikimedia.org> |
49 | * @author Benny Situ <bsitu@wikimedia.org> |
50 | */ |
51 | class WikimediaEventsHooks implements |
52 | BeforeInitializeHook, |
53 | BeforePageDisplayHook, |
54 | PageSaveCompleteHook, |
55 | ArticleViewHeaderHook, |
56 | ListDefinedTagsHook, |
57 | ChangeTagsListActiveHook, |
58 | SpecialSearchGoResultHook, |
59 | SpecialSearchResultsHook, |
60 | RecentChange_saveHook, |
61 | ResourceLoaderRegisterModulesHook, |
62 | MakeGlobalVariablesScriptHook |
63 | { |
64 | |
65 | /** |
66 | * @param OutputPage $out |
67 | * @param Skin $skin |
68 | */ |
69 | public function onBeforePageDisplay( $out, $skin ): void { |
70 | $out->addModules( 'ext.wikimediaEvents' ); |
71 | |
72 | if ( ExtensionRegistry::getInstance()->isLoaded( 'WikibaseRepository' ) ) { |
73 | // If we are in Wikibase Repo, load Wikibase module |
74 | $out->addModules( 'ext.wikimediaEvents.wikibase' ); |
75 | } |
76 | } |
77 | |
78 | /** |
79 | * On XAnalyticsSetHeader |
80 | * |
81 | * When adding new headers here please update the docs: |
82 | * https://wikitech.wikimedia.org/wiki/X-Analytics |
83 | * |
84 | * Insert a 'page_id' key with the page ID as value (if the request is for a page with a pageid) |
85 | * Insert a 'ns' key with the namespace ID as value (if the request is for a valid title) |
86 | * Insert a 'special' key with the resolved name of the special page (if the request is for a |
87 | * special page). If the name does not resolve, special is set to 'unknown' (see T304362). |
88 | * |
89 | * Add a 'loggedIn' key with the value of 1 if the user is logged in |
90 | * @param OutputPage $out |
91 | * @param array &$headerItems |
92 | */ |
93 | public static function onXAnalyticsSetHeader( OutputPage $out, array &$headerItems ): void { |
94 | $title = $out->getTitle(); |
95 | if ( $title !== null && !defined( 'MW_API' ) ) { |
96 | $pageId = $title->getArticleID(); |
97 | $headerItems['ns'] = $title->getNamespace(); |
98 | if ( is_int( $pageId ) && $pageId > 0 ) { |
99 | $headerItems['page_id'] = $pageId; |
100 | } |
101 | if ( $title->isSpecialPage() ) { |
102 | [ $name, /* $subpage */ ] = MediaWikiServices::getInstance()->getSpecialPageFactory() |
103 | ->resolveAlias( $title->getDBkey() ); |
104 | |
105 | $headerItems['special'] = $name ?? 'unknown'; |
106 | } |
107 | $revId = $out->getRevisionId(); |
108 | // The revision ID will be positive for page and diff views, as well |
109 | // as viewing old revisions. |
110 | if ( $revId > 0 ) { |
111 | $headerItems['rev_id'] = $revId; |
112 | } |
113 | } |
114 | |
115 | if ( $out->getUser()->isRegistered() ) { |
116 | $headerItems['loggedIn'] = 1; |
117 | } |
118 | } |
119 | |
120 | /** |
121 | * Log server-side event on successful page edit. |
122 | * |
123 | * Imported from EventLogging extension |
124 | * |
125 | * @param WikiPage $wikiPage |
126 | * @param UserIdentity $userIdentity |
127 | * @param string $summary |
128 | * @param int $flags |
129 | * @param RevisionRecord $revisionRecord |
130 | * @param EditResult $editResult |
131 | * |
132 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete |
133 | */ |
134 | public function onPageSaveComplete( |
135 | $wikiPage, |
136 | $userIdentity, |
137 | $summary, |
138 | $flags, |
139 | $revisionRecord, |
140 | $editResult |
141 | ): void { |
142 | if ( PHP_SAPI === 'cli' ) { |
143 | return; // ignore maintenance scripts |
144 | } |
145 | |
146 | // Discard null edits from these metrics as they do not produce a |
147 | // saved an edit to a page, and thus notably differ in code execution. |
148 | // Note that null edits can still be relatively slow, as they do perform |
149 | // reparses. |
150 | if ( $editResult->isNullEdit() ) { |
151 | return; |
152 | } |
153 | |
154 | $title = $wikiPage->getTitle(); |
155 | |
156 | $request = RequestContext::getMain()->getRequest(); |
157 | $services = MediaWikiServices::getInstance(); |
158 | $nsInfo = $services->getNamespaceInfo(); |
159 | $permMgr = $services->getPermissionManager(); |
160 | |
161 | $user = User::newFromIdentity( $userIdentity ); |
162 | $content = $wikiPage->getContent(); |
163 | |
164 | if ( |
165 | $user->isBot() || |
166 | ( $request->getCheck( 'bot' ) && $permMgr->userHasRight( $user, 'bot' ) ) |
167 | ) { |
168 | $accType = 'bot'; // registered bot or script acting on behalf of a user |
169 | } elseif ( $request->getCheck( 'maxlag' ) ) { |
170 | $accType = 'throttled'; // probably an unregistered bot |
171 | } else { |
172 | $accType = 'normal'; |
173 | } |
174 | |
175 | if ( in_array( $content->getModel(), [ 'wikibase-item', 'wikibase-property' ] ) ) { |
176 | $nsType = 'entity'; |
177 | } elseif ( $nsInfo->isContent( $title->getNamespace() ) ) { |
178 | $nsType = 'content'; |
179 | } elseif ( $nsInfo->isTalk( $title->getNamespace() ) ) { |
180 | $nsType = 'talk'; |
181 | } else { |
182 | $nsType = 'meta'; |
183 | } |
184 | |
185 | if ( MW_ENTRY_POINT === 'index' ) { |
186 | // non-AJAX submission from user interface |
187 | // (for non-WMF this could also mean jobrunner, since jobs run post-send |
188 | // from index.php by default) |
189 | $entry = 'index'; |
190 | } elseif ( MW_ENTRY_POINT === 'api' || MW_ENTRY_POINT === 'rest' ) { |
191 | $entry = 'api'; |
192 | } else { |
193 | // jobrunner, maint/cli |
194 | $entry = 'other'; |
195 | } |
196 | |
197 | $size = $content->getSize(); |
198 | |
199 | DeferredUpdates::addCallableUpdate( |
200 | static function () use ( $size, $nsType, $accType, $entry ) { |
201 | $reqCtxTiming = RequestContext::getMain()->getTiming(); |
202 | |
203 | $measure = $reqCtxTiming->measure( |
204 | 'editResponseTime', 'requestStart', 'requestShutdown' ); |
205 | if ( $measure === false ) { |
206 | return; |
207 | } |
208 | |
209 | $timeMs = $measure['duration'] * 1000; |
210 | |
211 | $statsFactory = MediaWikiServices::getInstance()->getStatsFactory() |
212 | ->withComponent( 'WikimediaEvents' ); |
213 | |
214 | $statsFactory->getTiming( 'editResponseTime_seconds' ) |
215 | ->setLabel( 'page', $nsType ) |
216 | ->setLabel( 'user', $accType ) |
217 | ->setLabel( 'entry', $entry ) |
218 | ->copyToStatsdAt( [ |
219 | "timing.editResponseTime", |
220 | "timing.editResponseTime.page.$nsType", |
221 | "timing.editResponseTime.user.$accType", |
222 | "timing.editResponseTime.entry.$entry", |
223 | ] )->observe( $timeMs ); |
224 | |
225 | $msPerKb = $timeMs / ( max( $size, 1 ) / 1e3 ); // T224686 |
226 | $statsFactory->getTiming( 'editResponseTimePerKB_seconds' ) |
227 | ->setLabel( 'page', $nsType ) |
228 | ->setLabel( 'user', $accType ) |
229 | ->setLabel( 'entry', $entry ) |
230 | ->copyToStatsdAt( [ |
231 | "timing.editResponseTimePerKB.page.$nsType", |
232 | "timing.editResponseTimePerKB.user.$accType", |
233 | "timing.editResponseTimePerKB.entry.$entry", |
234 | ] )->observe( $msPerKb ); |
235 | } |
236 | ); |
237 | } |
238 | |
239 | /** |
240 | * Callback for ext.wikimediaEvents virtual config.json file. |
241 | * |
242 | * @param RL\Context $context |
243 | * @param Config $config |
244 | * @return array |
245 | */ |
246 | public static function getModuleConfig( RL\Context $context, Config $config ) { |
247 | $vars = []; |
248 | $vars['clientErrorIntakeURL'] = $config->get( 'WMEClientErrorIntakeURL' ); |
249 | $vars['statsdBaseUri'] = $config->get( 'WMEStatsdBaseUri' ); |
250 | $vars['wikidataCompletionSearchClicks'] = $config->get( 'WMEWikidataCompletionSearchClicks' ); |
251 | $vars['sessionTick'] = $config->get( 'WMESessionTick' ); |
252 | $vars['readingDepthSamplingRate'] = $config->get( 'WMEReadingDepthSamplingRate' ); |
253 | $vars['newPHPSamplingRate'] = $config->get( 'WMENewPHPSamplingRate' ); |
254 | $vars['newPHPVersion'] = $config->get( 'WMENewPHPVersion' ); |
255 | $skin = $context->getSkin(); |
256 | if ( $skin === 'minerva' ) { |
257 | $vars['mobileWebUIActionsTracking'] = $config->get( 'WMEMobileWebUIActionsTracking' ); |
258 | } elseif ( in_array( $skin, [ 'vector', 'vector-2022' ] ) ) { |
259 | $vars['desktopWebUIActionsTracking'] = $config->get( 'WMEDesktopWebUIActionsTracking' ); |
260 | $vars['desktopWebUIActionsTrackingOversampleLoggedInUsers'] = |
261 | $config->get( 'WMEDesktopWebUIActionsTrackingOversampleLoggedInUsers' ); |
262 | $vars['webUIScrollTrackingSamplingRate'] = $config->get( 'WMEWebUIScrollTrackingSamplingRate' ); |
263 | $vars['webUIScrollTrackingSamplingRateAnons'] = $config->get( 'WMEWebUIScrollTrackingSamplingRateAnons' ); |
264 | $vars['webUIScrollTrackingTimeToWaitBeforeScrollUp'] = |
265 | $config->get( 'WMEWebUIScrollTrackingTimeToWaitBeforeScrollUp' ); |
266 | } |
267 | |
268 | // editAttemptStep.js |
269 | $vars['WMESchemaEditAttemptStepSamplingRate'] = |
270 | $config->get( 'WMESchemaEditAttemptStepSamplingRate' ); |
271 | $vars['WMESchemaVisualEditorFeatureUseSamplingRate'] = |
272 | $config->get( 'WMESchemaVisualEditorFeatureUseSamplingRate' ); |
273 | $vars['DTSchemaEditAttemptStepSamplingRate'] = |
274 | $config->get( 'DTSchemaEditAttemptStepSamplingRate' ); |
275 | $vars['DTSchemaEditAttemptStepOversample'] = |
276 | $config->get( 'DTSchemaEditAttemptStepOversample' ); |
277 | $vars['MFSchemaEditAttemptStepOversample'] = |
278 | $config->get( 'MFSchemaEditAttemptStepOversample' ); |
279 | |
280 | return $vars; |
281 | } |
282 | |
283 | /** |
284 | * Callback for dynamic source files, for conditional loading based on the current skin. |
285 | * |
286 | * @param RL\Context $context |
287 | * @param Config $config |
288 | * @param string $param callback param - corresponds to the file name to conditionally load |
289 | * @return RL\FilePath|string |
290 | */ |
291 | public static function getModuleFile( RL\Context $context, Config $config, $param ) { |
292 | $skin = $context->getSkin(); |
293 | |
294 | switch ( $skin ) { |
295 | case 'vector': |
296 | case 'vector-2022': |
297 | switch ( $param ) { |
298 | case 'clickTracking/desktop': |
299 | case 'searchSatisfaction': |
300 | case 'searchSli': |
301 | case 'universalLanguageSelector': |
302 | case 'webUIScroll': |
303 | return new RL\FilePath( $param . '.js' ); |
304 | default: |
305 | return ''; |
306 | } |
307 | case 'minerva': |
308 | switch ( $param ) { |
309 | case 'clickTracking/mobile': |
310 | return new RL\FilePath( $param . '.js' ); |
311 | default: |
312 | return ''; |
313 | } |
314 | default: |
315 | switch ( $param ) { |
316 | case 'searchSatisfaction': |
317 | case 'searchSli': |
318 | return new RL\FilePath( $param . '.js' ); |
319 | default: |
320 | return ''; |
321 | } |
322 | } |
323 | } |
324 | |
325 | /** |
326 | * Register change tags. |
327 | * |
328 | * @param array &$tags |
329 | */ |
330 | public function onListDefinedTags( &$tags ): void { |
331 | if ( WikiMap::getCurrentWikiId() === 'commonswiki' ) { |
332 | $tags[] = 'cross-wiki-upload'; |
333 | // For A/B test |
334 | $tags[] = 'cross-wiki-upload-1'; |
335 | $tags[] = 'cross-wiki-upload-2'; |
336 | $tags[] = 'cross-wiki-upload-3'; |
337 | $tags[] = 'cross-wiki-upload-4'; |
338 | } |
339 | } |
340 | |
341 | /** |
342 | * Mark active change tags. |
343 | * |
344 | * @param array &$tags |
345 | */ |
346 | public function onChangeTagsListActive( &$tags ): void { |
347 | if ( WikiMap::getCurrentWikiId() === 'commonswiki' ) { |
348 | $tags[] = 'cross-wiki-upload'; |
349 | // For A/B test |
350 | $tags[] = 'cross-wiki-upload-1'; |
351 | $tags[] = 'cross-wiki-upload-2'; |
352 | $tags[] = 'cross-wiki-upload-3'; |
353 | $tags[] = 'cross-wiki-upload-4'; |
354 | } |
355 | } |
356 | |
357 | /** |
358 | * @param string $term |
359 | * @param Title $title |
360 | * @param string|null &$url |
361 | */ |
362 | public function onSpecialSearchGoResult( $term, $title, &$url ): void { |
363 | $request = RequestContext::getMain()->getRequest(); |
364 | |
365 | $wprov = $request->getRawVal( 'wprov' ); |
366 | if ( $wprov ) { |
367 | $url = $title->getFullURL( [ 'wprov' => $wprov ] ); |
368 | } |
369 | } |
370 | |
371 | /** |
372 | * The javascript that records search metrics needs to know if it is on a |
373 | * SERP or not. This ends up being non-trivial due to localization, so |
374 | * make it trivial by injecting a boolean value to check. |
375 | * |
376 | * @param string $term |
377 | * @param ?ISearchResultSet &$titleMatches |
378 | * @param ?ISearchResultSet &$textMatches |
379 | */ |
380 | public function onSpecialSearchResults( $term, &$titleMatches, &$textMatches ): void { |
381 | $out = RequestContext::getMain()->getOutput(); |
382 | |
383 | $out->addJsConfigVars( [ |
384 | 'wgIsSearchResultPage' => true, |
385 | ] ); |
386 | } |
387 | |
388 | /** |
389 | * @param RecentChange $rc |
390 | */ |
391 | public function onRecentChange_save( $rc ): void { |
392 | self::onRecentChangeSaveCrossWikiUpload( $rc ); |
393 | self::onRecentChangeSaveEditCampaign( $rc ); |
394 | } |
395 | |
396 | /** |
397 | * Add a change tag 'cross-wiki-upload' to cross-wiki uploads to Commons, to track usage of the |
398 | * new feature. (Both to track adoption, and to let Commons editors review the uploads.) (T115328) |
399 | * |
400 | * @param RecentChange $rc |
401 | */ |
402 | public static function onRecentChangeSaveCrossWikiUpload( RecentChange $rc ): void { |
403 | if ( !defined( 'MW_API' ) || WikiMap::getCurrentWikiId() !== 'commonswiki' ) { |
404 | return; |
405 | } |
406 | |
407 | if ( |
408 | $rc->getAttribute( 'rc_log_type' ) !== 'upload' || |
409 | $rc->getAttribute( 'rc_log_action' ) !== 'upload' |
410 | ) { |
411 | return; |
412 | } |
413 | $request = RequestContext::getMain()->getRequest(); |
414 | if ( !$request->response()->getHeader( 'Access-Control-Allow-Origin' ) ) { |
415 | return; |
416 | } |
417 | |
418 | // A/B test |
419 | $bucket = $request->getRawVal( 'bucket' ); |
420 | if ( !in_array( $bucket, [ '1', '2', '3', '4' ] ) ) { |
421 | $bucket = null; |
422 | } |
423 | |
424 | $tags = [ 'cross-wiki-upload' ]; |
425 | if ( $bucket ) { |
426 | $tags[] = "cross-wiki-upload-$bucket"; |
427 | } |
428 | $rc->addTags( $tags ); |
429 | } |
430 | |
431 | /** |
432 | * Add a change tag 'campaign-...' to edits made via edit campaigns (identified by an URL |
433 | * parameter passed to the API via VisualEditor). (T209132) |
434 | * |
435 | * @param RecentChange $rc |
436 | */ |
437 | public static function onRecentChangeSaveEditCampaign( RecentChange $rc ): void { |
438 | global $wgWMEEditCampaigns; |
439 | |
440 | if ( !defined( 'MW_API' ) ) { |
441 | return; |
442 | } |
443 | |
444 | $request = RequestContext::getMain()->getRequest(); |
445 | $campaign = $request->getRawVal( 'campaign' ); |
446 | if ( !in_array( $campaign, $wgWMEEditCampaigns ) ) { |
447 | return; |
448 | } |
449 | |
450 | $tags = [ "campaign-$campaign" ]; |
451 | $rc->addTags( $tags ); |
452 | } |
453 | |
454 | /** |
455 | * @param ResourceLoader $rl |
456 | */ |
457 | public function onResourceLoaderRegisterModules( ResourceLoader $rl ): void { |
458 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) ) { |
459 | return; |
460 | } |
461 | |
462 | $dir = dirname( __DIR__ ) . DIRECTORY_SEPARATOR; |
463 | |
464 | $rl->register( "ext.wikimediaEvents.visualEditor", [ |
465 | 'localBasePath' => $dir . 'modules', |
466 | 'remoteExtPath' => 'WikimediaEvents/modules', |
467 | "scripts" => "ext.wikimediaEvents.visualEditor/campaigns.js", |
468 | "dependencies" => "ext.visualEditor.targetLoader", |
469 | ] ); |
470 | } |
471 | |
472 | /** |
473 | * @param Article $article |
474 | * @param bool|ParserOutput|null &$outputDone |
475 | * @param bool &$pcache |
476 | */ |
477 | public function onArticleViewHeader( $article, &$outputDone, &$pcache ) { |
478 | DeferredUpdates::addCallableUpdate( static function () { |
479 | $context = RequestContext::getMain(); |
480 | $timing = $context->getTiming(); |
481 | if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) |
482 | && MobileContext::singleton()->shouldDisplayMobileView() |
483 | ) { |
484 | $platform = 'mobile'; |
485 | } else { |
486 | $platform = 'desktop'; |
487 | } |
488 | |
489 | $measure = $timing->measure( 'viewResponseTime', 'requestStart', 'requestShutdown' ); |
490 | if ( $measure !== false ) { |
491 | MediaWikiServices::getInstance()->getStatsdDataFactory()->timing( |
492 | "timing.viewResponseTime.{$platform}", $measure['duration'] * 1000 ); |
493 | } |
494 | } ); |
495 | } |
496 | |
497 | /** |
498 | * @param array &$vars |
499 | * @param OutputPage $out |
500 | */ |
501 | public function onMakeGlobalVariablesScript( &$vars, $out ): void { |
502 | $vars['wgWMESchemaEditAttemptStepOversample'] = |
503 | static::shouldSchemaEditAttemptStepOversample( $out->getContext() ); |
504 | |
505 | // Set page length for reading depth instrument T294777. |
506 | $length = $out->getTitle()->getLength(); |
507 | $log = log10( $length ); |
508 | $vars[ 'wgWMEPageLength' ] = round( $length, -intval( $log ) ); |
509 | } |
510 | |
511 | /** |
512 | * @param IContextSource $context |
513 | * @return bool |
514 | */ |
515 | public static function shouldSchemaEditAttemptStepOversample( IContextSource $context ) { |
516 | // The editingStatsOversample request parameter can trigger oversampling |
517 | $shouldOversample = $context->getRequest()->getBool( 'editingStatsOversample' ); |
518 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
519 | ->onWikimediaEventsShouldSchemaEditAttemptStepOversample( $context, $shouldOversample ); |
520 | return $shouldOversample; |
521 | } |
522 | |
523 | /** |
524 | * WMDE runs banner campaigns to encourage users to create an account and edit. |
525 | * |
526 | * The tracking already implemented in the Campaigns extension doesn't quite cover the WMDE |
527 | * use case. WMDE has a landing page that must be shown before the user progresses to |
528 | * registration. This could one day be factored out into its own extension, or made |
529 | * part of the Campaigns extension. |
530 | * |
531 | * Task for moving this to the Campaigns extension: |
532 | * https://phabricator.wikimedia.org/T174939 |
533 | * |
534 | * Active WMDE campaigns tracked at: |
535 | * https://phabricator.wikimedia.org/project/subprojects/2821/ |
536 | * |
537 | * @author addshore on behalf of WMDE |
538 | * |
539 | * @param Title $title |
540 | * @param mixed $unused |
541 | * @param OutputPage $output |
542 | * @param User $user |
543 | * @param WebRequest $request |
544 | * @param ActionEntryPoint $mediaWiki |
545 | */ |
546 | public function onBeforeInitialize( |
547 | $title, |
548 | $unused, |
549 | $output, |
550 | $user, |
551 | $request, |
552 | $mediaWiki |
553 | ): void { |
554 | // Only run for dewiki |
555 | if ( WikiMap::getCurrentWikiId() !== 'dewiki' ) { |
556 | return; |
557 | } |
558 | |
559 | // Setup the campaign prefix. |
560 | // Everything below this block is agnostic to which campaign is being run. |
561 | $campaignPrefix = 'WMDE_'; |
562 | $cookieName = 'wmdecampaign-' . $campaignPrefix; |
563 | |
564 | $hasCampaignCookie = $request->getCookie( $cookieName ) !== null; |
565 | $hasCampaignQuery = strpos( $request->getRawVal( 'campaign' ), $campaignPrefix ) === 0; |
566 | |
567 | // Get the campaign name from either the cookie or query param |
568 | // Cookie has precedence |
569 | if ( $hasCampaignCookie ) { |
570 | $campaign = $request->getCookie( $cookieName ); |
571 | } elseif ( $hasCampaignQuery ) { |
572 | $campaign = $request->getRawVal( 'campaign' ); |
573 | } else { |
574 | // Request has nothing to do with our campaign |
575 | return; |
576 | } |
577 | |
578 | // If an anon user clicks on the banner and doesn't yet have a session cookie then |
579 | // add a session cookie and log the click. |
580 | if ( !$hasCampaignCookie && $hasCampaignQuery && !$user->isRegistered() ) { |
581 | $request->response()->setCookie( $cookieName, $campaign, null ); |
582 | wfDebugLog( 'WMDE', "$campaign - 1 - Banner click by anon user without cookie" ); |
583 | } |
584 | |
585 | // If an anon user with the cookie, views the create account page without a campaign |
586 | // query param, then inject it into the WebRequest object to influence the Campaigns |
587 | // extension. |
588 | if ( |
589 | !$hasCampaignQuery && |
590 | $hasCampaignCookie && |
591 | !$user->isRegistered() && |
592 | $title->isSpecial( 'CreateAccount' ) |
593 | ) { |
594 | $request->setVal( 'campaign', $campaign ); |
595 | wfDebugLog( 'WMDE', "$campaign - 2 - Inject campaign value on CreateAccount" ); |
596 | } |
597 | } |
598 | } |