Firefox for Android (codenamed Fennec) started using KeepSafe’s Switchboard library and server component in order to run A/B testing or staged rollout of features, but since then the code changed significantly. It used a special switchboard server that decided which experiments a client is part of and returned a simplified list for the client to consume. However, this required the client to send data (including a unique id) to the server.
To avoid this Firefox moved to using Kinto as storage and server of the experiment configuration. Clients now download the whole list of experiments and decide locally what experiments they are enrolled in (the configuration for Fennec looks like this).
The purpose of this Google Summer of Code project, which is called Fretboard, was to develop an A/B testing framework written in Kotlin based on the existing code from Fennec, but making it independent of both the server used and the local storage mechanism, allowing it to be used on other apps, such as Firefox Focus, which started to need to do some A/B tests.
This is a basic and non-exhaustive list of features, for all details you can view the README here
- Query if a device is part of a specific experiment
- Get experiment associated metadata
- Override a specific experiment (force activate / deactivate it)
- Override device values (such as the appId, version, etc)
- Update experiment list from the server manually or using JobScheduler
- Update experiment list using WorkManager (blocked waiting for a non-alpha version of WorkManager to be released by Google)
- Default source implementation for Kinto
- Uses diff requests to reduce bandwith usage
- Support for certificate pinning (pending security review)
- Support for validating the signature of the downloaded experiment collection (pending security review)
- Default storage implementation using a flat JSON file
Fretboard allows you to specify the following filters:
- Buckets: Every user is in one of 100 buckets (0-99). For every experiment you can set up a min and max value (0 <= min <= max <= 100). The bounds are [min, max).
- Both max and min are optional. For example, specifying only min = 0 or only max = 100 includes all users
- 0-100 includes all users (as opposed to 0-99)
- 0-0 includes no users (as opposed to just bucket 0)
- 0-1 includes just bucket 0
- Users will always stay in the same bucket. An experiment targeting 0-25 will always target the same 25% of users
- appId (regex): The app ID (package name)
- version (regex): The app version
- country (regex): country, pulled from the default locale
- lang (regex): language, pulled from the default locale
- device (regex): Android device name
- manufacturer (regex): Android device manufacturer
- region: custom region, different from the one from the default locale (like a GeoIP, or something similar).
- release channel: release channel of the app (alpha, beta, etc)
- Issue #17: Run code quality tools: detekt, ktlint and codecov
- Remove codecov uploading from taskcluster
- Issue #20: Upload test coverage results to codecov
- Issue #3: Implement client for loading (partial) experiment configuration from server, Issue #4: Implement storage for saving experiment configuration to disk
- Make experiments variable private and fetch experiments from local storage when updating
- Make loadExperiments and updateExperiments synchronized and guard against storage file not found
- Issue #5: Schedule frequent updates of experiments configuration
- Issue #6: Implement code for evaluating experiment configuration and bucketing users
- Changed uuid type to String
- Add RegionProvider
- Issue #7: Implement simple API for checking if an installation is part of a specific experiment
- Issue #29: Add a more Kotlin idiomatic method for checking experiments
- Issue #12: Implement simple API for getting experiment metadata
- Issue #32: JSONExperimentParserTest is not deterministic
- Issue #37: Add “export TERM=dumb” to taskcluster script
- Issue #38: detekt is configured to only run on ‘fretboard’ module
- Issue #41: Add test for IOException on HttpURLConnectionHttpClient
- Issue #14: Add mechanism for overriding the local experiment configuration
- Issue #46: Rename AtomicFileExperimentStorage to FlatFileExperimentStorage
- Issue #53: Change FlatFileExperimentStorage instrumentation tests to use File and remove temp file when done
- Issue #56: Rename FlatFileExperimentStorage package to flatfile
- Issue #50: Make FlatFileExperimentStorage receive a File
- Issue #432: Fretboard: Kinto delete diffs might lead to crash
- Issue #435: Fretboard: Documentation and guides
- Issue #460: Fretboard: Update documentation
- Issue #456: Fretboard: Allow filtering by release channel
- Issue #464: Let app access a list of experiments
- Issue #466: Fretboard: Move JSONExtensions into support-ktx
- Issue #115: Core ping: Report experiments
- Issue #485: Remove List.toJSONArray extension method
- Issue #487: Fretboard: Add helper method to get active experiments
- Issue #501: Move JSON extensions to package android.org.json
- Issue #504: Fretboard: Require network for JobScheduler-based scheduler
- Issue #526: fretboard: README: Explain buckets with examples
- Issue #524: Fretboard: Add kdoc to Experiment properties
- Issue #542: Fretboard: Remove deleted RegionProvider from README
- Issue #541: Fretboard: ExperimentDescriptor should use the experiment name instead of the id
- Issue #555: Fretboard: Add test for non HTTP url for HttpURLConnectionHttpClient
- Issue #557: Fretboard: ExperimentEvaluator: Add tests for empty values and release channel
- Issue #559: Fretboard: Add tests for ExperimentPayload
- Issue #561: Fretboard: Add more tests for Fretboard class
- Issue #563: Fretboard: Make kinto properties private
- Issue #565: Fretboard: Handle JSON exceptions on Kinto
- Issue #570: Fretboard: More idiomatic Kotlin on FlatFileExperimentStorage and on ExperimentSerializer
- Issue #572: Fretboard: Complete kdoc
- Issue #577: Fretboard: Pass original exception to ExperimentDownloadException
- Issue #576: Fretboard: Log ExperimentDownloadException
- Issue #590: Fretboard: Blog post
- Issue #434: Fretboard: Verify signatures of experiments collection
Open pull requests
There are two open pull requests. The first one is open pending a review from the security team and the last one is waiting for a non-alpha version of WorkManager to be released by Google:
Project progress and difficulties faced
Prior to starting the project, I became familiar with Kinto and the diff response format, as well as with the existing code of the Switchboard fork from Fennec. I also thought it was a good idea to send a pull request to Firefox Focus because this library was going to be integrated into it, and also to become familiar with a code review process at Mozilla. I looked at the issue list and discovered a problem with display cutouts, so I sent two pull requests to address the issue: the first one and the second one.
The most difficult pull requests for me were the ones related to certificate pinning and experiment collection signature verification. For the first one I had to broaden my knowledge about it, as well as research how to properly implement it on Android, avoiding common mistakes.
For the second one the most difficult part was to understand what algorithm Mozilla was using to validate the signatures, and how it worked. I discovered from the Kinto collection
mode field that it was
p384ecdsa, and then I had to research how to properly implement it in Kotlin. For this later I needed the help of Julien Vehent and Franziskus Kiefer, which pointed me to a great talk and also a Go and C++ implementation. After seeing the two implementations I realized my solution wasn’t working because I didn’t know that the signature actually contained two values concatenated (r and s), which then needed to be encoded using DER syntax
Overall I think I learned a lot doing this project and I really loved working with Mozilla.
Right now there is an ongoing discussion about enhancing Fretboard with an expression language (being that JEXL/CEL/etc) for the matchers values instead of regular expressions like it’s using now.
I would like to thank my mentor Sebastian Kaspari for all the help and guidance, for being so friendly and available to talk at any moment I needed, as well as reviewing my pull requests quickly.
I would also like to thank Franziskus Kiefer and Julien Vehent for helping me understand the signature validation system used by Kinto.