Skip to content

Commit

Permalink
feat: feature flags implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gjedlicska committed Mar 26, 2024
1 parent 3d8b7f9 commit 0614deb
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 12 deletions.
88 changes: 88 additions & 0 deletions feature_flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Feature flags in the web stack

This document describes the recommended way to use of feature flags in our web stack.

## Flag management

At the moment for managing feature flags, we're relying on good old environment variables.
Currently we do not have a need to use an external flag managing service, but that might change in the future.

## Usage

The Speckle shared package exposes a `FeatureFlags` object, that contains all the available flags.

> ⚠️ Warning
> The feature flag mechanism doesn't work on the old frontend stack the same way.
> If you still need some of this functionality, the backend `serverInfo` graphql query is probably the best place to expose the value at.
### Backend, script usage

For any usecase that is not Nuxt based, the code below is the preferred way of using feature flags.

```typescript
import { Environment } from '@speckle/shared'

if (Environment.getFeatureFlags().ENABLE_AUTOMATE_MODULE)
console.log("Hurray I'm enabled")
```

### Frontend usage

For our Nuxt based frontend we hook into Nuxt's config module to expose the feature flags in both SSR and frontend context.
So using the feature flag is the same as using any nuxt public runtime config value.

```typescript
const config = useRuntimeConfig()

if (config.public.ENABLE_AUTOMATE_MODULE) console.log("Hurray I'm enabled")
```

## Definition

The `@speckle/shared` package is the place where the common implementation of the feature flags is declared.
To add a new feature flag, you need to modify the `featureFlagSchema` zod definition in `./src/environment/index.ts`.

> 📣 Important
>
> Always add a default value, most probably `false` to the flag.
> This will ensure that the feature you areKeep in mind, in order to support adding doesn't automatically roll out to all our deployments
Once the flag is added to the zod schema, the flag is ready to be used in our apps.
To enable the specific feature, please add an environment variable to the `.env` file of the component.

> Note
>
> Since `znv` uses 1-1 name matching from the environment variables, we prefer using `MACRO_CASE` names.
## Deployment

With the use of `znv` we are directly parsing all environment variables into feature flags. So in general, all feature flags are just environment variables. We need to supply them to the application runtimes where they are needed.

### Docker compose

This is less relevant nowadays, but practically it is a copy pasta exercise, each container definition declares env vars.

### Helm chart

Helm charts allow configurations via the chart `values.yaml` for this purpose (intentionally omitting secrets). The chart values file defines a `featureFlags` object, that should be extended with the newly added flag.

```yaml
## @section Feature flags
## @descriptionStart
## This object is a central location to define feature flags for the whole chart.
## @descriptionEnd
featureFlags:
## @param enableAutomateModule High level flag fully toggles the integrated automate module
enableAutomateModule: false
```
To expose the flag to specific deployments, we need to add the value into each deployment's env array like so
```yaml
# ...
env:
- name: ENABLE_AUTOMATE_MODULE
// prettier-ignore
value: {{ .Values.featureFlags.enableAutomateModule | quote }}
# ...
```
4 changes: 2 additions & 2 deletions packages/frontend-2/composables/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { useGlobalToast } from '~/lib/common/composables/toast'

export const useIsAutomateModuleEnabled = () => {
const {
public: { enableAutomateModule }
public: { ENABLE_AUTOMATE_MODULE }
} = useRuntimeConfig()

return ref(enableAutomateModule)
return ref(ENABLE_AUTOMATE_MODULE)
}

export { useGlobalToast, useActiveUser }
5 changes: 4 additions & 1 deletion packages/frontend-2/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { withoutLeadingSlash } from 'ufo'
import { sanitizeFilePath } from 'mlly'
import { filename } from 'pathe/utils'
import legacy from '@vitejs/plugin-legacy'
import { Environment } from '@speckle/shared'

// Copied out from nuxt vite-builder source to correctly build output chunk/entry/asset/etc file names
const buildOutputFileName = (chunkName: string) =>
Expand All @@ -16,6 +17,8 @@ const {
NUXT_PUBLIC_LOG_PRETTY = false
} = process.env

const featureFlags = Environment.getFeatureFlags()

const isLogPretty = ['1', 'true', true, 1].includes(NUXT_PUBLIC_LOG_PRETTY)

// https://v3.nuxtjs.org/api/configuration/nuxt.config
Expand Down Expand Up @@ -68,7 +71,7 @@ export default defineNuxtConfig({
datadogSite: '',
datadogService: '',
datadogEnv: '',
enableAutomateModule: false
...featureFlags
}
},

