import { SinonStub, stub, spy, SinonSpy } from 'sinon'; import { expect } from 'chai'; import * as mixpanel from 'mixpanel'; import log from '../src/lib/supervisor-console'; import supervisorVersion = require('../src/lib/supervisor-version'); import * as config from '../src/config'; describe('EventTracker', () => { let logEventStub: SinonStub; before(() => { logEventStub = stub(log, 'event'); delete require.cache[require.resolve('../src/event-tracker')]; }); afterEach(() => { logEventStub.reset(); }); after(() => { logEventStub.restore(); }); describe('Unmanaged', () => { let configStub: SinonStub; let eventTracker: typeof import('../src/event-tracker'); before(async () => { configStub = stub(config, 'getMany').returns( Promise.resolve({ unmanaged: true, uuid: 'foobar', mixpanelHost: { host: '', path: '' }, mixpanelToken: '', }) as any, ); eventTracker = await import('../src/event-tracker'); }); after(() => { configStub.restore(); delete require.cache[require.resolve('../src/event-tracker')]; }); it('initializes in unmanaged mode', () => { expect(eventTracker.initialized).to.be.fulfilled.then(() => { expect(eventTracker.client).to.be.null; }); }); it('logs events in unmanaged mode, with the correct properties', async () => { await eventTracker.track('Test event', { appId: 'someValue' }); expect(logEventStub).to.be.calledWith( 'Event:', 'Test event', JSON.stringify({ appId: 'someValue' }), ); }); }); describe('Init', () => { let eventTracker: typeof import('../src/event-tracker'); let configStub: SinonStub; let mixpanelSpy: SinonSpy; before(async () => { configStub = stub(config, 'getMany').returns( Promise.resolve({ mixpanelToken: 'someToken', uuid: 'barbaz', mixpanelHost: { host: '', path: '' }, unmanaged: false, }) as any, ); mixpanelSpy = spy(mixpanel, 'init'); eventTracker = await import('../src/event-tracker'); }); after(() => { configStub.restore(); mixpanelSpy.restore(); delete require.cache[require.resolve('../src/event-tracker')]; }); it('initializes a mixpanel client when not in unmanaged mode', () => { expect(eventTracker.initialized).to.be.fulfilled.then(() => { expect(mixpanel.init).to.have.been.calledWith('someToken'); // @ts-ignore expect(eventTracker.client.token).to.equal('someToken'); // @ts-ignore expect(eventTracker.client.track).to.be.a('function'); }); }); }); describe('Managed', () => { let eventTracker: typeof import('../src/event-tracker'); let configStub: SinonStub; let mixpanelStub: SinonStub; before(async () => { configStub = stub(config, 'getMany').returns( Promise.resolve({ mixpanelToken: 'someToken', uuid: 'barbaz', mixpanelHost: { host: '', path: '' }, unmanaged: false, }) as any, ); mixpanelStub = stub(mixpanel, 'init').returns({ token: 'someToken', track: stub(), } as any); eventTracker = await import('../src/event-tracker'); await eventTracker.initialized; }); after(() => { configStub.restore(); mixpanelStub.restore(); delete require.cache[require.resolve('../src/event-tracker')]; }); it('calls the mixpanel client track function with the event, properties and uuid as distinct_id', async () => { await eventTracker.track('Test event 2', { appId: 'someOtherValue' }); expect(logEventStub).to.be.calledWith( 'Event:', 'Test event 2', JSON.stringify({ appId: 'someOtherValue' }), ); // @ts-ignore expect(eventTracker.client.track).to.be.calledWith('Test event 2', { appId: 'someOtherValue', uuid: 'barbaz', distinct_id: 'barbaz', supervisorVersion, }); }); it('can be passed an Error and it is added to the event properties', async () => { const theError = new Error('something went wrong'); await eventTracker.track('Error event', theError); // @ts-ignore expect(eventTracker.client.track).to.be.calledWith('Error event', { error: { message: theError.message, stack: theError.stack, }, uuid: 'barbaz', distinct_id: 'barbaz', supervisorVersion, }); }); it('hides service environment variables, to avoid logging keys or secrets', async () => { const props = { service: { appId: '1', environment: { RESIN_API_KEY: 'foo', RESIN_SUPERVISOR_API_KEY: 'bar', OTHER_VAR: 'hi', }, }, }; await eventTracker.track('Some app event', props); // @ts-ignore expect(eventTracker.client.track).to.be.calledWith('Some app event', { service: { appId: '1' }, uuid: 'barbaz', distinct_id: 'barbaz', supervisorVersion, }); }); it('should handle being passed no properties object', () => { expect(eventTracker.track('no-options')).to.be.fulfilled; }); }); describe('Rate limiting', () => { let eventTracker: typeof import('../src/event-tracker'); let mixpanelStub: SinonStub; before(async () => { mixpanelStub = stub(mixpanel, 'init').returns({ track: stub(), } as any); eventTracker = await import('../src/event-tracker'); await eventTracker.initialized; }); after(() => { mixpanelStub.restore(); delete require.cache[require.resolve('../src/event-tracker')]; }); it('should rate limit events of the same type', async () => { // @ts-expect-error resetting a non-stub typed function eventTracker.client?.track.reset(); await eventTracker.track('test', {}); await eventTracker.track('test', {}); await eventTracker.track('test', {}); await eventTracker.track('test', {}); await eventTracker.track('test', {}); expect(eventTracker.client?.track).to.have.callCount(1); }); it('should rate limit events of the same type with different arguments', async () => { // @ts-expect-error resetting a non-stub typed function eventTracker.client?.track.reset(); await eventTracker.track('test2', { a: 1 }); await eventTracker.track('test2', { b: 2 }); await eventTracker.track('test2', { c: 3 }); await eventTracker.track('test2', { d: 4 }); await eventTracker.track('test2', { e: 5 }); expect(eventTracker.client?.track).to.have.callCount(1); }); it('should not rate limit events of different types', async () => { // @ts-expect-error resetting a non-stub typed function eventTracker.client?.track.reset(); await eventTracker.track('test3', { a: 1 }); await eventTracker.track('test4', { b: 2 }); await eventTracker.track('test5', { c: 3 }); await eventTracker.track('test6', { d: 4 }); await eventTracker.track('test7', { e: 5 }); expect(eventTracker.client?.track).to.have.callCount(5); }); }); });