Files
fluxer/fluxer_docs/src/content/docs/http-api/experiments.mdx
T

347 lines
23 KiB
Plaintext

---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Experiments
description: The experiment assignments envelope, the revalidation and polling contract, and the experiments this server defines.
---
import RouteHeader from '@/components/RouteHeader.astro';
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `voice_noise_suppression`, whose placement protocol [Voice](/voice/) defines, `message_hover_tracking`, which selects one of two client implementations of the message hover state, `message_keyboard_focus`, which selects one of two client implementations of keyboard navigation in the message list, `blocked_message_groups`, which selects how a client renders a revealed block of blocked messages, `guild_activity_log_presentation`, which selects one of two client renderings of the community activity log, `expression_info_card`, which selects what a client shows for an emoji or a sticker in a message, `guild_header_collapse`, which selects how a client draws the guild banner above the channel list, and `typing_indicator_rework`, which selects how a client sends and shows typing indicators.
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
## Experiment assignments object
One resolution of every defined experiment against one account. Every field is present on every response.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| poll_interval_seconds | integer | Seconds to wait before revalidating, from 60 through 86400 |
| poll_jitter_percent | integer | The largest random offset added to or subtracted from the wait, as a percentage of the wait, from 0 through 50 |
| assignments | [assignment map](#assignment-map-object) object | One entry for each experiment the server defines |
Every account on the instance receives the same `poll_interval_seconds` and `poll_jitter_percent`. One request refreshes every experiment.
## Assignment map object
One entry per experiment. The envelope reports this object even when it is empty.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| voice_noise_suppression? | [noise suppression assignment](#noise-suppression-assignment-object) object | The caller's noise suppression assignment |
| message_hover_tracking? | [message hover tracking assignment](#message-hover-tracking-assignment-object) object | The caller's message hover tracking assignment |
| message_keyboard_focus? | [message keyboard focus assignment](#message-keyboard-focus-assignment-object) object | The caller's message keyboard focus assignment |
| blocked_message_groups? | [blocked message groups assignment](#blocked-message-groups-assignment-object) object | The caller's blocked message groups assignment |
| guild_activity_log_presentation? | [guild activity log presentation assignment](#guild-activity-log-presentation-assignment-object) object | The caller's guild activity log presentation assignment |
| expression_info_card? | [expression info card assignment](#expression-info-card-assignment-object) object | The caller's expression info card assignment |
| guild_header_collapse? | [guild header collapse assignment](#guild-header-collapse-assignment-object) object | The caller's guild header collapse assignment |
| typing_indicator_rework? | [typing indicator rework assignment](#typing-indicator-rework-assignment-object) object | The caller's typing indicator rework assignment |
Ignore unknown experiments and treat a missing experiment as off.
This server version writes `voice_noise_suppression`, `message_hover_tracking`, `message_keyboard_focus`, `blocked_message_groups`, `guild_activity_log_presentation`, `expression_info_card`, `guild_header_collapse`, and `typing_indicator_rework` on every response, including while a rollout is disabled. The disabled value is the first [resolution outcome](#resolution-outcomes) below, which reports `enabled` false and the stored `config_version`, so a client that compares `config_version` with the value from its previous response can see that an operator saved the configuration, even while the rollout stays disabled, and needs no second request for it.
## Noise suppression backends
Noise suppression runs in the client, on the microphone track, before that track is published. Fluxer processes no audio for it. The registry is closed, and a backend outside it is not a value this API produces or accepts.
| Value | Description |
| --- | --- |
| none | No processing |
| standard | The browser or platform suppressor the client already has |
| gate | A noise gate keyed on input level |
| speex | The Speex preprocessor |
| rnnoise | The RNNoise recurrent model |
| gtcrn | The GTCRN model |
| deep_filter | The DeepFilterNet model |
A client MUST treat a backend absent from `enabled_backends` as unavailable, including one named by `backend` or by a [guild override](#noise-suppression-guild-override-object).
## Noise suppression assignment object
One resolution of the instance noise suppression rollout against one account. Every field is present whenever the key is written.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| backend | ?string | The [backend](#noise-suppression-backends) the caller applies, and null where the caller is not targeted |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
| guild_overrides | array[[guild override](#noise-suppression-guild-override-object) object] | Per-guild backend replacements that apply to the caller |
| enabled_backends | array[string] | The [backends](#noise-suppression-backends) the client MAY run |
| allow_user_override | boolean | Whether the account's own stored choice replaces `backend` |
| stereo_enabled | boolean | Whether the client publishes a stereo microphone track |
| suppression_strength | integer | Suppression strength from 0 through 100, which only a backend that reads it applies |
`config_version` identifies the configuration revision. It can change without changing the caller's assignment.
### Resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false, `enabled_backends` and `guild_overrides` are empty, and `allow_user_override` and `stereo_enabled` are false.
2. The operator has excluded the caller. `enabled` is true, and every other field is as in the first outcome.
3. The caller was not drawn. `enabled` is true, and `enabled_backends`, `guild_overrides`, `allow_user_override`, and `stereo_enabled` all hold their configured values.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. A caller that is drawn while `backend` is absent from `enabled_backends` is reported as not drawn, with `user_targeted` false and both `backend` and `source` null.
A client branches on `user_targeted` rather than on `enabled_backends`, because the third outcome keeps the array populated. A `guild_overrides` entry applies in its guild whether or not the caller was drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included.
## Noise suppression guild override object
One backend replacement scoped to one guild. While the caller is connected to a voice channel of that guild, the override's `backend` replaces the assignment's `backend`.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| guild_id | snowflake | The guild the replacement applies in |
| backend | string | The [backend](#noise-suppression-backends) to run in that guild |
An override naming a backend that is absent from `enabled_backends` is dropped before the response is written, so every entry is runnable.
## Message hover tracking assignment object
One resolution of the instance message hover tracking rollout against one account. Every field is present whenever the key is written.
The rollout selects which implementation of the message hover state a client runs. A drawn client resolves the hovered message from one shared pointer oracle and drives the message action bar from that state. A client that is not drawn keeps the per-row implementation it ships with. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Hover tracking resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Message keyboard focus assignment object
One resolution of the instance message keyboard focus rollout against one account. Every field is present whenever the key is written.
The rollout selects which implementation of keyboard navigation a client runs in the message list. A drawn client reaches the message list from the composer with one Tab, walks messages with the arrow keys through revealed blocked groups, and draws the focus ring inside each row. A client that is not drawn keeps the implementation it ships with. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Keyboard navigation resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Blocked message groups assignment object
One resolution of the instance blocked message groups rollout against one account. Every field is present whenever the key is written.
The rollout selects how a client renders a revealed block of blocked or suspected spam messages. A drawn client draws the block full width, spaces consecutive message groups inside it, and keys an unread divider apart from the group below it. A client that is not drawn keeps the rendering it ships with. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Blocked message groups resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Guild activity log presentation assignment object
One resolution of the instance guild activity log presentation rollout against one account. Every field is present whenever the key is written.
The rollout selects how a client renders the community activity log. A drawn client renders each entry as a summary sentence followed by its recorded changes. A client that is not drawn keeps the rendering it ships with. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Activity log presentation resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Expression info card assignment object
One resolution of the instance expression info card rollout against one account. Every field is present whenever the key is written.
The rollout selects what a client shows for an emoji or a sticker in a message. A drawn client opens an info card on click that names the expression, says where it comes from, and offers a row for the source community the reader can open. A client that is not drawn keeps the hover tooltip it ships with. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Expression info card resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Guild header collapse assignment object
One resolution of the instance guild header collapse rollout against one account. Every field is present whenever the key is written.
The rollout selects how a client draws the guild banner above the channel list. A drawn client reduces that banner to the height of the header as the channel list scrolls down and returns it to full height as the list scrolls back up. A client that is not drawn keeps the fixed banner height it ships with. A guild without a banner has the same header in either arm. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Guild header collapse resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Typing indicator rework assignment object
One resolution of the instance typing indicator rework rollout against one account. Every field is present whenever the key is written.
The rollout selects how a client sends and shows typing indicators. A drawn client sends one typing signal 1.5 seconds after a person starts typing, or at once when they resume 16 seconds or more after it scheduled the previous one, and then at most once every 8 seconds while they keep typing. Only a keystroke starts a signal, so none is sent more than 1.5 seconds after they stop. It treats a signal it receives as valid for 10 seconds from the moment it arrives, unless a further signal refreshes it, and it drops a person from the list once their message arrives. It names up to three people and collapses a larger group into one line, and it announces up to three names to screen readers even where the visible row has no room for them. A client that is not drawn keeps the typing indicators it ships with. Neither arm changes what the API returns, and no route reports which arm a client ran.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout is running on this instance |
| config_version | integer | The revision of the instance configuration this assignment was resolved from |
| user_targeted | boolean | Whether the caller is inside the rollout |
| source | ?string | Which rule targeted the caller, one of `user_rule` or `canary`, and null where the caller is not targeted |
### Typing indicator rework resolution outcomes
The outcomes below set `user_targeted` to false, and they differ in what else they report.
1. The rollout is off. `enabled` is false and `source` is null.
2. The operator has excluded the caller. `enabled` is true and `source` is null.
3. The caller was not drawn. `enabled` is true and `source` is null.
A caller is drawn either by the operator's allowlist, which sets `source` to `user_rule`, or by the sampled share of the account population, which sets `source` to `canary`. The blocklist is read before the allowlist, so an account named in both is not drawn.
`config_version` reports the stored revision in all outcomes, the rollout being off included. A client branches on `user_targeted` alone.
## Get experiment assignments
<RouteHeader method="GET" path="/v1/experiments" bot />
Resolves every defined experiment for the caller and returns an [experiment assignments](#experiment-assignments-object) object. The response is derived per account, so it is never shared between accounts.
### Response body
[Experiment assignments](#experiment-assignments-object) object.
### Response
| Status | Body | Condition |
| --- | --- | --- |
| 200 | response body | The envelope was resolved |
| 304 | empty | The request sent a matching `If-None-Match` |
### Request headers
| Field | Type | Description |
| --- | --- | --- |
| If-None-Match? | string | An `ETag` from an earlier response to this route |
A client sends the `ETag` it last received. Fluxer compares it against the tag of the envelope it just resolved and answers 304 with no body on a match. `*` matches any current tag. A weak comparison is used, so a `W/` prefix on either side does not defeat the match.
### Response headers
| Field | Type | Description |
| --- | --- | --- |
| ETag | string | A strong tag over the envelope body, sent on 200 and on 304 |
| Cache-Control | string | The literal value `private, no-cache` |
| Vary | string | The literal value `Authorization`, replaced by `Origin` where the [cross-origin policy](/http-api/#cross-origin-requests) echoed an allowed origin |
The `ETag` changes when the response changes. Treat it as an opaque value.
`ETag` is listed in `Access-Control-Expose-Headers` and `If-None-Match` in `Access-Control-Allow-Headers`, so a cross-origin client reads the tag and revalidates with it.
### Polling
A client reads this route once per session and then again every `poll_interval_seconds`, offset by a random amount up to `poll_jitter_percent` of that interval in either direction. It sends the last `ETag` on every request after the first. A client MUST NOT poll faster than the lower end of that jitter range.
Until the first successful response, use 300 seconds with 15 percent jitter.
### Rate limit
60 requests per 10 seconds for each authenticated user, on the `default` bucket.