UNPKG

serverless-offline-msk

Version:

A serverless offline plugin that enables AWS MSK events

196 lines (171 loc) 5.87 kB
'use strict'; const { join } = require('path'); const ensurePlainObject = require('type/plain-object/ensure'); const { v1: uuid } = require('uuid'); const fetch = require('node-fetch'); const fse = require('fs-extra'); const fsp = require('fs').promises; const telemetryUrl = require('@serverless/utils/analytics-and-notfications-url'); const { log } = require('@serverless/utils/log'); const isTelemetryDisabled = require('./are-disabled'); const cacheDirPath = require('./cache-path'); const telemetryLog = log.get('telemetry'); const timestampWeekBefore = Date.now() - 1000 * 60 * 60 * 24 * 7; 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}$/ ); let serverlessRunEndTime; const logError = (type, error) => { telemetryLog.debug('User stats error: %s: %O', type, error); if (!process.env.SLS_STATS_DEBUG) return; }; const markServerlessRunEnd = () => (serverlessRunEndTime = Date.now()); const processResponseBody = async (response, ids, startTime) => { let result; try { result = await response.json(); } catch (error) { logError(`Response processing error for ${ids || '<no id>'}`, error); return null; } const endTime = Date.now(); if (serverlessRunEndTime) { telemetryLog.debug( 'Stats request prevented process from exiting for %dms (request time: %dms)', endTime - serverlessRunEndTime, endTime - startTime ); } return result; }; async function request(payload, { ids, timeout } = {}) { const startTime = Date.now(); let response; const body = JSON.stringify(payload); try { response = await fetch(telemetryUrl, { headers: { 'content-type': 'application/json', }, method: 'POST', // Ensure reasonable timeout to not block process from exiting timeout: timeout || 3500, body, }); } catch (networkError) { logError('Request network error', networkError); return null; } if (response.status < 200 || response.status >= 300) { logError('Unexpected request response', response); return processResponseBody(response, ids, startTime); } if (!ids) return processResponseBody(response, ids, startTime); await Promise.all( ids.map(async (id) => { const cachePath = join(cacheDirPath, id); try { await fsp.unlink(cachePath); } catch (error) { logError(`Could not remove cache file ${id}`, error); } }) ); return processResponseBody(response, ids, startTime); } // This method is explicitly kept as synchronous. The reason for it being the fact that it needs to // be executed in such manner due to its use in `process.on('SIGINT')` handler. function storeLocally(payload, options = {}) { ensurePlainObject(payload); if (!telemetryUrl) return null; const isForced = options && options.isForced; if (isTelemetryDisabled && !isForced) return null; if (!cacheDirPath) return null; const id = uuid(); return (function self() { try { // Additionally, we also append `id` to the payload to be used as $insert_id in Mixpanel to ensure event deduplication return fse.writeJsonSync(join(cacheDirPath, id), { payload: { ...payload, id }, timestamp: Date.now(), }); } catch (error) { if (error.code === 'ENOENT') { try { fse.ensureDirSync(cacheDirPath); return self(); } catch (ensureDirError) { logError('Cache dir creation error:', ensureDirError); } } logError(`Write cache file error: ${id}`, error); return null; } })(); } async function send(options = {}) { const isForced = options && options.isForced; serverlessRunEndTime = null; // Needed for testing if (options.serverlessExecutionSpan) { options.serverlessExecutionSpan.then(markServerlessRunEnd, markServerlessRunEnd); } if (isTelemetryDisabled && !isForced) return null; if (!cacheDirPath) return null; if (!telemetryUrl) return null; let dirFilenames; try { dirFilenames = await fsp.readdir(cacheDirPath); } catch (readdirError) { if (readdirError.code !== 'ENOENT') logError('Cannot access cache dir', readdirError); return null; } const payloadsWithIds = ( await Promise.all( dirFilenames.map(async (dirFilename) => { if (!isUuid(dirFilename)) return null; let data; try { data = await fse.readJson(join(cacheDirPath, dirFilename)); } catch (readJsonError) { if (readJsonError.code === 'ENOENT') return null; // Race condition logError(`Cannot read cache file: ${dirFilename}`, readJsonError); const cacheFile = join(cacheDirPath, dirFilename); try { return await fsp.unlink(cacheFile); } catch (error) { logError(`Could not remove cache file ${dirFilename}`, error); } } if (data && data.payload) { const timestamp = Number(data.timestamp); if (timestamp > timestampWeekBefore) { return { payload: data.payload, id: dirFilename, }; } } else { logError(`Invalid cached data ${dirFilename}`, data); } const cacheFile = join(cacheDirPath, dirFilename); try { return await fsp.unlink(cacheFile); } catch (error) { logError(`Could not remove cache file ${dirFilename}`, error); } return null; }) ) ).filter(Boolean); if (!payloadsWithIds.length) return null; return request( payloadsWithIds .map((item) => item.payload) .sort((item, other) => item.timestamp - other.timestamp), { ids: payloadsWithIds.map((item) => item.id), timeout: 3000, } ); } module.exports = { storeLocally, send };