Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
EventBusEventSubmitter
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
3 / 3
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 submit
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 prepareEvent
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\EventLogging\EventSubmitter;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Deferred\DeferredUpdates;
7use MediaWiki\Extension\EventBus\EventBus;
8use MediaWiki\MainConfigNames;
9use MediaWiki\MediaWikiServices;
10use Psr\Log\LoggerInterface;
11
12/**
13 * Submits events to an instance of EventGate via EventBus.
14 *
15 * @see https://wikitech.wikimedia.org/wiki/Event_Platform/EventGate
16 * @see https://www.mediawiki.org/wiki/Extension:EventBus
17 */
18class EventBusEventSubmitter implements EventSubmitter {
19
20    /** @var LoggerInterface */
21    private $logger;
22
23    /** @var string */
24    private $domain;
25
26    public function __construct( LoggerInterface $logger, Config $config ) {
27        $this->logger = $logger;
28        $this->domain = $config->get( MainConfigNames::ServerName );
29    }
30
31    /**
32     * @inheritDoc
33     */
34    public function submit( string $streamName, array $event ): void {
35        if ( !isset( $event['$schema'] ) ) {
36            $this->logger->warning(
37                'Event data is missing required field "$schema"',
38                [ 'event' => $event ]
39            );
40
41            return;
42        }
43
44        $event = $this->prepareEvent( $streamName, $event );
45        $logger = $this->logger;
46
47        DeferredUpdates::addCallableUpdate( static function () use ( $streamName, $event, $logger ) {
48            $services = MediaWikiServices::getInstance();
49            $streamConfigs = $services->getService( 'EventLogging.StreamConfigs' );
50
51            if ( $streamConfigs !== false && !array_key_exists( $streamName, $streamConfigs ) ) {
52                $logger->warning(
53                    'Event submitted for unregistered stream name "{streamName}"',
54                    [ 'streamName' => $streamName ]
55                );
56                return;
57            }
58
59            // @phan-suppress-next-line PhanUndeclaredClassMethod
60            EventBus::getInstanceForStream( $streamName )->send( [ $event ] );
61        } );
62    }
63
64    /**
65     * Prepares the event for submission by:
66     *
67     * 1. Setting the `meta.stream` required property;
68     * 2. Setting the `dt` required property, if it is not set; and
69     * 3. Setting the `http.request_headers.user_agent` and `meta.domain` properties, if they are
70     *    not set
71     *
72     * Note well that we test for the `meta.$schema` required property being set in
73     * {@link EventBusEventSubmitter::submit()} above.
74     *
75     * @see https://wikitech.wikimedia.org/wiki/Event_Platform/Schemas/Guidelines#Required_fields
76     *
77     * @param string $streamName
78     * @param array $event
79     * @return array The prepared event
80     */
81    private function prepareEvent( string $streamName, array $event ): array {
82        $defaults = [
83            'http' => [
84                'request_headers' => [
85                    'user-agent' => $_SERVER[ 'HTTP_USER_AGENT' ] ?? ''
86                ]
87            ],
88            'meta' => [
89                'domain' => $this->domain,
90            ],
91            'dt' => wfTimestamp( TS_ISO_8601 ),
92        ];
93
94        $requiredData = [
95            'meta' => [
96                'stream' => $streamName,
97            ],
98        ];
99
100        $event = array_replace_recursive(
101            $defaults,
102            $event,
103            $requiredData
104        );
105
106        //
107        // If this is a migrated legacy event, client_dt will have been set already by
108        // EventLogging::encapsulate, and the dt field should be left unset so that it can be set
109        // to the intake time by EventGate. If dt was set by a caller, we unset it here.
110        //
111        // If client_dt is absent, this schema is native to the Event Platform, and dt represents
112        // the client-side event time. We set it here, overwriting any caller-provided value to
113        // ensure consistency.
114        //
115        // https://phabricator.wikimedia.org/T277253
116        // https://phabricator.wikimedia.org/T277330
117        //
118        if ( isset( $event['client_dt'] ) ) {
119            unset( $event['dt'] );
120        }
121
122        return $event;
123    }
124}