@featurevisor/sdk
Version:
Featurevisor SDK for Node.js and the browser
140 lines (111 loc) • 3.35 kB
text/typescript
import * as murmurhash from "murmurhash";
import { Feature, BucketKey, BucketValue, Context, AttributeValue } from "@featurevisor/types";
import { Logger } from "./logger";
/**
* Generic hashing
*/
const HASH_SEED = 1;
const MAX_HASH_VALUE = Math.pow(2, 32);
export const MAX_BUCKETED_NUMBER = 100000; // 100% * 1000 to include three decimal places in the same integer value
export function getBucketedNumber(bucketKey: string): number {
const hashValue = murmurhash.v3(bucketKey, HASH_SEED);
const ratio = hashValue / MAX_HASH_VALUE;
return Math.floor(ratio * MAX_BUCKETED_NUMBER);
}
/**
* Feature specific bucketing
*/
export type ConfigureBucketKey = (
feature: Feature,
context: Context,
bucketKey: BucketKey,
) => BucketKey;
export interface BucketKeyOptions {
feature: Feature;
context: Context;
logger: Logger;
bucketKeySeparator?: string;
configureBucketKey?: ConfigureBucketKey;
}
export function getBucketKey(options: BucketKeyOptions): BucketKey {
const { feature, context, logger, bucketKeySeparator = ".", configureBucketKey } = options;
const featureKey = feature.key;
let type;
let attributeKeys;
if (typeof feature.bucketBy === "string") {
type = "plain";
attributeKeys = [feature.bucketBy];
} else if (Array.isArray(feature.bucketBy)) {
type = "and";
attributeKeys = feature.bucketBy;
} else if (typeof feature.bucketBy === "object" && Array.isArray(feature.bucketBy.or)) {
type = "or";
attributeKeys = feature.bucketBy.or;
} else {
logger.error("invalid bucketBy", { featureKey, bucketBy: feature.bucketBy });
throw new Error("invalid bucketBy");
}
const bucketKey: AttributeValue[] = [];
attributeKeys.forEach((attributeKey) => {
const attributeValue = context[attributeKey];
if (typeof attributeValue === "undefined") {
return;
}
if (type === "plain" || type === "and") {
bucketKey.push(attributeValue);
} else {
// or
if (bucketKey.length === 0) {
bucketKey.push(attributeValue);
}
}
});
bucketKey.push(featureKey);
const result = bucketKey.join(bucketKeySeparator);
if (configureBucketKey) {
return configureBucketKey(feature, context, result);
}
return result;
}
export interface Bucket {
bucketKey: BucketKey;
bucketValue: BucketValue;
}
export type ConfigureBucketValue = (
feature: Feature,
context: Context,
bucketValue: BucketValue,
) => BucketValue;
export interface BucketValueOptions {
// common with BucketKeyOptions
feature: Feature;
context: Context;
logger: Logger;
bucketKeySeparator?: string;
configureBucketKey?: ConfigureBucketKey;
// specific to BucketValueOptions
configureBucketValue?: ConfigureBucketValue;
}
export function getBucket(options: BucketValueOptions): Bucket {
const { feature, context, logger, bucketKeySeparator, configureBucketKey, configureBucketValue } =
options;
const bucketKey = getBucketKey({
feature,
context,
logger,
bucketKeySeparator,
configureBucketKey,
});
const value = getBucketedNumber(bucketKey);
if (configureBucketValue) {
const configuredValue = configureBucketValue(feature, context, value);
return {
bucketKey,
bucketValue: configuredValue,
};
}
return {
bucketKey,
bucketValue: value,
};
}