All files bucket.ts

95.45% Statements 42/44
88.23% Branches 15/17
100% Functions 4/4
95.45% Lines 42/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 1402x               2x 2x   2x   2x 105x 105x   105x                                       2x 95x   95x         95x 88x 88x 7x 2x 2x 5x 5x 5x             95x   95x 102x   102x 13x     89x 81x     8x 5x         95x   95x   95x 10x     85x                                                   2x   95x 95x             95x   95x 2x   2x           93x          
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 E{
    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,
  };
}