Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: External GTM #83

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Block/DataLayer.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ public function getCookieRestrictionName($store_id = null)
}
}

/**
* @param null $store_id
* @return bool
*/
public function isExternal($store_id = null)
{
return (int) $this->_gtmHelper->isExternal($store_id);
}

/**
* @param null $store_id
* @return int
Expand Down
11 changes: 11 additions & 0 deletions Block/GtmCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ public function getEmbeddedAccountId()
$this->getAccountId() . $this->_gtmHelper->getMultiContainerCode() : $this->getAccountId();
}

/**
* If the GTM should be loaded externally by an third party,
* like an GDPR service, this function will return true
* and false otherwise
*
* @return bool
*/
public function isExternal(){
return $this->_gtmHelper->isExternal();
}

/**
* Render tag manager JS
*
Expand Down
16 changes: 16 additions & 0 deletions Helper/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ public function isEnabled($store_id = null)
return $accountId && $active;
}

/**
* Whether Tag Manager should be loaded
* by an external third party app
*
* @param null $store_id
* @return bool
*/
public function isExternal($store_id = null)
{
return $this->scopeConfig->getValue(
'googletagmanager/general/external',
ScopeInterface::SCOPE_STORE,
$store_id
);
}

/**
* @param null $store_id
* @return string
Expand Down
19 changes: 18 additions & 1 deletion etc/adminhtml/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,40 +55,57 @@
<label>Enable</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="external" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>External</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment>
If you like to include GTM by your self due to an external gdpr solution for example,
you need to set this to "Yes" and include GTM like described by your provider.
</comment>
<depends>
<field id="*/*/active">1</field>
</depends>
</field>
<field id="account" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Account Number</label>
<comment>e.g GTM-XXXXXX</comment>
<validate>required-entry validate-no-html-tags</validate>
<depends>
<field id="*/*/active">1</field>
<field id="*/*/external">0</field>
</depends>
</field>
<field id="item_variant_layer" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Add item variant to data layer?</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="*/*/active">1</field>
<field id="*/*/external">0</field>
</depends>
</field>
<field id="item_variant_format" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Item variant format</label>
<source_model>MagePal\GoogleTagManager\Block\Adminhtml\System\Config\Form\Field\ItemVariantFormat</source_model>
<depends>
<field id="*/*/active">1</field>
<field id="*/*/external">0</field>
<field id="*/*/item_variant_layer">1</field>
</depends>
</field>

<field id="category_layer" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Add Category to data layer?</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="*/*/active">1</field>
<field id="*/*/external">0</field>
</depends>
</field>
</group>
<group id="gdpr" translate="label" type="text" sortOrder="11" showInDefault="1" showInWebsite="1" showInStore="1">
<label>General Data Protection Regulation (GDPR)</label>
<depends>
<field id="googletagmanager/general/external">0</field>
</depends>
<field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable GDPR</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
Expand Down
1 change: 1 addition & 0 deletions etc/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<item_variant_layer>0</item_variant_layer>
<item_variant_format>2</item_variant_format>
<category_layer>0</category_layer>
<external>0</external>
</general>
<gdpr>
<enabled>0</enabled>
Expand Down
15 changes: 11 additions & 4 deletions view/frontend/templates/iframe.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
/** @var $block MagePal\GoogleTagManager\Block\GtmCode **/
?>

<!-- Google Tag Manager by MagePal -->
<noscript><iframe src="<?= '/' . '/' ?>www.googletagmanager.com/ns.html?id=<?= /* @noEscape */ $block->getEmbeddedAccountId() ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager by MagePal -->
<?php
// only include the noscript tag and iFrame
// if the script is not loaded externally by
// a third party...
if(!$block->isExternal()): ?>
<!-- Google Tag Manager by MagePal -->
<noscript>
<iframe src="<?= '/' . '/' ?>www.googletagmanager.com/ns.html?id=<?= /* @noEscape */ $block->getEmbeddedAccountId() ?>" height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<!-- End Google Tag Manager by MagePal -->
<?php endif; ?>
9 changes: 5 additions & 4 deletions view/frontend/templates/js.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ $containerCode = $block->getEmbeddedCode();
<script type="text/javascript">
window.<?= /* @noEscape */ $dataLayerName ?> = window.<?= /* @noEscape */ $dataLayerName ?> || [];

