Source

src/bucketing-manager.ts

/*!
 * Convert JS SDK
 * Version 1.0.0
 * Copyright(c) 2020 Convert Insights, Inc
 * License Apache-2.0
 */

import Murmurhash from 'murmurhash';

import {BucketingManagerInterface} from './interfaces/bucketing-manager';

import {Config} from './types/Config';
import {Id} from './types/Id';
import {objectDeepValue} from './utils/object-utils';
import {LogManagerInterface} from './interfaces/log-manager';
import {MESSAGES} from './enums/dictionary';

//default hash seed
const DEFAULT_HASH_SEED = 9999;
const DEFAULT_MAX_TRAFFIC = 10000;
const DEFAULT_MAX_HASH = 4294967296;

/**
 * Provides logic for bucketing for specific visitor (by visitorId) or randomly
 * @category Modules
 * @constructor
 * @implements {BucketingManagerInterface}
 */
export class BucketingManager implements BucketingManagerInterface {
  private readonly _max_traffic: number = DEFAULT_MAX_TRAFFIC;
  private readonly _hash_seed: number = DEFAULT_HASH_SEED;
  private _loggerManager: LogManagerInterface | null;
  /**
   * @param {Config=} config
   * @param {Object=} dependencies
   * @param {LogManagerInterface=} dependencies.loggerManager
   */
  constructor(
    config?: Config,
    {loggerManager}: {loggerManager?: LogManagerInterface} = {}
  ) {
    this._loggerManager = loggerManager;
    this._max_traffic = objectDeepValue(
      config,
      'bucketing.max_traffic',
      DEFAULT_MAX_TRAFFIC,
      true
    );
    this._hash_seed = objectDeepValue(
      config,
      'bucketing.hash_seed',
      DEFAULT_HASH_SEED,
      true
    );
    this._loggerManager?.trace?.(MESSAGES.BUCKETING_CONSTRUCTOR, this);
  }

  /**
   * Select variation based on its percentages and value provided
   * @param {object} buckets Key-value object with variations IDs as keys and percentages as values
   * @param {number} value A bucket value
   * @param {number=} redistribute Defaults to '0'
   * @return {string | null}
   */
  selectBucket(
    buckets: Record<string, number>,
    value: number,
    redistribute = 0
  ): string | null {
    let variation = null;
    let prev = 0;
    Object.keys(buckets).some((id) => {
      prev += buckets[id] * 100 + redistribute;
      if (value < prev) {
        variation = id;
        return true;
      }
      return false;
    });
    this._loggerManager?.debug?.(
      'BucketingManager.selectBucket()',
      {
        buckets: buckets,
        value: value,
        redistribute: redistribute
      },
      {variation: variation}
    );
    return variation || null;
  }

  /**
   * Get a value based on hash from Visitor id to use for bucket selecting
   * @param {Id} visitorId
   * @param {number=} seed
   * @return {number}
   */
  getValueVisitorBased(visitorId: Id, seed = this._hash_seed): number {
    const hash = Murmurhash.v3(String(visitorId), seed);
    const val = (hash / DEFAULT_MAX_HASH) * this._max_traffic;
    const result = parseInt(String(val), 10);
    this._loggerManager?.debug?.('BucketingManager.getValueVisitorBased()', {
      visitorId: visitorId,
      seed: seed,
      val: val,
      result: result
    });
    return result;
  }

  /**
   * Get a bucket for the visitor
   * @param {object} buckets Key-value object with variations IDs as keys and percentages as values
   * @param {Id} visitorId
   * @param {number} [redistribute=0]
   * @param {number} [seed=]
   * @return {string | null}
   */
  getBucketForVisitor(
    buckets: Record<string, number>,
    visitorId: Id,
    redistribute = 0,
    seed?: number
  ): string | null {
    const value = this.getValueVisitorBased(visitorId, seed);
    return this.selectBucket(buckets, value, redistribute);
  }
}