UNPKG

@devcycle/nodejs-server-sdk

Version:

The DevCycle NodeJS Server SDK used for feature management.

309 lines 14.9 kB
"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