Skip to content

Fix flaky RECAPTCHA_SIGN_IN A/B test#11264

Merged
jmhooper merged 2 commits intomainfrom
jmhooper-fix-flaky-ab-test
Sep 19, 2024
Merged

Fix flaky RECAPTCHA_SIGN_IN A/B test#11264
jmhooper merged 2 commits intomainfrom
jmhooper-fix-flaky-ab-test

Conversation

@jmhooper
Copy link
Contributor

@jmhooper jmhooper commented Sep 19, 2024

The RECAPTCHA_SIGN_IN A/B test has a discriminator block that does the following:

  1. Return nil if captcha was not performed
  2. Return the user's UUID if the user is present
  3. Return a random value if no user is present with the intent of sorting them into a bucket

The implementation of AbTest#bucket checks if the discriminator resolved by this discriminator block is blank and returns nil if it is (ref).

The blank? method has a special implementation for String (ref) that returns true if the string consists of only whitespace.

The random value here was generated using SecureRandom.gen_random. This value was then used in the AbTest#bucket to randomly sort unidentified users into AB test buckets. SecureRandom.gen_random returns a string of random bytes which can include " ", "\n", "\r", etc.

The combination of the above meant that the discriminator could generate a value that caused the AbTest#bucket to return nil for RECAPTCHA_SIGN_IN when it was intended to return a bucket. This lead to a flaky test (this spec tests that the bucket is not nil; it is nil in the case where SecureRandom.gen_random returns a blank string).

This commit changes the call to SecureRandom so that it only ever generates alphanumeric strings which will never be blank.

The `RECAPTCHA_SIGN_IN` has a discriminator block that does the following:

1. Return `nil` if captcha was not performed (this puts the user in the default bucket)
2. Return the user's UUID if the user is present
3. Return a random value if no user is present

The implementation of the bucket method checks if the discriminator is blank and returns `nil` if it is ([ref](https://github.com/18F/identity-idp/blob/983e21648dc7695f11c37e3eff7997ef4bd90f1e/lib/ab_test.rb#L46)).

The `blank?` method has a special implementation implementation for `String` ([ref](https://github.com/rails/rails/blob/dfd1e951aa1aeef06c39fffb2994db8a8fa1914f/activesupport/lib/active_support/core_ext/object/blank.rb#L141-L163)) that returns `true` if the string consists of only whitespace.

The random value here was generated using `SecureRandom.gen_random`. This value was then used in the `AbTest#bucket` to randomnly sort unidentified users into AB test buckets. `SecureRandom.gen_random` returns a string of random bytes which can include `" "`, `"\n"`, `"\r"`, etc.

The combination of the above meant that the discriminator could generate value that caused the `AbTest#bucket` to return `nil` for `RECAPTCHA_SIGN_IN` when it was intended to return a bucket. This lead to a flaky test.

This commit changes the call to generate a random string such that it only ever generates alphanumeric strings which will never be blank.

[skip changelog]
Copy link
Contributor

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks 👍

@aduth
Copy link
Contributor

aduth commented Sep 19, 2024

The implementation of AbTest#bucket checks if the discriminator resolved by this discriminator block is blank and returns nil if it is (ref).

Alternatively, maybe this should check .nil? ? I don't know if we're expecting other values to follow this code path (e.g. false, empty string '')

@jmhooper
Copy link
Contributor Author

I figured it was safest to change the implementation in the ReCaptcha bucket to avoid any possible side-effects. I'm not sure why we use #blank? and not #nil?, but it looks like it has been there since the beginning.

Another thought is that SecureRandom.alphanumeric(1) has a much smaller set of values it can generate. That could lead to issues where you don't see anybody sorted into buckets with small percentages. WDYT of changing that to something like SecureRandom.alphanumeric(8)?

@aduth
Copy link
Contributor

aduth commented Sep 19, 2024

That seems fine, yeah 👍

@jmhooper jmhooper merged commit 47c2325 into main Sep 19, 2024
@jmhooper jmhooper deleted the jmhooper-fix-flaky-ab-test branch September 19, 2024 16:00
AShukla-GSA pushed a commit that referenced this pull request Sep 30, 2024
The `RECAPTCHA_SIGN_IN` A/B test has a discriminator block that does the following:

1. Return `nil` if captcha was not performed
2. Return the user's UUID if the user is present
3. Return a random value if no user is present with the intent of sorting them into a bucket

The implementation of `AbTest#bucket` checks if the discriminator resolved by this discriminator block is blank and returns `nil` if it is ([ref](https://github.com/18F/identity-idp/blob/983e21648dc7695f11c37e3eff7997ef4bd90f1e/lib/ab_test.rb#L46)).

The `blank?` method has a special implementation for `String` ([ref](https://github.com/rails/rails/blob/dfd1e951aa1aeef06c39fffb2994db8a8fa1914f/activesupport/lib/active_support/core_ext/object/blank.rb#L141-L163)) that returns `true` if the string consists of only whitespace.

The random value here was generated using `SecureRandom.gen_random`. This value was then used in the `AbTest#bucket` to randomly sort unidentified users into AB test buckets. `SecureRandom.gen_random` returns a string of random bytes which can include `" "`, `"\n"`, `"\r"`, etc.

The combination of the above meant that the discriminator could generate a value that caused the `AbTest#bucket` to return `nil` for `RECAPTCHA_SIGN_IN` when it was intended to return a bucket. This lead to a flaky test ([this spec](https://github.com/18F/identity-idp/blob/983e21648dc7695f11c37e3eff7997ef4bd90f1e/spec/config/initializers/ab_tests_spec.rb#L197-L203) tests that the bucket is not nil; it is nil in the case where `SecureRandom.gen_random` returns a blank string).

This commit changes the call to `SecureRandom` so that it only ever generates alphanumeric strings which will never be blank.


[skip changelog]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants