@bucketco/node-sdk
Advanced tools
Comparing version 1.0.0-alpha.1 to 1.0.0-alpha.2
{ | ||
"name": "@bucketco/node-sdk", | ||
"version": "1.0.0-alpha.1", | ||
"version": "1.0.0-alpha.2", | ||
"license": "MIT", | ||
@@ -14,3 +14,3 @@ "repository": { | ||
"test": "vitest -c vite.config.js", | ||
"test:ci": "vitest run -c vite.config.js --reporter=junit --outputFile=junit.xml", | ||
"test:ci": "vitest run -c vite.config.js --reporter=default --reporter=junit --outputFile=junit.xml", | ||
"coverage": "vitest run --coverage", | ||
@@ -29,4 +29,4 @@ "lint": "eslint .", | ||
}, | ||
"main": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"main": "./dist/src/index.js", | ||
"types": "./dist/types/src/index.d.ts", | ||
"devDependencies": { | ||
@@ -49,4 +49,4 @@ "@babel/core": "~7.24.7", | ||
"dependencies": { | ||
"@bucketco/flag-evaluation": "~0.0.4" | ||
"@bucketco/flag-evaluation": "~0.0.7" | ||
} | ||
} |
@@ -26,4 +26,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.BucketClientClass = void 0; | ||
exports.BucketClient = BucketClient; | ||
exports.BoundBucketClient = exports.BucketClient = void 0; | ||
const flag_evaluation_1 = require("@bucketco/flag-evaluation"); | ||
@@ -35,16 +34,6 @@ const cache_1 = __importDefault(require("./cache")); | ||
/** | ||
* Creates a new SDK client. | ||
* | ||
* @remarks | ||
* The client is used to interact with the Bucket API. | ||
* Use the client to track users, companies, events, and feature flag usage. | ||
*/ | ||
function BucketClient(options) { | ||
return new BucketClientClass(options); | ||
} | ||
/** | ||
* The SDK client. | ||
* | ||
**/ | ||
class BucketClientClass { | ||
class BucketClient { | ||
/** | ||
@@ -57,9 +46,2 @@ * Creates a new SDK client. | ||
constructor(options) { | ||
if (options instanceof BucketClientClass) { | ||
this._shared = options._shared; | ||
this._otherContext = options._otherContext; | ||
this._company = options._company; | ||
this._user = options._user; | ||
return; | ||
} | ||
(0, utils_1.ok)((0, utils_1.isObject)(options), "options must be an object"); | ||
@@ -71,12 +53,13 @@ (0, utils_1.ok)(typeof options.secretKey === "string" && options.secretKey.length > 22, "secretKey must be a string"); | ||
(0, utils_1.ok)(options.httpClient === undefined || (0, utils_1.isObject)(options.httpClient), "httpClient must be an object"); | ||
(0, utils_1.ok)(options.fallbackFlags === undefined || (0, utils_1.isObject)(options.fallbackFlags), "fallbackFlags must be an object"); | ||
const flags = options.fallbackFlags && | ||
Object.entries(options.fallbackFlags).reduce((acc, [key, value]) => { | ||
(0, utils_1.ok)(options.fallbackFeatures === undefined || | ||
Array.isArray(options.fallbackFeatures), "fallbackFeatures must be an object"); | ||
const features = options.fallbackFeatures && | ||
options.fallbackFeatures.reduce((acc, key) => { | ||
acc[key] = { | ||
isEnabled: true, | ||
key, | ||
value: value, | ||
}; | ||
return acc; | ||
}, {}); | ||
this._shared = { | ||
this._config = { | ||
logger: options.logger && (0, utils_1.decorateLogger)(config_1.BUCKET_LOG_PREFIX, options.logger), | ||
@@ -90,5 +73,5 @@ host: options.host || config_1.API_HOST, | ||
httpClient: options.httpClient || fetch_http_client_1.default, | ||
refetchInterval: config_1.FLAGS_REFETCH_MS, | ||
staleWarningInterval: config_1.FLAGS_REFETCH_MS * 5, | ||
fallbackFlags: flags, | ||
refetchInterval: config_1.FEATURES_REFETCH_MS, | ||
staleWarningInterval: config_1.FEATURES_REFETCH_MS * 5, | ||
fallbackFeatures: features, | ||
}; | ||
@@ -110,8 +93,8 @@ } | ||
try { | ||
const response = yield this._shared.httpClient.post(`${this._shared.host}/${path}`, this._shared.headers, body); | ||
(_a = this._shared.logger) === null || _a === void 0 ? void 0 : _a.debug(`post request to "${path}"`, response); | ||
const response = yield this._config.httpClient.post(`${this._config.host}/${path}`, this._config.headers, body); | ||
(_a = this._config.logger) === null || _a === void 0 ? void 0 : _a.debug(`post request to "${path}"`, response); | ||
return ((_b = response.body) === null || _b === void 0 ? void 0 : _b.success) === true; | ||
} | ||
catch (error) { | ||
(_c = this._shared.logger) === null || _c === void 0 ? void 0 : _c.error(`post request to "${path}" failed with error`, error); | ||
(_c = this._config.logger) === null || _c === void 0 ? void 0 : _c.error(`post request to "${path}" failed with error`, error); | ||
return false; | ||
@@ -133,4 +116,4 @@ } | ||
try { | ||
const response = yield this._shared.httpClient.get(`${this._shared.host}/${path}`, this._shared.headers); | ||
(_a = this._shared.logger) === null || _a === void 0 ? void 0 : _a.debug(`get request to "${path}"`, response); | ||
const response = yield this._config.httpClient.get(`${this._config.host}/${path}`, this._config.headers); | ||
(_a = this._config.logger) === null || _a === void 0 ? void 0 : _a.debug(`get request to "${path}"`, response); | ||
if (!(0, utils_1.isObject)(response.body) || response.body.success !== true) { | ||
@@ -143,3 +126,3 @@ return undefined; | ||
catch (error) { | ||
(_b = this._shared.logger) === null || _b === void 0 ? void 0 : _b.error(`get request to "${path}" failed with error`, error); | ||
(_b = this._config.logger) === null || _b === void 0 ? void 0 : _b.error(`get request to "${path}" failed with error`, error); | ||
return undefined; | ||
@@ -150,4 +133,9 @@ } | ||
/** | ||
* Sends a feature flag event to the Bucket API. | ||
* Sends a feature event to the Bucket API. | ||
* | ||
* Feature events are used to track the evaluation of feature targeting rules. | ||
* "check" events are sent when a feature's `isEnabled` property is checked. | ||
* "evaluate" events are sent when a feature's targeting rules are matched against | ||
* the current context. | ||
* | ||
* @param event - The event to send. | ||
@@ -161,3 +149,3 @@ * | ||
**/ | ||
sendFeatureFlagEvent(event) { | ||
sendFeatureEvent(event) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
@@ -167,4 +155,5 @@ (0, utils_1.ok)(typeof event === "object", "event must be an object"); | ||
(event.action === "evaluate" || event.action === "check"), "event must have an action"); | ||
(0, utils_1.ok)(typeof event.flagKey === "string" && event.flagKey.length > 0, "event must have a flag key"); | ||
(0, utils_1.ok)(typeof event.flagVersion === "number" || event.flagVersion === undefined, "event must have a flag version"); | ||
(0, utils_1.ok)(typeof event.key === "string" && event.key.length > 0, "event must have a feature key"); | ||
(0, utils_1.ok)(typeof event.targetingVersion === "number" || | ||
event.targetingVersion === undefined, "event must have a targeting version"); | ||
(0, utils_1.ok)(typeof event.evalResult === "boolean", "event must have an evaluation result"); | ||
@@ -177,9 +166,9 @@ (0, utils_1.ok)(event.evalContext === undefined || typeof event.evalContext === "object", "event context must be an object"); | ||
const contextKey = new URLSearchParams((0, flag_evaluation_1.flattenJSON)(event.evalContext || {})).toString(); | ||
if (!(0, utils_1.checkWithinAllottedTimeWindow)(config_1.FLAG_EVENTS_PER_MIN, `${event.action}:${contextKey}:${event.flagKey}:${event.flagVersion}:${event.evalResult}`)) { | ||
if (!(0, utils_1.checkWithinAllottedTimeWindow)(config_1.FEATURE_EVENTS_PER_MIN, `${event.action}:${contextKey}:${event.key}:${event.targetingVersion}:${event.evalResult}`)) { | ||
return false; | ||
} | ||
return yield this.post("flags/events", { | ||
return yield this.post("features/events", { | ||
action: event.action, | ||
flagKey: event.flagKey, | ||
flagVersion: event.flagVersion, | ||
key: event.key, | ||
targetingVersion: event.targetingVersion, | ||
evalContext: event.evalContext, | ||
@@ -192,7 +181,7 @@ evalResult: event.evalResult, | ||
} | ||
getFeatureFlagDefinitionCache() { | ||
if (!this._shared.featureFlagDefinitionCache) { | ||
this._shared.featureFlagDefinitionCache = (0, cache_1.default)(this._shared.refetchInterval, this._shared.staleWarningInterval, this._shared.logger, () => __awaiter(this, void 0, void 0, function* () { | ||
const res = yield this.get("flags"); | ||
if (!(0, utils_1.isObject)(res) || !Array.isArray(res === null || res === void 0 ? void 0 : res.flags)) { | ||
getFeaturesCache() { | ||
if (!this._config.featuresCache) { | ||
this._config.featuresCache = (0, cache_1.default)(this._config.refetchInterval, this._config.staleWarningInterval, this._config.logger, () => __awaiter(this, void 0, void 0, function* () { | ||
const res = yield this.get("features"); | ||
if (!(0, utils_1.isObject)(res) || !Array.isArray(res === null || res === void 0 ? void 0 : res.features)) { | ||
return undefined; | ||
@@ -203,118 +192,59 @@ } | ||
} | ||
return this._shared.featureFlagDefinitionCache; | ||
return this._config.featuresCache; | ||
} | ||
/** | ||
* Sets the user that is used for feature flag evaluation. | ||
* Gets the logger associated with the client. | ||
* | ||
* @param userId - The user ID to set. | ||
* @param attributes - The attributes of the user (optional). | ||
* | ||
* @returns A new client with the user set. | ||
* @throws An error if the user ID is not a string or the options are invalid. | ||
* @remarks | ||
* If the user ID is the same as the current company, the attributes will be merged, and | ||
* the new attributes will take precedence. | ||
* | ||
* The `updateUser` method will automatically be called after setting the user. | ||
* @returns The logger or `undefined` if it is not set. | ||
**/ | ||
withUser(userId, attributes) { | ||
var _a; | ||
(0, utils_1.ok)(typeof userId === "string" && userId.length > 0, "userId must be a string"); | ||
(0, utils_1.ok)(attributes === undefined || (0, utils_1.isObject)(attributes), "attributes must be an object"); | ||
const client = new BucketClientClass(this); | ||
if (userId !== ((_a = this._user) === null || _a === void 0 ? void 0 : _a.userId)) { | ||
client._user = { userId, attrs: attributes }; | ||
} | ||
else { | ||
client._user = { | ||
userId: this._user.userId, | ||
attrs: Object.assign(Object.assign({}, this._user.attrs), attributes), | ||
}; | ||
} | ||
void client.updateUser(); | ||
return client; | ||
get logger() { | ||
return this._config.logger; | ||
} | ||
/** | ||
* Sets the company that is used for feature flag evaluation. | ||
* Returns a new BoundBucketClient with the user/company/otherContext | ||
* set to be used in subsequent calls. | ||
* For example, for evaluating feature targeting or tracking events. | ||
* | ||
* @param companyId - The company ID to set. | ||
* @param attributes - The attributes of the user (optional). | ||
* @param context - The user/company/otherContext to bind to the client. | ||
* | ||
* @returns A new client with the company set. | ||
* @throws An error if the company ID is not a string or the options are invalid. | ||
* @returns A new client bound with the arguments given. | ||
* @throws An error if the user/company is given but their ID is not a string. | ||
* @remarks | ||
* If the company ID is the same as the current company, the attributes will be merged, and | ||
* the new attributes will take precedence. | ||
* | ||
* The `updateCompany` method will automatically be called after setting the company. | ||
* The `updateUser` / `updateCompany` methods will automatically be called when | ||
* the user/company is set respectively. | ||
**/ | ||
withCompany(companyId, attributes) { | ||
var _a; | ||
(0, utils_1.ok)(typeof companyId === "string" && companyId.length > 0, "companyId must be a string"); | ||
(0, utils_1.ok)(attributes === undefined || (0, utils_1.isObject)(attributes), "attributes must be an object"); | ||
const client = new BucketClientClass(this); | ||
if (companyId !== ((_a = this._company) === null || _a === void 0 ? void 0 : _a.companyId)) { | ||
client._company = { companyId, attrs: attributes }; | ||
bindClient(context) { | ||
const boundClient = new BoundBucketClient(this, context); | ||
// TODO: batch these updates and send to the bulk endpoint | ||
if (context.company) { | ||
const _a = context.company, { id: _ } = _a, attributes = __rest(_a, ["id"]); | ||
void this.updateCompany(context.company.id, { | ||
attributes, | ||
meta: { active: false }, | ||
}); | ||
} | ||
else { | ||
client._company = { | ||
companyId: this._company.companyId, | ||
attrs: Object.assign(Object.assign({}, this._company.attrs), attributes), | ||
}; | ||
if (context.user) { | ||
const _b = context.user, { id: _ } = _b, attributes = __rest(_b, ["id"]); | ||
void this.updateUser(context.user.id, { | ||
attributes, | ||
meta: { active: false }, | ||
}); | ||
} | ||
void client.updateCompany(); | ||
return client; | ||
return boundClient; | ||
} | ||
/** | ||
* Sets the extra, custom context for the client. | ||
* Updates the associated user in Bucket. | ||
* | ||
* @param context - The "extra" context to set. | ||
* | ||
* @returns A new client with the context set. | ||
* @throws An error if the context is not an object or the options are invalid. | ||
**/ | ||
withOtherContext(context) { | ||
(0, utils_1.ok)((0, utils_1.isObject)(context), "context must be an object"); | ||
const client = new BucketClientClass(this); | ||
client._otherContext = context; | ||
return client; | ||
} | ||
/** | ||
* Gets the extra, custom context associated with the client. | ||
* | ||
* @returns The extra, custom context or `undefined` if it is not set. | ||
**/ | ||
get otherContext() { | ||
return this._otherContext; | ||
} | ||
/** | ||
* Gets the user associated with the client. | ||
* | ||
* @returns The user or `undefined` if it is not set. | ||
**/ | ||
get user() { | ||
return this._user; | ||
} | ||
/** | ||
* Gets the company associated with the client. | ||
* | ||
* @returns The company or `undefined` if it is not set. | ||
**/ | ||
get company() { | ||
return this._company; | ||
} | ||
/** | ||
* Updates a user in Bucket. | ||
* | ||
* @param opts.attributes - The additional attributes of the user (optional). | ||
* @param opts.attributes - The additional attributes of the company (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the user is not set or the options are invalid. | ||
* @throws An error if the company is not set or the options are invalid. | ||
* @remarks | ||
* The user must be set using `withUser` before calling this method. | ||
* The company must be set using `withCompany` before calling this method. | ||
* If the user is set, the company will be associated with the user. | ||
**/ | ||
updateUser(opts) { | ||
updateUser(userId, opts) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
(0, utils_1.ok)((0, utils_1.isObject)(this._user), "user must be set"); | ||
(0, utils_1.ok)(typeof userId === "string" && userId.length > 0, "companyId must be a string"); | ||
(0, utils_1.ok)(opts === undefined || (0, utils_1.isObject)(opts), "opts must be an object"); | ||
@@ -324,4 +254,4 @@ (0, utils_1.ok)((opts === null || opts === void 0 ? void 0 : opts.attributes) === undefined || (0, utils_1.isObject)(opts.attributes), "attributes must be an object"); | ||
return yield this.post("user", { | ||
userId: this._user.userId, | ||
attributes: Object.assign(Object.assign({}, this._user.attrs), opts === null || opts === void 0 ? void 0 : opts.attributes), | ||
userId, | ||
attributes: opts === null || opts === void 0 ? void 0 : opts.attributes, | ||
context: opts === null || opts === void 0 ? void 0 : opts.meta, | ||
@@ -343,6 +273,5 @@ }); | ||
**/ | ||
updateCompany(opts) { | ||
updateCompany(companyId, opts) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
var _a; | ||
(0, utils_1.ok)((0, utils_1.isObject)(this._company), "company must be set"); | ||
(0, utils_1.ok)(typeof companyId === "string" && companyId.length > 0, "companyId must be a string"); | ||
(0, utils_1.ok)(opts === undefined || (0, utils_1.isObject)(opts), "opts must be an object"); | ||
@@ -352,5 +281,5 @@ (0, utils_1.ok)((opts === null || opts === void 0 ? void 0 : opts.attributes) === undefined || (0, utils_1.isObject)(opts.attributes), "attributes must be an object"); | ||
return yield this.post("company", { | ||
companyId: this._company.companyId, | ||
userId: (_a = this._user) === null || _a === void 0 ? void 0 : _a.userId, | ||
attributes: Object.assign(Object.assign({}, this._company.attrs), opts === null || opts === void 0 ? void 0 : opts.attributes), | ||
companyId, | ||
userId: opts === null || opts === void 0 ? void 0 : opts.userId, | ||
attributes: opts === null || opts === void 0 ? void 0 : opts.attributes, | ||
context: opts === null || opts === void 0 ? void 0 : opts.meta, | ||
@@ -362,6 +291,8 @@ }); | ||
* Tracks an event in Bucket. | ||
* | ||
* @param event - The event to track. | ||
* @param userId - The userId of the user who performed the event | ||
* @param opts.attributes - The attributes of the event (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* @param opts.companyId - Optional company ID for the event (optional). | ||
* | ||
@@ -373,6 +304,5 @@ * @returns A boolean indicating if the request was successful. | ||
**/ | ||
trackFeatureUsage(event, opts) { | ||
track(userId, event, opts) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b; | ||
(0, utils_1.ok)((0, utils_1.isObject)(this._user), "user must be set"); | ||
(0, utils_1.ok)(typeof userId === "string" && userId.length > 0, "userId must be a string"); | ||
(0, utils_1.ok)(typeof event === "string" && event.length > 0, "event must be a string"); | ||
@@ -382,6 +312,7 @@ (0, utils_1.ok)(opts === undefined || (0, utils_1.isObject)(opts), "opts must be an object"); | ||
(0, utils_1.ok)((opts === null || opts === void 0 ? void 0 : opts.meta) === undefined || (0, utils_1.isObject)(opts.meta), "meta must be an object"); | ||
(0, utils_1.ok)((opts === null || opts === void 0 ? void 0 : opts.companyId) === undefined || typeof opts.companyId === "string", "companyId must be an string"); | ||
return yield this.post("event", { | ||
event, | ||
companyId: (_a = this._company) === null || _a === void 0 ? void 0 : _a.companyId, | ||
userId: (_b = this._user) === null || _b === void 0 ? void 0 : _b.userId, | ||
companyId: opts === null || opts === void 0 ? void 0 : opts.companyId, | ||
userId, | ||
attributes: opts === null || opts === void 0 ? void 0 : opts.attributes, | ||
@@ -393,3 +324,3 @@ context: opts === null || opts === void 0 ? void 0 : opts.meta, | ||
/** | ||
* Initializes the client by caching the feature flag definitions. | ||
* Initializes the client by caching the features definitions. | ||
* | ||
@@ -399,35 +330,29 @@ * @returns void | ||
* @remarks | ||
* Call this method before calling `getFlags` to ensure the feature flag definitions are cached. | ||
* Call this method before calling `getFeatures` to ensure the feature definitions are cached. | ||
**/ | ||
initialize() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
yield this.getFeatureFlagDefinitionCache().refresh(); | ||
yield this.getFeaturesCache().refresh(); | ||
}); | ||
} | ||
/** | ||
* Gets the evaluated feature flags for the current context which includes the user, company, and custom context. | ||
* Gets the evaluated feature for the current context which includes the user, company, and custom context. | ||
* | ||
* @typeparam TFlagKey - The type of the feature flag keys, `string` by default. | ||
* | ||
* @returns The evaluated feature flags. | ||
* @returns The evaluated features. | ||
* @remarks | ||
* Call `initialize` before calling this method to ensure the feature flag definitions are cached, empty flags will be returned otherwise. | ||
* Call `initialize` before calling this method to ensure the feature definitions are cached, no features will be returned otherwise. | ||
**/ | ||
getFlags() { | ||
getFeatures(context) { | ||
var _a, _b; | ||
const mergedContext = { | ||
user: this._user && Object.assign({ id: this._user.userId }, this._user.attrs), | ||
company: this._company && Object.assign({ id: this._company.companyId }, this._company.attrs), | ||
other: this._otherContext, | ||
}; | ||
const flagDefinitions = this.getFeatureFlagDefinitionCache().get(); | ||
let evaluatedFlags = this._shared.fallbackFlags || {}; | ||
if (flagDefinitions) { | ||
const keyToVersionMap = new Map(flagDefinitions.flags.map((f) => [f.key, f.version])); | ||
const evaluated = flagDefinitions.flags.map((flag) => (0, flag_evaluation_1.evaluateFlag)({ context: mergedContext, flag })); | ||
const featureDefinitions = this.getFeaturesCache().get(); | ||
let evaluatedFeatures = this._config.fallbackFeatures || {}; | ||
if (featureDefinitions) { | ||
const keyToVersionMap = new Map(featureDefinitions.features.map((f) => [f.key, f.targeting.version])); | ||
const evaluated = featureDefinitions.features.map((feature) => (0, flag_evaluation_1.evaluateTargeting)({ context, feature })); | ||
// TODO: use the bulk endpoint | ||
evaluated.forEach((res) => __awaiter(this, void 0, void 0, function* () { | ||
yield this.sendFeatureFlagEvent({ | ||
this.sendFeatureEvent({ | ||
action: "evaluate", | ||
flagKey: res.flag.key, | ||
flagVersion: keyToVersionMap.get(res.flag.key), | ||
key: res.feature.key, | ||
targetingVersion: keyToVersionMap.get(res.feature.key), | ||
evalResult: res.value, | ||
@@ -437,31 +362,134 @@ evalContext: res.context, | ||
evalMissingFields: res.missingContextFields, | ||
}).catch((err) => { | ||
var _a; | ||
(_a = this._config.logger) === null || _a === void 0 ? void 0 : _a.error(`failed to send evaluate event for "${res.feature.key}"`, err); | ||
}); | ||
})); | ||
evaluatedFlags = evaluated | ||
evaluatedFeatures = evaluated | ||
.filter((e) => e.value) | ||
.reduce((acc, res) => { | ||
acc[res.flag.key] = { | ||
key: res.flag.key, | ||
value: res.value, | ||
version: keyToVersionMap.get(res.flag.key), | ||
acc[res.feature.key] = { | ||
key: res.feature.key, | ||
isEnabled: res.value, | ||
targetingVersion: keyToVersionMap.get(res.feature.key), | ||
}; | ||
return acc; | ||
}, {}); | ||
(_a = this._shared.logger) === null || _a === void 0 ? void 0 : _a.debug("evaluated flags", evaluatedFlags); | ||
(_a = this._config.logger) === null || _a === void 0 ? void 0 : _a.debug("evaluated features", evaluatedFeatures); | ||
} | ||
else { | ||
(_b = this._shared.logger) === null || _b === void 0 ? void 0 : _b.warn("failed to use feature flag definitions, there are none cached yet. using fallback flags."); | ||
(_b = this._config.logger) === null || _b === void 0 ? void 0 : _b.warn("failed to use feature definitions, there are none cached yet. Using fallback features."); | ||
} | ||
return (0, utils_1.maskedProxy)(evaluatedFlags, (flags, key) => { | ||
void this.sendFeatureFlagEvent({ | ||
return (0, utils_1.maskedProxy)(evaluatedFeatures, (features, key) => { | ||
var _a; | ||
void this.sendFeatureEvent({ | ||
action: "check", | ||
flagKey: key, | ||
flagVersion: flags[key].version, | ||
evalResult: flags[key].value, | ||
key: key, | ||
targetingVersion: features[key].targetingVersion, | ||
evalResult: features[key].isEnabled, | ||
}).catch((err) => { | ||
var _a; | ||
(_a = this._config.logger) === null || _a === void 0 ? void 0 : _a.error(`failed to send check event for "${key}": ${err}`, err); | ||
}); | ||
return flags[key].value; | ||
const feature = features[key]; | ||
return { | ||
key, | ||
isEnabled: (_a = feature === null || feature === void 0 ? void 0 : feature.isEnabled) !== null && _a !== void 0 ? _a : false, | ||
track: () => __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b; | ||
const userId = (_a = context.user) === null || _a === void 0 ? void 0 : _a.id; | ||
if (!userId) { | ||
(_b = this._config.logger) === null || _b === void 0 ? void 0 : _b.warn("feature.track(): no user set, cannot track event"); | ||
return; | ||
} | ||
yield this.track(userId, key); | ||
}), | ||
}; | ||
}); | ||
} | ||
} | ||
exports.BucketClientClass = BucketClientClass; | ||
exports.BucketClient = BucketClient; | ||
/** | ||
* A client bound with a specific user, company, and other context. | ||
*/ | ||
class BoundBucketClient { | ||
constructor(client, context) { | ||
this._client = client; | ||
this._context = context; | ||
this.checkContext(context); | ||
} | ||
checkContext(context) { | ||
var _a, _b; | ||
(0, utils_1.ok)((0, utils_1.isObject)(context), "context must be an object"); | ||
(0, utils_1.ok)(context.user === undefined || typeof ((_a = context.user) === null || _a === void 0 ? void 0 : _a.id) === "string", "user.id must be a string if user is given"); | ||
(0, utils_1.ok)(context.company === undefined || typeof ((_b = context.company) === null || _b === void 0 ? void 0 : _b.id) === "string", "company.id must be a string if company is given"); | ||
(0, utils_1.ok)(context.other === undefined || (0, utils_1.isObject)(context.other), "other must be an object if given"); | ||
} | ||
/** | ||
* Gets the "other" context associated with the client. | ||
* | ||
* @returns The "other" context or `undefined` if it is not set. | ||
**/ | ||
get otherContext() { | ||
return this._context.other; | ||
} | ||
/** | ||
* Gets the user associated with the client. | ||
* | ||
* @returns The user or `undefined` if it is not set. | ||
**/ | ||
get user() { | ||
return this._context.user; | ||
} | ||
/** | ||
* Gets the company associated with the client. | ||
* | ||
* @returns The company or `undefined` if it is not set. | ||
**/ | ||
get company() { | ||
return this._context.company; | ||
} | ||
/** | ||
* Get features for the user/company/other context bound to this client. | ||
* | ||
* @returns Features for the given user/company and whether each one is enabled or not | ||
*/ | ||
getFeatures() { | ||
return this._client.getFeatures(this._context); | ||
} | ||
/** | ||
* Track an event in Bucket. | ||
* | ||
* @param event - The event to track. | ||
* @param opts.attributes - The attributes of the event (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the event is invalid or the options are invalid. | ||
*/ | ||
track(event, opts) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b; | ||
const userId = (_a = this._context.user) === null || _a === void 0 ? void 0 : _a.id; | ||
if (!userId) { | ||
(_b = this._client.logger) === null || _b === void 0 ? void 0 : _b.warn("No user set, cannot track event"); | ||
return false; | ||
} | ||
return yield this._client.track(userId, event, opts); | ||
}); | ||
} | ||
/** | ||
* Create a new client bound with the additional context. | ||
* Note: This performs a shallow merge for user/company/other individually. | ||
* | ||
* @param context User/company/other context to bind to the client object | ||
* @returns new client bound with the additional context | ||
*/ | ||
bindClient({ user, company, other }) { | ||
// merge new context into existing | ||
const newContext = Object.assign(Object.assign({}, this._context), { user: user ? Object.assign(Object.assign({}, this._context.user), user) : undefined, company: company ? Object.assign(Object.assign({}, this._context.company), company) : undefined, other: Object.assign(Object.assign({}, this._context.other), other) }); | ||
return new BoundBucketClient(this._client, newContext); | ||
} | ||
} | ||
exports.BoundBucketClient = BoundBucketClient; | ||
//# sourceMappingURL=client.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.FLAGS_REFETCH_MS = exports.FLAG_EVENTS_PER_MIN = exports.BUCKET_LOG_PREFIX = exports.API_TIMEOUT_MS = exports.SDK_VERSION = exports.SDK_VERSION_HEADER_NAME = exports.API_HOST = void 0; | ||
exports.FEATURES_REFETCH_MS = exports.FEATURE_EVENTS_PER_MIN = exports.BUCKET_LOG_PREFIX = exports.API_TIMEOUT_MS = exports.SDK_VERSION = exports.SDK_VERSION_HEADER_NAME = exports.API_HOST = void 0; | ||
const package_json_1 = require("../package.json"); | ||
@@ -10,4 +10,4 @@ exports.API_HOST = "https://front.bucket.co"; | ||
exports.BUCKET_LOG_PREFIX = "[Bucket]"; | ||
exports.FLAG_EVENTS_PER_MIN = 1; | ||
exports.FLAGS_REFETCH_MS = 60 * 1000; // re-fetch every 60 seconds | ||
exports.FEATURE_EVENTS_PER_MIN = 1; | ||
exports.FEATURES_REFETCH_MS = 60 * 1000; // re-fetch every 60 seconds | ||
//# sourceMappingURL=config.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.BucketClient = void 0; | ||
exports.BucketClient = exports.BoundBucketClient = void 0; | ||
var client_1 = require("./client"); | ||
Object.defineProperty(exports, "BoundBucketClient", { enumerable: true, get: function () { return client_1.BoundBucketClient; } }); | ||
Object.defineProperty(exports, "BucketClient", { enumerable: true, get: function () { return client_1.BucketClient; } }); | ||
//# sourceMappingURL=index.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.clearRateLimiter = clearRateLimiter; | ||
exports.checkWithinAllottedTimeWindow = checkWithinAllottedTimeWindow; | ||
@@ -9,3 +10,6 @@ exports.maskedProxy = maskedProxy; | ||
const oneMinute = 60 * 1000; | ||
const eventsByKey = {}; | ||
let eventsByKey = {}; | ||
function clearRateLimiter() { | ||
eventsByKey = {}; | ||
} | ||
function checkWithinAllottedTimeWindow(eventsPerMinute, key) { | ||
@@ -12,0 +16,0 @@ const now = Date.now(); |
@@ -1,23 +0,12 @@ | ||
import { Attributes, BucketClient as BucketClientType, ClientOptions, TrackOptions, TypedFlags } from "./types"; | ||
import { ClientOptions, Context, Logger, TrackOptions, TypedFeatures } from "./types"; | ||
/** | ||
* Creates a new SDK client. | ||
* | ||
* @remarks | ||
* The client is used to interact with the Bucket API. | ||
* Use the client to track users, companies, events, and feature flag usage. | ||
*/ | ||
export declare function BucketClient(options: ClientOptions): BucketClientType; | ||
/** | ||
* The SDK client. | ||
* | ||
**/ | ||
export declare class BucketClientClass { | ||
private _shared; | ||
private _otherContext; | ||
private _company; | ||
private _user; | ||
export declare class BucketClient { | ||
private _config; | ||
/** | ||
* Creates a new SDK client. | ||
* | ||
* @param options - The options for the client. | ||
* @param options - The options for the client or an existing client to clone. | ||
* @throws An error if the options are invalid. | ||
@@ -27,9 +16,2 @@ **/ | ||
/** | ||
* Creates a new SDK client. | ||
* | ||
* @param client - An existing client to clone. | ||
* @throws An error if the client is invalid. | ||
**/ | ||
constructor(client: BucketClientClass); | ||
/** | ||
* Sends a POST request to the specified path. | ||
@@ -52,4 +34,9 @@ * | ||
/** | ||
* Sends a feature flag event to the Bucket API. | ||
* Sends a feature event to the Bucket API. | ||
* | ||
* Feature events are used to track the evaluation of feature targeting rules. | ||
* "check" events are sent when a feature's `isEnabled` property is checked. | ||
* "evaluate" events are sent when a feature's targeting rules are matched against | ||
* the current context. | ||
* | ||
* @param event - The event to send. | ||
@@ -63,79 +50,37 @@ * | ||
**/ | ||
private sendFeatureFlagEvent; | ||
private getFeatureFlagDefinitionCache; | ||
private sendFeatureEvent; | ||
private getFeaturesCache; | ||
/** | ||
* Sets the user that is used for feature flag evaluation. | ||
* Gets the logger associated with the client. | ||
* | ||
* @param userId - The user ID to set. | ||
* @param attributes - The attributes of the user (optional). | ||
* | ||
* @returns A new client with the user set. | ||
* @throws An error if the user ID is not a string or the options are invalid. | ||
* @remarks | ||
* If the user ID is the same as the current company, the attributes will be merged, and | ||
* the new attributes will take precedence. | ||
* | ||
* The `updateUser` method will automatically be called after setting the user. | ||
* @returns The logger or `undefined` if it is not set. | ||
**/ | ||
withUser(userId: string, attributes?: Attributes): BucketClientClass; | ||
get logger(): Logger | undefined; | ||
/** | ||
* Sets the company that is used for feature flag evaluation. | ||
* Returns a new BoundBucketClient with the user/company/otherContext | ||
* set to be used in subsequent calls. | ||
* For example, for evaluating feature targeting or tracking events. | ||
* | ||
* @param companyId - The company ID to set. | ||
* @param attributes - The attributes of the user (optional). | ||
* @param context - The user/company/otherContext to bind to the client. | ||
* | ||
* @returns A new client with the company set. | ||
* @throws An error if the company ID is not a string or the options are invalid. | ||
* @returns A new client bound with the arguments given. | ||
* @throws An error if the user/company is given but their ID is not a string. | ||
* @remarks | ||
* If the company ID is the same as the current company, the attributes will be merged, and | ||
* the new attributes will take precedence. | ||
* | ||
* The `updateCompany` method will automatically be called after setting the company. | ||
* The `updateUser` / `updateCompany` methods will automatically be called when | ||
* the user/company is set respectively. | ||
**/ | ||
withCompany(companyId: string, attributes?: Attributes): BucketClientClass; | ||
bindClient(context: Context): BoundBucketClient; | ||
/** | ||
* Sets the extra, custom context for the client. | ||
* Updates the associated user in Bucket. | ||
* | ||
* @param context - The "extra" context to set. | ||
* | ||
* @returns A new client with the context set. | ||
* @throws An error if the context is not an object or the options are invalid. | ||
**/ | ||
withOtherContext(context: Record<string, any>): BucketClientClass; | ||
/** | ||
* Gets the extra, custom context associated with the client. | ||
* | ||
* @returns The extra, custom context or `undefined` if it is not set. | ||
**/ | ||
get otherContext(): Record<string, any> | undefined; | ||
/** | ||
* Gets the user associated with the client. | ||
* | ||
* @returns The user or `undefined` if it is not set. | ||
**/ | ||
get user(): { | ||
userId: string; | ||
attrs?: Attributes; | ||
} | undefined; | ||
/** | ||
* Gets the company associated with the client. | ||
* | ||
* @returns The company or `undefined` if it is not set. | ||
**/ | ||
get company(): { | ||
companyId: string; | ||
attrs?: Attributes; | ||
} | undefined; | ||
/** | ||
* Updates a user in Bucket. | ||
* | ||
* @param opts.attributes - The additional attributes of the user (optional). | ||
* @param opts.attributes - The additional attributes of the company (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the user is not set or the options are invalid. | ||
* @throws An error if the company is not set or the options are invalid. | ||
* @remarks | ||
* The user must be set using `withUser` before calling this method. | ||
* The company must be set using `withCompany` before calling this method. | ||
* If the user is set, the company will be associated with the user. | ||
**/ | ||
updateUser(opts?: TrackOptions): Promise<boolean>; | ||
updateUser(userId: string, opts?: TrackOptions): Promise<boolean>; | ||
/** | ||
@@ -153,9 +98,13 @@ * Updates the associated company in Bucket. | ||
**/ | ||
updateCompany(opts?: TrackOptions): Promise<boolean>; | ||
updateCompany(companyId: string, opts: TrackOptions & { | ||
userId?: string; | ||
}): Promise<boolean>; | ||
/** | ||
* Tracks an event in Bucket. | ||
* | ||
* @param event - The event to track. | ||
* @param userId - The userId of the user who performed the event | ||
* @param opts.attributes - The attributes of the event (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* @param opts.companyId - Optional company ID for the event (optional). | ||
* | ||
@@ -167,5 +116,7 @@ * @returns A boolean indicating if the request was successful. | ||
**/ | ||
trackFeatureUsage(event: string, opts?: TrackOptions): Promise<boolean>; | ||
track(userId: string, event: string, opts?: TrackOptions & { | ||
companyId?: string; | ||
}): Promise<boolean>; | ||
/** | ||
* Initializes the client by caching the feature flag definitions. | ||
* Initializes the client by caching the features definitions. | ||
* | ||
@@ -175,15 +126,73 @@ * @returns void | ||
* @remarks | ||
* Call this method before calling `getFlags` to ensure the feature flag definitions are cached. | ||
* Call this method before calling `getFeatures` to ensure the feature definitions are cached. | ||
**/ | ||
initialize(): Promise<void>; | ||
/** | ||
* Gets the evaluated feature flags for the current context which includes the user, company, and custom context. | ||
* Gets the evaluated feature for the current context which includes the user, company, and custom context. | ||
* | ||
* @typeparam TFlagKey - The type of the feature flag keys, `string` by default. | ||
* | ||
* @returns The evaluated feature flags. | ||
* @returns The evaluated features. | ||
* @remarks | ||
* Call `initialize` before calling this method to ensure the feature flag definitions are cached, empty flags will be returned otherwise. | ||
* Call `initialize` before calling this method to ensure the feature definitions are cached, no features will be returned otherwise. | ||
**/ | ||
getFlags(): Readonly<TypedFlags>; | ||
getFeatures(context: Context): TypedFeatures; | ||
} | ||
/** | ||
* A client bound with a specific user, company, and other context. | ||
*/ | ||
export declare class BoundBucketClient { | ||
private _client; | ||
private _context; | ||
constructor(client: BucketClient, context: Context); | ||
private checkContext; | ||
/** | ||
* Gets the "other" context associated with the client. | ||
* | ||
* @returns The "other" context or `undefined` if it is not set. | ||
**/ | ||
get otherContext(): Record<string, any> | undefined; | ||
/** | ||
* Gets the user associated with the client. | ||
* | ||
* @returns The user or `undefined` if it is not set. | ||
**/ | ||
get user(): { | ||
[k: string]: any; | ||
id: string; | ||
} | undefined; | ||
/** | ||
* Gets the company associated with the client. | ||
* | ||
* @returns The company or `undefined` if it is not set. | ||
**/ | ||
get company(): { | ||
[k: string]: any; | ||
id: string; | ||
} | undefined; | ||
/** | ||
* Get features for the user/company/other context bound to this client. | ||
* | ||
* @returns Features for the given user/company and whether each one is enabled or not | ||
*/ | ||
getFeatures(): Record<string, import("./types").Feature>; | ||
/** | ||
* Track an event in Bucket. | ||
* | ||
* @param event - The event to track. | ||
* @param opts.attributes - The attributes of the event (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the event is invalid or the options are invalid. | ||
*/ | ||
track(event: string, opts?: TrackOptions & { | ||
companyId?: string; | ||
}): Promise<boolean>; | ||
/** | ||
* Create a new client bound with the additional context. | ||
* Note: This performs a shallow merge for user/company/other individually. | ||
* | ||
* @param context User/company/other context to bind to the client object | ||
* @returns new client bound with the additional context | ||
*/ | ||
bindClient({ user, company, other }: Context): BoundBucketClient; | ||
} |
@@ -6,3 +6,3 @@ export declare const API_HOST = "https://front.bucket.co"; | ||
export declare const BUCKET_LOG_PREFIX = "[Bucket]"; | ||
export declare const FLAG_EVENTS_PER_MIN = 1; | ||
export declare const FLAGS_REFETCH_MS: number; | ||
export declare const FEATURE_EVENTS_PER_MIN = 1; | ||
export declare const FEATURES_REFETCH_MS: number; |
@@ -1,2 +0,2 @@ | ||
export { BucketClient } from "./client"; | ||
export type { Attributes, ClientOptions, Flags, HttpClient, Logger, TrackingMeta, } from "./types"; | ||
export { BoundBucketClient, BucketClient } from "./client"; | ||
export type { Attributes, ClientOptions, Features, HttpClient, Logger, TrackingMeta, } from "./types"; |
@@ -1,2 +0,2 @@ | ||
import { FlagData } from "@bucketco/flag-evaluation"; | ||
import { FeatureData } from "@bucketco/flag-evaluation"; | ||
/** | ||
@@ -16,5 +16,5 @@ * Describes the meta context associated with tracking. | ||
/** | ||
* Describes a feature flag evaluation event. | ||
* Describes a feature event. Can be "check" or "evaluate". | ||
**/ | ||
export type FeatureFlagEvent = { | ||
export type FeatureEvent = { | ||
/** | ||
@@ -25,11 +25,11 @@ * The action that was performed. | ||
/** | ||
* The flag key. | ||
* The feature key. | ||
**/ | ||
flagKey: string; | ||
key: string; | ||
/** | ||
* The flag version (optional). | ||
* The feature targeting version (optional). | ||
**/ | ||
flagVersion: number | undefined; | ||
targetingVersion: number | undefined; | ||
/** | ||
* The result of flag evaluation. | ||
* The result of targeting evaluation. | ||
**/ | ||
@@ -51,39 +51,61 @@ evalResult: boolean; | ||
/** | ||
* Describes an evaluated feature flag. | ||
* Describes a feature | ||
*/ | ||
export interface Flag { | ||
export interface InternalFeature { | ||
/** | ||
* The value of the flag. | ||
* The key of the feature. | ||
*/ | ||
value: boolean; | ||
key: string; | ||
/** | ||
* The key of the flag. | ||
* If the feature is enabled. | ||
*/ | ||
isEnabled: boolean; | ||
/** | ||
* The version of the targeting used to evaluate if the feature is enabled (optional). | ||
*/ | ||
targetingVersion?: number; | ||
} | ||
/** | ||
* Describes a feature | ||
*/ | ||
export interface Feature { | ||
/** | ||
* The key of the feature. | ||
*/ | ||
key: string; | ||
/** | ||
* The version of the flag (optional). | ||
* If the feature is enabled. | ||
*/ | ||
version?: number; | ||
isEnabled: boolean; | ||
/** | ||
* Track feature usage in Bucket. | ||
*/ | ||
track(): Promise<void>; | ||
} | ||
/** | ||
* Describes a collection of evaluated feature flags. | ||
* Describes a collection of evaluated features. | ||
* | ||
* @remarks | ||
* Extends the Flags interface to define the available flags. | ||
* You should extend the Features interface to define the available features. | ||
*/ | ||
export interface Flags { | ||
export interface Features { | ||
} | ||
/** | ||
* Describes a collection of (strong-typed) evaluated feature flags. | ||
* Describes a collection of evaluated feature. | ||
* | ||
* @typeParam Flags - The type of the flags that is declared by the developer. | ||
* @remarks | ||
* This types falls back to a generic Record<string, Feature> if the Features interface | ||
* has not been extended. | ||
* | ||
*/ | ||
export type TypedFlags = keyof Flags extends never ? Record<string, boolean> : Record<keyof Flags, boolean>; | ||
export type TypedFeatures = keyof Features extends never ? Record<string, Feature> : Record<keyof Features, Feature>; | ||
/** | ||
* Describes the response of the feature flags endpoint | ||
* Describes the response of the features endpoint | ||
*/ | ||
export type FlagDefinitions = { | ||
/** The flag definitions */ | ||
flags: (FlagData & { | ||
version: number; | ||
export type FeaturesAPIResponse = { | ||
/** The feature definitions */ | ||
features: (FeatureData & { | ||
targeting: { | ||
version: number; | ||
}; | ||
})[]; | ||
@@ -201,5 +223,5 @@ }; | ||
/** | ||
* The flags to use as fallbacks when the API is unavailable (optional). | ||
* The features to "enable" as fallbacks when the API is unavailable (optional). | ||
**/ | ||
fallbackFlags?: TypedFlags; | ||
fallbackFeatures?: (keyof TypedFeatures)[]; | ||
/** | ||
@@ -225,164 +247,24 @@ * The HTTP client to use for sending requests (optional). Default is the built-in fetch client. | ||
/** | ||
* The base, unbound Bucket client. | ||
*/ | ||
export interface BucketClient extends BucketClientBase, BucketClientWithUserMethod<BucketClientUserMethods & BucketClientWithCompanyMethod<BucketClientCompanyMethods & BucketClientUserMethods>>, BucketClientWithCompanyMethod<BucketClientCompanyMethods & BucketClientWithUserMethod<BucketClientUserMethods & BucketClientCompanyMethods>> { | ||
} | ||
/** | ||
* Useful interface to use as a type for the fully initialized Bucket client. | ||
* Describes the current user context, company context, and other context. | ||
* This is used to determine if feature targeting matches and to track events. | ||
**/ | ||
export interface BoundBucketClient extends BucketClientBase, BucketClientUserMethods, BucketClientCompanyMethods { | ||
} | ||
/** | ||
* The base interface used for composition | ||
* | ||
* @remarks | ||
* Internal interface for composition. | ||
*/ | ||
interface BucketClientBase { | ||
export type Context = { | ||
/** | ||
* Sets the extra, custom context for the client. | ||
* | ||
* @param context - The "extra" context to set. | ||
* | ||
* @returns A new client with the context set. | ||
* @throws An error if the context is not an object or the options are invalid. | ||
**/ | ||
withOtherContext(context: Record<string, any>): this; | ||
/** | ||
* Gets the extra, custom context associated with the client. | ||
* | ||
* @returns The extra, custom context or `undefined` if it is not set. | ||
**/ | ||
get otherContext(): Record<string, any> | undefined; | ||
/** | ||
* Initializes the client by caching the feature flag definitions. | ||
* | ||
* @returns void | ||
* | ||
* @remarks | ||
* Call this method before calling `getFlags` to ensure the feature flag definitions are cached. | ||
**/ | ||
initialize(): Promise<void>; | ||
/** | ||
* Gets the evaluated feature flags for the current context which includes the user, company, and custom context. | ||
* | ||
* @typeparam TFlagKey - The type of the feature flag keys, `string` by default. | ||
* | ||
* @returns The evaluated feature flags. | ||
* @remarks | ||
* Call `initialize` before calling this method to ensure the feature flag definitions are cached, empty flags will be returned otherwise. | ||
**/ | ||
getFlags(): Readonly<TypedFlags>; | ||
} | ||
/** | ||
* Contains the `withUser` method. | ||
* | ||
* @remarks | ||
* Internal interface for composition. | ||
*/ | ||
interface BucketClientWithUserMethod<TReturn> { | ||
/** | ||
* Sets the user that is used for feature flag evaluation. | ||
* | ||
* @param userId - The user ID to set. | ||
* @param attributes - The attributes of the user (optional). | ||
* | ||
* @returns A new client with the user set. | ||
* @throws An error if the user ID is not a string or the options are invalid. | ||
* @remarks | ||
* If the user ID is the same as the current company, the attributes will be merged, and | ||
* the new attributes will take precedence. | ||
**/ | ||
withUser(userId: string, attributes?: Attributes): BucketClientBase & TReturn; | ||
} | ||
/** | ||
* Contains the `withCompany` method. | ||
* | ||
* @remarks | ||
* Internal interface for composition. | ||
*/ | ||
interface BucketClientWithCompanyMethod<TReturn> { | ||
/** | ||
* Sets the company that is used for feature flag evaluation. | ||
* | ||
* @param companyId - The company ID to set. | ||
* @param attributes - The attributes of the user (optional). | ||
* | ||
* @returns A new client with the company set. | ||
* @throws An error if the company ID is not a string or the options are invalid. | ||
* @remarks | ||
* If the company ID is the same as the current company, the attributes will be merged, and | ||
* the new attributes will take precedence. | ||
**/ | ||
withCompany(companyId: string, ttributes?: Attributes): BucketClientBase & TReturn; | ||
} | ||
/** | ||
* Contains all user related methods. | ||
* | ||
* @remarks | ||
* Internal interface for composition. | ||
*/ | ||
interface BucketClientUserMethods { | ||
/** | ||
* Gets the user associated with the client. | ||
* | ||
* @returns The user associated with the client. | ||
**/ | ||
get user(): { | ||
userId: string; | ||
attrs?: Attributes; | ||
* The user context. If the user is set, the user ID is required. | ||
*/ | ||
user?: { | ||
id: string; | ||
[k: string]: any; | ||
}; | ||
/** | ||
* Updates a user in Bucket. | ||
* | ||
* @param opts.attributes - The additional attributes of the user (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the options are invalid. | ||
**/ | ||
updateUser(opts?: TrackOptions): Promise<boolean>; | ||
/** | ||
* Tracks an event in Bucket. | ||
* | ||
* @param event - The event to track. | ||
* @param opts.attributes - The attributes of the event (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the event is invalid or the options are invalid. | ||
* @remarks | ||
* If the company is set, the event will be associated with the company. | ||
**/ | ||
trackFeatureUsage(event: string, opts?: TrackOptions): Promise<boolean>; | ||
} | ||
/** | ||
* Contains all company related methods. | ||
* | ||
* @remarks | ||
* Internal interface for composition. | ||
*/ | ||
interface BucketClientCompanyMethods { | ||
/** | ||
* Gets the company associated with the client. | ||
* | ||
* @returns The company associated with the client. | ||
**/ | ||
get company(): { | ||
companyId: string; | ||
attrs?: Attributes; | ||
* The company context. If the company is set, the company ID is required. | ||
*/ | ||
company?: { | ||
id: string; | ||
[k: string]: any; | ||
}; | ||
/** | ||
* Updates the associated company in Bucket. | ||
* | ||
* @param opts.attributes - The additional attributes of the company (optional). | ||
* @param opts.meta - The meta context associated with tracking (optional). | ||
* | ||
* @returns A boolean indicating if the request was successful. | ||
* @throws An error if the company is not set or the options are invalid. | ||
* @remarks | ||
* If the user is set, the company will be associated with the user. | ||
**/ | ||
updateCompany(opts?: TrackOptions): Promise<boolean>; | ||
} | ||
export {}; | ||
* The other context. This is used for any additional context that is not related to user or company. | ||
*/ | ||
other?: Record<string, any>; | ||
}; |
import { Logger } from "./types"; | ||
export declare function clearRateLimiter(): void; | ||
export declare function checkWithinAllottedTimeWindow(eventsPerMinute: number, key: string): boolean; | ||
@@ -3,0 +4,0 @@ /** |
{ | ||
"name": "@bucketco/node-sdk", | ||
"version": "1.0.0-alpha.1", | ||
"version": "1.0.0-alpha.2", | ||
"license": "MIT", | ||
@@ -14,3 +14,3 @@ "repository": { | ||
"test": "vitest -c vite.config.js", | ||
"test:ci": "vitest run -c vite.config.js --reporter=junit --outputFile=junit.xml", | ||
"test:ci": "vitest run -c vite.config.js --reporter=default --reporter=junit --outputFile=junit.xml", | ||
"coverage": "vitest run --coverage", | ||
@@ -29,4 +29,4 @@ "lint": "eslint .", | ||
}, | ||
"main": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"main": "./dist/src/index.js", | ||
"types": "./dist/types/src/index.d.ts", | ||
"devDependencies": { | ||
@@ -49,5 +49,5 @@ "@babel/core": "~7.24.7", | ||
"dependencies": { | ||
"@bucketco/flag-evaluation": "~0.0.4" | ||
"@bucketco/flag-evaluation": "~0.0.7" | ||
}, | ||
"gitHead": "425a9f3d7124f6d87d0de52ae21cbaeabb11664c" | ||
"gitHead": "ac0c84c4b4e0f241b463b48195500f29a7cf35e7" | ||
} |
203
README.md
@@ -33,5 +33,9 @@ # Bucket Node.js SDK | ||
// Create a new instance of the client with the secret key. Additional options | ||
// are available, such as supplying a logging sink, a custom HTTP client and | ||
// are available, such as supplying a logger, fallback features and | ||
// other custom properties. | ||
// | ||
// Fallback features are used in the situation when the server-side | ||
// features are not yet loaded or there are issues retrieving them. | ||
// See "Initialization Options" below for more information. | ||
// | ||
// We recommend that only one global instance of `client` should be created | ||
@@ -41,112 +45,116 @@ // to avoid multiple round-trips to our servers. | ||
secretKey: "sec_prod_xxxxxxxxxxxxxxxxxxxxx", | ||
fallbackFlags: { can_see_new_reports: true }, | ||
fallbackFeatures: ["huddle", "voice-huddle"], | ||
}); | ||
// Initialize the client and begin fetching flag definitions. | ||
// You must call this method prior to any calls to `getFlags()`, | ||
// Initialize the client and begin fetching feature targeting definitions. | ||
// You must call this method prior to any calls to `getFeatures()`, | ||
// otherwise an empty object will be returned. | ||
// | ||
// You can also supply your `fallbackFlags` to the `initialize()` method. These | ||
// fallback flags are used in the situation when the server-side flags are not | ||
// yet loaded or there are issues retrieving them. | ||
// See "Initialization Options" below for more information. | ||
await client.initialize(); | ||
``` | ||
Once the client is initialized, one can obtain the evaluated flags. | ||
Once the client is initialized, you can obtain features along with the isEnabled status to indicate whether the feature is targeted for this user/company: | ||
At any point where the client needs to be used, we can configure it through | ||
`withUser()`, `withCompany()` and `withOtherContext()` methods. Each of | ||
these three methods returns a new instance of `Client` class which includes | ||
the applied user, company or context: | ||
```ts | ||
// configure the client | ||
const boundClient = client | ||
.withUser("john_doe", { attributes: { name: "John Doe" } }) | ||
.withCompany("acme_inc", { attributes: { name "Acme Inc."} }) | ||
.withOtherContext({ additional: "context", number: [1,2,3] }) | ||
const boundClient = client.bindClient({ | ||
user: { id: "john_doe", name: "John Doe" }, | ||
company: { id: "acme_inc", name: "Acme, Inc." }, | ||
}); | ||
``` | ||
// get the current features (uses company, user and custom context to evaluate the features). | ||
const { huddle } = boundClient.getFeatures(); | ||
```ts | ||
// get the current flags (uses company, user and custom context to evaluate the flags). | ||
const flags = boundClient.getFlags(); | ||
if (flags.can_see_new_reports) { | ||
// this is your flag-protected code ... | ||
if (huddle.isEnabled) { | ||
// this is your feature gated code ... | ||
// send an event when the feature is used: | ||
boundClient.trackFeatureUsage("new_reports_used", { | ||
attributes: { | ||
some: "attribute", | ||
}, | ||
}); | ||
huddle.track(); | ||
} | ||
``` | ||
## Tracking | ||
When you bind a client to a user/company, this data is matched against the targeting rules. | ||
To get accurate targeting, you must ensure that the user/company information provided is sufficient to match against the targeting rules you've created. | ||
The user/company data is automatically transferred to Bucket. | ||
This ensures that you'll have up-to-date information about companies and users and accurate targeting information available in Bucket at all time. | ||
Tracking allows sending `user`, `company` and `event` messages to Bucket. | ||
`user` events can be used to register or update a user's attributes | ||
with Bucket. `company` allows the same, and additionally, allows | ||
associating an user with a company on the Bucket side. Finally, `event` | ||
is used to track feature usage across your application. | ||
## High performance feature targeting | ||
The following example shows how to register a new user, and associate it with a company: | ||
The Bucket Node SDK contacts the Bucket servers when you call `initialize` | ||
and downloads the features with their targeting rules. | ||
These rules are then matched against the user/company information you provide | ||
to `getFeatures` (or through `bindClient(..).getFeatures()`). That means the | ||
`getFeatures` call does not need to contact the Bucket servers once initialize | ||
has completed. `BucketClient` will continue to periodically download the | ||
targeting rules from the Bucket servers in the background. | ||
## Tracking custom events and setting custom attributes | ||
Tracking allows events and updating user/company attributes in Bucket. For example, if a | ||
customer changes their plan, you'll want Bucket to know about it in order to continue to | ||
provide up-do-date targeting information in the Bucket interface. | ||
The following example shows how to register a new user, associate it with a company and | ||
finally update the plan they are on. | ||
```ts | ||
// registers the user with Bucket using the provided unique ID, and | ||
// providing a set of custom attributes (can be anything) | ||
const boundClient = client | ||
.withUser("your_user_id", { | ||
attributes: { longTimeUser: true, payingCustomer: false }, | ||
}) | ||
.withCompany("company_id"); | ||
client.updateUser("user_id", { | ||
attributes: { longTimeUser: true, payingCustomer: false }, | ||
}); | ||
client.updateCompany("company_id", { userId: "user_id" }); | ||
// track the user (send a `user` event to Bucket). | ||
await boundClient.updateUser(); | ||
// register the user as being part of a given company | ||
boundClient.updateCompany(); | ||
// the user started a voice huddle | ||
client.track("user_id", "huddle", { attributes: { voice: true } }); | ||
``` | ||
If one needs to simply update a company's attributes on Bucket side, | ||
one calls `updateCompany` without supplying a user ID: | ||
It's also possible to achieve the same through a bound client in the following manner: | ||
```ts | ||
// either creates a new company on Bucket or updates an existing | ||
// one by supplying custom attributes | ||
client.withCompany("company_id").updateCompany({ | ||
attributes: { | ||
status: "active", | ||
plan: "trial", | ||
}, | ||
const boundClient = client.bindClient({ | ||
user: { id: "user_id", longTimeUser: true, payingCustomer: false }, | ||
company: { id: "company_id" }, | ||
}); | ||
// if a company is not active, and one needs to make sure its | ||
// "Last Seen" status does not get updated, one can supply | ||
// an additional meta argument at the end: | ||
client | ||
.withCompany("updated_company_id", { | ||
attributes: { status: "active", plan: "trial" }, | ||
}) | ||
.updateCompany({ meta: { active: false } }); | ||
boundClient.track("huddle", { attributes: { voice: true } }); | ||
``` | ||
To generate feature tracking `event`s: | ||
Some attributes are used by Bucket to improve the UI, and are recommended | ||
to provide for easier navigation: | ||
- `name` -- display name for `user`/`company`, | ||
- `email` -- the email of the user. | ||
Attributes cannot be nested (multiple levels) and must be either strings, | ||
integers or booleans. | ||
## Managing `Last seen` | ||
By default `updateUser`/`updateCompany` calls automatically update the given | ||
user/company `Last seen` property on Bucket servers. | ||
You can control if `Last seen` should be updated when the events are sent by setting | ||
`meta.active = false`. This is often useful if you | ||
have a background job that goes through a set of companies just to update their | ||
attributes but not their activity. | ||
Example: | ||
```ts | ||
// this simply sends an event to Bucket, not associated with any company or user. | ||
client.trackFeatureUsage("some_feature_name"); | ||
client.updateUser("john_doe", { | ||
attributes: { name: "John O." }, | ||
meta: { active: true }, | ||
}); | ||
// to specify to which user/company this event belongs one can do | ||
client | ||
.withUser("user_id") | ||
.withCompany("company_id") | ||
.trackFeatureUsage("some_feature_name"); | ||
client.updateCompany("acme_inc", { | ||
attributes: { name: "Acme, Inc" }, | ||
meta: { active: false }, | ||
}); | ||
``` | ||
`bindClient()` updates attributes on the Bucket servers but does not automatically | ||
update `Last seen`. | ||
### Initialization Options | ||
Supply these to the `constructor` of the `Client` class: | ||
Supply these to the `constructor` of the `BucketClient` class: | ||
@@ -162,6 +170,6 @@ ```ts | ||
// The custom http client. By default the internal `fetchClient` is used. | ||
httpClient?: HttpClient = fetchCient, | ||
// A map of fallback flags that will be used when no actual flags | ||
// are available yet. | ||
fallbackFlags?: Record<string, boolean> | ||
httpClient?: HttpClient = fetchClient, | ||
// A list of fallback features that will be enabled the Bucket servers | ||
// have not been contacted yet. | ||
fallbackFeatures?: string[] | ||
} | ||
@@ -182,42 +190,5 @@ ``` | ||
client.withUser({ userId: await sha256("john_doe"), ... }); | ||
client.updateUser({ userId: await sha256("john_doe"), ... }); | ||
``` | ||
### Custom Attributes & Context | ||
You can pass attributes as part of the object literal passed to the `withUser()`, | ||
`withCompany()`, `updateUser`, `updateCompany` and `trackFeatureUsage()`, methods. | ||
Attributes cannot be nested (multiple levels) and must be either strings, | ||
integers or booleans. | ||
Some attributes are used by Bucket.co to improve the UI, and are recommended | ||
to provide for easier navigation: | ||
- `name` or `$name` -- display name for `user`/`company`, | ||
- `email` or `$email` -- the email of the user. | ||
You can supply an additional `context` to these functions as well. The `context` | ||
object contains additional data that Bucket uses to make some behavioural choices. | ||
By default, `updateUser`, `updateCompany` and `trackFeatureUsage` calls | ||
automatically update the given user/company `Last seen` property on Bucket side. | ||
You can control if `Last seen` should be updated when the events are sent by setting | ||
`meta.active = false`. This is often useful if you | ||
have a background job that goes through a set of companies just to update their | ||
attributes but not their activity. | ||
Example: | ||
```ts | ||
client.updateUser({ | ||
attributes: { name: "John O." }, | ||
meta: { active: true }, | ||
}); | ||
client.updateCompany({ | ||
attributes: { name: "My SaaS Inc." }, | ||
meta: { active: false }, | ||
}); | ||
``` | ||
### Typescript | ||
@@ -224,0 +195,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
26
79761
1234
199