[DigitalPoint] Better Google Analytics

[DigitalPoint] Better Google Analytics 1.1.3

No permission to download
How does the "Host Google Tag Manager JavaScript on your own domain" work? I think this tries to load /jc/gtm.js ? Is that dynamically generated? I don't the addon is able to create it for me, as there's no such file
 
Heh, I didn't remember that I had created special rule for serving *.js files, had to create exempt for that. Once again, can only blame myself 😅
 
Heh, I didn't remember that I had created special rule for serving *.js files, had to create exempt for that. Once again, can only blame myself 😅
Probably better to apply special rules that route a request away from the application (XenForo) only if the file exists in the filesystem. I mean if you want to do it “right”. 😀

XenForo can handle any extension, so not sending certain requests to XenForo based on extension isn’t the best, so a check on that logic if the file exists or not would be better (I know, just repeating).
 
  • GuzzleHttp\Exception\ServerException: XF\Job\MailSend: Server error: POST https://www.google-analytics.com/mp/collect?measurement_id=G-xxx&api_secret=xxx resulted in a 502 Bad Gateway response:<!DOCTYPE html><html lang=en> <meta charset=utf-8> <meta name=viewport content="initial-scale=1, minimum-scale=1, w (truncated...)
  • src/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:113
  • Generated by: Unknown account
  • May 3, 2026 at 12:59 AM

Stack trace​


#0 src/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\Exception\RequestException::create(Object(GuzzleHttp\Psr7\Request), Object(GuzzleHttp\Psr7\Response), NULL, Array, NULL)
#1 src/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\Middleware::GuzzleHttp\{closure}(Object(GuzzleHttp\Psr7\Response))
#2 src/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\Promise\Promise::callHandler(1, Object(GuzzleHttp\Psr7\Response), NULL)
#3 src/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\Promise\Promise::GuzzleHttp\Promise\{closure}()
#4 src/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php(163): GuzzleHttp\Promise\TaskQueue->run()
#5 src/vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php(189): GuzzleHttp\Handler\CurlMultiHandler->tick()
#6 src/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\Handler\CurlMultiHandler->execute(true)
#7 src/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\Promise\Promise->invokeWaitFn()
#8 src/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\Promise\Promise->waitIfPending()
#9 src/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\Promise\Promise->invokeWaitList()
#10 src/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\Promise\Promise->waitIfPending()
#11 src/addons/DigitalPoint/Analytics/GoogleApi/Measurement.php(22): GuzzleHttp\Promise\Promise->wait()
#12 src/addons/DigitalPoint/Analytics/Repository/Measurement.php(11): DigitalPoint\Analytics\GoogleApi\Measurement->__destruct()
#13 src/addons/DigitalPoint/Analytics/XF/Mail/Mailer.php(99): DigitalPoint\Analytics\Repository\Measurement->collect('fb3059b7-e06e-4...', Array, '325457')
#14 src/XF/Job/MailSend.php(37): DigitalPoint\Analytics\XF\Mail\Mailer->send(Object(Symfony\Component\Mime\Email))
#15 src/addons/DBTech/Mail/XF/Job/MailSend.php(26): XF\Job\MailSend->run(22.4301)
#16 src/XF/Job/Manager.php(275): DBTech\Mail\XF\Job\MailSend->run(22.4301)
#17 src/XF/Job/Manager.php(205): XF\Job\Manager->runJobInternal(Array, 22.4301)
#18 src/XF/Job/Manager.php(89): XF\Job\Manager->runJobEntry(Array, 22.4301)
#19 src/XF/Cli/Command/RunJobs.php(58): XF\Job\Manager->runQueue(false, 45)
#20 src/vendor/symfony/console/Command/Command.php(298): XF\Cli\Command\RunJobs->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#21 src/vendor/symfony/console/Application.php(1040): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#22 src/vendor/symfony/console/Application.php(301): Symfony\Component\Console\Application->doRunCommand(Object(XF\Cli\Command\RunJobs), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#23 src/vendor/symfony/console/Application.php(171): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#24 src/XF/Cli/Runner.php(115): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#25 cmd.php(17): XF\Cli\Runner->run()
#26 {main}


Request state​

array(1) {
["cli"] => string(62) "/cmd.php xf:run-jobs"
}

php 8.3.30
xf2.3.10
I recently moved to a new server.
 
Not sure if its relevant or accurate, but I ran it through Claude Code and got this response:

The ServerException propagates up uncaught, terminating the MailSend job. Queued emails are not sent until the job is retried and the GA endpoint happens to be healthy.

