@atlaskit/feature-gate-js-client
Advanced tools
Comparing version 4.19.0 to 4.20.0
# @atlaskit/feature-gate-js-client | ||
## 4.20.0 | ||
### Minor Changes | ||
- [#151557](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/151557) | ||
[`0935b95608ca3`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/0935b95608ca3) - | ||
Add support for providers to initialize and update users | ||
## 4.19.0 | ||
@@ -4,0 +12,0 @@ |
@@ -8,2 +8,8 @@ "use strict"; | ||
}); | ||
Object.defineProperty(exports, "CLIENT_VERSION", { | ||
enumerable: true, | ||
get: function get() { | ||
return _version.CLIENT_VERSION; | ||
} | ||
}); | ||
Object.defineProperty(exports, "DynamicConfig", { | ||
@@ -42,2 +48,3 @@ enumerable: true, | ||
var _statsigJsLite = _interopRequireWildcard(require("statsig-js-lite")); | ||
var _subscriptions = _interopRequireDefault(require("../subscriptions")); | ||
var _fetcher = _interopRequireDefault(require("./fetcher")); | ||
@@ -120,2 +127,68 @@ var _types = require("./types"); | ||
return initialize; | ||
}() | ||
/** | ||
* @description | ||
* This method initializes the client using the provider given to call to fetch the bootstrap values. | ||
* If the client is initialized with an `analyticsWebClient`, it will send an operational event | ||
* to GASv3 with the following attributes: | ||
* - targetApp: the target app of the client | ||
* - clientVersion: the version of the client | ||
* - success: whether the initialization was successful | ||
* - startTime: the time when the initialization started | ||
* - totalTime: the total time it took to initialize the client | ||
* - apiKey: the api key used to initialize the client | ||
* @param clientOptions {ClientOptions} | ||
* @param provider {Provider} | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
* @returns {Promise<void>} | ||
*/ | ||
) | ||
}, { | ||
key: "initializeWithProvider", | ||
value: (function () { | ||
var _initializeWithProvider = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(clientOptions, provider, identifiers, customAttributes) { | ||
var startTime; | ||
return _regenerator.default.wrap(function _callee2$(_context2) { | ||
while (1) switch (_context2.prev = _context2.next) { | ||
case 0: | ||
if (!FeatureGates.initPromise) { | ||
_context2.next = 3; | ||
break; | ||
} | ||
if (!FeatureGates.shallowEquals(clientOptions, FeatureGates.initOptions)) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Feature Gates client already initialized with different options. New options were not applied.'); | ||
} | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
case 3: | ||
startTime = performance.now(); | ||
FeatureGates.initOptions = clientOptions; | ||
FeatureGates.subscriptions = new _subscriptions.default(); | ||
FeatureGates.provider = provider; | ||
FeatureGates.provider.setClientVersion(_version.CLIENT_VERSION); | ||
if (FeatureGates.provider.setApplyUpdateCallback) { | ||
FeatureGates.provider.setApplyUpdateCallback(function (experimentsResult) { | ||
_statsigJsLite.default.setInitializeValues(experimentsResult.experimentValues); | ||
FeatureGates.subscriptions.anyUpdated(); | ||
}); | ||
} | ||
FeatureGates.initPromise = FeatureGates.initWithProvider(clientOptions, identifiers, customAttributes).then(function () { | ||
FeatureGates.initCompleted = true; | ||
}).finally(function () { | ||
var endTime = performance.now(); | ||
var totalTime = endTime - startTime; | ||
FeatureGates.fireClientEvent(startTime, totalTime, 'initialize', FeatureGates.initCompleted, provider.getApiKey ? provider.getApiKey() : undefined); | ||
}); | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
case 11: | ||
case "end": | ||
return _context2.stop(); | ||
} | ||
}, _callee2); | ||
})); | ||
function initializeWithProvider(_x4, _x5, _x6, _x7) { | ||
return _initializeWithProvider.apply(this, arguments); | ||
} | ||
return initializeWithProvider; | ||
}()) | ||
@@ -154,12 +227,12 @@ }, { | ||
value: function () { | ||
var _initializeFromValues = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(clientOptions, identifiers, customAttributes) { | ||
var _initializeFromValues = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(clientOptions, identifiers, customAttributes) { | ||
var initializeValues, | ||
startTime, | ||
_args2 = arguments; | ||
return _regenerator.default.wrap(function _callee2$(_context2) { | ||
while (1) switch (_context2.prev = _context2.next) { | ||
_args3 = arguments; | ||
return _regenerator.default.wrap(function _callee3$(_context3) { | ||
while (1) switch (_context3.prev = _context3.next) { | ||
case 0: | ||
initializeValues = _args2.length > 3 && _args2[3] !== undefined ? _args2[3] : {}; | ||
initializeValues = _args3.length > 3 && _args3[3] !== undefined ? _args3[3] : {}; | ||
if (!FeatureGates.initPromise) { | ||
_context2.next = 4; | ||
_context3.next = 4; | ||
break; | ||
@@ -171,3 +244,3 @@ } | ||
} | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
return _context3.abrupt("return", FeatureGates.initPromise); | ||
case 4: | ||
@@ -183,10 +256,10 @@ startTime = performance.now(); | ||
}); | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
return _context3.abrupt("return", FeatureGates.initPromise); | ||
case 8: | ||
case "end": | ||
return _context2.stop(); | ||
return _context3.stop(); | ||
} | ||
}, _callee2); | ||
}, _callee3); | ||
})); | ||
function initializeFromValues(_x4, _x5, _x6) { | ||
function initializeFromValues(_x8, _x9, _x10) { | ||
return _initializeFromValues.apply(this, arguments); | ||
@@ -205,6 +278,6 @@ } | ||
value: (function () { | ||
var _updateUser = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(fetchOptions, identifiers, customAttributes) { | ||
var _updateUser = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(fetchOptions, identifiers, customAttributes) { | ||
var initializeValuesProducer; | ||
return _regenerator.default.wrap(function _callee3$(_context3) { | ||
while (1) switch (_context3.prev = _context3.next) { | ||
return _regenerator.default.wrap(function _callee4$(_context4) { | ||
while (1) switch (_context4.prev = _context4.next) { | ||
case 0: | ||
@@ -221,11 +294,11 @@ initializeValuesProducer = function initializeValuesProducer() { | ||
}; | ||
_context3.next = 3; | ||
_context4.next = 3; | ||
return FeatureGates.updateUserUsingInitializeValuesProducer(initializeValuesProducer, identifiers, customAttributes); | ||
case 3: | ||
case "end": | ||
return _context3.stop(); | ||
return _context4.stop(); | ||
} | ||
}, _callee3); | ||
}, _callee4); | ||
})); | ||
function updateUser(_x7, _x8, _x9) { | ||
function updateUser(_x11, _x12, _x13) { | ||
return _updateUser.apply(this, arguments); | ||
@@ -236,2 +309,36 @@ } | ||
/** | ||
* This method updates the user using the provider given on initialisation to get the new set of values | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
*/ | ||
) | ||
}, { | ||
key: "updateUserWithProvider", | ||
value: (function () { | ||
var _updateUserWithProvider = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5(identifiers, customAttributes) { | ||
return _regenerator.default.wrap(function _callee5$(_context5) { | ||
while (1) switch (_context5.prev = _context5.next) { | ||
case 0: | ||
if (FeatureGates.provider) { | ||
_context5.next = 2; | ||
break; | ||
} | ||
throw new Error('Cannot update user using provider as the client was not initialised with a provider'); | ||
case 2: | ||
_context5.next = 4; | ||
return FeatureGates.updateUserUsingInitializeValuesProducer(function () { | ||
return FeatureGates.provider.getExperimentValues(FeatureGates.initOptions, identifiers, customAttributes); | ||
}, identifiers, customAttributes); | ||
case 4: | ||
case "end": | ||
return _context5.stop(); | ||
} | ||
}, _callee5); | ||
})); | ||
function updateUserWithProvider(_x14, _x15) { | ||
return _updateUserWithProvider.apply(this, arguments); | ||
} | ||
return updateUserWithProvider; | ||
}() | ||
/** | ||
* This method updates the user given a new set of bootstrap values obtained from one of the server-side SDKs. | ||
@@ -247,10 +354,10 @@ * | ||
value: (function () { | ||
var _updateUserWithValues = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(identifiers, customAttributes) { | ||
var _updateUserWithValues = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee6(identifiers, customAttributes) { | ||
var initializeValues, | ||
initializeValuesProducer, | ||
_args4 = arguments; | ||
return _regenerator.default.wrap(function _callee4$(_context4) { | ||
while (1) switch (_context4.prev = _context4.next) { | ||
_args6 = arguments; | ||
return _regenerator.default.wrap(function _callee6$(_context6) { | ||
while (1) switch (_context6.prev = _context6.next) { | ||
case 0: | ||
initializeValues = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}; | ||
initializeValues = _args6.length > 2 && _args6[2] !== undefined ? _args6[2] : {}; | ||
initializeValuesProducer = function initializeValuesProducer() { | ||
@@ -262,11 +369,11 @@ return Promise.resolve({ | ||
}; | ||
_context4.next = 4; | ||
_context6.next = 4; | ||
return FeatureGates.updateUserUsingInitializeValuesProducer(initializeValuesProducer, identifiers, customAttributes); | ||
case 4: | ||
case "end": | ||
return _context4.stop(); | ||
return _context6.stop(); | ||
} | ||
}, _callee4); | ||
}, _callee6); | ||
})); | ||
function updateUserWithValues(_x10, _x11) { | ||
function updateUserWithValues(_x16, _x17) { | ||
return _updateUserWithValues.apply(this, arguments); | ||
@@ -559,3 +666,3 @@ } | ||
* | ||
* If this method returns false, then the {@link FeatureGates.updateUser} or {@link FeatureGates.updateUserWithValues} | ||
* If this method returns false, then the {@link FeatureGates.updateUser}, {@link FeatureGates.updateUserWithValues} or {@link FeatureGates.updateUserWithProvider} | ||
* methods can be used to re-align these values. | ||
@@ -574,2 +681,71 @@ * | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current checkGate value | ||
* @param gateName | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
}, { | ||
key: "onGateUpdated", | ||
value: function onGateUpdated(gateName, callback) { | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
var wrapCallback = function wrapCallback(value) { | ||
var _options$fireGateExpo2 = options.fireGateExposure, | ||
fireGateExposure = _options$fireGateExpo2 === void 0 ? true : _options$fireGateExpo2; | ||
if (fireGateExposure) { | ||
FeatureGates.manuallyLogGateExposure(gateName); | ||
} | ||
try { | ||
callback(value); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn("Error calling callback for gate ".concat(gateName, " with value ").concat(value), error); | ||
} | ||
}; | ||
return FeatureGates.subscriptions.onGateUpdated(gateName, wrapCallback, FeatureGates.checkGate, options); | ||
} | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current experiment value | ||
* @param experimentName | ||
* @param parameterName | ||
* @param defaultValue | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
}, { | ||
key: "onExperimentValueUpdated", | ||
value: function onExperimentValueUpdated(experimentName, parameterName, defaultValue, callback) { | ||
var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; | ||
var wrapCallback = function wrapCallback(value) { | ||
var _options$fireExperime2 = options.fireExperimentExposure, | ||
fireExperimentExposure = _options$fireExperime2 === void 0 ? true : _options$fireExperime2; | ||
if (fireExperimentExposure) { | ||
FeatureGates.manuallyLogExperimentExposure(experimentName); | ||
} | ||
try { | ||
callback(value); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn("Error calling callback for experiment ".concat(experimentName, " with value ").concat(value), error); | ||
} | ||
}; | ||
return FeatureGates.subscriptions.onExperimentValueUpdated(experimentName, parameterName, defaultValue, wrapCallback, FeatureGates.getExperimentValue, options); | ||
} | ||
/** | ||
* Subscribe so on any update the callback will be called. | ||
* NOTE: The callback will be called whenever the values are updated even if the values have not changed. | ||
* @param callback | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
}, { | ||
key: "onAnyUpdated", | ||
value: function onAnyUpdated(callback) { | ||
var _FeatureGates$subscri; | ||
return (_FeatureGates$subscri = FeatureGates.subscriptions) === null || _FeatureGates$subscri === void 0 ? void 0 : _FeatureGates$subscri.onAnyUpdated(callback); | ||
} | ||
/** | ||
* This method initializes the client using a network call to fetch the bootstrap values for the given user. | ||
@@ -585,6 +761,6 @@ * | ||
value: (function () { | ||
var _init = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5(clientOptions, identifiers, customAttributes) { | ||
var _init = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee7(clientOptions, identifiers, customAttributes) { | ||
var fromValuesClientOptions, experimentValues, customAttributesFromResult, clientSdkKeyPromise, experimentValuesPromise, _yield$Promise$all, _yield$Promise$all2, experimentValuesResult; | ||
return _regenerator.default.wrap(function _callee5$(_context5) { | ||
while (1) switch (_context5.prev = _context5.next) { | ||
return _regenerator.default.wrap(function _callee7$(_context7) { | ||
while (1) switch (_context7.prev = _context7.next) { | ||
case 0: | ||
@@ -594,3 +770,3 @@ fromValuesClientOptions = _objectSpread(_objectSpread({}, clientOptions), {}, { | ||
}); | ||
_context5.prev = 1; | ||
_context7.prev = 1; | ||
// If client sdk key fetch fails, an error would be thrown and handled instead of waiting for the experiment | ||
@@ -603,6 +779,6 @@ // values request to be settled, and it will fall back to use default values. | ||
// values if both requests are successful. Else an error would be thrown and handled by the catch | ||
_context5.next = 6; | ||
_context7.next = 6; | ||
return Promise.all([clientSdkKeyPromise, experimentValuesPromise]); | ||
case 6: | ||
_yield$Promise$all = _context5.sent; | ||
_yield$Promise$all = _context7.sent; | ||
_yield$Promise$all2 = (0, _slicedToArray2.default)(_yield$Promise$all, 2); | ||
@@ -612,30 +788,86 @@ experimentValuesResult = _yield$Promise$all2[1]; | ||
customAttributesFromResult = experimentValuesResult.customAttributes; | ||
_context5.next = 20; | ||
_context7.next = 20; | ||
break; | ||
case 13: | ||
_context5.prev = 13; | ||
_context5.t0 = _context5["catch"](1); | ||
if (_context5.t0 instanceof Error) { | ||
_context7.prev = 13; | ||
_context7.t0 = _context7["catch"](1); | ||
if (_context7.t0 instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error("Error occurred when trying to fetch the Feature Gates client values, error: ".concat(_context5.t0 === null || _context5.t0 === void 0 ? void 0 : _context5.t0.message)); | ||
console.error("Error occurred when trying to fetch the Feature Gates client values, error: ".concat(_context7.t0 === null || _context7.t0 === void 0 ? void 0 : _context7.t0.message)); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn("Initialising Statsig client without values"); | ||
_context5.next = 19; | ||
_context7.next = 19; | ||
return FeatureGates.initFromValues(fromValuesClientOptions, identifiers, customAttributes); | ||
case 19: | ||
throw _context5.t0; | ||
throw _context7.t0; | ||
case 20: | ||
_context5.next = 22; | ||
_context7.next = 22; | ||
return this.initFromValues(fromValuesClientOptions, identifiers, customAttributesFromResult, experimentValues); | ||
case 22: | ||
case "end": | ||
return _context5.stop(); | ||
return _context7.stop(); | ||
} | ||
}, _callee5, this, [[1, 13]]); | ||
}, _callee7, this, [[1, 13]]); | ||
})); | ||
function init(_x12, _x13, _x14) { | ||
function init(_x18, _x19, _x20) { | ||
return _init.apply(this, arguments); | ||
} | ||
return init; | ||
}()) | ||
}, { | ||
key: "initWithProvider", | ||
value: function () { | ||
var _initWithProvider = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee8(baseClientOptions, identifiers, customAttributes) { | ||
var fromValuesClientOptions, experimentValues, customAttributesFromResult, clientSdkKeyPromise, experimentValuesPromise, _yield$Promise$all3, _yield$Promise$all4, experimentValuesResult; | ||
return _regenerator.default.wrap(function _callee8$(_context8) { | ||
while (1) switch (_context8.prev = _context8.next) { | ||
case 0: | ||
fromValuesClientOptions = _objectSpread(_objectSpread({}, baseClientOptions), {}, { | ||
disableCurrentPageLogging: true | ||
}); | ||
_context8.prev = 1; | ||
// If client sdk key fetch fails, an error would be thrown and handled instead of waiting for the experiment | ||
// values request to be settled, and it will fall back to use default values. | ||
clientSdkKeyPromise = FeatureGates.provider.getClientSdkKey(baseClientOptions).then(function (value) { | ||
return fromValuesClientOptions.sdkKey = value; | ||
}); | ||
experimentValuesPromise = FeatureGates.provider.getExperimentValues(baseClientOptions, identifiers, customAttributes); // Only wait for the experiment values request to finish and try to initialise the client with experiment | ||
// values if both requests are successful. Else an error would be thrown and handled by the catch | ||
_context8.next = 6; | ||
return Promise.all([clientSdkKeyPromise, experimentValuesPromise]); | ||
case 6: | ||
_yield$Promise$all3 = _context8.sent; | ||
_yield$Promise$all4 = (0, _slicedToArray2.default)(_yield$Promise$all3, 2); | ||
experimentValuesResult = _yield$Promise$all4[1]; | ||
experimentValues = experimentValuesResult.experimentValues; | ||
customAttributesFromResult = experimentValuesResult.customAttributesFromFetch; | ||
_context8.next = 20; | ||
break; | ||
case 13: | ||
_context8.prev = 13; | ||
_context8.t0 = _context8["catch"](1); | ||
if (_context8.t0 instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error("Error occurred when trying to fetch the Feature Gates client values, error: ".concat(_context8.t0 === null || _context8.t0 === void 0 ? void 0 : _context8.t0.message)); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn("Initialising Statsig client without values"); | ||
_context8.next = 19; | ||
return FeatureGates.initFromValues(fromValuesClientOptions, identifiers, customAttributes); | ||
case 19: | ||
throw _context8.t0; | ||
case 20: | ||
_context8.next = 22; | ||
return this.initFromValues(fromValuesClientOptions, identifiers, customAttributesFromResult, experimentValues); | ||
case 22: | ||
case "end": | ||
return _context8.stop(); | ||
} | ||
}, _callee8, this, [[1, 13]]); | ||
})); | ||
function initWithProvider(_x21, _x22, _x23) { | ||
return _initWithProvider.apply(this, arguments); | ||
} | ||
return initWithProvider; | ||
}() | ||
@@ -651,7 +883,6 @@ /** | ||
*/ | ||
) | ||
}, { | ||
key: "initFromValues", | ||
value: (function () { | ||
var _initFromValues = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee6(clientOptions, identifiers, customAttributes) { | ||
var _initFromValues = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee9(clientOptions, identifiers, customAttributes) { | ||
var initializeValues, | ||
@@ -661,7 +892,7 @@ user, | ||
statsigOptions, | ||
_args6 = arguments; | ||
return _regenerator.default.wrap(function _callee6$(_context6) { | ||
while (1) switch (_context6.prev = _context6.next) { | ||
_args9 = arguments; | ||
return _regenerator.default.wrap(function _callee9$(_context9) { | ||
while (1) switch (_context9.prev = _context9.next) { | ||
case 0: | ||
initializeValues = _args6.length > 3 && _args6[3] !== undefined ? _args6[3] : {}; | ||
initializeValues = _args9.length > 3 && _args9[3] !== undefined ? _args9[3] : {}; | ||
user = this.toStatsigUser(identifiers, customAttributes); | ||
@@ -682,18 +913,18 @@ FeatureGates.currentIdentifiers = identifiers; | ||
statsigOptions = this.toStatsigOptions(clientOptions, initializeValues); | ||
_context6.prev = 9; | ||
_context6.next = 12; | ||
_context9.prev = 9; | ||
_context9.next = 12; | ||
return _statsigJsLite.default.initialize(sdkKey, user, statsigOptions); | ||
case 12: | ||
_context6.next = 21; | ||
_context9.next = 21; | ||
break; | ||
case 14: | ||
_context6.prev = 14; | ||
_context6.t0 = _context6["catch"](9); | ||
if (_context6.t0 instanceof Error) { | ||
_context9.prev = 14; | ||
_context9.t0 = _context9["catch"](9); | ||
if (_context9.t0 instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error("Error occurred when trying to initialise the Statsig client, error: ".concat(_context6.t0 === null || _context6.t0 === void 0 ? void 0 : _context6.t0.message)); | ||
console.error("Error occurred when trying to initialise the Statsig client, error: ".concat(_context9.t0 === null || _context9.t0 === void 0 ? void 0 : _context9.t0.message)); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn("Initialising Statsig client with default sdk key and without values"); | ||
_context6.next = 20; | ||
_context9.next = 20; | ||
return _statsigJsLite.default.initialize(DEFAULT_CLIENT_KEY, user, _objectSpread(_objectSpread({}, statsigOptions), {}, { | ||
@@ -703,10 +934,10 @@ initializeValues: {} | ||
case 20: | ||
throw _context6.t0; | ||
throw _context9.t0; | ||
case 21: | ||
case "end": | ||
return _context6.stop(); | ||
return _context9.stop(); | ||
} | ||
}, _callee6, this, [[9, 14]]); | ||
}, _callee9, this, [[9, 14]]); | ||
})); | ||
function initFromValues(_x15, _x16, _x17) { | ||
function initFromValues(_x24, _x25, _x26) { | ||
return _initFromValues.apply(this, arguments); | ||
@@ -750,9 +981,9 @@ } | ||
value: (function () { | ||
var _updateUserUsingInitializeValuesProducer = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee7(getInitializeValues, identifiers, customAttributes) { | ||
var _updateUserUsingInitializeValuesProducer = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee10(getInitializeValues, identifiers, customAttributes) { | ||
var originalInitPromise, initializeValuesPromise, updateUserPromise; | ||
return _regenerator.default.wrap(function _callee7$(_context7) { | ||
while (1) switch (_context7.prev = _context7.next) { | ||
return _regenerator.default.wrap(function _callee10$(_context10) { | ||
while (1) switch (_context10.prev = _context10.next) { | ||
case 0: | ||
if (FeatureGates.initPromise) { | ||
_context7.next = 2; | ||
_context10.next = 2; | ||
break; | ||
@@ -763,18 +994,18 @@ } | ||
if (!FeatureGates.isCurrentUser(identifiers, customAttributes)) { | ||
_context7.next = 4; | ||
_context10.next = 4; | ||
break; | ||
} | ||
return _context7.abrupt("return", FeatureGates.initPromise); | ||
return _context10.abrupt("return", FeatureGates.initPromise); | ||
case 4: | ||
// Wait for the current initialize/update to finish | ||
originalInitPromise = FeatureGates.initPromise; | ||
_context7.prev = 5; | ||
_context7.next = 8; | ||
_context10.prev = 5; | ||
_context10.next = 8; | ||
return FeatureGates.initPromise; | ||
case 8: | ||
_context7.next = 12; | ||
_context10.next = 12; | ||
break; | ||
case 10: | ||
_context7.prev = 10; | ||
_context7.t0 = _context7["catch"](5); | ||
_context10.prev = 10; | ||
_context10.t0 = _context10["catch"](5); | ||
case 12: | ||
@@ -788,10 +1019,10 @@ initializeValuesPromise = getInitializeValues(); | ||
}); | ||
return _context7.abrupt("return", updateUserPromise); | ||
return _context10.abrupt("return", updateUserPromise); | ||
case 16: | ||
case "end": | ||
return _context7.stop(); | ||
return _context10.stop(); | ||
} | ||
}, _callee7, null, [[5, 10]]); | ||
}, _callee10, null, [[5, 10]]); | ||
})); | ||
function updateUserUsingInitializeValuesProducer(_x18, _x19, _x20) { | ||
function updateUserUsingInitializeValuesProducer(_x27, _x28, _x29) { | ||
return _updateUserUsingInitializeValuesProducer.apply(this, arguments); | ||
@@ -813,27 +1044,27 @@ } | ||
value: (function () { | ||
var _updateStatsigClientUser = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee8(initializeValuesPromise, identifiers, customAttributes) { | ||
var _updateStatsigClientUser = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee11(initializeValuesPromise, identifiers, customAttributes) { | ||
var initializeValues, user, _FeatureGates$initOpt2, _FeatureGates$initOpt3, errMsg, success; | ||
return _regenerator.default.wrap(function _callee8$(_context8) { | ||
while (1) switch (_context8.prev = _context8.next) { | ||
return _regenerator.default.wrap(function _callee11$(_context11) { | ||
while (1) switch (_context11.prev = _context11.next) { | ||
case 0: | ||
_context8.prev = 0; | ||
_context8.next = 3; | ||
_context11.prev = 0; | ||
_context11.next = 3; | ||
return initializeValuesPromise; | ||
case 3: | ||
initializeValues = _context8.sent; | ||
initializeValues = _context11.sent; | ||
user = FeatureGates.toStatsigUser(identifiers, initializeValues.customAttributesFromFetch); | ||
_context8.next = 12; | ||
_context11.next = 12; | ||
break; | ||
case 7: | ||
_context8.prev = 7; | ||
_context8.t0 = _context8["catch"](0); | ||
_context11.prev = 7; | ||
_context11.t0 = _context11["catch"](0); | ||
// Make sure the updateUserCompletionCallback is called for any errors in our custom code. | ||
// This is not necessary for the updateUserWithValues call, because the Statsig client will already invoke the callback itself. | ||
errMsg = _context8.t0 instanceof Error ? _context8.t0.message : JSON.stringify(_context8.t0); | ||
errMsg = _context11.t0 instanceof Error ? _context11.t0.message : JSON.stringify(_context11.t0); | ||
(_FeatureGates$initOpt2 = (_FeatureGates$initOpt3 = FeatureGates.initOptions).updateUserCompletionCallback) === null || _FeatureGates$initOpt2 === void 0 || _FeatureGates$initOpt2.call(_FeatureGates$initOpt3, false, errMsg); | ||
throw _context8.t0; | ||
throw _context11.t0; | ||
case 12: | ||
success = _statsigJsLite.default.updateUserWithValues(user, initializeValues.experimentValues); | ||
if (!success) { | ||
_context8.next = 18; | ||
_context11.next = 18; | ||
break; | ||
@@ -843,3 +1074,3 @@ } | ||
FeatureGates.currentAttributes = customAttributes; | ||
_context8.next = 19; | ||
_context11.next = 19; | ||
break; | ||
@@ -850,7 +1081,7 @@ case 18: | ||
case "end": | ||
return _context8.stop(); | ||
return _context11.stop(); | ||
} | ||
}, _callee8, null, [[0, 7]]); | ||
}, _callee11, null, [[0, 7]]); | ||
})); | ||
function updateStatsigClientUser(_x21, _x22, _x23) { | ||
function updateStatsigClientUser(_x30, _x31, _x32) { | ||
return _updateStatsigClientUser.apply(this, arguments); | ||
@@ -1051,6 +1282,7 @@ } | ||
(0, _defineProperty2.default)(FeatureGates, "hasCheckGateErrorOccurred", false); | ||
(0, _defineProperty2.default)(FeatureGates, "subscriptions", new _subscriptions.default()); | ||
var boundFGJS = FeatureGates; | ||
// This makes it possible to get a reference to the FeatureGates client at runtime. | ||
// This is important for overriding values in Cyprus tests, as there needs to be a | ||
// This is important for overriding values in Cypress tests, as there needs to be a | ||
// way to get the exact instance for a window in order to mock some of its methods. | ||
@@ -1057,0 +1289,0 @@ if (typeof window !== 'undefined') { |
@@ -11,3 +11,3 @@ "use strict"; | ||
/** | ||
* Base client options. | ||
* Base client options. Does not include any options specific to providers | ||
* @interface BaseClientOptions | ||
@@ -14,0 +14,0 @@ * @property {FeatureGateEnvironment} environment - The environment for the client. |
@@ -7,2 +7,3 @@ "use strict"; | ||
exports.CLIENT_VERSION = void 0; | ||
var CLIENT_VERSION = exports.CLIENT_VERSION = "4.19.0"; | ||
/// <reference types="node" /> | ||
var CLIENT_VERSION = exports.CLIENT_VERSION = "4.20.0"; |
@@ -7,2 +7,8 @@ "use strict"; | ||
}); | ||
Object.defineProperty(exports, "CLIENT_VERSION", { | ||
enumerable: true, | ||
get: function get() { | ||
return _client.CLIENT_VERSION; | ||
} | ||
}); | ||
Object.defineProperty(exports, "DynamicConfig", { | ||
@@ -9,0 +15,0 @@ enumerable: true, |
import _defineProperty from "@babel/runtime/helpers/defineProperty"; | ||
import Statsig, { DynamicConfig, EvaluationReason, Layer } from 'statsig-js-lite'; | ||
import Subscriptions from '../subscriptions'; | ||
import Fetcher from './fetcher'; | ||
@@ -8,2 +9,3 @@ import { FeatureGateEnvironment, PerimeterType } from './types'; | ||
export { FeatureGateEnvironment, PerimeterType } from './types'; | ||
export { CLIENT_VERSION } from './version'; | ||
const DEFAULT_CLIENT_KEY = 'client-default-key'; | ||
@@ -55,2 +57,48 @@ // the default event logging api is the Atlassian proxy rather than Statsig's domain, to avoid ad blockers | ||
} | ||
/** | ||
* @description | ||
* This method initializes the client using the provider given to call to fetch the bootstrap values. | ||
* If the client is initialized with an `analyticsWebClient`, it will send an operational event | ||
* to GASv3 with the following attributes: | ||
* - targetApp: the target app of the client | ||
* - clientVersion: the version of the client | ||
* - success: whether the initialization was successful | ||
* - startTime: the time when the initialization started | ||
* - totalTime: the total time it took to initialize the client | ||
* - apiKey: the api key used to initialize the client | ||
* @param clientOptions {ClientOptions} | ||
* @param provider {Provider} | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
* @returns {Promise<void>} | ||
*/ | ||
static async initializeWithProvider(clientOptions, provider, identifiers, customAttributes) { | ||
if (FeatureGates.initPromise) { | ||
if (!FeatureGates.shallowEquals(clientOptions, FeatureGates.initOptions)) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Feature Gates client already initialized with different options. New options were not applied.'); | ||
} | ||
return FeatureGates.initPromise; | ||
} | ||
const startTime = performance.now(); | ||
FeatureGates.initOptions = clientOptions; | ||
FeatureGates.subscriptions = new Subscriptions(); | ||
FeatureGates.provider = provider; | ||
FeatureGates.provider.setClientVersion(CLIENT_VERSION); | ||
if (FeatureGates.provider.setApplyUpdateCallback) { | ||
FeatureGates.provider.setApplyUpdateCallback(experimentsResult => { | ||
Statsig.setInitializeValues(experimentsResult.experimentValues); | ||
FeatureGates.subscriptions.anyUpdated(); | ||
}); | ||
} | ||
FeatureGates.initPromise = FeatureGates.initWithProvider(clientOptions, identifiers, customAttributes).then(() => { | ||
FeatureGates.initCompleted = true; | ||
}).finally(() => { | ||
const endTime = performance.now(); | ||
const totalTime = endTime - startTime; | ||
FeatureGates.fireClientEvent(startTime, totalTime, 'initialize', FeatureGates.initCompleted, provider.getApiKey ? provider.getApiKey() : undefined); | ||
}); | ||
return FeatureGates.initPromise; | ||
} | ||
static fireClientEvent(startTime, totalTime, action, success, apiKey = undefined) { | ||
@@ -121,2 +169,14 @@ var _FeatureGates$initOpt; | ||
/** | ||
* This method updates the user using the provider given on initialisation to get the new set of values | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
*/ | ||
static async updateUserWithProvider(identifiers, customAttributes) { | ||
if (!FeatureGates.provider) { | ||
throw new Error('Cannot update user using provider as the client was not initialised with a provider'); | ||
} | ||
await FeatureGates.updateUserUsingInitializeValuesProducer(() => FeatureGates.provider.getExperimentValues(FeatureGates.initOptions, identifiers, customAttributes), identifiers, customAttributes); | ||
} | ||
/** | ||
* This method updates the user given a new set of bootstrap values obtained from one of the server-side SDKs. | ||
@@ -388,3 +448,3 @@ * | ||
* | ||
* If this method returns false, then the {@link FeatureGates.updateUser} or {@link FeatureGates.updateUserWithValues} | ||
* If this method returns false, then the {@link FeatureGates.updateUser}, {@link FeatureGates.updateUserWithValues} or {@link FeatureGates.updateUserWithProvider} | ||
* methods can be used to re-align these values. | ||
@@ -401,2 +461,65 @@ * | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current checkGate value | ||
* @param gateName | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onGateUpdated(gateName, callback, options = {}) { | ||
const wrapCallback = value => { | ||
const { | ||
fireGateExposure = true | ||
} = options; | ||
if (fireGateExposure) { | ||
FeatureGates.manuallyLogGateExposure(gateName); | ||
} | ||
try { | ||
callback(value); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn(`Error calling callback for gate ${gateName} with value ${value}`, error); | ||
} | ||
}; | ||
return FeatureGates.subscriptions.onGateUpdated(gateName, wrapCallback, FeatureGates.checkGate, options); | ||
} | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current experiment value | ||
* @param experimentName | ||
* @param parameterName | ||
* @param defaultValue | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onExperimentValueUpdated(experimentName, parameterName, defaultValue, callback, options = {}) { | ||
const wrapCallback = value => { | ||
const { | ||
fireExperimentExposure = true | ||
} = options; | ||
if (fireExperimentExposure) { | ||
FeatureGates.manuallyLogExperimentExposure(experimentName); | ||
} | ||
try { | ||
callback(value); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn(`Error calling callback for experiment ${experimentName} with value ${value}`, error); | ||
} | ||
}; | ||
return FeatureGates.subscriptions.onExperimentValueUpdated(experimentName, parameterName, defaultValue, wrapCallback, FeatureGates.getExperimentValue, options); | ||
} | ||
/** | ||
* Subscribe so on any update the callback will be called. | ||
* NOTE: The callback will be called whenever the values are updated even if the values have not changed. | ||
* @param callback | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onAnyUpdated(callback) { | ||
var _FeatureGates$subscri; | ||
return (_FeatureGates$subscri = FeatureGates.subscriptions) === null || _FeatureGates$subscri === void 0 ? void 0 : _FeatureGates$subscri.onAnyUpdated(callback); | ||
} | ||
/** | ||
* This method initializes the client using a network call to fetch the bootstrap values for the given user. | ||
@@ -439,3 +562,33 @@ * | ||
} | ||
static async initWithProvider(baseClientOptions, identifiers, customAttributes) { | ||
const fromValuesClientOptions = { | ||
...baseClientOptions, | ||
disableCurrentPageLogging: true | ||
}; | ||
let experimentValues; | ||
let customAttributesFromResult; | ||
try { | ||
// If client sdk key fetch fails, an error would be thrown and handled instead of waiting for the experiment | ||
// values request to be settled, and it will fall back to use default values. | ||
const clientSdkKeyPromise = FeatureGates.provider.getClientSdkKey(baseClientOptions).then(value => fromValuesClientOptions.sdkKey = value); | ||
const experimentValuesPromise = FeatureGates.provider.getExperimentValues(baseClientOptions, identifiers, customAttributes); | ||
// Only wait for the experiment values request to finish and try to initialise the client with experiment | ||
// values if both requests are successful. Else an error would be thrown and handled by the catch | ||
const [, experimentValuesResult] = await Promise.all([clientSdkKeyPromise, experimentValuesPromise]); | ||
experimentValues = experimentValuesResult.experimentValues; | ||
customAttributesFromResult = experimentValuesResult.customAttributesFromFetch; | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error(`Error occurred when trying to fetch the Feature Gates client values, error: ${error === null || error === void 0 ? void 0 : error.message}`); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn(`Initialising Statsig client without values`); | ||
await FeatureGates.initFromValues(fromValuesClientOptions, identifiers, customAttributes); | ||
throw error; | ||
} | ||
await this.initFromValues(fromValuesClientOptions, identifiers, customAttributesFromResult, experimentValues); | ||
} | ||
/** | ||
@@ -752,6 +905,7 @@ * This method initializes the client using a set of boostrap values obtained from one of the server-side SDKs. | ||
_defineProperty(FeatureGates, "hasCheckGateErrorOccurred", false); | ||
_defineProperty(FeatureGates, "subscriptions", new Subscriptions()); | ||
let boundFGJS = FeatureGates; | ||
// This makes it possible to get a reference to the FeatureGates client at runtime. | ||
// This is important for overriding values in Cyprus tests, as there needs to be a | ||
// This is important for overriding values in Cypress tests, as there needs to be a | ||
// way to get the exact instance for a window in order to mock some of its methods. | ||
@@ -758,0 +912,0 @@ if (typeof window !== 'undefined') { |
@@ -6,3 +6,3 @@ /** | ||
/** | ||
* Base client options. | ||
* Base client options. Does not include any options specific to providers | ||
* @interface BaseClientOptions | ||
@@ -9,0 +9,0 @@ * @property {FeatureGateEnvironment} environment - The environment for the client. |
@@ -1,1 +0,2 @@ | ||
export const CLIENT_VERSION = "4.19.0"; | ||
/// <reference types="node" /> | ||
export const CLIENT_VERSION = "4.20.0"; |
@@ -1,3 +0,3 @@ | ||
export { default, FeatureGateEnvironment, PerimeterType, | ||
export { default, FeatureGateEnvironment, PerimeterType, CLIENT_VERSION, | ||
// Statsig | ||
DynamicConfig, EvaluationReason } from './client'; |
@@ -12,2 +12,3 @@ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; | ||
import Statsig, { DynamicConfig, EvaluationReason, Layer } from 'statsig-js-lite'; | ||
import Subscriptions from '../subscriptions'; | ||
import Fetcher from './fetcher'; | ||
@@ -18,2 +19,3 @@ import { FeatureGateEnvironment, PerimeterType } from './types'; | ||
export { FeatureGateEnvironment, PerimeterType } from './types'; | ||
export { CLIENT_VERSION } from './version'; | ||
var DEFAULT_CLIENT_KEY = 'client-default-key'; | ||
@@ -88,2 +90,68 @@ // the default event logging api is the Atlassian proxy rather than Statsig's domain, to avoid ad blockers | ||
return initialize; | ||
}() | ||
/** | ||
* @description | ||
* This method initializes the client using the provider given to call to fetch the bootstrap values. | ||
* If the client is initialized with an `analyticsWebClient`, it will send an operational event | ||
* to GASv3 with the following attributes: | ||
* - targetApp: the target app of the client | ||
* - clientVersion: the version of the client | ||
* - success: whether the initialization was successful | ||
* - startTime: the time when the initialization started | ||
* - totalTime: the total time it took to initialize the client | ||
* - apiKey: the api key used to initialize the client | ||
* @param clientOptions {ClientOptions} | ||
* @param provider {Provider} | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
* @returns {Promise<void>} | ||
*/ | ||
) | ||
}, { | ||
key: "initializeWithProvider", | ||
value: (function () { | ||
var _initializeWithProvider = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(clientOptions, provider, identifiers, customAttributes) { | ||
var startTime; | ||
return _regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
while (1) switch (_context2.prev = _context2.next) { | ||
case 0: | ||
if (!FeatureGates.initPromise) { | ||
_context2.next = 3; | ||
break; | ||
} | ||
if (!FeatureGates.shallowEquals(clientOptions, FeatureGates.initOptions)) { | ||
// eslint-disable-next-line no-console | ||
console.warn('Feature Gates client already initialized with different options. New options were not applied.'); | ||
} | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
case 3: | ||
startTime = performance.now(); | ||
FeatureGates.initOptions = clientOptions; | ||
FeatureGates.subscriptions = new Subscriptions(); | ||
FeatureGates.provider = provider; | ||
FeatureGates.provider.setClientVersion(CLIENT_VERSION); | ||
if (FeatureGates.provider.setApplyUpdateCallback) { | ||
FeatureGates.provider.setApplyUpdateCallback(function (experimentsResult) { | ||
Statsig.setInitializeValues(experimentsResult.experimentValues); | ||
FeatureGates.subscriptions.anyUpdated(); | ||
}); | ||
} | ||
FeatureGates.initPromise = FeatureGates.initWithProvider(clientOptions, identifiers, customAttributes).then(function () { | ||
FeatureGates.initCompleted = true; | ||
}).finally(function () { | ||
var endTime = performance.now(); | ||
var totalTime = endTime - startTime; | ||
FeatureGates.fireClientEvent(startTime, totalTime, 'initialize', FeatureGates.initCompleted, provider.getApiKey ? provider.getApiKey() : undefined); | ||
}); | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
case 11: | ||
case "end": | ||
return _context2.stop(); | ||
} | ||
}, _callee2); | ||
})); | ||
function initializeWithProvider(_x4, _x5, _x6, _x7) { | ||
return _initializeWithProvider.apply(this, arguments); | ||
} | ||
return initializeWithProvider; | ||
}()) | ||
@@ -122,12 +190,12 @@ }, { | ||
value: function () { | ||
var _initializeFromValues = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(clientOptions, identifiers, customAttributes) { | ||
var _initializeFromValues = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(clientOptions, identifiers, customAttributes) { | ||
var initializeValues, | ||
startTime, | ||
_args2 = arguments; | ||
return _regeneratorRuntime.wrap(function _callee2$(_context2) { | ||
while (1) switch (_context2.prev = _context2.next) { | ||
_args3 = arguments; | ||
return _regeneratorRuntime.wrap(function _callee3$(_context3) { | ||
while (1) switch (_context3.prev = _context3.next) { | ||
case 0: | ||
initializeValues = _args2.length > 3 && _args2[3] !== undefined ? _args2[3] : {}; | ||
initializeValues = _args3.length > 3 && _args3[3] !== undefined ? _args3[3] : {}; | ||
if (!FeatureGates.initPromise) { | ||
_context2.next = 4; | ||
_context3.next = 4; | ||
break; | ||
@@ -139,3 +207,3 @@ } | ||
} | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
return _context3.abrupt("return", FeatureGates.initPromise); | ||
case 4: | ||
@@ -151,10 +219,10 @@ startTime = performance.now(); | ||
}); | ||
return _context2.abrupt("return", FeatureGates.initPromise); | ||
return _context3.abrupt("return", FeatureGates.initPromise); | ||
case 8: | ||
case "end": | ||
return _context2.stop(); | ||
return _context3.stop(); | ||
} | ||
}, _callee2); | ||
}, _callee3); | ||
})); | ||
function initializeFromValues(_x4, _x5, _x6) { | ||
function initializeFromValues(_x8, _x9, _x10) { | ||
return _initializeFromValues.apply(this, arguments); | ||
@@ -173,6 +241,6 @@ } | ||
value: (function () { | ||
var _updateUser = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(fetchOptions, identifiers, customAttributes) { | ||
var _updateUser = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4(fetchOptions, identifiers, customAttributes) { | ||
var initializeValuesProducer; | ||
return _regeneratorRuntime.wrap(function _callee3$(_context3) { | ||
while (1) switch (_context3.prev = _context3.next) { | ||
return _regeneratorRuntime.wrap(function _callee4$(_context4) { | ||
while (1) switch (_context4.prev = _context4.next) { | ||
case 0: | ||
@@ -189,11 +257,11 @@ initializeValuesProducer = function initializeValuesProducer() { | ||
}; | ||
_context3.next = 3; | ||
_context4.next = 3; | ||
return FeatureGates.updateUserUsingInitializeValuesProducer(initializeValuesProducer, identifiers, customAttributes); | ||
case 3: | ||
case "end": | ||
return _context3.stop(); | ||
return _context4.stop(); | ||
} | ||
}, _callee3); | ||
}, _callee4); | ||
})); | ||
function updateUser(_x7, _x8, _x9) { | ||
function updateUser(_x11, _x12, _x13) { | ||
return _updateUser.apply(this, arguments); | ||
@@ -204,2 +272,36 @@ } | ||
/** | ||
* This method updates the user using the provider given on initialisation to get the new set of values | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
*/ | ||
) | ||
}, { | ||
key: "updateUserWithProvider", | ||
value: (function () { | ||
var _updateUserWithProvider = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5(identifiers, customAttributes) { | ||
return _regeneratorRuntime.wrap(function _callee5$(_context5) { | ||
while (1) switch (_context5.prev = _context5.next) { | ||
case 0: | ||
if (FeatureGates.provider) { | ||
_context5.next = 2; | ||
break; | ||
} | ||
throw new Error('Cannot update user using provider as the client was not initialised with a provider'); | ||
case 2: | ||
_context5.next = 4; | ||
return FeatureGates.updateUserUsingInitializeValuesProducer(function () { | ||
return FeatureGates.provider.getExperimentValues(FeatureGates.initOptions, identifiers, customAttributes); | ||
}, identifiers, customAttributes); | ||
case 4: | ||
case "end": | ||
return _context5.stop(); | ||
} | ||
}, _callee5); | ||
})); | ||
function updateUserWithProvider(_x14, _x15) { | ||
return _updateUserWithProvider.apply(this, arguments); | ||
} | ||
return updateUserWithProvider; | ||
}() | ||
/** | ||
* This method updates the user given a new set of bootstrap values obtained from one of the server-side SDKs. | ||
@@ -215,10 +317,10 @@ * | ||
value: (function () { | ||
var _updateUserWithValues = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee4(identifiers, customAttributes) { | ||
var _updateUserWithValues = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee6(identifiers, customAttributes) { | ||
var initializeValues, | ||
initializeValuesProducer, | ||
_args4 = arguments; | ||
return _regeneratorRuntime.wrap(function _callee4$(_context4) { | ||
while (1) switch (_context4.prev = _context4.next) { | ||
_args6 = arguments; | ||
return _regeneratorRuntime.wrap(function _callee6$(_context6) { | ||
while (1) switch (_context6.prev = _context6.next) { | ||
case 0: | ||
initializeValues = _args4.length > 2 && _args4[2] !== undefined ? _args4[2] : {}; | ||
initializeValues = _args6.length > 2 && _args6[2] !== undefined ? _args6[2] : {}; | ||
initializeValuesProducer = function initializeValuesProducer() { | ||
@@ -230,11 +332,11 @@ return Promise.resolve({ | ||
}; | ||
_context4.next = 4; | ||
_context6.next = 4; | ||
return FeatureGates.updateUserUsingInitializeValuesProducer(initializeValuesProducer, identifiers, customAttributes); | ||
case 4: | ||
case "end": | ||
return _context4.stop(); | ||
return _context6.stop(); | ||
} | ||
}, _callee4); | ||
}, _callee6); | ||
})); | ||
function updateUserWithValues(_x10, _x11) { | ||
function updateUserWithValues(_x16, _x17) { | ||
return _updateUserWithValues.apply(this, arguments); | ||
@@ -527,3 +629,3 @@ } | ||
* | ||
* If this method returns false, then the {@link FeatureGates.updateUser} or {@link FeatureGates.updateUserWithValues} | ||
* If this method returns false, then the {@link FeatureGates.updateUser}, {@link FeatureGates.updateUserWithValues} or {@link FeatureGates.updateUserWithProvider} | ||
* methods can be used to re-align these values. | ||
@@ -542,2 +644,71 @@ * | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current checkGate value | ||
* @param gateName | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
}, { | ||
key: "onGateUpdated", | ||
value: function onGateUpdated(gateName, callback) { | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
var wrapCallback = function wrapCallback(value) { | ||
var _options$fireGateExpo2 = options.fireGateExposure, | ||
fireGateExposure = _options$fireGateExpo2 === void 0 ? true : _options$fireGateExpo2; | ||
if (fireGateExposure) { | ||
FeatureGates.manuallyLogGateExposure(gateName); | ||
} | ||
try { | ||
callback(value); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn("Error calling callback for gate ".concat(gateName, " with value ").concat(value), error); | ||
} | ||
}; | ||
return FeatureGates.subscriptions.onGateUpdated(gateName, wrapCallback, FeatureGates.checkGate, options); | ||
} | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current experiment value | ||
* @param experimentName | ||
* @param parameterName | ||
* @param defaultValue | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
}, { | ||
key: "onExperimentValueUpdated", | ||
value: function onExperimentValueUpdated(experimentName, parameterName, defaultValue, callback) { | ||
var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; | ||
var wrapCallback = function wrapCallback(value) { | ||
var _options$fireExperime2 = options.fireExperimentExposure, | ||
fireExperimentExposure = _options$fireExperime2 === void 0 ? true : _options$fireExperime2; | ||
if (fireExperimentExposure) { | ||
FeatureGates.manuallyLogExperimentExposure(experimentName); | ||
} | ||
try { | ||
callback(value); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.warn("Error calling callback for experiment ".concat(experimentName, " with value ").concat(value), error); | ||
} | ||
}; | ||
return FeatureGates.subscriptions.onExperimentValueUpdated(experimentName, parameterName, defaultValue, wrapCallback, FeatureGates.getExperimentValue, options); | ||
} | ||
/** | ||
* Subscribe so on any update the callback will be called. | ||
* NOTE: The callback will be called whenever the values are updated even if the values have not changed. | ||
* @param callback | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
}, { | ||
key: "onAnyUpdated", | ||
value: function onAnyUpdated(callback) { | ||
var _FeatureGates$subscri; | ||
return (_FeatureGates$subscri = FeatureGates.subscriptions) === null || _FeatureGates$subscri === void 0 ? void 0 : _FeatureGates$subscri.onAnyUpdated(callback); | ||
} | ||
/** | ||
* This method initializes the client using a network call to fetch the bootstrap values for the given user. | ||
@@ -553,6 +724,6 @@ * | ||
value: (function () { | ||
var _init = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee5(clientOptions, identifiers, customAttributes) { | ||
var _init = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee7(clientOptions, identifiers, customAttributes) { | ||
var fromValuesClientOptions, experimentValues, customAttributesFromResult, clientSdkKeyPromise, experimentValuesPromise, _yield$Promise$all, _yield$Promise$all2, experimentValuesResult; | ||
return _regeneratorRuntime.wrap(function _callee5$(_context5) { | ||
while (1) switch (_context5.prev = _context5.next) { | ||
return _regeneratorRuntime.wrap(function _callee7$(_context7) { | ||
while (1) switch (_context7.prev = _context7.next) { | ||
case 0: | ||
@@ -562,3 +733,3 @@ fromValuesClientOptions = _objectSpread(_objectSpread({}, clientOptions), {}, { | ||
}); | ||
_context5.prev = 1; | ||
_context7.prev = 1; | ||
// If client sdk key fetch fails, an error would be thrown and handled instead of waiting for the experiment | ||
@@ -571,6 +742,6 @@ // values request to be settled, and it will fall back to use default values. | ||
// values if both requests are successful. Else an error would be thrown and handled by the catch | ||
_context5.next = 6; | ||
_context7.next = 6; | ||
return Promise.all([clientSdkKeyPromise, experimentValuesPromise]); | ||
case 6: | ||
_yield$Promise$all = _context5.sent; | ||
_yield$Promise$all = _context7.sent; | ||
_yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2); | ||
@@ -580,30 +751,86 @@ experimentValuesResult = _yield$Promise$all2[1]; | ||
customAttributesFromResult = experimentValuesResult.customAttributes; | ||
_context5.next = 20; | ||
_context7.next = 20; | ||
break; | ||
case 13: | ||
_context5.prev = 13; | ||
_context5.t0 = _context5["catch"](1); | ||
if (_context5.t0 instanceof Error) { | ||
_context7.prev = 13; | ||
_context7.t0 = _context7["catch"](1); | ||
if (_context7.t0 instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error("Error occurred when trying to fetch the Feature Gates client values, error: ".concat(_context5.t0 === null || _context5.t0 === void 0 ? void 0 : _context5.t0.message)); | ||
console.error("Error occurred when trying to fetch the Feature Gates client values, error: ".concat(_context7.t0 === null || _context7.t0 === void 0 ? void 0 : _context7.t0.message)); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn("Initialising Statsig client without values"); | ||
_context5.next = 19; | ||
_context7.next = 19; | ||
return FeatureGates.initFromValues(fromValuesClientOptions, identifiers, customAttributes); | ||
case 19: | ||
throw _context5.t0; | ||
throw _context7.t0; | ||
case 20: | ||
_context5.next = 22; | ||
_context7.next = 22; | ||
return this.initFromValues(fromValuesClientOptions, identifiers, customAttributesFromResult, experimentValues); | ||
case 22: | ||
case "end": | ||
return _context5.stop(); | ||
return _context7.stop(); | ||
} | ||
}, _callee5, this, [[1, 13]]); | ||
}, _callee7, this, [[1, 13]]); | ||
})); | ||
function init(_x12, _x13, _x14) { | ||
function init(_x18, _x19, _x20) { | ||
return _init.apply(this, arguments); | ||
} | ||
return init; | ||
}()) | ||
}, { | ||
key: "initWithProvider", | ||
value: function () { | ||
var _initWithProvider = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee8(baseClientOptions, identifiers, customAttributes) { | ||
var fromValuesClientOptions, experimentValues, customAttributesFromResult, clientSdkKeyPromise, experimentValuesPromise, _yield$Promise$all3, _yield$Promise$all4, experimentValuesResult; | ||
return _regeneratorRuntime.wrap(function _callee8$(_context8) { | ||
while (1) switch (_context8.prev = _context8.next) { | ||
case 0: | ||
fromValuesClientOptions = _objectSpread(_objectSpread({}, baseClientOptions), {}, { | ||
disableCurrentPageLogging: true | ||
}); | ||
_context8.prev = 1; | ||
// If client sdk key fetch fails, an error would be thrown and handled instead of waiting for the experiment | ||
// values request to be settled, and it will fall back to use default values. | ||
clientSdkKeyPromise = FeatureGates.provider.getClientSdkKey(baseClientOptions).then(function (value) { | ||
return fromValuesClientOptions.sdkKey = value; | ||
}); | ||
experimentValuesPromise = FeatureGates.provider.getExperimentValues(baseClientOptions, identifiers, customAttributes); // Only wait for the experiment values request to finish and try to initialise the client with experiment | ||
// values if both requests are successful. Else an error would be thrown and handled by the catch | ||
_context8.next = 6; | ||
return Promise.all([clientSdkKeyPromise, experimentValuesPromise]); | ||
case 6: | ||
_yield$Promise$all3 = _context8.sent; | ||
_yield$Promise$all4 = _slicedToArray(_yield$Promise$all3, 2); | ||
experimentValuesResult = _yield$Promise$all4[1]; | ||
experimentValues = experimentValuesResult.experimentValues; | ||
customAttributesFromResult = experimentValuesResult.customAttributesFromFetch; | ||
_context8.next = 20; | ||
break; | ||
case 13: | ||
_context8.prev = 13; | ||
_context8.t0 = _context8["catch"](1); | ||
if (_context8.t0 instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error("Error occurred when trying to fetch the Feature Gates client values, error: ".concat(_context8.t0 === null || _context8.t0 === void 0 ? void 0 : _context8.t0.message)); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn("Initialising Statsig client without values"); | ||
_context8.next = 19; | ||
return FeatureGates.initFromValues(fromValuesClientOptions, identifiers, customAttributes); | ||
case 19: | ||
throw _context8.t0; | ||
case 20: | ||
_context8.next = 22; | ||
return this.initFromValues(fromValuesClientOptions, identifiers, customAttributesFromResult, experimentValues); | ||
case 22: | ||
case "end": | ||
return _context8.stop(); | ||
} | ||
}, _callee8, this, [[1, 13]]); | ||
})); | ||
function initWithProvider(_x21, _x22, _x23) { | ||
return _initWithProvider.apply(this, arguments); | ||
} | ||
return initWithProvider; | ||
}() | ||
@@ -619,7 +846,6 @@ /** | ||
*/ | ||
) | ||
}, { | ||
key: "initFromValues", | ||
value: (function () { | ||
var _initFromValues = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee6(clientOptions, identifiers, customAttributes) { | ||
var _initFromValues = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee9(clientOptions, identifiers, customAttributes) { | ||
var initializeValues, | ||
@@ -629,7 +855,7 @@ user, | ||
statsigOptions, | ||
_args6 = arguments; | ||
return _regeneratorRuntime.wrap(function _callee6$(_context6) { | ||
while (1) switch (_context6.prev = _context6.next) { | ||
_args9 = arguments; | ||
return _regeneratorRuntime.wrap(function _callee9$(_context9) { | ||
while (1) switch (_context9.prev = _context9.next) { | ||
case 0: | ||
initializeValues = _args6.length > 3 && _args6[3] !== undefined ? _args6[3] : {}; | ||
initializeValues = _args9.length > 3 && _args9[3] !== undefined ? _args9[3] : {}; | ||
user = this.toStatsigUser(identifiers, customAttributes); | ||
@@ -650,18 +876,18 @@ FeatureGates.currentIdentifiers = identifiers; | ||
statsigOptions = this.toStatsigOptions(clientOptions, initializeValues); | ||
_context6.prev = 9; | ||
_context6.next = 12; | ||
_context9.prev = 9; | ||
_context9.next = 12; | ||
return Statsig.initialize(sdkKey, user, statsigOptions); | ||
case 12: | ||
_context6.next = 21; | ||
_context9.next = 21; | ||
break; | ||
case 14: | ||
_context6.prev = 14; | ||
_context6.t0 = _context6["catch"](9); | ||
if (_context6.t0 instanceof Error) { | ||
_context9.prev = 14; | ||
_context9.t0 = _context9["catch"](9); | ||
if (_context9.t0 instanceof Error) { | ||
// eslint-disable-next-line no-console | ||
console.error("Error occurred when trying to initialise the Statsig client, error: ".concat(_context6.t0 === null || _context6.t0 === void 0 ? void 0 : _context6.t0.message)); | ||
console.error("Error occurred when trying to initialise the Statsig client, error: ".concat(_context9.t0 === null || _context9.t0 === void 0 ? void 0 : _context9.t0.message)); | ||
} | ||
// eslint-disable-next-line no-console | ||
console.warn("Initialising Statsig client with default sdk key and without values"); | ||
_context6.next = 20; | ||
_context9.next = 20; | ||
return Statsig.initialize(DEFAULT_CLIENT_KEY, user, _objectSpread(_objectSpread({}, statsigOptions), {}, { | ||
@@ -671,10 +897,10 @@ initializeValues: {} | ||
case 20: | ||
throw _context6.t0; | ||
throw _context9.t0; | ||
case 21: | ||
case "end": | ||
return _context6.stop(); | ||
return _context9.stop(); | ||
} | ||
}, _callee6, this, [[9, 14]]); | ||
}, _callee9, this, [[9, 14]]); | ||
})); | ||
function initFromValues(_x15, _x16, _x17) { | ||
function initFromValues(_x24, _x25, _x26) { | ||
return _initFromValues.apply(this, arguments); | ||
@@ -718,9 +944,9 @@ } | ||
value: (function () { | ||
var _updateUserUsingInitializeValuesProducer = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee7(getInitializeValues, identifiers, customAttributes) { | ||
var _updateUserUsingInitializeValuesProducer = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee10(getInitializeValues, identifiers, customAttributes) { | ||
var originalInitPromise, initializeValuesPromise, updateUserPromise; | ||
return _regeneratorRuntime.wrap(function _callee7$(_context7) { | ||
while (1) switch (_context7.prev = _context7.next) { | ||
return _regeneratorRuntime.wrap(function _callee10$(_context10) { | ||
while (1) switch (_context10.prev = _context10.next) { | ||
case 0: | ||
if (FeatureGates.initPromise) { | ||
_context7.next = 2; | ||
_context10.next = 2; | ||
break; | ||
@@ -731,18 +957,18 @@ } | ||
if (!FeatureGates.isCurrentUser(identifiers, customAttributes)) { | ||
_context7.next = 4; | ||
_context10.next = 4; | ||
break; | ||
} | ||
return _context7.abrupt("return", FeatureGates.initPromise); | ||
return _context10.abrupt("return", FeatureGates.initPromise); | ||
case 4: | ||
// Wait for the current initialize/update to finish | ||
originalInitPromise = FeatureGates.initPromise; | ||
_context7.prev = 5; | ||
_context7.next = 8; | ||
_context10.prev = 5; | ||
_context10.next = 8; | ||
return FeatureGates.initPromise; | ||
case 8: | ||
_context7.next = 12; | ||
_context10.next = 12; | ||
break; | ||
case 10: | ||
_context7.prev = 10; | ||
_context7.t0 = _context7["catch"](5); | ||
_context10.prev = 10; | ||
_context10.t0 = _context10["catch"](5); | ||
case 12: | ||
@@ -756,10 +982,10 @@ initializeValuesPromise = getInitializeValues(); | ||
}); | ||
return _context7.abrupt("return", updateUserPromise); | ||
return _context10.abrupt("return", updateUserPromise); | ||
case 16: | ||
case "end": | ||
return _context7.stop(); | ||
return _context10.stop(); | ||
} | ||
}, _callee7, null, [[5, 10]]); | ||
}, _callee10, null, [[5, 10]]); | ||
})); | ||
function updateUserUsingInitializeValuesProducer(_x18, _x19, _x20) { | ||
function updateUserUsingInitializeValuesProducer(_x27, _x28, _x29) { | ||
return _updateUserUsingInitializeValuesProducer.apply(this, arguments); | ||
@@ -781,27 +1007,27 @@ } | ||
value: (function () { | ||
var _updateStatsigClientUser = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee8(initializeValuesPromise, identifiers, customAttributes) { | ||
var _updateStatsigClientUser = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee11(initializeValuesPromise, identifiers, customAttributes) { | ||
var initializeValues, user, _FeatureGates$initOpt2, _FeatureGates$initOpt3, errMsg, success; | ||
return _regeneratorRuntime.wrap(function _callee8$(_context8) { | ||
while (1) switch (_context8.prev = _context8.next) { | ||
return _regeneratorRuntime.wrap(function _callee11$(_context11) { | ||
while (1) switch (_context11.prev = _context11.next) { | ||
case 0: | ||
_context8.prev = 0; | ||
_context8.next = 3; | ||
_context11.prev = 0; | ||
_context11.next = 3; | ||
return initializeValuesPromise; | ||
case 3: | ||
initializeValues = _context8.sent; | ||
initializeValues = _context11.sent; | ||
user = FeatureGates.toStatsigUser(identifiers, initializeValues.customAttributesFromFetch); | ||
_context8.next = 12; | ||
_context11.next = 12; | ||
break; | ||
case 7: | ||
_context8.prev = 7; | ||
_context8.t0 = _context8["catch"](0); | ||
_context11.prev = 7; | ||
_context11.t0 = _context11["catch"](0); | ||
// Make sure the updateUserCompletionCallback is called for any errors in our custom code. | ||
// This is not necessary for the updateUserWithValues call, because the Statsig client will already invoke the callback itself. | ||
errMsg = _context8.t0 instanceof Error ? _context8.t0.message : JSON.stringify(_context8.t0); | ||
errMsg = _context11.t0 instanceof Error ? _context11.t0.message : JSON.stringify(_context11.t0); | ||
(_FeatureGates$initOpt2 = (_FeatureGates$initOpt3 = FeatureGates.initOptions).updateUserCompletionCallback) === null || _FeatureGates$initOpt2 === void 0 || _FeatureGates$initOpt2.call(_FeatureGates$initOpt3, false, errMsg); | ||
throw _context8.t0; | ||
throw _context11.t0; | ||
case 12: | ||
success = Statsig.updateUserWithValues(user, initializeValues.experimentValues); | ||
if (!success) { | ||
_context8.next = 18; | ||
_context11.next = 18; | ||
break; | ||
@@ -811,3 +1037,3 @@ } | ||
FeatureGates.currentAttributes = customAttributes; | ||
_context8.next = 19; | ||
_context11.next = 19; | ||
break; | ||
@@ -818,7 +1044,7 @@ case 18: | ||
case "end": | ||
return _context8.stop(); | ||
return _context11.stop(); | ||
} | ||
}, _callee8, null, [[0, 7]]); | ||
}, _callee11, null, [[0, 7]]); | ||
})); | ||
function updateStatsigClientUser(_x21, _x22, _x23) { | ||
function updateStatsigClientUser(_x30, _x31, _x32) { | ||
return _updateStatsigClientUser.apply(this, arguments); | ||
@@ -1019,6 +1245,7 @@ } | ||
_defineProperty(FeatureGates, "hasCheckGateErrorOccurred", false); | ||
_defineProperty(FeatureGates, "subscriptions", new Subscriptions()); | ||
var boundFGJS = FeatureGates; | ||
// This makes it possible to get a reference to the FeatureGates client at runtime. | ||
// This is important for overriding values in Cyprus tests, as there needs to be a | ||
// This is important for overriding values in Cypress tests, as there needs to be a | ||
// way to get the exact instance for a window in order to mock some of its methods. | ||
@@ -1025,0 +1252,0 @@ if (typeof window !== 'undefined') { |
@@ -6,3 +6,3 @@ /** | ||
/** | ||
* Base client options. | ||
* Base client options. Does not include any options specific to providers | ||
* @interface BaseClientOptions | ||
@@ -9,0 +9,0 @@ * @property {FeatureGateEnvironment} environment - The environment for the client. |
@@ -1,1 +0,2 @@ | ||
export var CLIENT_VERSION = "4.19.0"; | ||
/// <reference types="node" /> | ||
export var CLIENT_VERSION = "4.20.0"; |
@@ -1,3 +0,3 @@ | ||
export { default, FeatureGateEnvironment, PerimeterType, | ||
export { default, FeatureGateEnvironment, PerimeterType, CLIENT_VERSION, | ||
// Statsig | ||
DynamicConfig, EvaluationReason } from './client'; |
import { DynamicConfig, Layer, type LocalOverrides } from 'statsig-js-lite'; | ||
import { type FetcherOptions } from './fetcher'; | ||
import { type CheckGateOptions, type ClientOptions, type CustomAttributes, type FromValuesClientOptions, type GetExperimentOptions, type GetExperimentValueOptions, type GetLayerOptions, type GetLayerValueOptions, type Identifiers } from './types'; | ||
import { type BaseClientOptions, type CheckGateOptions, type ClientOptions, type CustomAttributes, type FromValuesClientOptions, type GetExperimentOptions, type GetExperimentValueOptions, type GetLayerOptions, type GetLayerValueOptions, type Identifiers, type Provider } from './types'; | ||
export type { EvaluationDetails, LocalOverrides } from 'statsig-js-lite'; | ||
export { DynamicConfig, EvaluationReason } from 'statsig-js-lite'; | ||
export type { AnalyticsWebClient, ClientOptions, CustomAttributes, FromValuesClientOptions, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, } from './types'; | ||
export type { AnalyticsWebClient, BaseClientOptions, ClientOptions, CustomAttributes, FromValuesClientOptions, FrontendExperimentsResult, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, Provider, } from './types'; | ||
export { FeatureGateEnvironment, PerimeterType } from './types'; | ||
export { CLIENT_VERSION } from './version'; | ||
declare global { | ||
@@ -30,2 +31,4 @@ interface Window { | ||
private static hasCheckGateErrorOccurred; | ||
private static provider; | ||
private static subscriptions; | ||
/** | ||
@@ -48,2 +51,20 @@ * @description | ||
static initialize(clientOptions: ClientOptions, identifiers: Identifiers, customAttributes?: CustomAttributes): Promise<void>; | ||
/** | ||
* @description | ||
* This method initializes the client using the provider given to call to fetch the bootstrap values. | ||
* If the client is initialized with an `analyticsWebClient`, it will send an operational event | ||
* to GASv3 with the following attributes: | ||
* - targetApp: the target app of the client | ||
* - clientVersion: the version of the client | ||
* - success: whether the initialization was successful | ||
* - startTime: the time when the initialization started | ||
* - totalTime: the total time it took to initialize the client | ||
* - apiKey: the api key used to initialize the client | ||
* @param clientOptions {ClientOptions} | ||
* @param provider {Provider} | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
* @returns {Promise<void>} | ||
*/ | ||
static initializeWithProvider(clientOptions: BaseClientOptions, provider: Provider, identifiers: Identifiers, customAttributes?: CustomAttributes): Promise<void>; | ||
private static fireClientEvent; | ||
@@ -59,2 +80,8 @@ static initializeFromValues(clientOptions: FromValuesClientOptions, identifiers: Identifiers, customAttributes?: CustomAttributes, initializeValues?: Record<string, unknown>): Promise<void>; | ||
/** | ||
* This method updates the user using the provider given on initialisation to get the new set of values | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
*/ | ||
static updateUserWithProvider(identifiers: Identifiers, customAttributes?: CustomAttributes): Promise<void>; | ||
/** | ||
* This method updates the user given a new set of bootstrap values obtained from one of the server-side SDKs. | ||
@@ -210,3 +237,3 @@ * | ||
* | ||
* If this method returns false, then the {@link FeatureGates.updateUser} or {@link FeatureGates.updateUserWithValues} | ||
* If this method returns false, then the {@link FeatureGates.updateUser}, {@link FeatureGates.updateUserWithValues} or {@link FeatureGates.updateUserWithProvider} | ||
* methods can be used to re-align these values. | ||
@@ -220,2 +247,27 @@ * | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current checkGate value | ||
* @param gateName | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onGateUpdated(gateName: string, callback: (value: boolean) => void, options?: CheckGateOptions): () => void; | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current experiment value | ||
* @param experimentName | ||
* @param parameterName | ||
* @param defaultValue | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onExperimentValueUpdated<T>(experimentName: string, parameterName: string, defaultValue: T, callback: (value: T) => void, options?: GetExperimentValueOptions<T>): () => void; | ||
/** | ||
* Subscribe so on any update the callback will be called. | ||
* NOTE: The callback will be called whenever the values are updated even if the values have not changed. | ||
* @param callback | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onAnyUpdated(callback: () => void): () => void; | ||
/** | ||
* This method initializes the client using a network call to fetch the bootstrap values for the given user. | ||
@@ -229,2 +281,3 @@ * | ||
private static init; | ||
private static initWithProvider; | ||
/** | ||
@@ -231,0 +284,0 @@ * This method initializes the client using a set of boostrap values obtained from one of the server-side SDKs. |
@@ -39,3 +39,3 @@ import { type StatsigOptions } from 'statsig-js-lite'; | ||
/** | ||
* Base client options. | ||
* Base client options. Does not include any options specific to providers | ||
* @interface BaseClientOptions | ||
@@ -47,3 +47,3 @@ * @property {FeatureGateEnvironment} environment - The environment for the client. | ||
*/ | ||
interface BaseClientOptions extends Omit<FeatureGateOptions, 'environment' | 'initializeValues' | 'sdkKey' | 'updateUserCompletionCallback'> { | ||
export interface BaseClientOptions extends Omit<FeatureGateOptions, 'environment' | 'initializeValues' | 'sdkKey' | 'updateUserCompletionCallback'> { | ||
environment: FeatureGateEnvironment; | ||
@@ -71,2 +71,5 @@ targetApp: string; | ||
} | ||
export interface FrontendExperimentsResult extends InitializeValues { | ||
clientSdkKey?: string; | ||
} | ||
/** | ||
@@ -106,2 +109,12 @@ * The custom attributes for the user. | ||
}; | ||
export interface FrontendExperimentsResult extends InitializeValues { | ||
clientSdkKey?: string; | ||
} | ||
export interface Provider { | ||
setClientVersion: (clientVersion: string) => void; | ||
setApplyUpdateCallback?: (applyUpdate: (experimentsResult: FrontendExperimentsResult) => void) => void; | ||
getExperimentValues: (clientOptions: BaseClientOptions, identifiers: Identifiers, customAttributes?: CustomAttributes) => Promise<FrontendExperimentsResult>; | ||
getClientSdkKey: (clientOptions: BaseClientOptions) => Promise<string>; | ||
getApiKey?: () => string; | ||
} | ||
export {}; |
@@ -1,2 +0,2 @@ | ||
export type { AnalyticsWebClient, ClientOptions, CustomAttributes, FromValuesClientOptions, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, EvaluationDetails, LocalOverrides, } from './client'; | ||
export { default, FeatureGateEnvironment, PerimeterType, DynamicConfig, EvaluationReason, } from './client'; | ||
export type { AnalyticsWebClient, BaseClientOptions, ClientOptions, CustomAttributes, FrontendExperimentsResult, FromValuesClientOptions, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, Provider, EvaluationDetails, LocalOverrides, } from './client'; | ||
export { default, FeatureGateEnvironment, PerimeterType, CLIENT_VERSION, DynamicConfig, EvaluationReason, } from './client'; |
import { DynamicConfig, Layer, type LocalOverrides } from 'statsig-js-lite'; | ||
import { type FetcherOptions } from './fetcher'; | ||
import { type CheckGateOptions, type ClientOptions, type CustomAttributes, type FromValuesClientOptions, type GetExperimentOptions, type GetExperimentValueOptions, type GetLayerOptions, type GetLayerValueOptions, type Identifiers } from './types'; | ||
import { type BaseClientOptions, type CheckGateOptions, type ClientOptions, type CustomAttributes, type FromValuesClientOptions, type GetExperimentOptions, type GetExperimentValueOptions, type GetLayerOptions, type GetLayerValueOptions, type Identifiers, type Provider } from './types'; | ||
export type { EvaluationDetails, LocalOverrides } from 'statsig-js-lite'; | ||
export { DynamicConfig, EvaluationReason } from 'statsig-js-lite'; | ||
export type { AnalyticsWebClient, ClientOptions, CustomAttributes, FromValuesClientOptions, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, } from './types'; | ||
export type { AnalyticsWebClient, BaseClientOptions, ClientOptions, CustomAttributes, FromValuesClientOptions, FrontendExperimentsResult, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, Provider, } from './types'; | ||
export { FeatureGateEnvironment, PerimeterType } from './types'; | ||
export { CLIENT_VERSION } from './version'; | ||
declare global { | ||
@@ -30,2 +31,4 @@ interface Window { | ||
private static hasCheckGateErrorOccurred; | ||
private static provider; | ||
private static subscriptions; | ||
/** | ||
@@ -48,2 +51,20 @@ * @description | ||
static initialize(clientOptions: ClientOptions, identifiers: Identifiers, customAttributes?: CustomAttributes): Promise<void>; | ||
/** | ||
* @description | ||
* This method initializes the client using the provider given to call to fetch the bootstrap values. | ||
* If the client is initialized with an `analyticsWebClient`, it will send an operational event | ||
* to GASv3 with the following attributes: | ||
* - targetApp: the target app of the client | ||
* - clientVersion: the version of the client | ||
* - success: whether the initialization was successful | ||
* - startTime: the time when the initialization started | ||
* - totalTime: the total time it took to initialize the client | ||
* - apiKey: the api key used to initialize the client | ||
* @param clientOptions {ClientOptions} | ||
* @param provider {Provider} | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
* @returns {Promise<void>} | ||
*/ | ||
static initializeWithProvider(clientOptions: BaseClientOptions, provider: Provider, identifiers: Identifiers, customAttributes?: CustomAttributes): Promise<void>; | ||
private static fireClientEvent; | ||
@@ -59,2 +80,8 @@ static initializeFromValues(clientOptions: FromValuesClientOptions, identifiers: Identifiers, customAttributes?: CustomAttributes, initializeValues?: Record<string, unknown>): Promise<void>; | ||
/** | ||
* This method updates the user using the provider given on initialisation to get the new set of values | ||
* @param identifiers {Identifiers} | ||
* @param customAttributes {CustomAttributes} | ||
*/ | ||
static updateUserWithProvider(identifiers: Identifiers, customAttributes?: CustomAttributes): Promise<void>; | ||
/** | ||
* This method updates the user given a new set of bootstrap values obtained from one of the server-side SDKs. | ||
@@ -210,3 +237,3 @@ * | ||
* | ||
* If this method returns false, then the {@link FeatureGates.updateUser} or {@link FeatureGates.updateUserWithValues} | ||
* If this method returns false, then the {@link FeatureGates.updateUser}, {@link FeatureGates.updateUserWithValues} or {@link FeatureGates.updateUserWithProvider} | ||
* methods can be used to re-align these values. | ||
@@ -220,2 +247,27 @@ * | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current checkGate value | ||
* @param gateName | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onGateUpdated(gateName: string, callback: (value: boolean) => void, options?: CheckGateOptions): () => void; | ||
/** | ||
* Subscribe to updates where the given callback will be called with the current experiment value | ||
* @param experimentName | ||
* @param parameterName | ||
* @param defaultValue | ||
* @param callback | ||
* @param options | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onExperimentValueUpdated<T>(experimentName: string, parameterName: string, defaultValue: T, callback: (value: T) => void, options?: GetExperimentValueOptions<T>): () => void; | ||
/** | ||
* Subscribe so on any update the callback will be called. | ||
* NOTE: The callback will be called whenever the values are updated even if the values have not changed. | ||
* @param callback | ||
* @returns off function to unsubscribe from updates | ||
*/ | ||
static onAnyUpdated(callback: () => void): () => void; | ||
/** | ||
* This method initializes the client using a network call to fetch the bootstrap values for the given user. | ||
@@ -229,2 +281,3 @@ * | ||
private static init; | ||
private static initWithProvider; | ||
/** | ||
@@ -231,0 +284,0 @@ * This method initializes the client using a set of boostrap values obtained from one of the server-side SDKs. |
@@ -39,3 +39,3 @@ import { type StatsigOptions } from 'statsig-js-lite'; | ||
/** | ||
* Base client options. | ||
* Base client options. Does not include any options specific to providers | ||
* @interface BaseClientOptions | ||
@@ -47,3 +47,3 @@ * @property {FeatureGateEnvironment} environment - The environment for the client. | ||
*/ | ||
interface BaseClientOptions extends Omit<FeatureGateOptions, 'environment' | 'initializeValues' | 'sdkKey' | 'updateUserCompletionCallback'> { | ||
export interface BaseClientOptions extends Omit<FeatureGateOptions, 'environment' | 'initializeValues' | 'sdkKey' | 'updateUserCompletionCallback'> { | ||
environment: FeatureGateEnvironment; | ||
@@ -71,2 +71,5 @@ targetApp: string; | ||
} | ||
export interface FrontendExperimentsResult extends InitializeValues { | ||
clientSdkKey?: string; | ||
} | ||
/** | ||
@@ -106,2 +109,12 @@ * The custom attributes for the user. | ||
}; | ||
export interface FrontendExperimentsResult extends InitializeValues { | ||
clientSdkKey?: string; | ||
} | ||
export interface Provider { | ||
setClientVersion: (clientVersion: string) => void; | ||
setApplyUpdateCallback?: (applyUpdate: (experimentsResult: FrontendExperimentsResult) => void) => void; | ||
getExperimentValues: (clientOptions: BaseClientOptions, identifiers: Identifiers, customAttributes?: CustomAttributes) => Promise<FrontendExperimentsResult>; | ||
getClientSdkKey: (clientOptions: BaseClientOptions) => Promise<string>; | ||
getApiKey?: () => string; | ||
} | ||
export {}; |
@@ -1,2 +0,2 @@ | ||
export type { AnalyticsWebClient, ClientOptions, CustomAttributes, FromValuesClientOptions, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, EvaluationDetails, LocalOverrides, } from './client'; | ||
export { default, FeatureGateEnvironment, PerimeterType, DynamicConfig, EvaluationReason, } from './client'; | ||
export type { AnalyticsWebClient, BaseClientOptions, ClientOptions, CustomAttributes, FrontendExperimentsResult, FromValuesClientOptions, GetExperimentOptions, GetExperimentValueOptions, Identifiers, InitializeValues, UpdateUserCompletionCallback, Provider, EvaluationDetails, LocalOverrides, } from './client'; | ||
export { default, FeatureGateEnvironment, PerimeterType, CLIENT_VERSION, DynamicConfig, EvaluationReason, } from './client'; |
{ | ||
"name": "@atlaskit/feature-gate-js-client", | ||
"version": "4.19.0", | ||
"version": "4.20.0", | ||
"description": "Atlassians wrapper for the Statsig js-lite client.", | ||
@@ -35,6 +35,7 @@ "author": "Atlassian Pty Ltd", | ||
"@babel/runtime": "^7.0.0", | ||
"eventemitter2": "^4.1.0", | ||
"statsig-js-lite": "^1.4.0" | ||
}, | ||
"devDependencies": { | ||
"@atlassiansox/analytics-web-client": "^4.25.0", | ||
"@atlassiansox/analytics-web-client": "^4.26.0", | ||
"jest-fetch-mock": "^3.0.3", | ||
@@ -41,0 +42,0 @@ "typescript": "~5.4.2" |
621
README.md
@@ -9,13 +9,16 @@ # FeatureGateJsClient | ||
Detailed docs and example usage can be found [here](https://atlaskit.atlassian.com/packages/measurement/feature-gate-js-client). | ||
Detailed docs and example usage can be found | ||
[here](https://atlaskit.atlassian.com/packages/measurement/feature-gate-js-client). | ||
## What is this repository for? ## | ||
## What is this repository for? | ||
The js-client covers frontend feature gate use cases. | ||
This client is modelled around bootstrapping feature gate values from the backend and does not receive live updates. | ||
The js-client covers frontend feature gate use cases. This client is modelled around bootstrapping | ||
feature gate values from the backend and does not receive live updates. | ||
## Client usage ## | ||
## Client usage | ||
### Installation | ||
The client can be pulled from the Artifactory NPM repository. | ||
```shell | ||
@@ -26,10 +29,13 @@ yarn add @atlaskit/feature-gate-js-client | ||
### Initialization | ||
The client must be initialized before attempted usage or it will throw an error. | ||
There are two ways to initialize your client: | ||
There are three ways to initialize your client: | ||
#### 1. Default initialization mechanism | ||
This will initialize the client by calling out to feature-flag-service ([fx3](https://go.atlassian.com/fx3)), and bootstrapping the client | ||
with the returned values. If the client fails to initialize for any reason, including taking longer than 2 seconds to fetch the values, | ||
default values will be used. | ||
This will initialize the client by calling out to feature-flag-service | ||
([fx3](https://go.atlassian.com/fx3)), and bootstrapping the client with the returned values. If the | ||
client fails to initialize for any reason, including taking longer than 2 seconds to fetch the | ||
values, default values will be used. | ||
@@ -42,3 +48,3 @@ ```javascript | ||
{ | ||
// This is an fx3 api key used to fetch the feature flag values. | ||
// This is an fx3 api key used to fetch the feature flag values. | ||
// Supported keys found at go/fx3/resources/api-keys | ||
@@ -49,4 +55,4 @@ apiKey: 'client-test', | ||
// This will be used to filter data from Statsig to only one target app. | ||
// View [doc](https://hello.atlassian.net/wiki/spaces/MEASURE/pages/2955970231/How-to+Use+TargetApps+in+Statsig) | ||
// for details on using targetApp. | ||
// View [doc](https://hello.atlassian.net/wiki/spaces/MEASURE/pages/2955970231/How-to+Use+TargetApps+in+Statsig) | ||
// for details on using targetApp. | ||
targetApp: 'jira_web', | ||
@@ -77,3 +83,3 @@ // [Optional] Default is 2000ms | ||
trelloWorkspaceId: '<trelloWorkspaceId>' | ||
}, | ||
}, | ||
{ | ||
@@ -89,5 +95,11 @@ // These are additional custom attributes that can be used for targeting and will be added to exposure events | ||
If your application has a log-in flow or other mechanism that makes it possible for the user to change during a session, then you can use the `updateUser` method to apply this change. The signature of this method is almost identical to `initialize`, except that it only requires options that relate to the network call it will perform to fetch the new set of values. | ||
If your application has a log-in flow or other mechanism that makes it possible for the user to | ||
change during a session, then you can use the `updateUser` method to apply this change. The | ||
signature of this method is almost identical to `initialize`, except that it only requires options | ||
that relate to the network call it will perform to fetch the new set of values. | ||
**IMPORTANT**: Calling this method will completely re-initialize the client with a new set of flags. You will need to re-render the entire page after this completes to ensure everything picks up the new flag values. You should avoid using this frequently as it has implications on the user experience. | ||
**IMPORTANT**: Calling this method will completely re-initialize the client with a new set of flags. | ||
You will need to re-render the entire page after this completes to ensure everything picks up the | ||
new flag values. You should avoid using this frequently as it has implications on the user | ||
experience. | ||
@@ -100,3 +112,3 @@ ```javascript | ||
{ | ||
// This is an fx3 api key used to fetch the feature flag values. | ||
// This is an fx3 api key used to fetch the feature flag values. | ||
// Supported keys found at go/fx3/resources/api-keys | ||
@@ -128,3 +140,3 @@ apiKey: 'client-test', | ||
trelloWorkspaceId: '<trelloWorkspaceId>' | ||
}, | ||
}, | ||
{ | ||
@@ -140,5 +152,7 @@ // These are additional custom attributes that can be used for targeting and will be added to exposure events | ||
``` | ||
#### 2. Initializing from values | ||
You must fetch the values yourself using one of our wrapper backend clients (Also found in this repo) and providing them to this frontend client | ||
You must fetch the values yourself using one of our wrapper backend clients (Also found in this | ||
repo) and providing them to this frontend client | ||
@@ -151,3 +165,3 @@ ```javascript | ||
{ | ||
// [Optional] This should come directly from a backend or service from the Statsig.getClientInitializeResponse(user) call | ||
// [Optional] This should come directly from a backend or service from the Statsig.getClientInitializeResponse(user) call | ||
// Or supplied from the statsig UI | ||
@@ -171,3 +185,3 @@ // Should be provided if it is available. It is optional so initialisation can still occur if the request to fetch the key fails | ||
trelloWorkspaceId: '<trelloWorkspaceId>' | ||
}, | ||
}, | ||
{ | ||
@@ -185,5 +199,11 @@ // These are additional custom attributes that will be added to exposure events | ||
If your application has a log-in flow or other mechanism that makes it possible for the user to change during a session, then you can use the `updateUserWithValues` method to apply this change. The signature of this method is almost identical to `initializeFromValues`, except that it does not require any options. | ||
If your application has a log-in flow or other mechanism that makes it possible for the user to | ||
change during a session, then you can use the `updateUserWithValues` method to apply this change. | ||
The signature of this method is almost identical to `initializeFromValues`, except that it does not | ||
require any options. | ||
**IMPORTANT**: Calling this method will completely re-initialize the client with a new set of flags. You will need to re-render the entire page after this completes to ensure everything picks up the new flag values. You should avoid using this frequently as it has implications on the user experience. | ||
**IMPORTANT**: Calling this method will completely re-initialize the client with a new set of flags. | ||
You will need to re-render the entire page after this completes to ensure everything picks up the | ||
new flag values. You should avoid using this frequently as it has implications on the user | ||
experience. | ||
@@ -204,3 +224,3 @@ ```javascript | ||
trelloWorkspaceId: '<trelloWorkspaceId>' | ||
}, | ||
}, | ||
{ | ||
@@ -217,12 +237,104 @@ // These are additional custom attributes that will be added to exposure events | ||
``` | ||
--- | ||
If there are any issues during initialization, then the client will be put in a mode which always returns default values, and a rejected promise will be returned. You can catch this rejected promise if you wish to record your own logs and metrics, or if you wish to stop your application from loading with the defaults. | ||
If there are any issues during initialization, then the client will be put in a mode which always | ||
returns default values, and a rejected promise will be returned. You can catch this rejected promise | ||
if you wish to record your own logs and metrics, or if you wish to stop your application from | ||
loading with the defaults. | ||
There is only once instance of the FeatureGates client, so only the first initialize call will start the initialization. Any subsequent calls will return the existing Promise for the first initialization, and the argument values will be ignored. In order to confirm whether the client has started to initialize already you can call | ||
`FeatureGates.initializeCalled()` | ||
There is only once instance of the FeatureGates client, so only the first initialize call will start | ||
the initialization. Any subsequent calls will return the existing Promise for the first | ||
initialization, and the argument values will be ignored. In order to confirm whether the client has | ||
started to initialize already you can call `FeatureGates.initializeCalled()` | ||
#### 3. Initializing using a Provider | ||
This initialization is done using an implementation of the Provider in order to fetch the client sdk key and experiment values needed. | ||
Supported providers are: | ||
* `@atlaskit/feature-gate-single-fetch-provider` | ||
* `@atlaskit/feature-gate-polling-provider` | ||
```javascript | ||
import FeatureGates, { FeatureGateEnvironment, FeatureGateProducts } from '@atlaskit/feature-gate-js-client'; | ||
try { | ||
await FeatureGates.initializeWithProvider( | ||
{ | ||
// This is an fx3 api key used to fetch the feature flag values. | ||
// Supported keys found at go/fx3/resources/api-keys | ||
apiKey: 'client-test', | ||
// This is the environment that you are operating in, targeting rules can target specific environments | ||
environment: FeatureGateEnvironment.Production, | ||
// This will be used to filter data from Statsig to only one target app. | ||
// View [doc](https://hello.atlassian.net/wiki/spaces/MEASURE/pages/2955970231/How-to+Use+TargetApps+in+Statsig) | ||
// for details on using targetApp. | ||
targetApp: 'jira_web', | ||
// [Optional] Must be one of the strings from the exported enum PerimeterType. | ||
// If provided, will build base url for the `feature-flag-service` based on environment and perimeter type, and | ||
// will disable all logging to Statsig in perimeters where it is prohibited.. | ||
perimeter: 'fedramp-moderate' | ||
}, | ||
new Provider(...), | ||
{ | ||
// These are expected identifiers, you must provide them if they are relevant to your product and will be added to exposures events | ||
analyticsAnonymousId: '<analyticsAnonymousId>', | ||
atlassianAccountId: '<aaid>', | ||
atlassianOrgId: '<orgid>', | ||
tenantId: '<tenantid>', | ||
transactionAccountId: '<transactionAccountId'>, | ||
trelloUserId: '<trelloUserId'>, | ||
trelloWorkspaceId: '<trelloWorkspaceId>' | ||
}, | ||
{ | ||
// These are additional custom attributes that can be used for targeting and will be added to exposure events | ||
exampleCustomAttribute: '<attributeValue>', | ||
} | ||
); | ||
} catch (err) { | ||
console.error("Failed to initialize FeatureGates client.", err); | ||
} | ||
``` | ||
If your application has a log-in flow or other mechanism that makes it possible for the user to | ||
change during a session, then you can use the `updateUserWithProvider` method to apply this change. This method will use the same provider and options provided in `initializeWithProvider`. It takes the identifiers and custom attributes of the new user. | ||
that relate to the network call it will perform to fetch the new set of values. | ||
**IMPORTANT**: Calling this method will completely re-initialize the client with a new set of flags. | ||
You will need to re-render the entire page after this completes to ensure everything picks up the | ||
new flag values. You should avoid using this frequently as it has implications on the user | ||
experience. | ||
```javascript | ||
import FeatureGates, { FeatureGateEnvironment, FeatureGateProducts } from '@atlaskit/feature-gate-js-client'; | ||
try { | ||
await FeatureGates.updateUserWithProvider( | ||
{ | ||
// These are expected identifiers, you must provide them if they are relevant to your product and will be added to exposures events | ||
analyticsAnonymousId: '<analyticsAnonymousId>', | ||
atlassianAccountId: '<aaid>', | ||
atlassianOrgId: '<orgid>', | ||
tenantId: '<tenantid>', | ||
transactionAccountId: '<transactionAccountId'>, | ||
trelloUserId: '<trelloUserId'>, | ||
trelloWorkspaceId: '<trelloWorkspaceId>' | ||
}, | ||
{ | ||
// These are additional custom attributes that can be used for targeting and will be added to exposure events | ||
exampleCustomAttribute: '<attributeValue>', | ||
} | ||
); | ||
} catch (err) { | ||
console.error("Failed to update the FeatureGates user.", err); | ||
} | ||
``` | ||
### Evaluation | ||
In order to evaluate a gate | ||
```javascript | ||
@@ -233,3 +345,3 @@ import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
if (FeatureGates.checkGate('gateName')) { | ||
// do something here | ||
// do something here | ||
} | ||
@@ -239,2 +351,3 @@ ``` | ||
In order to evaluate an experiment | ||
```javascript | ||
@@ -245,3 +358,3 @@ import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
if (FeatureGates.getExperimentValue('myExperiment', 'myBooleanParameter', false)) { | ||
// do something here | ||
// do something here | ||
} | ||
@@ -251,2 +364,3 @@ ``` | ||
In order to use more complex experiment configuration | ||
```typescript | ||
@@ -257,21 +371,83 @@ import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
// If this function does not pass, the default value will be returned instead. | ||
const isHexCode = (value: unknown) => typeof value === 'string' && value.startsWith('#') && value.length === 7; | ||
const isHexCode = (value: unknown) => | ||
typeof value === 'string' && value.startsWith('#') && value.length === 7; | ||
// Note: this call will automatically fire an exposure event. You can provide "fireExposureEvent: false" in the options if you wish to suppress it. | ||
const buttonColor: string = FeatureGates.getExperimentValue('myExperiment', 'myButtonColorStringParameter', '#000000', { | ||
typeGuard: isHexCode | ||
}); | ||
const buttonColor: string = FeatureGates.getExperimentValue( | ||
'myExperiment', | ||
'myButtonColorStringParameter', | ||
'#000000', | ||
{ | ||
typeGuard: isHexCode, | ||
}, | ||
); | ||
``` | ||
#### Exposure Event Logging | ||
Exposure events are batched and sent to Statsig every 10 seconds. Statsig's domain for their event logging API is | ||
blocked by some ad blockers, so by default we are proxying these requests through `xp.atlassian.com` to reduce exposure | ||
loss. | ||
Exposure events are batched and sent to Statsig every 10 seconds. Statsig's domain for their event | ||
logging API is blocked by some ad blockers, so by default we are proxying these requests through | ||
`xp.atlassian.com` to reduce exposure loss. | ||
### Subscriptions | ||
To subscribe to changes to gates. The callback will be called when the check gate value changes. | ||
```typescript | ||
import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
const unsubscribe = FeatureGates.onGateUpdated( | ||
'gateName', | ||
() => {} | ||
); | ||
// To unsubscribe | ||
unsubscribe(); | ||
``` | ||
To subscribe to changes to experiment values. The callback will be called when the experiment value changes. | ||
```typescript | ||
import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
const unsubscribe = FeatureGates.onExperimentValueUpdated( | ||
'myExperiment', | ||
'myButtonColorStringParameter', | ||
'#000000', | ||
() => {} | ||
{ | ||
typeGuard: isHexCode, | ||
}, | ||
); | ||
// To unsubscribe | ||
unsubscribe(); | ||
``` | ||
To subscribe to whenever a new set of values is updated on the client, no matter if the underlying values have changed. | ||
```typescript | ||
import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
// Note: The callback will be called whenever a new set of values is set even if none of the values in the set have changed. | ||
const unsubscribe = FeatureGates.onAnyUpdated( | ||
() => {} | ||
); | ||
// To unsubscribe | ||
unsubscribe(); | ||
``` | ||
## Testing | ||
### Jest | ||
#### Testing initialization states | ||
You can test the various initialization states by mocking the return values for `initialize` and `updateUser`. | ||
Note that you will also need to mock `initializeCalled`, as this is usually updated by the real `initialize` function. | ||
You can test the various initialization states by mocking the return values for `initialize` and | ||
`updateUser`. Note that you will also need to mock `initializeCalled`, as this is usually updated by | ||
the real `initialize` function. | ||
```typescript | ||
@@ -281,6 +457,6 @@ import FeatureGates from '@atlaskit/feature-gate-js-client'; | ||
jest.mock('@atlaskit/feature-gate-js-client', () => ({ | ||
...jest.requireActual('@atlaskit/feature-gate-js-client'), | ||
initializeCalled: jest.fn(), | ||
initialize: jest.fn(), | ||
updateUser: jest.fn(), | ||
...jest.requireActual('@atlaskit/feature-gate-js-client'), | ||
initializeCalled: jest.fn(), | ||
initialize: jest.fn(), | ||
updateUser: jest.fn(), | ||
})); | ||
@@ -291,37 +467,37 @@ | ||
describe('with successful initialization', () => { | ||
beforeEach(() => { | ||
MockedFeatureGates.initializeCalled.mockReturnValue(true); | ||
MockedFeatureGates.initialize.mockResolvedValue(); | ||
MockedFeatureGates.updateUser.mockResolvedValue(); | ||
}); | ||
beforeEach(() => { | ||
MockedFeatureGates.initializeCalled.mockReturnValue(true); | ||
MockedFeatureGates.initialize.mockResolvedValue(); | ||
MockedFeatureGates.updateUser.mockResolvedValue(); | ||
}); | ||
afterEach(() => jest.resetAllMocks()); | ||
afterEach(() => jest.resetAllMocks()); | ||
}); | ||
describe('with failed initialization', () => { | ||
beforeEach(() => { | ||
MockedFeatureGates.initializeCalled.mockReturnValue(true); | ||
MockedFeatureGates.initialize.mockRejectedValue(); | ||
MockedFeatureGates.updateUser.mockRejectedValue(); | ||
}); | ||
beforeEach(() => { | ||
MockedFeatureGates.initializeCalled.mockReturnValue(true); | ||
MockedFeatureGates.initialize.mockRejectedValue(); | ||
MockedFeatureGates.updateUser.mockRejectedValue(); | ||
}); | ||
afterEach(() => jest.resetAllMocks()); | ||
afterEach(() => jest.resetAllMocks()); | ||
}); | ||
describe('with pending initialization', () => { | ||
// These can be called within your tests transition from the pending | ||
// initialization state into a successful/failed state. | ||
let resolveInitPromise; | ||
let rejectInitPromise; | ||
beforeEach(() => { | ||
const initPromise = new Promise((resolve, reject) => { | ||
resolveInitPromise = resolve; | ||
rejectInitPromise = reject; | ||
}); | ||
MockedFeatureGates.initializeCalled.mockReturnValue(true); | ||
MockedFeatureGates.initialize.mockReturnValue(initPromise); | ||
MockedFeatureGates.updateUser.mockReturnValue(initPromise); | ||
}); | ||
// These can be called within your tests transition from the pending | ||
// initialization state into a successful/failed state. | ||
let resolveInitPromise; | ||
let rejectInitPromise; | ||
beforeEach(() => { | ||
const initPromise = new Promise((resolve, reject) => { | ||
resolveInitPromise = resolve; | ||
rejectInitPromise = reject; | ||
}); | ||
MockedFeatureGates.initializeCalled.mockReturnValue(true); | ||
MockedFeatureGates.initialize.mockReturnValue(initPromise); | ||
MockedFeatureGates.updateUser.mockReturnValue(initPromise); | ||
}); | ||
afterEach(() => jest.resetAllMocks()); | ||
afterEach(() => jest.resetAllMocks()); | ||
}); | ||
@@ -331,3 +507,5 @@ ``` | ||
#### Overriding values | ||
There are two ways that you can override values in Jest tests: | ||
1. Using mocks | ||
@@ -337,2 +515,3 @@ 2. Using the built-in override methods | ||
#### Using mocks | ||
```typescript | ||
@@ -342,5 +521,5 @@ import FeatureGates, { DynamicConfig, EvaluationReason } from '@atlaskit/feature-gate-js-client'; | ||
jest.mock('@atlaskit/feature-gate-js-client', () => ({ | ||
...jest.requireActual('@atlaskit/feature-gate-js-client'), | ||
getExperiment: jest.fn(), | ||
checkGate: jest.fn() | ||
...jest.requireActual('@atlaskit/feature-gate-js-client'), | ||
getExperiment: jest.fn(), | ||
checkGate: jest.fn(), | ||
})); | ||
@@ -350,29 +529,29 @@ | ||
describe("with mocked experiments and gates", () => { | ||
beforeEach(() => { | ||
const overrides = { | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation' | ||
} | ||
}, | ||
gates: { | ||
'example-gate': true | ||
} | ||
}; | ||
describe('with mocked experiments and gates', () => { | ||
beforeEach(() => { | ||
const overrides = { | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation', | ||
}, | ||
}, | ||
gates: { | ||
'example-gate': true, | ||
}, | ||
}; | ||
MockedFeatureGates.getExperiment.mockImplementation(experimentName => { | ||
const values = overrides.configs[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride | ||
}); | ||
}); | ||
MockedFeatureGates.getExperiment.mockImplementation((experimentName) => { | ||
const values = overrides.configs[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
}); | ||
MockedFeatureGates.checkGate.mockImplementation((gateName, defaultValue) => { | ||
return overrides.gates[gateName] || defaultValue; | ||
}); | ||
}); | ||
MockedFeatureGates.checkGate.mockImplementation((gateName, defaultValue) => { | ||
return overrides.gates[gateName] || defaultValue; | ||
}); | ||
}); | ||
afterEach(() => jest.resetAllMocks()); | ||
afterEach(() => jest.resetAllMocks()); | ||
}); | ||
@@ -382,31 +561,36 @@ ``` | ||
#### Using overrides methods | ||
```typescript | ||
import FeatureGates, { FeatureGateEnvironment } from '@atlaskit/feature-gate-js-client'; | ||
describe("with overridden gates and experiments", () => { | ||
beforeAll(async () => { | ||
// setOverrides can only be called if the client is _actually_ initialized. You can't mock the initialization, you will have invoke it properly. | ||
await FeatureGates.initializeWithValues({ | ||
environment: FeatureGateEnvironment.Development, | ||
sdkKey: 'client-default-key', | ||
localMode: true | ||
}, {}, {}); | ||
}); | ||
describe('with overridden gates and experiments', () => { | ||
beforeAll(async () => { | ||
// setOverrides can only be called if the client is _actually_ initialized. You can't mock the initialization, you will have invoke it properly. | ||
await FeatureGates.initializeWithValues( | ||
{ | ||
environment: FeatureGateEnvironment.Development, | ||
sdkKey: 'client-default-key', | ||
localMode: true, | ||
}, | ||
{}, | ||
{}, | ||
); | ||
}); | ||
beforeEach(() => { | ||
const overrides = { | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation' | ||
} | ||
}, | ||
gates: { | ||
'example-gate': true | ||
} | ||
}; | ||
beforeEach(() => { | ||
const overrides = { | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation', | ||
}, | ||
}, | ||
gates: { | ||
'example-gate': true, | ||
}, | ||
}; | ||
FeatureGates.setOverrides(overrides); | ||
}); | ||
FeatureGates.setOverrides(overrides); | ||
}); | ||
afterEach(() => FeatureGates.clearAllOverrides()); | ||
afterEach(() => FeatureGates.clearAllOverrides()); | ||
}); | ||
@@ -416,5 +600,8 @@ ``` | ||
### Cypress | ||
#### Overriding values | ||
The `.visit` command in Cypress creates a new window with its own instance of FeatureGates, so you will not be able to simply import the module and apply stubs to it. | ||
The `.visit` command in Cypress creates a new window with its own instance of FeatureGates, so you | ||
will not be able to simply import the module and apply stubs to it. | ||
```typescript | ||
@@ -426,34 +613,40 @@ // ❌ This will not work! | ||
const overrides: LocalOverrides = { | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation' | ||
} | ||
}, | ||
gates: { | ||
'example-gate': true | ||
} | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation', | ||
}, | ||
}, | ||
gates: { | ||
'example-gate': true, | ||
}, | ||
}; | ||
// These interact with the FeatureGates instance that your test framework is running in | ||
cy.stub(FeatureGates, 'checkGate', (gateName, defaultValue) => overrides.gates[gateName] || defaultValue); | ||
cy.stub( | ||
FeatureGates, | ||
'checkGate', | ||
(gateName, defaultValue) => overrides.gates[gateName] || defaultValue, | ||
); | ||
cy.stub(FeatureGates, 'getExperiment', (experimentName) => { | ||
const values = overrides.configs[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
const values = overrides.configs[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
}); | ||
// This will creates a new window, with its own FeatureGates instance. | ||
cy.visit("http://localhost:3000/"); | ||
cy.visit('http://localhost:3000/'); | ||
// The test feature will not exist since the stubs don't exist on the visited window. | ||
cy.get("#test-feature").dblclick(); | ||
cy.get('#test-feature').dblclick(); | ||
``` | ||
Instead, you will need to obtain a reference to the client that exists on the generated window, and apply your overrides to that instead. | ||
We have exposed a `window.__FEATUREGATES_JS__` variable which will contain the instance attached to the window. | ||
Instead, you will need to obtain a reference to the client that exists on the generated window, and | ||
apply your overrides to that instead. | ||
We have exposed a `window.__FEATUREGATES_JS__` variable which will contain the instance attached to | ||
the window. | ||
```typescript | ||
@@ -465,34 +658,40 @@ // ✅ Do this instead! | ||
const overrides: LocalOverrides = { | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation' | ||
} | ||
}, | ||
gates: { | ||
'example-gate': true | ||
} | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation', | ||
}, | ||
}, | ||
gates: { | ||
'example-gate': true, | ||
}, | ||
}; | ||
cy.visit('http://localhost:3001', { | ||
// onLoad provides a reference to the generated window, and it is also only invoked when all scripts have finished loading, so the __FEATUREGATES_JS__ | ||
// variable will be available at this point. | ||
onLoad: (contentWindow) => { | ||
const FeatureGates = contentWindow.__FEATUREGATES_JS__; | ||
// onLoad provides a reference to the generated window, and it is also only invoked when all scripts have finished loading, so the __FEATUREGATES_JS__ | ||
// variable will be available at this point. | ||
onLoad: (contentWindow) => { | ||
const FeatureGates = contentWindow.__FEATUREGATES_JS__; | ||
// Note that the client would not have been initialized by this point, so we can't use the override* or setOverrides methods. | ||
// We also don't want to wait until the initialization completes, because then the page may have already started to render without these overrides. | ||
cy.stub(FeatureGates, 'checkGate', (gateName, defaultValue) => overrides.gates[gateName] || defaultValue); | ||
// Note that the client would not have been initialized by this point, so we can't use the override* or setOverrides methods. | ||
// We also don't want to wait until the initialization completes, because then the page may have already started to render without these overrides. | ||
cy.stub( | ||
FeatureGates, | ||
'checkGate', | ||
(gateName, defaultValue) => overrides.gates[gateName] || defaultValue, | ||
); | ||
cy.stub(FeatureGates, 'getExperiment', (experimentName) => { | ||
const values = overrides.configs[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
}); | ||
} | ||
cy.stub(FeatureGates, 'getExperiment', (experimentName) => { | ||
const values = overrides.configs[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
}); | ||
}, | ||
}); | ||
``` | ||
You can also set up your own custom command which listens to the next `window:load` event to do the same thing, which you can invoke before any page visit: | ||
You can also set up your own custom command which listens to the next `window:load` event to do the | ||
same thing, which you can invoke before any page visit: | ||
```typescript | ||
@@ -502,17 +701,21 @@ import { LocalOverrides } from '@atlaskit/feature-gate-js-client'; | ||
Cypress.Commands.add('featureGateOverrides', (overrides: LocalOverrides) => { | ||
// Use cy.once instead of cy.on so that this only affects the next visit. | ||
cy.once('window:load', (contentWindow) => { | ||
const FeatureGates = contentWindow.__FEATUREGATES_JS__; | ||
// Use cy.once instead of cy.on so that this only affects the next visit. | ||
cy.once('window:load', (contentWindow) => { | ||
const FeatureGates = contentWindow.__FEATUREGATES_JS__; | ||
cy.stub(FeatureGates, 'checkGate', (gateName, defaultValue) => overrides?.gates?.[gateName] || defaultValue); | ||
cy.stub( | ||
FeatureGates, | ||
'checkGate', | ||
(gateName, defaultValue) => overrides?.gates?.[gateName] || defaultValue, | ||
); | ||
cy.stub(FeatureGates, 'getExperiment', (experimentName) => { | ||
const values = overrides?.configs?.[experimentName] || {}; | ||
cy.stub(FeatureGates, 'getExperiment', (experimentName) => { | ||
const values = overrides?.configs?.[experimentName] || {}; | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
}); | ||
}); | ||
return new DynamicConfig(experimentName, values, { | ||
time: Date.now(), | ||
reason: EvaluationReason.LocalOverride, | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -522,10 +725,10 @@ | ||
cy.featureGateOverrides({ | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation' | ||
} | ||
}, | ||
gates: { | ||
'example-gate': true | ||
} | ||
configs: { | ||
'example-experiment': { | ||
cohort: 'variation', | ||
}, | ||
}, | ||
gates: { | ||
'example-gate': true, | ||
}, | ||
}).visit('http://localhost:3001'); | ||
@@ -535,35 +738,51 @@ ``` | ||
### Storybook | ||
#### Overriding values | ||
Storybook does not have any mocking or stubbing APIs, but you can use the `override*` and `setOverrides` methods on this client as a replacement. | ||
Please note that the client must be initialized before these methods can be called, and that the overrides will need to be cleared after each storybook is unmounted, since they are persisted to localStorage. | ||
Storybook does not have any mocking or stubbing APIs, but you can use the `override*` and | ||
`setOverrides` methods on this client as a replacement. | ||
The easiest way to get set up is to use the `FeatureGatesInitializationWithDefaults` component in our React SDK (`@atlassian/feature-gates-react`) with the `overrides` prop set, since this manages the initialization and clean-up for you. Please see the [component documentation](../../docs/react-sdk/FEATURE_GATES_INITIALIZATION_WITH_DEFAULTS.md) for more information. | ||
Please note that the client must be initialized before these methods can be called, and that the | ||
overrides will need to be cleared after each storybook is unmounted, since they are persisted to | ||
localStorage. | ||
The easiest way to get set up is to use the `FeatureGatesInitializationWithDefaults` component in | ||
our React SDK (`@atlassian/feature-gates-react`) with the `overrides` prop set, since this manages | ||
the initialization and clean-up for you. Please see the | ||
[component documentation](../../docs/react-sdk/FEATURE_GATES_INITIALIZATION_WITH_DEFAULTS.md) for | ||
more information. | ||
## Development | ||
### How do I get set up? | ||
* Summary of set up | ||
* This repo package uses yarn | ||
* Run `yarn install` in the root directory to set up your git hooks. | ||
* In order to get started run `yarn` to install the dependencies | ||
* How to run tests | ||
* In order to run all tests simply run `yarn test packages/measurement/feature-gate-js-client` from the platform directory | ||
* In order to run jest tests in watch mode while doing development run `yarn test:jest --watch` | ||
* NOTE: You may need to run `yarn build @atlaskit/feature-gate-js-client` to create a version.ts file thats required for some tests | ||
- Summary of set up | ||
- This repo package uses yarn | ||
- Run `yarn install` in the root directory to set up your git hooks. | ||
- In order to get started run `yarn` to install the dependencies | ||
- How to run tests | ||
- In order to run all tests simply run `yarn test packages/measurement/feature-gate-js-client` | ||
from the platform directory | ||
- In order to run jest tests in watch mode while doing development run `yarn test:jest --watch` | ||
- NOTE: You may need to run `yarn build @atlaskit/feature-gate-js-client` to create a version.ts | ||
file thats required for some tests | ||
### Contribution guidelines | ||
* All new logic must be tested | ||
* There is no need to test direct pass through of Statsig APIs | ||
* Transformation of arguments counts as new logic | ||
* Code review | ||
* Changes must go through a pull request to be merged | ||
* Other guidelines | ||
- All new logic must be tested | ||
- There is no need to test direct pass through of Statsig APIs | ||
- Transformation of arguments counts as new logic | ||
- Code review | ||
- Changes must go through a pull request to be merged | ||
- Other guidelines | ||
### Releasing | ||
This package is part of the AFP monorepo. Create a changeset using `yarn changeset` and commit. [Documentation](https://hello.atlassian.net/wiki/spaces/AF/pages/2630205905/Releasing+Packages) | ||
This package is part of the AFP monorepo. Create a changeset using `yarn changeset` and commit. | ||
[Documentation](https://hello.atlassian.net/wiki/spaces/AF/pages/2630205905/Releasing+Packages) | ||
### Who do I talk to? | ||
This repo is owned by the experimentation platform team, | ||
reach out to !disturbed in [#help-switcheroo-statsig](https://atlassian.enterprise.slack.com/archives/C04PR2YE4UC) if you need a hand. | ||
This repo is owned by the experimentation platform team, reach out to !disturbed in | ||
[#help-switcheroo-statsig](https://atlassian.enterprise.slack.com/archives/C04PR2YE4UC) if you need | ||
a hand. |
302033
54
5541
757
3
+ Addedeventemitter2@^4.1.0
+ Addedeventemitter2@4.1.2(transitive)