UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

199 lines 9.17 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const p_retry_1 = __importDefault(require("p-retry")); const types_1 = require("../types"); const toDimensionTypeEnum_1 = __importDefault(require("./toDimensionTypeEnum")); const uniqueId_1 = __importDefault(require("./uniqueId")); const getMetadata_1 = __importDefault(require("./getMetadata")); const constants_1 = require("../constants"); /** * `RemoteLogger` provides a granular API to send evaluations, events, exposures * and generic log messages to a remote server. It queues them internally and * sends them to the remote server when the flush method is called manually. * When `flushIntervalMs` option is set to a non null value it also sends them * periodically in the background. */ class RemoteLogger { constructor({ traceId, token, createLogs, localLogger, flushIntervalMs, }) { this.queue = { logs: [], evaluations: new Map(), events: [], exposures: [], }; this.shouldClose = false; this.flushLogsTimeout = null; this.token = token; this.createLogs = createLogs; this.localLogger = localLogger; this.startFlushIntervals(traceId, flushIntervalMs); } // eslint-disable-next-line max-params log(type, level, commitId, message, metadata) { const { queue } = this; // Avoid memory leak in case we generate lots of logs or fail to flush them. if (queue.logs.length > 1000) { this.localLogger(types_1.LogLevel.Debug, "Ignoring log, as more than 1000 in the queue already.", /* metadata */ {}); return; } queue.logs.push({ commitId, type, level, message, metadataJson: JSON.stringify(metadata), createdAt: new Date().toJSON(), }); } evaluations(commitId, evaluations) { const { queue } = this; let maybeCommitMap = queue.evaluations.get(commitId); if (!maybeCommitMap) { maybeCommitMap = new Map(); queue.evaluations.set(commitId, maybeCommitMap); } const commitMap = maybeCommitMap; Object.entries(evaluations).forEach(([expressionId, count]) => { var _a; commitMap.set(expressionId, ((_a = commitMap.get(expressionId)) !== null && _a !== void 0 ? _a : 0) + count); }); } events(commitId, events) { const { queue } = this; events.forEach(({ objectTypeName: eventObjectTypeName, payload: eventPayload }) => queue.events.push({ commitId, eventObjectTypeName, eventPayloadJson: stringifyEventPayload(eventPayload), createdAt: new Date().toJSON(), })); } exposures(commitId, exposures) { const { queue } = this; exposures.forEach(({ splitId, unitId, assignment, event }) => { var _a, _b; return queue.exposures.push({ commitId, splitId, unitId, assignment: Object.entries(assignment).map(([dimensionId, entry]) => (Object.assign({ dimensionId, entryType: (0, toDimensionTypeEnum_1.default)(entry.type) }, (entry.type === "discrete" ? { discreteArmId: entry.armId } : { continuousValue: entry.value })))), eventObjectTypeName: (_a = event === null || event === void 0 ? void 0 : event.objectTypeName) !== null && _a !== void 0 ? _a : null, eventPayloadJson: stringifyEventPayload((_b = event === null || event === void 0 ? void 0 : event.payload) !== null && _b !== void 0 ? _b : null), createdAt: new Date().toJSON(), }); }); } flush(traceId) { return __awaiter(this, void 0, void 0, function* () { const { queue, token, createLogs, localLogger } = this; const anythingToFlush = queue.logs.length > 0 || queue.evaluations.size > 0 || queue.events.length > 0 || queue.exposures.length > 0; if (!anythingToFlush) { return; } // Only include 1 random log to ensure the payload doesn't get too big const randomLog = queue.logs.length > 0 ? queue.logs[Math.floor(queue.logs.length * Math.random())] : null; const createLogsInput = { token, idempotencyKey: (0, uniqueId_1.default)(), logs: randomLog ? [randomLog] : [], evaluations: [...queue.evaluations.entries()].flatMap(([commitId, commitMap]) => [...commitMap.entries()].map(([expressionId, count]) => ({ commitId, expressionId, count, }))), events: queue.events, exposures: queue.exposures, }; queue.logs = []; queue.evaluations.clear(); queue.events = []; queue.exposures = []; try { yield (0, p_retry_1.default)((attemptNumber) => { localLogger(types_1.LogLevel.Debug, `Attempt ${attemptNumber} to flush logs to backend...`, /* metadata */ { traceId, createLogsInput }); return createLogs(traceId, createLogsInput); }, { onFailedAttempt: (error) => { localLogger(types_1.LogLevel.Debug, `Attempt ${error.attemptNumber} to flush logs failed. There are ${error.retriesLeft} retries left.`, Object.assign(Object.assign({ traceId }, (0, getMetadata_1.default)(error)), { createLogsInput })); }, }); localLogger(types_1.LogLevel.Debug, "Successfully flushed logs to backend.", /* metadata */ { traceId, createLogsInput }); } catch (error) { localLogger(types_1.LogLevel.Error, "All attempts to flush logs failed. These logs will be lost.", Object.assign(Object.assign({ traceId }, (0, getMetadata_1.default)(error)), { createLogsInput })); } }); } close(traceId) { return __awaiter(this, void 0, void 0, function* () { this.shouldClose = true; if (this.flushLogsTimeout) { clearTimeout(this.flushLogsTimeout); this.flushLogsTimeout = null; } yield this.flush(traceId); }); } startFlushIntervals(initTraceId, flushIntervalMs) { if (!flushIntervalMs) { this.localLogger(types_1.LogLevel.Info, "Not automatically flushing logs.", { traceId: initTraceId, }); return; } // eslint-disable-next-line func-style const flushLogQueue = () => { this.flushLogsTimeout = setTimeout(() => { const traceId = (0, uniqueId_1.default)(); if (this.shouldClose) { this.flushLogsTimeout = null; this.localLogger(types_1.LogLevel.Debug, "Stopped flushing log queue.", { traceId, }); return; } this.flush(traceId) .catch((error) => this.localLogger(types_1.LogLevel.Error, "Error flushing logs.", Object.assign(Object.assign({}, (0, getMetadata_1.default)(error)), { traceId }))) .finally(() => { if (this.shouldClose) { this.flushLogsTimeout = null; this.localLogger(types_1.LogLevel.Debug, "Stopped flushing log queue.", { traceId, }); return; } flushLogQueue(); }); }, flushIntervalMs); }; flushLogQueue(); } } exports.default = RemoteLogger; function stringifyEventPayload(eventPayload) { return eventPayload ? JSON.stringify(eventPayload, (key, value) => key === constants_1.graphqlTypeNameKey ? undefined : value) : null; } //# sourceMappingURL=RemoteLogger.js.map