Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preferences for analytics #843

Merged
merged 8 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ node_modules/
npm-debug.log*
.pnpm-debug.log

# Nuxt
.nuxt

# Local certificates
*.pem
*.crt
Expand Down
119 changes: 119 additions & 0 deletions documentation/reference/frontend/feature_flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Feature flags

Feature flags control how the app works in different environments and for
different users.

## Concepts

### Status

The status of a feature can be one of three values:

- `enabled`
- `disabled`
- `switchable`

This is configurable and must be set in the feature flags configuration file,
`~/feat/feature-flags.json`.

A `switchable` flag in a public environment like 'staging' or 'production'
effectively becomes a user-level preferences toggle.

### State

The state of a feature can be one of two values:

- `on`
- `off`

This is determined by the [feature status](#status) and user-preferences.

- An `enabled` feature is always `on`.
- A `disabled` feature is always `off`.
- A switchable feature can be `on` or `off` based on the user's preference. This
`preferredState` is recorded in a cookie. If no preference has been set, the
[`defaultState`](#defaultstate) will be used.

### Cascade

The current environment can be 'local', 'development', 'staging' or 'production'
based on the `DEPLOYMENT_ENV` environment variable (assumed 'local' by default).

If a feature status is set for an environment, it is assumed to be same value
for all environments before it and `disabled` for all environments after it
(unless specified otherwise). For example, consider the following configuration:

```json
{
"local": "enabled",
"staging": "switchable"
}
```

Here the feature will be

- `enabled` for 'local' environment (configured)
- `switchable` for 'development' environment (cascaded from staging)
- `switchable` for 'staging' environment (configured)
- `disabled` for 'production' environment (default)

## File structure

The feature flags configuration file, `~/feat/feature-flags.json` contains two
top-level keys, `features` and `groups`.

### `features`

This is a mapping of feature names to feature configurations.

#### Key: title

This is an identifier for the feature. This identifier is looked up in the i18n
strings to generate a human-readable purpose (`pref-page.features.${title}`) for
the feature.

Conventionally, `_` is used as the separator for the name so that the name is a
valid JS identifier.

#### Value: configuration

This controls the behavior of the feature flag. It is an object containing three
fields, `status`, `description` and `defaultState`.

##### `status`

It is a mapping of different environments with the status of the flag in that
environment. For example, a feature that's in development may be 'switchable' in
'staging' but 'disabled' in 'production'.

##### `defaultState`

This is the state of a switchable feature when the user has not switched it. For
example, a feature that's in development may start as 'opt-in', being 'off' by
default and may gradually change to opt-out, becoming 'on' by default.

##### `description`

This is a description of the feature that appears in the internal `/preferences`
page.

⚠️ This field is not internationalised because it's for internal use only. You
should [populate `en.json5`](#key-title) if the feature is switchable and
visible to the user.

### `groups`

This setting pertains to how feature flags can serve as user-level preferences.
It is an array of objects, each containing two fields, `title` and `features`.

#### `title`

This is an identifier for the group. This identifier is looked up in the i18n
strings to generate a human-readable name (`pref-page.groups.${title}.title`)
and description (`pref-page.groups.${title}.desc`) for the group.

#### `features`

This is a list of strings where each string must be a
[key from the upper `features` section](#key-title). Each switchable feature
from this list will show up in the preferences modal.
7 changes: 7 additions & 0 deletions documentation/reference/frontend/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Frontend

```{toctree}
:maxdepth: 1

feature_flags
```
1 change: 1 addition & 0 deletions documentation/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
github_contribution_practices
dev_flow
api/index
frontend/index
search_algorithm
```
26 changes: 13 additions & 13 deletions frontend/feat/feature-flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
},
"description": "Toggle the new header, including recent searches."
dhruvkb marked this conversation as resolved.
Show resolved Hide resolved
},
"feat_disabled": {
"status": "disabled",
"description": "Will always be disabled"
},
"feat_enabled": {
"status": "enabled",
"description": "Will always be enabled"
},
"feat_switchable": {
"status": "switchable",
"description": "Can be switched between on and off",
"defaultState": "on"
"analytics": {
"status": {
"staging": "switchable",
"production": "disabled"
},
"description": "Record custom events and page views."
}
},
"groups": [
{
"title": "analytics",
"features": ["analytics"]
}
}
]
}
74 changes: 56 additions & 18 deletions frontend/src/components/VCheckbox/VCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,55 @@
class="relative flex text-sm leading-5"
:class="labelClasses"
>
<!--
The checkbox focus style is a slight variation on the `focus-slim-tx` style.
Because it becomes filled when checked, it also needs the
`checked:focus-visible:border-white` class.
-->
<input
:id="id"
type="checkbox"
class="checkbox h-5 w-5 flex-shrink-0 appearance-none rounded-sm border border-dark-charcoal bg-white me-3 focus-slim-tx checked:bg-dark-charcoal checked:focus-visible:border-white disabled:border-dark-charcoal-40 disabled:bg-dark-charcoal-10 checked:disabled:border-dark-charcoal-40 checked:disabled:bg-dark-charcoal-40"
v-bind="inputAttrs"
@change="onChange"
/>
<VIcon
v-show="localCheckedState"
class="absolute transform text-white"
:icon-path="checkIcon"
:size="5"
/>
<div class="relative">
<!--
The checkbox focus style is a slight variation on the `focus-slim-tx` style.
Because it becomes filled when checked, it also needs the
`checked:focus-visible:border-white` class.
-->
<input
:id="id"
type="checkbox"
class="block appearance-none border border-dark-charcoal bg-white transition-colors duration-100 me-3 checked:bg-dark-charcoal disabled:border-dark-charcoal-40 disabled:bg-dark-charcoal-10 checked:disabled:border-dark-charcoal-40 checked:disabled:bg-dark-charcoal-40"
:class="
isSwitch
? ['h-4.5', 'w-9', 'rounded-full', 'focus-slim-offset']
: [
'h-5',
'w-5',
'rounded-sm',
'focus-slim-tx',
'checked:focus-visible:border-white',
]
"
v-bind="inputAttrs"
@click="onChange"
/>

<!-- Knob, for when `ifSwitch` is `true` -->
<div
v-if="isSwitch"
class="absolute top-0.75 left-0.75 h-3 w-3 rounded-full transition-transform duration-100"
:class="
localCheckedState
? ['bg-white', 'translate-x-[1.125rem]']
: disabled
? ['bg-dark-charcoal-40']
: ['bg-dark-charcoal']
"
aria-hidden="true"
/>

<!-- Checkmark, for when `ifSwitch` is `false` -->
<VIcon
v-else
v-show="localCheckedState"
class="absolute inset-0 transform text-white"
:icon-path="checkIcon"
:size="5"
/>
</div>

<!-- @slot The checkbox label --><slot />
</label>
</template>
Expand Down Expand Up @@ -103,6 +134,13 @@ export default defineComponent({
type: Boolean,
default: false,
},
/**
* whether to make the checkbox appear like a switch
*/
isSwitch: {
type: Boolean,
default: false,
},
},
emits: {
change: defineEvent<[Omit<CheckboxAttrs, "disabled">]>(),
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/components/VCheckbox/meta/VCheckbox.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const Template = (args) => ({

<Description of={VCheckbox} />

<ArgsTable of={VCheckbox} />

## Default checkbox

<Canvas>
Expand All @@ -58,7 +60,25 @@ export const Template = (args) => ({
</Story>
</Canvas>

<ArgsTable of={VCheckbox} />
## Switch

The checkbox can alternatively be rendered as a switch, when the choice is
between an on/off decision.

<Canvas>
<Story
name="Switch"
args={{
id: "default",
name: "storybook",
value: "codeIsPoetry",
isSwitch: true,
}}
argTypes={checkboxArgTypes}
>
{Template.bind({})}
</Story>
</Canvas>

export const LicenseCheckboxTemplate = (args) => ({
template: `<fieldset>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/locales/scripts/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,15 @@
"skip-to-content": "Skip to content",
"pref-page": {
title: "Preferences",
groups: {
analytics: {
title: "Analytics",
desc: "Participate in anonymous analytics to improve user experience.",
},
},
features: {
analytics: "Record custom events and page views.",
},
"non-switchable": {
title: "Non-switchable features",
desc: "You cannot modify the status of these features.",
Expand Down
Loading