├── .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 | [](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