UNPKG

simple-tracker

Version:

Easy client-side tracking library to log events, metrics, errors, and messages

549 lines (465 loc) 14.2 kB
/* global describe, it, beforeEach */ const assert = require('chai').assert const sinon = require('sinon') const rewire = require('rewire') const simpleTracker = require('../../index.js') function silenceConsoleLogs() { // dont silence console.log as that will hide test results console.info = () => {} console.error = () => {} console.warn = () => {} console.debug = () => {} } describe('simple-tracker', function() { silenceConsoleLogs() let window, document, tracker, mockRequest const mockEndpoint = '**ENDPOINT**' const mockSessionId = '**SESSION_ID**' const mockData1 = '**DATA1**' const mockData2 = '**DATA2**' const mockHref = '**HREF**' const mockUserAgent = '**USER_AGENT**' const mockPlatform = '**PLATFORM**' const mockCookieValue = 'MOCK_COOKIE' const cookieKey = 'trcksesh' const mockCookies = `${cookieKey}=${mockCookieValue}` // mock XMLHttpRequest. Its prototype functions will be stubbed in reset() with createStubInstance function MockXMLHttpRequest() {} MockXMLHttpRequest.prototype = { open: function() {}, setRequestHeader: function() {}, send: function() {}, } let onerrorSpy function reset() { onerrorSpy = sinon.spy() document = { cookie: '', } window = { document, XMLHttpRequest: MockXMLHttpRequest, location: { href: mockHref, }, navigator: { userAgent: mockUserAgent, platform: mockPlatform, }, onerror: onerrorSpy, performance: { now: sinon.stub(), } } simpleTracker(window) // sets tracker to window object mockRequest = sinon.createStubInstance(MockXMLHttpRequest) sinon.stub(window, 'XMLHttpRequest').returns(mockRequest) tracker = window.tracker } function assertSentRequest(expectedEndpoint, expectedData, requestIndex) { if (mockRequest.open.callCount === 0) { assert(false, 'No outgoing request made') return } const lastRequestIndex = mockRequest.open.callCount - 1 const callIndex = requestIndex !== undefined ? requestIndex : lastRequestIndex // ternary op because requestIndex could be 0, which would then use last invocation const openSpy = mockRequest.open.getCall(callIndex) // assert specific request or assert last invocation. assert.equal(openSpy.args[0], 'POST') assert.equal(openSpy.args[1], expectedEndpoint) assert.isTrue(openSpy.args[2]) const sendSpy = mockRequest.send.getCall(callIndex) assert.deepEqual(JSON.parse(sendSpy.args[0]), expectedData) assert.isTrue(mockRequest.setRequestHeader.getCall(callIndex).calledWith('Content-Type', 'application/json')) } beforeEach(function() { reset() }) it('Initialized correctly', function(done) { assert.isFunction(tracker.onerror) done() }) it('should not send request if no data to send', function(done) { tracker.push({ endpoint: mockEndpoint, sendCaughtExceptions: true, sessionId: mockSessionId, }) assert.isTrue(mockRequest.open.notCalled) assert.isTrue(mockRequest.send.notCalled) done() }) it('should send request if there is data to send', function(done) { tracker.push({ endpoint: mockEndpoint, sendCaughtExceptions: true, sessionId: mockSessionId, attachClientContext: false, mockData1, }) assert.isTrue(mockRequest.open.calledOnce) assert.isTrue(mockRequest.send.calledOnce) assertSentRequest(mockEndpoint, { mockData1, sessionId: mockSessionId, }) done() }) it('should send request for each push', function(done) { // first push tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, mockData1, }) assert.isTrue(mockRequest.open.calledOnce) assert.isTrue(mockRequest.send.calledOnce) assertSentRequest(mockEndpoint, { mockData1, sessionId: mockSessionId, }) // second push tracker.push({ mockData2, sessionId: mockSessionId, }) assert.isTrue(mockRequest.open.calledTwice) assert.isTrue(mockRequest.send.calledTwice) assertSentRequest(mockEndpoint, { mockData2, sessionId: mockSessionId, }) done() }) it('should set and read sessionId to/from cookies', function(done) { document.cookie = mockCookies assert.equal(document.cookie, mockCookies) // first push sets endpoint, which triggers set session and will read cookie tracker.push({ endpoint: mockEndpoint, mockData1, attachClientContext: false, }) assert.equal(document.cookie, mockCookies) // should remain unchanged assertSentRequest(mockEndpoint, { mockData1, sessionId: mockCookieValue, // sessionId sent containing value from cookie }) // second push sets session id, which will set new cookie tracker.push({ sessionId: mockSessionId, mockData2, }) assert.equal(document.cookie, `trcksesh=${mockSessionId}`) // cookie should change assertSentRequest(mockEndpoint, { mockData2, sessionId: mockSessionId, // new sessionId sent }) done() }) it('should generate new sessionId if one does not exist, and store in cookie', function(done) { assert.equal(document.cookie, '') tracker.push({ endpoint: mockEndpoint, mockData1, attachClientContext: false, }) assert.notEqual(document.cookie, '') // should change // get newly generated sessionId from cookie const splitCookie = document.cookie.split('=') const newSessionId = splitCookie[1] assert.equal(splitCookie[0], cookieKey) assert.notEqual(newSessionId, '') assertSentRequest(mockEndpoint, { mockData1, sessionId: newSessionId, }) done() }) it('should honor attachSessionId flag', function(done) { // should send session Id tracker.push({ endpoint: mockEndpoint, attachClientContext: false, attachSessionId: true, sessionId: mockSessionId, mockData1, }) assertSentRequest(mockEndpoint, { mockData1, sessionId: mockSessionId, }) // should not send session id tracker.push({ attachSessionId: false, mockData2, }) assertSentRequest(mockEndpoint, { mockData2, }) done() }) it('should accept data of type "text"', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) tracker.push(mockData1) // push a string assert.isTrue(mockRequest.open.calledOnce) assert.isTrue(mockRequest.send.calledOnce) assertSentRequest(mockEndpoint, { text: mockData1, sessionId: mockSessionId, }) done() }) it('should track exceptions if enabled', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, sendCaughtExceptions: true, attachClientContext: false, }) const mockError = Error(mockData2) window.onerror(mockData1, mockEndpoint, 1, 1, mockError) assert.isTrue(onerrorSpy.calledWith(mockData1, mockEndpoint, 1, 1, mockError)) assert.isTrue(mockRequest.open.calledOnce) assert.isTrue(mockRequest.send.calledOnce) assertSentRequest(mockEndpoint, { type: 'exception', level: 'error', exception: { colno: 1, lineno: 1, message: mockData1, stack: mockError.stack, }, sessionId: mockSessionId, }) done() }) it('should not track exceptions if disabled', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, sendCaughtExceptions: false, attachClientContext: false, }) const mockError = Error(mockData2) window.onerror(mockData1, mockEndpoint, 1, 1, mockError) assert.isTrue(onerrorSpy.calledWith(mockData1, mockEndpoint, 1, 1, mockError)) assert.isTrue(mockRequest.open.notCalled) assert.isTrue(mockRequest.send.notCalled) done() }) it('should send client context', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: true, mockData1, mockData2, }) assertSentRequest(mockEndpoint, { mockData1, mockData2, sessionId: mockSessionId, context: { platform: mockPlatform, url: mockHref, userAgent: mockUserAgent, }, }) done() }) it('should persist additional client context values', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: true, }) // assign additional values to client object tracker.clientContext.mockData2 = mockData2 tracker.push({ mockData1 }) assertSentRequest(mockEndpoint, { mockData1, sessionId: mockSessionId, context: { platform: mockPlatform, url: mockHref, userAgent: mockUserAgent, mockData2, }, }) // overwrite client object tracker.clientContext = { mockData1, mockData2 } tracker.push({ mockData1 }) assertSentRequest(mockEndpoint, { mockData1, sessionId: mockSessionId, context: { mockData1, mockData2, }, }) done() }) it('should send data that was pushed prior to loading tracker', function(done) { const initialTracker = [] window.tracker = initialTracker // tracker is not yet loaded, 3 pushes: one config, 2 data initialTracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) initialTracker.push({ mockData1 }) initialTracker.push({ mockData2, mockCookieValue }) assert.isTrue(mockRequest.open.notCalled) assert.isTrue(mockRequest.send.notCalled) // let's load our tracker simpleTracker(window) assert.isTrue(mockRequest.open.calledTwice) assert.isTrue(mockRequest.send.calledTwice) assertSentRequest(mockEndpoint, { mockData1, sessionId: mockSessionId, }, 0) assertSentRequest(mockEndpoint, { mockData2, mockCookieValue, sessionId: mockSessionId, }) done() }) it('should log events and events with params', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) // log event without additional params tracker.logEvent(mockData1) assertSentRequest(mockEndpoint, { type: 'event', event: mockData1, sessionId: mockSessionId, }) // log event with additional params tracker.logEvent(mockData1, { mockData2 }) assertSentRequest(mockEndpoint, { type: 'event', event: mockData1, sessionId: mockSessionId, mockData2, }) done() }) it('should log message', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) const level = 'info' tracker.logMessage(mockData1, level) assertSentRequest(mockEndpoint, { level, type: 'message', message: mockData1, sessionId: mockSessionId, }) done() }) it('should log metric', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) tracker.logMetric(mockData1, mockData2) assertSentRequest(mockEndpoint, { type: 'metric', metric: mockData1, value: mockData2, sessionId: mockSessionId, }) done() }) it('should log timing metric', function(done) { const perfStub = window.performance.now perfStub.onCall(0).returns(1000.000005) perfStub.onCall(1).returns(5000.700001) tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) tracker.startTimer(mockData1) tracker.stopTimer(mockData1) assertSentRequest(mockEndpoint, { type: 'metric', metric: mockData1, value: 4001, sessionId: mockSessionId, }) done() }) it('should persist singleton tracker across multiple loads', function(done) { // 1st load tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, attachClientContext: false, }) tracker.push({ mockData1 }) assert.isTrue(mockRequest.open.calledOnce) assert.isTrue(mockRequest.send.calledOnce) assertSentRequest(mockEndpoint, { sessionId: mockSessionId, mockData1, }) // second load, same window obj simpleTracker(window) tracker.push({ mockData2 }) assert.isTrue(mockRequest.open.calledTwice) assert.isTrue(mockRequest.send.calledTwice) assertSentRequest(mockEndpoint, { sessionId: mockSessionId, mockData2, }) done() }) it('dont send request out in devMode', function(done) { tracker.push({ endpoint: mockEndpoint, sessionId: mockSessionId, devMode: true, }) tracker.push({ mockData1 }) assert.isTrue(mockRequest.open.notCalled) assert.isTrue(mockRequest.send.notCalled) done() }) it('should auto initialize with window object if one exists', function(done) { delete window.tracker // ensure no instance of tracker loaded delete window.SimpleTracker global.window = window assert.isUndefined(window.tracker) assert.isUndefined(window.SimpleTracker) // window object is now in global, so should be picked up automatically const rTracker = rewire('../../index.js') // rewire loads fresh instance of module assert.isDefined(window.SimpleTracker) assert.instanceOf(window.tracker, window.SimpleTracker) assert.instanceOf(rTracker, window.SimpleTracker) delete global.window // cleanup done() }) it('dont do anything if data pushed is not an object or string', function(done) { tracker.push(() => {}) assert.isTrue(mockRequest.open.notCalled) assert.isTrue(mockRequest.send.notCalled) done() }) })