Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
48.07% |
112 / 233 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
EventDispatcher | |
48.07% |
112 / 233 |
|
18.18% |
2 / 11 |
777.00 | |
0.00% |
0 / 1 |
getParsedRevision | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
generateEventsForRevision | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
groupCommentsByThreadAndName | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
9 | |||
groupSubscribableHeadings | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
findAddedItems | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
8.23 | |||
generateEventsFromItemSets | |
84.54% |
82 / 97 |
|
0.00% |
0 / 1 |
21.48 | |||
addCommentChangeTag | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
addAutoSubscription | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
locateSubscribedUsers | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
logAddedComments | |
7.41% |
4 / 54 |
|
0.00% |
0 / 1 |
246.42 | |||
inEventSample | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * DiscussionTools event dispatcher |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @license MIT |
8 | */ |
9 | |
10 | namespace MediaWiki\Extension\DiscussionTools\Notifications; |
11 | |
12 | use DateInterval; |
13 | use DateTimeImmutable; |
14 | use Iterator; |
15 | use MediaWiki\Context\RequestContext; |
16 | use MediaWiki\Deferred\DeferredUpdates; |
17 | use MediaWiki\Extension\DiscussionTools\CommentUtils; |
18 | use MediaWiki\Extension\DiscussionTools\ContentThreadItemSet; |
19 | use MediaWiki\Extension\DiscussionTools\Hooks\HookUtils; |
20 | use MediaWiki\Extension\DiscussionTools\SubscriptionItem; |
21 | use MediaWiki\Extension\DiscussionTools\SubscriptionStore; |
22 | use MediaWiki\Extension\DiscussionTools\ThreadItem\CommentItem; |
23 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem; |
24 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentHeadingItem; |
25 | use MediaWiki\Extension\DiscussionTools\ThreadItem\ContentThreadItem; |
26 | use MediaWiki\Extension\DiscussionTools\ThreadItem\HeadingItem; |
27 | use MediaWiki\Extension\EventLogging\EventLogging; |
28 | use MediaWiki\Extension\EventLogging\Libs\UserBucketProvider\UserBucketProvider; |
29 | use MediaWiki\Extension\Notifications\Model\Event; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Page\PageIdentity; |
32 | use MediaWiki\Registration\ExtensionRegistry; |
33 | use MediaWiki\Revision\RevisionRecord; |
34 | use MediaWiki\Title\Title; |
35 | use MediaWiki\User\UserIdentity; |
36 | use Wikimedia\Assert\Assert; |
37 | use Wikimedia\Parsoid\Core\ResourceLimitExceededException; |
38 | use Wikimedia\Parsoid\Utils\DOMCompat; |
39 | use Wikimedia\Parsoid\Utils\DOMUtils; |
40 | use Wikimedia\Rdbms\IDBAccessObject; |
41 | |
42 | class EventDispatcher { |
43 | /** |
44 | * @throws ResourceLimitExceededException |
45 | */ |
46 | private static function getParsedRevision( RevisionRecord $revRecord ): ContentThreadItemSet { |
47 | return HookUtils::parseRevisionParsoidHtml( $revRecord, __METHOD__ ); |
48 | } |
49 | |
50 | /** |
51 | * @throws ResourceLimitExceededException |
52 | */ |
53 | public static function generateEventsForRevision( array &$events, RevisionRecord $newRevRecord ): void { |
54 | $services = MediaWikiServices::getInstance(); |
55 | |
56 | $title = Title::newFromLinkTarget( |
57 | $newRevRecord->getPageAsLinkTarget() |
58 | ); |
59 | if ( !HookUtils::isAvailableForTitle( $title ) ) { |
60 | // Not a talk page |
61 | return; |
62 | } |
63 | |
64 | $user = $newRevRecord->getUser(); |
65 | if ( !$user ) { |
66 | // User can be null if the user is deleted, but this is unlikely |
67 | // to be the case if the user just made an edit |
68 | return; |
69 | } |
70 | |
71 | $revisionStore = $services->getRevisionStore(); |
72 | $oldRevRecord = $revisionStore->getPreviousRevision( $newRevRecord, IDBAccessObject::READ_LATEST ); |
73 | |
74 | if ( $oldRevRecord !== null ) { |
75 | $oldItemSet = static::getParsedRevision( $oldRevRecord ); |
76 | } else { |
77 | // Page creation |
78 | $doc = DOMUtils::parseHTML( '' ); |
79 | $container = DOMCompat::getBody( $doc ); |
80 | $oldItemSet = $services->getService( 'DiscussionTools.CommentParser' ) |
81 | ->parse( $container, $title->getTitleValue() ); |
82 | } |
83 | $newItemSet = static::getParsedRevision( $newRevRecord ); |
84 | |
85 | static::generateEventsFromItemSets( $events, $oldItemSet, $newItemSet, $newRevRecord, $title, $user ); |
86 | } |
87 | |
88 | /** |
89 | * For each level 2 heading, get a list of comments in the thread grouped by names, then IDs. |
90 | * (Compare by name first, as ID could be changed by a parent comment being moved/deleted.) |
91 | * Comments in level 3+ sub-threads are grouped together with the parent thread. |
92 | * |
93 | * For any other headings (including level 3+ before the first level 2 heading, level 1, and |
94 | * section zero placeholder headings), ignore comments in those threads. |
95 | * |
96 | * @param ContentThreadItem[] $items |
97 | * @return ContentCommentItem[][][] |
98 | */ |
99 | private static function groupCommentsByThreadAndName( array $items ): array { |
100 | $comments = []; |
101 | $threadName = null; |
102 | foreach ( $items as $item ) { |
103 | if ( $item instanceof HeadingItem && ( $item->getHeadingLevel() < 2 || $item->isPlaceholderHeading() ) ) { |
104 | $threadName = null; |
105 | } elseif ( $item instanceof HeadingItem && $item->getHeadingLevel() === 2 ) { |
106 | $threadName = $item->getName(); |
107 | } elseif ( $item instanceof CommentItem && $threadName !== null ) { |
108 | $comments[ $threadName ][ $item->getName() ][ $item->getId() ] = $item; |
109 | } |
110 | } |
111 | return $comments; |
112 | } |
113 | |
114 | /** |
115 | * Get a list of all subscribable headings, grouped by name in case there are duplicates. |
116 | * |
117 | * @param ContentHeadingItem[] $items |
118 | * @return ContentHeadingItem[][] |
119 | */ |
120 | private static function groupSubscribableHeadings( array $items ): array { |
121 | $headings = []; |
122 | foreach ( $items as $item ) { |
123 | if ( $item->isSubscribable() ) { |
124 | $headings[ $item->getName() ][ $item->getId() ] = $item; |
125 | } |
126 | } |
127 | return $headings; |
128 | } |
129 | |
130 | /** |
131 | * Compare two lists of thread items, return those in $new but not $old. |
132 | * |
133 | * @param ContentThreadItem[][] $old |
134 | * @param ContentThreadItem[][] $new |
135 | * @return iterable<ContentThreadItem> |
136 | */ |
137 | private static function findAddedItems( array $old, array $new ) { |
138 | foreach ( $new as $itemName => $nameNewItems ) { |
139 | // Usually, there will be 0 or 1 $nameNewItems, and 0 $nameOldItems, |
140 | // and $addedCount will be 0 or 1. |
141 | // |
142 | // But when multiple replies are added in one edit, or in multiple edits within the same |
143 | // minute, there may be more, and the complex logic below tries to make the best guess |
144 | // as to which items are actually new. See the 'multiple' and 'sametime' test cases. |
145 | // |
146 | $nameOldItems = $old[ $itemName ] ?? []; |
147 | $addedCount = count( $nameNewItems ) - count( $nameOldItems ); |
148 | |
149 | if ( $addedCount > 0 ) { |
150 | // For any name that occurs more times in new than old, report that many new items, |
151 | // preferring IDs that did not occur in old, then preferring items lower on the page. |
152 | foreach ( array_reverse( $nameNewItems ) as $itemId => $newItem ) { |
153 | if ( $addedCount > 0 && !isset( $nameOldItems[ $itemId ] ) ) { |
154 | yield $newItem; |
155 | $addedCount--; |
156 | } |
157 | } |
158 | foreach ( array_reverse( $nameNewItems ) as $itemId => $newItem ) { |
159 | if ( $addedCount > 0 ) { |
160 | yield $newItem; |
161 | $addedCount--; |
162 | } |
163 | } |
164 | Assert::postcondition( $addedCount === 0, 'Reported expected number of items' ); |
165 | } |
166 | } |
167 | } |
168 | |
169 | /** |
170 | * Helper for generateEventsForRevision(), separated out for easier testing. |
171 | */ |
172 | protected static function generateEventsFromItemSets( |
173 | array &$events, |
174 | ContentThreadItemSet $oldItemSet, |
175 | ContentThreadItemSet $newItemSet, |
176 | RevisionRecord $newRevRecord, |
177 | PageIdentity $title, |
178 | UserIdentity $user |
179 | ): void { |
180 | $newComments = static::groupCommentsByThreadAndName( $newItemSet->getThreadItems() ); |
181 | $oldComments = static::groupCommentsByThreadAndName( $oldItemSet->getThreadItems() ); |
182 | $addedComments = []; |
183 | foreach ( $newComments as $threadName => $threadNewComments ) { |
184 | $threadOldComments = $oldComments[ $threadName ] ?? []; |
185 | foreach ( static::findAddedItems( $threadOldComments, $threadNewComments ) as $newComment ) { |
186 | Assert::precondition( $newComment instanceof ContentCommentItem, 'Must be ContentCommentItem' ); |
187 | $addedComments[] = $newComment; |
188 | } |
189 | } |
190 | |
191 | $newHeadings = static::groupSubscribableHeadings( $newItemSet->getThreads() ); |
192 | $oldHeadings = static::groupSubscribableHeadings( $oldItemSet->getThreads() ); |
193 | |
194 | $addedHeadings = []; |
195 | foreach ( static::findAddedItems( $oldHeadings, $newHeadings ) as $newHeading ) { |
196 | Assert::precondition( $newHeading instanceof ContentHeadingItem, 'Must be ContentHeadingItem' ); |
197 | $addedHeadings[] = $newHeading; |
198 | } |
199 | |
200 | $removedHeadings = []; |
201 | // Pass swapped parameters to findAddedItems() to find *removed* items |
202 | foreach ( static::findAddedItems( $newHeadings, $oldHeadings ) as $oldHeading ) { |
203 | Assert::precondition( $oldHeading instanceof ContentHeadingItem, 'Must be ContentHeadingItem' ); |
204 | $removedHeadings[] = $oldHeading; |
205 | } |
206 | |
207 | $mentionedUsers = []; |
208 | foreach ( $events as &$event ) { |
209 | if ( $event['type'] === 'mention' || $event['type'] === 'mention-summary' ) { |
210 | // Save mentioned users in our events, so that we can exclude them from our notification, |
211 | // to avoid duplicate notifications for a single comment. |
212 | // Array is keyed by user id so we can do a simple array merge. |
213 | $mentionedUsers += $event['extra']['mentioned-users']; |
214 | } |
215 | |
216 | if ( count( $addedComments ) === 1 ) { |
217 | // If this edit was a new user talk message according to Echo, |
218 | // and we also found exactly one new comment, |
219 | // add some extra information to the edit-user-talk event. |
220 | if ( $event['type'] === 'edit-user-talk' ) { |
221 | $event['extra'] += [ |
222 | 'comment-id' => $addedComments[0]->getId(), |
223 | 'comment-name' => $addedComments[0]->getName(), |
224 | 'content' => $addedComments[0]->getBodyText( true ), |
225 | ]; |
226 | } |
227 | |
228 | // Similarly for mentions. |
229 | // We don't handle 'content' in this case, as Echo makes its own snippets. |
230 | if ( $event['type'] === 'mention' ) { |
231 | $event['extra'] += [ |
232 | 'comment-id' => $addedComments[0]->getId(), |
233 | 'comment-name' => $addedComments[0]->getName(), |
234 | ]; |
235 | } |
236 | } |
237 | } |
238 | |
239 | if ( $addedComments ) { |
240 | // It's a bit weird to do this here, in the middle of the hook handler for Echo. However: |
241 | // * Echo calls this from a PageSaveComplete hook handler as a DeferredUpdate, |
242 | // which is exactly how we would do this otherwise |
243 | // * It allows us to reuse the generated comment trees without any annoying caching |
244 | static::addCommentChangeTag( $newRevRecord ); |
245 | // For very similar reasons, we do logging here |
246 | static::logAddedComments( $addedComments, $newRevRecord, $title, $user ); |
247 | } |
248 | |
249 | foreach ( $addedComments as $newComment ) { |
250 | // Ignore comments by other users, e.g. in case of reverts or a discussion being moved. |
251 | // TODO: But what about someone signing another's comment? |
252 | if ( $newComment->getAuthor() !== $user->getName() ) { |
253 | continue; |
254 | } |
255 | // Ignore comments which are more than 10 minutes old, as this may be a user archiving |
256 | // their own comment. (T290803) |
257 | $revTimestamp = new DateTimeImmutable( $newRevRecord->getTimestamp() ); |
258 | $threshold = $revTimestamp->sub( new DateInterval( 'PT10M' ) ); |
259 | if ( $newComment->getTimestamp() <= $threshold ) { |
260 | continue; |
261 | } |
262 | $heading = $newComment->getSubscribableHeading(); |
263 | if ( !$heading ) { |
264 | continue; |
265 | } |
266 | $events[] = [ |
267 | // This probably should've been called "dt-new-comment": this code is |
268 | // unaware if there are any subscriptions to the containing topic and |
269 | // an event is generated for every comment posted. |
270 | // However, changing this would require a complex migration. |
271 | 'type' => 'dt-subscribed-new-comment', |
272 | 'title' => $title, |
273 | 'extra' => [ |
274 | 'subscribed-comment-name' => $heading->getName(), |
275 | 'comment-id' => $newComment->getId(), |
276 | 'comment-name' => $newComment->getName(), |
277 | 'content' => $newComment->getBodyText( true ), |
278 | 'section-title' => $heading->getLinkableTitle(), |
279 | 'revid' => $newRevRecord->getId(), |
280 | 'mentioned-users' => $mentionedUsers, |
281 | ], |
282 | 'agent' => $user, |
283 | ]; |
284 | |
285 | $titleForSubscriptions = Title::castFromPageIdentity( $title )->createFragmentTarget( $heading->getText() ); |
286 | static::addAutoSubscription( $user, $titleForSubscriptions, $heading->getName() ); |
287 | } |
288 | |
289 | foreach ( $removedHeadings as $oldHeading ) { |
290 | $events[] = [ |
291 | 'type' => 'dt-removed-topic', |
292 | 'title' => $title, |
293 | 'extra' => [ |
294 | 'subscribed-comment-name' => $oldHeading->getName(), |
295 | 'heading-id' => $oldHeading->getId(), |
296 | 'heading-name' => $oldHeading->getName(), |
297 | 'section-title' => $oldHeading->getLinkableTitle(), |
298 | 'revid' => $newRevRecord->getId(), |
299 | ], |
300 | 'agent' => $user, |
301 | ]; |
302 | } |
303 | |
304 | $titleObj = Title::castFromPageIdentity( $title ); |
305 | if ( $titleObj ) { |
306 | foreach ( $addedHeadings as $newHeading ) { |
307 | // Don't use $event here as that already exists as a reference from above |
308 | $addTopicEvent = [ |
309 | 'type' => 'dt-added-topic', |
310 | 'title' => $title, |
311 | 'extra' => [ |
312 | // As no one can be subscribed to a topic before it has been created, |
313 | // we will notify users who have subscribed to the whole page. |
314 | 'subscribed-comment-name' => CommentUtils::getNewTopicsSubscriptionId( $titleObj ), |
315 | 'heading-id' => $newHeading->getId(), |
316 | 'heading-name' => $newHeading->getName(), |
317 | 'section-title' => $newHeading->getLinkableTitle(), |
318 | 'revid' => $newRevRecord->getId(), |
319 | ], |
320 | 'agent' => $user, |
321 | ]; |
322 | // Add metadata about the accompanying comment |
323 | $firstComment = $newHeading->getOldestReply(); |
324 | if ( $firstComment ) { |
325 | $addTopicEvent['extra']['comment-id'] = $firstComment->getId(); |
326 | $addTopicEvent['extra']['comment-name'] = $firstComment->getName(); |
327 | $addTopicEvent['extra']['content'] = $firstComment->getBodyText( true ); |
328 | } |
329 | $events[] = $addTopicEvent; |
330 | } |
331 | } |
332 | } |
333 | |
334 | /** |
335 | * Add our change tag for a revision that adds new comments. |
336 | */ |
337 | protected static function addCommentChangeTag( RevisionRecord $newRevRecord ): void { |
338 | // Unclear if DeferredUpdates::addCallableUpdate() is needed, |
339 | // but every extension does it that way. |
340 | DeferredUpdates::addCallableUpdate( static function () use ( $newRevRecord ) { |
341 | MediaWikiServices::getInstance()->getChangeTagsStore() |
342 | ->addTags( [ 'discussiontools-added-comment' ], null, $newRevRecord->getId() ); |
343 | } ); |
344 | } |
345 | |
346 | /** |
347 | * Add an automatic subscription to the given item, assuming the user has automatic subscriptions |
348 | * enabled. |
349 | */ |
350 | protected static function addAutoSubscription( UserIdentity $user, Title $title, string $itemName ): void { |
351 | $dtConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'discussiontools' ); |
352 | |
353 | if ( |
354 | $dtConfig->get( 'DiscussionToolsAutoTopicSubEditor' ) === 'any' && |
355 | HookUtils::shouldAddAutoSubscription( $user, $title ) |
356 | ) { |
357 | /** @var SubscriptionStore $parser */ |
358 | $subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' ); |
359 | $subscriptionStore->addAutoSubscriptionForUser( $user, $title, $itemName ); |
360 | } |
361 | } |
362 | |
363 | /** |
364 | * Return all users subscribed to a comment |
365 | * |
366 | * @param Event $event |
367 | * @param int $batchSize |
368 | * @return UserIdentity[]|Iterator<UserIdentity> |
369 | */ |
370 | public static function locateSubscribedUsers( Event $event, $batchSize = 500 ) { |
371 | $commentName = $event->getExtraParam( 'subscribed-comment-name' ); |
372 | |
373 | /** @var SubscriptionStore $subscriptionStore */ |
374 | $subscriptionStore = MediaWikiServices::getInstance()->getService( 'DiscussionTools.SubscriptionStore' ); |
375 | $subscriptionItems = $subscriptionStore->getSubscriptionItemsForTopic( |
376 | $commentName, |
377 | [ SubscriptionStore::STATE_SUBSCRIBED, SubscriptionStore::STATE_AUTOSUBSCRIBED ] |
378 | ); |
379 | |
380 | // Update notified timestamps |
381 | $subscriptionStore->updateSubscriptionNotifiedTimestamp( |
382 | null, |
383 | $commentName |
384 | ); |
385 | |
386 | // TODD: Have this return an Iterator instead? |
387 | $users = array_map( static function ( SubscriptionItem $item ) { |
388 | return $item->getUserIdentity(); |
389 | }, $subscriptionItems ); |
390 | |
391 | return $users; |
392 | } |
393 | |
394 | /** |
395 | * Log stuff to EventLogging's Schema:TalkPageEvent |
396 | * If you don't have EventLogging installed, does nothing. |
397 | * |
398 | * @param array $addedComments |
399 | * @param RevisionRecord $newRevRecord |
400 | * @param PageIdentity $title |
401 | * @param UserIdentity $identity |
402 | * @return bool Whether events were logged |
403 | */ |
404 | protected static function logAddedComments( |
405 | array $addedComments, |
406 | RevisionRecord $newRevRecord, |
407 | PageIdentity $title, |
408 | UserIdentity $identity |
409 | ): bool { |
410 | global $wgDTSchemaEditAttemptStepOversample, $wgDBname; |
411 | $context = RequestContext::getMain(); |
412 | $request = $context->getRequest(); |
413 | // We've reached here through Echo's post-save deferredupdate, which |
414 | // might either be after an API request from DiscussionTools or a |
415 | // regular POST from WikiEditor. Both should have this value snuck |
416 | // into their request if their session is being logged. |
417 | if ( !$request->getCheck( 'editingStatsId' ) ) { |
418 | return false; |
419 | } |
420 | $editingStatsId = $request->getVal( 'editingStatsId' ); |
421 | $isDiscussionTools = $request->getCheck( 'dttags' ); |
422 | |
423 | $extensionRegistry = ExtensionRegistry::getInstance(); |
424 | if ( !$extensionRegistry->isLoaded( 'EventLogging' ) ) { |
425 | return false; |
426 | } |
427 | if ( !$extensionRegistry->isLoaded( 'WikimediaEvents' ) ) { |
428 | return false; |
429 | } |
430 | $inSample = static::inEventSample( $editingStatsId ); |
431 | $shouldOversample = ( $isDiscussionTools && $wgDTSchemaEditAttemptStepOversample ) || ( |
432 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
433 | \WikimediaEvents\WikimediaEventsHooks::shouldSchemaEditAttemptStepOversample( $context ) |
434 | ); |
435 | if ( !$inSample && !$shouldOversample ) { |
436 | return false; |
437 | } |
438 | |
439 | $services = MediaWikiServices::getInstance(); |
440 | $editTracker = $services->getUserEditTracker(); |
441 | $userIdentityUtils = $services->getUserIdentityUtils(); |
442 | |
443 | $commonData = [ |
444 | '$schema' => '/analytics/mediawiki/talk_page_edit/1.2.0', |
445 | 'action' => 'publish', |
446 | 'session_id' => $editingStatsId, |
447 | 'page_id' => $newRevRecord->getPageId(), |
448 | 'page_namespace' => $title->getNamespace(), |
449 | 'revision_id' => $newRevRecord->getId() ?: 0, |
450 | 'performer' => [ |
451 | // Note: we're logging the user who made the edit, not the user who's signed on the comment |
452 | 'user_id' => $identity->getId(), |
453 | 'user_edit_count' => $editTracker->getUserEditCount( $identity ) ?: 0, |
454 | // Retention-safe values: |
455 | 'user_is_anonymous' => !$identity->isRegistered(), |
456 | 'user_is_temp' => $userIdentityUtils->isTemp( $identity ), |
457 | 'user_edit_count_bucket' => UserBucketProvider::getUserEditCountBucket( $identity ) ?: 'N/A', |
458 | ], |
459 | 'database' => $wgDBname, |
460 | // This is unreliable, but sufficient for our purposes; we |
461 | // mostly just want to see the difference between DT and |
462 | // everything-else: |
463 | 'integration' => $isDiscussionTools ? 'discussiontools' : 'page', |
464 | ]; |
465 | |
466 | foreach ( $addedComments as $comment ) { |
467 | $heading = $comment->getSubscribableHeading(); |
468 | $parent = $comment->getParent(); |
469 | if ( !$heading || !$parent ) { |
470 | continue; |
471 | } |
472 | if ( $parent->getType() === 'heading' ) { |
473 | if ( count( $heading->getReplies() ) === 1 ) { |
474 | // A new heading was added when this comment was created |
475 | $component_type = 'topic'; |
476 | } else { |
477 | $component_type = 'comment'; |
478 | } |
479 | } else { |
480 | $component_type = 'response'; |
481 | } |
482 | EventLogging::submit( 'mediawiki.talk_page_edit', array_merge( $commonData, [ |
483 | 'component_type' => $component_type, |
484 | 'topic_id' => $heading->getId(), |
485 | 'comment_id' => $comment->getId(), |
486 | 'comment_parent_id' => $parent->getId(), |
487 | ] ) ); |
488 | } |
489 | |
490 | return true; |
491 | } |
492 | |
493 | /** |
494 | * Should the current session be sampled for EventLogging? |
495 | * |
496 | * @param string $sessionId |
497 | * @return bool Whether to sample the session |
498 | */ |
499 | protected static function inEventSample( string $sessionId ): bool { |
500 | global $wgDTSchemaEditAttemptStepSamplingRate, $wgWMESchemaEditAttemptStepSamplingRate; |
501 | // Sample 6.25% |
502 | $samplingRate = 0.0625; |
503 | if ( isset( $wgDTSchemaEditAttemptStepSamplingRate ) ) { |
504 | $samplingRate = $wgDTSchemaEditAttemptStepSamplingRate; |
505 | } |
506 | if ( isset( $wgWMESchemaEditAttemptStepSamplingRate ) ) { |
507 | $samplingRate = $wgWMESchemaEditAttemptStepSamplingRate; |
508 | } |
509 | if ( $samplingRate === 0 ) { |
510 | return false; |
511 | } |
512 | $inSample = EventLogging::sessionInSample( |
513 | (int)( 1 / $samplingRate ), $sessionId |
514 | ); |
515 | return $inSample; |
516 | } |
517 | |
518 | } |