mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 05:37:53 +00:00
Refactor: Convert event tracker module to typescript
Change-type: patch Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
parent
524f2580d8
commit
8ee26adbbe
@ -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
93
src/event-tracker.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
@ -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
12
typings/json-mask.d.ts
vendored
Normal 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
1
typings/mixpanel.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'mixpanel';
|
Loading…
Reference in New Issue
Block a user