Passed
Pull Request — develop-v4 (#13)
by
unknown
20:44
created

Analytics   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 330
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 134
c 3
b 1
f 0
dl 0
loc 330
rs 6.4799
wmc 54

9 Methods

Rating   Name   Duplication   Size   Complexity  
A gaGenUUID() 0 20 1
A getSessionCookie() 0 19 5
A getUserId() 0 9 2
B getDocumentPathFromUrl() 0 34 9
B getClientId() 0 20 7
A getEventTrackingUrl() 0 20 1
A getTitleFromSeomatic() 0 21 5
D shouldSendAnalytics() 0 94 23
A getPageViewTrackingUrl() 0 18 1

How to fix   Complexity   

Complex Class

Complex classes like Analytics often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Analytics, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Instant Analytics plugin for Craft CMS
4
 *
5
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
6
 * @copyright Copyright (c) 2017 nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 2 should be the @author tag
Loading history...
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
7
 * @link      http://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @copyright tag
Loading history...
8
 * @package   InstantAnalytics
0 ignored issues
show
Coding Style introduced by
The tag in position 4 should be the @link tag
Loading history...
9
 * @since     1.0.0
10
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
11
12
namespace nystudio107\instantanalyticsGa4\helpers;
13
14
use Craft;
15
use craft\elements\User as UserElement;
16
use craft\helpers\StringHelper;
17
use craft\helpers\UrlHelper;
18
use craft\helpers\App;
19
use Jaybizzle\CrawlerDetect\CrawlerDetect;
20
use nystudio107\instantanalyticsGa4\InstantAnalytics;
21
use nystudio107\seomatic\Seomatic;
0 ignored issues
show
Bug introduced by
The type nystudio107\seomatic\Seomatic was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use yii\base\Exception;
23
24
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
25
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
26
 * @package   InstantAnalytics
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
27
 * @since     5.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
28
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
29
class Analytics
30
{
31
    /**
32
     * If SEOmatic is installed, set the page title from it
33
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
34
    public static function getTitleFromSeomatic(): ?string
35
    {
36
        if (!InstantAnalytics::$seomaticPlugin) {
37
            return null;
38
        }
39
        if (!Seomatic::$settings->renderEnabled) {
40
            return null;
41
        }
42
        $titleTag = Seomatic::$plugin->title->get('title');
43
44
        if ($titleTag === null) {
45
            return null;
46
        }
47
48
        $titleArray = $titleTag->renderAttributes();
49
50
        if (empty($titleArray['title'])) {
51
            return null;
52
        }
53
54
        return $titleArray['title'];
55
    }
56
57
    /**
58
     * Return a sanitized documentPath from a URL
59
     *
60
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
61
     *
62
     * @return string
63
     */
64
    public static function getDocumentPathFromUrl(string $url = ''): string
65
    {
66
        if ($url === '') {
67
            $url = Craft::$app->getRequest()->getFullPath();
68
        }
69
70
        // We want to send just a path to GA for page views
71
        if (UrlHelper::isAbsoluteUrl($url)) {
72
            $urlParts = parse_url($url);
73
            $url = $urlParts['path'] ?? '/';
74
            if (isset($urlParts['query'])) {
75
                $url .= '?' . $urlParts['query'];
76
            }
77
        }
78
79
        // We don't want to send protocol-relative URLs either
80
        if (UrlHelper::isProtocolRelativeUrl($url)) {
81
            $url = substr($url, 1);
82
        }
83
84
        // Strip the query string if that's the global config setting
85
        if (InstantAnalytics::$settings) {
86
            if (InstantAnalytics::$settings->stripQueryString !== null
87
                && InstantAnalytics::$settings->stripQueryString) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
88
                $url = UrlHelper::stripQueryString($url);
89
            }
90
        }
91
92
        // We always want the path to be / rather than empty
93
        if ($url === '') {
94
            $url = '/';
95
        }
96
97
        return $url;
98
    }
99
100
    /**
101
     * Get a PageView tracking URL
102
     *
103
     * @param $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
104
     * @param $title
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
105
     *
106
     * @return string
107
     * @throws Exception
108
     */
109
    public static function getPageViewTrackingUrl($url, $title): string
110
    {
111
        $urlParams = compact('url', 'title');
112
113
        $path = parse_url($url, PHP_URL_PATH);
114
        $pathFragments = explode('/', rtrim($path, '/'));
115
        $fileName = end($pathFragments);
116
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/pageViewTrack/' . $fileName, $urlParams);
117
118
        InstantAnalytics::$plugin->logAnalyticsEvent(
0 ignored issues
show
Bug introduced by
The method logAnalyticsEvent() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

118
        InstantAnalytics::$plugin->/** @scrutinizer ignore-call */ 
119
                                   logAnalyticsEvent(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119
            'Created pageViewTrackingUrl for: {trackingUrl}',
120
            [
121
                'trackingUrl' => $trackingUrl
122
            ],
123
            __METHOD__
124
        );
125
126
        return $trackingUrl;
127
    }
128
129
    /**
130
     * Get an Event tracking URL
131
     *
132
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
133
     * @param string $eventName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
134
     * @param array $params
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
135
     * @return string
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
136
     * @throws Exception
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
137
     */
138
    public static function getEventTrackingUrl(
139
        string $url,
140
        string $eventName,
141
        array $params = [],
142
    ): string
143
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
144
        $urlParams = compact('url', 'eventName', 'params');
145
146
        $fileName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
147
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . $fileName, $urlParams);
0 ignored issues
show
Bug introduced by
Are you sure $fileName of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

147
        $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . /** @scrutinizer ignore-type */ $fileName, $urlParams);
