Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/bucketing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
* License Apache-2.0
*/
export {BucketingManager} from './src/bucketing-manager';
export {BucketingManagerInterface} from './src/interfaces/bucketing-manager';
export {
BucketingManagerInterface,
BucketAnchoredRange
} from './src/interfaces/bucketing-manager';
99 changes: 97 additions & 2 deletions packages/bucketing/src/bucketing-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
* License Apache-2.0
*/

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

import {
BucketingAllocation,
BucketingHash,
Config
Config,
VariationAllocation
} from '@convertcom/js-sdk-types';
import {generateHash} from '@convertcom/js-sdk-utils';
import {LogManagerInterface} from '@convertcom/js-sdk-logger';
Expand Down Expand Up @@ -134,4 +138,95 @@ export class BucketingManager implements BucketingManagerInterface {
bucketingAllocation: value
} as BucketingAllocation;
}

/**
* Build the anchored bucket layout for a set of variation allocations (qs-01 / BUCK-2).
* Anchors are computed over the total weight of ALL entries (active and inactive) so
* that raising an experience's total allocation only ever grows arms (superset property)
* and never reshuffles an already-bucketed visitor into a different arm. Inactive (or
* explicit zero-allocation) entries keep their weight for anchor stability but get a
* zero-width range so they can never be selected.
* @param {VariationAllocation[]} allocations Variation allocations in config order
* @return {BucketAnchoredRange[]}
*/
getBucketRanges(allocations: VariationAllocation[]): BucketAnchoredRange[] {
const totalWeight = allocations.reduce(
(sum, {allocation}) => sum + allocation,
0
);
const ranges: BucketAnchoredRange[] = [];
if (totalWeight <= 0) {
this._loggerManager?.debug?.('BucketingManager.getBucketRanges()', {
allocations: allocations,
totalWeight: totalWeight
});
return ranges;
}
let cumWeight = 0;
allocations.forEach(({id, allocation, active}) => {
const anchor = (cumWeight / totalWeight) * DEFAULT_MAX_TRAFFIC;
const width = active ? allocation * 100 : 0;
ranges.push({id, anchor, width});
cumWeight += allocation;
});
this._loggerManager?.debug?.(
'BucketingManager.getBucketRanges()',
{allocations: allocations, totalWeight: totalWeight},
{ranges: ranges}
);
return ranges;
}
Comment thread
abbaseya marked this conversation as resolved.

/**
* Select the variation whose anchored range contains the provided value.
* @param {BucketAnchoredRange[]} ranges Anchored bucket ranges (see {@link getBucketRanges})
* @param {number} value A bucket value
* @return {string | null}
*/
selectBucketAnchored(
ranges: BucketAnchoredRange[],
value: number
): string | null {
let variation = null;
ranges.some(({id, anchor, width}) => {
if (value >= anchor && value < anchor + width) {
variation = id;
return true;
}
return false;
});
this._loggerManager?.debug?.(
'BucketingManager.selectBucketAnchored()',
{ranges: ranges, value: value},
{variation: variation}
);
return variation;
}

/**
* Get an anchored bucket for the visitor (qs-01 / BUCK-2). Reuses the existing
* visitor-based hash value unchanged, then resolves it through the anchored layout.
* @param {VariationAllocation[]} allocations Variation allocations in config order
* @param {string} visitorId
* @param {BucketingHash=} options
* @param {number=} [options.seed=]
* @param {string=} [options.experienceId=]
* @return {BucketingAllocation | null}
*/
getBucketForVisitorAnchored(
allocations: VariationAllocation[],
visitorId: string,
options?: BucketingHash
): BucketingAllocation | null {
const value = this.getValueVisitorBased(visitorId, options);
const selectedBucket = this.selectBucketAnchored(
this.getBucketRanges(allocations),
value
);
if (!selectedBucket) return null;
return {
variationId: selectedBucket,
bucketingAllocation: value
} as BucketingAllocation;
}
}
30 changes: 29 additions & 1 deletion packages/bucketing/src/interfaces/bucketing-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,22 @@
* Copyright(c) 2020 Convert Insights, Inc
* License Apache-2.0
*/
import {BucketingAllocation, BucketingHash} from '@convertcom/js-sdk-types';
import {
BucketingAllocation,
BucketingHash,
VariationAllocation
} from '@convertcom/js-sdk-types';

/**
* A precomputed anchored bucket range for a single variation.
* `anchor` and `anchor + width` bound the half-open interval `[anchor, anchor + width)`
* (per-10000 traffic space) that maps to `id` in the anchored layout.
*/
export type BucketAnchoredRange = {
id: string;
anchor: number;
width: number;
};

export interface BucketingManagerInterface {
selectBucket(
Expand All @@ -20,4 +35,17 @@ export interface BucketingManagerInterface {
visitorId: string,
options?: BucketingHash
): BucketingAllocation | null;

getBucketRanges(allocations: VariationAllocation[]): BucketAnchoredRange[];

selectBucketAnchored(
ranges: BucketAnchoredRange[],
value: number
): string | null;

getBucketForVisitorAnchored(
allocations: VariationAllocation[],
visitorId: string,
options?: BucketingHash
): BucketingAllocation | null;
}
Loading