Skip to content

Commit

Permalink
Add preferences for analytics (#843)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvkb authored Mar 10, 2023
1 parent ed85afe commit 47343a8
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 79 deletions.
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
```
29 changes: 11 additions & 18 deletions frontend/feat/feature-flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,18 @@
"description": "Toggle the external sources in the header's search type switcher.",
"defaultState": "off"
},
"new_header": {
"analytics": {
"status": {
"staging": "enabled",
"production": "enabled"
"staging": "switchable",
"production": "disabled"
},
"description": "Toggle the new header, including recent searches."
},
"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"
"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

0 comments on commit 47343a8

Please sign in to comment.