Experiments

Experiments are a form of A/B testing used by Discord in the client- and server-side of their applications to serve different experiences or behaviours to different users randomly and/or based on location, client version, etc.

Currently, Discord uses two different systems for experiments: the legacy system, which encapsulates user and guild experiments, and the new Apex system, which is more flexible but provides clients with less data. Both systems are still in use, but most new experiments are created in the Apex system, so the legacy system will eventually be deprecated.

At their core, rollouts are simply YAML inputted into the Discord admin panel; however, not all of the data is required by clients. Therefore, the actual experiments clients see are minified and complex to decipher.

Fingerprints

Even the marketing website uses A/B tests to hook users into the app. Therefore, the app needs a unique way to identify the person using the website without using authentication (for users initially visiting the landing page), so it resorts to using "fingerprints".

These aren't the usual fingerprints generated by collecting information about the browser; instead, they are snowflakes generated by unauthenticated requests to Get Experiment Assignments. It is expected that fingerprints are sent in the X-Fingerprint header in all subsequent requests to the API until authentication, in order to track A/B tests and allow access to API-locked portions of experiments.

A fingerprint is comprised of a snowflake and a hashed cryptographic value. It looks like this: 1084179945133187083.JQddgNMmwJPghoBtFmaH7jTmdsw.

When registering a new account, the fingerprint is passed, and (if valid) is used as the created user's ID. This is done in order to preserve experiments across to the registered user. Therefore, a user account's creation time theoretically represents the first time they visited Discord's marketing website.

Installations

The new Apex experiment system introduces the concept of installations, which are a unique identifier for a specific installation of a Discord client. This allows experiments to be targeted not just by user or guild, but by specific app installations, enabling more granular testing and rollouts.

Installations use an ID system very similar to fingerprints. The biggest difference is that they persist even after authentication. If a client already has an installation ID, it should be sent in the X-Installation-ID header in all requests to the API, and provided in the installation_id field when identifying with the Gateway. If an installation ID is invalid or not provided, a new one will be provided in the installation field of the response body of the Get Apex Experiment Assignments endpoint, or the apex_experiments field in the Ready event.

Rollouts

Legacy experiment rollouts are defined in populations based on the user or guild's rollout position and can have filters to narrow each population's availability.

Example Rollout

Note that this example is editorial and does not exactly represent how experiments are represented internally.

2023-02_stage_boosting (1816004721)
### Treatment 1
Filters
Guild Features: [COMMUNITY]
Member Count Range: 1000 - null
Hash Range: Hash Key: 1816004721, Target: 10000
Position Ranges
5000-9500, 9500-10000
### Control
Filters
Guild Features: [COMMUNITY]
Member Count Range: 1000 - null
Position Ranges
0 - 10000
### None
Position Ranges
0 - 10000

Treatments

Control and None are represented in rollouts as the integer values 0 and -1, respectively. Note that there are some instances of experiments having specific unnecessary treatments labelled as Control with different treatment values.

Rollout Positions

A rollout position is calculated using the following pseudo code, where exp_name is the human readable name for the experiment and resource_id is the user, fingerprint or guild's ID. This position is used in conjunction with the rollout populations and filters to figure out what the assigned bucket for the experiments are by simply checking which treatment the population is included in.

result = mmh3.hash('exp_name:resource_id', signed=False) % 10000

Data Structures

There are two types of experiments, user experiments and guild experiments. Metadata and human-readable experiment names are available in the user-facing clients. However, API objects do not contain such data, except for guild experiments which may have a human-readable name provided for hash calculations, overriding the one in clients.

Most of the below objects are represented as arrays following the order the fields are documented in.

User Experiments

User experiment data returned by the API is very limited. In contrast to the wide range of data the API provides for guild rollouts, the only values we can programmatically retrieve from the API are the user or fingerprint's assigned bucket for the experiment.

User Experiment Structure

This object is represented as an array of the following fields:

FieldTypeDescription
hashinteger32-bit unsigned Murmur3 hash of the experiment's name
revisionintegerCurrent version of the rollout
bucketintegerThe requesting user or fingerprint's assigned experiment bucket
overrideintegerWhether the user or fingerprint has an override for the experiment (-1 for false, 0 for true)
populationintegerThe internal population group the requesting user or fingerprint is in
hash_resultintegerThe calculated rollout position to use, prioritized over local calculations
aa_mode 1integerThe experiment's A/A testing mode, represented as an integer-casted boolean
trigger_debuggingintegerWhether the experiment's analytics trigger debugging is enabled, represented as an integer-casted boolean
holdout_name 2?stringA human-readable experiment name (formatted as year-month_name) that disables the experiment
holdout_revision 2?integerThe revision of the holdout experiment
holdout_bucket 2?integerThe requesting user or fingerprint's assigned bucket for the holdout experiment

