Refactor: Convert event tracker module to typescript

Change-type: patch
Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
Cameron Diver 2018-08-25 21:18:11 +01:00
parent 524f2580d8
commit 8ee26adbbe
No known key found for this signature in database
GPG Key ID: 69264F9C923F55C1
6 changed files with 118 additions and 66 deletions

View File

@ -1,54 +0,0 @@
Promise = require 'bluebird'
_ = require 'lodash'
mixpanel = require 'mixpanel'
mask = require 'json-mask'
mixpanelMask = [
'appId'
'delay'
'error'
'interval'
'image'
'app(appId,name)'
'service(appId,serviceId,serviceName,commit,releaseId,image,labels)'
'stateDiff/local(os_version,superisor_version,ip_address,apps/*/services)'
].join(',')
module.exports = class EventTracker
constructor: ->
@_client = null
@_properties = null
_logEvent: ->
console.log(arguments...)
track: (ev, properties = {}) =>
# Allow passing in an error directly and having it assigned to the error property.
if properties instanceof Error
properties = error: properties
properties = _.cloneDeep(properties)
# If the properties has an error argument that is an Error object then it treats it nicely,
# rather than letting it become `{}`
if properties.error instanceof Error
properties.error =
message: properties.error.message
stack: properties.error.stack
# Don't log private env vars (e.g. api keys) or other secrets - use a whitelist to mask what we send
properties = mask(properties, mixpanelMask)
@_logEvent('Event:', ev, JSON.stringify(properties))
if !@_client?
return
# Mutation is bad, and it should feel bad
properties = _.assign(properties, @_properties)
@_client.track(ev, properties)
init: ({ offlineMode, mixpanelToken, uuid, mixpanelHost }) ->
Promise.try =>
@_properties =
distinct_id: uuid
uuid: uuid
if offlineMode
return
@_client = mixpanel.init(mixpanelToken, { host: mixpanelHost })

93
src/event-tracker.ts Normal file
View File

@ -0,0 +1,93 @@
import * as Bluebird from 'bluebird';
import mask = require('json-mask');
import * as _ from 'lodash';
import Mixpanel = require('mixpanel');
export type EventTrackProperties = Dictionary<any>;
interface InitArgs {
uuid: string;
offlineMode: boolean;
mixpanelHost: string;
mixpanelToken: string;
}
const mixpanelMask = [
'appId',
'delay',
'error',
'interval',
'image',
'app(appId,name)',
'service(appId,serviceId,serviceName,commit,releaseId,image,labels)',
'stateDiff/local(os_version,superisor_version,ip_address,apps/*/services)',
].join(',');
export class EventTracker {
private defaultProperties: EventTrackProperties | null;
private client: any;
public constructor() {
this.client = null;
this.defaultProperties = null;
}
public init({
offlineMode,
mixpanelHost,
mixpanelToken,
uuid,
}: InitArgs): Bluebird<void> {
return Bluebird.try(() => {
this.defaultProperties = {
distinct_id: uuid,
uuid,
};
if (offlineMode) {
return;
}
this.client = Mixpanel.init(mixpanelToken, { host: mixpanelHost });
});
}
public track(
event: string,
properties: EventTrackProperties | Error,
) {
if (properties instanceof Error) {
properties = { error: properties };
}
properties = _.cloneDeep(properties);
if (properties.error instanceof Error) {
// Format the error for printing, to avoid display as { }
properties.error = {
message: properties.error.message,
stack: properties.error.stack,
};
}
// Don't send potentially sensitive information, by using a whitelist
properties = mask(properties, mixpanelMask);
this.logEvent('Event:', event, JSON.stringify(properties));
if (this.client == null) {
return;
}
properties = this.assignDefaultProperties(properties);
this.client.track(event, properties);
}
private logEvent(...args: string[]) {
console.log(...args);
}
private assignDefaultProperties(
properties: EventTrackProperties,
): EventTrackProperties {
return _.merge({ }, properties, this.defaultProperties);
}
}

View File

