├── .babelrc ├── .github └── dependabot.yml ├── .gitignore ├── License ├── README.md ├── dist ├── openpixel.js ├── openpixel.min.js └── snippet.html ├── gulpfile.js ├── package-lock.json ├── package.json ├── pixel.gif └── src ├── browser.js ├── config.js ├── cookie.js ├── helper.js ├── pixel.js ├── setup.js ├── snippet.js └── url.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@babel/core" 11 | versions: 12 | - 7.12.10 13 | - 7.12.13 14 | - 7.12.16 15 | - 7.12.17 16 | - 7.13.1 17 | - 7.13.10 18 | - 7.13.13 19 | - 7.13.14 20 | - 7.13.15 21 | - 7.13.8 22 | - dependency-name: "@babel/preset-env" 23 | versions: 24 | - 7.12.11 25 | - 7.12.13 26 | - 7.12.16 27 | - 7.12.17 28 | - 7.13.0 29 | - 7.13.10 30 | - 7.13.12 31 | - 7.13.5 32 | - 7.13.8 33 | - 7.13.9 34 | - dependency-name: "@babel/plugin-proposal-class-properties" 35 | versions: 36 | - 7.12.13 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 - Present Dockwa, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Openpixel 2 | 3 | 4 | 5 | [![Powered by Dockwa](https://raw.githubusercontent.com/dockwa/openpixel/dockwa/by-dockwa.png)](https://engineering.dockwa.com/) 6 | 7 | ## About 8 | Openpixel is a customizable JavaScript library for building tracking pixels. Openpixel uses the latest technologies available with fall back support for older browsers. For example, if the browser supports web beacons, openpixel will send a web beacon, if it doesn't support them it will inject a 1x1 gif into the page with tracking information as part of the images get request. 9 | 10 | At Dockwa we built openpixel to solve our own problems of implementing a tracking service that our marinas could put on their website to track traffic and attribution to the reservations coming through our platform. 11 | 12 | Openpixel handles the hard things about building a tracking library so you don't have to. It handles things like tracking unique users with cookies, tracking utm tags and persisting them to that users session, getting all of the information about the clients browser and device, and many other neat tricks for performant and accurate analytics. 13 | 14 | Openpixel has two parts, the snippet (`snippet.html`), and the core (`openpixel.min.js`). 15 | 16 | ### Snippet 17 | The openpixel snippet (found at `dist/snippet.html`) is the HTML code that will be put onto any webpage that will be reporting analytics. For Dockwa, our marina websites put this on every page of their website so that it would load the JS to execute beacons back to a tracking server. The snippet can be placed anywhere on the page and it will load the core openpixel JS asynchronously. To be accurate, the first part of the snippet gets the timestamp as soon as it is loaded, applies an ID (just like a Google analytics ID, to be determined by you), and queues up a "pageload" event that will be sent as soon as the core JS has asynchronously loaded. 18 | 19 | The snippet handles things like making sure the core JavaScript will always be loaded async and is cache busted every 24 hours so you can update the core and have customers using the updates within the next day. 20 | 21 | ### Core 22 | The openpixel core (found at `src/openpixel.min.js`) is the JavaScript code that that the snippet loads asynchronously onto the client's website. The core is what does all of the heavy lifting. The core handles settings cookies, collecting utms, and of course sending beacons and tracking pixels of data when events are called. 23 | 24 | ### Events 25 | There are 2 automatic events, the `pageload` event which is sent as the main event when a page is loaded, you could consider it to be a "hit". The other event is `pageclose` and this is sent when the pages is closed or navigated away from. For example, to calculate how long a user viewed a page, you could calculate the difference between the timestamps on pageload and pageclose and those timestamps will be accurate because they are triggered on the client side when the events actually happened. 26 | 27 | Openpixel is flexible with events though, you can make calls to any events with any data you want to be sent with the beacon. Whenever an event is called, it sends a beacon just like the other beacons that have a timestamp and everything else. Here is an example of a custom event being called. Note: In this case we are using the `opix` function name but this will be custom based on your build of openpixel. 28 | 29 | ```js 30 | opix('event', 'reservation_requested') 31 | ``` 32 | You can also pass a string or json as the third parameter to send other data with the event. 33 | 34 | ```js 35 | opix('event', 'reservation_requested', {someData: 1, otherData: 'cool'}) 36 | opix('event', 'reservation_requested', {someData: 1, otherData: 'cool'}) 37 | ``` 38 | You can also add an attribute to any HTML element that will automatically fire the event on click. 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | ## Setup and Customize 45 | Openpixel needs to be customized for your needs before you can start using it. Luckily for you it is really easy to do. 46 | 47 | 1. Make sure you have [node.js](https://nodejs.org/en/download/) installed on your computer. 48 | 2. Install openpixel `npm i openpixel` 49 | 3. Install the dependencies for compiling openpixel via the command line with `npm install` 50 | 4. Update the variables at the top of the `gulpfile.js` for your custom configurations. Each configuration has comments explaining it. 51 | 5. Run gulp via the command `npm run dist`. 52 | 53 | The core files and the snippet are located under the `src/` directory. If you are working on those files you can run `npm run watch` and that will watch for any files changed in the `src/` directory and rerun gulp to recompile these files and drop them in the `dist/` directory. 54 | 55 | The `src/snippet.js` file is what is compiled into the `dist/snippet.html` file. All of the other files in the `src` directory are compiled into the `dist/openpixel.js` and the minified `dist/openpixel.min.js` files. 56 | 57 | ## Continuous integration 58 | You may also need to build different versions of openpixel for different environments with custom options. 59 | Environment variables can be used to configure the build: 60 | ``` 61 | OPIX_DESTINATION_FOLDER, OPIX_PIXEL_ENDPOINT, OPIX_JS_ENDPOINT, OPIX_VERSIONOPIX_PIXEL_FUNC_NAME, OPIX_VERSION, OPIX_HEADER_COMMENT 62 | ``` 63 | 64 | You can install openpixel as an npm module `npm i -ED openpixel` and use it from your bash or js code. 65 | ``` 66 | OPIX_DESTINATION_FOLDER=/home/ubuntu/app/dist OPIX_PIXEL_ENDPOINT=http://localhost:8000/pixel.gif OPIX_JS_ENDPOINT=http://localhost:800/pixel_script.js OPIX_PIXEL_FUNC_NAME=track-function OPIX_VERSION=1 OPIX_HEADER_COMMENT="// My custom tracker\n" npx gulp --gulpfile ./node_modules/openpixel/gulpfile.js build 67 | ``` 68 | 69 | ## Tracking Data 70 | Below is a table that has all of the keys, example values, and details on each value of information that is sent with each beacon on tracking pixel. A beacon might look something like this. Note: every key is always sent regardless of if it has a value so the structure will always be the same. 71 | 72 | ``` 73 | https://tracker.example.com/pixel.gif?id=R29X8&uid=1-ovbam3yz-iolwx617&ev=pageload&ed=&v=1&dl=http://edgartownharbor.com/&rl=&ts=1464811823300&de=UTF-8&sr=1680x1050&vp=874x952&cd=24&dt=Edgartown%20Harbormaster&bn=Chrome%2050&md=false&ua=Mozilla/5.0%20(Macintosh;%20Intel%20Mac%20OS%20X%2010_11_5)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Chrome/50.0.2661.102%20Safari/537.36&utm_source=&utm_medium=&utm_term=&utm_content=&utm_campaign= 74 | ``` 75 | 76 | | Key | Value | Details | 77 | | ------------ | ------------------- | --------------------------------------------------------------- | 78 | | id | SJO12ZW | id for the app/website you are tracking | 79 | | uid | 1-cwq4oelu-in95g8xy | id of the user | 80 | | ev | pageload | the event that is being triggered | 81 | | ed | {'somedata': 123} | optional event data that can be passed in, string or json string| 82 | | v | 1 | openpixel js version number | 83 | | dl | http://example.com/ | document location | 84 | | rl | http://google.com/ | referrer location | 85 | | ts | 1461175033655 | timestamp in microseconds | 86 | | de | UTF-8 | document encoding | 87 | | sr | 1680x1050 | screen resolution | 88 | | vp | 1680x295 | viewport | 89 | | cd | 24 | color depth | 90 | | dt | Example Title | document title | 91 | | bn | Chrome 50 | browser name | 92 | | md | false | mobile device | 93 | | ua | _full user agent_ | user agent | 94 | | tz | 240 | timezone offset (minutes away from utc) | 95 | | utm_source | | Campaign Source | 96 | | utm_medium | | Campaign Medium | 97 | | utm_term | | Campaign Term | 98 | | utm_content | | Campaign Content | 99 | | utm_campaign | | Campaign Name | 100 | | utm_source_platform | | Source platform | 101 | | utm_creative_format | | Creative format | 102 | | utm_marketing_tactic | | Marketing tactic | 103 | -------------------------------------------------------------------------------- /dist/openpixel.js: -------------------------------------------------------------------------------- 1 | // Open Pixel v1.3.1 | Published By Dockwa | Created By Stuart Yamartino | MIT License 2 | ;(function(window, document, pixelFunc, pixelFuncName, pixelEndpoint, versionNumber) { 3 | "use strict"; 4 | 5 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } 6 | 7 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } 8 | 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | 11 | function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 14 | 15 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 16 | 17 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } 18 | 19 | var Config = { 20 | id: '', 21 | params: {}, 22 | version: versionNumber 23 | }; 24 | 25 | var Helper = /*#__PURE__*/function () { 26 | function Helper() { 27 | _classCallCheck(this, Helper); 28 | } 29 | 30 | _createClass(Helper, null, [{ 31 | key: "isPresent", 32 | value: function isPresent(variable) { 33 | return typeof variable !== 'undefined' && variable !== null && variable !== ''; 34 | } 35 | }, { 36 | key: "now", 37 | value: function now() { 38 | return 1 * new Date(); 39 | } 40 | }, { 41 | key: "guid", 42 | value: function guid() { 43 | return Config.version + '-xxxxxxxx-'.replace(/[x]/g, function (c) { 44 | var r = Math.random() * 36 | 0, 45 | v = c == 'x' ? r : r & 0x3 | 0x8; 46 | return v.toString(36); 47 | }) + (1 * new Date()).toString(36); 48 | } // reduces all optional data down to a string 49 | 50 | }, { 51 | key: "optionalData", 52 | value: function optionalData(data) { 53 | if (Helper.isPresent(data) === false) { 54 | return ''; 55 | } else if (_typeof(data) === 'object') { 56 | // runs Helper.optionalData again to reduce to string in case something else was returned 57 | return Helper.optionalData(JSON.stringify(data)); 58 | } else if (typeof data === 'function') { 59 | // runs the function and calls Helper.optionalData again to reduce further if it isn't a string 60 | return Helper.optionalData(data()); 61 | } else { 62 | return String(data); 63 | } 64 | } 65 | }]); 66 | 67 | return Helper; 68 | }(); 69 | 70 | var Browser = /*#__PURE__*/function () { 71 | function Browser() { 72 | _classCallCheck(this, Browser); 73 | } 74 | 75 | _createClass(Browser, null, [{ 76 | key: "nameAndVersion", 77 | value: function nameAndVersion() { 78 | // http://stackoverflow.com/questions/5916900/how-can-you-detect-the-version-of-a-browser 79 | var ua = navigator.userAgent, 80 | tem, 81 | M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; 82 | 83 | if (/trident/i.test(M[1])) { 84 | tem = /\brv[ :]+(\d+)/g.exec(ua) || []; 85 | return 'IE ' + (tem[1] || ''); 86 | } 87 | 88 | if (M[1] === 'Chrome') { 89 | tem = ua.match(/\b(OPR|Edge)\/(\d+)/); 90 | if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera'); 91 | } 92 | 93 | M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?']; 94 | if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]); 95 | return M.join(' '); 96 | } 97 | }, { 98 | key: "isMobile", 99 | value: function isMobile() { 100 | return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; 101 | } 102 | }, { 103 | key: "userAgent", 104 | value: function userAgent() { 105 | return window.navigator.userAgent; 106 | } 107 | }]); 108 | 109 | return Browser; 110 | }(); 111 | 112 | var Cookie = /*#__PURE__*/function () { 113 | function Cookie() { 114 | _classCallCheck(this, Cookie); 115 | } 116 | 117 | _createClass(Cookie, null, [{ 118 | key: "prefix", 119 | value: function prefix() { 120 | return "__".concat(pixelFuncName, "_"); 121 | } 122 | }, { 123 | key: "set", 124 | value: function set(name, value, minutes) { 125 | var path = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '/'; 126 | var expires = ''; 127 | 128 | if (Helper.isPresent(minutes)) { 129 | var date = new Date(); 130 | date.setTime(date.getTime() + minutes * 60 * 1000); 131 | expires = "expires=".concat(date.toGMTString(), "; "); 132 | } 133 | 134 | document.cookie = "".concat(this.prefix()).concat(name, "=").concat(value, "; ").concat(expires, "path=").concat(path, "; SameSite=Lax"); 135 | } 136 | }, { 137 | key: "get", 138 | value: function get(name) { 139 | var name = "".concat(this.prefix()).concat(name, "="); 140 | var ca = document.cookie.split(';'); 141 | 142 | for (var i = 0; i < ca.length; i++) { 143 | var c = ca[i]; 144 | 145 | while (c.charAt(0) == ' ') { 146 | c = c.substring(1); 147 | } 148 | 149 | if (c.indexOf(name) == 0) return c.substring(name.length, c.length); 150 | } 151 | 152 | return; 153 | } 154 | }, { 155 | key: "delete", 156 | value: function _delete(name) { 157 | this.set(name, '', -100); 158 | } 159 | }, { 160 | key: "exists", 161 | value: function exists(name) { 162 | return Helper.isPresent(this.get(name)); 163 | } 164 | }, { 165 | key: "setUtms", 166 | value: function setUtms() { 167 | var utmArray = ['utm_source', 'utm_medium', 'utm_term', 'utm_content', 'utm_campaign', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic']; 168 | var exists = false; 169 | 170 | for (var i = 0, l = utmArray.length; i < l; i++) { 171 | if (Helper.isPresent(Url.getParameterByName(utmArray[i]))) { 172 | exists = true; 173 | break; 174 | } 175 | } 176 | 177 | if (exists) { 178 | var val, 179 | save = {}; 180 | 181 | for (var i = 0, l = utmArray.length; i < l; i++) { 182 | val = Url.getParameterByName(utmArray[i]); 183 | 184 | if (Helper.isPresent(val)) { 185 | save[utmArray[i]] = val; 186 | } 187 | } 188 | 189 | this.set('utm', JSON.stringify(save)); 190 | } 191 | } 192 | }, { 193 | key: "getUtm", 194 | value: function getUtm(name) { 195 | if (this.exists('utm')) { 196 | var utms = JSON.parse(this.get('utm')); 197 | return name in utms ? utms[name] : ''; 198 | } 199 | } 200 | }]); 201 | 202 | return Cookie; 203 | }(); 204 | 205 | var Url = /*#__PURE__*/function () { 206 | function Url() { 207 | _classCallCheck(this, Url); 208 | } 209 | 210 | _createClass(Url, null, [{ 211 | key: "getParameterByName", 212 | value: // http://stackoverflow.com/a/901144/1231563 213 | function getParameterByName(name, url) { 214 | if (!url) url = window.location.href; 215 | name = name.replace(/[\[\]]/g, "\\$&"); 216 | var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)", "i"), 217 | results = regex.exec(url); 218 | if (!results) return null; 219 | if (!results[2]) return ''; 220 | return decodeURIComponent(results[2].replace(/\+/g, " ")); 221 | } 222 | }, { 223 | key: "externalHost", 224 | value: function externalHost(link) { 225 | return link.hostname != location.hostname && link.protocol.indexOf('http') === 0; 226 | } 227 | }]); 228 | 229 | return Url; 230 | }(); 231 | 232 | var Pixel = /*#__PURE__*/function () { 233 | function Pixel(event, timestamp, optional) { 234 | _classCallCheck(this, Pixel); 235 | 236 | this.params = []; 237 | this.event = event; 238 | this.timestamp = timestamp; 239 | this.optional = Helper.optionalData(optional); 240 | this.buildParams(); 241 | this.send(); 242 | } 243 | 244 | _createClass(Pixel, [{ 245 | key: "buildParams", 246 | value: function buildParams() { 247 | var attr = this.getAttribute(); 248 | 249 | for (var index in attr) { 250 | if (attr.hasOwnProperty(index)) { 251 | this.setParam(index, attr[index](index)); 252 | } 253 | } 254 | } 255 | }, { 256 | key: "getAttribute", 257 | value: function getAttribute() { 258 | var _this = this; 259 | 260 | return _objectSpread({ 261 | id: function id() { 262 | return Config.id; 263 | }, 264 | // website Id 265 | uid: function uid() { 266 | return Cookie.get('uid'); 267 | }, 268 | // user Id 269 | ev: function ev() { 270 | return _this.event; 271 | }, 272 | // event being triggered 273 | ed: function ed() { 274 | return _this.optional; 275 | }, 276 | // any event data to pass along 277 | v: function v() { 278 | return Config.version; 279 | }, 280 | // openpixel.js version 281 | dl: function dl() { 282 | return window.location.href; 283 | }, 284 | // document location 285 | rl: function rl() { 286 | return document.referrer; 287 | }, 288 | // referrer location 289 | ts: function ts() { 290 | return _this.timestamp; 291 | }, 292 | // timestamp when event was triggered 293 | de: function de() { 294 | return document.characterSet; 295 | }, 296 | // document encoding 297 | sr: function sr() { 298 | return window.screen.width + 'x' + window.screen.height; 299 | }, 300 | // screen resolution 301 | vp: function vp() { 302 | return window.innerWidth + 'x' + window.innerHeight; 303 | }, 304 | // viewport size 305 | cd: function cd() { 306 | return window.screen.colorDepth; 307 | }, 308 | // color depth 309 | dt: function dt() { 310 | return document.title; 311 | }, 312 | // document title 313 | bn: function bn() { 314 | return Browser.nameAndVersion(); 315 | }, 316 | // browser name and version number 317 | md: function md() { 318 | return Browser.isMobile(); 319 | }, 320 | // is a mobile device? 321 | ua: function ua() { 322 | return Browser.userAgent(); 323 | }, 324 | // user agent 325 | tz: function tz() { 326 | return new Date().getTimezoneOffset(); 327 | }, 328 | // timezone 329 | utm_source: function utm_source(key) { 330 | return Cookie.getUtm(key); 331 | }, 332 | // get the utm source 333 | utm_medium: function utm_medium(key) { 334 | return Cookie.getUtm(key); 335 | }, 336 | // get the utm medium 337 | utm_term: function utm_term(key) { 338 | return Cookie.getUtm(key); 339 | }, 340 | // get the utm term 341 | utm_content: function utm_content(key) { 342 | return Cookie.getUtm(key); 343 | }, 344 | // get the utm content 345 | utm_campaign: function utm_campaign(key) { 346 | return Cookie.getUtm(key); 347 | }, 348 | // get the utm campaign 349 | utm_source_platform: function utm_source_platform(key) { 350 | return Cookie.getUtm(key); 351 | }, 352 | // get the utm source platform 353 | utm_creative_format: function utm_creative_format(key) { 354 | return Cookie.getUtm(key); 355 | }, 356 | // get the utm creative format 357 | utm_marketing_tactic: function utm_marketing_tactic(key) { 358 | return Cookie.getUtm(key); 359 | } 360 | }, Config.params); 361 | } 362 | }, { 363 | key: "setParam", 364 | value: function setParam(key, val) { 365 | if (Helper.isPresent(val)) { 366 | this.params.push("".concat(key, "=").concat(encodeURIComponent(val))); 367 | } else { 368 | this.params.push("".concat(key, "=")); 369 | } 370 | } 371 | }, { 372 | key: "send", 373 | value: function send() { 374 | window.navigator.sendBeacon ? this.sendBeacon() : this.sendImage(); 375 | } 376 | }, { 377 | key: "sendBeacon", 378 | value: function sendBeacon() { 379 | window.navigator.sendBeacon(this.getSourceUrl()); 380 | } 381 | }, { 382 | key: "sendImage", 383 | value: function sendImage() { 384 | this.img = document.createElement('img'); 385 | this.img.src = this.getSourceUrl(); 386 | this.img.style.display = 'none'; 387 | this.img.width = '1'; 388 | this.img.height = '1'; 389 | document.getElementsByTagName('body')[0].appendChild(this.img); 390 | } 391 | }, { 392 | key: "getSourceUrl", 393 | value: function getSourceUrl() { 394 | return "".concat(pixelEndpoint, "?").concat(this.params.join('&')); 395 | } 396 | }]); 397 | 398 | return Pixel; 399 | }(); // update the cookie if it exists, if it doesn't, create a new one, lasting 2 years 400 | 401 | 402 | Cookie.exists('uid') ? Cookie.set('uid', Cookie.get('uid'), 2 * 365 * 24 * 60) : Cookie.set('uid', Helper.guid(), 2 * 365 * 24 * 60); // save any utms through as session cookies 403 | 404 | Cookie.setUtms(); // process the queue and future incoming commands 405 | 406 | pixelFunc.process = function (method, value, optional) { 407 | if (method === 'init') { 408 | Config.id = value; 409 | } else if (method === 'param') { 410 | Config.params[value] = function () { 411 | return optional; 412 | }; 413 | } else if (method === 'event') { 414 | if (value === 'pageload' && !Config.pageLoadOnce) { 415 | Config.pageLoadOnce = true; 416 | new Pixel(value, pixelFunc.t, optional); 417 | } else if (value !== 'pageload' && value !== 'pageclose') { 418 | new Pixel(value, Helper.now(), optional); 419 | } 420 | } 421 | }; // run the queued calls from the snippet to be processed 422 | 423 | 424 | for (var i = 0, l = pixelFunc.queue.length; i < l; i++) { 425 | pixelFunc.process.apply(pixelFunc, pixelFunc.queue[i]); 426 | } // https://github.com/GoogleChromeLabs/page-lifecycle/blob/master/src/Lifecycle.mjs 427 | // Safari does not reliably fire the `pagehide` or `visibilitychange` 428 | 429 | 430 | var isSafari = (typeof safari === "undefined" ? "undefined" : _typeof(safari)) === 'object' && safari.pushNotification; 431 | var isPageHideSupported = ('onpageshow' in self); // IE9-10 do not support the pagehide event, so we fall back to unload 432 | // pagehide event is more reliable but less broad than unload event for mobile and modern browsers 433 | 434 | var pageCloseEvent = isPageHideSupported && !isSafari ? 'pagehide' : 'unload'; 435 | window.addEventListener(pageCloseEvent, function () { 436 | if (!Config.pageCloseOnce) { 437 | Config.pageCloseOnce = true; 438 | new Pixel('pageclose', Helper.now(), function () { 439 | // if a link was clicked in the last 5 seconds that goes to an external host, pass it through as event data 440 | if (Helper.isPresent(Config.externalHost) && Helper.now() - Config.externalHost.time < 5 * 1000) { 441 | return Config.externalHost.link; 442 | } 443 | }); 444 | } 445 | }); 446 | 447 | window.onload = function () { 448 | var aTags = document.getElementsByTagName('a'); 449 | 450 | for (var i = 0, l = aTags.length; i < l; i++) { 451 | aTags[i].addEventListener('click', function (_e) { 452 | if (Url.externalHost(this)) { 453 | Config.externalHost = { 454 | link: this.href, 455 | time: Helper.now() 456 | }; 457 | } 458 | }.bind(aTags[i])); 459 | } 460 | 461 | var dataAttributes = document.querySelectorAll('[data-opix-event]'); 462 | 463 | for (var i = 0, l = dataAttributes.length; i < l; i++) { 464 | dataAttributes[i].addEventListener('click', function (_e) { 465 | var event = this.getAttribute('data-opix-event'); 466 | 467 | if (event) { 468 | new Pixel(event, Helper.now(), this.getAttribute('data-opix-data')); 469 | } 470 | }.bind(dataAttributes[i])); 471 | } 472 | }; 473 | }(window, document, window["opix"], "opix", "/pixel.gif", 1)); 474 | -------------------------------------------------------------------------------- /dist/openpixel.min.js: -------------------------------------------------------------------------------- 1 | // Open Pixel v1.3.1 | Published By Dockwa | Created By Stuart Yamartino | MIT License 2 | !function(i,u,r){"use strict";function e(e,t){var n,r=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),r.push.apply(r,n)),r}function n(i){for(var t=1;t 2 | 5 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // ---------- Configurations for your custom build of open pixel ---------- // 2 | 3 | // This is the header comment that will be included at the top of the "dist/openpixel.js" file 4 | var HEADER_COMMENT = process.env.OPIX_HEADER_COMMENT || '// Open Pixel v1.3.1 | Published By Dockwa | Created By Stuart Yamartino | MIT License\n'; 5 | 6 | // This is where the compiled snippet and openpixel.js files will be dropped 7 | var DESTINATION_FOLDER = process.env.OPIX_DESTINATION_FOLDER || './dist'; 8 | 9 | // The name of the global function and the cookie prefix that will be included in the snippet and is the client to fire off custom events 10 | var PIXEL_FUNC_NAME = process.env.OPIX_PIXEL_FUNC_NAME || 'opix'; 11 | 12 | // The remote URL of the pixel.gif file that will be pinged by the browser to send tracking information 13 | var PIXEL_ENDPOINT = process.env.OPIX_PIXEL_ENDPOINT || '/pixel.gif'; 14 | 15 | // The core openpixel.min.js file that the snippet will loaded asynchronously into the browser 16 | var JS_ENDPOINT = process.env.OPIX_JS_ENDPOINT || '/openpixel.js'; 17 | 18 | // The current version of your openpixel configuration 19 | var VERSION = process.env.OPIX_VERSION || '1'; 20 | 21 | // ------------------------------------------------------------------------// 22 | 23 | 24 | // include plug-ins 25 | var gulp = require('gulp'); 26 | var concat = require('gulp-concat'); 27 | var iife = require('gulp-iife'); 28 | var inject = require('gulp-inject-string'); 29 | var rename = require('gulp-rename'); 30 | var uglify = require('gulp-uglify'); 31 | var babel = require('gulp-babel'); 32 | 33 | // ---- Compile openpixel.js and openpixel.min.js files ---- // 34 | function openpixel() { 35 | return gulp.src([ 36 | './src/config.js', 37 | './src/helper.js', 38 | './src/browser.js', 39 | './src/cookie.js', 40 | './src/url.js', 41 | './src/pixel.js', 42 | './src/setup.js', 43 | ]) 44 | .pipe(concat('openpixel.js')) 45 | .pipe(babel()) 46 | .pipe(iife({ 47 | useStrict: false, 48 | params: ['window', 'document', 'pixelFunc', 'pixelFuncName', 'pixelEndpoint', 'versionNumber'], 49 | args: ['window', 'document', 'window["'+PIXEL_FUNC_NAME+'"]', '"'+PIXEL_FUNC_NAME+'"', '"'+PIXEL_ENDPOINT+'"', VERSION] 50 | })) 51 | .pipe(inject.prepend(HEADER_COMMENT)) 52 | .pipe(inject.replace('OPIX_FUNC', PIXEL_FUNC_NAME)) 53 | // This will output the non-minified version 54 | .pipe(gulp.dest(DESTINATION_FOLDER)) 55 | // This will minify and rename to openpixel.min.js 56 | .pipe(uglify()) 57 | .pipe(inject.prepend(HEADER_COMMENT)) 58 | .pipe(rename({ extname: '.min.js' })) 59 | .pipe(gulp.dest(DESTINATION_FOLDER)); 60 | } 61 | 62 | // ---- Compile snippet.html file ---- // 63 | function snippet() { 64 | return gulp.src('./src/snippet.js') 65 | .pipe(inject.replace('JS_URL', JS_ENDPOINT)) 66 | .pipe(inject.replace('OPIX_FUNC', PIXEL_FUNC_NAME)) 67 | // This will minify and rename to snippet.html 68 | .pipe(uglify()) 69 | .pipe(inject.prepend('\n\n')) 71 | .pipe(rename({ extname: '.html' })) 72 | .pipe(gulp.dest(DESTINATION_FOLDER)); 73 | } 74 | 75 | // watch files and run gulp 76 | function watch() { 77 | gulp.watch('src/*', openpixel); 78 | gulp.watch('src/*', snippet); 79 | } 80 | 81 | // run all tasks once 82 | var build = gulp.parallel(openpixel, snippet); 83 | 84 | exports.openpixel = openpixel; 85 | exports.snippet = snippet; 86 | exports.watch = watch; 87 | exports.build = build; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openpixel", 3 | "version": "1.3.1", 4 | "description": "Open Pixel is a JavaScript library for creating embeddable and intelligent tracking pixels", 5 | "main": "openpixel.min.js", 6 | "dependencies": { 7 | "@babel/core": "^7.10.4", 8 | "@babel/plugin-proposal-class-properties": "^7.10.4", 9 | "@babel/preset-env": "^7.10.4", 10 | "gulp": "^4.0.2", 11 | "gulp-babel": "^8.0.0", 12 | "gulp-concat": "^2.6.1", 13 | "gulp-iife": "^0.4.0", 14 | "gulp-inject-string": "^1.1.2", 15 | "gulp-rename": "^2.0.0", 16 | "gulp-uglify": "^3.0.2", 17 | "natives": "^1.1.6" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1", 21 | "gulp": "./node_modules/.bin/gulp", 22 | "dist": "gulp build", 23 | "watch": "gulp watch" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/dockwa/openpixel.git" 28 | }, 29 | "author": "Stuart Yamartino", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /pixel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dockwa/openpixel/fdc705c109017336c972e3c93c7944879077ac4f/pixel.gif -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | class Browser { 2 | static nameAndVersion() { 3 | // http://stackoverflow.com/questions/5916900/how-can-you-detect-the-version-of-a-browser 4 | var ua= navigator.userAgent, tem, 5 | M= ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; 6 | if (/trident/i.test(M[1])) { 7 | tem= /\brv[ :]+(\d+)/g.exec(ua) || []; 8 | return 'IE '+(tem[1] || ''); 9 | } 10 | if (M[1]=== 'Chrome') { 11 | tem= ua.match(/\b(OPR|Edge)\/(\d+)/); 12 | if(tem!= null) return tem.slice(1).join(' ').replace('OPR', 'Opera'); 13 | } 14 | M= M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?']; 15 | if((tem= ua.match(/version\/(\d+)/i))!= null) M.splice(1, 1, tem[1]); 16 | return M.join(' '); 17 | } 18 | 19 | static isMobile() { 20 | return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); 21 | } 22 | 23 | static userAgent() { 24 | return window.navigator.userAgent; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | id: '', 3 | params: {}, 4 | version: versionNumber 5 | } 6 | -------------------------------------------------------------------------------- /src/cookie.js: -------------------------------------------------------------------------------- 1 | class Cookie { 2 | static prefix() { 3 | return `__${pixelFuncName}_`; 4 | } 5 | 6 | static set(name, value, minutes, path = '/') { 7 | var expires = ''; 8 | if (Helper.isPresent(minutes)) { 9 | var date = new Date(); 10 | date.setTime(date.getTime() + (minutes * 60 * 1000)); 11 | expires = `expires=${date.toGMTString()}; `; 12 | } 13 | document.cookie = `${this.prefix()}${name}=${value}; ${expires}path=${path}; SameSite=Lax`; 14 | } 15 | 16 | static get(name) { 17 | var name = `${this.prefix()}${name}=`; 18 | var ca = document.cookie.split(';'); 19 | for (var i=0; i Config.id, // website Id 23 | uid: () => Cookie.get('uid'), // user Id 24 | ev: () => this.event, // event being triggered 25 | ed: () => this.optional, // any event data to pass along 26 | v: () => Config.version, // openpixel.js version 27 | dl: () => window.location.href, // document location 28 | rl: () => document.referrer, // referrer location 29 | ts: () => this.timestamp, // timestamp when event was triggered 30 | de: () => document.characterSet, // document encoding 31 | sr: () => window.screen.width + 'x' + window.screen.height, // screen resolution 32 | vp: () => window.innerWidth + 'x' + window.innerHeight, // viewport size 33 | cd: () => window.screen.colorDepth, // color depth 34 | dt: () => document.title, // document title 35 | bn: () => Browser.nameAndVersion(), // browser name and version number 36 | md: () => Browser.isMobile(), // is a mobile device? 37 | ua: () => Browser.userAgent(), // user agent 38 | tz: () => (new Date()).getTimezoneOffset(), // timezone 39 | utm_source: key => Cookie.getUtm(key), // get the utm source 40 | utm_medium: key => Cookie.getUtm(key), // get the utm medium 41 | utm_term: key => Cookie.getUtm(key), // get the utm term 42 | utm_content: key => Cookie.getUtm(key), // get the utm content 43 | utm_campaign: key => Cookie.getUtm(key), // get the utm campaign 44 | utm_source_platform: key => Cookie.getUtm(key), // get the utm source platform 45 | utm_creative_format: key => Cookie.getUtm(key), // get the utm creative format 46 | utm_marketing_tactic: key => Cookie.getUtm(key), // get the utm marketing tactic 47 | ...Config.params 48 | } 49 | } 50 | 51 | setParam(key, val) { 52 | if (Helper.isPresent(val)) { 53 | this.params.push(`${key}=${encodeURIComponent(val)}`); 54 | } else { 55 | this.params.push(`${key}=`); 56 | } 57 | } 58 | 59 | send() { 60 | window.navigator.sendBeacon ? this.sendBeacon() : this.sendImage(); 61 | } 62 | 63 | sendBeacon() { 64 | window.navigator.sendBeacon(this.getSourceUrl()); 65 | } 66 | 67 | sendImage() { 68 | this.img = document.createElement('img'); 69 | this.img.src = this.getSourceUrl(); 70 | this.img.style.display = 'none'; 71 | this.img.width = '1'; 72 | this.img.height = '1'; 73 | document.getElementsByTagName('body')[0].appendChild(this.img); 74 | } 75 | 76 | getSourceUrl() { 77 | return `${pixelEndpoint}?${this.params.join('&')}`; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/setup.js: -------------------------------------------------------------------------------- 1 | // update the cookie if it exists, if it doesn't, create a new one, lasting 2 years 2 | Cookie.exists('uid') ? Cookie.set('uid', Cookie.get('uid'), 2*365*24*60) : Cookie.set('uid', Helper.guid(), 2*365*24*60); 3 | // save any utms through as session cookies 4 | Cookie.setUtms(); 5 | 6 | // process the queue and future incoming commands 7 | pixelFunc.process = function(method, value, optional) { 8 | if (method === 'init') { 9 | Config.id = value; 10 | } else if(method === 'param') { 11 | Config.params[value] = () => optional 12 | } else if(method === 'event') { 13 | if(value === 'pageload' && !Config.pageLoadOnce) { 14 | Config.pageLoadOnce = true; 15 | new Pixel(value, pixelFunc.t, optional); 16 | } else if(value !== 'pageload' && value !== 'pageclose') { 17 | new Pixel(value, Helper.now(), optional); 18 | } 19 | } 20 | } 21 | 22 | // run the queued calls from the snippet to be processed 23 | for (var i = 0, l = pixelFunc.queue.length; i < l; i++) { 24 | pixelFunc.process.apply(pixelFunc, pixelFunc.queue[i]); 25 | } 26 | 27 | // https://github.com/GoogleChromeLabs/page-lifecycle/blob/master/src/Lifecycle.mjs 28 | // Safari does not reliably fire the `pagehide` or `visibilitychange` 29 | var isSafari = typeof safari === 'object' && safari.pushNotification; 30 | var isPageHideSupported = 'onpageshow' in self; 31 | 32 | // IE9-10 do not support the pagehide event, so we fall back to unload 33 | // pagehide event is more reliable but less broad than unload event for mobile and modern browsers 34 | var pageCloseEvent = isPageHideSupported && !isSafari ? 'pagehide' : 'unload'; 35 | 36 | window.addEventListener(pageCloseEvent, function() { 37 | if (!Config.pageCloseOnce) { 38 | Config.pageCloseOnce = true; 39 | new Pixel('pageclose', Helper.now(), function() { 40 | // if a link was clicked in the last 5 seconds that goes to an external host, pass it through as event data 41 | if (Helper.isPresent(Config.externalHost) && (Helper.now() - Config.externalHost.time) < 5*1000) { 42 | return Config.externalHost.link; 43 | } 44 | }); 45 | } 46 | }); 47 | 48 | window.onload = function() { 49 | var aTags = document.getElementsByTagName('a'); 50 | for (var i = 0, l = aTags.length; i < l; i++) { 51 | aTags[i].addEventListener('click', function(_e) { 52 | if (Url.externalHost(this)) { 53 | Config.externalHost = { link: this.href, time: Helper.now() }; 54 | } 55 | }.bind(aTags[i])); 56 | } 57 | 58 | var dataAttributes = document.querySelectorAll('[data-OPIX_FUNC-event]') 59 | for (var i = 0, l = dataAttributes.length; i < l; i++) { 60 | dataAttributes[i].addEventListener('click', function(_e) { 61 | var event = this.getAttribute('data-OPIX_FUNC-event'); 62 | if (event) { 63 | new Pixel(event, Helper.now(), this.getAttribute('data-OPIX_FUNC-data')); 64 | } 65 | }.bind(dataAttributes[i])); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/snippet.js: -------------------------------------------------------------------------------- 1 | !function(window, document, script, http, opix, cacheTime, one, two, three) { 2 | // return if the setup has already occurred 3 | // this is to prevent double loading openpixel.js if someone accidentally had this more than once on a page 4 | if (window[opix]) return; 5 | 6 | // setup the queue to collect all of the calls to openpixel.js before it is loaded 7 | one = window[opix] = function() { 8 | // if openpixel.js has loaded, pass the argument through to it 9 | // if openpixel.js has not loaded yet, queue the calls in an array 10 | one.process ? one.process.apply(one, arguments) : one.queue.push(arguments) 11 | } 12 | // setup an empty queue array 13 | one.queue = []; 14 | // get the current time (integer) that the page was loaded and save for later 15 | one.t = 1 * new Date(); 16 | 17 | // create a script tag 18 | two = document.createElement(script); 19 | // set the script tag to run async 20 | two.async = 1; 21 | // set the source of the script tag and cache bust every 24 hours 22 | two.src = http + '?t=' + (Math.ceil(new Date()/cacheTime)*cacheTime); 23 | 24 | // get the first