Expand Down
2 changes: 1 addition & 1 deletion packages/server/modules/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ exports.init = async (app) => {
require('./rest/diffUpload')(app)
require('./rest/diffDownload')(app)

// Register core-based scoeps
// Register core-based scopes
const scopes = require('./scopes.js')
for (const scope of scopes) {
await registerOrUpdateScope(scope)
Expand Down
5 changes: 0 additions & 5 deletions packages/server/modules/shared/helpers/envHelper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { MisconfiguredEnvironmentError } from '@/modules/shared/errors'
import { trimEnd } from 'lodash'
import { parseEnv } from 'znv'
import { z } from 'zod'

export function isTestEnv() {
return process.env.NODE_ENV === 'test'
Expand Down Expand Up @@ -274,6 +272,3 @@ export function delayGraphqlResponsesBy() {
if (!isDevEnv()) return 0
return getIntFromEnv('DELAY_GQL_RESPONSES_BY', '0')
}
export const { ENABLE_AUTOMATE_MODULE } = parseEnv(process.env, {
ENABLE_AUTOMATE_MODULE: z.boolean().default(false)
})
1 change: 0 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@
"undici": "^5.28.3",
"verror": "^1.10.1",
"xml-escape": "^1.1.0",
"znv": "^0.4.0",
"zod": "^3.22.4",
"zod-validation-error": "^1.5.0",
"zxcvbn": "^4.4.2"
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"dependencies": {
"lodash": "^4.17.0",
"lodash-es": "^4.17.21",
"type-fest": "^3.11.1"
"type-fest": "^3.11.1",
"znv": "^0.4.0",
"zod": "^3.22.4"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0-beta.176",
Expand Down
17 changes: 17 additions & 0 deletions packages/shared/src/environment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { parseEnv } from 'znv'
import { z } from 'zod'

const featureFlagSchema = z.object({
ENABLE_AUTOMATE_MODULE: z.boolean().default(false)
})

function parseFeatureFlags() {
return parseEnv(process.env, featureFlagSchema.shape)
}

let parsedFlags: ReturnType<typeof parseFeatureFlags> | undefined

export function getFeatureFlags() {
if (!parsedFlags) parsedFlags = parseFeatureFlags()
return parsedFlags
}
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * as RichTextEditor from './rich-text-editor'
export * as Observability from './observability'
export * as SpeckleViewer from './viewer'
export * as Environment from './environment'
export * from './core'
3 changes: 3 additions & 0 deletions utils/helm/speckle-server/templates/frontend_2/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ spec:
value: {{ .Values.analytics.datadog_env | quote }}
{{- end }}

- name: ENABLE_AUTOMATE_MODULE
value: {{ .Values.featureFlags.enableAutomateModule | quote }}

priorityClassName: high-priority
{{- if .Values.frontend_2.affinity }}
affinity: {{- include "speckle.renderTpl" (dict "value" .Values.frontend_2.affinity "context" $) | nindent 8 }}
Expand Down
3 changes: 3 additions & 0 deletions utils/helm/speckle-server/templates/server/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ spec:
- name: ENABLE_FE2_MESSAGING
value: {{ .Values.server.enableFe2Messaging | quote }}

- name: ENABLE_AUTOMATE_MODULE
value: {{ .Values.featureFlags.enableAutomateModule | quote }}

- name: ONBOARDING_STREAM_URL
value: {{ .Values.server.onboarding.stream_url }}
- name: ONBOARDING_STREAM_CACHE_BUST_NUMBER
Expand Down
10 changes: 10 additions & 0 deletions utils/helm/speckle-server/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@
"description": "The name of the ClusterIssuer kubernetes resource that provides the SSL Certificate",
"default": "letsencrypt-staging"
},
"featureFlags": {
"type": "object",
"properties": {
"enableAutomateModule": {
"type": "boolean",
"description": "High level flag fully toggles the integrated automate module",
"default": false
}
}
},
"analytics": {
"type": "object",
"properties": {
Expand Down
8 changes: 8 additions & 0 deletions utils/helm/speckle-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ tlsRejectUnauthorized: '1'
##
cert_manager_issuer: letsencrypt-staging

## @section Feature flags
## @descriptionStart
## This object is a central location to define feature flags for the whole chart.
## @descriptionEnd
featureFlags:
## @param featureFlags.enableAutomateModule High level flag fully toggles the integrated automate module
enableAutomateModule: false

analytics:
## @param analytics.enabled Enable or disable analytics
enabled: true
Expand Down
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14361,7 +14361,6 @@ __metadata:
ws: ^7.5.7
xml-escape: ^1.1.0
yargs: ^17.3.1
znv: ^0.4.0
zod: ^3.22.4
zod-validation-error: ^1.5.0
zxcvbn: ^4.4.2
Expand Down Expand Up @@ -14402,6 +14401,8 @@ __metadata:
rollup-plugin-typescript2: ^0.34.1
type-fest: ^3.11.1
typescript: ^4.5.4
znv: ^0.4.0
zod: ^3.22.4
peerDependencies:
"@tiptap/core": ^2.0.0-beta.176
pino: ^8.7.0
Expand Down

0 comments on commit 0614deb

Please sign in to comment.