mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-24 15:56:40 +00:00
Merge pull request #1356 from balena-io/singleton-eventtracker
Make the event-tracker module a singleton
This commit is contained in:
commit
f8e48573c7
@ -10,7 +10,7 @@ import * as url from 'url';
|
||||
import * as deviceRegister from './lib/register-device';
|
||||
|
||||
import * as config from './config';
|
||||
import { EventTracker } from './event-tracker';
|
||||
import * as eventTracker from './event-tracker';
|
||||
import { loadBackupFromMigration } from './lib/migration';
|
||||
|
||||
import constants = require('./lib/constants');
|
||||
@ -41,7 +41,6 @@ const INTERNAL_STATE_KEYS = [
|
||||
];
|
||||
|
||||
export interface APIBinderConstructOpts {
|
||||
eventTracker: EventTracker;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
@ -68,7 +67,6 @@ export class APIBinder {
|
||||
public router: express.Router;
|
||||
|
||||
private deviceState: DeviceState;
|
||||
private eventTracker: EventTracker;
|
||||
private logger: Logger;
|
||||
|
||||
public balenaApi: PinejsClientRequest | null = null;
|
||||
@ -84,8 +82,7 @@ export class APIBinder {
|
||||
private stateReportErrors = 0;
|
||||
private readyForUpdates = false;
|
||||
|
||||
public constructor({ eventTracker, logger }: APIBinderConstructOpts) {
|
||||
this.eventTracker = eventTracker;
|
||||
public constructor({ logger }: APIBinderConstructOpts) {
|
||||
this.logger = logger;
|
||||
|
||||
this.router = this.createAPIBinderRouter(this);
|
||||
@ -549,7 +546,7 @@ export class APIBinder {
|
||||
await this.report();
|
||||
this.reportCurrentState();
|
||||
} catch (e) {
|
||||
this.eventTracker.track('Device state report failure', { error: e });
|
||||
eventTracker.track('Device state report failure', { error: e });
|
||||
// We use the poll interval as the upper limit of
|
||||
// the exponential backoff
|
||||
const maxDelay = await config.get('appUpdatePollInterval');
|
||||
@ -828,7 +825,7 @@ export class APIBinder {
|
||||
apiKey: null,
|
||||
};
|
||||
await config.set(configToUpdate);
|
||||
this.eventTracker.track('Device bootstrap success');
|
||||
eventTracker.track('Device bootstrap success');
|
||||
}
|
||||
|
||||
// Now check if we need to pin the device
|
||||
@ -847,11 +844,11 @@ export class APIBinder {
|
||||
}
|
||||
|
||||
private async provisionOrRetry(retryDelay: number): Promise<void> {
|
||||
this.eventTracker.track('Device bootstrap');
|
||||
eventTracker.track('Device bootstrap');
|
||||
try {
|
||||
await this.provision();
|
||||
} catch (e) {
|
||||
this.eventTracker.track(`Device bootstrap failed, retrying`, {
|
||||
eventTracker.track(`Device bootstrap failed, retrying`, {
|
||||
error: e,
|
||||
delay: retryDelay,
|
||||
});
|
||||
@ -889,7 +886,7 @@ export class APIBinder {
|
||||
router.use(bodyParser.json({ limit: '10mb' }));
|
||||
|
||||
router.post('/v1/update', (req, res, next) => {
|
||||
apiBinder.eventTracker.track('Update notification');
|
||||
eventTracker.track('Update notification');
|
||||
if (apiBinder.readyForUpdates) {
|
||||
config
|
||||
.get('instantUpdates')
|
||||
|
3
src/application-manager.d.ts
vendored
3
src/application-manager.d.ts
vendored
@ -4,7 +4,6 @@ import { Router } from 'express';
|
||||
import Knex = require('knex');
|
||||
|
||||
import { ServiceAction } from './device-api/common';
|
||||
import { EventTracker } from './event-tracker';
|
||||
import { Logger } from './logger';
|
||||
import { DeviceStatus, InstancedAppState } from './types/state';
|
||||
|
||||
@ -49,7 +48,6 @@ class ApplicationManager extends EventEmitter {
|
||||
public _lockingIfNecessary: any;
|
||||
public logger: Logger;
|
||||
public deviceState: DeviceState;
|
||||
public eventTracker: EventTracker;
|
||||
public apiBinder: APIBinder;
|
||||
|
||||
public services: ServiceManager;
|
||||
@ -67,7 +65,6 @@ class ApplicationManager extends EventEmitter {
|
||||
|
||||
public constructor({
|
||||
logger: Logger,
|
||||
eventTracker: EventTracker,
|
||||
deviceState: DeviceState,
|
||||
apiBinder: APIBinder,
|
||||
});
|
||||
|
@ -79,7 +79,7 @@ const createApplicationManagerRouter = function (applications) {
|
||||
};
|
||||
|
||||
export class ApplicationManager extends EventEmitter {
|
||||
constructor({ logger, eventTracker, deviceState, apiBinder }) {
|
||||
constructor({ logger, deviceState, apiBinder }) {
|
||||
super();
|
||||
|
||||
this.serviceAction = serviceAction;
|
||||
@ -170,7 +170,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
this.localModeSwitchCompletion = this.localModeSwitchCompletion.bind(this);
|
||||
this.reportOptionalContainers = this.reportOptionalContainers.bind(this);
|
||||
this.logger = logger;
|
||||
this.eventTracker = eventTracker;
|
||||
this.deviceState = deviceState;
|
||||
this.apiBinder = apiBinder;
|
||||
this.images = new Images({
|
||||
|
@ -1,12 +1,12 @@
|
||||
import * as Promise from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as eventTracker from '../event-tracker';
|
||||
import * as constants from '../lib/constants';
|
||||
import { checkInt, checkTruthy } from '../lib/validation';
|
||||
import { doRestart, doPurge, serviceAction } from './common';
|
||||
|
||||
export const createV1Api = function (router, applications) {
|
||||
const { eventTracker } = applications;
|
||||
|
||||
router.post('/v1/restart', function (req, res, next) {
|
||||
const appId = checkInt(req.body.appId);
|
||||
const force = checkTruthy(req.body.force) ?? false;
|
||||
|
@ -10,7 +10,6 @@ import prettyMs = require('pretty-ms');
|
||||
|
||||
import * as config from './config';
|
||||
import * as db from './db';
|
||||
import EventTracker from './event-tracker';
|
||||
import Logger from './logger';
|
||||
|
||||
import {
|
||||
@ -177,7 +176,6 @@ function createDeviceStateRouter(deviceState: DeviceState) {
|
||||
}
|
||||
|
||||
interface DeviceStateConstructOpts {
|
||||
eventTracker: EventTracker;
|
||||
logger: Logger;
|
||||
apiBinder: APIBinder;
|
||||
}
|
||||
@ -218,7 +216,6 @@ type DeviceStateStep<T extends PossibleStepTargets> =
|
||||
| ConfigStep;
|
||||
|
||||
export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmitter) {
|
||||
public eventTracker: EventTracker;
|
||||
public logger: Logger;
|
||||
|
||||
public applications: ApplicationManager;
|
||||
@ -242,16 +239,14 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
|
||||
public connected: boolean;
|
||||
public router: express.Router;
|
||||
|
||||
constructor({ eventTracker, logger, apiBinder }: DeviceStateConstructOpts) {
|
||||
constructor({ logger, apiBinder }: DeviceStateConstructOpts) {
|
||||
super();
|
||||
this.eventTracker = eventTracker;
|
||||
this.logger = logger;
|
||||
this.deviceConfig = new DeviceConfig({
|
||||
logger: this.logger,
|
||||
});
|
||||
this.applications = new ApplicationManager({
|
||||
logger: this.logger,
|
||||
eventTracker: this.eventTracker,
|
||||
deviceState: this,
|
||||
apiBinder,
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import { fs } from 'mz';
|
||||
import { Image } from '../compose/images';
|
||||
import DeviceState from '../device-state';
|
||||
import * as config from '../config';
|
||||
import * as eventTracker from '../event-tracker';
|
||||
|
||||
import constants = require('../lib/constants');
|
||||
import { AppsJsonParseError, EISDIR, ENOENT } from '../lib/errors';
|
||||
@ -111,7 +112,7 @@ export async function loadTargetFromFile(
|
||||
if (ENOENT(e) || EISDIR(e)) {
|
||||
log.debug('No apps.json file present, skipping preload');
|
||||
} else {
|
||||
deviceState.eventTracker.track('Loading preloaded apps failed', {
|
||||
eventTracker.track('Loading preloaded apps failed', {
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
@ -1,22 +1,14 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import mask = require('json-mask');
|
||||
import * as _ from 'lodash';
|
||||
import * as memoizee from 'memoizee';
|
||||
import * as mixpanel from 'mixpanel';
|
||||
|
||||
import { ConfigType } from './config';
|
||||
import * as config from './config';
|
||||
import log from './lib/supervisor-console';
|
||||
import supervisorVersion = require('./lib/supervisor-version');
|
||||
|
||||
export type EventTrackProperties = Dictionary<any>;
|
||||
|
||||
interface InitArgs {
|
||||
uuid: ConfigType<'uuid'>;
|
||||
unmanaged: ConfigType<'unmanaged'>;
|
||||
mixpanelHost: ConfigType<'mixpanelHost'>;
|
||||
mixpanelToken: ConfigType<'mixpanelToken'>;
|
||||
}
|
||||
|
||||
// The minimum amount of time to wait between sending
|
||||
// events of the same type
|
||||
const eventDebounceTime = 60000;
|
||||
@ -32,38 +24,47 @@ const mixpanelMask = [
|
||||
'stateDiff/local(os_version,superisor_version,ip_address,apps/*/services)',
|
||||
].join(',');
|
||||
|
||||
export class EventTracker {
|
||||
private defaultProperties: EventTrackProperties | null;
|
||||
private client: mixpanel.Mixpanel | null;
|
||||
let defaultProperties: EventTrackProperties;
|
||||
// We must export this for the tests, but we make no references
|
||||
// to it within the rest of the supervisor codebase
|
||||
export let client: mixpanel.Mixpanel | null = null;
|
||||
|
||||
public constructor() {
|
||||
this.client = null;
|
||||
this.defaultProperties = null;
|
||||
}
|
||||
export const initialized = (async () => {
|
||||
await config.initialized;
|
||||
|
||||
public init({
|
||||
const {
|
||||
unmanaged,
|
||||
mixpanelHost,
|
||||
mixpanelToken,
|
||||
uuid,
|
||||
}: InitArgs): Bluebird<void> {
|
||||
return Bluebird.try(() => {
|
||||
this.defaultProperties = {
|
||||
} = await config.getMany([
|
||||
'unmanaged',
|
||||
'mixpanelHost',
|
||||
'mixpanelToken',
|
||||
'uuid',
|
||||
]);
|
||||
|
||||
defaultProperties = {
|
||||
distinct_id: uuid,
|
||||
uuid,
|
||||
supervisorVersion,
|
||||
};
|
||||
|
||||
if (unmanaged || mixpanelHost == null || mixpanelToken == null) {
|
||||
return;
|
||||
}
|
||||
this.client = mixpanel.init(mixpanelToken, {
|
||||
client = mixpanel.init(mixpanelToken, {
|
||||
host: mixpanelHost.host,
|
||||
path: mixpanelHost.path,
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
export async function track(
|
||||
event: string,
|
||||
properties: EventTrackProperties | Error = {},
|
||||
) {
|
||||
await initialized;
|
||||
|
||||
public track(event: string, properties: EventTrackProperties | Error = {}) {
|
||||
if (properties instanceof Error) {
|
||||
properties = { error: properties };
|
||||
}
|
||||
@ -79,21 +80,21 @@ export class EventTracker {
|
||||
|
||||
// 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) {
|
||||
log.event('Event:', event, JSON.stringify(properties));
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
properties = this.assignDefaultProperties(properties);
|
||||
this.throttleddLogger(event)(properties);
|
||||
properties = assignDefaultProperties(properties);
|
||||
throttleddLogger(event)(properties);
|
||||
}
|
||||
|
||||
private throttleddLogger = memoizee(
|
||||
const throttleddLogger = memoizee(
|
||||
(event: string) => {
|
||||
// Call this function at maximum once every minute
|
||||
return _.throttle(
|
||||
(properties: EventTrackProperties | Error) => {
|
||||
this.client?.track(event, properties);
|
||||
client?.track(event, properties);
|
||||
},
|
||||
eventDebounceTime,
|
||||
{ leading: true },
|
||||
@ -102,15 +103,8 @@ export class EventTracker {
|
||||
{ primitive: true },
|
||||
);
|
||||
|
||||
private logEvent(...args: string[]) {
|
||||
log.event(...args);
|
||||
}
|
||||
|
||||
private assignDefaultProperties(
|
||||
function assignDefaultProperties(
|
||||
properties: EventTrackProperties,
|
||||
): EventTrackProperties {
|
||||
return _.merge({}, properties, this.defaultProperties);
|
||||
return _.merge({}, properties, defaultProperties);
|
||||
}
|
||||
}
|
||||
|
||||
export default EventTracker;
|
||||
|
@ -3,7 +3,7 @@ import * as _ from 'lodash';
|
||||
|
||||
import * as config from './config';
|
||||
import * as db from './db';
|
||||
import { EventTracker } from './event-tracker';
|
||||
import * as eventTracker from './event-tracker';
|
||||
import { LogType } from './lib/log-types';
|
||||
import { writeLock } from './lib/update-lock';
|
||||
import {
|
||||
@ -30,22 +30,16 @@ interface LoggerSetupOptions {
|
||||
|
||||
type LogEventObject = Dictionary<any> | null;
|
||||
|
||||
interface LoggerConstructOptions {
|
||||
eventTracker: EventTracker;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private backend: LogBackend | null = null;
|
||||
private balenaBackend: BalenaLogBackend | null = null;
|
||||
private localBackend: LocalLogBackend | null = null;
|
||||
|
||||
private eventTracker: EventTracker;
|
||||
private containerLogs: { [containerId: string]: ContainerLogs } = {};
|
||||
private logMonitor: LogMonitor;
|
||||
|
||||
public constructor({ eventTracker }: LoggerConstructOptions) {
|
||||
public constructor() {
|
||||
this.backend = null;
|
||||
this.eventTracker = eventTracker;
|
||||
this.logMonitor = new LogMonitor();
|
||||
}
|
||||
|
||||
@ -144,7 +138,7 @@ export class Logger {
|
||||
}
|
||||
this.log(msgObj);
|
||||
if (track) {
|
||||
this.eventTracker.track(
|
||||
eventTracker.track(
|
||||
eventName != null ? eventName : message,
|
||||
eventObj != null ? eventObj : {},
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import * as _ from 'lodash';
|
||||
import * as morgan from 'morgan';
|
||||
|
||||
import * as config from './config';
|
||||
import { EventTracker } from './event-tracker';
|
||||
import * as eventTracker from './event-tracker';
|
||||
import blink = require('./lib/blink');
|
||||
import * as iptables from './lib/iptables';
|
||||
import { checkTruthy } from './lib/validation';
|
||||
@ -76,7 +76,6 @@ const expressLogger = morgan(
|
||||
);
|
||||
|
||||
interface SupervisorAPIConstructOpts {
|
||||
eventTracker: EventTracker;
|
||||
routers: express.Router[];
|
||||
healthchecks: Array<() => Promise<boolean>>;
|
||||
}
|
||||
@ -86,7 +85,6 @@ interface SupervisorAPIStopOpts {
|
||||
}
|
||||
|
||||
export class SupervisorAPI {
|
||||
private eventTracker: EventTracker;
|
||||
private routers: express.Router[];
|
||||
private healthchecks: Array<() => Promise<boolean>>;
|
||||
|
||||
@ -101,12 +99,7 @@ export class SupervisorAPI {
|
||||
}
|
||||
: this.applyListeningRules.bind(this);
|
||||
|
||||
public constructor({
|
||||
eventTracker,
|
||||
routers,
|
||||
healthchecks,
|
||||
}: SupervisorAPIConstructOpts) {
|
||||
this.eventTracker = eventTracker;
|
||||
public constructor({ routers, healthchecks }: SupervisorAPIConstructOpts) {
|
||||
this.routers = routers;
|
||||
this.healthchecks = healthchecks;
|
||||
|
||||
@ -132,7 +125,7 @@ export class SupervisorAPI {
|
||||
this.api.use(authenticate());
|
||||
|
||||
this.api.post('/v1/blink', (_req, res) => {
|
||||
this.eventTracker.track('Device blink');
|
||||
eventTracker.track('Device blink');
|
||||
blink.pattern.start();
|
||||
setTimeout(blink.pattern.stop, 15000);
|
||||
return res.sendStatus(200);
|
||||
|
@ -2,7 +2,7 @@ import APIBinder from './api-binder';
|
||||
import * as db from './db';
|
||||
import * as config from './config';
|
||||
import DeviceState from './device-state';
|
||||
import EventTracker from './event-tracker';
|
||||
import * as eventTracker from './event-tracker';
|
||||
import { intialiseContractRequirements } from './lib/contracts';
|
||||
import { normaliseLegacyDatabase } from './lib/migration';
|
||||
import * as osRelease from './lib/os-release';
|
||||
@ -29,21 +29,17 @@ const startupConfigFields: config.ConfigKey[] = [
|
||||
];
|
||||
|
||||
export class Supervisor {
|
||||
private eventTracker: EventTracker;
|
||||
private logger: Logger;
|
||||
private deviceState: DeviceState;
|
||||
private apiBinder: APIBinder;
|
||||
private api: SupervisorAPI;
|
||||
|
||||
public constructor() {
|
||||
this.eventTracker = new EventTracker();
|
||||
this.logger = new Logger({ eventTracker: this.eventTracker });
|
||||
this.logger = new Logger();
|
||||
this.apiBinder = new APIBinder({
|
||||
eventTracker: this.eventTracker,
|
||||
logger: this.logger,
|
||||
});
|
||||
this.deviceState = new DeviceState({
|
||||
eventTracker: this.eventTracker,
|
||||
logger: this.logger,
|
||||
apiBinder: this.apiBinder,
|
||||
});
|
||||
@ -55,7 +51,6 @@ export class Supervisor {
|
||||
this.deviceState.applications.proxyvisor.bindToAPI(this.apiBinder);
|
||||
|
||||
this.api = new SupervisorAPI({
|
||||
eventTracker: this.eventTracker,
|
||||
routers: [this.apiBinder.router, this.deviceState.router],
|
||||
healthchecks: [
|
||||
this.apiBinder.healthcheck.bind(this.apiBinder),
|
||||
@ -69,16 +64,10 @@ export class Supervisor {
|
||||
|
||||
await db.initialized;
|
||||
await config.initialized;
|
||||
await eventTracker.initialized;
|
||||
|
||||
const conf = await config.getMany(startupConfigFields);
|
||||
|
||||
// We can't print to the dashboard until the logger
|
||||
// has started up, so we leave a trail of breadcrumbs
|
||||
// in the logs in case runtime fails to get to the
|
||||
// first dashboard logs
|
||||
log.debug('Starting event tracker');
|
||||
await this.eventTracker.init(conf);
|
||||
|
||||
log.debug('Starting logging infrastructure');
|
||||
this.logger.init({
|
||||
enableLogs: conf.loggingEnabled,
|
||||
|
@ -216,9 +216,6 @@ describe('deviceState', () => {
|
||||
let deviceState: DeviceState;
|
||||
before(async () => {
|
||||
await prepare();
|
||||
const eventTracker = {
|
||||
track: console.log,
|
||||
};
|
||||
|
||||
stub(Service as any, 'extendEnvVars').callsFake((env) => {
|
||||
env['ADDITIONAL_ENV_VAR'] = 'foo';
|
||||
@ -231,7 +228,6 @@ describe('deviceState', () => {
|
||||
});
|
||||
|
||||
deviceState = new DeviceState({
|
||||
eventTracker: eventTracker as any,
|
||||
logger: logger as any,
|
||||
apiBinder: null as any,
|
||||
});
|
||||
|
@ -1,67 +1,96 @@
|
||||
import { Mixpanel } from 'mixpanel';
|
||||
import * as mixpanel from 'mixpanel';
|
||||
import { SinonStub, stub } from 'sinon';
|
||||
import { SinonStub, stub, spy, SinonSpy } from 'sinon';
|
||||
|
||||
import { expect } from './lib/chai-config';
|
||||
|
||||
import EventTracker from '../src/event-tracker';
|
||||
import log from '../src/lib/supervisor-console';
|
||||
import supervisorVersion = require('../src/lib/supervisor-version');
|
||||
import * as config from '../src/config';
|
||||
|
||||
describe('EventTracker', () => {
|
||||
let eventTrackerOffline: EventTracker;
|
||||
let eventTracker: EventTracker;
|
||||
let initStub: SinonStub;
|
||||
|
||||
let logEventStub: SinonStub;
|
||||
before(() => {
|
||||
initStub = stub(mixpanel, 'init').callsFake(
|
||||
(token) =>
|
||||
(({
|
||||
token,
|
||||
track: stub().returns(undefined),
|
||||
} as unknown) as Mixpanel),
|
||||
);
|
||||
logEventStub = stub(log, 'event');
|
||||
|
||||
eventTrackerOffline = new EventTracker();
|
||||
eventTracker = new EventTracker();
|
||||
return stub(EventTracker.prototype as any, 'logEvent');
|
||||
delete require.cache[require.resolve('../src/event-tracker')];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logEventStub.reset();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(EventTracker.prototype as any).logEvent.restore();
|
||||
return initStub.restore();
|
||||
logEventStub.restore();
|
||||
});
|
||||
|
||||
it('initializes in unmanaged mode', () => {
|
||||
const promise = eventTrackerOffline.init({
|
||||
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');
|
||||
});
|
||||
expect(promise).to.be.fulfilled.then(() => {
|
||||
// @ts-ignore
|
||||
expect(eventTrackerOffline.client).to.be.null;
|
||||
|
||||
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', () => {
|
||||
eventTrackerOffline.track('Test event', { appId: 'someValue' });
|
||||
// @ts-ignore
|
||||
expect(eventTrackerOffline.logEvent).to.be.calledWith(
|
||||
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' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes a mixpanel client when not in unmanaged mode', () => {
|
||||
const promise = eventTracker.init({
|
||||
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');
|
||||
});
|
||||
expect(promise).to.be.fulfilled.then(() => {
|
||||
|
||||
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');
|
||||
@ -69,11 +98,43 @@ describe('EventTracker', () => {
|
||||
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' });
|
||||
// @ts-ignore
|
||||
expect(eventTracker.logEvent).to.be.calledWith(
|
||||
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' }),
|
||||
@ -87,9 +148,9 @@ describe('EventTracker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can be passed an Error and it is added to the event properties', () => {
|
||||
it('can be passed an Error and it is added to the event properties', async () => {
|
||||
const theError = new Error('something went wrong');
|
||||
eventTracker.track('Error event', theError);
|
||||
await eventTracker.track('Error event', theError);
|
||||
// @ts-ignore
|
||||
expect(eventTracker.client.track).to.be.calledWith('Error event', {
|
||||
error: {
|
||||
@ -102,7 +163,7 @@ describe('EventTracker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('hides service environment variables, to avoid logging keys or secrets', () => {
|
||||
it('hides service environment variables, to avoid logging keys or secrets', async () => {
|
||||
const props = {
|
||||
service: {
|
||||
appId: '1',
|
||||
@ -113,7 +174,7 @@ describe('EventTracker', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
eventTracker.track('Some app event', props);
|
||||
await eventTracker.track('Some app event', props);
|
||||
// @ts-ignore
|
||||
expect(eventTracker.client.track).to.be.calledWith('Some app event', {
|
||||
service: { appId: '1' },
|
||||
@ -124,50 +185,65 @@ describe('EventTracker', () => {
|
||||
});
|
||||
|
||||
it('should handle being passed no properties object', () => {
|
||||
expect(eventTracker.track('no-options')).to.not.throw;
|
||||
expect(eventTracker.track('no-options')).to.be.fulfilled;
|
||||
});
|
||||
});
|
||||
|
||||
return describe('Rate limiting', () => {
|
||||
it('should rate limit events of the same type', () => {
|
||||
// @ts-ignore
|
||||
eventTracker.client.track.reset();
|
||||
describe('Rate limiting', () => {
|
||||
let eventTracker: typeof import('../src/event-tracker');
|
||||
let mixpanelStub: SinonStub;
|
||||
|
||||
eventTracker.track('test', {});
|
||||
eventTracker.track('test', {});
|
||||
eventTracker.track('test', {});
|
||||
eventTracker.track('test', {});
|
||||
eventTracker.track('test', {});
|
||||
|
||||
// @ts-ignore
|
||||
expect(eventTracker.client.track).to.have.callCount(1);
|
||||
before(async () => {
|
||||
mixpanelStub = stub(mixpanel, 'init').returns({
|
||||
track: stub(),
|
||||
} as any);
|
||||
eventTracker = await import('../src/event-tracker');
|
||||
await eventTracker.initialized;
|
||||
});
|
||||
|
||||
it('should rate limit events of the same type with different arguments', () => {
|
||||
// @ts-ignore
|
||||
eventTracker.client.track.reset();
|
||||
after(() => {
|
||||
mixpanelStub.restore();
|
||||
|
||||
eventTracker.track('test2', { a: 1 });
|
||||
eventTracker.track('test2', { b: 2 });
|
||||
eventTracker.track('test2', { c: 3 });
|
||||
eventTracker.track('test2', { d: 4 });
|
||||
eventTracker.track('test2', { e: 5 });
|
||||
|
||||
// @ts-ignore
|
||||
expect(eventTracker.client.track).to.have.callCount(1);
|
||||
delete require.cache[require.resolve('../src/event-tracker')];
|
||||
});
|
||||
|
||||
it('should not rate limit events of different types', () => {
|
||||
// @ts-ignore
|
||||
eventTracker.client.track.reset();
|
||||
it('should rate limit events of the same type', async () => {
|
||||
// @ts-expect-error resetting a non-stub typed function
|
||||
eventTracker.client?.track.reset();
|
||||
|
||||
eventTracker.track('test3', { a: 1 });
|
||||
eventTracker.track('test4', { b: 2 });
|
||||
eventTracker.track('test5', { c: 3 });
|
||||
eventTracker.track('test6', { d: 4 });
|
||||
eventTracker.track('test7', { e: 5 });
|
||||
await eventTracker.track('test', {});
|
||||
await eventTracker.track('test', {});
|
||||
await eventTracker.track('test', {});
|
||||
await eventTracker.track('test', {});
|
||||
await eventTracker.track('test', {});
|
||||
|
||||
// @ts-ignore
|
||||
expect(eventTracker.client.track).to.have.callCount(5);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import ApiBinder from '../src/api-binder';
|
||||
import prepare = require('./lib/prepare');
|
||||
import * as config from '../src/config';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as eventTracker from '../src/event-tracker';
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import chai = require('./lib/chai-config');
|
||||
import balenaAPI = require('./lib/mocked-balena-api');
|
||||
@ -28,10 +29,6 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
|
||||
(config.configJsonBackend as any).cache = await (config.configJsonBackend as any).read();
|
||||
await config.generateRequiredFields();
|
||||
|
||||
obj.eventTracker = {
|
||||
track: stub().callsFake((ev, props) => console.log(ev, props)),
|
||||
} as any;
|
||||
|
||||
obj.logger = {
|
||||
clearOutOfDateDBLogs: () => {
|
||||
/* noop */
|
||||
@ -40,11 +37,9 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
|
||||
|
||||
obj.apiBinder = new ApiBinder({
|
||||
logger: obj.logger,
|
||||
eventTracker: obj.eventTracker,
|
||||
});
|
||||
|
||||
obj.deviceState = new DeviceState({
|
||||
eventTracker: obj.eventTracker,
|
||||
logger: obj.logger,
|
||||
apiBinder: obj.apiBinder,
|
||||
});
|
||||
@ -65,6 +60,14 @@ const mockProvisioningOpts = {
|
||||
describe('ApiBinder', () => {
|
||||
let server: Server;
|
||||
|
||||
beforeEach(() => {
|
||||
stub(eventTracker, 'track');
|
||||
});
|
||||
afterEach(() => {
|
||||
// @ts-expect-error Restoring a non-stub type function
|
||||
eventTracker.track.restore();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
spy(balenaAPI.balenaBackend!, 'registerHandler');
|
||||
server = balenaAPI.listen(3000);
|
||||
@ -102,9 +105,7 @@ describe('ApiBinder', () => {
|
||||
|
||||
// @ts-ignore
|
||||
balenaAPI.balenaBackend!.registerHandler.resetHistory();
|
||||
expect(components.eventTracker.track).to.be.calledWith(
|
||||
'Device bootstrap success',
|
||||
);
|
||||
expect(eventTracker.track).to.be.calledWith('Device bootstrap success');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -7,6 +7,9 @@ import * as sinon from 'sinon';
|
||||
|
||||
import { Logger } from '../src/logger';
|
||||
import { ContainerLogs } from '../src/logging/container';
|
||||
import * as eventTracker from '../src/event-tracker';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
describe('Logger', function () {
|
||||
beforeEach(function () {
|
||||
this._req = new stream.PassThrough();
|
||||
@ -20,12 +23,9 @@ describe('Logger', function () {
|
||||
|
||||
this.requestStub = sinon.stub(https, 'request').returns(this._req);
|
||||
|
||||
this.fakeEventTracker = {
|
||||
track: sinon.spy(),
|
||||
};
|
||||
this.eventTrackerStub = stub(eventTracker, 'track');
|
||||
|
||||
// @ts-ignore missing db property
|
||||
this.logger = new Logger({ eventTracker: this.fakeEventTracker });
|
||||
this.logger = new Logger();
|
||||
return this.logger.init({
|
||||
apiEndpoint: 'https://example.com',
|
||||
uuid: 'deadbeef',
|
||||
@ -38,6 +38,7 @@ describe('Logger', function () {
|
||||
|
||||
afterEach(function () {
|
||||
this.requestStub.restore();
|
||||
this.eventTrackerStub.restore();
|
||||
});
|
||||
|
||||
it('waits the grace period before sending any logs', function () {
|
||||
@ -108,7 +109,7 @@ describe('Logger', function () {
|
||||
);
|
||||
|
||||
return Promise.delay(5500).then(() => {
|
||||
expect(this.fakeEventTracker.track).to.be.calledWith('Some event name', {
|
||||
expect(this.eventTrackerStub).to.be.calledWith('Some event name', {
|
||||
someProp: 'someVal',
|
||||
});
|
||||
const lines = this._req.body.split('\n');
|
||||
|
@ -7,7 +7,6 @@ import Network from '../src/compose/network';
|
||||
import Service from '../src/compose/service';
|
||||
import Volume from '../src/compose/volume';
|
||||
import DeviceState from '../src/device-state';
|
||||
import EventTracker from '../src/event-tracker';
|
||||
import * as dockerUtils from '../src/lib/docker-utils';
|
||||
|
||||
import chai = require('./lib/chai-config');
|
||||
@ -126,14 +125,12 @@ const dependentDBFormat = {
|
||||
describe('ApplicationManager', function () {
|
||||
before(async function () {
|
||||
await prepare();
|
||||
const eventTracker = new EventTracker();
|
||||
this.logger = {
|
||||
clearOutOfDateDBLogs: () => {
|
||||
/* noop */
|
||||
},
|
||||
} as any;
|
||||
this.deviceState = new DeviceState({
|
||||
eventTracker,
|
||||
logger: this.logger,
|
||||
apiBinder: null as any,
|
||||
});
|
||||
|
@ -13,7 +13,6 @@ import { createV1Api } from '../../src/device-api/v1';
|
||||
import { createV2Api } from '../../src/device-api/v2';
|
||||
import APIBinder from '../../src/api-binder';
|
||||
import DeviceState from '../../src/device-state';
|
||||
import EventTracker from '../../src/event-tracker';
|
||||
import SupervisorAPI from '../../src/supervisor-api';
|
||||
|
||||
const DB_PATH = './test/data/supervisor-api.sqlite';
|
||||
@ -68,19 +67,17 @@ const STUBBED_VALUES = {
|
||||
|
||||
async function create(): Promise<SupervisorAPI> {
|
||||
// Get SupervisorAPI construct options
|
||||
const { eventTracker, deviceState, apiBinder } = await createAPIOpts();
|
||||
const { deviceState, apiBinder } = await createAPIOpts();
|
||||
// Stub functions
|
||||
setupStubs();
|
||||
// Create ApplicationManager
|
||||
const appManager = new ApplicationManager({
|
||||
eventTracker,
|
||||
logger: null,
|
||||
deviceState,
|
||||
apiBinder: null,
|
||||
});
|
||||
// Create SupervisorAPI
|
||||
const api = new SupervisorAPI({
|
||||
eventTracker,
|
||||
routers: [buildRoutes(appManager)],
|
||||
healthchecks: [deviceState.healthcheck, apiBinder.healthcheck],
|
||||
});
|
||||
@ -103,20 +100,15 @@ async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||
await db.initialized;
|
||||
// Initialize and set values for mocked Config
|
||||
await initConfig();
|
||||
// Create EventTracker
|
||||
const tracker = new EventTracker();
|
||||
// Create deviceState
|
||||
const deviceState = new DeviceState({
|
||||
eventTracker: tracker,
|
||||
logger: null as any,
|
||||
apiBinder: null as any,
|
||||
});
|
||||
const apiBinder = new APIBinder({
|
||||
eventTracker: tracker,
|
||||
logger: null as any,
|
||||
});
|
||||
return {
|
||||
eventTracker: tracker,
|
||||
deviceState,
|
||||
apiBinder,
|
||||
};
|
||||
@ -165,7 +157,6 @@ function restoreStubs() {
|
||||
}
|
||||
|
||||
interface SupervisorAPIOpts {
|
||||
eventTracker: EventTracker;
|
||||
deviceState: DeviceState;
|
||||
apiBinder: APIBinder;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user