1 The bucket for A/A tested experiments should always be None (-1) unless an override is present for the resource.

2 Holdout information is only present if the user or fingerprint has an assigned bucket for the holdout experiment. Therefore, if holdout experiment information is present and the population bucket is set to None (-1), the experiment has been disabled by the holdout. As user experiments are opaque, no client handling is required for this field. Just follow the population field as usual.

Example User Experiment
[826493636, 3, -1, -1, 0, 203, 0, 1, "2025-02_user_profile_editing", 2, 0]

Guild Experiments

The data provided here is more detailed, because the client has to figure out itself the assigned bucket for each guild. It may seem daunting to parse this given the sheer amount of arrays, but it's really quite simple.

Guild Experiment Structure

This object is represented as an array of the following fields:

FieldTypeDescription
hashinteger32-bit unsigned Murmur3 hash of the experiment's name
hash_key 1?stringA human-readable experiment name (formatted as year-month_name) to use for hashing calculations, prioritized over the client name
revisionintegerCurrent version of the rollout
populationsarray[experiment population object]The experiment rollout's populations
overrides 2array[experiment bucket override object]Specific bucket overrides for the experiment
overrides_formatted 2array[array[experiment population object]]Populations of overrides for the experiment
holdout_name 3?stringA human-readable experiment name (formatted as year-month_name) that disables the experiment
holdout_bucket 3?integerThe holdout experiment bucket that disables the experiment
aa_mode 2integerThe experiment's A/A testing mode, represented as an integer-casted boolean
trigger_debuggingintegerWhether the experiment's analytics trigger debugging is enabled, represented as an integer-casted boolean

1 Used to categorize multiple experiments together for coordinated rollouts.

2 The population bucket for A/A tested experiments should always be None (-1) unless an override is present for the resource.

3 If a holdout experiment is present and the guild is in the holdout bucket, the population bucket will be set to None (-1), disabling the experiment unless an override is present.

Example Guild Experiment
[
1405831955,
"2021-06_guild_role_subscriptions",
0,
[
[
[
[
-1,
[
{
"s": 7200,
"e": 10000
}
]
],
[
1,
[
{
"s": 0,
"e": 7200
}
]
]
],
[
[
2294888943,
[
[2690752156, 1405831955],
[1982804121, 10000]
]
]
]
]
],
[],
[
[
[
[
[
1,
[
{
"s": 0,
"e": 10000
}
]
]
],
[[1604612045, [[1183251248, ["GUILD_ROLE_SUBSCRIPTIONS"]]]]]
]
]
],
null,
null,
0,
0
]

Experiment Population Object

The population object defines a set of filters and position ranges required to meet specific buckets.

Experiment Population Structure

This object is represented as an array of the following fields:

FieldTypeDescription
rangesarray[experiment population range object]The ranges for this population
filtersexperiment population filters objectThe filters that the resource must satisfy to be in this population
Example Experiment Population
[
[
[
-1,
[
{
"s": 7200,
"e": 10000
}
]
]
],
[
[
2294888943,
[
[2690752156, 1405831955],
[1982804121, 10000]
]
]
]
]

Experiment Population Range Object

If the filters in a given population are satisfied and a range includes the resource's rollout position, the resource is then eligible for the given bucket.

Experiment Population Range Structure

This object is represented as an array of the following fields:

FieldTypeDescription
bucketintegerThe bucket this range grants
rolloutarray[experiment population rollout object]The range rollout
Experiment Population Rollout Structure
FieldTypeDescription
sintegerThe start of this range
eintegerThe end of this range
Example Experiment Population Range
[
1,
[
{
"s": 0,
"e": 4750
}
]
]

Experiment Population Filters Object

This object defines the filters required to be eligible for the ranges. All provided filters must be satisfied for the resource to be eligible for the given bucket.

The filters are an object represented as an array of arrays. The first item in the nested array is a 32-bit unsigned Murmur3 hashed representation of the key, and the second item is the value, with the value being another array-represented object. All structures below are represented in this way.