Loading history...
148
149
        InstantAnalytics::$plugin->logAnalyticsEvent(
150
            'Created eventTrackingUrl for: {trackingUrl}',
151
            [
152
                'trackingUrl' => $trackingUrl
153
            ],
154
            __METHOD__
155
        );
156
157
        return $trackingUrl;
158
    }
159
160
    /**
161
     * _shouldSendAnalytics determines whether we should be sending Google
162
     * Analytics data
163
     *
164
     * @return bool
165
     */
166
    public static function shouldSendAnalytics(): bool
167
    {
168
        $result = true;
169
        $request = Craft::$app->getRequest();
170
171
        $logExclusion = static function (string $setting)
172
        {
0 ignored issues
show
Coding Style introduced by
Opening brace should be on the same line as the declaration
Loading history...
173
            if (InstantAnalytics::$settings->logExcludedAnalytics) {
174
                $request = Craft::$app->getRequest();
175
                $requestIp = $request->getUserIP();
176
                InstantAnalytics::$plugin->logAnalyticsEvent(
177
                    'Analytics excluded for:: {requestIp} due to: `{setting}`',
178
                    compact('requestIp', 'setting'),
179
                    __METHOD__
180
                );
181
            }
182
        };
183
184
        if (!InstantAnalytics::$settings->sendAnalyticsData) {
185
            $logExclusion('sendAnalyticsData');
186
            return false;
187
        }
188
189
        if (!InstantAnalytics::$settings->sendAnalyticsInDevMode && Craft::$app->getConfig()->getGeneral()->devMode) {
190
            $logExclusion('sendAnalyticsInDevMode');
191
            return false;
192
        }
193
194
        if ($request->getIsConsoleRequest()) {
195
            $logExclusion('Craft::$app->getRequest()->getIsConsoleRequest()');
196
            return false;
197
        }
198
199
        if ($request->getIsCpRequest()) {
200
            $logExclusion('Craft::$app->getRequest()->getIsCpRequest()');
201
            return false;
202
        }
203
204
        if ($request->getIsLivePreview()) {
205
            $logExclusion('Craft::$app->getRequest()->getIsLivePreview()');
206
            return false;
207
        }
208
209
        // Check the $_SERVER[] super-global exclusions
210
        if (InstantAnalytics::$settings->serverExcludes !== null
211
            && is_array(InstantAnalytics::$settings->serverExcludes)) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
212
            foreach (InstantAnalytics::$settings->serverExcludes as $match => $matchArray) {
213
                if (isset($_SERVER[$match])) {
214
                    foreach ($matchArray as $matchItem) {
215
                        if (preg_match($matchItem, $_SERVER[$match])) {
216
                            $logExclusion('serverExcludes');
217
218
                            return false;
219
                        }
220
                    }
221
                }
222
            }
223
        }
224
225
        // Filter out bot/spam requests via UserAgent
226
        if (InstantAnalytics::$settings->filterBotUserAgents) {
227
            $crawlerDetect = new CrawlerDetect;
228
            // Check the user agent of the current 'visitor'
229
            if ($crawlerDetect->isCrawler()) {
230
                $logExclusion('filterBotUserAgents');
231
232
                return false;
233
            }
234
        }
235
236
        // Filter by user group
237
        $userService = Craft::$app->getUser();
238
        /** @var UserElement $user */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
239
        $user = $userService->getIdentity();
240
        if ($user) {
0 ignored issues
show
introduced by
$user is of type craft\elements\User, thus it always evaluated to true.
Loading history...
241
            if (InstantAnalytics::$settings->adminExclude && $user->admin) {
242
                $logExclusion('adminExclude');
243
244
                return false;
245
            }
246
247
            if (InstantAnalytics::$settings->groupExcludes !== null
248
                && is_array(InstantAnalytics::$settings->groupExcludes)) {
0 ignored issues
show
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
249
                foreach (InstantAnalytics::$settings->groupExcludes as $matchItem) {
250
                    if ($user->isInGroup($matchItem)) {
251
                        $logExclusion('groupExcludes');
252
253
                        return false;
254
                    }
255
                }
256
            }
257
        }
