From e96c0e4d09219ca41534f2ba5a53fa52df2fc609 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 2 Sep 2025 12:26:54 +0300 Subject: [PATCH] docs: document feature flags (#13388) --- .../configurations/medusa-config/page.mdx | 18 +- .../feature-flags/create/page.mdx | 135 +++++++ .../feature-flags/page.mdx | 196 +++++++++ .../integration-tests/api-routes/page.mdx | 25 +- .../scheduled-jobs/interval/page.mdx | 4 +- www/apps/book/generated/edit-dates.mjs | 8 +- www/apps/book/generated/sidebar.mjs | 21 + www/apps/book/public/llms-full.txt | 375 ++++++++++++++++-- www/apps/book/sidebar.mjs | 12 + 9 files changed, 727 insertions(+), 67 deletions(-) create mode 100644 www/apps/book/app/learn/debugging-and-testing/feature-flags/create/page.mdx create mode 100644 www/apps/book/app/learn/debugging-and-testing/feature-flags/page.mdx diff --git a/www/apps/book/app/learn/configurations/medusa-config/page.mdx b/www/apps/book/app/learn/configurations/medusa-config/page.mdx index c9d8d500c6..0a0deea2b4 100644 --- a/www/apps/book/app/learn/configurations/medusa-config/page.mdx +++ b/www/apps/book/app/learn/configurations/medusa-config/page.mdx @@ -1103,17 +1103,7 @@ module.exports = { The `featureFlags` configuration allows you to manage enabled beta features in the Medusa application. -Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. You can enable or disable these features using the `featureFlags` configuration. - -The `featureFlags`'s value is an object whose keys are the names of the feature flags, and their values a boolean indicating whether the feature flag is enabled. - - - -Only enable feature flags in testing or development environments. Enabling a feature flag may introduce breaking changes or unexpected behavior. - - - -You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags). +Learn more in the [Feature Flags](../../debugging-and-testing/feature-flags/page.mdx) chapter. ### Example @@ -1127,12 +1117,6 @@ module.exports = defineConfig({ }) ``` -After enabling a feature flag, make sure to run migrations, as the feature may introduce database changes: - -```bash -npx medusa db:migrate -``` - --- ## Custom Logger (`logger`) diff --git a/www/apps/book/app/learn/debugging-and-testing/feature-flags/create/page.mdx b/www/apps/book/app/learn/debugging-and-testing/feature-flags/create/page.mdx new file mode 100644 index 0000000000..8cf5816d14 --- /dev/null +++ b/www/apps/book/app/learn/debugging-and-testing/feature-flags/create/page.mdx @@ -0,0 +1,135 @@ +import { TypeList } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Create Custom Feature Flag`, +} + +# {metadata.title} + +In this chapter, you'll learn how to create a custom feature flag in Medusa. + +## Why Create a Custom Feature Flag? + +As explained in the [Feature Flags](../page.mdx) chapter, feature flags allow the Medusa team to ship new features that are still under development and testing, but not ready for production or wide use yet. + +You can also create custom feature flags for your Medusa project or plugin to control the availability of experimental or in-development features. Feature flags allow you to test features in staging, and only enable them in production when they are ready. + +--- + +## How to Create a Custom Feature Flag + +### 1. Define the Feature Flag + +To create a custom feature flag, you need to define it in a new file under the `src/feature-flags` directory of your Medusa project or plugin. The file must export a configuration object. + +For example, to define a feature flag that enables a blog feature, create the file `src/feature-flags/blog.ts` with the following content: + +export const featureFlagHighlights = [ + ["4", "key", "The unique identifier for the feature flag."], + ["5", "default_val", "Whether the feature flag is enabled by default."], + ["6", "env_key", "The environment variable key for the feature flag."], + ["7", "description", "A brief description of what the feature flag does."] +] + +```ts title="src/feature-flags/blog.ts" highlights={featureFlagHighlights} +import { FlagSettings } from "@medusajs/framework/feature-flags" + +const BlogFeatureFlag: FlagSettings = { + key: "blog_feature", + default_val: false, + env_key: "FF_BLOG_FEATURE", + description: "Enable blog features", +} + +export default BlogFeatureFlag +``` + +The feature flag configuration is an object having the following properties: + + + +### 2. Hide Features Behind the Flag + +Next, you can build the features you want to hide behind the feature flag. + +To build backend customizations around feature flags, you can either: + +- [Conditionally run code blocks](../page.mdx#conditionally-run-code-blocks) based on the feature flag status; +- [Conditionally load files](../page.mdx#conditionally-load-files) if the feature flag is not enabled. + +For client customizations, such as admin widgets, you can use the [Feature Flags API route](../page.mdx#feature-flags-api-route). + +### 3. Toggle Feature Flag + +To enable or disable your custom feature flag, you can either add it to your `medusa-config.ts` file: + +```ts title="medusa-config.ts" +import BlogFeatureFlag from "./src/feature-flags/blog" + +module.exports = defineConfig({ + // ... + featureFlags: { + [BlogFeatureFlag.key]: true, + }, +}) +``` + +Or set the environment variable for the feature flag: + +```shell +FF_BLOG_FEATURE=true +``` + +Afterwards, make sure to [run migrations](../../../fundamentals/data-models/write-migration/page.mdx#run-the-migration) if your feature flag requires database changes. + +If you're disabling a feature flag, make sure to [roll back any migrations](../../../fundamentals/data-models/write-migration/page.mdx#rollback-the-migration) that depend on it first. + +--- + +## Write Tests for Features Behind Flags + +If you're writing integration tests for features hidden behind feature flags, you can enable the feature flag's environment variable in your integration test using the `env` option. + +For example: + +```ts title="integration-tests/http/test.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" + +medusaIntegrationTestRunner({ + env: { + FF_BLOG_FEATURE: true, + }, + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) +``` + +Then, the Medusa application will load your customizations hidden behind the feature flag as part of the integration tests. \ No newline at end of file diff --git a/www/apps/book/app/learn/debugging-and-testing/feature-flags/page.mdx b/www/apps/book/app/learn/debugging-and-testing/feature-flags/page.mdx new file mode 100644 index 0000000000..c1b8fb5eb8 --- /dev/null +++ b/www/apps/book/app/learn/debugging-and-testing/feature-flags/page.mdx @@ -0,0 +1,196 @@ +export const metadata = { + title: `${pageNumber} Feature Flags`, +} + +# {metadata.title} + +In this chapter, you'll learn what feature flags are, what the available feature flags in Medusa are, and how to toggle them. + +## What are Feature Flags? + +Feature flags allow you to ship new features that are still under development and testing, but not ready for production or wide use yet. + +Medusa uses feature flags to ship new versions even when some features are not fully ready. This approach allows our team to continuously deliver updates and improvements while stabilizing new features. + +--- + +## Available Feature Flags in Medusa + +For a list of features hidden behind a feature flag in Medusa, check out the [feature-flags](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/feature-flags) directory in the `@medusajs/medusa` package. + +--- + +## Toggle Feature Flags + +There are multiple ways to enable or disable feature flags in Medusa: + +- In the `medusa-config.ts` file; +- Or using environment variables. + +### 1. Using the `medusa-config.ts` file + +The Medusa configurations in `medusa-config.ts` accept a `featureFlags` configuration to toggle feature flags. Its value is an object whose key is the feature flag key (defined in the feature flag's file), and the value is a boolean indicating whether the feature is enabled. + +For example, to enable the [Index Module](../../fundamentals/module-links/index-module/page.mdx)'s feature flag in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + featureFlags: { + "index_engine": true, + }, +}) +``` + +Make sure to [run migrations](../../fundamentals/data-models/write-migration/page.mdx#run-the-migration) after enabling a feature flag, in case it requires database changes. + +Before disabling a feature flag, make sure to [roll back the migrations](../../fundamentals/data-models/write-migration/page.mdx#rollback-the-migration) that depend on it. + +### 2. Using Environment Variables + +The feature flags can also be enabled or disabled using environment variables. This is useful to control the feature flag based on the environment. + +To toggle a feature flag using an environment variable, set an environment variable with its environment variable name (defined in the feature flag's file) and set its value to `true` or `false`. + +For example, to enable the [Index Module](../../fundamentals/module-links/index-module/page.mdx)'s feature flag using an environment variable: + +```shell +MEDUSA_FF_INDEX_ENGINE=true +``` + +Make sure to [run migrations](../../fundamentals/data-models/write-migration/page.mdx#run-the-migration) after enabling a feature flag, in case it requires database changes. + +Before disabling a feature flag, make sure to [roll back the migrations](../../fundamentals/data-models/write-migration/page.mdx#rollback-the-migration) that depend on it. + +--- + +## Check Feature Flag Status + +During development, you can check whether a feature flag is enabled. This is useful to add customizations that only apply when a specific feature is enabled. + +To build backend customizations around feature flags, you can either: + +- [Conditionally run code blocks](#conditionally-run-code-blocks) with the `FeatureFlag` utility; +- Or [conditionally load files](#conditionally-load-files) with the `defineFileConfig` utility. + +For client customizations, you can use the [Feature Flags API Route](#feature-flags-api-route). + +### Conditionally Run Code Blocks + +The `FeatureFlag` utility allows you to check whether a specific feature flag is enabled in your backend customizations, including scheduled jobs, subscribers, and workflow steps. + +For example, to enable access to a route only if a feature flag is enabled, you can use the `FeatureFlag` utility in a [middleware](../../fundamentals/api-routes/middlewares/page.mdx): + +export const middlewareHighlights = [ + ["11", "isFeatureEnabled", "Check if the Index Module flag is enabled."] +] + +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" +import { FeatureFlag } from "@medusajs/framework/utils" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: ["GET"], + middlewares: [ + async (req, res, next) => { + if (!FeatureFlag.isFeatureEnabled("index_engine")) { + return res.sendStatus(404) + } + next() + }, + ], + }, + ], +}) +``` + +The `FeatureFlag.isFeatureEnabled` method accepts the feature flag key as a parameter and returns a boolean indicating whether the feature is enabled or not. + +In the above example, you return a `404` response if a `GET` request is sent to the `/custom` route and the `index_engine` feature flag is disabled. + +### Conditionally Load Files + +You can also combine the `FeatureFlag` utility with the `defineFileConfig` utility that allows you to fully enable or disable loading a file based on a condition. + +For example, instead of adding a middleware, you can use the `defineFileConfig` utility to conditionally load an API route file only if a feature flag is enabled: + +export const apiRouteHighlights = [ + ["13", "defineFileConfig", "Define logic to conditionally load the API route file"], + ["14", "isFeatureEnabled", "Only load the file if the feature flag is enabled"] +] + +```ts title="src/api/routes/custom.ts" highlights={apiRouteHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +): Promise { + res.json({ + message: "Hello World", + }) +} + +defineFileConfig({ + isDisabled: () => !FeatureFlag.isFeatureEnabled("index_engine"), +}) +``` + +The `defineFileConfig` function accepts an object with an `isDisabled` property. Its value is a function that returns a boolean indicating whether the file should be disabled. + +In the above example, the `GET` API route at `/custom` will only be available if the `index_engine` feature flag is enabled. + + + +While both approaches can be used to disable API routes, a middleware is useful to disable specific HTTP methods at a route. For example, if a route file has `GET` and `POST` route handlers, the `defineFileConfig` approach will disable both `GET` and `POST` methods if the feature flag is disabled. If you need to disable only the `POST` route, you should use a middleware instead. + + + +### Feature Flags API Route + +For client customizations, you can use the [List Feature Flags API Route](!api!/admin#feature-flags_getfeatureflags) to check the status of feature flags. + +For example, you can show an [admin widget](../../fundamentals/admin/widgets/page.mdx) only if a feature flag is enabled: + +export const widgetHighlights = [ + ["6", "featureFlags", "Retrieve feature flag statuses from the backend."], + ["13", "", "Hide widget if feature flag is disabled"] +] + +```tsx title="src/admin/widgets/product-details.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { sdk } from "../lib/sdk" +import { useQuery } from "@tanstack/react-query" + +const ProductWidget = () => { + const { data: featureFlags } = useQuery({ + queryKey: ["featureFlags"], + queryFn: () => sdk.client.fetch<{ + feature_flags: Record + }>(`/admin/feature-flags`), + }) + + if (!featureFlags?.feature_flags.index_engine) { + return null + } + + return ( +
+

Product Widget

+

Index engine feature is enabled

+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.after", +}) + +export default ProductWidget +``` + +In the above example, the Product Widget will only be displayed if the `index_engine` feature flag is enabled. It retrieves the feature flag statuses from the `/admin/feature-flags` API route. diff --git a/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx b/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx index 314ad7b296..7546040723 100644 --- a/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx +++ b/www/apps/book/app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx @@ -631,4 +631,27 @@ const response = await api.post(`/custom`, form, { ...authHeaders, }, }) -``` \ No newline at end of file +``` + +--- + +## Write Tests for Feature-Flagged API Routes + +If your API route is hidden behind a [feature flag], you can use the `env` option of `medusaIntegrationTestRunner` its feature flag. + +For example: + +```ts title="integration-tests/http/custom-routes.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" + +medusaIntegrationTestRunner({ + env: { + FF_BLOG_FEATURE: true, + }, + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) +``` + +The `env` option accepts an object of environment variables to set for the test suite. \ No newline at end of file diff --git a/www/apps/book/app/learn/fundamentals/scheduled-jobs/interval/page.mdx b/www/apps/book/app/learn/fundamentals/scheduled-jobs/interval/page.mdx index a0c7bd8e99..32f853779c 100644 --- a/www/apps/book/app/learn/fundamentals/scheduled-jobs/interval/page.mdx +++ b/www/apps/book/app/learn/fundamentals/scheduled-jobs/interval/page.mdx @@ -36,8 +36,8 @@ export default async function greetingJob(container: MedusaContainer) { export const config = { name: "greeting-every-minute", schedule: { - interval: 60000 // 1 minute - } + interval: 60000, // 1 minute + }, } ``` diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 1983055b6c..3a747779dd 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -50,7 +50,7 @@ export const generatedEditDates = { "app/learn/fundamentals/modules/isolation/page.mdx": "2025-05-21T15:10:15.499Z", "app/learn/fundamentals/data-models/index/page.mdx": "2025-03-18T07:59:07.798Z", "app/learn/fundamentals/custom-cli-scripts/page.mdx": "2025-07-25T15:32:47.587Z", - "app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2025-03-18T15:06:27.864Z", + "app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2025-09-02T08:36:12.714Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/page.mdx": "2024-12-09T15:52:01.019Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2025-07-30T13:43:44.636Z", "app/learn/debugging-and-testing/testing-tools/page.mdx": "2025-07-23T15:32:18.008Z", @@ -111,7 +111,7 @@ export const generatedEditDates = { "app/learn/resources/contribution-guidelines/admin-translations/page.mdx": "2025-02-11T16:57:46.726Z", "app/learn/resources/contribution-guidelines/docs/page.mdx": "2025-08-20T06:41:30.822Z", "app/learn/resources/usage/page.mdx": "2025-02-26T13:35:34.824Z", - "app/learn/configurations/medusa-config/page.mdx": "2025-08-28T15:35:36.344Z", + "app/learn/configurations/medusa-config/page.mdx": "2025-09-02T08:09:21.283Z", "app/learn/configurations/ts-aliases/page.mdx": "2025-07-23T15:32:18.008Z", "app/learn/production/worker-mode/page.mdx": "2025-07-18T15:19:45.352Z", "app/learn/fundamentals/module-links/read-only/page.mdx": "2025-08-15T11:52:13.403Z", @@ -129,5 +129,7 @@ export const generatedEditDates = { "app/learn/debugging-and-testing/debug-workflows/page.mdx": "2025-07-30T13:45:14.117Z", "app/learn/fundamentals/data-models/json-properties/page.mdx": "2025-07-31T14:25:01.268Z", "app/learn/debugging-and-testing/logging/custom-logger/page.mdx": "2025-08-28T15:37:07.328Z", - "app/learn/fundamentals/scheduled-jobs/interval/page.mdx": "2025-08-29T11:45:01.882Z" + "app/learn/fundamentals/scheduled-jobs/interval/page.mdx": "2025-09-02T08:36:12.714Z", + "app/learn/debugging-and-testing/feature-flags/create/page.mdx": "2025-09-02T08:36:12.714Z", + "app/learn/debugging-and-testing/feature-flags/page.mdx": "2025-09-02T08:36:12.714Z" } \ No newline at end of file diff --git a/www/apps/book/generated/sidebar.mjs b/www/apps/book/generated/sidebar.mjs index 45a86cfd28..100c6c9f6c 100644 --- a/www/apps/book/generated/sidebar.mjs +++ b/www/apps/book/generated/sidebar.mjs @@ -1233,6 +1233,27 @@ export const generatedSidebars = [ ], "chapterTitle": "7.6. Logging", "number": "7.6." + }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/debugging-and-testing/feature-flags", + "title": "Feature Flags", + "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/learn/debugging-and-testing/feature-flags/create", + "title": "Create Feature Flag", + "children": [], + "chapterTitle": "7.7.1. Create Feature Flag", + "number": "7.7.1." + } + ], + "chapterTitle": "7.7. Feature Flags", + "number": "7.7." } ], "chapterTitle": "7. Debugging & Testing", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 159d41a6d2..14f4f62cff 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -967,13 +967,7 @@ module.exports = { The `featureFlags` configuration allows you to manage enabled beta features in the Medusa application. -Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. You can enable or disable these features using the `featureFlags` configuration. - -The `featureFlags`'s value is an object whose keys are the names of the feature flags, and their values a boolean indicating whether the feature flag is enabled. - -Only enable feature flags in testing or development environments. Enabling a feature flag may introduce breaking changes or unexpected behavior. - -You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags). +Learn more in the [Feature Flags](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags/index.html.md) chapter. ### Example @@ -987,12 +981,6 @@ module.exports = defineConfig({ }) ``` -After enabling a feature flag, make sure to run migrations, as the feature may introduce database changes: - -```bash -npx medusa db:migrate -``` - *** ## Custom Logger (`logger`) @@ -3898,6 +3886,282 @@ Refer to the [Store Workflow Executions](https://docs.medusajs.com/learn/fundame You can view all executions of this workflow in the Medusa Admin under the [Workflows settings page](https://docs.medusajs.com/user-guide/settings/developer/workflows/index.html.md). Each execution will show you the status, input, and output data. +# Create Custom Feature Flag + +In this chapter, you'll learn how to create a custom feature flag in Medusa. + +## Why Create a Custom Feature Flag? + +As explained in the [Feature Flags](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags/index.html.md) chapter, feature flags allow the Medusa team to ship new features that are still under development and testing, but not ready for production or wide use yet. + +You can also create custom feature flags for your Medusa project or plugin to control the availability of experimental or in-development features. Feature flags allow you to test features in staging, and only enable them in production when they are ready. + +*** + +## How to Create a Custom Feature Flag + +### 1. Define the Feature Flag + +To create a custom feature flag, you need to define it in a new file under the `src/feature-flags` directory of your Medusa project or plugin. The file must export a configuration object. + +For example, to define a feature flag that enables a blog feature, create the file `src/feature-flags/blog.ts` with the following content: + +```ts title="src/feature-flags/blog.ts" highlights={featureFlagHighlights} +import { FlagSettings } from "@medusajs/framework/feature-flags" + +const BlogFeatureFlag: FlagSettings = { + key: "blog_feature", + default_val: false, + env_key: "FF_BLOG_FEATURE", + description: "Enable blog features", +} + +export default BlogFeatureFlag +``` + +The feature flag configuration is an object having the following properties: + +- key: (\`string\`) The unique identifier for the feature flag. This key is used to enable or disable the feature flag and check its status. +- default\_val: (\`boolean\`) Whether the feature flag is enabled by default. +- env\_key: (\`string\`) The environment variable key for the feature flag. This key is used to enable or disable the feature flag using environment variables. +- description: (\`string\`) A brief description of what the feature flag does. + +### 2. Hide Features Behind the Flag + +Next, you can build the features you want to hide behind the feature flag. + +To build backend customizations around feature flags, you can either: + +- [Conditionally run code blocks](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags#conditionally-run-code-blocks/index.html.md) based on the feature flag status; +- [Conditionally load files](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags#conditionally-load-files/index.html.md) if the feature flag is not enabled. + +For client customizations, such as admin widgets, you can use the [Feature Flags API route](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags#feature-flags-api-route/index.html.md). + +### 3. Toggle Feature Flag + +To enable or disable your custom feature flag, you can either add it to your `medusa-config.ts` file: + +```ts title="medusa-config.ts" +import BlogFeatureFlag from "./src/feature-flags/blog" + +module.exports = defineConfig({ + // ... + featureFlags: { + [BlogFeatureFlag.key]: true, + }, +}) +``` + +Or set the environment variable for the feature flag: + +```shell +FF_BLOG_FEATURE=true +``` + +Afterwards, make sure to [run migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#run-the-migration/index.html.md) if your feature flag requires database changes. + +If you're disabling a feature flag, make sure to [roll back any migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#rollback-the-migration/index.html.md) that depend on it first. + +*** + +## Write Tests for Features Behind Flags + +If you're writing integration tests for features hidden behind feature flags, you can enable the feature flag's environment variable in your integration test using the `env` option. + +For example: + +```ts title="integration-tests/http/test.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" + +medusaIntegrationTestRunner({ + env: { + FF_BLOG_FEATURE: true, + }, + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) +``` + +Then, the Medusa application will load your customizations hidden behind the feature flag as part of the integration tests. + + +# Feature Flags + +In this chapter, you'll learn what feature flags are, what the available feature flags in Medusa are, and how to toggle them. + +## What are Feature Flags? + +Feature flags allow you to ship new features that are still under development and testing, but not ready for production or wide use yet. + +Medusa uses feature flags to ship new versions even when some features are not fully ready. This approach allows our team to continuously deliver updates and improvements while stabilizing new features. + +*** + +## Available Feature Flags in Medusa + +For a list of features hidden behind a feature flag in Medusa, check out the [feature-flags](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/feature-flags) directory in the `@medusajs/medusa` package. + +*** + +## Toggle Feature Flags + +There are multiple ways to enable or disable feature flags in Medusa: + +- In the `medusa-config.ts` file; +- Or using environment variables. + +### 1. Using the `medusa-config.ts` file + +The Medusa configurations in `medusa-config.ts` accept a `featureFlags` configuration to toggle feature flags. Its value is an object whose key is the feature flag key (defined in the feature flag's file), and the value is a boolean indicating whether the feature is enabled. + +For example, to enable the [Index Module](https://docs.medusajs.com/learn/fundamentals/module-links/index-module/index.html.md)'s feature flag in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + featureFlags: { + "index_engine": true, + }, +}) +``` + +Make sure to [run migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#run-the-migration/index.html.md) after enabling a feature flag, in case it requires database changes. + +Before disabling a feature flag, make sure to [roll back the migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#rollback-the-migration/index.html.md) that depend on it. + +### 2. Using Environment Variables + +The feature flags can also be enabled or disabled using environment variables. This is useful to control the feature flag based on the environment. + +To toggle a feature flag using an environment variable, set an environment variable with its environment variable name (defined in the feature flag's file) and set its value to `true` or `false`. + +For example, to enable the [Index Module](https://docs.medusajs.com/learn/fundamentals/module-links/index-module/index.html.md)'s feature flag using an environment variable: + +```shell +MEDUSA_FF_INDEX_ENGINE=true +``` + +Make sure to [run migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#run-the-migration/index.html.md) after enabling a feature flag, in case it requires database changes. + +Before disabling a feature flag, make sure to [roll back the migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#rollback-the-migration/index.html.md) that depend on it. + +*** + +## Check Feature Flag Status + +During development, you can check whether a feature flag is enabled. This is useful to add customizations that only apply when a specific feature is enabled. + +To build backend customizations around feature flags, you can either: + +- [Conditionally run code blocks](#conditionally-run-code-blocks) with the `FeatureFlag` utility; +- Or [conditionally load files](#conditionally-load-files) with the `defineFileConfig` utility. + +For client customizations, you can use the [Feature Flags API Route](#feature-flags-api-route). + +### Conditionally Run Code Blocks + +The `FeatureFlag` utility allows you to check whether a specific feature flag is enabled in your backend customizations, including scheduled jobs, subscribers, and workflow steps. + +For example, to enable access to a route only if a feature flag is enabled, you can use the `FeatureFlag` utility in a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md): + +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" +import { FeatureFlag } from "@medusajs/framework/utils" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: ["GET"], + middlewares: [ + async (req, res, next) => { + if (!FeatureFlag.isFeatureEnabled("index_engine")) { + return res.sendStatus(404) + } + next() + }, + ], + }, + ], +}) +``` + +The `FeatureFlag.isFeatureEnabled` method accepts the feature flag key as a parameter and returns a boolean indicating whether the feature is enabled or not. + +In the above example, you return a `404` response if a `GET` request is sent to the `/custom` route and the `index_engine` feature flag is disabled. + +### Conditionally Load Files + +You can also combine the `FeatureFlag` utility with the `defineFileConfig` utility that allows you to fully enable or disable loading a file based on a condition. + +For example, instead of adding a middleware, you can use the `defineFileConfig` utility to conditionally load an API route file only if a feature flag is enabled: + +```ts title="src/api/routes/custom.ts" highlights={apiRouteHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +): Promise { + res.json({ + message: "Hello World", + }) +} + +defineFileConfig({ + isDisabled: () => !FeatureFlag.isFeatureEnabled("index_engine"), +}) +``` + +The `defineFileConfig` function accepts an object with an `isDisabled` property. Its value is a function that returns a boolean indicating whether the file should be disabled. + +In the above example, the `GET` API route at `/custom` will only be available if the `index_engine` feature flag is enabled. + +While both approaches can be used to disable API routes, a middleware is useful to disable specific HTTP methods at a route. For example, if a route file has `GET` and `POST` route handlers, the `defineFileConfig` approach will disable both `GET` and `POST` methods if the feature flag is disabled. If you need to disable only the `POST` route, you should use a middleware instead. + +### Feature Flags API Route + +For client customizations, you can use the [List Feature Flags API Route](https://docs.medusajs.com/api/admin#feature-flags_getfeatureflags) to check the status of feature flags. + +For example, you can show an [admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) only if a feature flag is enabled: + +```tsx title="src/admin/widgets/product-details.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { sdk } from "../lib/sdk" +import { useQuery } from "@tanstack/react-query" + +const ProductWidget = () => { + const { data: featureFlags } = useQuery({ + queryKey: ["featureFlags"], + queryFn: () => sdk.client.fetch<{ + feature_flags: Record + }>(`/admin/feature-flags`), + }) + + if (!featureFlags?.feature_flags.index_engine) { + return null + } + + return ( +
+

Product Widget

+

Index engine feature is enabled

+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.after", +}) + +export default ProductWidget +``` + +In the above example, the Product Widget will only be displayed if the `index_engine` feature flag is enabled. It retrieves the feature flag statuses from the `/admin/feature-flags` API route. + + # Configure Instrumentation In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. @@ -4861,6 +5125,29 @@ const response = await api.post(`/custom`, form, { }) ``` +*** + +## Write Tests for Feature-Flagged API Routes + +If your API route is hidden behind a \[feature flag], you can use the `env` option of `medusaIntegrationTestRunner` its feature flag. + +For example: + +```ts title="integration-tests/http/custom-routes.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" + +medusaIntegrationTestRunner({ + env: { + FF_BLOG_FEATURE: true, + }, + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) +``` + +The `env` option accepts an object of environment variables to set for the test suite. + # Write Integration Tests @@ -17913,8 +18200,8 @@ export default async function greetingJob(container: MedusaContainer) { export const config = { name: "greeting-every-minute", schedule: { - interval: 60000 // 1 minute - } + interval: 60000, // 1 minute + }, } ``` @@ -80529,9 +80816,9 @@ Now that you've created the collections, you need to add them to Payload's confi In `src/payload.config.ts`, add the following imports at the top of the file: ```ts title="src/payload.config.ts" badgeLabel="Storefront" badgeColor="blue" -import { Users } from './collections/Users' -import { Products } from './collections/Products' -import { Media } from './collections/Media' +import { Users } from "./collections/Users" +import { Products } from "./collections/Products" +import { Media } from "./collections/Media" ``` Then, add the collections to the `collections` array of the `buildConfig` function: @@ -82282,9 +82569,9 @@ The `updatePayloadItemsStep` will update an item in a Payload collection, such a To create the step, create the file `src/workflows/steps/update-payload-items.ts` with the following content: ```ts title="src/workflows/steps/update-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"; -import { PayloadItemResult, PayloadUpsertData } from "../../modules/payload/types"; -import { PAYLOAD_MODULE } from "../../modules/payload"; +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { PayloadItemResult, PayloadUpsertData } from "../../modules/payload/types" +import { PAYLOAD_MODULE } from "../../modules/payload" type StepInput = { collection: string; @@ -82295,20 +82582,20 @@ export const updatePayloadItemsStep = createStep( "update-payload-items", async ({ items, collection }: StepInput, { container }) => { const payloadModuleService = container.resolve(PAYLOAD_MODULE) - const ids: string[] = items.map(item => item.id); + const ids: string[] = items.map((item) => item.id) const prevData = await payloadModuleService.find(collection, { where: { id: { - in: ids.join(",") - } - } + in: ids.join(","), + }, + }, }) const updatedItems: PayloadItemResult[] = [] for (const item of items) { - const { id, ...data } = item; + const { id, ...data } = item updatedItems.push( await payloadModuleService.update( collection, @@ -82316,16 +82603,16 @@ export const updatePayloadItemsStep = createStep( { where: { id: { - equals: id - } - } + equals: id, + }, + }, } ) ) } return new StepResponse({ - items: updatedItems.map(item => item.doc) + items: updatedItems.map((item) => item.doc), }, { prevData, collection, @@ -82333,11 +82620,11 @@ export const updatePayloadItemsStep = createStep( }, async (data, { container }) => { if (!data) { - return; + return } - const { prevData, collection } = data; + const { prevData, collection } = data - const payloadModuleService = container.resolve(PAYLOAD_MODULE); + const payloadModuleService = container.resolve(PAYLOAD_MODULE) await Promise.all( prevData.docs.map(async ({ @@ -82350,9 +82637,9 @@ export const updatePayloadItemsStep = createStep( { where: { id: { - equals: id - } - } + equals: id, + }, + }, } ) }) @@ -94201,9 +94488,9 @@ The method returns an object, whose keys are of the format `{camel_case_data_mod For example, the returned object of the above example is: -```ts +```json { - "post_id": ["123"], + "post_id": ["123"] } ``` @@ -94228,12 +94515,12 @@ The method returns an object, whose keys are of the format `{camel_case_data_mod For example, the returned object of the above example is: -```ts +```json { "post_id": [ "123", - "321", - ], + "321" + ] } ``` @@ -94259,9 +94546,9 @@ The method returns an object, whose keys are of the format `{camel_case_data_mod For example, the returned object of the above example is: -```ts +```json { - "post_id": ["123"], + "post_id": ["123"] } ``` @@ -110574,7 +110861,7 @@ Follow the step-by-step [Digital Products Example](https://docs.medusajs.com/Use ## Overview -Digial products are products that are stored and deliveerd electronically. Examples include e-books, software, and digital art. +Digital products are products that are stored and delivered electronically. Examples include e-books, software, and digital art. When the customer buys a digital product, an email is sent to them where they can download the product. diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index d64ad8e4d1..906a7585ff 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -648,6 +648,18 @@ export const sidebars = [ }, ], }, + { + type: "link", + path: "/learn/debugging-and-testing/feature-flags", + title: "Feature Flags", + children: [ + { + type: "link", + path: "/learn/debugging-and-testing/feature-flags/create", + title: "Create Feature Flag", + }, + ], + }, ], }, {