@devcycle/nodejs-server-sdk
Version:
The DevCycle NodeJS Server SDK used for feature management.
202 lines • 9.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventQueue = exports.EventTypes = exports.AggregateEventTypes = void 0;
const request_1 = require("./request");
exports.AggregateEventTypes = {
variableEvaluated: 'variableEvaluated',
aggVariableEvaluated: 'aggVariableEvaluated',
variableDefaulted: 'variableDefaulted',
aggVariableDefaulted: 'aggVariableDefaulted',
};
exports.EventTypes = {
...exports.AggregateEventTypes,
};
class EventQueue {
constructor(sdkKey, clientUUID, bucketing, options) {
this.sdkKey = sdkKey;
this.clientUUID = clientUUID;
this.bucketing = bucketing;
this.flushInProgress = false;
this.flushCallbacks = [];
this.logger = options.logger;
this.reporter = options.reporter;
this.eventsAPIURI = options.eventsAPIURI;
this.eventFlushIntervalMS = (options === null || options === void 0 ? void 0 : options.eventFlushIntervalMS) || 10 * 1000;
this.disabledEventFlush = false;
if (this.eventFlushIntervalMS < 500) {
throw new Error(`eventFlushIntervalMS: ${this.eventFlushIntervalMS} must be larger than 500ms`);
}
else if (this.eventFlushIntervalMS > 60 * 1000) {
throw new Error(`eventFlushIntervalMS: ${this.eventFlushIntervalMS} must be smaller than 1 minute`);
}
this.flushEventQueueSize = (options === null || options === void 0 ? void 0 : options.flushEventQueueSize) || 1000;
this.maxEventQueueSize = (options === null || options === void 0 ? void 0 : options.maxEventQueueSize) || 2000;
const chunkSize = (options === null || options === void 0 ? void 0 : options.eventRequestChunkSize) || 100;
if (this.flushEventQueueSize >= this.maxEventQueueSize) {
throw new Error(`flushEventQueueSize: ${this.flushEventQueueSize} must be smaller than ` +
`maxEventQueueSize: ${this.maxEventQueueSize}`);
}
else if (this.flushEventQueueSize < chunkSize ||
this.maxEventQueueSize < chunkSize) {
throw new Error(`flushEventQueueSize: ${this.flushEventQueueSize} and ` +
`maxEventQueueSize: ${this.maxEventQueueSize} ` +
`must be smaller than eventRequestChunkSize: ${chunkSize}`);
}
else if (this.flushEventQueueSize > 20000 ||
this.maxEventQueueSize > 20000) {
throw new Error(`flushEventQueueSize: ${this.flushEventQueueSize} or ` +
`maxEventQueueSize: ${this.maxEventQueueSize} ` +
'must be smaller than 20,000');
}
this.flushInterval = setInterval(this.flushEvents.bind(this), this.eventFlushIntervalMS);
const eventQueueOptions = {
eventRequestChunkSize: chunkSize,
disableAutomaticEventLogging: options.disableAutomaticEventLogging,
disableCustomEventLogging: options.disableCustomEventLogging,
};
this.bucketing.initEventQueue(sdkKey, this.clientUUID, JSON.stringify(eventQueueOptions));
}
cleanup() {
clearInterval(this.flushInterval);
this.disabledEventFlush = true;
}
async _flushEvents() {
var _a, _b, _c, _d, _e;
const metricTags = {
envKey: this.sdkKey,
sdkKey: this.sdkKey,
};
(_a = this.reporter) === null || _a === void 0 ? void 0 : _a.reportMetric('queueLength', this.bucketing.eventQueueSize(this.sdkKey), metricTags);
let flushPayloadsStr;
try {
flushPayloadsStr = this.bucketing.flushEventQueue(this.sdkKey);
(_b = this.reporter) === null || _b === void 0 ? void 0 : _b.reportMetric('flushPayloadSize', flushPayloadsStr === null || flushPayloadsStr === void 0 ? void 0 : flushPayloadsStr.length, metricTags);
}
catch (ex) {
this.logger.error(`DevCycle Error Flushing Events: ${ex.message}`);
}
const results = {
failures: 0,
retries: 0,
successes: 0,
};
if (!flushPayloadsStr)
return;
this.logger.debug(`Flush Payloads: ${flushPayloadsStr}`);
const startTimeJson = Date.now();
const flushPayloads = JSON.parse(flushPayloadsStr);
const endTimeJson = Date.now();
(_c = this.reporter) === null || _c === void 0 ? void 0 : _c.reportMetric('jsonParseDuration', endTimeJson - startTimeJson, metricTags);
if (flushPayloads.length === 0) {
return;
}
const reducer = (val, batches) => val + batches.eventCount;
const eventCount = flushPayloads.reduce(reducer, 0);
this.logger.debug(`DevCycle Flush ${eventCount} Events, for ${flushPayloads.length} Users`);
const startTimeRequests = Date.now();
await Promise.all(flushPayloads.map(async (flushPayload) => {
try {
const res = await (0, request_1.publishEvents)(this.logger, this.sdkKey, flushPayload.records, this.eventsAPIURI);
if (res.status !== 201) {
this.logger.debug(`Error publishing events, status: ${res.status}, body: ${await res.text()}`);
if (res.status >= 500) {
results.retries++;
this.bucketing.onPayloadFailure(this.sdkKey, flushPayload.payloadId, true);
}
else {
results.failures++;
this.bucketing.onPayloadFailure(this.sdkKey, flushPayload.payloadId, false);
}
}
else {
this.logger.debug(`DevCycle Flushed ${eventCount} Events, for ${flushPayload.records.length} Users`);
this.bucketing.onPayloadSuccess(this.sdkKey, flushPayload.payloadId);
results.successes++;
}
}
catch (ex) {
this.logger.debug(`DevCycle Error Flushing Events response message: ${ex.message}`);
if ('status' in ex && ex.status === 401) {
this.logger.debug(`SDK key is invalid, closing event flushing interval`);
this.bucketing.onPayloadFailure(this.sdkKey, flushPayload.payloadId, false);
results.failures++;
this.cleanup();
}
else {
this.bucketing.onPayloadFailure(this.sdkKey, flushPayload.payloadId, true);
results.retries++;
}
}
}));
const endTimeRequests = Date.now();
(_d = this.reporter) === null || _d === void 0 ? void 0 : _d.reportMetric('flushRequestDuration', endTimeRequests - startTimeRequests, metricTags);
if (results) {
(_e = this.reporter) === null || _e === void 0 ? void 0 : _e.reportFlushResults(results, metricTags);
}
}
/**
* Flush events in queue to DevCycle Events API. Requeue events if flush fails
*/
async flushEvents() {
if (this.flushInProgress) {
await new Promise((resolve) => {
this.flushCallbacks.push(resolve);
});
return;
}
this.flushInProgress = true;
const currentFlushCallbacks = this.flushCallbacks.splice(0, this.flushCallbacks.length);
try {
await this._flushEvents();
}
catch (e) {
this.logger.error(`DVC Error Flushing Events`, e);
}
this.flushInProgress = false;
currentFlushCallbacks.forEach((cb) => cb(null));
if (this.flushCallbacks.length > 0) {
this.flushEvents();
}
}
/**
* Queue DVCAPIEvent for publishing to DevCycle Events API.
*/
queueEvent(user, event) {
if (this.disabledEventFlush) {
this.logger.warn(`Event flushing is disabled, dropping event: ${event.type}, event queue size: ${this.bucketing.eventQueueSize(this.sdkKey)}`);
return;
}
if (this.checkEventQueueSize()) {
this.logger.warn(`Max event queue size reached, dropping event: ${event.type}`);
return;
}
this.bucketing.queueEvent(this.sdkKey, JSON.stringify(user), JSON.stringify(event));
}
/**
* Queue DVCEvent that can be aggregated together, where multiple calls are aggregated
* by incrementing the 'value' field.
*/
queueAggregateEvent(user, event, bucketedConfig) {
if (this.disabledEventFlush) {
this.logger.warn(`Event flushing is disabled, dropping aggregate event: ${event.type}`);
return;
}
if (this.checkEventQueueSize()) {
this.logger.warn(`Max event queue size reached, dropping aggregate event: ${event.type}`);
return;
}
this.bucketing.queueAggregateEvent(this.sdkKey, JSON.stringify(event), JSON.stringify((bucketedConfig === null || bucketedConfig === void 0 ? void 0 : bucketedConfig.variableVariationMap) || {}));
}
checkEventQueueSize() {
const queueSize = this.bucketing.eventQueueSize(this.sdkKey);
if (queueSize >= this.flushEventQueueSize) {
this.flushEvents();
if (queueSize >= this.maxEventQueueSize) {
return true;
}
}
return false;
}
}
exports.EventQueue = EventQueue;
//# sourceMappingURL=eventQueue.js.map