Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
cichy380 committed Jun 8, 2017
1 parent b1d888c commit de7c3e6
Show file tree
Hide file tree
Showing 4 changed files with 657 additions and 0 deletions.
89 changes: 89 additions & 0 deletions README.md
@@ -0,0 +1,89 @@
# GTM-Storage

Idea of this solution is based on [HTML5 Local Storage](https://www.w3schools.com/html/html5_webstorage.asp). I did not
want to send data to [Google Tag Manager](http://www.google.pl/tagmanager/) immediately after the event is triggered on
my website.

* Demo: [http://example.silversite.pl/gtm/storage/](http://example.silversite.pl/gtm/storage/)

### Problems with regular solution

On the [Developer Guide](https://developers.google.com/tag-manager/devguide#events) you can see the sample of usage:

```html
<a href="page.html" onclick="dataLayer.push({'event': 'button-click'});">My button</a>
```

where method `dataLayer.push()` starts immediately after clicking on the link. And here you can have the problem with
sending data to [GTM](http://www.google.pl/tagmanager/) because **you have to trust that sending will be faster than
page reloading** to _page.html_. You can use also
[event callback datalayer variable](https://developers.google.com/tag-manager/enhanced-ecommerce#product-clicks) but
cache (GTM-Storage) is better...

### GTM-Storage solution

GTM-Storage **is independent** of [GTM](http://www.google.pl/tagmanager/) and `dataLayer` object. It just creates
numerous elements which the event was made on. In your code you can use storaged data to recursive reading it and
sending to [GTM](http://www.google.pl/tagmanager/) in given interval time.

Saving data to storage and reading it **is safe** because no javascript error will stop the default website event
like page reloading based on
[callback event](https://developers.google.com/tag-manager/enhanced-ecommerce#product-clicks).

## Quick start

Copy the following link to the main GTM-Storage file and paste it to the `<head>` tag on every page of your website:

```html
<script src="gtmstorage.js"></script>
```

Put the following link to the script at the [bottom](https://developer.yahoo.com/performance/rules.html#js_bottom) of
your markup right after [jQuery](https://jquery.com/):

```html
<script src="jquery.js"></script>
<script src="script.js"></script>
```

## Usage

Use [HTML event attributes](https://www.w3schools.com/tags/ref_eventattributes.asp) to set the event in the HTML tag
and call the `gtmStorage.push()` method with `this` argument (HTML DOM element).

Use HTML tag attribute `data-gtm` to set data to send to [GTM](http://www.google.pl/tagmanager/). The attribute
`data-gtm` needs to get value in JSON format with one required property:
* `event` - custom event name [required]
* `data` - custom data with any format (object, string, number, bool)

Here is an example: in order to set an event when a user clicks a link, you might modify the link to call the `push()`
and enter data by `data-gtm` as follows:

```html
<a href="#url"
onclick="gtmStorage.push(this)"
data-gtm='{"event":"customEventName", "data":{"any":"data","you":"need"}}'>link anchor</a>
```

Feel free to modify _script.js_ file and create any solution you need. For example conditional statements on handling
Ecommerce object format like
[Measuring Product Clicks](https://developers.google.com/tag-manager/enhanced-ecommerce#product-clicks) with required
`ecommerce` property.

### Why DOM Level 0 event model?

In the previous example I used [DOM Level 0](https://www.w3.org/TR/uievents/#dom-level-0) event model. It means
triggering of event in HTML tag property, eg. `<span onclick="gtmStorage.push(this)" />`.

I have chosen this event model because **it works faster**. For example, if you want to use
[DOM Level 2](https://www.w3.org/TR/DOM-Level-2-Events/) event model based on [jQuery](https://jquery.com/), event
listeners start working after DOM is ready. And if your website has 100K lines of code, it is possible that the user
will start using the website before it is ready, and you lose some events and stats. If you paste the _gtmstorage.js_
file in `<head>` and you use [DOM Level 0](https://www.w3.org/TR/uievents/#dom-level-0) event model - you do not lose
the events.

This solution is ready also in content loaded by AJAX.

## License

Code released under the MIT license.
173 changes: 173 additions & 0 deletions src/gtmstorage.js
@@ -0,0 +1,173 @@
/**
* GTM-Storage
*
* Using HTML5 Local Storage creates numerous stack elements which the event was made on.
*
* @version 0.0.1
* @link https://github.com/cichy380/GTM-Storage
* @author Marcin Dobroszek
* @license The MIT License (MIT)
*
* @todo action after lock storage handling
*/
var gtmStorage = (function() {
'use strict';

var namespace = 'gtm',

/**
* Returns all data about storaged events.
* @return {object} data - List of events
*/
getItems = function () {
var localStorageValue = typeof localStorage.getItem(namespace) !== 'undefined'
&& localStorage.getItem(namespace) ? localStorage.getItem(namespace) : '[]';

return JSON.parse(localStorageValue);
},

/**
* Saves new data in storage.
* @param {array} data - List of events
*/
_saveGtmStorage = function (data) {
// if GTM Storage is not lock we can save new data (add new event) ..
if (localStorage.getItem(namespace + 'lock') === 'off') {

// set lock on GTM Storage
localStorage.setItem(namespace + 'lock', 'on');

localStorage.setItem(namespace, JSON.stringify(data));

// set unlock on GTM Storage
localStorage.setItem(namespace + 'lock', 'off');
} else {
// .. else lost info about new data (new events)
// TODO: lock storage handling
}
},

/**
* Changes item data in storage.
* @param {array} data - New data of item event
* @param {int} id - ID of item event to edit
*/
editItem = function (data, id) {
var currentGtmStorage,
newGtmStorage;

newGtmStorage = [];
currentGtmStorage = getItems();
currentGtmStorage.forEach(function(item, index) {
if (item.id === id) {
newGtmStorage.push(data);
} else {
newGtmStorage.push(item);
}
});

_saveGtmStorage(newGtmStorage);
},

removeItem = function (id) {
var currentGtmStorage = getItems(),
newGtmStorage = [];

currentGtmStorage.forEach(function (item) {
if (item.id !== id) {
newGtmStorage.push(item);
}
});

_saveGtmStorage(newGtmStorage);
},

/**
* Clears flags "sending" from all data items and allows next sending try.
*/
_clearSendingFlag = function () {
var gtmStorage = getItems();

gtmStorage.forEach(function (item, index) {
if (item.sending === true) { // not sending yet
// prevent double sending
item.sending = false;
editItem(item, item.id);
}
});
},

/**
* Adds element to storage.
* @param {HTML DOM element} element
*/
push = function (element) {
var gtmLocalStorage;

if (typeof localStorage !== 'object' || typeof JSON !== 'object') {
// browser does not support required function
return;
}

try {
// reads current data and convert to array
gtmLocalStorage = JSON.parse(localStorage.getItem(namespace) || '[]');
} catch (errMsg) {
// problem with data in localStorage -- clear all data
localStorage.removeItem(namespace);
gtmLocalStorage = [];

window.console && console.error(errMsg);
}

// push new data (new element)
gtmLocalStorage.push({
id: Math.random(),
time: new Date(),
element: element.outerHTML,
sending: false, // FALSE == item did not send yet, TRUE == just sending
});

// save
_saveGtmStorage(gtmLocalStorage);
},

/**
* Initialization of GTM-Storage.
*/
_init = function () {
if (typeof localStorage === 'object') {
// reset always after start loading website (this file loaded)
localStorage.setItem(namespace + 'lock', 'off');

_clearSendingFlag();
}
},

/**
* Returns easy readable descrition of HTML element.
* Eg. "<a.btn.btn-submit>"
* @param {HTML DOM element} element
* @return {string}
*/
getElementName = function (element) {
var tagName = element.localName,
idName = element.id ? '#' + element.id : '',
classNameListString = element.classList.length
? '.' + Array.prototype.join.call(element.classList, '.') : '';

return '<' + tagName + idName + classNameListString + '>';
};

// initialization
_init();

return {
namespace: namespace,
push: push,
getItems: getItems,
editItem: editItem,
removeItem: removeItem,
getElementName: getElementName,
}
}());
62 changes: 62 additions & 0 deletions src/script.js
@@ -0,0 +1,62 @@
/**
* GTM-Storage example of usage
*
* @link https://github.com/cichy380/GTM-Storage
* @author Marcin Dobroszek
* @license The MIT License (MIT)
*
* @todo Enhanced Ecommerce handling
*/
;(function($, undefined) {
'use strict';

/**
* Read Storage data and send it to GTM
*/
function sendGtmStorage() {
var gtmData;

if (typeof gtmStorage !== 'object') {
$.error('Required object GTM Storage (gtmStorage) missing.');
}

if (typeof dataLayer !== 'object') {
$.error('Required object GTM (dataLayer) missing.');
}

gtmData = gtmStorage.getItems();
gtmData.forEach(function (item, index) {
var data2send = {};

if (item.sending === false) { // data item not sending yet
data2send = $(item.element).data(gtmStorage.namespace);

// check if required object and property exists
if (typeof data2send === 'object' && typeof data2send.event !== 'undefined') {
// sending data to GTM...
dataLayer.push({
event: data2send.event,
data: data2send.data || null,
eventCallback: function () {
// remove item data from storage after GTM callback
gtmStorage.removeItem(item.id);
},
});

// prevent double sending
item.sending = true;
gtmStorage.editItem(item, item.id);
}
else {
// item data is invalid - remove it
gtmStorage.removeItem(item.id);
window.console && console.error('HTML tag ' + gtmStorage.getElementName($(item.element).get(0)) +
' does not have required data-gtm attribute or this attribute has wrong data format.');
}
}
});
}

// checking new data in GTM-Storage every 1.5 sec.
setInterval(sendGtmStorage, 1500);
})(jQuery);

0 comments on commit de7c3e6

Please sign in to comment.