1

For soft rollouts of new features, I want my server which holds the feature flags to return a percentage number, and then have the client figure out which side of the percentage it is on. I want to use the user-id to make this choice consistently, so that an individual user will always see the same side of the flag.

I've gotten as far as

(crypto.createHash('md5').update(userID).digest('hex').charCodeAt(0)

Which hashes the ID(which will be some large string), then takes the first character of the hash and converts it to an integer id. Unfortunately that number seems to not be a random distribution, so even if I shift and multiply it so that it is always between 0 and 100, it doesn't seem randomly even. Here are some hashes of random strings I ran:

checking  25
15.2
checking  26
96.89999999999999
checking  27
98.8
checking  28
13.299999999999999
checking  29
15.2
checking  30
102.6
checking  31
15.2
checking  32
7.6
checking  33
102.6
checking  34
15.2
checking  35
102.6
checking  36
15.2
checking  37
7.6
checking  38
96.89999999999999
checking  39
96.89999999999999
checking  40
7.6
checking  41
100.69999999999999
checking  42
13.299999999999999
checking  43
96.89999999999999
checking  44
93.1
checking  45
96.89999999999999
checking  46
1.9
checking  47
15.2
checking  48

Is there a better way to do this check so that it creates a random distribution of percentages?

light24bulbs
  • 2,871
  • 3
  • 24
  • 33
  • 1
    Your code creates a hex-formatted digest, so the first character will always be one of the characters from "0" through "F". If you don't pass an encoding to `.digest()` you get back a Buffer and you can use the first byte of that. – Pointy Jun 13 '18 at 16:17
  • What if you took, say, the last two characters of the hex (giving you a decimal value between 0 and 255)? You could then divide that by 255 to get a percentage. Not sure if that would give you an acceptable distribution. You could always take additional hex characters to get a larger range of numbers. – Mike B. Jun 13 '18 at 16:25
  • 1
    It's going to be hard to wrangle a binary value into a neat 0-100 range, or 101 possible values. The best you can do is divide some arbitrarily large value (64-bit?) by a factor to cut that space down to a 0-100 range. For example, divide your unsigned 64-bit value by 182641030432767837. – tadman Jun 13 '18 at 16:36
  • 1
    @tadman That's pretty much exactly what I did, please have a look at my answer below. I think there are a lot of ways to skin this cat, but taking two numbers out of the middle of a big integer seems just fine to me – light24bulbs Jun 13 '18 at 17:52

1 Answers1

0

I came up with this, which seems to work pretty well:

parseInt(crypto.createHash('md5').update(flagName+ userID).digest('hex'), 16).toString().substring(4, 6)

That's a bit insane, but it seems to work well. ParseInt creates a huge number, and then I grab some digits from within the decimal.

checking  D
17
checking  N
23
checking  F
17
checking  J
30
checking  Z
76
checking  L
20
checking  N
23
checking  X
50
checking  L
20
checking  F
17
checking  H
67
checking  U
52
checking  H
67
checking  3
75
checking  8
42
checking  2
00
checking  9
37
checking  3
75
checking  5
19
checking  0
21
checking  1
57
checking  3
75
checking  9
37
checking  4
97
checking  5
19
checking  0
21
checking  d
42
checking  k
27
checking  l
77
checking  z
53
checking  i
59
ecm
  • 2,583
  • 4
  • 21
  • 29
light24bulbs
  • 2,871
  • 3
  • 24
  • 33
  • 1
    I'm allergic to MD5, but otherwise this should be good enough so long as those values don't really matter other than being scattered. You should probably check the distribution with a test on a million test strings to be sure it balances like you're expecting. – tadman Jun 13 '18 at 18:12