├── .editorconfig
├── .gitignore
├── .npmignore
├── .travis.yml
├── Gruntfile.js
├── LICENSE
├── README.md
├── dist
├── simple-tracker.min.js
└── simple-tracker.min.map
├── examples
├── README.md
├── all-functions.html
└── server-examples
│ ├── .gitignore
│ ├── README.md
│ ├── aws-lambda
│ ├── google-analytics.js
│ └── track.js
│ ├── netlify.toml
│ └── package.json
├── index.js
├── package.json
├── test
└── unit
│ └── simple-tracker.js
└── version.sh
/.editorconfig:
--------------------------------------------------------------------------------
1 | # This file is for unifying the coding style for different editors and IDEs
2 | # editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
14 | [package.json]
15 | indent_style = space
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | /bower_components
4 | /test/simple-tracker.js
5 | /.nyc_output
6 | /coverage
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | jasminetest
2 | Gruntfile.js
3 | bower.json
4 | .editorconfig
5 | package-lock.json
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | after_success: npm run coverage
5 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | var packageJson = grunt.file.readJSON('package.json');
3 | grunt.initConfig({
4 | pkg: packageJson,
5 | uglify: {
6 | options: {
7 | sourceMap: true,
8 | sourceMapName: 'dist/simple-tracker.min.map',
9 | banner: '/*! <%= pkg.name %> - v<%= pkg.version %> | MIT\n' +
10 | ' * https://github.com/codeniko/simple-tracker */'
11 | },
12 | main: {
13 | files: [{
14 | src: 'index.js',
15 | dest: 'dist/simple-tracker.min.js'
16 | }]
17 | },
18 | }
19 | });
20 | grunt.loadNpmTasks('grunt-contrib-uglify');
21 | grunt.registerTask('default', ['uglify']);
22 | };
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Nikolay Feldman
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Simple Tracker
2 | ===============
3 | [](https://npmjs.org/package/simple-tracker)
4 | [](https://travis-ci.com/codeniko/simple-tracker)
5 | [](https://codecov.io/gh/codeniko/simple-tracker)
6 | 
7 | [](https://david-dm.org/codeniko/simple-tracker)
8 | [](https://github.com/codeniko/simple-tracker/blob/master/LICENSE)
9 |
10 | Easy client-side tracking library to log events, metrics, errors, and messages. Send data to any server endpoint for log management and event tracking services like Google Analytics, Splunk, ELK/Logstash, Loggly, Open Web Analytics, etc...
11 |
12 | You can make use of Simple Tracker one of two ways. You can install through [npm and use it as a module](#installation-through-npm-as-module), or you can [include the uglified script file in your HTML page](#installation-in-html).
13 |
14 | Inspiration
15 | ------------
16 | If you run an adblocker or a trackerblocker plugin in your browser, page requests to google analytics and other well known tracking libraries get blocked causing you to lose valuable metrics/logs from your websites. To circumvent these blockers, you'd have to write some javascript on your pages to send tracking data to an endpoint that won't be blocked and configure a server or cloud function to proxy this data to a service of your choice. Simple Tracker is the first piece to that solution. It's a light-weight client library that makes sending tracking data simple.
17 |
18 | If you're looking to connect your tracking data sent from Simple Tracker to a cloud function, [check out these example AWS Lambda functions](examples/server-examples) which proxies the data to a free log management service called [Loggly](https://www.loggly.com/).
19 |
20 |
21 | Installation through NPM as module
22 | ------------
23 | In command line, run:
24 | ```sh
25 | npm install simple-tracker
26 | ```
27 | In code:
28 | ```javascript
29 | import tracker from 'simple-tracker' // or const tracker = require('simple-tracker')
30 |
31 | // initialize tracker endpoint and settings
32 | tracker.push({
33 | endpoint: '/endpoint', // Endpoint to send tracking data to
34 | attachClientContext: true, // Attach various client context, such as useragent, platform, and page url
35 | });
36 | ```
37 |
38 | You only need to initialize endpoint and settings as above once. After initializing, simply import `tracker` in other modules or components:
39 | ```javascript
40 | import tracker from 'simple-tracker' // or const tracker = require('simple-tracker')
41 |
42 | tracker.push({ event: 'pageview' })
43 | ```
44 | Here is a live example page: [https://codeniko.github.io/simple-tracker/examples/all-functions.html](https://codeniko.github.io/simple-tracker/examples/all-functions.html)
45 |
46 |
47 | Installation in HTML
48 | ------------
49 | Place the following on your page. While you can use the script at the [CDN link](https://unpkg.com/simple-tracker@latest/dist/simple-tracker.min.js) below, I recommend you to download the script and host it yourself.
50 | ```html
51 |
52 |
59 | ```
60 |
61 | Here is a live example page: [https://codeniko.github.io/simple-tracker/examples/all-functions.html](https://codeniko.github.io/simple-tracker/examples/all-functions.html)
62 |
63 | Quick Usage
64 | -----
65 | Logging text:
66 | ```javascript
67 | tracker.push('some text to track');
68 | ```
69 |
70 | Logging JSON:
71 | ```javascript
72 | tracker.push({
73 | message: 'my tracking string',
74 | values: [1, 2, 3, 'a', 'b', 'c']
75 | });
76 | ```
77 |
78 | This will send a POST request containing a sessionId, and client context if enabled (enabled by default). An example request payload:
79 | ```json
80 | {
81 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000",
82 | "message": "my tracking string",
83 | "values": [1, 2, 3, "a", "b", "c"],
84 | "context": {
85 | "url": "https://nfeld.com/",
86 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
87 | "platform": "MacIntel"
88 | }
89 | }
90 | ```
91 |
92 | There are also several convenience functions defined to push common tracking data such as `tracker.logEvent(event)`, `tracker.logMessage('message')`, and `tracker.logMetric('metric', 'value')`. [You can find examples of these and more below.](#tracker-functions)
93 |
94 | Session Id
95 | -----
96 | Simple Tracker makes use of cookies to persist the sessionId that accompanies all tracking data. If the sessionId is not explicitly set in [configuration](#all-configurations), one will be generated as a UUIDv4 string. Regardless if explicitly set or generated, the sessionId will be stored in a cookie named `trcksesh` and will be destroyed when session ends (browser closes)
97 |
98 | All configurations
99 | -----
100 | ```javascript
101 | tracker.push({
102 | endpoint: '/endpoint', // Endpoint to send tracking data to
103 | sessionId: 'SESSION_ID', // Explicitly set a session id
104 | sendCaughtExceptions: true/false, // Send exceptions caught by browser. DEFAULT: false
105 | attachClientContext: true/false, // Attach client context to requests. Includes: useragent, platform, and page url. DEFAULT: true
106 | attachSessionId: true/false, // Attach sessionId to requests. DEFAULT: true
107 | devMode: true/false // Toggle dev mode. If enabled, outgoing requests are blocked and logged for debugging instead. DEFAULT: false
108 | });
109 | ```
110 |
111 | Adding to client context object
112 | -----
113 | You can add your own values to persist inside of the client context object, or even overwrite the object entirely. You can access the object with `tracker.clientContext`. Any values you add to the clientContext object will go out on every tracking request
114 | ```javascript
115 | // assign more values
116 | tracker.clientContext.username = 'codeniko',
117 | tracker.clientContext.location = 'San Francisco, CA'
118 |
119 | // overwriting context entirely
120 | tracker.clientContext = {
121 | username = 'codeniko',
122 | location = 'San Francisco, CA'
123 | }
124 | ```
125 |
126 | Tracker functions
127 | -----
128 | Here is a live example page showing all of the convenience functions:
129 | [https://codeniko.github.io/simple-tracker/examples/all-functions.html](https://codeniko.github.io/simple-tracker/examples/all-functions.html)
130 |
131 | `logEvent(event, additionalParams)`: Log an event that occurred, with optional additionalParams
132 | ```javascript
133 | tracker.logEvent('contact_form_submitted', { name: 'niko', fromEmail: 'niko@nfeld.com' });
134 |
135 | // Request: POST /endpoint
136 | {
137 | "type": "event",
138 | "event": "contact_form_submitted",
139 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000",
140 | "name": "niko",
141 | "fromEmail": "niko@nfeld.com"
142 | }
143 | ```
144 |
145 | `logMessage(message, optionalLevel)`: Log simple message, with optional level as second argument
146 | ```javascript
147 | tracker.logMessage('some message', 'info');
148 |
149 | // Request: POST /endpoint
150 | {
151 | "type": "message",
152 | "message": "some message",
153 | "level": "info",
154 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000"
155 | }
156 | ```
157 |
158 | `logException(exceptionObject)`: Log exceptional error. Can pass an exception object, or other value
159 | ```javascript
160 | tracker.logException(new Error('some exception').stack);
161 |
162 | // Request: POST /endpoint
163 | {
164 | "type": "exception",
165 | "level": "error",
166 | "exception": "Error: some exception at :1:1",
167 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000"
168 | }
169 | ```
170 |
171 | `logMetric(metricKey, metricValue)`: Log a metric and its value
172 | ```javascript
173 | tracker.logMetric('page_load_time', 254);
174 |
175 | // Request: POST /endpoint
176 | {
177 | "type": "metric",
178 | "metric": "page_load_time",
179 | "value": 254,
180 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000"
181 | }
182 | ```
183 |
184 | Start/stop a timer to record a metric
185 | `startTimer(metricKey)`: Start a timer named by metricKey
186 | `stopTimer(metricKey, metricValue)`: Stop timer named metricKey and log result in millis as metric value
187 | ```javascript
188 | tracker.startTimer('page_load_time');
189 | // wait 1 second
190 | tracker.stopTimer('page_load_time');
191 |
192 | // Request: POST /endpoint
193 | {
194 | "type": "metric",
195 | "metric": "page_load_time",
196 | "value": 1000,
197 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000"
198 | }
199 | ```
200 |
201 | `push(dataObject)`: Push any data of your choice
202 | ```javascript
203 | tracker.push({
204 | message: 'my tracking string',
205 | values: [1, 2, 3, 'a', 'b', 'c'],
206 | userMap: {
207 | codeniko: 1234,
208 | chance: 8888
209 | }
210 | });
211 |
212 | // Request: POST /endpoint
213 | {
214 | "message": "my tracking string",
215 | "values": [1, 2, 3, "a", "b", "c"],
216 | "userMap": {
217 | "codeniko": 1234,
218 | "chance": 8888
219 | },
220 | "sessionId": "11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000"
221 | }
222 | ```
223 |
224 | Examples out in production
225 | -----
226 | You can find Simple Tracker used on the following websites. For some fun, ensure your adblocker is enabled, open up devtool console, refresh/navigate the pages and observe network requests to `/ga` for google analytics pageviews and `/track` for log messages.
227 | [https://jessicalchang.com](https://jessicalchang.com)
228 | [https://nfeld.com](https://nfeld.com)
229 |
230 |
231 | Bugs, feature requests, & contributing
232 | -----
233 | If you found a bug or want to request a feature, [create a new issue](https://github.com/codeniko/simple-tracker/issues). Contributions via pull requests are more than welcome :)
234 |
235 | Running unit tests and code coverage
236 | ----------
237 | ```sh
238 | npm test
239 | ```
240 |
--------------------------------------------------------------------------------
/dist/simple-tracker.min.js:
--------------------------------------------------------------------------------
1 | /*! simple-tracker - v1.2.3 | MIT
2 | * https://github.com/codeniko/simple-tracker */
3 | !function(){"use strict";function e(r){var i,s,a,o="trcksesh",c=o.length+1,l=r.document,d=!0,u=!0,p=!1,f={};function n(e){return e?(e^16*Math.random()>>e/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,n)}function h(e){var t;s=e||s||function(){var e=l.cookie,t=e.indexOf(o);if(0<=t){var n=e.indexOf(";",t+1);return n=n<0?e.length:n,e.slice(t+c,n)}}()||n(),t=s,l.cookie=o+"="+t}function e(){this.clientContext={url:r.location.href,userAgent:r.navigator.userAgent||null,platform:r.navigator.platform||null}}e.prototype={onerror:function(e,t,n,o,i){var r={message:e,lineno:n,colno:o,stack:i?i.stack:"n/a"};this.logException(r)},logEvent:function(e,t){var n={type:"event",event:e};if("object"==typeof t)for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);this.push(n)},logException:function(e){this.push({level:"error",type:"exception",exception:e})},logMessage:function(e,t){var n={type:"message",message:e};t&&(n.level=t),this.push(n)},logMetric:function(e,t){this.push({type:"metric",metric:e,value:t})},startTimer:function(e){var t=r.performance;t.now&&(f[e]&&p&&console.warn("Timing metric '"+e+"' already started"),p&&console.debug("timer started for:",e),f[e]=t.now())},stopTimer:function(e){var t=r.performance;if(t.now){var n=t.now(),o=f[e];if(void 0!==o){var i=Math.round(n-o);f[e]=void 0,p&&console.debug("timer stopped for:",e,"time="+i),this.logMetric(e,i)}else p&&console.warn("Timing metric '"+e+"' wasn't started")}},push:function(e){var t,n,o=typeof e;"object"!==o&&"string"!==o||("string"===o?e={text:e}:(void 0!==e.devMode&&(p=!!e.devMode,delete e.devMode),void 0!==e.attachClientContext&&(d=!!e.attachClientContext,delete e.attachClientContext),void 0!==e.attachSessionId&&(u=!!e.attachSessionId,delete e.attachSessionId),e.sessionId&&(h(e.sessionId),delete e.sessionId),e.endpoint&&(s||h(),i=e.endpoint,delete e.endpoint),void 0!==e.sendCaughtExceptions&&(!!e.sendCaughtExceptions&&(t=this.onerror,n=r.onerror,r.onerror=function(){t.apply(a,arguments),"function"==typeof n&&n.apply(r,arguments)}),delete e.sendCaughtExceptions)),function(e){if(i&&0
2 |
3 |
4 | Simple-Tracker example
5 |
6 |
13 |
14 |
15 | Simple-Tracker example
16 | https://github.com/codeniko/simple-tracker
17 |
18 | Open up the console through devTools to see the tracker in action. Refresh the page to restart
19 |
20 |
21 |
22 |
23 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/examples/server-examples/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # production
5 | /build
6 |
7 | # misc
8 | .DS_Store
9 |
10 | npm-debug.log*
11 |
--------------------------------------------------------------------------------
/examples/server-examples/README.md:
--------------------------------------------------------------------------------
1 | Server examples using AWS Lambda
2 | ===============
3 |
4 | Here you'll find a project that includes two AWS Lambda functions. One is used to proxy pageviews to google analytics incase google analytics is blocked on the client side. Another is used to proxy arbitrary tracking data to a log management service I found called [Loggly](https://www.loggly.com/). These are fully functional AWS Lambda examples and I currently use these for my own websites.
5 |
6 | Project details
7 | ---------
8 | This project uses a service called [Netlify](https://www.netlify.com/) to continously deploy the AWS Lambda functions. Underlying, Netlify uses AWS Lambda and you can use the example lambda function code interchangeably between Netlify and AWS. What's nice about Netlify is it's easier to setup than AWS and builds/deploys your lambda functions as you make commits and push to your repository.
9 |
10 |
11 | Proxying to Google Analytics
12 | ---------
13 | This was the main reason I created [simple-tracker](https://github.com/codeniko/simple-tracker). If a user has an adblocker, google analytics is usually blocked and you lose metrics, including pageviews. This means that the data google analytics tool shows to you is underreported. You're getting more traffic than you think you do because [~40% of users use an adblocker](https://marketingland.com/survey-shows-us-ad-blocking-usage-40-percent-laptops-15-percent-mobile-216324), including myself.
14 |
15 | The example lambda function is [aws-lambda/google-analytics.js](aws-lambda/google-analytics.js). Edit the function and add your own domains the whitelist for CORS (needed for POST requests depending on how you choose to host these functions.)
16 |
17 |
18 | Proxying to a log management service (Loggly in this case)
19 | ---------
20 | Log management is useful to debug issues in production. During development, everything may look fine to you, but your page is probably exploding somewhere out in the world if you never had logs. There are a multitude amount of various browsers across different platforms and browser versions with varying support for features that make development for all of them difficult. You will write webpages that can have bugs for some browsers, but hopefully you'll catch them. You'll also be surprised when you see how many people are still using old and unsupported browsers/versions that may not support some features you're dependent on.
21 |
22 | The example lambda function for this is [aws-lambda/track.js](aws-lambda/track.js). In it, you'll see I'm proxying to a free log management service called [Loggly](https://www.loggly.com). Loggly is similar to splunk, but you may choose to change this to whatever service you want. Basically, use simple-tracker to send any and all useful data to this proxy like pageviews, events, errors, and other messages to help you debug things in production. Sign up at Loggly, and edit the function to put in your Loggly Key. Don't forget to whitelist your domains in there for CORS.
23 |
24 |
25 | Development setup
26 | ---------
27 | If you want to modify these examples, you can test your code locally.
28 |
29 | First install the dependencies and then run them
30 | ```shell
31 | npm install
32 | npm run serve
33 | ```
34 |
35 | This will serve your lambda functions. You can find each at
36 | `http://localhost:9000/google-analytics`
37 | `http://localhost:9000/track`
38 |
39 | Use simple-tracker to point to either of those endpoints and test away. Depending on where you send your requests from, you may need to modify the lambda functions to allow all cross origin request using`'*'`. You'll see a comment in the code for this.
40 |
41 | Assuming you continue to use netlify, your endpoint paths in production will be `/.netlify/functions/track` and `/.netlify/functions/google-analytics`
42 |
--------------------------------------------------------------------------------
/examples/server-examples/aws-lambda/google-analytics.js:
--------------------------------------------------------------------------------
1 | const request = require('request')
2 | const querystring = require('querystring')
3 | const uuidv4 = require('uuid/v4')
4 |
5 | const GA_ENDPOINT = `https://www.google-analytics.com/collect`
6 |
7 | // Domains to whitelist. Replace with your own!
8 | const originWhitelist = [] // keep this empty and append domains to whitelist using whiteListDomain()
9 | whitelistDomain('test.com')
10 | whitelistDomain('nfeld.com')
11 |
12 |
13 | function whitelistDomain(domain, addWww = true) {
14 | const prefixes = [
15 | 'https://',
16 | 'http://',
17 | ]
18 | if (addWww) {
19 | prefixes.push('https://www.')
20 | prefixes.push('http://www.')
21 | }
22 | prefixes.forEach(prefix => originWhitelist.push(prefix + domain))
23 | }
24 |
25 |
26 | function proxyToGoogleAnalytics(event, done) {
27 | // get GA params whether GET or POST request
28 | const params = event.httpMethod.toUpperCase() === 'GET' ? event.queryStringParameters : JSON.parse(event.body)
29 | const headers = event.headers || {}
30 |
31 | // attach other GA params, required for IP address since client doesn't have access to it. UA and CID can be sent from client
32 | params.uip = headers['x-forwarded-for'] || headers['x-bb-ip'] || '' // ip override. Look into headers for clients IP address, as opposed to IP address of host running lambda function
33 | params.ua = params.ua || headers['user-agent'] || '' // user agent override
34 | params.cid = params.cid || uuidv4() // REQUIRED: use given cid, or generate a new one as last resort. Generating should be avoided because one user can show up in GA multiple times. If user refresh page `n` times, you'll get `n` pageviews logged into GA from "different" users. Client should generate a uuid and store in cookies, local storage, or generate a fingerprint. Check simple-tracker client example
35 |
36 | console.info('proxying params:', params)
37 | const qs = querystring.stringify(params)
38 |
39 | const reqOptions = {
40 | method: 'POST',
41 | headers: {
42 | 'Content-Type': 'image/gif',
43 | },
44 | url: GA_ENDPOINT,
45 | body: qs,
46 | }
47 |
48 | request(reqOptions, (error, result) => {
49 | if (error) {
50 | console.info('googleanalytics error!', error)
51 | } else {
52 | console.info('googleanalytics status code', result.statusCode, result.statusMessage)
53 | }
54 | })
55 |
56 | done()
57 | }
58 |
59 | exports.handler = function(event, context, callback) {
60 | const origin = event.headers['origin'] || event.headers['Origin'] || ''
61 | console.log(`Received ${event.httpMethod} request from, origin: ${origin}`)
62 |
63 | const isOriginWhitelisted = originWhitelist.indexOf(origin) >= 0
64 | console.info('is whitelisted?', isOriginWhitelisted)
65 |
66 | const headers = {
67 | //'Access-Control-Allow-Origin': '*', // allow all domains to POST. Use for localhost development only
68 | 'Access-Control-Allow-Origin': isOriginWhitelisted ? origin : originWhitelist[0],
69 | 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
70 | 'Access-Control-Allow-Headers': 'Content-Type,Accept',
71 | }
72 |
73 | const done = () => {
74 | callback(null, {
75 | statusCode: 200,
76 | headers,
77 | body: '',
78 | })
79 | }
80 |
81 | const httpMethod = event.httpMethod.toUpperCase()
82 |
83 | if (event.httpMethod === 'OPTIONS') { // CORS (required if you use a different subdomain to host this function, or a different domain entirely)
84 | done()
85 | } else if ((httpMethod === 'GET' || httpMethod === 'POST') && isOriginWhitelisted) { // allow GET or POST, but only for whitelisted domains
86 | proxyToGoogleAnalytics(event, done)
87 | } else {
88 | callback('Not found')
89 | }
90 | }
91 |
92 | /*
93 | Docs on GA endpoint and example params
94 |
95 | https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide
96 |
97 | v: 1
98 | _v: j67
99 | a: 751874410
100 | t: pageview
101 | _s: 1
102 | dl: https://nfeld.com/contact.html
103 | dr: https://google.com
104 | ul: en-us
105 | de: UTF-8
106 | dt: Nikolay Feldman - Software Engineer
107 | sd: 24-bit
108 | sr: 1440x900
109 | vp: 945x777
110 | je: 0
111 | _u: blabla~
112 | jid:
113 | gjid:
114 | cid: 1837873423.1522911810
115 | tid: UA-116530991-1
116 | _gid: 1828045325.1524815793
117 | gtm: u4d
118 | z: 1379041260
119 | */
120 |
--------------------------------------------------------------------------------
/examples/server-examples/aws-lambda/track.js:
--------------------------------------------------------------------------------
1 | const request = require('request')
2 |
3 | const LOGGLY_KEY = '' // replace with your loggly key
4 | const TAG = 'simple-track' // can replace with a tag of your choice
5 | const ENDPOINT = `http://logs-01.loggly.com/inputs/${LOGGLY_KEY}/tag/${TAG}/`
6 |
7 | // Domains to whitelist. Replace with your own!
8 | const originWhitelist = [] // keep this empty and append domains to whitelist using whiteListDomain()
9 | whitelistDomain('test.com')
10 | whitelistDomain('nfeld.com')
11 |
12 |
13 | function whitelistDomain(domain, addWww = true) {
14 | const prefixes = [
15 | 'https://',
16 | 'http://',
17 | ]
18 | if (addWww) {
19 | prefixes.push('https://www.')
20 | prefixes.push('http://www.')
21 | }
22 | prefixes.forEach(prefix => originWhitelist.push(prefix + domain))
23 | }
24 |
25 |
26 | function track(event, done) {
27 | // event.queryStringParameters for querystring params already parsed into object
28 | const trackerData = JSON.parse(event.body) // data from simple-tracker request
29 | const headers = event.headers || {}
30 | const ip = headers['x-forwarded-for'] || headers['x-bb-ip'] || '' // ip address of user incase you want to append to trackerData. I do it below.
31 |
32 | console.info('tracker payload:', event.body)
33 | console.info('ip:', ip)
34 |
35 | // attach ip to context
36 | if (trackerData && trackerData.context) {
37 | trackerData.context.ip = ip
38 | }
39 |
40 | const reqOptions = {
41 | method: 'POST',
42 | headers: {
43 | 'Content-Type': 'application/json',
44 | },
45 | json: true,
46 | url: ENDPOINT,
47 | body: trackerData,
48 | }
49 |
50 | request(reqOptions, (error, result) => {
51 | if (error) {
52 | console.info('loggly error!', error)
53 | } else {
54 | console.info('result from loggly:', result.statusCode, result.statusMessage)
55 | }
56 | })
57 |
58 | done()
59 | }
60 |
61 |
62 | exports.handler = function(event, context, callback) {
63 | const origin = event.headers['origin'] || event.headers['Origin'] || ''
64 | console.log(`Received ${event.httpMethod} request from, origin: ${origin}`)
65 |
66 | const isOriginWhitelisted = originWhitelist.indexOf(origin) >= 0
67 | console.info('is whitelisted?', isOriginWhitelisted)
68 |
69 | const headers = {
70 | //'Access-Control-Allow-Origin': '*', // allow all domains to POST. Use for localhost development only
71 | 'Access-Control-Allow-Origin': isOriginWhitelisted ? origin : originWhitelist[0],
72 | 'Access-Control-Allow-Methods': 'POST,OPTIONS',
73 | 'Access-Control-Allow-Headers': 'Content-Type,Accept',
74 | }
75 |
76 | const done = () => {
77 | callback(null, {
78 | statusCode: 200,
79 | headers,
80 | body: '',
81 | })
82 | }
83 |
84 | if (event.httpMethod === 'OPTIONS') { // CORS (required if you use a different subdomain to host this function, or a different domain entirely)
85 | done()
86 | } else if (event.httpMethod !== 'POST' || !isOriginWhitelisted) { // allow POST request from whitelisted domains
87 | callback('Not found')
88 | } else {
89 | track(event, done)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/examples/server-examples/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | Command = "npm run build"
3 | Functions = "build"
4 | Publish = "build"
5 |
--------------------------------------------------------------------------------
/examples/server-examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google-analytics-aws-proxy",
3 | "version": "0.1.0",
4 | "author": "Nikolay Feldman",
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:codeniko/simple-tracker.git"
8 | },
9 | "private": true,
10 | "scripts": {
11 | "build": "netlify-lambda build aws-lambda",
12 | "serve": "netlify-lambda serve aws-lambda",
13 | "clean": "rm -rf ./npm-debug.log ./build"
14 | },
15 | "dependencies": {
16 | "querystring": "^0.2.0",
17 | "request": "^2.85.0",
18 | "uuid": "^3.2.1"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.0.0-beta.44",
22 | "netlify-lambda": "^1.0.0-babel-7-beta"
23 | },
24 | "proxy": {
25 | "/.netlify/functions": {
26 | "target": "http://localhost:9000",
27 | "pathRewrite": {
28 | "^/\\.netlify/functions": ""
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /*! simple-tracker | MIT *
2 | * https://github.com/codeniko/simple-tracker !*/
3 |
4 | (function() {
5 | 'use strict'
6 |
7 | function simpleTracker(window) {
8 | var SESSION_KEY = 'trcksesh'
9 | var SESSION_KEY_LENGTH = SESSION_KEY.length + 1
10 |
11 | var document = window.document
12 | var sendCaughtExceptions = false
13 | var attachClientContext = true
14 | var attachSessionId = true
15 | var devMode = false
16 | var endpoint
17 | var sessionId
18 | var tracker
19 | var timer = {}
20 |
21 | function uuidv4(a) {
22 | return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuidv4)
23 | }
24 |
25 | // override to call own onerror function, followed by original onerror
26 | function setOnError(f) {
27 | var _onerror = window.onerror
28 | // msg, url, line, col, err
29 | window.onerror = function() {
30 | f.apply(tracker, arguments)
31 | if (typeof _onerror === 'function') {
32 | _onerror.apply(window, arguments)
33 | }
34 | }
35 | }
36 |
37 | function getClientContext() {
38 | return {
39 | url: window.location.href,
40 | userAgent: window.navigator.userAgent || null,
41 | platform: window.navigator.platform || null
42 | }
43 | }
44 |
45 | function readCookie() {
46 | var cookie = document.cookie
47 | var i = cookie.indexOf(SESSION_KEY)
48 | if (i >= 0) {
49 | var end = cookie.indexOf(';', i + 1)
50 | end = end < 0 ? cookie.length : end
51 | return cookie.slice(i + SESSION_KEY_LENGTH, end)
52 | }
53 | }
54 |
55 | function setCookie(value) {
56 | document.cookie = SESSION_KEY + '=' + value
57 | }
58 |
59 | function setSession(newSessionId) {
60 | sessionId = newSessionId || sessionId || readCookie() || uuidv4()
61 | setCookie(sessionId)
62 | }
63 |
64 | function track(data) {
65 | if (endpoint && Object.keys(data).length > 0) {
66 | if (attachSessionId) {
67 | data.sessionId = sessionId
68 | }
69 | if (attachClientContext) {
70 | data.context = tracker.clientContext
71 | }
72 |
73 | if (!devMode) {
74 | try {
75 | // let's not use fetch to avoid a polyfill
76 | var xmlHttp = new window.XMLHttpRequest()
77 | xmlHttp.open('POST', endpoint, true) // true for async
78 | xmlHttp.setRequestHeader('Content-Type', 'application/json')
79 | xmlHttp.send(JSON.stringify(data))
80 | } catch(ex) { }
81 | } else {
82 | console.debug('SimpleTracker: POST ' + endpoint, data)
83 | }
84 | }
85 | }
86 |
87 | function SimpleTracker() {
88 | this.clientContext = getClientContext()
89 | }
90 |
91 | SimpleTracker.prototype = {
92 |
93 | // accessible to those have this tracker object so they can create their own onerror functions and still able to invoke default onerror behavior for our tracker.
94 | onerror: function(msg, url, line, col, err) {
95 | var exception = {
96 | message: msg,
97 | lineno: line,
98 | colno: col,
99 | stack: err ? err.stack : 'n/a'
100 | }
101 |
102 | this.logException(exception)
103 | },
104 |
105 | logEvent: function(event, additionalParams) {
106 | var data = {
107 | type: 'event',
108 | event: event
109 | }
110 |
111 | // if additional params defined, copy them over
112 | if (typeof additionalParams === 'object') {
113 | for (var prop in additionalParams) {
114 | if (additionalParams.hasOwnProperty(prop)) {
115 | data[prop] = additionalParams[prop]
116 | }
117 | }
118 | }
119 |
120 | this.push(data)
121 | },
122 |
123 | logException: function(exception) {
124 | this.push({
125 | level: 'error',
126 | type: 'exception',
127 | exception: exception
128 | })
129 | },
130 |
131 | logMessage: function(message, level) {
132 | var data = {
133 | type: 'message',
134 | message: message
135 | }
136 |
137 | // add optional level if defined, not included otherwise
138 | if (level) {
139 | data.level = level
140 | }
141 |
142 | this.push(data)
143 | },
144 |
145 | logMetric: function(metric, value) {
146 | this.push({
147 | type: 'metric',
148 | metric: metric,
149 | value: value
150 | })
151 | },
152 |
153 | startTimer: function(metric) {
154 | var performance = window.performance
155 | if (performance.now) {
156 | /* istanbul ignore if */
157 | if (timer[metric] && devMode) {
158 | console.warn("Timing metric '" + metric + "' already started")
159 | }
160 | devMode && console.debug('timer started for:', metric)
161 | timer[metric] = performance.now()
162 | }
163 | },
164 |
165 | stopTimer: function(metric) {
166 | var performance = window.performance
167 | if (performance.now) {
168 | var stopTime = performance.now()
169 | var startTime = timer[metric]
170 | /* istanbul ignore else */
171 | if (startTime !== undefined) {
172 | var diff = Math.round(stopTime - startTime)
173 | timer[metric] = undefined
174 | devMode && console.debug('timer stopped for:', metric, 'time=' + diff)
175 | this.logMetric(metric, diff)
176 | } else {
177 | devMode && console.warn("Timing metric '" + metric + "' wasn't started")
178 | }
179 | }
180 | },
181 |
182 | push: function(data) {
183 | var type = typeof data
184 |
185 | if (type !== 'object' && type !== 'string') {
186 | return
187 | }
188 |
189 | if (type === 'string') {
190 | data = {
191 | text: data
192 | }
193 | } else {
194 | // toggle devmode, where requests wont be sent, but logged in console for debugging instead
195 | if (data.devMode !== undefined) {
196 | devMode = !!data.devMode
197 | delete data.devMode
198 | }
199 |
200 | if (data.attachClientContext !== undefined) {
201 | attachClientContext = !!data.attachClientContext
202 | delete data.attachClientContext
203 | }
204 |
205 | if (data.attachSessionId !== undefined) {
206 | attachSessionId = !!data.attachSessionId
207 | delete data.attachSessionId
208 | }
209 |
210 | if (data.sessionId) {
211 | setSession(data.sessionId)
212 | delete data.sessionId
213 | }
214 |
215 | if (data.endpoint) {
216 | // other initializations should go here since endpoint is the only required property that needs to be set
217 | if (!sessionId) {
218 | setSession()
219 | }
220 | endpoint = data.endpoint
221 | delete data.endpoint
222 | }
223 |
224 | if (data.sendCaughtExceptions !== undefined) {
225 | sendCaughtExceptions = !!data.sendCaughtExceptions
226 | if (sendCaughtExceptions) {
227 | setOnError(this.onerror)
228 | }
229 | delete data.sendCaughtExceptions
230 | }
231 | }
232 |
233 | track(data)
234 | }
235 | }
236 |
237 | var existingTracker = window.tracker // either instance of SimpleTracker or existing array of events to log that were added while this script was being loaded asyncronously
238 |
239 | if (existingTracker && existingTracker.length) {
240 | // move all from existing and push into our tracker object
241 | tracker = new SimpleTracker()
242 | var i = 0
243 | var length = existingTracker.length
244 | for (i = 0; i < length; i++) {
245 | tracker.push(existingTracker[i])
246 | }
247 | } else {
248 | tracker = new SimpleTracker()
249 | }
250 |
251 | window.tracker = tracker
252 | window.SimpleTracker = SimpleTracker
253 | return tracker
254 | }
255 |
256 | var isModule = typeof module !== 'undefined' && module.exports
257 | /* istanbul ignore else */
258 | if (typeof window !== 'undefined') {
259 | var tracker = simpleTracker(window) // sets window.tracker
260 | if (isModule) {
261 | simpleTracker.default = tracker
262 | module.exports = tracker
263 | }
264 | } else if (isModule) {
265 | // for testing
266 | simpleTracker.default = simpleTracker
267 | module.exports = simpleTracker
268 | } else {
269 | throw new Error('')
270 | }
271 | })()
272 |
273 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple-tracker",
3 | "version": "1.2.3",
4 | "description": "Easy client-side tracking library to log events, metrics, errors, and messages",
5 | "main": "dist/simple-tracker.min.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/codeniko/simple-tracker.git"
9 | },
10 | "scripts": {
11 | "build": "grunt",
12 | "unit-test": "ENV=tests ./node_modules/.bin/_mocha -R spec ./test/unit/**/*.js ./test/unit/**/**/*.js",
13 | "test": "nyc --reporter=html --reporter=text npm run unit-test",
14 | "preversion": "npm test",
15 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov"
16 | },
17 | "author": "Nikolay Feldman",
18 | "keywords": [
19 | "simple tracker",
20 | "tracker",
21 | "track",
22 | "instrumentation",
23 | "i13n",
24 | "analytics",
25 | "log management",
26 | "logger",
27 | "jslogger",
28 | "log"
29 | ],
30 | "license": "MIT",
31 | "devDependencies": {
32 | "bower": "^1.8.0",
33 | "chai": "^4.1.2",
34 | "codecov": "^3.0.1",
35 | "grunt": "^1.0.1",
36 | "grunt-contrib-uglify": "^3.0.1",
37 | "mocha": "^5.1.1",
38 | "mocha-lcov-reporter": "^1.3.0",
39 | "nyc": "^11.7.1",
40 | "rewire": "^4.0.0",
41 | "sinon": "^4.5.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/test/unit/simple-tracker.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, beforeEach */
2 |
3 | const assert = require('chai').assert
4 | const sinon = require('sinon')
5 | const rewire = require('rewire')
6 |
7 | const simpleTracker = require('../../index.js')
8 |
9 | function silenceConsoleLogs() {
10 | // dont silence console.log as that will hide test results
11 | console.info = () => {}
12 | console.error = () => {}
13 | console.warn = () => {}
14 | console.debug = () => {}
15 | }
16 |
17 | describe('simple-tracker', function() {
18 | silenceConsoleLogs()
19 |
20 | let window, document, tracker, mockRequest
21 | const mockEndpoint = '**ENDPOINT**'
22 | const mockSessionId = '**SESSION_ID**'
23 | const mockData1 = '**DATA1**'
24 | const mockData2 = '**DATA2**'
25 | const mockHref = '**HREF**'
26 | const mockUserAgent = '**USER_AGENT**'
27 | const mockPlatform = '**PLATFORM**'
28 | const mockCookieValue = 'MOCK_COOKIE'
29 | const cookieKey = 'trcksesh'
30 | const mockCookies = `${cookieKey}=${mockCookieValue}`
31 |
32 | // mock XMLHttpRequest. Its prototype functions will be stubbed in reset() with createStubInstance
33 | function MockXMLHttpRequest() {}
34 | MockXMLHttpRequest.prototype = {
35 | open: function() {},
36 | setRequestHeader: function() {},
37 | send: function() {},
38 | }
39 |
40 | let onerrorSpy
41 |
42 | function reset() {
43 | onerrorSpy = sinon.spy()
44 | document = {
45 | cookie: '',
46 | }
47 | window = {
48 | document,
49 | XMLHttpRequest: MockXMLHttpRequest,
50 | location: {
51 | href: mockHref,
52 | },
53 | navigator: {
54 | userAgent: mockUserAgent,
55 | platform: mockPlatform,
56 | },
57 | onerror: onerrorSpy,
58 | performance: {
59 | now: sinon.stub(),
60 | }
61 | }
62 | simpleTracker(window) // sets tracker to window object
63 | mockRequest = sinon.createStubInstance(MockXMLHttpRequest)
64 | sinon.stub(window, 'XMLHttpRequest').returns(mockRequest)
65 | tracker = window.tracker
66 | }
67 |
68 | function assertSentRequest(expectedEndpoint, expectedData, requestIndex) {
69 | if (mockRequest.open.callCount === 0) {
70 | assert(false, 'No outgoing request made')
71 | return
72 | }
73 |
74 | const lastRequestIndex = mockRequest.open.callCount - 1
75 | const callIndex = requestIndex !== undefined ? requestIndex : lastRequestIndex // ternary op because requestIndex could be 0, which would then use last invocation
76 | const openSpy = mockRequest.open.getCall(callIndex) // assert specific request or assert last invocation.
77 | assert.equal(openSpy.args[0], 'POST')
78 | assert.equal(openSpy.args[1], expectedEndpoint)
79 | assert.isTrue(openSpy.args[2])
80 |
81 | const sendSpy = mockRequest.send.getCall(callIndex)
82 | assert.deepEqual(JSON.parse(sendSpy.args[0]), expectedData)
83 |
84 | assert.isTrue(mockRequest.setRequestHeader.getCall(callIndex).calledWith('Content-Type', 'application/json'))
85 | }
86 |
87 | beforeEach(function() {
88 | reset()
89 | })
90 |
91 |
92 | it('Initialized correctly', function(done) {
93 | assert.isFunction(tracker.onerror)
94 | done()
95 | })
96 |
97 | it('should not send request if no data to send', function(done) {
98 | tracker.push({
99 | endpoint: mockEndpoint,
100 | sendCaughtExceptions: true,
101 | sessionId: mockSessionId,
102 | })
103 |
104 | assert.isTrue(mockRequest.open.notCalled)
105 | assert.isTrue(mockRequest.send.notCalled)
106 | done()
107 | })
108 |
109 | it('should send request if there is data to send', function(done) {
110 | tracker.push({
111 | endpoint: mockEndpoint,
112 | sendCaughtExceptions: true,
113 | sessionId: mockSessionId,
114 | attachClientContext: false,
115 | mockData1,
116 | })
117 |
118 | assert.isTrue(mockRequest.open.calledOnce)
119 | assert.isTrue(mockRequest.send.calledOnce)
120 | assertSentRequest(mockEndpoint, {
121 | mockData1,
122 | sessionId: mockSessionId,
123 | })
124 | done()
125 | })
126 |
127 | it('should send request for each push', function(done) {
128 | // first push
129 | tracker.push({
130 | endpoint: mockEndpoint,
131 | sessionId: mockSessionId,
132 | attachClientContext: false,
133 | mockData1,
134 | })
135 |
136 | assert.isTrue(mockRequest.open.calledOnce)
137 | assert.isTrue(mockRequest.send.calledOnce)
138 | assertSentRequest(mockEndpoint, {
139 | mockData1,
140 | sessionId: mockSessionId,
141 | })
142 |
143 | // second push
144 | tracker.push({
145 | mockData2,
146 | sessionId: mockSessionId,
147 | })
148 |
149 | assert.isTrue(mockRequest.open.calledTwice)
150 | assert.isTrue(mockRequest.send.calledTwice)
151 | assertSentRequest(mockEndpoint, {
152 | mockData2,
153 | sessionId: mockSessionId,
154 | })
155 | done()
156 | })
157 |
158 | it('should set and read sessionId to/from cookies', function(done) {
159 | document.cookie = mockCookies
160 | assert.equal(document.cookie, mockCookies)
161 |
162 | // first push sets endpoint, which triggers set session and will read cookie
163 | tracker.push({
164 | endpoint: mockEndpoint,
165 | mockData1,
166 | attachClientContext: false,
167 | })
168 |
169 | assert.equal(document.cookie, mockCookies) // should remain unchanged
170 | assertSentRequest(mockEndpoint, {
171 | mockData1,
172 | sessionId: mockCookieValue, // sessionId sent containing value from cookie
173 | })
174 |
175 | // second push sets session id, which will set new cookie
176 | tracker.push({
177 | sessionId: mockSessionId,
178 | mockData2,
179 | })
180 |
181 | assert.equal(document.cookie, `trcksesh=${mockSessionId}`) // cookie should change
182 | assertSentRequest(mockEndpoint, {
183 | mockData2,
184 | sessionId: mockSessionId, // new sessionId sent
185 | })
186 | done()
187 | })
188 |
189 | it('should generate new sessionId if one does not exist, and store in cookie', function(done) {
190 | assert.equal(document.cookie, '')
191 |
192 | tracker.push({
193 | endpoint: mockEndpoint,
194 | mockData1,
195 | attachClientContext: false,
196 | })
197 |
198 | assert.notEqual(document.cookie, '') // should change
199 | // get newly generated sessionId from cookie
200 | const splitCookie = document.cookie.split('=')
201 | const newSessionId = splitCookie[1]
202 | assert.equal(splitCookie[0], cookieKey)
203 | assert.notEqual(newSessionId, '')
204 | assertSentRequest(mockEndpoint, {
205 | mockData1,
206 | sessionId: newSessionId,
207 | })
208 | done()
209 | })
210 |
211 | it('should honor attachSessionId flag', function(done) {
212 | // should send session Id
213 | tracker.push({
214 | endpoint: mockEndpoint,
215 | attachClientContext: false,
216 | attachSessionId: true,
217 | sessionId: mockSessionId,
218 | mockData1,
219 | })
220 | assertSentRequest(mockEndpoint, {
221 | mockData1,
222 | sessionId: mockSessionId,
223 | })
224 |
225 | // should not send session id
226 | tracker.push({
227 | attachSessionId: false,
228 | mockData2,
229 | })
230 | assertSentRequest(mockEndpoint, {
231 | mockData2,
232 | })
233 | done()
234 | })
235 |
236 | it('should accept data of type "text"', function(done) {
237 | tracker.push({
238 | endpoint: mockEndpoint,
239 | sessionId: mockSessionId,
240 | attachClientContext: false,
241 | })
242 | tracker.push(mockData1) // push a string
243 |
244 | assert.isTrue(mockRequest.open.calledOnce)
245 | assert.isTrue(mockRequest.send.calledOnce)
246 | assertSentRequest(mockEndpoint, {
247 | text: mockData1,
248 | sessionId: mockSessionId,
249 | })
250 | done()
251 | })
252 |
253 | it('should track exceptions if enabled', function(done) {
254 | tracker.push({
255 | endpoint: mockEndpoint,
256 | sessionId: mockSessionId,
257 | sendCaughtExceptions: true,
258 | attachClientContext: false,
259 | })
260 | const mockError = Error(mockData2)
261 |
262 | window.onerror(mockData1, mockEndpoint, 1, 1, mockError)
263 |
264 | assert.isTrue(onerrorSpy.calledWith(mockData1, mockEndpoint, 1, 1, mockError))
265 | assert.isTrue(mockRequest.open.calledOnce)
266 | assert.isTrue(mockRequest.send.calledOnce)
267 | assertSentRequest(mockEndpoint, {
268 | type: 'exception',
269 | level: 'error',
270 | exception: {
271 | colno: 1,
272 | lineno: 1,
273 | message: mockData1,
274 | stack: mockError.stack,
275 | },
276 | sessionId: mockSessionId,
277 | })
278 | done()
279 | })
280 |
281 | it('should not track exceptions if disabled', function(done) {
282 | tracker.push({
283 | endpoint: mockEndpoint,
284 | sessionId: mockSessionId,
285 | sendCaughtExceptions: false,
286 | attachClientContext: false,
287 | })
288 | const mockError = Error(mockData2)
289 |
290 | window.onerror(mockData1, mockEndpoint, 1, 1, mockError)
291 |
292 | assert.isTrue(onerrorSpy.calledWith(mockData1, mockEndpoint, 1, 1, mockError))
293 | assert.isTrue(mockRequest.open.notCalled)
294 | assert.isTrue(mockRequest.send.notCalled)
295 | done()
296 | })
297 |
298 | it('should send client context', function(done) {
299 | tracker.push({
300 | endpoint: mockEndpoint,
301 | sessionId: mockSessionId,
302 | attachClientContext: true,
303 | mockData1,
304 | mockData2,
305 | })
306 |
307 | assertSentRequest(mockEndpoint, {
308 | mockData1,
309 | mockData2,
310 | sessionId: mockSessionId,
311 | context: {
312 | platform: mockPlatform,
313 | url: mockHref,
314 | userAgent: mockUserAgent,
315 | },
316 | })
317 | done()
318 | })
319 |
320 | it('should persist additional client context values', function(done) {
321 | tracker.push({
322 | endpoint: mockEndpoint,
323 | sessionId: mockSessionId,
324 | attachClientContext: true,
325 | })
326 |
327 | // assign additional values to client object
328 | tracker.clientContext.mockData2 = mockData2
329 | tracker.push({ mockData1 })
330 |
331 | assertSentRequest(mockEndpoint, {
332 | mockData1,
333 | sessionId: mockSessionId,
334 | context: {
335 | platform: mockPlatform,
336 | url: mockHref,
337 | userAgent: mockUserAgent,
338 | mockData2,
339 | },
340 | })
341 |
342 | // overwrite client object
343 | tracker.clientContext = { mockData1, mockData2 }
344 | tracker.push({ mockData1 })
345 | assertSentRequest(mockEndpoint, {
346 | mockData1,
347 | sessionId: mockSessionId,
348 | context: {
349 | mockData1,
350 | mockData2,
351 | },
352 | })
353 | done()
354 | })
355 |
356 | it('should send data that was pushed prior to loading tracker', function(done) {
357 | const initialTracker = []
358 | window.tracker = initialTracker
359 |
360 | // tracker is not yet loaded, 3 pushes: one config, 2 data
361 | initialTracker.push({
362 | endpoint: mockEndpoint,
363 | sessionId: mockSessionId,
364 | attachClientContext: false,
365 | })
366 | initialTracker.push({ mockData1 })
367 | initialTracker.push({ mockData2, mockCookieValue })
368 |
369 | assert.isTrue(mockRequest.open.notCalled)
370 | assert.isTrue(mockRequest.send.notCalled)
371 |
372 | // let's load our tracker
373 | simpleTracker(window)
374 |
375 | assert.isTrue(mockRequest.open.calledTwice)
376 | assert.isTrue(mockRequest.send.calledTwice)
377 |
378 | assertSentRequest(mockEndpoint, {
379 | mockData1,
380 | sessionId: mockSessionId,
381 | }, 0)
382 | assertSentRequest(mockEndpoint, {
383 | mockData2,
384 | mockCookieValue,
385 | sessionId: mockSessionId,
386 | })
387 | done()
388 | })
389 |
390 | it('should log events and events with params', function(done) {
391 | tracker.push({
392 | endpoint: mockEndpoint,
393 | sessionId: mockSessionId,
394 | attachClientContext: false,
395 | })
396 |
397 | // log event without additional params
398 | tracker.logEvent(mockData1)
399 |
400 | assertSentRequest(mockEndpoint, {
401 | type: 'event',
402 | event: mockData1,
403 | sessionId: mockSessionId,
404 | })
405 |
406 | // log event with additional params
407 | tracker.logEvent(mockData1, { mockData2 })
408 |
409 | assertSentRequest(mockEndpoint, {
410 | type: 'event',
411 | event: mockData1,
412 | sessionId: mockSessionId,
413 | mockData2,
414 | })
415 | done()
416 | })
417 |
418 | it('should log message', function(done) {
419 | tracker.push({
420 | endpoint: mockEndpoint,
421 | sessionId: mockSessionId,
422 | attachClientContext: false,
423 | })
424 | const level = 'info'
425 | tracker.logMessage(mockData1, level)
426 |
427 | assertSentRequest(mockEndpoint, {
428 | level,
429 | type: 'message',
430 | message: mockData1,
431 | sessionId: mockSessionId,
432 | })
433 | done()
434 | })
435 |
436 | it('should log metric', function(done) {
437 | tracker.push({
438 | endpoint: mockEndpoint,
439 | sessionId: mockSessionId,
440 | attachClientContext: false,
441 | })
442 | tracker.logMetric(mockData1, mockData2)
443 |
444 | assertSentRequest(mockEndpoint, {
445 | type: 'metric',
446 | metric: mockData1,
447 | value: mockData2,
448 | sessionId: mockSessionId,
449 | })
450 | done()
451 | })
452 |
453 | it('should log timing metric', function(done) {
454 | const perfStub = window.performance.now
455 | perfStub.onCall(0).returns(1000.000005)
456 | perfStub.onCall(1).returns(5000.700001)
457 |
458 | tracker.push({
459 | endpoint: mockEndpoint,
460 | sessionId: mockSessionId,
461 | attachClientContext: false,
462 | })
463 | tracker.startTimer(mockData1)
464 | tracker.stopTimer(mockData1)
465 |
466 | assertSentRequest(mockEndpoint, {
467 | type: 'metric',
468 | metric: mockData1,
469 | value: 4001,
470 | sessionId: mockSessionId,
471 | })
472 | done()
473 | })
474 |
475 | it('should persist singleton tracker across multiple loads', function(done) {
476 | // 1st load
477 | tracker.push({
478 | endpoint: mockEndpoint,
479 | sessionId: mockSessionId,
480 | attachClientContext: false,
481 | })
482 |
483 | tracker.push({ mockData1 })
484 |
485 | assert.isTrue(mockRequest.open.calledOnce)
486 | assert.isTrue(mockRequest.send.calledOnce)
487 | assertSentRequest(mockEndpoint, {
488 | sessionId: mockSessionId,
489 | mockData1,
490 | })
491 |
492 | // second load, same window obj
493 | simpleTracker(window)
494 | tracker.push({ mockData2 })
495 |
496 | assert.isTrue(mockRequest.open.calledTwice)
497 | assert.isTrue(mockRequest.send.calledTwice)
498 | assertSentRequest(mockEndpoint, {
499 | sessionId: mockSessionId,
500 | mockData2,
501 | })
502 |
503 | done()
504 | })
505 |
506 | it('dont send request out in devMode', function(done) {
507 | tracker.push({
508 | endpoint: mockEndpoint,
509 | sessionId: mockSessionId,
510 | devMode: true,
511 | })
512 |
513 | tracker.push({ mockData1 })
514 |
515 | assert.isTrue(mockRequest.open.notCalled)
516 | assert.isTrue(mockRequest.send.notCalled)
517 |
518 | done()
519 | })
520 |
521 | it('should auto initialize with window object if one exists', function(done) {
522 | delete window.tracker // ensure no instance of tracker loaded
523 | delete window.SimpleTracker
524 | global.window = window
525 |
526 | assert.isUndefined(window.tracker)
527 | assert.isUndefined(window.SimpleTracker)
528 |
529 | // window object is now in global, so should be picked up automatically
530 | const rTracker = rewire('../../index.js') // rewire loads fresh instance of module
531 |
532 | assert.isDefined(window.SimpleTracker)
533 | assert.instanceOf(window.tracker, window.SimpleTracker)
534 | assert.instanceOf(rTracker, window.SimpleTracker)
535 |
536 | delete global.window // cleanup
537 |
538 | done()
539 | })
540 |
541 | it('dont do anything if data pushed is not an object or string', function(done) {
542 | tracker.push(() => {})
543 | assert.isTrue(mockRequest.open.notCalled)
544 | assert.isTrue(mockRequest.send.notCalled)
545 | done()
546 | })
547 |
548 | })
549 |
--------------------------------------------------------------------------------
/version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | TEMP=/tmp/st-temp
4 | PACKAGE=package.json
5 |
6 | function bump() {
7 | local version="$1"
8 | echo "Bumping version to v$version"
9 |
10 | sed "3s/: \".*\"/: \"${version}\"/" "$PACKAGE" > "$TEMP"
11 | mv "$TEMP" "$PACKAGE"
12 |
13 | npm run build
14 |
15 | git add $PACKAGE dist
16 | git commit -m "$version"
17 | git tag "v$version"
18 |
19 | git --no-pager show
20 | echo "Tag v$version ready to be published!"
21 | }
22 |
23 | if [[ $# -eq 1 ]]; then
24 | version="$1"
25 | read -p "Bump to version $version?" yn
26 | case $yn in
27 | [Yy]* ) bump "$version";;
28 | [Nn]* ) exit;;
29 | * ) echo "Please answer yes or no.";;
30 | esac
31 | fi
32 |
--------------------------------------------------------------------------------