├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── electric-love.js ├── event-layer-logo.png ├── event-layer.js ├── index.d.ts ├── index.html ├── integration-progress.md └── package.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | index.js 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 James Futhey 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Event Layer](https://raw.githubusercontent.com/kidGodzilla/event-layer/master/event-layer-logo.png) 2 | 3 | ----- 4 | 5 | [![npm version](https://badge.fury.io/js/event-layer.svg)](https://www.npmjs.com/package/event-layer) 6 | [![License](https://img.shields.io/badge/license-MIT%20License-blue.svg)](https://opensource.org/licenses/MIT) 7 | ![Contains](https://img.shields.io/badge/contains-badges-orange.svg) 8 | 9 | A very very simple abstraction layer for analytics code. Write your events once, then send them where ever you want. 10 | 11 | ### [Demo](https://kidgodzilla.github.io/event-layer/) 12 | 13 | ## Stats 14 | 15 | **Number of integrations:** 32 16 | 17 | **Number of tested integrations fully tested / considered stable & production-ready:** 14 18 | 19 | __Would you like to request an integration?__ We'd love to help out! [Open an issue](https://github.com/kidGodzilla/event-layer/issues/new) to get started. 20 | 21 | If the docs are publicly available or it's supported by Analytics.js, it should be even easier. Link any docs you may have for Analytics.js or the Javascript API, where available. 22 | 23 | ## Installation via NPM 24 | 25 | ``` 26 | npm install event-layer 27 | ``` 28 | 29 | Once installed, you'll need to include `event-layer.js` in your project, and then (optionally) instantiate a new object (aliasing it to a new global, if you prefer). Continue reading to see how that might work. 30 | 31 | ## Installation via CDN 32 | 33 | Include the following scripts in your project: 34 | 35 | `https://cdn.jsdelivr.net/npm/event-layer@latest/event-layer.js` 36 | 37 | Then follow the instructions below. 38 | 39 | ## What is it? 40 | 41 | **“Event Layer”** is an extensible abstraction layer for working with common third-party Analytics libraries. 42 | 43 | Since more or less all analytics libraries work the same way (allowing you to identify and describe users, and track the events which they perform), 44 | **“Event Layer”** creates an abstraction layer and a set of adapters that allow you to write generic Analytics Tracking code once, and update your 45 | configuration later to send your data anywhere you need. 46 | 47 | 48 | ## How does it work? 49 | 50 | 1. Install your third-party analytics libraries in your application or website (you don't need to do anything special) 51 | 2. Install `event-layer.js` in your website or app. 52 | 3. Instantiate a new instance of **EventLayer:** (e.g. `var Analytics = new EventLayer();`). 53 | 4. Instead of filling your app or website with service-specific tracking code, write generic code using the **“Event Layer” Javascript API** (described below). 54 | 5. **“Event Layer”** will detect the third-party Analytics services you have installed on your website or app, and use it's own, community-maintained adapters to propagate your events, in an identical format, to each third-party service, using the latest version(s) of their APIs. 55 | 6. **(Optional):** You can extend **“Event Layer”** by writing your own custom adapters for third-party services not yet supported. Each adapter only requires about 12 lines of Javascript, and the community is available to help you ship your first PR to **“Event Layer”**. 56 | 57 | 58 | ## Integrations / Services Currently Supported 59 | 60 | Integration | Stable 61 | ------------ | ------------- 62 | Segment.com | ✔️ 63 | Mixpanel | ✔️ 64 | Google Analytics | ✔️ 65 | PostHog | ✔️ 66 | Beam Analytics (New!) | ✔️ 67 | June.so (New!) | ✔️ 68 | Crisp.chat | ✔️ 69 | Intercom | ✔️ 70 | Sentry | ✔️ 71 | Facebook Tracking Pixel | ✔️ 72 | Google Tag Manager | ✔️ 73 | Heap | ✔️ 74 | Amplitude | ✔️ 75 | Keen.io | ✔️ 76 | Rollbar | ✔️ 77 | Talkus | ✔️ 78 | Crazy Egg | ✔️ 79 | Elev.io | ✔️ 80 | Drift | ✔️ 81 | Drip | ✔️ 82 | Hello Bar | ✔️ 83 | Improvely | ✔️ 84 | Helpscout | 85 | Fullstory | 86 | Olark | 87 | Calq | 88 | Castle | 89 | Lucky Orange | 90 | BugHerd | 91 | Bugsnag | 92 | Chameleon | 93 | Inspectlet | 94 | Qualaroo | 95 | Customer.io | 96 | Logspot.io | 97 | 98 | *We rely on users to report their usage. Feel free to open an issue just to say "I tested ____, and it works!" 99 | 100 | ## Creating a new instance of EventLayer 101 | We realize that not everyone wants to litter their code with calls to a global named **“Event Layer”**. So you'll probably want to start off by instantiating a new instance of **“Event Layer”**. 102 | 103 | ``` 104 | var Analytics = new EventLayer(); 105 | ``` 106 | 107 | You can name it whatever you like, but the examples given below will need to be modified appropriately. 108 | 109 | ## EventLayer.identify(userId, userProperties) 110 | 111 | This method allows you to identify the current visitor, and (optionally) describe the user. 112 | 113 | _Only identify the user, do not describe any user properties:_ 114 | 115 | ``` 116 | Analytics.identify(''); 117 | ``` 118 | 119 | 120 | _Identify the user and describe user properties:_ 121 | 122 | ``` 123 | Analytics.identify('', { 124 | name: 'John Doe', 125 | subscribedToNewsletter: false 126 | }); 127 | ``` 128 | 129 | This is similar to the `Identify` method found in Mixpanel, Heap, and Analytics.js. 130 | 131 | 132 | ## EventLayer.track(eventName, eventProperties) 133 | 134 | This method allows you to identify the current visitor, and (optionally) describe the user. 135 | 136 | _Track a simple event, with no event properties:_ 137 | 138 | ``` 139 | Analytics.track('click_signup_button'); 140 | ``` 141 | 142 | _Track a more complex event, with event properties:_ 143 | 144 | ``` 145 | Analytics.track('purchase', { 146 | amount: 32.00, 147 | itemCount: 4, 148 | shippingSpeed: 'next-day' 149 | }); 150 | ``` 151 | 152 | This is similar to the `Track` method found in Mixpanel, Heap, and Analytics.js. 153 | 154 | ## EventLayer.page(category, name, properties) 155 | 156 | _This method has been implemented identically to Analytics.js. See documentation for more details. Feel free to open an issue if you have any questions!_ 157 | 158 | ## EventLayer.alias(userId, previousId) 159 | 160 | _This method has been implemented identically to Analytics.js. See documentation for more details. Feel free to open an issue if you have any questions!_ 161 | 162 | ## EventLayer.group(groupId, traits) 163 | 164 | _This method has been implemented identically to Analytics.js. See documentation for more details. Feel free to open an issue if you have any questions!_ 165 | 166 | ## EventLayer.fbTrack(eventName, eventProperties) 167 | 168 | _Send track events to the facebook tracking pixel_ 169 | 170 | This method implements a copy of the track() method above, but it supports events specific to the Facebook Tracking Pixel as well. 171 | 172 | See documentation https://developers.facebook.com/docs/facebook-pixel/api-reference for more details on implementing the Facebook tracking pixel on your website. 173 | 174 | 175 | 176 | ## Writing an Adapter 177 | 178 | Below is an example of a blank adapter. 179 | 180 | ``` 181 | 'blank-adapter-template': { // Do not modify this template 182 | enabled: false, // Change to true once you're completed testing 183 | test: function () {}, 184 | identify: function (userId, userProperties) {}, 185 | track: function (eventName, eventProperties) {} 186 | } 187 | ``` 188 | 189 | It has three main components: 190 | 191 | ### 1. Test: 192 | This function, once evaluated, should provide an answer to the question “has a specific third-party analytics library been installed on this page? Is it active? Should we try to send events to this service?” 193 | 194 | This can be as simple as sniffing the window object for a global variables, and a commonly-used (and not likely to disappear) method or property: 195 | 196 | ``` 197 | test: function () { 198 | return window.ga && window.ga.loaded; 199 | } 200 | ``` 201 | 202 | (A simple example taken from the Google Analytics adapter) 203 | 204 | ### 2. identify: 205 | This function takes data from our generic `identify` method, and passes it along to a third-party library, via an adapter. 206 | 207 | This should contain a minimal number of integrity checks and transforms, as well as a lightweight wrapper for the library's identify and/or describe functionality. 208 | 209 | ``` 210 | identify: function (userId, userProperties) { 211 | // Send the identify call to Mixpanel's JS library 212 | if (window.mixpanel && userId) 213 | mixpanel.identify(userId); 214 | 215 | // Set people properties on our identified user 216 | if (window.mixpanel && userProperties) 217 | mixpanel.people.set(userProperties); 218 | } 219 | ``` 220 | 221 | (A simple example taken from the Mixpanel adapter) 222 | 223 | ### 3. track: 224 | This function takes data from our generic `track` method, and passes it along to a third-party library, via an adapter. 225 | 226 | This should contain a minimal number of integrity checks and transforms, as well as a lightweight wrapper for the library's event tracking functionality. 227 | 228 | ``` 229 | track: function (eventName, eventProperties) { 230 | // Send the tracked event to Heap's JS library 231 | if (window.heap && eventName) 232 | heap.track(eventName, eventProperties); 233 | } 234 | ``` 235 | 236 | (A simple example taken from the Heap Analytics adapter) 237 | 238 | ## Adding or modifying an adapter on the fly 239 | You can add or modify an adapter on the fly, after you instantiate EventLayer, by calling the `addAdapter` method. 240 | 241 | ``` 242 | Analytics.addAdapter('my-analytics', { 243 | enabled: true, 244 | test: function () { return true }, 245 | track: function (eventName, eventProperties) { console.log('tracking') } 246 | }); 247 | ``` 248 | 249 | ## Debugging 250 | 251 | You can set `window.__debug` to `true` to enable debugging (console output) for EventLayer. 252 | 253 | ``` 254 | window.__debug = true; 255 | ``` 256 | 257 | 258 | ## Pull Requests 259 | 260 | **Yes, Please & Thank you!** 261 | 262 | To keep things organized, please open an issue for discussion before putting too much work into a pull request. I would feel really bad if you put a lot of work into something, only for it to not make sense to include it or merge the Pull Request. 263 | 264 | Also, to help keep things organized, try to submit individual pull requests for each issue, and keep the scope of each issue relatively small. 265 | 266 | For example, if you wanted to add a couple of new adapters, split them into multiple pull requests so we can review them individually. 267 | 268 | 269 | ## Issues 270 | 271 | Something feel broken? Open an issue! 272 | Something you feel is missing? Open an issue (Although we may not be able to get to everything, pull requests are most welcome!) 273 | 274 | 275 | ## Thanks! 276 | -------------------------------------------------------------------------------- /electric-love.js: -------------------------------------------------------------------------------- 1 | var EventLayer = (function EventLayer () { 2 | 3 | /** 4 | * Third-party adapters for EventLayer.track(), EventLayer.identify(), etc. 5 | */ 6 | var thirdPartyAdapters = { 7 | 'segment': { 8 | enabled: true, 9 | test: function () { 10 | // This test not only tests for Segment's variety of Analytics.js, 11 | // it distinguishes it from this library, so you can simply alias this to window.analytics without worry. 12 | return window.analytics && typeof(window.analytics) === 'object' && window.analytics.Integrations && typeof(window.analytics.Integrations) === 'object'; 13 | }, 14 | identify: function (userId, userProperties) { 15 | // Send the identify call to Segment's Analytics.js library 16 | // console.log('Identifying: ', userId, userProperties); 17 | if (window.analytics && userId) analytics.identify(userId, userProperties); 18 | }, 19 | track: function (eventName, eventProperties) { 20 | // Send the tracked event to Segment's Analytics.js library 21 | // console.log('tracking: ', eventName, eventProperties); 22 | if (window.analytics && eventName) analytics.track(eventName, eventProperties); 23 | }, 24 | page: function (category, name, properties) { 25 | // Send the page call to Segment's Analytics.js library 26 | // console.log('page: ', category, name, properties); 27 | if (window.analytics && name) analytics.page(category, name, properties); 28 | }, 29 | alias: function (userId, previousId) { 30 | // Send the alias call to Segment's Analytics.js library 31 | // console.log('alias: ', userId, previousId); 32 | if (window.analytics && userId && previousId) analytics.alias(userId, previousId); 33 | }, 34 | group: function (groupId, traits) { 35 | // Send the group call to Segment's Analytics.js library 36 | // console.log('group: ', groupId, traits); 37 | if (window.analytics && groupId) analytics.group(groupId, traits); 38 | } 39 | }, 40 | 'mixpanel': { 41 | enabled: true, 42 | test: function () { 43 | return window.mixpanel && window.mixpanel.__loaded; 44 | }, 45 | identify: function (userId, userProperties, options) { 46 | // Send the identify call to Mixpanel's JS library 47 | // console.log('Identifying: ', userId, userProperties); 48 | if (window.mixpanel && userId) mixpanel.identify(userId); 49 | 50 | if (window.mixpanel && userProperties) { 51 | if (options && options.setOnce) { 52 | // Set people properties on our identified user, but only if they have not yet been set. 53 | mixpanel.people.set_once(userProperties); 54 | } else { 55 | // Set people properties on our identified user 56 | mixpanel.people.set(userProperties); 57 | } 58 | } 59 | }, 60 | track: function (eventName, eventProperties) { 61 | // Send the tracked event to Mixpanel's JS library 62 | // console.log('tracking: ', eventName, eventProperties); 63 | if (window.mixpanel && eventName) mixpanel.track(eventName, eventProperties); 64 | } 65 | }, 66 | 'heap': { 67 | enabled: true, 68 | test: function () { 69 | return (window.heap && window.heap.track) ? true : false; 70 | }, 71 | identify: function (userId, userProperties) { 72 | // Send the identify call to Heap's JS library 73 | // console.log('Identifying: ', userId, userProperties); 74 | if (window.heap && userId) heap.identify(userId); 75 | 76 | // Set people properties on our identified user 77 | if (window.heap && userProperties) heap.addUserProperties(userProperties); 78 | }, 79 | track: function (eventName, eventProperties) { 80 | // Send the tracked event to Heap's JS library 81 | // console.log('tracking: ', eventName, eventProperties); 82 | if (window.heap && eventName) heap.track(eventName, eventProperties); 83 | } 84 | }, 85 | 'intercom': { 86 | enabled: true, 87 | test: function () { 88 | return (window.Intercom && window.Intercom('getVisitorId') && window.Intercom.booted) ? true : false; 89 | }, 90 | identify: function (userId, userProperties) { 91 | // Send the identify call to Intercom's JS library 92 | // console.log('Identifying: ', userId, userProperties); 93 | if (window.Intercom && userId) 94 | Intercom('update', { user_id: userId }); 95 | 96 | // Set people properties on our identified user 97 | if (window.Intercom && userProperties) 98 | Intercom('update', userProperties); 99 | }, 100 | track: function (eventName, eventProperties) { 101 | // Send the tracked event to Intercom's JS library 102 | // console.log('tracking: ', eventName, eventProperties); 103 | if (window.Intercom && eventName) 104 | Intercom('trackEvent', eventName, eventProperties); 105 | } 106 | }, 107 | 'posthog': { 108 | enabled: true, 109 | test: function () { 110 | return (window.posthog && posthog.__loaded) ? true : false; 111 | }, 112 | identify: function (userId, userProperties) { 113 | // Send the identify call to Posthog's JS library 114 | // console.log('Identifying: ', userId, userProperties); 115 | if (window.posthog && userId) 116 | posthog.identify(userId); 117 | 118 | // Set people properties on our identified user 119 | if (window.posthog && userProperties) 120 | posthog.people.set(userProperties); // Just this session! 121 | 122 | if (window.posthog && userProperties) 123 | posthog.register(userProperties); // These persist across sessions 124 | }, 125 | track: function (eventName, eventProperties) { 126 | // Send the tracked event to Posthog's JS library 127 | // console.log('tracking: ', eventName, eventProperties); 128 | if (window.posthog && eventName) 129 | posthog.capture(eventName, eventProperties); 130 | } 131 | }, 132 | 'crisp': { 133 | enabled: true, 134 | test: function () { 135 | return (window.$crisp && window.$crisp.push && window.$crisp.get && window.$crisp.get('session:identifier')) ? true : false; 136 | }, 137 | identify: function (userId, userProperties) { 138 | // Send the identify call to Crisp.chat's JS library 139 | var newArr = []; 140 | 141 | // morph user property hash into array 142 | for (var k in userProperties) { 143 | var v = userProperties[k]; 144 | 145 | if (k !== 'email' && k !== 'company') newArr.push([k, v]); 146 | } 147 | 148 | // console.log('Identifying (Crisp): ', newArr); 149 | 150 | // Set people properties on our identified user 151 | if (window.$crisp && userProperties) 152 | $crisp.push(["set", "session:data", newArr ] ) 153 | 154 | }, 155 | track: function (eventName, eventProperties) { 156 | // Send the tracked event to Crisp.chat's JS library 157 | console.log('tracking (Crisp): ', eventName, eventProperties); 158 | if (window.$crisp && eventName) 159 | $crisp.push(["set", "session:event", [ eventName, eventProperties ] ]) 160 | }, 161 | group: function (groupId, traits) { 162 | // Send the group call to Segment's Analytics.js library 163 | // console.log('group: ', groupId, traits); 164 | if (window.$crisp && groupId) window.$crisp.push(["set", "session:segments", [ groupId ] ]) 165 | } 166 | }, 167 | 'amplitude': { 168 | enabled: true, 169 | test: function () { 170 | return (window.amplitude && window.amplitude.options) ? true : false; 171 | }, 172 | identify: function (userId, userProperties) { 173 | // Send the identify call to Amplitude's JS library 174 | // console.log('Identifying: ', userId, userProperties); 175 | if (window.amplitude && userId) amplitude.getInstance().setUserId(userId); 176 | 177 | // Set people properties on our identified user 178 | if (window.amplitude && userProperties) amplitude.getInstance().setUserProperties(userProperties); 179 | }, 180 | track: function (eventName, eventProperties) { 181 | // Send the tracked event to Amplitude's JS library 182 | // console.log('tracking: ', eventName, eventProperties); 183 | if (window.amplitude && eventName) amplitude.getInstance().logEvent(eventName, eventProperties); 184 | }, 185 | page: function (category, name, properties) { 186 | if (window.amplitude) { 187 | if (category || name) { 188 | if (category) 189 | amplitude.getInstance().logEvent('pageview_' + category, properties); 190 | 191 | if (name) 192 | amplitude.getInstance().logEvent('pageview_' + name, properties); 193 | } else { 194 | amplitude.getInstance().logEvent('pageview', properties); 195 | } 196 | } 197 | } 198 | }, 199 | 'google-analytics': { 200 | enabled: true, 201 | test: function () { 202 | return window.ga; 203 | }, 204 | identify: function (userId, userProperties) { 205 | if (!userProperties) userProperties = {}; 206 | 207 | if (window.ga) { 208 | userProperties.userId = userId; 209 | ga('set', userProperties); 210 | } 211 | }, 212 | track: function (eventName, eventProperties) { 213 | if (!eventProperties) eventProperties = {}; 214 | 215 | if (window.ga) { 216 | if (!eventProperties.hasOwnProperty("eventCategory")) { 217 | eventProperties.eventCategory = "All" 218 | } 219 | eventProperties.eventAction = eventName; 220 | eventProperties.hitType = 'event'; 221 | ga('send', eventProperties); 222 | } 223 | }, 224 | page: function (category, name, properties) { 225 | 226 | if (!properties) properties = {}; 227 | 228 | if (window.ga) { 229 | if (category) properties.category = category; 230 | properties.hitType = 'pageview'; 231 | properties.page = name || properties.path; 232 | properties.location = properties.url; 233 | ga('send', properties); 234 | } 235 | }, 236 | page: function (category, name, properties) { 237 | if (window.ga) { 238 | var tracker; 239 | 240 | try { 241 | tracker = ga.getAll()[0]; 242 | } catch(e){} 243 | 244 | // See: https://developers.google.com/analytics/devguides/collection/analyticsjs/pages 245 | 246 | if (category) properties.category = category; 247 | properties.hitType = 'pageview'; 248 | properties.page = name || properties.path; 249 | properties.location = properties.url; 250 | 251 | if (tracker) { 252 | tracker.set(properties); 253 | tracker.send(properties); 254 | } else { 255 | ga('set', properties); 256 | ga('send', properties); 257 | } 258 | 259 | // Default (Simpler) approach used by GA default code snippet: 260 | // ga('send', 'pageview'); 261 | } 262 | } 263 | }, 264 | 'keen': { 265 | enabled: true, 266 | // Todo: This test sucks (keen is not opinionated as to what global you place it in (you don't even need to expose it as a global), 267 | // but defaults to client, which is too common to use as a test.) 268 | test: function () { 269 | return ((window.Keen && window.Keen.loaded) || ((window.KeenAsync && window.KeenAsync.loaded))) && window.client; 270 | }, 271 | identify: function (userId, userProperties) { 272 | try { 273 | if (window.client && userId) client.extendEvents({ 274 | 'user_id': userId 275 | }); 276 | 277 | if (window.client && userProperties) client.extendEvents(userProperties); 278 | } catch(e){ 279 | console.log(e); 280 | } 281 | }, 282 | track: function (eventName, eventProperties) { 283 | if (window.client && eventName) client.recordEvent(eventName, eventProperties); 284 | } 285 | }, 286 | 'helpscout': { // Helpscout.net 287 | enabled: true, 288 | test: function () { 289 | return window.HS && window.HSCW; 290 | }, 291 | identify: function (userId, userProperties) { 292 | 293 | if (userId) { 294 | if (!userProperties) userProperties = {}; 295 | userProperties.userId = userId; 296 | } 297 | 298 | if (window.HS && userProperties) 299 | HS.beacon.identify(userProperties); 300 | } 301 | }, 302 | 'fullstory': { 303 | enabled: true, 304 | test: function () { 305 | return window.FS && window._fs_loaded; 306 | }, 307 | identify: function (userId, userProperties) { 308 | if (window.FS && userId) FS.identify(userId, userProperties); 309 | } 310 | }, 311 | 'olark': { 312 | enabled: true, 313 | test: function () { 314 | return window.olark; 315 | }, 316 | identify: function (userId, userProperties) { 317 | if (window.olark && userId) olark.identify(userId); 318 | 319 | if (userProperties.email) { 320 | olark('api.visitor.updateEmailAddress', { 321 | emailAddress: userProperties.email 322 | }); 323 | } 324 | } 325 | }, 326 | 'calq': { 327 | enabled: true, 328 | test: function () { 329 | return window.calq; 330 | }, 331 | track: function (eventName, eventProperties) { 332 | if (window.calq && eventName) 333 | calq.action.track(eventName, eventProperties); 334 | }, 335 | identify: function (userId, userProperties) { 336 | if (window.calq) calq.user.identify(userId); 337 | 338 | if (window.calq && userProperties) { 339 | if (userProperties.email) userProperties.$email = userProperties.email; 340 | if (userProperties.phone) userProperties.$phone = userProperties.phone; 341 | 342 | delete userProperties.email; 343 | delete userProperties.phone; 344 | 345 | if (userProperties.city) userProperties.$city = userProperties.city; 346 | if (userProperties.country) userProperties.$country = userProperties.country; 347 | if (userProperties.region) userProperties.$region = userProperties.region; 348 | if (userProperties.full_name) userProperties.$full_name = userProperties.full_name; 349 | 350 | delete userProperties.city; 351 | delete userProperties.country; 352 | delete userProperties.region; 353 | delete userProperties.full_name; 354 | 355 | if (userProperties.utm_campaign) userProperties.$utm_campaign = userProperties.utm_campaign; 356 | if (userProperties.utm_source) userProperties.$utm_source = userProperties.utm_source; 357 | if (userProperties.utm_medium) userProperties.$utm_medium = userProperties.utm_medium; 358 | if (userProperties.utm_content) userProperties.$utm_content = userProperties.utm_content; 359 | if (userProperties.utm_term) userProperties.$utm_term = userProperties.utm_term; 360 | 361 | delete userProperties.utm_campaign; 362 | delete userProperties.utm_source; 363 | delete userProperties.utm_medium; 364 | delete userProperties.utm_content; 365 | delete userProperties.utm_term; 366 | 367 | calq.user.profile(userProperties); 368 | } 369 | }, 370 | page: function (category, name, properties) { 371 | if (window.calq) calq.action.trackPageView(); 372 | } 373 | }, 374 | 'chameleon': { 375 | enabled: true, 376 | test: function () { 377 | return !!window.chmln; 378 | }, 379 | track: function (eventName, eventProperties) { 380 | if (window.chmln && eventName) chmln.track(eventName, eventProperties); 381 | }, 382 | identify: function (userId, userProperties) { 383 | if (window.chmln && userId) { 384 | var obj = { uid: userId }; 385 | 386 | if (userProperties.email) obj.email = userProperties.email; 387 | if (userProperties.created) obj.created = userProperties.created; 388 | if (userProperties.createdAt) obj.created = userProperties.createdAt; 389 | 390 | if (userProperties.city) obj.city = userProperties.city; 391 | if (userProperties.state) obj.state = userProperties.state; 392 | if (userProperties.country) obj.country = userProperties.country; 393 | 394 | // platform, device, screen, browser, IP address, locale, timezone, language 395 | 396 | chmln.identify(obj); 397 | } 398 | }, 399 | alias: function (userId, previousId) { 400 | if (window.chmln && userId && previousId) chmln.alias({ from: previousId, to: userId }); 401 | }, 402 | group: function (groupId, traits) { 403 | if (!groupId) return; 404 | var options = {}; 405 | 406 | if (traits) { 407 | for (var key in traits) { 408 | options['group:' + key] = traits[key]; 409 | } 410 | } 411 | 412 | options['group:id'] = groupId; 413 | window.chmln.set(options); 414 | } 415 | }, 416 | 'sentry': { 417 | enabled: true, 418 | test: function () { 419 | return window.Raven; 420 | }, 421 | identify: function (userId, userProperties) { 422 | if (!userProperties) userProperties = {}; 423 | if (userId) userProperties.userId = userId; 424 | 425 | if (window.Raven) 426 | Raven.setUserContext(userProperties); 427 | } 428 | }, 429 | 'luckyorange': { 430 | enabled: true, 431 | test: function () { 432 | return !!window.__lo_cs_added; 433 | }, 434 | identify: function (userId, userProperties) { 435 | if (!userProperties) userProperties = {}; 436 | if (userId) userProperties.userId = userId; 437 | 438 | if (window.__lo_cs_added) 439 | window.__wtw_custom_user_data = userProperties; 440 | } 441 | }, 442 | 'castle': { 443 | enabled: true, 444 | test: function () { 445 | return typeof window._castle === 'function'; 446 | }, 447 | identify: function (userId, userProperties) { 448 | delete userProperties.id; 449 | if (window._castle) _castle('identify', userId, userProperties); 450 | }, 451 | track: function (eventName, eventProperties) { 452 | if (window._castle) _castle('track', eventName, eventProperties); 453 | }, 454 | page: function (category, name, properties) { 455 | if (window._castle) _castle('page', properties.url, properties.title); 456 | } 457 | }, 458 | 'rollbar': { 459 | enabled: true, 460 | test: function () { 461 | return window.Rollbar; 462 | }, 463 | identify: function (userId, userProperties) { 464 | if (!userProperties) userProperties = {}; 465 | userProperties.id = userId; 466 | 467 | if (window.Rollbar && userId) 468 | Rollbar.configure({ payload: { person: userProperties } }); 469 | } 470 | }, 471 | 'talkus': { 472 | enabled: true, 473 | test: function () { 474 | return !!window.talkus; 475 | }, 476 | identify: function (userId, userProperties) { 477 | // Hacky way of getting the AppId (for now Todo update) 478 | var lsObj = JSON.parse(localStorage.getItem('talkusBubbleTS')); 479 | var appId = Object.keys(lsObj)[0]; 480 | 481 | if (!userProperties) userProperties = {}; 482 | userProperties.userId = userId; 483 | 484 | if (window.talkus && appId) talkus('init', appId, userProperties); 485 | } 486 | }, 487 | 'google-tag-manager': { 488 | enabled: true, 489 | test: function () { 490 | return !!(window.dataLayer && Array.prototype.push !== window.dataLayer.push); 491 | }, 492 | track: function (eventName, eventProperties) { 493 | if (!eventProperties) eventProperties = {}; 494 | eventProperties.event = eventName; 495 | 496 | if (window.dataLayer && eventProperties) dataLayer.push(eventProperties); 497 | }, 498 | page: function (category, name, properties) { 499 | if (!properties) properties = {}; 500 | properties.event = 'pageview_' + name; 501 | properties.category = category; 502 | 503 | if (window.dataLayer) 504 | dataLayer.push(properties); 505 | } 506 | }, 507 | 'elevio': { 508 | enabled: true, 509 | test: function () { 510 | return !!window._elev; 511 | }, 512 | identify: function (userId, userProperties) { 513 | if (!userProperties || !window._elev) return; 514 | 515 | var user = {}; 516 | user.via = 'event-layer'; 517 | 518 | if (userProperties.email) user.email = userProperties.email; 519 | if (userProperties.name) user.name = userProperties.name; 520 | if (userProperties.plan) user.plan = [userProperties.plan]; 521 | if (userProperties.plan) user.groups = [userProperties.plan]; 522 | 523 | // Delete those 524 | delete userProperties.firstName; 525 | delete userProperties.lastName; 526 | delete userProperties.email; 527 | delete userProperties.name; 528 | delete userProperties.plan; 529 | delete userProperties.id; 530 | 531 | if (Object.keys(userProperties).length > 0) user.traits = userProperties; 532 | window._elev.user = user; 533 | } 534 | }, 535 | 'drift': { 536 | enabled: true, 537 | test: function () { 538 | return window.drift !== undefined; 539 | }, 540 | track: function (eventName, eventProperties) { 541 | // Todo: Nice to have, Investigate convertDates for eventProperties. 542 | // This seems to iterate through dates and apply `Math.floor(date.getTime() / 1000)` 543 | 544 | if (window.drift && eventName) 545 | window.drift.track(eventName, eventProperties); 546 | }, 547 | identify: function (userId, userProperties) { 548 | if (!window.drift || !userId) return; 549 | 550 | delete userProperties.id; 551 | window.drift.identify(userId, userProperties); 552 | }, 553 | page: function (category, name, properties) { 554 | if (window.drift && name) 555 | window.drift.page(name); 556 | } 557 | }, 558 | 'drip': { 559 | enabled: true, 560 | test: function () { 561 | return window._dc && typeof(window._dc) === 'object'; 562 | }, 563 | track: function (eventName, eventProperties) { 564 | if (!window._dcq || !eventName) return; 565 | 566 | if (eventProperties) { 567 | // Convert all keys with spaces to underscores 568 | for (var key in eventProperties) { 569 | if (key.indexOf(' ') === -1) return; // Skip keys w/o spaces 570 | 571 | var formattedKey = key.replace(' ', '_'); 572 | eventProperties[formattedKey] = eventProperties[key]; 573 | delete eventProperties[key]; 574 | } 575 | 576 | if (eventProperties.revenue) { 577 | var cents = Math.round(eventProperties.revenue * 100); 578 | eventProperties.cents = cents; 579 | delete eventProperties.revenue; 580 | } 581 | } 582 | 583 | window._dcq.push('track', eventName, eventProperties); 584 | }, 585 | identify: function (userId, userProperties) { 586 | if (window._dcq && userProperties) 587 | window._dcq.push('identify', userProperties); 588 | } 589 | }, 590 | 'bugsnag': { 591 | enabled: true, 592 | test: function () { 593 | return window.Bugsnag && typeof(window.Bugsnag) === 'object'; 594 | }, 595 | identify: function (userId, userProperties) { 596 | if (!window.Bugsnag) return; 597 | window.Bugsnag.user = window.Bugsnag.user || {}; 598 | window.Bugsnag.user = Object.assign(window.Bugsnag.user, userProperties); 599 | } 600 | }, 601 | 'improvely': { 602 | enabled: true, 603 | test: function () { 604 | return !!(window.improvely && window.improvely.identify); 605 | }, 606 | track: function (eventName, eventProperties) { 607 | var props = eventProperties; 608 | 609 | // Todo: What does track.properties({ revenue: 'amount' }) do? 610 | // Does it do this? 611 | // props = Object.assign(props, { revenue: 'amount' }); 612 | // or this? 613 | // props.revenue = props.amount; 614 | props.revenue = props.amount; 615 | delete props.amount; 616 | 617 | props.type = eventName; 618 | window.improvely.goal(props); 619 | }, 620 | identify: function (userId, userProperties) { 621 | if (userId && window.improvely) 622 | window.improvely.label(userId); 623 | } 624 | }, 625 | 'inspectlet': { 626 | enabled: true, 627 | test: function () { 628 | return !!(window.__insp_ && window.__insp); 629 | }, 630 | track: function (eventName, eventProperties) { 631 | if (window.__insp && eventName) 632 | __insp.push('tagSession', eventName, eventProperties); 633 | }, 634 | identify: function (userId, userProperties) { 635 | if (!window.__insp) return; 636 | 637 | //var traits = identify.traits({ id: 'userid' }); 638 | // Todo: Am I doing it right? 639 | var traits = Object.assign({}, userProperties); 640 | traits.id = userId || traits.uid; 641 | delete traits.uid; 642 | 643 | if (userProperties && userProperties.email) 644 | __insp.push('identify', userProperties.email); 645 | 646 | if (userId || userProperties) 647 | __insp.push('tagSession', traits); 648 | }, 649 | page: function (category, name, properties) { 650 | if (window.__insp) 651 | __insp.push('virtualPage'); 652 | } 653 | }, 654 | 'qualaroo': { 655 | enabled: true, 656 | test: function () { 657 | return !!(window._kiq && window._kiq.push !== Array.prototype.push); 658 | }, 659 | track: function (eventName, eventProperties, options) { 660 | var traits = {}; 661 | traits['Triggered: ' + eventName] = true; 662 | // this.identify(new Identify({ traits: traits })); // Identify = require('segmentio-facade').Identify; 663 | }, 664 | identify: function (userId, userProperties) { 665 | if (!window._kiq) return; 666 | 667 | if (userProperties && userProperties.email) userId = userProperties.email; 668 | if (userProperties) _kiq.push('set', userProperties); 669 | if (userId) _kiq.push('identify', userId); 670 | } 671 | }, 672 | 'facebook-tracking-pixel': { 673 | enabled: true, 674 | test: function () { 675 | return !!(window.fbq && typeof window.fbq === 'function'); 676 | }, 677 | track: function (eventName, eventProperties) { 678 | if (!window.fbq) return; 679 | 680 | fbq('trackCustom', eventName, eventProperties); 681 | }, 682 | page: function (category, name, properties) { 683 | if (!window.fbq) return; 684 | 685 | fbq('track', "PageView"); 686 | }, 687 | facebookTrackEvent: function (eventName, eventProperties) { 688 | if (!window.fbq) return; 689 | 690 | fbq('track', eventName, eventProperties); 691 | } 692 | }, 693 | 'customerio': { 694 | enabled: true, 695 | test: function () { 696 | return !!(window._cio && window._cio.push !== Array.prototype.push); 697 | }, 698 | track: function (eventName, eventProperties) { 699 | if (!window._cio) return; 700 | 701 | window._cio.track(eventName, eventProperties); 702 | }, 703 | identify: function (userId, userProperties) { 704 | if (!window._cio) return; 705 | if (!userId) return console.warn('user id required by customer.io for identify function.'); 706 | 707 | // Expects userProperties { id: string unique, email: string, created_at: unix-timestamp } 708 | 709 | // Transform createdAt -> created_at 710 | if (userProperties && userProperties.createdAt && !userProperties.created_at) 711 | userProperties.created_at = userProperties.createdAt; 712 | 713 | // Add userId if no id is present 714 | if (userProperties && !userProperties.id) 715 | userProperties.id = userId; 716 | 717 | window._cio.identify(userProperties); 718 | }, 719 | alias: function (userId, previousId) { 720 | // Todo 721 | }, 722 | group: function (groupId, traits) { 723 | // Todo 724 | }, 725 | page: function (category, name, properties) { 726 | if (!window._cio) return; 727 | 728 | if (!window.__currentUserId) return console.warn('You must call the Identify function for Customer.io before the page function, passing a valid userId.'); 729 | if (!name) return console.warn('Customer.io requires a valid name property when calling the page event. Since Analytics.js expects a category field as well, this must be sent (even if it is empty). See documentation for more details.'); 730 | 731 | if (!properties) properties = {}; 732 | 733 | properties.id = window.__currentUserId; 734 | properties.type = 'page'; 735 | properties.name = name; 736 | properties.category = category; 737 | 738 | window._cio.page(location.href, properties); 739 | } 740 | }, 741 | 'debug': { // This sends some debugging messages if enabled 742 | enabled: true, 743 | test: function () { return !!window.__debug }, 744 | track: function (eventName, eventProperties) { 745 | console.log('Track:', eventName, eventProperties); 746 | }, 747 | identify: function (userId, userProperties) { 748 | console.log('Identify:', userId, userProperties); 749 | }, 750 | page: function (category, name, properties) { 751 | console.log('Page:', category, name, properties); 752 | }, 753 | alias: function (userId, previousId) { 754 | console.log('Alias:', userId, previousId); 755 | }, 756 | group: function (groupId, traits) { 757 | console.log('Group:', groupId, traits); 758 | } 759 | }, 760 | 'blank-adapter-template': { // Do not modify this template 761 | enabled: false, 762 | test: function () {}, 763 | track: function (eventName, eventProperties) {}, 764 | identify: function (userId, userProperties) {}, 765 | page: function (category, name, properties) {}, 766 | alias: function (userId, previousId) {}, 767 | group: function (groupId, traits) {} 768 | } 769 | }; 770 | 771 | // Recursively convert an `obj`'s dates to new values, using an input function, convert(). 772 | function convertDates (oObj, convert) { 773 | if (typeof(oObj) !== 'object') return oObj; 774 | 775 | var obj = Object.assign({}, oObj); 776 | 777 | for (var key in obj) { 778 | var val = obj[key]; 779 | if (typeof(val) === 'date') obj[key] = convert(val); 780 | if (typeof(val) === 'object') obj[key] = convertDates(val, convert); 781 | } 782 | 783 | return obj; 784 | } 785 | 786 | function runTest (f) { 787 | try { 788 | return f(); 789 | } catch (e) { 790 | return false; 791 | } 792 | return false; 793 | } 794 | 795 | function track (eventName, eventProperties, options, callback) { 796 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 797 | 798 | onReady(); 799 | 800 | for (var adapterName in thirdPartyAdapters) { 801 | var adapter = thirdPartyAdapters[adapterName]; 802 | 803 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 804 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 805 | // If everything checks out for the data we've received, 806 | // pass the data to the adapter so it can be tracked 807 | 808 | if (window.__debug) console.log('Track method executing on', adapterName); 809 | 810 | // If TRANSLATE_EVENT_NAMES exists, use it to translate event names 811 | if (window.TRANSLATE_EVENT_NAMES && typeof window.TRANSLATE_EVENT_NAMES === 'object') 812 | eventName = TRANSLATE_EVENT_NAMES(eventName); 813 | 814 | if (adapter.track && typeof(adapter.track) === 'function') 815 | adapter.track(eventName, eventProperties); 816 | } 817 | } 818 | 819 | if (callback && typeof(callback) === 'function') callback(); 820 | } 821 | 822 | function identify (userId, userProperties, options, callback) { 823 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 824 | 825 | onReady(); 826 | 827 | // Stash this for later 828 | window.__currentUserId = userId; 829 | 830 | for (var adapterName in thirdPartyAdapters) { 831 | var adapter = thirdPartyAdapters[adapterName]; 832 | 833 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 834 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 835 | // If everything checks out for the data we've received, 836 | // pass the data to the adapter so it can be tracked 837 | 838 | if (window.__debug) console.log('Identify method executing on', adapterName); 839 | 840 | if (adapter.identify && typeof(adapter.identify) === 'function') 841 | adapter.identify(userId, userProperties); 842 | } 843 | } 844 | 845 | if (callback && typeof(callback) === 'function') callback(); 846 | } 847 | 848 | function page (category, name, properties, options, callback) { 849 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 850 | 851 | onReady(); 852 | 853 | // Handle not passing the category (shift right) 854 | if (category && (!name || typeof(name) !== 'string')) { 855 | callback = options; 856 | options = properties; 857 | properties = name; 858 | name = category; 859 | category = null; 860 | } 861 | 862 | // url (canonical?), title, referrer, path 863 | var url = document.querySelector("link[rel='canonical']") ? document.querySelector("link[rel='canonical']").href : document.location.href; 864 | var title = document.title; 865 | var referrer = document.referrer; 866 | var path = location.pathname; 867 | 868 | var props = { 869 | url: url, 870 | title: title, 871 | referrer: referrer, 872 | path: path 873 | }; 874 | 875 | var properties = Object.assign(props, properties); 876 | 877 | for (var adapterName in thirdPartyAdapters) { 878 | var adapter = thirdPartyAdapters[adapterName]; 879 | 880 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 881 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 882 | 883 | if (window.__debug) console.log('Page method executing on', adapterName); 884 | 885 | // If everything checks out for the data we've received, 886 | // pass the data to the adapter so it can be tracked 887 | if (adapter.page && typeof(adapter.page) === 'function') 888 | adapter.page(category, name, properties); 889 | } 890 | } 891 | 892 | if (callback && typeof(callback) === 'function') callback(); 893 | } 894 | 895 | function group (groupId, traits, options, callback) { 896 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 897 | 898 | onReady(); 899 | 900 | for (var adapterName in thirdPartyAdapters) { 901 | var adapter = thirdPartyAdapters[adapterName]; 902 | 903 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 904 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 905 | 906 | if (window.__debug) console.log('Group method executing on', adapterName); 907 | 908 | // If everything checks out for the data we've received, 909 | // pass the data to the adapter so we can perform a grouping 910 | if (adapter.group && typeof(adapter.group) === 'function') 911 | adapter.group(groupId, traits); 912 | } 913 | } 914 | 915 | if (callback && typeof(callback) === 'function') callback(); 916 | } 917 | 918 | function alias (userId, previousId, options, callback) { 919 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 920 | 921 | onReady(); 922 | 923 | for (var adapterName in thirdPartyAdapters) { 924 | var adapter = thirdPartyAdapters[adapterName]; 925 | 926 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 927 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 928 | 929 | if (window.__debug) console.log('Alias method executing on', adapterName); 930 | 931 | // If everything checks out for the data we've received, 932 | // pass the data to the adapter so we can alias this user 933 | if (adapter.alias && typeof(adapter.alias) === 'function') 934 | adapter.alias(userId, previousId); 935 | } 936 | } 937 | 938 | if (callback && typeof(callback) === 'function') callback(); 939 | } 940 | 941 | /** 942 | * Facebook tracking pixel support 943 | * 944 | * Based on: https://developers.facebook.com/docs/facebook-pixel/api-reference 945 | * 946 | * Facebook tracking pixel specific event names: 947 | * ViewContent 948 | * Search 949 | * AddToCart 950 | * AddToWishlist 951 | * InitiateCheckout 952 | * AddPaymentInfo 953 | * Purchase 954 | * Lead 955 | * CompleteRegistration 956 | */ 957 | function fbTrack (eventName, eventProperties, options, callback) { 958 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 959 | 960 | onReady(); 961 | 962 | // Iterate through third-party adapters, sending track events. 963 | for (var adapterName in thirdPartyAdapters) { 964 | var adapter = thirdPartyAdapters[adapterName]; 965 | 966 | if (adapterName === 'facebook-tracking-pixel') continue; // Skip FB Tracking pixel 967 | 968 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 969 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 970 | 971 | if (window.__debug) console.log('fbTrack method executing on', adapterName); 972 | 973 | // If everything checks out for the data we've received, 974 | // pass the data to the adapter so it can be tracked 975 | if (adapter.facebookTrackEvent && typeof(adapter.facebookTrackEvent) === 'function') { 976 | adapter.facebookTrackEvent(eventName, eventProperties); 977 | } else if (adapter.track && typeof(adapter.track) === 'function') { 978 | // If TRANSLATE_EVENT_NAMES exists, use it to translate event names 979 | if (window.TRANSLATE_EVENT_NAMES && typeof window.TRANSLATE_EVENT_NAMES === 'object') 980 | eventName = TRANSLATE_EVENT_NAMES(eventName); 981 | 982 | adapter.track(eventName, eventProperties); 983 | } 984 | 985 | } 986 | } 987 | 988 | if (callback && typeof(callback) === 'function') callback(); 989 | } 990 | 991 | /** 992 | * Event Layer defaults 993 | */ 994 | function ready (callback) { 995 | if (window.__debug) console.log('Event Layer Ready'); 996 | 997 | if (callback && typeof(callback) === 'function') 998 | EventLayer.readyFunction = callback; 999 | } 1000 | 1001 | function onReady () { 1002 | if (!EventLayer.readyFunction) return; 1003 | 1004 | if (EventLayer.readyFunction && typeof(EventLayer.readyFunction) === 'function') EventLayer.readyFunction(); 1005 | 1006 | EventLayer.readyFunction = null; 1007 | } 1008 | 1009 | // Execute directly before the first track/identify/page/group/alias call, or after a default timeout (5s) 1010 | setTimeout(onReady, 5000); 1011 | 1012 | 1013 | /** 1014 | * Add an adapter on the fly for testing or modifying EventLayer on your own 1015 | */ 1016 | function addAdapter (namespace, adapter) { 1017 | if (typeof adapter !== 'object') return; 1018 | 1019 | if (thirdPartyAdapters[namespace]) console.warn('Adapter for namespace ' + namespace + ' already exists, and is being overwritten.'); 1020 | else if (window.__debug) console.log('Adapter for ' + namespace + ' added.'); 1021 | 1022 | thirdPartyAdapters[namespace] = adapter; 1023 | } 1024 | 1025 | // Todo: 1026 | // QueryString API? 1027 | // Selecting Integrations should match analytics.js syntax 1028 | 1029 | // Create / export globals 1030 | window.EventLayer = {}; 1031 | EventLayer.thirdPartyAdapters = thirdPartyAdapters; 1032 | EventLayer.addAdapter = addAdapter; 1033 | EventLayer.readyFunction = null; 1034 | EventLayer.Integrations = null; // This needs to be null so that it's not confused with Segment.com's library. 1035 | EventLayer.identify = identify; 1036 | EventLayer.onReady = onReady; 1037 | EventLayer.fbTrack = fbTrack; // Facebook-tracking pixel 1038 | EventLayer.track = track; 1039 | EventLayer.group = group; 1040 | EventLayer.alias = alias; 1041 | EventLayer.ready = ready; 1042 | EventLayer.page = page; 1043 | 1044 | window.__currentUserId = null; 1045 | 1046 | return function () { 1047 | return EventLayer; 1048 | }; 1049 | 1050 | })(); 1051 | -------------------------------------------------------------------------------- /event-layer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidGodzilla/event-layer/36324f7ccc38b4398fd7fbce9da9d272a764793c/event-layer-logo.png -------------------------------------------------------------------------------- /event-layer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Object.assign() polyfill 3 | */ 4 | // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 5 | if (typeof Object.assign !== 'function') { 6 | // Must be writable: true, enumerable: false, configurable: true 7 | Object.defineProperty(Object, "assign", { 8 | value: function assign(target, varArgs) { // .length of function is 2 9 | 'use strict'; 10 | if (target == null) { // TypeError if undefined or null 11 | throw new TypeError('Cannot convert undefined or null to object'); 12 | } 13 | 14 | var to = Object(target); 15 | 16 | for (var index = 1; index < arguments.length; index++) { 17 | var nextSource = arguments[index]; 18 | 19 | if (nextSource != null) { // Skip over if undefined or null 20 | for (var nextKey in nextSource) { 21 | // Avoid bugs when hasOwnProperty is shadowed 22 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 23 | to[nextKey] = nextSource[nextKey]; 24 | } 25 | } 26 | } 27 | } 28 | return to; 29 | }, 30 | writable: true, 31 | configurable: true 32 | }); 33 | } 34 | 35 | var EventLayer = (function EventLayer () { 36 | if (typeof window === 'undefined') { 37 | window = { __node: !!global }; 38 | } 39 | 40 | /** 41 | * Third-party adapters for EventLayer.track(), EventLayer.identify(), etc. 42 | */ 43 | var thirdPartyAdapters = { 44 | 'segment': { 45 | enabled: true, 46 | test: function () { 47 | // This test not only tests for Segment's variety of Analytics.js, 48 | // it distinguishes it from this library, so you can simply alias this to window.analytics without worry. 49 | return window.analytics && typeof(window.analytics) === 'object' && window.analytics.Integrations && typeof(window.analytics.Integrations) === 'object'; 50 | }, 51 | identify: function (userId, userProperties) { 52 | // Send the identify call to Segment's Analytics.js library 53 | // console.log('Identifying: ', userId, userProperties); 54 | if (window.analytics && userId) analytics.identify(userId, userProperties); 55 | }, 56 | track: function (eventName, eventProperties) { 57 | // Send the tracked event to Segment's Analytics.js library 58 | // console.log('tracking: ', eventName, eventProperties); 59 | if (window.analytics && eventName) analytics.track(eventName, eventProperties); 60 | }, 61 | page: function (category, name, properties) { 62 | // Send the page call to Segment's Analytics.js library 63 | // console.log('page: ', category, name, properties); 64 | if (window.analytics && name) analytics.page(category, name, properties); 65 | }, 66 | alias: function (userId, previousId) { 67 | // Send the alias call to Segment's Analytics.js library 68 | // console.log('alias: ', userId, previousId); 69 | if (window.analytics && userId && previousId) analytics.alias(userId, previousId); 70 | }, 71 | group: function (groupId, traits) { 72 | // Send the group call to Segment's Analytics.js library 73 | // console.log('group: ', groupId, traits); 74 | if (window.analytics && groupId) analytics.group(groupId, traits); 75 | } 76 | }, 77 | 'mixpanel': { 78 | enabled: true, 79 | test: function () { 80 | return window.mixpanel && window.mixpanel.__loaded; 81 | }, 82 | identify: function (userId, userProperties, options) { 83 | // Send the identify call to Mixpanel's JS library 84 | // console.log('Identifying: ', userId, userProperties); 85 | if (window.mixpanel && userId) mixpanel.identify(userId); 86 | 87 | if (window.mixpanel && userProperties) { 88 | if (options && options.setOnce) { 89 | // Set people properties on our identified user, but only if they have not yet been set. 90 | mixpanel.people.set_once(userProperties); 91 | } else { 92 | // Set people properties on our identified user 93 | mixpanel.people.set(userProperties); 94 | } 95 | } 96 | }, 97 | track: function (eventName, eventProperties) { 98 | // Send the tracked event to Mixpanel's JS library 99 | // console.log('tracking: ', eventName, eventProperties); 100 | if (window.mixpanel && eventName) mixpanel.track(eventName, eventProperties); 101 | } 102 | }, 103 | 'heap': { 104 | enabled: true, 105 | test: function () { 106 | return (window.heap && window.heap.track) ? true : false; 107 | }, 108 | identify: function (userId, userProperties) { 109 | // Send the identify call to Heap's JS library 110 | // console.log('Identifying: ', userId, userProperties); 111 | if (window.heap && userId) heap.identify(userId); 112 | 113 | // Set people properties on our identified user 114 | if (window.heap && userProperties) heap.addUserProperties(userProperties); 115 | }, 116 | track: function (eventName, eventProperties) { 117 | // Send the tracked event to Heap's JS library 118 | // console.log('tracking: ', eventName, eventProperties); 119 | if (window.heap && eventName) heap.track(eventName, eventProperties); 120 | } 121 | }, 122 | 'intercom': { 123 | enabled: true, 124 | test: function () { 125 | return (window.Intercom && window.Intercom('getVisitorId') && window.Intercom.booted) ? true : false; 126 | }, 127 | identify: function (userId, userProperties) { 128 | // Send the identify call to Intercom's JS library 129 | // console.log('Identifying: ', userId, userProperties); 130 | if (window.Intercom && userId) 131 | Intercom('update', { user_id: userId }); 132 | 133 | // Set people properties on our identified user 134 | if (window.Intercom && userProperties) 135 | Intercom('update', userProperties); 136 | }, 137 | track: function (eventName, eventProperties) { 138 | // Send the tracked event to Intercom's JS library 139 | // console.log('tracking: ', eventName, eventProperties); 140 | if (window.Intercom && eventName) 141 | Intercom('trackEvent', eventName, eventProperties); 142 | } 143 | }, 144 | 'posthog': { 145 | enabled: true, 146 | test: function () { 147 | return (window.posthog && window.posthog.__loaded) ? true : false; 148 | }, 149 | identify: function (userId, userProperties) { 150 | // Send the identify call to Posthog's JS library 151 | // console.log('Identifying: ', userId, userProperties); 152 | if (window.posthog && userId) 153 | window.posthog.identify(userId, userProperties); 154 | 155 | // Deprecated in Posthog 156 | // // Set people properties on our identified user 157 | // if (window.posthog && userProperties) 158 | // window.posthog.people.set(userProperties); // Just this session! 159 | 160 | // if (window.posthog && userProperties) 161 | // window.posthog.register(userProperties); // These persist across sessions 162 | }, 163 | track: function (eventName, eventProperties) { 164 | // Send the tracked event to Posthog's JS library 165 | // console.log('tracking: ', eventName, eventProperties); 166 | if (window.posthog && eventName) 167 | window.posthog.capture(eventName, eventProperties); 168 | }, 169 | page: function (category, name, properties) { 170 | if (window.posthog) window.posthog.capture('$pageview'); 171 | }, 172 | group: function (groupId, traits) { 173 | // Send the group call to Posthog's JS library 174 | // console.log('group: ', groupId, traits); 175 | if (window.posthog && groupId) window.posthog.group(groupId, traits); 176 | } 177 | }, 178 | 'beamanalytics': { 179 | enabled: true, 180 | test: function () { 181 | return (window.beam && typeof window.beam === 'function') ? true : false; 182 | }, 183 | track: function (eventName, eventProperties) { 184 | // Send the tracked event to Beam analytics JS library 185 | // console.log('tracking (Beam Analytics custom event): ', eventName, eventProperties); 186 | if (window.beam && eventName) { 187 | var sanitized = eventName.toLowerCase().replace(/[\n\t\s]/g, '_').replace(/[-._~:/?#[\]@!$&'()*+,;=]+/g, '_'); 188 | window.beam("/custom-events/" + sanitized); 189 | } 190 | }, 191 | }, 192 | 'june': { 193 | enabled: true, 194 | test: function () { 195 | return (window.juneify && typeof window.juneify === 'function' && window.analytics && window.analytics.initialized) ? true : false; 196 | }, 197 | identify: function (userId, userProperties) { 198 | // Send the identify call to June.so's JS library 199 | // console.log('Identifying: ', userId, userProperties); 200 | if (window.analytics && userId) 201 | window.analytics.identify(userId, userProperties); 202 | }, 203 | track: function (eventName, eventProperties) { 204 | // Send the tracked event to June.so's JS library 205 | // console.log('tracking: ', eventName, eventProperties); 206 | if (window.analytics && eventName) 207 | window.analytics.track(eventName, eventProperties); 208 | }, 209 | page: function (category, name, properties) { 210 | if (window.analytics) window.analytics.page(category, name, properties); 211 | }, 212 | group: function (groupId, traits) { 213 | // Send the group call to June.so's JS library 214 | // console.log('group: ', groupId, traits); 215 | if (window.analytics && groupId) window.analytics.group(groupId, traits); 216 | } 217 | }, 218 | 'crisp': { 219 | enabled: true, 220 | test: function () { 221 | return (window.$crisp && window.$crisp.push && window.$crisp.get && window.$crisp.get('session:identifier')) ? true : false; 222 | }, 223 | identify: function (userId, userProperties) { 224 | // Send the identify call to Crisp.chat's JS library 225 | var newArr = []; 226 | 227 | // morph user property hash into array 228 | for (var k in userProperties) { 229 | var v = userProperties[k]; 230 | 231 | if (k !== 'email' && k !== 'company' && k && v && typeof k === 'string') { 232 | newArr.push([k, v]); 233 | } 234 | } 235 | 236 | // console.log('Identifying (Crisp): ', newArr); 237 | 238 | // Set people properties on our identified user 239 | if (window.$crisp && userProperties && newArr.length === 2) 240 | $crisp.push(["set", "session:data", newArr ] ); 241 | }, 242 | track: function (eventName, eventProperties) { 243 | // Send the tracked event to Crisp.chat's JS library 244 | // console.log('tracking (Crisp): ', eventName, eventProperties); 245 | if (window.$crisp && eventName && eventProperties) 246 | $crisp.push(["set", "session:event", [ eventName, eventProperties ] ]) 247 | }, 248 | group: function (groupId, traits) { 249 | // Send the group call to Segment's Analytics.js library 250 | // console.log('group: ', groupId, traits); 251 | if (window.$crisp && groupId) window.$crisp.push(["set", "session:segments", [ groupId ] ]) 252 | } 253 | }, 254 | 'amplitude': { 255 | enabled: true, 256 | test: function () { 257 | return (window.amplitude && window.amplitude.options) ? true : false; 258 | }, 259 | identify: function (userId, userProperties) { 260 | // Send the identify call to Amplitude's JS library 261 | // console.log('Identifying: ', userId, userProperties); 262 | if (window.amplitude && userId) amplitude.getInstance().setUserId(userId); 263 | 264 | // Set people properties on our identified user 265 | if (window.amplitude && userProperties) amplitude.getInstance().setUserProperties(userProperties); 266 | }, 267 | track: function (eventName, eventProperties) { 268 | // Send the tracked event to Amplitude's JS library 269 | // console.log('tracking: ', eventName, eventProperties); 270 | if (window.amplitude && eventName) amplitude.getInstance().logEvent(eventName, eventProperties); 271 | }, 272 | page: function (category, name, properties) { 273 | if (window.amplitude) { 274 | if (category || name) { 275 | if (category) 276 | amplitude.getInstance().logEvent('pageview_' + category, properties); 277 | 278 | if (name) 279 | amplitude.getInstance().logEvent('pageview_' + name, properties); 280 | } else { 281 | amplitude.getInstance().logEvent('pageview', properties); 282 | } 283 | } 284 | } 285 | }, 286 | 'google-analytics': { 287 | enabled: true, 288 | test: function () { 289 | return window.ga; 290 | }, 291 | identify: function (userId, userProperties) { 292 | if (!userProperties) userProperties = {}; 293 | 294 | if (window.ga) { 295 | userProperties.userId = userId; 296 | ga('set', userProperties); 297 | } 298 | }, 299 | track: function (eventName, eventProperties) { 300 | if (!eventProperties) eventProperties = {}; 301 | 302 | if (window.ga) { 303 | if (!eventProperties.hasOwnProperty("eventCategory")) { 304 | eventProperties.eventCategory = "All" 305 | } 306 | eventProperties.eventAction = eventName; 307 | eventProperties.hitType = 'event'; 308 | ga('send', eventProperties); 309 | } 310 | }, 311 | page: function (category, name, properties) { 312 | 313 | if (!properties) properties = {}; 314 | 315 | if (window.ga) { 316 | if (category) properties.category = category; 317 | properties.hitType = 'pageview'; 318 | properties.page = name || properties.path; 319 | properties.location = properties.url; 320 | ga('send', properties); 321 | } 322 | }, 323 | page: function (category, name, properties) { 324 | if (window.ga) { 325 | var tracker; 326 | 327 | try { 328 | tracker = ga.getAll()[0]; 329 | } catch(e){} 330 | 331 | // See: https://developers.google.com/analytics/devguides/collection/analyticsjs/pages 332 | 333 | if (category) properties.category = category; 334 | properties.hitType = 'pageview'; 335 | properties.page = name || properties.path; 336 | properties.location = properties.url; 337 | 338 | if (tracker) { 339 | tracker.set(properties); 340 | tracker.send(properties); 341 | } else { 342 | ga('set', properties); 343 | ga('send', properties); 344 | } 345 | 346 | // Default (Simpler) approach used by GA default code snippet: 347 | // ga('send', 'pageview'); 348 | } 349 | } 350 | }, 351 | 'keen': { 352 | enabled: true, 353 | // Todo: This test sucks (keen is not opinionated as to what global you place it in (you don't even need to expose it as a global), 354 | // but defaults to client, which is too common to use as a test.) 355 | test: function () { 356 | return ((window.Keen && window.Keen.loaded) || ((window.KeenAsync && window.KeenAsync.loaded))) && window.client; 357 | }, 358 | identify: function (userId, userProperties) { 359 | try { 360 | if (window.client && userId) client.extendEvents({ 361 | 'user_id': userId 362 | }); 363 | 364 | if (window.client && userProperties) client.extendEvents(userProperties); 365 | } catch(e){ 366 | console.log(e); 367 | } 368 | }, 369 | track: function (eventName, eventProperties) { 370 | if (window.client && eventName) client.recordEvent(eventName, eventProperties); 371 | } 372 | }, 373 | 'helpscout': { // Helpscout.net 374 | enabled: true, 375 | test: function () { 376 | return window.HS && window.HSCW; 377 | }, 378 | identify: function (userId, userProperties) { 379 | 380 | if (userId) { 381 | if (!userProperties) userProperties = {}; 382 | userProperties.userId = userId; 383 | } 384 | 385 | if (window.HS && userProperties) 386 | HS.beacon.identify(userProperties); 387 | } 388 | }, 389 | 'fullstory': { 390 | enabled: true, 391 | test: function () { 392 | return window.FS && window._fs_loaded; 393 | }, 394 | identify: function (userId, userProperties) { 395 | if (window.FS && userId) FS.identify(userId, userProperties); 396 | } 397 | }, 398 | 'olark': { 399 | enabled: true, 400 | test: function () { 401 | return window.olark; 402 | }, 403 | identify: function (userId, userProperties) { 404 | if (window.olark && userId) olark.identify(userId); 405 | 406 | if (userProperties.email) { 407 | olark('api.visitor.updateEmailAddress', { 408 | emailAddress: userProperties.email 409 | }); 410 | } 411 | } 412 | }, 413 | 'calq': { 414 | enabled: true, 415 | test: function () { 416 | return window.calq; 417 | }, 418 | track: function (eventName, eventProperties) { 419 | if (window.calq && eventName) 420 | calq.action.track(eventName, eventProperties); 421 | }, 422 | identify: function (userId, userProperties) { 423 | if (window.calq) calq.user.identify(userId); 424 | 425 | if (window.calq && userProperties) { 426 | if (userProperties.email) userProperties.$email = userProperties.email; 427 | if (userProperties.phone) userProperties.$phone = userProperties.phone; 428 | 429 | delete userProperties.email; 430 | delete userProperties.phone; 431 | 432 | if (userProperties.city) userProperties.$city = userProperties.city; 433 | if (userProperties.country) userProperties.$country = userProperties.country; 434 | if (userProperties.region) userProperties.$region = userProperties.region; 435 | if (userProperties.full_name) userProperties.$full_name = userProperties.full_name; 436 | 437 | delete userProperties.city; 438 | delete userProperties.country; 439 | delete userProperties.region; 440 | delete userProperties.full_name; 441 | 442 | if (userProperties.utm_campaign) userProperties.$utm_campaign = userProperties.utm_campaign; 443 | if (userProperties.utm_source) userProperties.$utm_source = userProperties.utm_source; 444 | if (userProperties.utm_medium) userProperties.$utm_medium = userProperties.utm_medium; 445 | if (userProperties.utm_content) userProperties.$utm_content = userProperties.utm_content; 446 | if (userProperties.utm_term) userProperties.$utm_term = userProperties.utm_term; 447 | 448 | delete userProperties.utm_campaign; 449 | delete userProperties.utm_source; 450 | delete userProperties.utm_medium; 451 | delete userProperties.utm_content; 452 | delete userProperties.utm_term; 453 | 454 | calq.user.profile(userProperties); 455 | } 456 | }, 457 | page: function (category, name, properties) { 458 | if (window.calq) calq.action.trackPageView(); 459 | } 460 | }, 461 | 'chameleon': { 462 | enabled: true, 463 | test: function () { 464 | return !!window.chmln; 465 | }, 466 | track: function (eventName, eventProperties) { 467 | if (window.chmln && eventName) chmln.track(eventName, eventProperties); 468 | }, 469 | identify: function (userId, userProperties) { 470 | if (window.chmln && userId) { 471 | var obj = { uid: userId }; 472 | 473 | if (userProperties.email) obj.email = userProperties.email; 474 | if (userProperties.created) obj.created = userProperties.created; 475 | if (userProperties.createdAt) obj.created = userProperties.createdAt; 476 | 477 | if (userProperties.city) obj.city = userProperties.city; 478 | if (userProperties.state) obj.state = userProperties.state; 479 | if (userProperties.country) obj.country = userProperties.country; 480 | 481 | // platform, device, screen, browser, IP address, locale, timezone, language 482 | 483 | chmln.identify(obj); 484 | } 485 | }, 486 | alias: function (userId, previousId) { 487 | if (window.chmln && userId && previousId) chmln.alias({ from: previousId, to: userId }); 488 | }, 489 | group: function (groupId, traits) { 490 | if (!groupId) return; 491 | var options = {}; 492 | 493 | if (traits) { 494 | for (var key in traits) { 495 | options['group:' + key] = traits[key]; 496 | } 497 | } 498 | 499 | options['group:id'] = groupId; 500 | window.chmln.set(options); 501 | } 502 | }, 503 | 'sentry': { 504 | enabled: true, 505 | test: function () { 506 | return window.Raven; 507 | }, 508 | identify: function (userId, userProperties) { 509 | if (!userProperties) userProperties = {}; 510 | if (userId) userProperties.userId = userId; 511 | 512 | if (window.Raven) 513 | Raven.setUserContext(userProperties); 514 | } 515 | }, 516 | 'luckyorange': { 517 | enabled: true, 518 | test: function () { 519 | return !!window.__lo_cs_added; 520 | }, 521 | identify: function (userId, userProperties) { 522 | if (!userProperties) userProperties = {}; 523 | if (userId) userProperties.userId = userId; 524 | 525 | if (window.__lo_cs_added) 526 | window.__wtw_custom_user_data = userProperties; 527 | } 528 | }, 529 | 'castle': { 530 | enabled: true, 531 | test: function () { 532 | return typeof window._castle === 'function'; 533 | }, 534 | identify: function (userId, userProperties) { 535 | delete userProperties.id; 536 | if (window._castle) _castle('identify', userId, userProperties); 537 | }, 538 | track: function (eventName, eventProperties) { 539 | if (window._castle) _castle('track', eventName, eventProperties); 540 | }, 541 | page: function (category, name, properties) { 542 | if (window._castle) _castle('page', properties.url, properties.title); 543 | } 544 | }, 545 | 'rollbar': { 546 | enabled: true, 547 | test: function () { 548 | return window.Rollbar; 549 | }, 550 | identify: function (userId, userProperties) { 551 | if (!userProperties) userProperties = {}; 552 | userProperties.id = userId; 553 | 554 | if (window.Rollbar && userId) 555 | Rollbar.configure({ payload: { person: userProperties } }); 556 | } 557 | }, 558 | 'talkus': { 559 | enabled: true, 560 | test: function () { 561 | return !!window.talkus; 562 | }, 563 | identify: function (userId, userProperties) { 564 | // Hacky way of getting the AppId (for now Todo update) 565 | var lsObj = JSON.parse(localStorage.getItem('talkusBubbleTS')); 566 | var appId = Object.keys(lsObj)[0]; 567 | 568 | if (!userProperties) userProperties = {}; 569 | userProperties.userId = userId; 570 | 571 | if (window.talkus && appId) talkus('init', appId, userProperties); 572 | } 573 | }, 574 | 'google-tag-manager': { 575 | enabled: true, 576 | test: function () { 577 | return !!(window.dataLayer && Array.prototype.push !== window.dataLayer.push); 578 | }, 579 | track: function (eventName, eventProperties) { 580 | if (!eventProperties) eventProperties = {}; 581 | eventProperties.event = eventName; 582 | 583 | if (window.dataLayer && eventProperties) dataLayer.push(eventProperties); 584 | }, 585 | page: function (category, name, properties) { 586 | if (!properties) properties = {}; 587 | properties.event = 'pageview_' + name; 588 | properties.category = category; 589 | 590 | if (window.dataLayer) 591 | dataLayer.push(properties); 592 | } 593 | }, 594 | 'elevio': { 595 | enabled: true, 596 | test: function () { 597 | return !!window._elev; 598 | }, 599 | identify: function (userId, userProperties) { 600 | if (!userProperties || !window._elev) return; 601 | 602 | var user = {}; 603 | user.via = 'event-layer'; 604 | 605 | if (userProperties.email) user.email = userProperties.email; 606 | if (userProperties.name) user.name = userProperties.name; 607 | if (userProperties.plan) user.plan = [userProperties.plan]; 608 | if (userProperties.plan) user.groups = [userProperties.plan]; 609 | 610 | // Delete those 611 | delete userProperties.firstName; 612 | delete userProperties.lastName; 613 | delete userProperties.email; 614 | delete userProperties.name; 615 | delete userProperties.plan; 616 | delete userProperties.id; 617 | 618 | if (Object.keys(userProperties).length > 0) user.traits = userProperties; 619 | window._elev.user = user; 620 | } 621 | }, 622 | 'drift': { 623 | enabled: true, 624 | test: function () { 625 | return window.drift !== undefined; 626 | }, 627 | track: function (eventName, eventProperties) { 628 | // Todo: Nice to have, Investigate convertDates for eventProperties. 629 | // This seems to iterate through dates and apply `Math.floor(date.getTime() / 1000)` 630 | 631 | if (window.drift && eventName) 632 | window.drift.track(eventName, eventProperties); 633 | }, 634 | identify: function (userId, userProperties) { 635 | if (!window.drift || !userId) return; 636 | 637 | delete userProperties.id; 638 | window.drift.identify(userId, userProperties); 639 | }, 640 | page: function (category, name, properties) { 641 | if (window.drift && name) 642 | window.drift.page(name); 643 | } 644 | }, 645 | 'drip': { 646 | enabled: true, 647 | test: function () { 648 | return window._dc && typeof(window._dc) === 'object'; 649 | }, 650 | track: function (eventName, eventProperties) { 651 | if (!window._dcq || !eventName) return; 652 | 653 | if (eventProperties) { 654 | // Convert all keys with spaces to underscores 655 | for (var key in eventProperties) { 656 | if (key.indexOf(' ') === -1) return; // Skip keys w/o spaces 657 | 658 | var formattedKey = key.replace(' ', '_'); 659 | eventProperties[formattedKey] = eventProperties[key]; 660 | delete eventProperties[key]; 661 | } 662 | 663 | if (eventProperties.revenue) { 664 | var cents = Math.round(eventProperties.revenue * 100); 665 | eventProperties.cents = cents; 666 | delete eventProperties.revenue; 667 | } 668 | } 669 | 670 | window._dcq.push('track', eventName, eventProperties); 671 | }, 672 | identify: function (userId, userProperties) { 673 | if (window._dcq && userProperties) 674 | window._dcq.push('identify', userProperties); 675 | } 676 | }, 677 | 'bugsnag': { 678 | enabled: true, 679 | test: function () { 680 | return window.Bugsnag && typeof(window.Bugsnag) === 'object'; 681 | }, 682 | identify: function (userId, userProperties) { 683 | if (!window.Bugsnag) return; 684 | window.Bugsnag.user = window.Bugsnag.user || {}; 685 | window.Bugsnag.user = Object.assign(window.Bugsnag.user, userProperties); 686 | } 687 | }, 688 | 'improvely': { 689 | enabled: true, 690 | test: function () { 691 | return !!(window.improvely && window.improvely.identify); 692 | }, 693 | track: function (eventName, eventProperties) { 694 | var props = eventProperties; 695 | 696 | // Todo: What does track.properties({ revenue: 'amount' }) do? 697 | // Does it do this? 698 | // props = Object.assign(props, { revenue: 'amount' }); 699 | // or this? 700 | // props.revenue = props.amount; 701 | props.revenue = props.amount; 702 | delete props.amount; 703 | 704 | props.type = eventName; 705 | window.improvely.goal(props); 706 | }, 707 | identify: function (userId, userProperties) { 708 | if (userId && window.improvely) 709 | window.improvely.label(userId); 710 | } 711 | }, 712 | 'inspectlet': { 713 | enabled: true, 714 | test: function () { 715 | return !!(window.__insp_ && window.__insp); 716 | }, 717 | track: function (eventName, eventProperties) { 718 | if (window.__insp && eventName) 719 | __insp.push('tagSession', eventName, eventProperties); 720 | }, 721 | identify: function (userId, userProperties) { 722 | if (!window.__insp) return; 723 | 724 | //var traits = identify.traits({ id: 'userid' }); 725 | // Todo: Am I doing it right? 726 | var traits = Object.assign({}, userProperties); 727 | traits.id = userId || traits.uid; 728 | delete traits.uid; 729 | 730 | if (userProperties && userProperties.email) 731 | __insp.push('identify', userProperties.email); 732 | 733 | if (userId || userProperties) 734 | __insp.push('tagSession', traits); 735 | }, 736 | page: function (category, name, properties) { 737 | if (window.__insp) 738 | __insp.push('virtualPage'); 739 | } 740 | }, 741 | 'qualaroo': { 742 | enabled: true, 743 | test: function () { 744 | return !!(window._kiq && window._kiq.push !== Array.prototype.push); 745 | }, 746 | track: function (eventName, eventProperties, options) { 747 | var traits = {}; 748 | traits['Triggered: ' + eventName] = true; 749 | // this.identify(new Identify({ traits: traits })); // Identify = require('segmentio-facade').Identify; 750 | }, 751 | identify: function (userId, userProperties) { 752 | if (!window._kiq) return; 753 | 754 | if (userProperties && userProperties.email) userId = userProperties.email; 755 | if (userProperties) _kiq.push('set', userProperties); 756 | if (userId) _kiq.push('identify', userId); 757 | } 758 | }, 759 | 'facebook-tracking-pixel': { 760 | enabled: true, 761 | test: function () { 762 | return !!(window.fbq && typeof window.fbq === 'function'); 763 | }, 764 | track: function (eventName, eventProperties) { 765 | if (!window.fbq) return; 766 | 767 | fbq('trackCustom', eventName, eventProperties); 768 | }, 769 | page: function (category, name, properties) { 770 | if (!window.fbq) return; 771 | 772 | fbq('track', "PageView"); 773 | }, 774 | facebookTrackEvent: function (eventName, eventProperties) { 775 | if (!window.fbq) return; 776 | 777 | fbq('track', eventName, eventProperties); 778 | } 779 | }, 780 | 'customerio': { 781 | enabled: true, 782 | test: function () { 783 | return !!(window._cio && window._cio.push !== Array.prototype.push); 784 | }, 785 | track: function (eventName, eventProperties) { 786 | if (!window._cio) return; 787 | 788 | window._cio.track(eventName, eventProperties); 789 | }, 790 | identify: function (userId, userProperties) { 791 | if (!window._cio) return; 792 | if (!userId) return console.warn('user id required by customer.io for identify function.'); 793 | 794 | // Expects userProperties { id: string unique, email: string, created_at: unix-timestamp } 795 | 796 | // Transform createdAt -> created_at 797 | if (userProperties && userProperties.createdAt && !userProperties.created_at) 798 | userProperties.created_at = userProperties.createdAt; 799 | 800 | // Add userId if no id is present 801 | if (userProperties && !userProperties.id) 802 | userProperties.id = userId; 803 | 804 | window._cio.identify(userProperties); 805 | }, 806 | alias: function (userId, previousId) { 807 | // Todo 808 | }, 809 | group: function (groupId, traits) { 810 | // Todo 811 | }, 812 | page: function (category, name, properties) { 813 | if (!window._cio) return; 814 | 815 | if (!window.__currentUserId) return console.warn('You must call the Identify function for Customer.io before the page function, passing a valid userId.'); 816 | if (!name) return console.warn('Customer.io requires a valid name property when calling the page event. Since Analytics.js expects a category field as well, this must be sent (even if it is empty). See documentation for more details.'); 817 | 818 | if (!properties) properties = {}; 819 | 820 | properties.id = window.__currentUserId; 821 | properties.type = 'page'; 822 | properties.name = name; 823 | properties.category = category; 824 | 825 | window._cio.page(location.href, properties); 826 | } 827 | }, 828 | 'logspot': { // This sends some debugging messages if enabled 829 | enabled: true, 830 | test: function () { return !!window.Logspot && Logspot.track }, 831 | track: function (eventName, eventProperties) { 832 | // console.log('Track:', eventName, eventProperties); 833 | 834 | window.Logspot.track({ 835 | event: eventName, 836 | userId: window.__currentUserId, 837 | metadata: eventProperties, 838 | }); 839 | }, 840 | }, 841 | 'telemetry': { // This sends some debugging messages if enabled 842 | enabled: true, 843 | test: function () { return !!window.TELEMETRY && window.TELEMETRY_ENABLED }, 844 | track: function (eventName, eventProperties) { 845 | // console.log('Track:', eventName, eventProperties); 846 | window.TELEMETRY.track( eventName, eventProperties ); 847 | }, 848 | identify: function (userId, userProperties) { 849 | // console.log('Track:', eventName, eventProperties); 850 | window.TELEMETRY.identify( userId, userProperties ); 851 | }, 852 | }, 853 | 'debug': { // This sends some debugging messages if enabled 854 | enabled: true, 855 | test: function () { return !!window.__debug }, 856 | track: function (eventName, eventProperties) { 857 | console.log('Track:', eventName, eventProperties); 858 | }, 859 | identify: function (userId, userProperties) { 860 | console.log('Identify:', userId, userProperties); 861 | }, 862 | page: function (category, name, properties) { 863 | console.log('Page:', category, name, properties); 864 | }, 865 | alias: function (userId, previousId) { 866 | console.log('Alias:', userId, previousId); 867 | }, 868 | group: function (groupId, traits) { 869 | console.log('Group:', groupId, traits); 870 | } 871 | }, 872 | 'blank-adapter-template': { // Do not modify this template 873 | enabled: false, 874 | test: function () {}, 875 | track: function (eventName, eventProperties) {}, 876 | identify: function (userId, userProperties) {}, 877 | page: function (category, name, properties) {}, 878 | alias: function (userId, previousId) {}, 879 | group: function (groupId, traits) {} 880 | } 881 | }; 882 | 883 | // Recursively convert an `obj`'s dates to new values, using an input function, convert(). 884 | function convertDates (oObj, convert) { 885 | if (typeof(oObj) !== 'object') return oObj; 886 | 887 | var obj = Object.assign({}, oObj); 888 | 889 | for (var key in obj) { 890 | var val = obj[key]; 891 | if (typeof(val) === 'date') obj[key] = convert(val); 892 | if (typeof(val) === 'object') obj[key] = convertDates(val, convert); 893 | } 894 | 895 | return obj; 896 | } 897 | 898 | function runTest (f) { 899 | try { 900 | return f(); 901 | } catch (e) { 902 | return false; 903 | } 904 | return false; 905 | } 906 | 907 | function track (eventName, eventProperties, options, callback) { 908 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 909 | 910 | onReady(); 911 | 912 | for (var adapterName in thirdPartyAdapters) { 913 | var adapter = thirdPartyAdapters[adapterName]; 914 | 915 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 916 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 917 | // If everything checks out for the data we've received, 918 | // pass the data to the adapter so it can be tracked 919 | 920 | if (window.__debug) console.log('Track method executing on', adapterName); 921 | 922 | // If TRANSLATE_EVENT_NAMES exists, use it to translate event names 923 | if (window.TRANSLATE_EVENT_NAMES && typeof window.TRANSLATE_EVENT_NAMES === 'object') 924 | eventName = TRANSLATE_EVENT_NAMES(eventName); 925 | 926 | if (adapter.track && typeof(adapter.track) === 'function') 927 | adapter.track(eventName, eventProperties); 928 | } 929 | } 930 | 931 | if (callback && typeof(callback) === 'function') callback(); 932 | } 933 | 934 | function identify (userId, userProperties, options, callback) { 935 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 936 | 937 | onReady(); 938 | 939 | // Stash this for later 940 | window.__currentUserId = userId; 941 | 942 | for (var adapterName in thirdPartyAdapters) { 943 | var adapter = thirdPartyAdapters[adapterName]; 944 | 945 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 946 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 947 | // If everything checks out for the data we've received, 948 | // pass the data to the adapter so it can be tracked 949 | 950 | if (window.__debug) console.log('Identify method executing on', adapterName); 951 | 952 | if (adapter.identify && typeof(adapter.identify) === 'function') 953 | adapter.identify(userId, userProperties); 954 | } 955 | } 956 | 957 | if (callback && typeof(callback) === 'function') callback(); 958 | } 959 | 960 | function page (category, name, properties, options, callback) { 961 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 962 | 963 | onReady(); 964 | 965 | // Handle not passing the category (shift right) 966 | if (category && (!name || typeof(name) !== 'string')) { 967 | callback = options; 968 | options = properties; 969 | properties = name; 970 | name = category; 971 | category = null; 972 | } 973 | 974 | // url (canonical?), title, referrer, path 975 | var url = document.querySelector("link[rel='canonical']") ? document.querySelector("link[rel='canonical']").href : document.location.href; 976 | var title = document.title; 977 | var referrer = document.referrer; 978 | var path = location.pathname; 979 | 980 | var props = { 981 | url: url, 982 | title: title, 983 | referrer: referrer, 984 | path: path 985 | }; 986 | 987 | var properties = Object.assign(props, properties); 988 | 989 | for (var adapterName in thirdPartyAdapters) { 990 | var adapter = thirdPartyAdapters[adapterName]; 991 | 992 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 993 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 994 | 995 | if (window.__debug) console.log('Page method executing on', adapterName); 996 | 997 | // If everything checks out for the data we've received, 998 | // pass the data to the adapter so it can be tracked 999 | if (adapter.page && typeof(adapter.page) === 'function') 1000 | adapter.page(category, name, properties); 1001 | } 1002 | } 1003 | 1004 | if (callback && typeof(callback) === 'function') callback(); 1005 | } 1006 | 1007 | function group (groupId, traits, options, callback) { 1008 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 1009 | 1010 | onReady(); 1011 | 1012 | for (var adapterName in thirdPartyAdapters) { 1013 | var adapter = thirdPartyAdapters[adapterName]; 1014 | 1015 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 1016 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 1017 | 1018 | if (window.__debug) console.log('Group method executing on', adapterName); 1019 | 1020 | // If everything checks out for the data we've received, 1021 | // pass the data to the adapter so we can perform a grouping 1022 | if (adapter.group && typeof(adapter.group) === 'function') 1023 | adapter.group(groupId, traits); 1024 | } 1025 | } 1026 | 1027 | if (callback && typeof(callback) === 'function') callback(); 1028 | } 1029 | 1030 | function alias (userId, previousId, options, callback) { 1031 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 1032 | 1033 | onReady(); 1034 | 1035 | for (var adapterName in thirdPartyAdapters) { 1036 | var adapter = thirdPartyAdapters[adapterName]; 1037 | 1038 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 1039 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 1040 | 1041 | if (window.__debug) console.log('Alias method executing on', adapterName); 1042 | 1043 | // If everything checks out for the data we've received, 1044 | // pass the data to the adapter so we can alias this user 1045 | if (adapter.alias && typeof(adapter.alias) === 'function') 1046 | adapter.alias(userId, previousId); 1047 | } 1048 | } 1049 | 1050 | if (callback && typeof(callback) === 'function') callback(); 1051 | } 1052 | 1053 | /** 1054 | * Facebook tracking pixel support 1055 | * 1056 | * Based on: https://developers.facebook.com/docs/facebook-pixel/api-reference 1057 | * 1058 | * Facebook tracking pixel specific event names: 1059 | * ViewContent 1060 | * Search 1061 | * AddToCart 1062 | * AddToWishlist 1063 | * InitiateCheckout 1064 | * AddPaymentInfo 1065 | * Purchase 1066 | * Lead 1067 | * CompleteRegistration 1068 | */ 1069 | function fbTrack (eventName, eventProperties, options, callback) { 1070 | if (!thirdPartyAdapters) return; // Early return if there are no adapters 1071 | 1072 | onReady(); 1073 | 1074 | // Iterate through third-party adapters, sending track events. 1075 | for (var adapterName in thirdPartyAdapters) { 1076 | var adapter = thirdPartyAdapters[adapterName]; 1077 | 1078 | if (adapterName === 'facebook-tracking-pixel') continue; // Skip FB Tracking pixel 1079 | 1080 | // If this adapter passes it's own internal test (usually to detect if a specific source is available) 1081 | if (adapter.enabled && adapter.test && typeof(adapter.test) === 'function' && runTest(adapter.test)) { 1082 | 1083 | if (window.__debug) console.log('fbTrack method executing on', adapterName); 1084 | 1085 | // If everything checks out for the data we've received, 1086 | // pass the data to the adapter so it can be tracked 1087 | if (adapter.facebookTrackEvent && typeof(adapter.facebookTrackEvent) === 'function') { 1088 | adapter.facebookTrackEvent(eventName, eventProperties); 1089 | } else if (adapter.track && typeof(adapter.track) === 'function') { 1090 | // If TRANSLATE_EVENT_NAMES exists, use it to translate event names 1091 | if (window.TRANSLATE_EVENT_NAMES && typeof window.TRANSLATE_EVENT_NAMES === 'object') 1092 | eventName = TRANSLATE_EVENT_NAMES(eventName); 1093 | 1094 | adapter.track(eventName, eventProperties); 1095 | } 1096 | 1097 | } 1098 | } 1099 | 1100 | if (callback && typeof(callback) === 'function') callback(); 1101 | } 1102 | 1103 | /** 1104 | * Event Layer defaults 1105 | */ 1106 | function ready (callback) { 1107 | if (window.__debug) console.log('Event Layer Ready'); 1108 | 1109 | if (callback && typeof(callback) === 'function') 1110 | EventLayer.readyFunction = callback; 1111 | } 1112 | 1113 | function onReady () { 1114 | if (!EventLayer.readyFunction) return; 1115 | 1116 | if (EventLayer.readyFunction && typeof(EventLayer.readyFunction) === 'function') EventLayer.readyFunction(); 1117 | 1118 | EventLayer.readyFunction = null; 1119 | } 1120 | 1121 | // Execute directly before the first track/identify/page/group/alias call, or after a default timeout (5s) 1122 | setTimeout(onReady, 5000); 1123 | 1124 | 1125 | /** 1126 | * Add an adapter on the fly for testing or modifying EventLayer on your own 1127 | */ 1128 | function addAdapter (namespace, adapter) { 1129 | if (typeof adapter !== 'object') return; 1130 | 1131 | if (thirdPartyAdapters[namespace]) console.warn('Adapter for namespace ' + namespace + ' already exists, and is being overwritten.'); 1132 | else if (window.__debug) console.log('Adapter for ' + namespace + ' added.'); 1133 | 1134 | thirdPartyAdapters[namespace] = adapter; 1135 | } 1136 | 1137 | // Todo: 1138 | // QueryString API? 1139 | // Selecting Integrations should match analytics.js syntax 1140 | 1141 | // Create / export globals 1142 | window.EventLayer = {}; 1143 | EventLayer.thirdPartyAdapters = thirdPartyAdapters; 1144 | EventLayer.addAdapter = addAdapter; 1145 | EventLayer.readyFunction = null; 1146 | EventLayer.Integrations = null; // This needs to be null so that it's not confused with Segment.com's library. 1147 | EventLayer.identify = identify; 1148 | EventLayer.onReady = onReady; 1149 | EventLayer.fbTrack = fbTrack; // Facebook-tracking pixel 1150 | EventLayer.track = track; 1151 | EventLayer.group = group; 1152 | EventLayer.alias = alias; 1153 | EventLayer.ready = ready; 1154 | EventLayer.page = page; 1155 | 1156 | window.__currentUserId = null; 1157 | 1158 | return function () { 1159 | return EventLayer; 1160 | }; 1161 | 1162 | })(); 1163 | 1164 | if (typeof module !== 'undefined') { 1165 | module.exports = function () { 1166 | // console.log('opts', opts); 1167 | return EventLayer(); 1168 | } 1169 | } 1170 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace EventLayer {} 2 | 3 | declare class EventLayer { 4 | constructor(); 5 | 6 | identify(userId: string, userProperties?: T): void; 7 | track(eventName: string, eventProperties?: T): void; 8 | page(category: string, name: string, properties?: T): void; 9 | alias(userId: string, previousId?: string): void; 10 | group(groupId: string, traits?: T): void; 11 | fbTrack(eventName: string, eventProperties?: T): void; 12 | } 13 | 14 | export = EventLayer; 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Event-layer.js Demo 6 | 7 | 8 | 9 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 |

A very very simple abstraction layer for analytics code. Write your events once, then send them where ever you want.

54 | 55 | Scroll Down for a Demo 56 | 57 |

What is it?

58 |

“Event Layer” is an extensible abstraction layer for working with common third-party Analytics libraries.

59 |

Since more or less all analytics libraries work the same way (allowing you to identify and describe users, and track the events which they perform), 60 | “Event Layer” creates an abstraction layer and a set of adapters that allow you to write generic Analytics Tracking code once, and update your 61 | configuration later to send your data anywhere you need.

62 |

How does it work?

63 |
    64 |
  1. Install your third-party analytics libraries in your application or website (you don't need to do anything special)
  2. 65 |
  3. Install event-layer.js in your website or app.
  4. 66 |
  5. Instantiate a new instance of EventLayer: (e.g. var Analytics = new EventLayer();).
  6. 67 |
  7. Instead of filling your app or website with service-specific tracking code, write generic code using the “Event Layer” Javascript API (described below).
  8. 68 |
  9. “Event Layer” will detect the third-party Analytics services you have installed on your website or app, and use it's own, community-maintained adapters to propagate your events, in an identical format, to each third-party service, using the latest version(s) of their APIs.
  10. 69 |
  11. (Optional): You can extend “Event Layer” by writing your own custom adapters for third-party services not yet supported. Each adapter only requires about 12 lines of Javascript, and the community is available to help you ship your first PR to “Event Layer”.
  12. 70 |
71 |

Instantiating a new instance of EventLayer

72 |

We realize that not everyone wants to litter their code with calls to a global named “Event Layer”. So you'll probably want to start off by instantiating a new instance of “Event Layer”.

73 |
var Analytics = new EventLayer();
 74 | 
75 |

You can name it whatever you like, but the examples given below will need to be modified appropriately.

76 |

EventLayer.identify(userId, userProperties)

77 |

This method allows you to identify the current visitor, and (optionally) describe the user.

78 |

Only identify the user, do not describe any user properties:

79 |
Analytics.identify('<unique-user-id>');
 80 | 
81 |

Identify the user and describe user properties:

82 |
Analytics.identify('<unique-user-id>', {
 83 |     name: 'John Doe',
 84 |     subscribedToNewsletter: false
 85 | });
 86 | 
87 |

This is similar to the Identify method found in Mixpanel, Heap, and Analytics.js.

88 |

EventLayer.track(eventName, eventProperties)

89 |

This method allows you to identify the current visitor, and (optionally) describe the user.

90 |

Track a simple event, with no event properties:

91 |
Analytics.track('click_signup_button');
 92 | 
93 |

Track a more complex event, with event properties:

94 |
Analytics.track('purchase', {
 95 |     amount: 32.00,
 96 |     itemCount: 4,
 97 |     shippingSpeed: 'next-day'
 98 | });
 99 | 
100 |

This is similar to the Track method found in Mixpanel, Heap, and Analytics.js.

101 |

Writing an Adapter

102 |

Below is an example of a blank adapter.

103 |
'blank-adapter-template': { // Do not modify this template
104 |     enabled: false, // Change to true once you're completed testing
105 |     test: function () {},
106 |     identify: function (userId, userProperties) {},
107 |     track: function (eventName, eventProperties) {}
108 | }
109 | 
110 | 111 |
112 |

It has three main components:

113 |

1. Test:

114 |

This function, once evaluated, should provide an answer to the question “has a specific third-party analytics library been installed on this page? Is it active? Should we try to send events to this service?”

115 |

This can be as simple as sniffing the window object for a global variables, and a commonly-used (and not likely to disappear) method or property:

116 |
test: function () {
117 |     return window.ga && window.ga.loaded;
118 | }
119 | 
120 |

(A simple example taken from the Google Analytics adapter)

121 |

2. identify:

122 |

This function takes data from our generic identify method, and passes it along to a third-party library, via an adapter.

123 |

This should contain a minimal number of integrity checks and transforms, as well as a lightweight wrapper for the library's identify and/or describe functionality.

124 |
identify: function (userId, userProperties) {
125 |     // Send the identify call to Mixpanel's JS library
126 |     if (window.mixpanel && userId)
127 |     mixpanel.identify(userId);
128 | 
129 |     // Set people properties on our identified user
130 |     if (window.mixpanel && userProperties)
131 |     mixpanel.people.set(userProperties);
132 | }
133 | 
134 |

(A simple example taken from the Mixpanel adapter)

135 |

3. track:

136 |

This function takes data from our generic track method, and passes it along to a third-party library, via an adapter.

137 |

This should contain a minimal number of integrity checks and transforms, as well as a lightweight wrapper for the library's event tracking functionality.

138 |
track: function (eventName, eventProperties) {
139 |     // Send the tracked event to Heap's JS library
140 |     if (window.heap && eventName)
141 |     heap.track(eventName, eventProperties);
142 | }
143 | 
144 |

(A simple example taken from the Heap Analytics adapter)

145 |
146 | 147 |

Pull Requests

148 |

Yes, Please & Thank you!

149 |

To keep things organized, please open an issue for discussion before putting too much work into a pull request. I would feel really bad if you put a lot of work into something, only for it to not make sense to include it or merge the Pull Request.

150 |

Also, to help keep things organized, try to submit individual pull requests for each issue, and keep the scope of each issue relatively small.

151 |

For example, if you wanted to add a couple of new adapters, split them into multiple pull requests so we can review them individually.

152 |

Issues

153 |

Something feel broken? Open an issue! 154 | Something you feel is missing? Open an issue (Although we may not be able to get to everything, pull requests are most welcome!)

155 | 156 |

Thanks!

157 | 158 |
159 | 160 |

Demo:

161 | 162 |

For this demo, we include Google Analytics and the event-layer.js library, instantiate event-layer as `Analytics`, and track an event called `button_clicked` when the button is clicked. You can inspect this page to see how the demo works.

163 | 164 | 165 | 166 |

 

167 | 168 |
169 | 170 | 171 | 172 | 176 | 177 | 178 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /integration-progress.md: -------------------------------------------------------------------------------- 1 | Integrations Progress 2 | -------------------------------------------- 3 | 4 | 1. Crazy Egg: No integrations provided 5 | 2. Elev.io: Implementation completed (identify) 6 | 3. Drift: Implementation completed (identify, track, page) 7 | 4. Drip: Implementation completed (identify, track). 8 | 5. Lucky Orange: Implementation completed (identify) 9 | 6. BugHerd: No integrations provided 10 | 7. Bugsnag: Implementation completed (identify) 11 | 8. Chameleon: Implementation completed (identify, track, group, alias) 12 | 9. Hello Bar: No integrations provided 13 | 10. Improvely: Implementation completed (identify, track) 14 | 11. Inspectlet: Implementation completed (identify, track, page) 15 | 12. Qualaroo: Partially implemented (identify). Track not implemented (dependency). 16 | 13. Facebook Tracking Pixel: Partially implemented. Track and Pageviews have been implemented, however, facebook tracking pixel also supports nine custom events. Three have been implemented. 17 | 14. Customer.io: Untested, Added integration based on Segment.com integration & available documentation. 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-layer", 3 | "version": "0.2.32", 4 | "description": "A very simple abstraction layer for analytics code. Write your events once, then send them where ever you want.", 5 | "main": "event-layer.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "static": "serve -p 1234", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/kidGodzilla/event-layer.git" 14 | }, 15 | "keywords": [ 16 | "Analytics.js", 17 | "event", 18 | "federation", 19 | "layer", 20 | "analytics", 21 | "abstraction", 22 | "segment", 23 | "simple" 24 | ], 25 | "author": "James Futhey (https://jamesfuthey.com/)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/kidGodzilla/event-layer/issues" 29 | }, 30 | "homepage": "https://github.com/kidGodzilla/event-layer#readme" 31 | } 32 | --------------------------------------------------------------------------------