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
JavaScript
;
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