Experiment Population Filters Structure
FieldTypeDescription
guild_has_feature?experiment population guild feature filter objectThe guild features that are eligible
guild_id_range?experiment population range filter objectThe range of snowflake resource IDs that are eligible
guild_age_range_days? 1experiment population range filter objectThe range of guild ages (in days) that are eligible
guild_member_count_range?experiment population range filter objectThe range of guild member counts that are eligible
guild_ids?experiment population ID filter objectA list of resource IDs that are eligible
guild_hub_types?experiment population hub type filter objectA list of hub types that are eligible
guild_has_vanity_url?experiment population vanity URL filter objectWhether the guild must or must not have a vanity to be eligible
guild_in_range_by_hash?experiment population range by hash filter objectThe special rollout position limits on the population

1 The guild age is determined from the guild's ID. See the snowflake documentation for more information. The age can be calculated using the following pseudocode, where resource_id is the guild's ID:

timestamp = ((resource_id >> 22) + 1420070400000) / 1000
guild_age = (time.time() - timestamp) / 86400
Experiment Population Guild Feature Filter Structure
FieldTypeDescription
guild_featuresarray[string]The guild features eligible for this population; only one feature is required for eligibility
Experiment Population Range Filter Structure
FieldTypeDescription
min_id?snowflakeThe exclusive minimum for this range, if any
max_id?snowflakeThe exclusive maximum for this range, if any
Experiment Population ID Filter Structure
FieldTypeDescription
guild_idsarray[snowflake]The list of snowflake resource IDs that are eligible for this population
Experiment Population Hub Type Filter Structure
FieldTypeDescription
guild_hub_typesarray[integer]The type of hubs that are eligible for this population
Experiment Population Vanity URL Filter Structure
FieldTypeDescription
guild_has_vanity_urlbooleanThe required vanity URL holding status for this population
Experiment Population Range By Hash Filter Structure
FieldTypeDescription
hash_keyintegerThe 32-bit unsigned Murmur3 hash of the key used to determine eligibility
targetintegerThe rollout position limit for this population
Example Experiment Population Filters
[
[
1604612045, // guild_has_feature
[
[
1183251248, // guild_features
["ROLE_SUBSCRIPTIONS_ENABLED"]
]
]
]
]

Experiment Bucket Override Object

An override represents a manual setting by Discord employees to grant a guild early or specific access to an experiment.

Experiment Bucket Override Structure
FieldTypeDescription
bintegerBucket assigned to these resources
karray[snowflake]Resources granted access to this bucket
Experiment Bucket Override Example
{
"b": 1,
"k": ["882680660588904448", "882703776794959873", "859533785225494528", "859533828754505741"]
}

Apex Experiments

Apex experiments are a new type of experiment that is far more flexible and allows for more complex targeting and assignment logic. While they are far more powerful, they also provide a lot less data to clients and allow for more opaque experimentation, preventing feature disclosure.

Notably, Apex experiments use the concept of variants instead of buckets, and are assigned to specific units (i.e. users, guilds, etc.) instead of being calculated on the client side based on filters and rollout positions.

Eligibility

Apex experiments use flags to determine evaluation precedence, especially for GUILD unit type experiments which evaluate both a guild-level and user-level assignment. Clients should determine final variant using the following priority:

  1. User Override: If a user assignment is present and has the IS_OVERRIDE flag, it should be used as the assigned variant for all guilds
  2. Guild Override: If a guild assignment is present and has the IS_OVERRIDE flag, it should be used as the assigned variant for the guild
  3. Eligibility Gate: If a guild assignment is present, if and only if a user assignment is present with the USE_AS_ELIGIBILITY flag, should the guild assignment be used as the assigned variant for the guild

If none of the above are satisfied, the guild does not have an assigned variant for the experiment. Additionally, if the final evaluated assignment ever possesses the USE_AS_ELIGIBILITY flag, it should be discarded and the resource should be considered ineligible for the experiment, as this flag is only meant to be used as an eligibility gate and should not be used as an actual assignment.

Apex Experiments Structure
FieldTypeDescription
assignmentsmap[integer, map[snowflake, apex experiment assignments object]]A mapping of unit type to unit IDs to their assignments
installation? 1stringA generated fingerprint of the current date and time

1 This field is omitted if a valid installation ID is provided in request headers or when identifying with the Gateway. See the installations section for more information.

