Merge pull request #1989 from balena-os/disable-events

Disable event tracking
This commit is contained in:
bulldozer-balena[bot] 2022-09-20 17:54:06 +00:00 committed by GitHub
commit 437a24e2f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 47 additions and 526 deletions

View File

@ -68,12 +68,10 @@ RUN apk add --no-cache \
ARG ARCH
ARG VERSION=master
ARG DEFAULT_MIXPANEL_TOKEN=bananasbananas
ENV CONFIG_MOUNT_POINT=/boot/config.json \
LED_FILE=/dev/null \
SUPERVISOR_IMAGE=balena/$ARCH-supervisor \
VERSION=$VERSION \
DEFAULT_MIXPANEL_TOKEN=$DEFAULT_MIXPANEL_TOKEN
VERSION=$VERSION
###############################################################
# Use the base image to run integration tests and for livepush

129
package-lock.json generated
View File

@ -78,7 +78,6 @@
"livepush": "^3.5.1",
"lodash": "^4.17.21",
"memoizee": "^0.4.14",
"mixpanel": "^0.10.3",
"mocha": "^8.3.2",
"mocha-pod": "^0.6.0",
"mock-fs": "^4.14.0",
@ -2077,18 +2076,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"dev": true,
"dependencies": {
"es6-promisify": "^5.0.0"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/agentkeepalive": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz",
@ -5381,21 +5368,6 @@
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"dev": true
},
"node_modules/es6-promisify": {
"version": "5.0.0",
"resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"dependencies": {
"es6-promise": "^4.0.3"
}
},
"node_modules/es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@ -7498,35 +7470,6 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"node_modules/https-proxy-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
"dev": true,
"dependencies": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
"dev": true,
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/human-signals": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
@ -9870,18 +9813,6 @@
"node": ">=0.10.0"
}
},
"node_modules/mixpanel": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.10.3.tgz",
"integrity": "sha512-wIYr5o+1XSzJ80o3QED35K/yfPAKi5FigZXTSfcs4vltfeKbilIjNgwxdno7LrqzhjoSjmIyDWkI7D3lr7TwDw==",
"dev": true,
"dependencies": {
"https-proxy-agent": "3.0.0"
},
"engines": {
"node": ">=6.9"
}
},
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@ -18117,15 +18048,6 @@
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
},
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"dev": true,
"requires": {
"es6-promisify": "^5.0.0"
}
},
"agentkeepalive": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz",
@ -20844,21 +20766,6 @@
"es6-symbol": "^3.1.1"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"dev": true
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"requires": {
"es6-promise": "^4.0.3"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@ -22510,33 +22417,6 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"https-proxy-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
"dev": true,
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"human-signals": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
@ -24376,15 +24256,6 @@
}
}
},
"mixpanel": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.10.3.tgz",
"integrity": "sha512-wIYr5o+1XSzJ80o3QED35K/yfPAKi5FigZXTSfcs4vltfeKbilIjNgwxdno7LrqzhjoSjmIyDWkI7D3lr7TwDw==",
"dev": true,
"requires": {
"https-proxy-agent": "3.0.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",

View File

@ -103,7 +103,6 @@
"livepush": "^3.5.1",
"lodash": "^4.17.21",
"memoizee": "^0.4.14",
"mixpanel": "^0.10.3",
"mocha": "^8.3.2",
"mocha-pod": "^0.6.0",
"mock-fs": "^4.14.0",

View File

@ -546,7 +546,6 @@ export let balenaApi: PinejsClientRequest | null = null;
export const initialized = _.once(async () => {
await config.initialized();
await eventTracker.initialized();
await deviceState.initialized();
const { unmanaged, apiEndpoint, currentApiKey } = await config.getMany([

View File

@ -2,7 +2,6 @@ import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as memoizee from 'memoizee';
import { promises as fs } from 'fs';
import { URL } from 'url';
import supervisorVersion = require('../lib/supervisor-version');
@ -114,15 +113,6 @@ export const fnSchema = {
};
});
},
mixpanelHost: () => {
return config.get('apiEndpoint').then((apiEndpoint) => {
if (!apiEndpoint) {
return null;
}
const url = new URL(apiEndpoint);
return { host: url.host, path: '/mixpanel' };
});
},
extendedEnvOptions: () => {
return config.getMany([
'uuid',

View File

@ -1,7 +1,5 @@
import * as t from 'io-ts';
import * as constants from '../lib/constants';
import {
NullOrUndefined,
PermissiveBoolean,
@ -66,10 +64,6 @@ export const schemaTypes = {
type: PermissiveBoolean,
default: true,
},
mixpanelToken: {
type: t.string,
default: constants.defaultMixpanelToken,
},
bootstrapRetryDelay: {
type: PermissiveNumber,
default: 30000,
@ -226,10 +220,6 @@ export const schemaTypes = {
}),
default: t.never,
},
mixpanelHost: {
type: t.union([t.null, t.interface({ host: t.string, path: t.string })]),
default: t.never,
},
extendedEnvOptions: {
type: t.interface({
uuid: t.union([t.string, NullOrUndefined]),

View File

@ -54,11 +54,6 @@ export const schema = {
mutable: true,
removeIfNull: false,
},
mixpanelToken: {
source: 'config.json',
mutable: false,
removeIfNull: false,
},
bootstrapRetryDelay: {
source: 'config.json',
mutable: false,

View File

@ -1,18 +1,10 @@
import mask = require('json-mask');
import * as _ from 'lodash';
import * as memoizee from 'memoizee';
import * as mixpanel from 'mixpanel';
import * as config from './config';
import log from './lib/supervisor-console';
import supervisorVersion = require('./lib/supervisor-version');
export type EventTrackProperties = Dictionary<any>;
// The minimum amount of time to wait between sending
// events of the same type
const eventDebounceTime = 60000;
const mixpanelMask = [
'appId',
'delay',
@ -24,39 +16,10 @@ const mixpanelMask = [
'stateDiff/local(os_version,supervisor_version,ip_address,apps/*/services)',
].join(',');
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;
export const initialized = _.once(async () => {
await config.initialized();
const { unmanaged, mixpanelHost, mixpanelToken, uuid } = await config.getMany(
['unmanaged', 'mixpanelHost', 'mixpanelToken', 'uuid'],
);
defaultProperties = {
distinct_id: uuid,
uuid,
supervisorVersion,
};
if (unmanaged || mixpanelHost == null || mixpanelToken == null) {
return;
}
client = mixpanel.init(mixpanelToken, {
host: mixpanelHost.host,
path: mixpanelHost.path,
});
});
export async function track(
event: string,
properties: EventTrackProperties | Error = {},
) {
await initialized();
if (properties instanceof Error) {
properties = { error: properties };
}
@ -73,30 +36,4 @@ export async function track(
// Don't send potentially sensitive information, by using a whitelist
properties = mask(properties, mixpanelMask);
log.event('Event:', event, JSON.stringify(properties));
if (client == null) {
return;
}
properties = assignDefaultProperties(properties);
throttleddLogger(event)(properties);
}
const throttleddLogger = memoizee(
(event: string) => {
// Call this function at maximum once every minute
return _.throttle(
(properties: EventTrackProperties | Error) => {
client?.track(event, properties);
},
eventDebounceTime,
{ leading: true },
);
},
{ primitive: true },
);
function assignDefaultProperties(
properties: EventTrackProperties,
): EventTrackProperties {
return _.merge({}, properties, defaultProperties);
}

View File

@ -156,7 +156,6 @@ export const provision = async (
opts: KeyExchangeOpts,
) => {
await config.initialized();
await eventTracker.initialized();
let device: Device | null = null;

View File

@ -41,7 +41,6 @@ const constants = {
configJsonPathOnHost: checkString(process.env.CONFIG_JSON_PATH),
proxyvisorHookReceiver: 'http://0.0.0.0:1337',
configJsonNonAtomicPath: '/boot/config.json',
defaultMixpanelToken: process.env.DEFAULT_MIXPANEL_TOKEN,
supervisorNetworkInterface,
allowedInterfaces: [
'resin-vpn',

View File

@ -2,7 +2,6 @@ import * as apiBinder from './api-binder';
import * as db from './db';
import * as config from './config';
import * as deviceState from './device-state';
import * as eventTracker from './event-tracker';
import { intialiseContractRequirements } from './lib/contracts';
import { normaliseLegacyDatabase } from './lib/legacy';
import * as osRelease from './lib/os-release';
@ -21,8 +20,6 @@ const startupConfigFields: config.ConfigKey[] = [
'apiTimeout',
'unmanaged',
'deviceApiKey',
'mixpanelToken',
'mixpanelHost',
'loggingEnabled',
'localMode',
'legacyAppsPresent',
@ -36,7 +33,6 @@ export class Supervisor {
await db.initialized();
await config.initialized();
await eventTracker.initialized();
await avahi.initialized();
log.debug('Starting logging infrastructure');
await logger.initialized();

View File

@ -5,8 +5,9 @@ import { Builder } from 'resin-docker-build';
import { promises as fs } from 'fs';
import * as Path from 'path';
import { Duplex, Readable, PassThrough, Stream } from 'stream';
import { Readable } from 'stream';
import * as tar from 'tar-stream';
import * as readline from 'readline';
import { exec } from '../src/lib/fs-utils';
@ -52,7 +53,7 @@ export async function getDeviceArch(docker: Docker): Promise<string> {
}
return arch.trim();
} catch (e) {
} catch (e: any) {
throw new Error(
`Unable to get device architecture: ${e.message}.\nTry specifying the architecture with -a.`,
);
@ -68,31 +69,61 @@ export async function getCacheFrom(docker: Docker): Promise<string[]> {
}
}
// Source: https://github.com/balena-io/balena-cli/blob/f6d668684a6f5ea8102a964ca1942b242eaa7ae2/lib/utils/device/live.ts#L539-L547
function extractDockerArrowMessage(outputLine: string): string | undefined {
const arrowTest = /^.*\s*-+>\s*(.+)/i;
const match = arrowTest.exec(outputLine);
if (match != null) {
return match[1];
}
}
export async function performBuild(
docker: Docker,
dockerfile: Dockerfile,
dockerOpts: { [key: string]: any },
): Promise<string> {
): Promise<string[]> {
const builder = Builder.fromDockerode(docker);
// tar the directory, but replace the dockerfile with the
// livepush generated one
const tarStream = await tarDirectory(Path.join(__dirname, '..'), dockerfile);
const bufStream = new PassThrough();
return new Promise((resolve, reject) => {
const chunks = [] as Buffer[];
bufStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
// Store the stage ids for caching
const ids = [] as string[];
builder.createBuildStream(dockerOpts, {
buildSuccess: () => {
// Return the build logs
resolve(Buffer.concat(chunks).toString('utf8'));
// Return the image ids
resolve(ids);
},
buildFailure: reject,
buildStream: (stream: Duplex) => {
stream.pipe(process.stdout);
stream.pipe(bufStream);
tarStream.pipe(stream);
buildStream: (input: NodeJS.ReadWriteStream) => {
// Parse the build output to get stage ids and
// for logging
let lastArrowMessage: string | undefined;
readline.createInterface({ input }).on('line', (line) => {
// If this was a FROM line, take the last found
// image id and save it as a stage id
// Source: https://github.com/balena-io/balena-cli/blob/f6d668684a6f5ea8102a964ca1942b242eaa7ae2/lib/utils/device/live.ts#L300-L325
if (
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage != null
) {
ids.push(lastArrowMessage);
} else {
const msg = extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage = msg;
}
}
// Log the build line
console.info(line);
});
// stream.pipe(bufStream);
tarStream.pipe(input);
},
});
});

View File

@ -14,39 +14,6 @@ interface Opts {
arch?: string;
}
// Source: https://github.com/balena-io/balena-cli/blob/f6d668684a6f5ea8102a964ca1942b242eaa7ae2/lib/utils/device/live.ts#L539-L547
function extractDockerArrowMessage(outputLine: string): string | undefined {
const arrowTest = /^.*\s*-+>\s*(.+)/i;
const match = arrowTest.exec(outputLine);
if (match != null) {
return match[1];
}
}
// Source: https://github.com/balena-io/balena-cli/blob/f6d668684a6f5ea8102a964ca1942b242eaa7ae2/lib/utils/device/live.ts#L300-L325
function getMultiStateImageIDs(buildLog: string): string[] {
const ids = [] as string[];
const lines = buildLog.split(/\r?\n/);
let lastArrowMessage: string | undefined;
for (const line of lines) {
// If this was a from line, take the last found
// image id and save it
if (
/step \d+(?:\/\d+)?\s*:\s*FROM/i.test(line) &&
lastArrowMessage != null
) {
ids.push(lastArrowMessage);
} else {
const msg = extractDockerArrowMessage(line);
if (msg != null) {
lastArrowMessage = msg;
}
}
}
return ids;
}
function getPathPrefix(arch: string) {
switch (arch) {
/**
@ -74,7 +41,7 @@ export async function initDevice(opts: Opts) {
const buildCache = await device.readBuildCache(opts.address);
const buildLog = await device.performBuild(opts.docker, opts.dockerfile, {
const stageImages = await device.performBuild(opts.docker, opts.dockerfile, {
buildargs: { ARCH: arch, PREFIX: getPathPrefix(arch) },
t: image,
labels: { 'io.balena.livepush-image': '1', 'io.balena.architecture': arch },
@ -84,8 +51,6 @@ export async function initDevice(opts: Opts) {
nocache: opts.nocache,
});
const stageImages = getMultiStateImageIDs(buildLog);
// Store the list of stage images for the next time the sync
// command is called. This will only live until the device is rebooted
await device.writeBuildCache(opts.address, stageImages);

View File

@ -93,12 +93,12 @@ const argv = yargs
sigint = () => reject(new Error('User interrupt (Ctrl+C) received'));
process.on('SIGINT', sigint);
});
} catch (e) {
} catch (e: any) {
console.error('Error:', e.message);
} finally {
console.info('Cleaning up. Please wait ...');
await cleanup();
process.removeListener('SIGINT', sigint);
process.exit(1);
process.exit(0);
}
})();

View File

@ -1,248 +0,0 @@
import { SinonStub, stub, spy, SinonSpy } from 'sinon';
import { expect } from 'chai';
import * as mixpanel from 'mixpanel';
import log from '~/lib/supervisor-console';
import supervisorVersion = require('~/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-expect-error
expect(eventTracker.client.token).to.equal('someToken');
// @ts-expect-error
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-expect-error
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-expect-error
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-expect-error
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);
});
});
});