@devcycle/nodejs-server-sdk
Version:
The DevCycle NodeJS Server SDK used for feature management.
309 lines • 14.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DevCycleClient = void 0;
const config_manager_1 = require("../config-manager/src");
const userBucketingHelper_1 = require("./utils/userBucketingHelper");
const eventQueue_1 = require("./eventQueue");
const bucketing_1 = require("./bucketing");
const types_1 = require("@devcycle/types");
const os_1 = __importDefault(require("os"));
const js_cloud_server_sdk_1 = require("@devcycle/js-cloud-server-sdk");
const populatedUserHelpers_1 = require("./models/populatedUserHelpers");
const crypto_1 = require("crypto");
const platformDetails_1 = require("./utils/platformDetails");
const DevCycleProvider_1 = require("./open-feature/DevCycleProvider");
const EvalHooksRunner_1 = require("./hooks/EvalHooksRunner");
const castIncomingUser = (user) => {
if (!(user instanceof js_cloud_server_sdk_1.DevCycleUser)) {
return new js_cloud_server_sdk_1.DevCycleUser(user);
}
return user;
};
class DevCycleClient {
get isInitialized() {
return this._isInitialized;
}
constructor(sdkKey, options) {
this._isInitialized = false;
this.clientUUID = (0, crypto_1.randomUUID)();
this.hostname = os_1.default.hostname();
this.sdkKey = sdkKey;
this.sdkPlatform = options === null || options === void 0 ? void 0 : options.sdkPlatform;
this.logger =
(options === null || options === void 0 ? void 0 : options.logger) || (0, js_cloud_server_sdk_1.dvcDefaultLogger)({ level: options === null || options === void 0 ? void 0 : options.logLevel });
this.hooksRunner = new EvalHooksRunner_1.EvalHooksRunner([], this.logger);
if (options === null || options === void 0 ? void 0 : options.enableEdgeDB) {
this.logger.info('EdgeDB can only be enabled for the DVC Cloud Client.');
}
this.bucketingImportPromise = this.initializeBucketing({
options,
}).catch((bucketingErr) => {
throw new types_1.UserError(bucketingErr);
});
const initializePromise = this.bucketingImportPromise.then(() => {
var _a;
this.configHelper = new config_manager_1.EnvironmentConfigManager(this.logger, sdkKey, (sdkKey, projectConfig) => (0, bucketing_1.setConfigDataUTF8)(this.bucketingLib, sdkKey, projectConfig), setInterval, clearInterval, this.trackSDKConfigEvent.bind(this), options || {}, options === null || options === void 0 ? void 0 : options.configSource);
if (options === null || options === void 0 ? void 0 : options.enableClientBootstrapping) {
this.clientConfigHelper = new config_manager_1.EnvironmentConfigManager(this.logger, sdkKey, (sdkKey, projectConfig) => (0, bucketing_1.setConfigDataUTF8)(this.bucketingLib, sdkKey, projectConfig), setInterval, clearInterval, this.trackSDKConfigEvent.bind(this), { ...options, clientMode: true }, options === null || options === void 0 ? void 0 : options.configSource);
}
this.eventQueue = new eventQueue_1.EventQueue(sdkKey, this.clientUUID, this.bucketingLib, {
...options,
logger: this.logger,
});
this.setPlatformDataInBucketingLib();
return Promise.all([
this.configHelper.fetchConfigPromise,
(_a = this.clientConfigHelper) === null || _a === void 0 ? void 0 : _a.fetchConfigPromise,
]);
});
this.onInitialized = initializePromise
.then(() => {
this.logger.info('DevCycle initialized');
return this;
})
.catch((err) => {
this.logger.error(`Error initializing DevCycle: ${err}`);
if (err instanceof types_1.UserError) {
throw err;
}
return this;
})
.finally(() => {
this._isInitialized = true;
});
process.on('exit', () => {
this.close();
});
}
setPlatformDataInBucketingLib() {
var _a;
if (!this.bucketingLib)
return;
this.platformDetails = (0, platformDetails_1.getNodeJSPlatformDetails)();
if (this.sdkPlatform || this.openFeatureProvider) {
this.platformDetails.sdkPlatform = (_a = this.sdkPlatform) !== null && _a !== void 0 ? _a : 'nodejs-of';
}
this.bucketingLib.setPlatformData(JSON.stringify(this.platformDetails));
}
async initializeBucketing({ options, }) {
;
[this.bucketingLib, this.bucketingTracker] = await (0, bucketing_1.importBucketingLib)({
options,
logger: this.logger,
});
}
/**
* @deprecated Use DevCycleProvider directly instead.
* See docs: https://docs.devcycle.com/sdk/server-side-sdks/node/node-openfeature
*/
async getOpenFeatureProvider() {
if (this.openFeatureProvider)
return this.openFeatureProvider;
this.openFeatureProvider = new DevCycleProvider_1.DevCycleProvider(this, {
logger: this.logger,
});
this.setPlatformDataInBucketingLib();
return this.openFeatureProvider;
}
/**
* Notify the user when Features have been loaded from the server.
* An optional callback can be passed in, and will return a promise if no callback has been passed in.
*
* @param onInitialized
*/
async onClientInitialized(onInitialized) {
if (onInitialized && typeof onInitialized === 'function') {
this.onInitialized
.then(() => onInitialized())
.catch((err) => onInitialized(err));
}
return this.onInitialized;
}
variable(user, key, defaultValue) {
const configMetadata = this.getConfigMetadata() || {};
const result = this.hooksRunner.runHooksForEvaluation(user, key, defaultValue, configMetadata, (context) => { var _a; return this._variable((_a = context === null || context === void 0 ? void 0 : context.user) !== null && _a !== void 0 ? _a : user, key, defaultValue); });
return result;
}
_variable(user, key, defaultValue) {
var _a;
const incomingUser = castIncomingUser(user);
// this will throw if type is invalid
const type = (0, types_1.getVariableTypeFromValue)(defaultValue, key, this.logger, true);
const populatedUser = (0, populatedUserHelpers_1.DVCPopulatedUserFromDevCycleUser)(incomingUser, this.platformDetails);
if (!((_a = this.configHelper) === null || _a === void 0 ? void 0 : _a.hasConfig)) {
this.logger.warn('variable called before DevCycleClient has config, returning default value');
const evalReason = {
reason: types_1.EVAL_REASONS.DEFAULT,
details: types_1.DEFAULT_REASON_DETAILS.MISSING_CONFIG,
};
this.queueAggregateEvent(populatedUser, {
type: eventQueue_1.EventTypes.aggVariableDefaulted,
target: key,
metaData: {
evalReason: evalReason.reason,
},
});
return new js_cloud_server_sdk_1.VariableAndMetadata({
defaultValue,
type,
key,
eval: evalReason,
});
}
const configVariable = (0, userBucketingHelper_1.variableForUser_PB)(this.bucketingLib, this.sdkKey, populatedUser, key, (0, userBucketingHelper_1.getVariableTypeCode)(this.bucketingLib, type));
const options = {
key,
type,
defaultValue,
};
if (configVariable) {
if (type === configVariable.type) {
options.value = configVariable.value;
if (configVariable.eval) {
options.eval = { ...configVariable.eval };
}
}
else {
options.eval = {
reason: types_1.EVAL_REASONS.DEFAULT,
details: types_1.DEFAULT_REASON_DETAILS.TYPE_MISMATCH,
};
this.logger.error(`Type mismatch for variable ${key}. Expected ${type}, got ${configVariable.type}`);
}
}
else {
options.eval = {
reason: types_1.EVAL_REASONS.DEFAULT,
details: types_1.DEFAULT_REASON_DETAILS.USER_NOT_TARGETED,
};
}
return new js_cloud_server_sdk_1.VariableAndMetadata(options, configVariable === null || configVariable === void 0 ? void 0 : configVariable._feature);
}
variableValue(user, key, defaultValue) {
return this.variable(user, key, defaultValue).value;
}
allVariables(user) {
var _a;
const incomingUser = castIncomingUser(user);
if (!((_a = this.configHelper) === null || _a === void 0 ? void 0 : _a.hasConfig)) {
this.logger.warn('allVariables called before DevCycleClient has config');
return {};
}
const populatedUser = (0, populatedUserHelpers_1.DVCPopulatedUserFromDevCycleUser)(incomingUser, this.platformDetails);
const bucketedConfig = (0, userBucketingHelper_1.bucketUserForConfig)(this.bucketingLib, populatedUser, this.sdkKey);
return (bucketedConfig === null || bucketedConfig === void 0 ? void 0 : bucketedConfig.variables) || {};
}
allFeatures(user) {
var _a;
const incomingUser = castIncomingUser(user);
if (!((_a = this.configHelper) === null || _a === void 0 ? void 0 : _a.hasConfig)) {
this.logger.warn('allFeatures called before DevCycleClient has config');
return {};
}
const populatedUser = (0, populatedUserHelpers_1.DVCPopulatedUserFromDevCycleUser)(incomingUser, this.platformDetails);
const bucketedConfig = (0, userBucketingHelper_1.bucketUserForConfig)(this.bucketingLib, populatedUser, this.sdkKey);
return (bucketedConfig === null || bucketedConfig === void 0 ? void 0 : bucketedConfig.features) || {};
}
track(user, event) {
const incomingUser = castIncomingUser(user);
if (!this._isInitialized) {
this.logger.warn('track called before DevCycleClient initialized, event will not be tracked');
return;
}
(0, js_cloud_server_sdk_1.checkParamDefined)('type', event.type);
const populatedUser = (0, populatedUserHelpers_1.DVCPopulatedUserFromDevCycleUser)(incomingUser, this.platformDetails);
this.queueEvent(populatedUser, event);
}
addHook(hook) {
this.hooksRunner.enqueue(hook);
}
queueEvent(populatedUser, event) {
// we need the config in order to queue events since we need to know the featureVars
this.onInitialized.then(() => {
this.eventQueue.queueEvent(populatedUser, event);
});
}
queueAggregateEvent(populatedUser, event) {
// we don't need the config for aggregate events since there are no featureVars stored, so just wait until
// bucketing lib itself is initialized
this.bucketingImportPromise.then(() => {
this.eventQueue.queueAggregateEvent(populatedUser, event);
});
}
trackSDKConfigEvent(url, responseTimeMS, metaData, err, reqEtag, reqLastModified, sseConnected) {
var _a, _b, _c;
const populatedUser = (0, populatedUserHelpers_1.DVCPopulatedUserFromDevCycleUser)({ user_id: `${this.clientUUID}@${this.hostname}` }, this.platformDetails);
this.queueEvent(populatedUser, {
type: 'sdkConfig',
target: url,
value: responseTimeMS,
metaData: {
clientUUID: this.clientUUID,
reqEtag,
reqLastModified,
...metaData,
resStatus: (_b = (_a = metaData === null || metaData === void 0 ? void 0 : metaData.resStatus) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.status) !== null && _b !== void 0 ? _b : undefined,
errMsg: (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : undefined,
sseConnected: sseConnected !== null && sseConnected !== void 0 ? sseConnected : undefined,
},
});
}
/**
* Call this to obtain a config that is suitable for use in the "bootstrapConfig" option of client-side JS SDKs
* Useful for serverside-rendering use cases where the server performs the initial rendering pass, and provides it
* to the client along with the DevCycle config to allow hydration
* @param user
* @param userAgent
*/
async getClientBootstrapConfig(user, userAgent) {
const incomingUser = castIncomingUser(user);
await this.onInitialized;
if (!this.clientConfigHelper) {
throw new Error('enableClientBootstrapping option must be set to true to use getClientBootstrapConfig');
}
const clientSDKKey = (0, userBucketingHelper_1.getSDKKeyFromConfig)(this.bucketingLib, `${this.sdkKey}_client`);
if (!clientSDKKey) {
throw new Error('Client bootstrapping config is malformed. Please contact DevCycle support.');
}
try {
const { generateClientPopulatedUser } = await import('./clientUser.js');
const populatedUser = await generateClientPopulatedUser(incomingUser, userAgent);
return {
...(0, userBucketingHelper_1.bucketUserForConfig)(this.bucketingLib, populatedUser, `${this.sdkKey}_client`),
clientSDKKey,
};
}
catch (e) {
throw new Error('@devcycle/js-client-sdk package could not be found. ' +
'Please install it to use client boostrapping. Error: ' +
e.message);
}
}
async flushEvents(callback) {
return this.bucketingImportPromise.then(() => this.eventQueue.flushEvents().then(callback));
}
async close() {
var _a;
await this.onInitialized;
await this.flushEvents();
(_a = this.configHelper) === null || _a === void 0 ? void 0 : _a.cleanup();
this.eventQueue.cleanup();
clearInterval(this.bucketingTracker);
}
setClientCustomData(clientCustomData) {
if (!this.bucketingLib)
return;
this.bucketingLib.setClientCustomData(this.sdkKey, JSON.stringify(clientCustomData));
}
getConfigMetadata() {
if (!this.bucketingLib) {
return;
}
return JSON.parse(this.bucketingLib.getConfigMetadata(this.sdkKey));
}
}
exports.DevCycleClient = DevCycleClient;
//# sourceMappingURL=client.js.map