Apex Experiment Unit Type
ValueNameDescription
1USERExperiment is for a user
2INSTALLATIONExperiment is for an installation
3GUILDExperiment is for a guild
4CUSTOMExperiment is for a custom unit type
Apex Experiment Assignments Structure
FieldTypeDescription
evaluation_idstringThe ID of the evaluation
assignmentsarray[apex experiment assignment object]The assignments for the apex experiment unit
Apex Experiment Assignment Structure

This object is represented as an array of the following fields:

FieldTypeDescription
hashed_nameinteger32-bit unsigned Murmur3 hash of the experiment's name
variant_idintegerThe assigned experiment variant for the target
flags?integerThe experiment's flags
revisionintegerCurrent version of the rollout
tracked_variant_id?integerThe variant that is currently being tracked in analytics
Apex Experiment Flags
ValueNameDescription
1 << 0IS_OVERRIDEExperiment assignment is an override
1 << 1EXPOSURE_TRACKING_ENABLEDExperiment has exposure tracking enabled
1 << 2DEPENDENT_EXPERIMENTExperiment is dependent on another experiment
1 << 3USE_AS_ELIGIBILITYExperiment assignment acts as an eligibility gate for guild experiments
Example Apex Experiments
{
"assignments": {
"1": {
"852892297661906993": {
"evaluation_id": "cd659a5d",
"assignments": [
[3759954850, 1, 0, 2],
[4240376458, 0, 0, 8],
[956087042, 1, 0, 2],
[1605213660, 0, 2, 0],
[3164674236, 1, 0, 4],
[3436651650, 1, 1, 0],
[1581149260, 0, 0, 3],
[13763179, 1, 2, 0],
[721000765, 1, 2, 0],
[2657741306, 0, 2, 3],
[341733491, 0, 2, 3],
[76075180, 0, 2, 3],
[2722069991, 0, 2, 3],
[196982166, 1, 0, 0],
[1228731397, 1, 2, 0],
[1132732657, 1, 2, 0]
]
}
}
}
}

Endpoints

Get Experiment Assignments

GET/experiments

Returns the user experiment assignments and optionally guild experiment rollouts for the requesting user or fingerprint.

Query String Parameters
FieldTypeDescription
with_guild_experiments?booleanWhether to include guild experiments in the returned data
platform? 1stringWhether to also include experiments for the given platform

1 Including this parameter requires valid authentication.

Experiment Platform
ValueDescription
DEVELOPER_PORTALThe developer portal
Response Body
FieldTypeDescription
fingerprint?stringA generated fingerprint of the current date and time
assignmentsarray[experiment assignment object]The experiment assignments for this user or fingerprint
guild_experiments?array[guild experiment object]Guild experiment rollouts for the client to assign

Create Fingerprint

POST/auth/fingerprint

Generates a new fingerprint.

Response Body
FieldTypeDescription
fingerprintstringThe generated fingerprint of the current date and time

Get Apex Experiment Assignments

GET/apex/experiments

Returns an apex experiments object for the requesting user and installation.

Query String Parameters
FieldTypeDescription
surfaceintegerThe surface to return apex experiments for (only APP and DEVELOPER_PORTAL is allowed)
Apex Experiment Surface
ValueNameDescription
1APIReturn apex experiments that alter API functionality
2APPReturn apex experiments that alter client functionality
3DEVELOPER_PORTALReturn apex experiments that alter developer portal functionality
4ADMIN_PANELReturn apex experiments that alter admin panel functionality
5ADS_BUDGET_ABReturn apex experiments that alter ads manager functionality

Get Metadata for Apex Experiments

GET/apex/experiments/metadata

Returns metadata for apex experiments.

Query String Parameters
FieldTypeDescription
surfaceintegerThe surface to return apex experiments for
Response Body
FieldTypeDescription
experimentsarray[apex experiment metadata object]The metadata for apex experiments
Apex Experiment Metadata Structure
FieldTypeDescription
idintegerThe ID of the apex experiment
namestringThe name of the apex experiment
titlestringThe title of the apex experiment
revisionintegerCurrent version of the rollout
unit_typeintegerThe unit type of the apex experiment
variantsarray[apex experiment variant object]The variants of the apex experiment
Apex Experiment Variant Structure
FieldTypeDescription
idintegerThe ID of the experiment variant
labelstringThe label of the variant
typeintegerThe type of the variant
Apex Experiment Variant Type
ValueNameDescription
1ACTIVEThe variant is active
2UNUSEDThe variant is currently unused
3BURNEDThe variant failed and is being reverted
4PRESERVEDThe variant is preserved