...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/evaluator_bucketing.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2

     1  package evaluation
     2  
     3  import (
     4  	"crypto/sha1" //nolint:gosec // SHA1 is cryptographically weak but we are not using it to hash any credentials
     5  	"encoding/hex"
     6  
     7  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/internal"
     8  
     9  	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
    10  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
    11  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    12  )
    13  
    14  const (
    15  	longScale = float32(0xFFFFFFFFFFFFFFF)
    16  
    17  	initialHashInputBufferSize = 100
    18  )
    19  
    20  type bucketingFailureReason int
    21  
    22  const (
    23  	bucketingFailureInvalidAttrRef bucketingFailureReason = iota + 1 // 0 means no failure
    24  	bucketingFailureContextLacksDesiredKind
    25  	bucketingFailureAttributeNotFound
    26  	bucketingFailureAttributeValueWrongType
    27  )
    28  
    29  // computeBucketValue is used for rollouts and experiments in flag rules, flag fallthroughs, and segment rules--
    30  // anywhere a rollout/experiment can be. It implements the logic in the flag evaluation spec for computing a
    31  // one-way hash from some combination of inputs related to the context and the flag or segment, and converting
    32  // that hash into a percentage represented as a floating-point value in the range [0,1].
    33  //
    34  // The isExperiment parameter is true if this is an experiment rather than a plain rollout. Experiments can use
    35  // the seed parameter in place of the context key and flag key; rollouts cannot. Rollouts can use the attr
    36  // parameter to specify a context attribute other than the key, and can include a context's "secondary" key in
    37  // the inputs; experiments cannot. Parameters that are irrelevant in either case are simply ignored.
    38  //
    39  // There are several conditions that could cause this computation to fail. The only one that causes an actual
    40  // error value to be returned is if there is an invalid attribute reference, since that indicates malformed
    41  // flag/segment data. For all other failure conditions, the method returns a zero bucket value, plus an enum
    42  // indicating the type of failure (since these may have somewhat different consequences in different areas of
    43  // evaluations).
    44  func (es *evaluationScope) computeBucketValue(
    45  	isExperiment bool,
    46  	seed ldvalue.OptionalInt,
    47  	contextKind ldcontext.Kind,
    48  	key string,
    49  	attr ldattr.Ref,
    50  	salt string,
    51  ) (float32, bucketingFailureReason, error) {
    52  	hashInput := internal.LocalBuffer{Data: make([]byte, 0, initialHashInputBufferSize)}
    53  	// As long as the total length of the append operations below doesn't exceed the initial size,
    54  	// this byte slice will stay on the stack. But since some of the data we're appending comes from
    55  	// context attributes created by the application, we can't rule out that they will be longer than
    56  	// that, in which case the buffer is reallocated automatically.
    57  
    58  	if seed.IsDefined() {
    59  		hashInput.AppendInt(seed.IntValue())
    60  	} else {
    61  		hashInput.AppendString(key)
    62  		hashInput.AppendByte('.')
    63  		hashInput.AppendString(salt)
    64  	}
    65  	hashInput.AppendByte('.')
    66  
    67  	if isExperiment || !attr.IsDefined() { // always bucket by key in an experiment
    68  		attr = ldattr.NewLiteralRef(ldattr.KeyAttr)
    69  	} else if attr.Err() != nil {
    70  		return 0, bucketingFailureInvalidAttrRef, badAttrRefError(attr.String())
    71  	}
    72  	selectedContext := es.context.IndividualContextByKind(contextKind)
    73  	if !selectedContext.IsDefined() {
    74  		return 0, bucketingFailureContextLacksDesiredKind, nil
    75  	}
    76  	uValue := selectedContext.GetValueForRef(attr)
    77  	if uValue.IsNull() { // attributes can't be null, so null means it doesn't exist
    78  		return 0, bucketingFailureAttributeNotFound, nil
    79  	}
    80  	switch {
    81  	case uValue.IsString():
    82  		hashInput.AppendString(uValue.StringValue())
    83  	case uValue.IsInt():
    84  		hashInput.AppendInt(uValue.IntValue())
    85  	default:
    86  		// Non-integer numbers, and values of any other JSON type, can't be used for bucketing because they have no
    87  		// single reliable representation as a string.
    88  		return 0, bucketingFailureAttributeValueWrongType, nil
    89  	}
    90  
    91  	if es.owner.enableSecondaryKey && !isExperiment { // secondary key is not supported in experiments
    92  		if secondary := selectedContext.Secondary(); secondary.IsDefined() { //nolint:staticcheck
    93  			// the nolint directive is because we're deliberately referencing the deprecated Secondary
    94  			hashInput.AppendByte('.')
    95  			hashInput.AppendString(secondary.StringValue())
    96  		}
    97  	}
    98  
    99  	hashOutputBytes := sha1.Sum(hashInput.Data) //nolint:gas // just used for insecure hashing
   100  	hexEncodedChars := make([]byte, 64)
   101  	hex.Encode(hexEncodedChars, hashOutputBytes[:])
   102  	hash := hexEncodedChars[:15]
   103  
   104  	intVal, _ := internal.ParseHexUint64(hash)
   105  
   106  	bucket := float32(intVal) / longScale
   107  
   108  	return bucket, 0, nil
   109  }
   110  

View as plain text