<?php if (!$block->isGdprEnabled() && $block->addJsInHead()): ?>
<?php if (!$block->isExternal() && !$block->isGdprEnabled() && $block->addJsInHead()): ?>
<?= /* @noEscape */ $block->getDataLayerJs() ?>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
Expand All @@ -24,7 +24,6 @@ $containerCode = $block->getEmbeddedCode();
})(window,document,'script','<?= /* @noEscape */ $dataLayerName ?>','<?= /* @noEscape */ $accountId ?>');
<?php endif; ?>
</script>

<?php if ($block->isGdprEnabled() || !$block->addJsInHead()): ?>
<script type="text/x-magento-init">
{
Expand All @@ -39,7 +38,8 @@ $containerCode = $block->getEmbeddedCode();
"isGdprEnabled": <?= /* @noEscape */ $block->isGdprEnabled() ?>,
"gdprOption": <?= /* @noEscape */ $block->getGdprOption() ?>,
"addJsInHeader": <?= /* @noEscape */ $block->addJsInHead() ?>,
"containerCode": "<?= /* @noEscape */ $block->getEmbeddedCode() ?>"
"containerCode": "<?= /* @noEscape */ $block->getEmbeddedCode() ?>",
"isExternal": <?= /* @noEscape */ $block->isExternal() ?>
}
}
}
Expand All @@ -49,7 +49,8 @@ $containerCode = $block->getEmbeddedCode();
{
"*": {
"magepalGtmDatalayer": {
"dataLayer": "<?= /* @noEscape */ $block->getDataLayerName() ?>"
"dataLayer": "<?= /* @noEscape */ $block->getDataLayerName() ?>",
"isExternal": <?= /* @noEscape */ $block->isExternal() ?>
}
}
}
Expand Down
149 changes: 142 additions & 7 deletions view/frontend/web/js/datalayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,32 @@ define([
var lastPushedCart = {};
var lastPushedCustomer = {};

//check if object contain keys

/**
* The function checks if an object has keys 'customer' and 'cart' and returns true if at least one
* object in the array has these keys.
*
* @param object - The `object` parameter is an object that contains multiple key-value pairs.
* @returns a boolean value.
*/
function objectKeyExist(object)
{
return _.some(object, function (o) {
return !_.isEmpty(_.pick(o, ['customer', 'cart']));
})
}

//Update datalayer
/**
* The function `updateDataLayer` updates the Google Tag Manager (GTM) data layer with customer and
* cart information if the data layer is not already defined or if forced to update.
*
* @param _gtmDataLayer - This parameter represents the Google Tag Manager (GTM) data layer. It is
* an array used to store data that can be accessed by GTM tags and triggers.
* @param _dataObject - The `_dataObject` parameter is an object that contains information about
* the customer and the cart.
* @param _forceUpdate - A boolean value indicating whether to force an update of the data layer
* even if it already exists.
*/
function updateDataLayer(_gtmDataLayer, _dataObject, _forceUpdate)
{
var customer = {isLoggedIn : false},
Expand All @@ -48,6 +65,13 @@ define([
}
}

/**
* The function checks if tracking is allowed based on the provided configuration.
*
* @param config - The `config` parameter is an object that contains various configuration options
* for tracking services. It may have the following properties:
* @returns the value of the variable "allowServices".
*/
function isTrackingAllowed(config)
{
var allowServices = false,
Expand Down Expand Up @@ -75,11 +99,89 @@ define([
return allowServices;
}

//load gtm
function initTracking(dataLayerName, accountId, containerCode)
/**
* The function checks if Google Tag Manager (GTM) tracking is initialized by verifying the
* presence of the dataLayer object and the start event, and optionally checking the unique event
* ID if strict mode is enabled.
*
* @param dataLayerName - The dataLayerName parameter is the name of the data layer object that is
* used by Google Tag Manager (GTM) on the website. The data layer is a JavaScript object that
* stores information that is used by GTM to track events and send data to various analytics and
* marketing tools.
* @param strict - The "strict" parameter is a boolean value that determines whether to perform an
* additional check for the "gtm.uniqueEventId" property in the start event. If set to true, the
* function will only return true if both the "gtm.start" and "gtm.uniqueEventId" properties
* @returns a boolean value. It returns true if the GTM (Google Tag Manager) tracking is
* initialized and ready to use, and false otherwise.
*/
function isTrackingInitialized(dataLayerName, strict)
{
// without the datalayer GTM is not ready
if(!window[dataLayerName]){
return false;
}
// try to get the start event and if thats not
// possible, GTM is not ready
const startEvent = window[dataLayerName].find(element => element['gtm.start']);
if(!startEvent){
return false;
}
// if strict is true, we also check the unique id, which seems not to be
// set by the magepal extension... Anyway, we let the check optional, so
// we can use it if the behaviour changes or I missed something.
if(strict === true && !startEvent['gtm.uniqueEventId']){
return false;
}
// GTM is ready to use
return true;
}

/**
* The function `initTracking` initializes tracking using Google Tag Manager, either by detecting
* if it was generated by an external party or by generating it internally.
*
* @param dataLayerName - The name of the data layer object that is used to store and pass data to
* Google Tag Manager.
* @param accountId - The accountId parameter is the unique identifier for your Google Analytics
* account. It is used to associate the tracking data with your account.
* @param containerCode - The containerCode parameter is a string that represents the code or ID of
* the Google Tag Manager container. This container is used to manage and deploy various tracking
* tags and scripts on a website.
* @param isExternal - A boolean value indicating whether the Google Tag Manager (GTM) is generated
* by an external party or not. If it is true, the tracking will be initialized by waiting until
* GTM is ready. If it is false, the tracking will be initialized by generating it internally.
*/
function initTracking(dataLayerName, accountId, containerCode, isExternal)
{
$(document).trigger('gtm:beforeInitialize');
// if true we also use the uniqueEventId of the start event
// to detect if GTM is ready. Seems like the extension doesn´t
// generate this id, our third party app does. But to keep
// the compatibility to the self generated dataLayer event
// we set it to false. However, it would be better if the
// extension adds this id. Or -if this would be an option-
// in the administration panel.
const strict = false;
// we need to know, if we need to create the tracker by our self,
// or if we need to detect if the tag manager was generated
// by an external party (an external gdpr check for example)
!!isExternal
// initialize the tracking, by waiting until GTM is ready
? initTrackingExternal(dataLayerName, strict)
// initialize the tracking the default way, by generating it
: initTrackingInternal(dataLayerName, accountId, containerCode);
}

/**
* Initialize the tracking by loading the GTM and setup
* the dataLayer.
*
* @param {string} dataLayerName
* @param {string} accountId
* @param {any} containerCode
*/
function initTrackingInternal(dataLayerName, accountId, containerCode)
{
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({
Expand All @@ -92,10 +194,43 @@ define([
j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl + containerCode;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', dataLayerName, accountId);

$(document).trigger('gtm:afterInitialize');
}


/**
* The function `initTrackingExternal` checks if GTM (Google Tag Manager) is ready and fires the
* "gtm:afterInitialize" event once it is.
*
* @param dataLayerName - The dataLayerName parameter is the name of the data layer object that is
* used for tracking in Google Tag Manager. It is typically set to "dataLayer" by default, but it
* can be customized to a different name if needed.
* @param strict - The "strict" parameter is a boolean value that determines whether the tracking
* initialization check should be strict or not. If set to true, we also check the uniqueEventId
* value, which should be defined in the start event
*/
function initTrackingExternal(dataLayerName, strict)
{
// now we can just check if gtm is ready. An interval seems to be fair for
// that check but if you find a better way, just change it ;)
const checkInterval = setInterval(() => {
// if GTM is ready we can clear the interval
// and fire the "gtm:afterInitialize" event
if(isTrackingInitialized(dataLayerName, strict)){
clearInterval(checkInterval);
$(document).trigger('gtm:afterInitialize');
}
}, 100);
}

/**
* The function "pushData" pushes data from an array into a specified data layer.
*
* @param dataLayerName - The dataLayerName parameter is the name of the data layer object that you
* want to push data into.
* @param dataLayer - The `dataLayer` parameter is an array that contains data objects to be pushed
* into the `dataLayerName` data layer.
*/
function pushData(dataLayerName, dataLayer)
{
if (_.isArray(dataLayer)) {
Expand All @@ -109,9 +244,9 @@ define([

window[config.dataLayer] = window[config.dataLayer] || [];

if (_.has(config, 'accountId') && isTrackingAllowed(config)) {
if ((_.has(config, 'accountId') || !!config.isExternal) && isTrackingAllowed(config)) {
pushData(config.dataLayer, config.data);
initTracking(config.dataLayer, config.accountId, config.containerCode);
initTracking(config.dataLayer, config.accountId, config.containerCode, config.isExternal);
}

var dataObject = customerData.get('magepal-gtm-jsdatalayer');
Expand Down