/**
*
* Generates a hash for the client identifier. Truncates the result
* to stay within the safe integer range (thx Tarjei). Runs async to
* support both browser and node contexts.
* @param identifier
* @returns {Promise<null|string>}
*/
const getTruncatedHash = async (identifier) => {
if (typeof window !== 'undefined') {
const encoder = new TextEncoder()
const data = encoder.encode(identifier)
try {
const hashBuffer = await crypto.subtle.digest('SHA-1', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray
.map((byte) => byte.toString(16).padStart(2, '0'))
.slice(0, 15)
.join('')
} catch (error) {
console.error('Error calculating hash:', error)
return null
}
} else {
return getTruncatedHashSync(identifier)
}
}
/**
* Only runs in node context, not browser context.
* @param identifier
* @returns {string}
*/
const getTruncatedHashSync = identifier => {
const crypto = require('crypto')
const hash = crypto.createHash('sha1')
hash.update(identifier)
// Extract the first 15 characters (or adjust as needed)
return hash.digest('hex').slice(0, 15)
}
/**
* Returns the qualification, bucket the client places in,
* and the hashed identifier.
* @param hashedIdentifier
* @param totalBuckets
* @param rolloutDirection
* @param rolloutPercentage
* @returns {{clientBucket: number, isQualified: boolean, hashedIdentifier}}
*/
const getQualification = ({hashedIdentifier, totalBuckets, rolloutDirection, rolloutPercentage}) => {
// Calculate the bucket for the client identifier
const clientBucket = parseInt(hashedIdentifier, 16) % totalBuckets;
// Determine the qualification threshold based on the rollout direction
const bucketRolloutThreshold = totalBuckets * (rolloutPercentage / 100)
// when rolling up, client bucket is compared to a range starting at 0 up
// to the rollout percentage. when rolling down, client bucket is compared
// to a range starting from the min (total buckets with the percentage rolled
// out subtracted from it), up to the max (total buckets)
const isRollingOutUp = (rolloutDirection === DIRECTION_UP)
// Check if the client qualifies
const isQualified = isRollingOutUp
? clientBucket <= bucketRolloutThreshold
: clientBucket > (totalBuckets - bucketRolloutThreshold)
// Return an object with the qualification result
return {
isQualified,
hashedIdentifier,
clientBucket
};
}
/**
* The default total number of buckets.
* @type {number}
*/
export const DEFAULT_TOTAL_BUCKETS = 100;
/**
* The constant representing the 'UP' direction for rollout.
* @type {string}
*/
export const DIRECTION_UP = 'UP';
/**
* The constant representing the 'DOWN' direction for rollout.
* @type {string}
*/
export const DIRECTION_DOWN = 'DOWN';
/**
*
* Checks if a client qualifies for rollout based on the provided parameters.
* @param {Object} options - The options for the rollout check.
* @param {string} options.clientIdentifier - The client identifier.
* @param {number} options.rolloutPercentage - The percentage of rollout.
* @param {string} [options.rolloutDirection=DIRECTION_UP] - The direction of rollout.
* @param {number} [options.totalBuckets=DEFAULT_TOTAL_BUCKETS] - The total number of buckets.
* @returns {Object} - An object containing the qualification result.
* @property {boolean} isQualified - Whether the client qualifies or not.
* @property {string} hashedIdentifier - The hashed client identifier.
* @property {number} bucket - The bucket value.
* @returns {Promise<{clientBucket: number, isQualified: boolean, hashedIdentifier}>}
*/
export const qualifiesForRollout = async ({ clientIdentifier, seed, rolloutPercentage, rolloutDirection = DIRECTION_UP, totalBuckets = DEFAULT_TOTAL_BUCKETS }) => {
// Hash the client identifier
const hashedIdentifier = await getTruncatedHash(`${seed?.length > 0 ? `${seed}:` : ''}${clientIdentifier}`);
return getQualification({
hashedIdentifier,
totalBuckets,
rolloutDirection,
rolloutPercentage
})
}
/**
*
* Checks if a client qualifies for rollout based on the provided parameters.
* Performs this synchronously -- only works in node context, not browser context.
*
* @param {Object} options - The options for the rollout check.
* @param {string} options.clientIdentifier - The client identifier.
* @param {number} options.rolloutPercentage - The percentage of rollout.
* @param {string} [options.rolloutDirection=DIRECTION_UP] - The direction of rollout.
* @param {number} [options.totalBuckets=DEFAULT_TOTAL_BUCKETS] - The total number of buckets.
* @returns {Object} - An object containing the qualification result.
* @property {boolean} isQualified - Whether the client qualifies or not.
* @property {string} hashedIdentifier - The hashed client identifier.
* @property {number} bucket - The bucket value.
*/
export const qualifiesForRolloutSync = ({ clientIdentifier, seed, rolloutPercentage, rolloutDirection = DIRECTION_UP, totalBuckets = DEFAULT_TOTAL_BUCKETS }) => {
// Hash the client identifier
const hashedIdentifier = getTruncatedHashSync(`${seed?.length > 0 ? `${seed}:` : ''}${clientIdentifier}`);
return getQualification({
hashedIdentifier,
totalBuckets,
rolloutDirection,
rolloutPercentage
})
}