1 | <?php |
||||||
2 | /** |
||||||
3 | * Instant Analytics plugin for Craft CMS |
||||||
4 | * |
||||||
5 | * @author nystudio107 |
||||||
0 ignored issues
–
show
Coding Style
introduced
by
![]() Content of the @author tag must be in the form "Display Name <[email protected]>"
![]() |
|||||||
6 | * @copyright Copyright (c) 2017 nystudio107 |
||||||
0 ignored issues
–
show
|
|||||||
7 | * @link http://nystudio107.com |
||||||
0 ignored issues
–
show
|
|||||||
8 | * @package InstantAnalytics |
||||||
0 ignored issues
–
show
|
|||||||
9 | * @since 1.0.0 |
||||||
10 | */ |
||||||
0 ignored issues
–
show
|
|||||||
11 | |||||||
12 | namespace nystudio107\instantanalyticsGa4\ga4; |
||||||
13 | |||||||
14 | use Br33f\Ga4\MeasurementProtocol\Dto\Event\AbstractEvent; |
||||||
15 | use Br33f\Ga4\MeasurementProtocol\Dto\Request\BaseRequest; |
||||||
16 | use Br33f\Ga4\MeasurementProtocol\Exception\HydrationException; |
||||||
17 | use Br33f\Ga4\MeasurementProtocol\Exception\ValidationException; |
||||||
18 | use Br33f\Ga4\MeasurementProtocol\HttpClient; |
||||||
19 | use Craft; |
||||||
20 | use craft\commerce\elements\Order; |
||||||
21 | use craft\commerce\elements\Product; |
||||||
22 | use craft\commerce\elements\Variant; |
||||||
23 | use craft\errors\MissingComponentException; |
||||||
24 | use craft\helpers\App; |
||||||
25 | use nystudio107\instantanalyticsGa4\helpers\Analytics as AnalyticsHelper; |
||||||
26 | use nystudio107\instantanalyticsGa4\InstantAnalytics; |
||||||
27 | use nystudio107\seomatic\Seomatic; |
||||||
28 | use yii\base\InvalidConfigException; |
||||||
29 | |||||||
30 | /** |
||||||
0 ignored issues
–
show
|
|||||||
31 | * @author nystudio107 |
||||||
0 ignored issues
–
show
Content of the @author tag must be in the form "Display Name <[email protected]>"
![]() |
|||||||
32 | * @package InstantAnalytics |
||||||
0 ignored issues
–
show
|
|||||||
33 | * @since 1.2.0 |
||||||
0 ignored issues
–
show
|
|||||||
34 | * |
||||||
35 | * @method Analytics setAllowGoogleSignals(string $value) |
||||||
36 | * @method Analytics setAllowAdPersonalizationSignals(string $value) |
||||||
37 | * @method Analytics setCampaignContent(string $value) |
||||||
38 | * @method Analytics setCampaignId(string $value) |
||||||
39 | * @method Analytics setCampaignMedium(string $value) |
||||||
40 | * @method Analytics setCampaignName(string $value) |
||||||
41 | * @method Analytics setCampaignSource(string $value) |
||||||
42 | * @method Analytics setCampaignTerm(string $value) |
||||||
43 | * @method Analytics setCampaign(string $value) |
||||||
44 | * @method Analytics setClientId(string $value) |
||||||
45 | * @method Analytics setContentGroup(string $value) |
||||||
46 | * @method Analytics setCookieDomain(string $value) |
||||||
47 | * @method Analytics setCookieExpires(string $value) |
||||||
48 | * @method Analytics setCookieFlags(string $value) |
||||||
49 | * @method Analytics setCookiePath(string $value) |
||||||
50 | * @method Analytics setCookiePrefix(string $value) |
||||||
51 | * @method Analytics setCookieUpdate(string $value) |
||||||
52 | * @method Analytics setLanguage(string $value) |
||||||
53 | * @method Analytics setPageLocation(string $value) |
||||||
54 | * @method Analytics setPageReferrer(string $value) |
||||||
55 | * @method Analytics setPageTitle(string $value) |
||||||
56 | * @method Analytics setSendPageView(string $value) |
||||||
57 | * @method Analytics setScreenResolution(string $value) |
||||||
58 | * @method Analytics setUserId(string $value) |
||||||
59 | */ |
||||||
0 ignored issues
–
show
|
|||||||
60 | class Analytics |
||||||
61 | { |
||||||
62 | /** |
||||||
0 ignored issues
–
show
|
|||||||
63 | * @var BaseRequest|null |
||||||
64 | */ |
||||||
65 | private ?BaseRequest $_request = null; |
||||||
66 | |||||||
67 | /** |
||||||
0 ignored issues
–
show
|
|||||||
68 | * @var Service|null|false |
||||||
69 | */ |
||||||
70 | private mixed $_service = null; |
||||||
71 | |||||||
72 | /** |
||||||
0 ignored issues
–
show
|
|||||||
73 | * @var string|null |
||||||
74 | */ |
||||||
75 | private ?string $_affiliation = null; |
||||||
76 | |||||||
77 | private ?bool $_shouldSendAnalytics = null; |
||||||
78 | |||||||
79 | private ?string $_sessionString = null; |
||||||
80 | |||||||
81 | private array $eventList = []; |
||||||
0 ignored issues
–
show
|
|||||||
82 | |||||||
83 | /** |
||||||
84 | * Component factory for creating events. |
||||||
85 | * |
||||||
86 | * @return ComponentFactory |
||||||
87 | */ |
||||||
88 | public function create(): ComponentFactory |
||||||
89 | { |
||||||
90 | return new ComponentFactory(); |
||||||
91 | } |
||||||
92 | |||||||
93 | /** |
||||||
94 | * Add an event to be sent to Google |
||||||
95 | * |
||||||
96 | * @param AbstractEvent $event |
||||||
0 ignored issues
–
show
|
|||||||
97 | * @return void |
||||||
0 ignored issues
–
show
|
|||||||
98 | */ |
||||||
99 | public function addEvent(AbstractEvent $event): void |
||||||
100 | { |
||||||
101 | if ($this->_sessionString === null) { |
||||||
102 | $this->_sessionString = AnalyticsHelper::getSessionString(); |
||||||
103 | } |
||||||
104 | |||||||
105 | if (str_contains($this->_sessionString, '.')) { |
||||||
0 ignored issues
–
show
It seems like
$this->_sessionString 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
![]() |
|||||||
106 | [$sessionId, $sessionNumber] = explode('.', $this->_sessionString); |
||||||
0 ignored issues
–
show
It seems like
$this->_sessionString 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
![]() |
|||||||
107 | $event->setParamValue('sessionId', $sessionId); |
||||||
108 | $event->setParamValue('sessionNumber', $sessionNumber); |
||||||
109 | } |
||||||
110 | |||||||
111 | $this->eventList[] = $event; |
||||||
112 | } |
||||||
113 | |||||||
114 | /** |
||||||
115 | * Send the events collected so far. |
||||||
116 | * |
||||||
117 | * @return ?array |
||||||
118 | * @throws HydrationException |
||||||
119 | * @throws ValidationException |
||||||
120 | */ |
||||||
121 | public function sendCollectedEvents(): ?array |
||||||
122 | { |
||||||
123 | if ($this->_shouldSendAnalytics === null) { |
||||||
124 | $this->_shouldSendAnalytics = AnalyticsHelper::shouldSendAnalytics(); |
||||||
125 | } |
||||||
126 | |||||||
127 | if (!$this->_shouldSendAnalytics) { |
||||||
0 ignored issues
–
show
The expression
$this->_shouldSendAnalytics of type boolean|null is loosely compared to false ; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.
If an expression can have both $a = canBeFalseAndNull();
// Instead of
if ( ! $a) { }
// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
![]() |
|||||||
128 | return null; |
||||||
129 | } |
||||||
130 | |||||||
131 | $service = $this->service(); |
||||||
132 | |||||||
133 | if (!$service) { |
||||||
134 | return null; |
||||||
135 | } |
||||||
136 | |||||||
137 | $request = $this->request(); |
||||||
138 | $eventCount = count($this->eventList); |
||||||
139 | |||||||
140 | if (!InstantAnalytics::$settings->sendAnalyticsData) { |
||||||
141 | InstantAnalytics::$plugin->logAnalyticsEvent( |
||||||
0 ignored issues
–
show
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
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. ![]() |
|||||||
142 | 'Analytics not enabled - skipped sending {count} events', |
||||||
143 | ['count' => $eventCount], |
||||||
144 | __METHOD__ |
||||||
145 | ); |
||||||
146 | |||||||
147 | return null; |
||||||
148 | } |
||||||
149 | |||||||
150 | if ($eventCount === 0) { |
||||||
151 | InstantAnalytics::$plugin->logAnalyticsEvent( |
||||||
152 | 'No events collected to send', |
||||||
153 | [], |
||||||
154 | __METHOD__ |
||||||
155 | ); |
||||||
156 | |||||||
157 | return null; |
||||||
158 | } |
||||||
159 | |||||||
160 | InstantAnalytics::$plugin->logAnalyticsEvent( |
||||||
161 | 'Sending {count} analytics events', |
||||||
162 | ['count' => $eventCount], |
||||||
163 | __METHOD__ |
||||||
164 | ); |
||||||
165 | |||||||
166 | // Batch into groups of 25 |
||||||
167 | $responses = []; |
||||||
168 | |||||||
169 | foreach (array_chunk($this->eventList, 25) as $chunk) { |
||||||
170 | $request->getEvents()->setEventList([]); |
||||||
171 | |||||||
172 | /** @var AbstractEvent $event */ |
||||||
0 ignored issues
–
show
|
|||||||
173 | foreach ($chunk as $event) { |
||||||
174 | $request->addEvent($event); |
||||||
175 | } |
||||||
176 | |||||||
177 | $responses[] = $service->send($request); |
||||||
178 | } |
||||||
179 | |||||||
180 | |||||||
181 | return $responses; |
||||||
182 | } |
||||||
183 | |||||||
184 | public function getAffiliation(): ?string |
||||||
0 ignored issues
–
show
|
|||||||
185 | { |
||||||
186 | return $this->_affiliation; |
||||||
187 | } |
||||||
188 | |||||||
189 | /** |
||||||
190 | * Set affiliation for all the events that incorporate Commerce Product info for the remaining duration of request. |
||||||
191 | * |
||||||
192 | * @param string $affiliation |
||||||
0 ignored issues
–
show
|
|||||||
193 | * @return $this |
||||||
0 ignored issues
–
show
|
|||||||
194 | */ |
||||||
195 | public function setAffiliation(string $affiliation): self |
||||||
196 | { |
||||||
197 | $this->_affiliation = $affiliation; |
||||||
198 | return $this; |
||||||
199 | } |
||||||
200 | |||||||
201 | /** |
||||||
202 | * Add a commerce item list impression. |
||||||
203 | * |
||||||
204 | * @param Product|Variant $productVariant |
||||||
0 ignored issues
–
show
|
|||||||
205 | * @param int $index |
||||||
0 ignored issues
–
show
|
|||||||
206 | * @param string $listName |
||||||
0 ignored issues
–
show
|
|||||||
207 | * @throws InvalidConfigException |
||||||
0 ignored issues
–
show
|
|||||||
208 | */ |
||||||
0 ignored issues
–
show
|
|||||||
209 | public function addCommerceProductImpression(Product|Variant $productVariant, int $index = 0, string $listName = 'default') |
||||||
210 | { |
||||||
211 | InstantAnalytics::$plugin->commerce->addCommerceProductImpression($productVariant); |
||||||
212 | } |
||||||
213 | |||||||
214 | /** |
||||||
215 | * Begin checkout. |
||||||
216 | * |
||||||
217 | * @param Order $cart |
||||||
0 ignored issues
–
show
|
|||||||
218 | */ |
||||||
0 ignored issues
–
show
|
|||||||
219 | public function beginCheckout(Order $cart) |
||||||
220 | { |
||||||
221 | InstantAnalytics::$plugin->commerce->triggerBeginCheckoutEvent($cart); |
||||||
222 | } |
||||||
223 | |||||||
224 | /** |
||||||
225 | * Add a commerce item list impression. |
||||||
226 | * |
||||||
227 | * @param Product|Variant $productVariant |
||||||
0 ignored issues
–
show
|
|||||||
228 | * @param int $index |
||||||
0 ignored issues
–
show
|
|||||||
229 | * @param string $listName |
||||||
0 ignored issues
–
show
|
|||||||
230 | * @throws InvalidConfigException |
||||||
0 ignored issues
–
show
|
|||||||
231 | * @deprecated `Analytics::addCommerceProductDetailView()` is deprecated. Use `Analytics::addCommerceProductImpression()` instead. |
||||||
0 ignored issues
–
show
|
|||||||
232 | */ |
||||||
0 ignored issues
–
show
|
|||||||
233 | public function addCommerceProductDetailView(Product|Variant $productVariant, int $index = 0, string $listName = 'default') |
||||||
234 | { |
||||||
235 | Craft::$app->getDeprecator()->log('Analytics::addCommerceProductDetailView()', '`Analytics::addCommerceProductDetailView()` is deprecated. Use `Analytics::addCommerceProductImpression()` instead.'); |
||||||
236 | $this->addCommerceProductImpression($productVariant); |
||||||
237 | } |
||||||
238 | |||||||
239 | /** |
||||||
240 | * Add a commerce product list impression. |
||||||
241 | * |
||||||
242 | * @param array $products |
||||||
0 ignored issues
–
show
|
|||||||
243 | * @param $listName |
||||||
0 ignored issues
–
show
|
|||||||
244 | */ |
||||||
0 ignored issues
–
show
|
|||||||
245 | public function addCommerceProductListImpression(array $products, string $listName = 'default') |
||||||
246 | { |
||||||
247 | InstantAnalytics::$plugin->commerce->addCommerceProductListImpression($products, $listName); |
||||||
248 | } |
||||||
249 | |||||||
250 | /** |
||||||
251 | * Set the measurement id. |
||||||
252 | * |
||||||
253 | * @param string $measurementId |
||||||
0 ignored issues
–
show
|
|||||||
254 | * @return $this |
||||||
0 ignored issues
–
show
|
|||||||
255 | */ |
||||||
256 | public function setMeasurementId(string $measurementId): self |
||||||
257 | { |
||||||
258 | $this->service()?->setMeasurementId($measurementId); |
||||||
259 | return $this; |
||||||
260 | } |
||||||
261 | |||||||
262 | /** |
||||||
263 | * Set the API secret. |
||||||
264 | * |
||||||
265 | * @param string $apiSecret |
||||||
0 ignored issues
–
show
|
|||||||
266 | * @return $this |
||||||
0 ignored issues
–
show
|
|||||||
267 | */ |
||||||
268 | public function setApiSecret(string $apiSecret): self |
||||||
269 | { |
||||||
270 | $this->service()?->setApiSecret($apiSecret); |
||||||
271 | return $this; |
||||||
272 | } |
||||||
273 | |||||||
274 | public function __call(string $methodName, array $arguments): ?self |
||||||
0 ignored issues
–
show
|
|||||||
275 | { |
||||||
276 | $knownProperties = [ |
||||||
277 | 'allowGoogleSignals' => 'allow_google_signals', |
||||||
278 | 'allowAdPersonalizationSignals' => 'allow_ad_personalization_signals', |
||||||
279 | 'campaignContent' => 'campaign_content', |
||||||
280 | 'campaignId' => 'campaign_id', |
||||||
281 | 'campaignMedium' => 'campaign_medium', |
||||||
282 | 'campaignName' => 'campaign_name', |
||||||
283 | 'campaignSource' => 'campaign_source', |
||||||
284 | 'campaignTerm' => 'campaign_term', |
||||||
285 | 'campaign' => 'campaign', |
||||||
286 | 'clientId' => 'client_id', |
||||||
287 | 'contentGroup' => 'content_group', |
||||||
288 | 'cookieDomain' => 'cookie_domain', |
||||||
289 | 'cookieExpires' => 'cookie_expires', |
||||||
290 | 'cookieFlags' => 'cookie_flags', |
||||||
291 | 'cookiePath' => 'cookie_path', |
||||||
292 | 'cookiePrefix' => 'cookie_prefix', |
||||||
293 | 'cookieUpdate' => 'cookie_update', |
||||||
294 | 'language' => 'language', |
||||||
295 | 'pageLocation' => 'page_location', |
||||||
296 | 'pageReferrer' => 'page_referrer', |
||||||
297 | 'pageTitle' => 'page_title', |
||||||
298 | 'sendPageView' => 'send_page_view', |
||||||
299 | 'screenResolution' => 'screen_resolution', |
||||||
300 | 'userId' => 'user_id', |
||||||
301 | ]; |
||||||
302 | |||||||
303 | if (str_starts_with($methodName, 'set')) { |
||||||
304 | $methodName = lcfirst(substr($methodName, 3)); |
||||||
305 | |||||||
306 | $service = $this->service(); |
||||||
307 | if ($service && !empty($knownProperties[$methodName])) { |
||||||
308 | $service->setAdditionalQueryParam($knownProperties[$methodName], $arguments[0]); |
||||||
309 | |||||||
310 | return $this; |
||||||
311 | } |
||||||
312 | } |
||||||
313 | |||||||
314 | return null; |
||||||
315 | } |
||||||
316 | |||||||
317 | public function request(): BaseRequest |
||||||
0 ignored issues
–
show
|
|||||||
318 | { |
||||||
319 | if ($this->_request === null) { |
||||||
320 | $this->_request = new BaseRequest(); |
||||||
321 | |||||||
322 | $this->_request->setClientId(AnalyticsHelper::getClientId()); |
||||||
0 ignored issues
–
show
The method
setClientId() 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
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. ![]() |
|||||||
323 | |||||||
324 | if (InstantAnalytics::$settings->sendUserId) { |
||||||
325 | $userId = AnalyticsHelper::getUserId(); |
||||||
326 | |||||||
327 | if ($userId) { |
||||||
328 | $this->request()->setUserId($userId); |
||||||
329 | } |
||||||
330 | } |
||||||
331 | } |
||||||
332 | |||||||
333 | |||||||
334 | return $this->_request; |
||||||
0 ignored issues
–
show
|
|||||||
335 | } |
||||||
336 | |||||||
337 | /** |
||||||
338 | * Init the service used to send events |
||||||
339 | */ |
||||||
0 ignored issues
–
show
|
|||||||
340 | public function init(): void |
||||||
341 | { |
||||||
342 | $this->service(); |
||||||
343 | $this->request(); |
||||||
344 | } |
||||||
345 | |||||||
346 | protected function service(): ?Service |
||||||
0 ignored issues
–
show
|
|||||||
347 | { |
||||||
348 | if ($this->_service === null) { |
||||||
349 | $settings = InstantAnalytics::$settings; |
||||||
350 | $apiSecret = App::parseEnv($settings->googleAnalyticsMeasurementApiSecret); |
||||||
351 | $measurementId = App::parseEnv($settings->googleAnalyticsMeasurementId); |
||||||
352 | |||||||
353 | if (empty($apiSecret) || empty($measurementId)) { |
||||||
354 | InstantAnalytics::$plugin->logAnalyticsEvent( |
||||||
355 | 'API secret or measurement ID not set up for Instant Analytics', |
||||||
356 | [], |
||||||
357 | __METHOD__ |
||||||
358 | ); |
||||||
359 | $this->_service = false; |
||||||
360 | |||||||
361 | return null; |
||||||
362 | } |
||||||
363 | $this->_service = new Service($apiSecret, $measurementId); |
||||||
364 | |||||||
365 | $ga4Client = new HttpClient(); |
||||||
366 | $ga4Client->setClient(Craft::createGuzzleClient()); |
||||||
367 | $this->_service->setHttpClient($ga4Client); |
||||||
368 | |||||||
369 | $request = Craft::$app->getRequest(); |
||||||
370 | try { |
||||||
371 | $session = Craft::$app->getSession(); |
||||||
372 | } catch (MissingComponentException $exception) { |
||||||
373 | $session = null; |
||||||
374 | } |
||||||
375 | |||||||
376 | $this->setPageReferrer($request->getReferrer()); |
||||||
377 | |||||||
378 | // Load any campaign values from session or request |
||||||
379 | $campaignParams = [ |
||||||
380 | 'utm_source' => 'CampaignSource', |
||||||
381 | 'utm_medium' => 'CampaignMedium', |
||||||
382 | 'utm_campaign' => 'CampaignName', |
||||||
383 | 'utm_content' => 'CampaignContent', |
||||||
384 | 'utm_term' => 'CampaignTerm', |
||||||
385 | ]; |
||||||
386 | |||||||
387 | // Load them up for GA4 |
||||||
388 | foreach ($campaignParams as $key => $method) { |
||||||
389 | $value = $request->getParam($key) ?? $session->get($key) ?? null; |
||||||
390 | $method = 'set' . $method; |
||||||
391 | |||||||
392 | $this->$method($value); |
||||||
393 | |||||||
394 | if ($session && $value) { |
||||||
395 | $session->set($key, $value); |
||||||
396 | } |
||||||
397 | } |
||||||
398 | |||||||
399 | // If SEOmatic is installed, set the affiliation as well |
||||||
400 | if (InstantAnalytics::$seomaticPlugin && Seomatic::$settings->renderEnabled && Seomatic::$plugin->metaContainers->metaSiteVars !== null) { |
||||||
401 | $siteName = Seomatic::$plugin->metaContainers->metaSiteVars->siteName; |
||||||
402 | $this->setAffiliation($siteName); |
||||||
403 | } |
||||||
404 | } |
||||||
405 | |||||||
406 | if ($this->_service === false) { |
||||||
407 | return null; |
||||||
408 | } |
||||||
409 | |||||||
410 | return $this->_service; |
||||||
411 | } |
||||||
412 | } |
||||||
413 |