serverless/lib/utils/tracking.js
2019-07-19 16:08:26 +02:00

143 lines
4.5 KiB
JavaScript

'use strict';
const { readdir, unlink } = require('fs');
const { join } = require('path');
const { homedir } = require('os');
const { format } = require('util');
const { v1: uuid } = require('uuid');
const BbPromise = require('bluebird');
const fetch = require('node-fetch');
const { ensureDir, readJson, writeJson } = require('fs-extra');
const isTrackingDisabled = require('./isTrackingDisabled');
const log = require('./log/serverlessLog');
const TRACKING_IS_DISABLED = isTrackingDisabled();
const isUuid = RegExp.prototype.test.bind(
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
);
const urls = new Map([
['user', 'https://serverless.com/api/framework/track'],
['segment', 'https://tracking.serverlessteam.com/v1/track'],
]);
const cacheDirPath = (() => {
const resolvedHomeDir = homedir();
if (!resolvedHomeDir) return null;
return join(resolvedHomeDir, '.serverless', 'tracking-cache');
})();
const logError = (type, error) => {
if (!process.env.SLS_DEBUG) return;
log(format('\nUser stats error: %s: %O', type, error));
};
const processResponseBody = (response, id) => {
return response.buffer().then(
() => {}, // For consistency do not expose any result
error => {
logError(`Response processing error for ${id}`, error);
}
);
};
/* note tracking swallows errors */
function request(type, event, { id, timeout } = {}) {
return fetch(urls.get(type), {
headers: {
'content-type': 'application/json',
},
method: 'POST',
// set to 1000 b/c no response needed
timeout: timeout || 1000,
body: JSON.stringify(event),
}).then(
response => {
if (response.status < 200 || response.status >= 300) {
logError('Unexpected request response', response);
return processResponseBody(response, id);
}
if (!id) return processResponseBody(response, id);
return new BbPromise(resolve => {
unlink(join(cacheDirPath, id), error => {
if (error) logError(`Could not remove cache file ${id}`, error);
resolve(processResponseBody(response, id));
});
});
},
networkError => {
logError('Request network error', networkError);
}
);
}
function track(type, event, options = {}) {
return BbPromise.try(() => {
const isForced = options && options.isForced;
if (TRACKING_IS_DISABLED && !isForced) return null;
if (!cacheDirPath) return request(type, event);
const id = uuid();
return BbPromise.all([
new BbPromise(function self(resolve) {
writeJson(join(cacheDirPath, id), { type, event }, error => {
if (error) {
if (error.code === 'ENOENT') {
ensureDir(cacheDirPath, ensureDirError => {
if (ensureDirError) {
logError('Cache dir creation error:', ensureDirError);
resolve();
return;
}
self(resolve);
});
return;
}
logError(`Write cache file error: ${id}`, error);
}
resolve();
});
}),
request(type, event, { id }),
]).then(([, requestResult]) => requestResult); // In all cases resolve with request result
});
}
function sendPending(options = {}) {
return BbPromise.try(() => {
const isForced = options && options.isForced;
if (TRACKING_IS_DISABLED && !isForced) return null;
if (!cacheDirPath) return null;
return new BbPromise(readdirResolve =>
readdir(cacheDirPath, (readdirError, dirFilenames) => {
if (readdirError) {
if (readdirError.code !== 'ENOENT') logError('Cannot access cache dir', readdirError);
readdirResolve();
return;
}
readdirResolve(
BbPromise.all(
dirFilenames.map(dirFilename => {
if (!isUuid(dirFilename)) return null;
return new BbPromise(readJsonResolve => {
readJson(join(cacheDirPath, dirFilename), (reaJsonError, data) => {
if (reaJsonError) {
logError(`Cannot read cache file: ${dirFilename}`, reaJsonError);
readJsonResolve();
return;
}
readJsonResolve(
request(data.type, data.event, { id: dirFilename, timeout: 3000 })
);
});
});
})
)
);
})
).then(() => {}); // Do not leak any result
});
}
module.exports = { track, sendPending };