Root Cause​

The issue originates in Measurement.php line 22, called from the __destruct() method of DigitalPoint\Analytics\GoogleApi\Measurement. The Guzzle promise is resolved with ->wait() without any error handling. The call chain is:

Mailer->send() calls Repository\Measurement->collect()

collect() triggers GoogleApi\Measurement->__destruct()

__destruct() calls Promise->wait() on the pending GA request

If the GA endpoint returned a 5xx, wait() throws GuzzleHttp\Exception\ServerException

No try/catch exists at any level in this chain

Suggested Fix​

Wrap the Promise->wait() call in Measurement.php (or the __destruct method) in a try/catch that catches GuzzleHttp\Exception\GuzzleException (or at minimum ServerException). Log the failure via \XF::logException() and return gracefully. For example:



public function __destruct()

{

try {

$this->promise->wait();

} catch (\GuzzleHttp\Exception\GuzzleException $e) {

\XF::logException($e, false, 'DigitalPoint Analytics: GA4 measurement failed: ');

}

}
 
It’s a server-side error on Google’s side (or a proxy if your server is going through a proxy). 5xx errors are on the side of the server receiving the request and can’t be fixed on your side. You can disable serving the JavaScript from your own domain to work around it (no request to Google’s servers to fetch the JavaScript in that case).
 
There is something wrong with GA in general where it looks like some sort of ghost bots in Singapore are able to take the measurement-id (which is public) and directly ping Analytics over and over, so its a useless service now for me. I can't tell you for how many months, maybe even years, I was trying to stop this traffic, only to find its not even hitting the CDN or origin at all. Its just phantom ghost traffic :(
 
There is something wrong with GA in general where it looks like some sort of ghost bots in Singapore are able to take the measurement-id (which is public) and directly ping Analytics over and over, so its a useless service now for me. I can't tell you for how many months, maybe even years, I was trying to stop this traffic, only to find its not even hitting the CDN or origin at all. Its just phantom ghost traffic :(

Same here. From Brazil. "Traffic" will spike 500%. Hosting the JS on our domain makes the problem way worse. Blocking BR in Cloudflare has little effect.
 
@digitalpoint we're looking at GA and the only user groups available to report on are 0 and 1 (guests and members).
As you can see in the screen cap below, we're tracking more usergroups than that. We have been for a long time.
Today I added Member Restriction - Ad Blocker because we want to track these, but then noticed that we can't report on other usergroups - I guess because they are not the primary usergroup?
How can we track secondary usergroups?
Thanks

1791891840671.webp
 
@digitalpoint we're looking at GA and the only user groups available to report on are 0 and 1 (guests and members).
As you can see in the screen cap below, we're tracking more usergroups than that. We have been for a long time.
Today I added Member Restriction - Ad Blocker because we want to track these, but then noticed that we can't report on other usergroups - I guess because they are not the primary usergroup?
How can we track secondary usergroups?
Thanks

View attachment 347108
There isn't a realistic way. Google Analytics custom fields are singular values (not arrays). In Google Analytics, user_group_id ends up being set to the first user group ID you have in your track list that the user is part of (primary or secondary).

Being unique to your setup, your best bet would be to edit the ba_google_analytics template and have a new Google Analytics variable for your property that is set based on the user being a member of your Ad Blocker group. But again... you can't really use the existing user_group_id variable because it's a singular value.

The other option would be to use the existing option and reorder them so the Ad Blocker group is higher up the list (again, it logs the first match in your sorted order). But then if someone was part of that group, you wouldn't see them as part of the other groups... because (as I keep saying), it can only be a singular value on Google's side.
 
There isn't a realistic way. Google Analytics custom fields are singular values (not arrays). In Google Analytics, user_group_id ends up being set to the first user group ID you have in your track list that the user is part of (primary or secondary).

Being unique to your setup, your best bet would be to edit the ba_google_analytics template and have a new Google Analytics variable for your property that is set based on the user being a member of your Ad Blocker group. But again... you can't really use the existing user_group_id variable because it's a singular value.

The other option would be to use the existing option and reorder them so the Ad Blocker group is higher up the list (again, it logs the first match in your sorted order). But then if someone was part of that group, you wouldn't see them as part of the other groups... because (as I keep saying), it can only be a singular value on Google's side.
Thanks, Shawn. Could we create a boolean GA variable 'is an adblocker' which is set when the visitor is_member_of the specific secondary usergroup?
 
Back
Top Bottom