258
259
        return $result;
260
    }
261
262
    /**
263
     * getClientId handles the parsing of the _ga cookie or setting it to a
0 ignored issues
show
Coding Style introduced by
Doc comment short description must start with a capital letter
Loading history...
264
     * unique identifier
265
     *
266
     * @return string the cid
267
     */
268
    public static function getClientId(): string
269
    {
270
        $cid = '';
271
        if (isset($_COOKIE['_ga'])) {
272
            $parts = explode(".", $_COOKIE['_ga'], 4);
273
            if ($parts !== false) {
274
                $cid = implode('.', array_slice($parts, 2));
275
            }
276
        } elseif (isset($_COOKIE['_ia']) && $_COOKIE['_ia'] !== '') {
277
            $cid = $_COOKIE['_ia'];
278
        } else {
279
            // Generate our own client id, otherwise.
280
            $cid = static::gaGenUUID() . '.1';
281
        }
282
283
        if (InstantAnalytics::$settings->createGclidCookie && !empty($cid)) {
284
            setcookie('_ia', $cid, strtotime('+' . InstantAnalytics::$settings->sessionDuration . ' minutes'), '/'); // Two years
285
        }
286
287
        return $cid;
288
    }
289
290
    /**
291
     * getSessionCookie handles the parsing of the _ga_*MEASUREMENT ID* cookie
0 ignored issues
show
Coding Style introduced by
Doc comment short description must start with a capital letter
Loading history...
292
     * unique identifier of session ID & number
293
     *
294
     * @return null|array $sessionCookie
295
     */
296
    public static function getSessionCookie(): ?array
297
    {
298
        $measurementId = App::parseEnv(InstantAnalytics::$settings->googleAnalyticsMeasurementId);
299
        $cookieName = '_ga_' . StringHelper::removeLeft($measurementId, 'G-');
300
        if (isset($_COOKIE[$cookieName])) {
301
            $sessionCookie = null;
302
303
            $parts = explode(".", $_COOKIE[$cookieName], 5);
304
            if ($parts && count($parts) > 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
305
                $sessionCookie = implode('.', array_slice($parts, 2, 2));
306
            }
307
308
            if (str_contains($sessionCookie, '.')) {
0 ignored issues
show
Bug introduced by
It seems like $sessionCookie can also be of type null; however, parameter $haystack of str_contains() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

308
            if (str_contains(/** @scrutinizer ignore-type */ $sessionCookie, '.')) {
Loading history...
309
                [$sessionId, $sessionNumber] = explode('.', $sessionCookie);
0 ignored issues
show
Bug introduced by
It seems like $sessionCookie can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
                [$sessionId, $sessionNumber] = explode('.', /** @scrutinizer ignore-type */ $sessionCookie);
Loading history...
310
                return ['sessionId' => $sessionId, 'sessionNumber' => $sessionNumber];
311
            }
312
        }
313
314
        return null;
315
    }
316
317
    /**
318
     * Get the user id.
319
     *
320
     * @return string
321
     */
322
    public static function getUserId(): string
323
    {
324
        $userId = Craft::$app->getUser()->getId();
325
326
        if (!$userId) {
327
            return '';
328
        }
329
330
        return $userId;
331
    }
332
333
    /**
334
     * gaGenUUID Generate UUID v4 function - needed to generate a CID when one
0 ignored issues
show
Coding Style introduced by
Doc comment short description must start with a capital letter
Loading history...
335
     * isn't available
336
     *
337
     * @return string The generated UUID
338
     */
339
    protected static function gaGenUUID()
340
    {
341
        return sprintf(
342
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
343
            // 32 bits for "time_low"
344
            mt_rand(0, 0xffff),
345
            mt_rand(0, 0xffff),
346
            // 16 bits for "time_mid"
347
            mt_rand(0, 0xffff),
348
            // 16 bits for "time_hi_and_version",
349
            // four most significant bits holds version number 4
350
            mt_rand(0, 0x0fff) | 0x4000,
351
            // 16 bits, 8 bits for "clk_seq_hi_res",
352
            // 8 bits for "clk_seq_low",
353
            // two most significant bits holds zero and one for variant DCE1.1
354
            mt_rand(0, 0x3fff) | 0x8000,
355
            // 48 bits for "node"
356
            mt_rand(0, 0xffff),
357
            mt_rand(0, 0xffff),
358
            mt_rand(0, 0xffff)
359
        );
360
    }
361
}
362