@ -1,6 +1,6 @@
EventEmitter = require 'events'
EventTracker = require './event-tracker'
{ EventTracker } = require './event-tracker'
DB = require './db'
Config = require './config'
APIBinder = require './api-binder'

View File

@ -4,7 +4,7 @@ m = require 'mochainon'
{ expect } = m.chai
{ stub } = m.sinon
EventTracker = require '../src/event-tracker'
{ EventTracker } = require '../src/event-tracker'
describe 'EventTracker', ->
before ->
stub(mixpanel, 'init').callsFake (token) ->
@ -15,10 +15,10 @@ describe 'EventTracker', ->
@eventTrackerOffline = new EventTracker()
@eventTracker = new EventTracker()
stub(EventTracker.prototype, '_logEvent')
stub(EventTracker.prototype, 'logEvent')
after ->
EventTracker.prototype._logEvent.restore()
EventTracker.prototype.logEvent.restore()
mixpanel.init.restore()
it 'initializes in offline mode', ->
@ -28,11 +28,11 @@ describe 'EventTracker', ->
})
expect(promise).to.be.fulfilled
.then =>
expect(@eventTrackerOffline._client).to.be.null
expect(@eventTrackerOffline.client).to.be.null
it 'logs events in offline mode, with the correct properties', ->
@eventTrackerOffline.track('Test event', { appId: 'someValue' })
expect(@eventTrackerOffline._logEvent).to.be.calledWith('Event:', 'Test event', JSON.stringify({ appId: 'someValue' }))
expect(@eventTrackerOffline.logEvent).to.be.calledWith('Event:', 'Test event', JSON.stringify({ appId: 'someValue' }))
it 'initializes a mixpanel client when not in offline mode', ->
promise = @eventTracker.init({
@ -42,18 +42,18 @@ describe 'EventTracker', ->
expect(promise).to.be.fulfilled
.then =>
expect(mixpanel.init).to.have.been.calledWith('someToken')
expect(@eventTracker._client.token).to.equal('someToken')
expect(@eventTracker._client.track).to.be.a('function')
expect(@eventTracker.client.token).to.equal('someToken')
expect(@eventTracker.client.track).to.be.a('function')
it 'calls the mixpanel client track function with the event, properties and uuid as distinct_id', ->
@eventTracker.track('Test event 2', { appId: 'someOtherValue' })
expect(@eventTracker._logEvent).to.be.calledWith('Event:', 'Test event 2', JSON.stringify({ appId: 'someOtherValue' }))
expect(@eventTracker._client.track).to.be.calledWith('Test event 2', { appId: 'someOtherValue', uuid: 'barbaz', distinct_id: 'barbaz' })
expect(@eventTracker.logEvent).to.be.calledWith('Event:', 'Test event 2', JSON.stringify({ appId: 'someOtherValue' }))
expect(@eventTracker.client.track).to.be.calledWith('Test event 2', { appId: 'someOtherValue', uuid: 'barbaz', distinct_id: 'barbaz' })
it 'can be passed an Error and it is added to the event properties', ->
theError = new Error('something went wrong')
@eventTracker.track('Error event', theError)
expect(@eventTracker._client.track).to.be.calledWith('Error event', {
expect(@eventTracker.client.track).to.be.calledWith('Error event', {
error:
message: theError.message
stack: theError.stack
@ -72,7 +72,7 @@ describe 'EventTracker', ->
}
}
@eventTracker.track('Some app event', props)
expect(@eventTracker._client.track).to.be.calledWith('Some app event', {
expect(@eventTracker.client.track).to.be.calledWith('Some app event', {
service: { appId: '1' }
uuid: 'barbaz'
distinct_id: 'barbaz'

12
typings/json-mask.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare module 'json-mask' {
function mask(obj: Dictionary<any>, mask: string): Dictionary<any>;
// These types are not strictly correct, but they don't need to be for our usage
namespace mask {
export const compile: (mask: string) => any;
export const filter: (obj: Dictionary<any>, compiledMask: any) => any;
}
export = mask;
}

1
typings/mixpanel.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'mixpanel';