Feature Control Trimming
Table of Contents
- Overview
- Basic Principle
- Runtime Flow
- Trim Conditions
- Static Route Key Generation
- Development Workflow
- Build and Deployment Workflow
- Troubleshooting
Overview
Feature Control Trimming reduces the feature-control payload used by SSR pages. Instead of keeping the full feature-control map returned by the backend, SSR can keep only the feature-control keys that the current route is known to use.
The goal is to reduce the amount of feature-control data serialized into the page and sent to the client, while keeping runtime behavior correct for routes whose feature-control usage can be proven statically.
The implementation has three parts:
- A static analyzer that generates one
feature-control-route-keys.jsonfile per service. - SSR normalization logic in
getFeatureControl.tsthat uses the generated JSON to trim feature-control data. - Build and CI checks that keep generated JSON committed, fresh, and available in production server output.
Basic Principle
Each service owns a generated file:
packages/<domain>/app-desktop/feature-control-route-keys.json
packages/<domain>/app-mobile/feature-control-route-keys.json
The file maps a Next.js route to the feature-control keys used by that route:
{
"service": "webfpr-desktop",
"routes": {
"/flight": ["flight-homepage-banner", "flight-search-form"]
},
"routeRegexes": {
"/flight": ["^flight-campaign-.+$"]
},
"untrimmableRoutes": {}
}
During SSR, getFeatureControl fetches the full feature-control map as usual. Before returning the normalized map, it checks the current route from ctx.asPath. If the route has generated static keys and trimming is enabled for the current service, keys that are not listed for that route are removed. Each route entry directly contains the complete exact key set needed by that route.
Regex entries are used for cases where the analyzer can prove the static shape of a feature-control key but cannot enumerate every possible concrete value, such as a static prefix with a dynamic suffix.
Runtime Flow
The runtime flow is:
getFeatureControlMap(ctx)fetches feature-control data from the backend or fallback.normalizeFeatures(features, ctx)normalizesenabledvalues and checks whether route-level trimming can run.- Runtime service name is read from public runtime config
appName. - Route-key JSON is read from one of the supported file locations.
- The current route is matched with
ctx.asPath. - If the route is trimmable, exact route keys and regex-matched keys are kept.
- Metadata keys are attached to the trimmed result so client-side
FeatureControlcan suppress warnings for keys known to belong to the current route static set.
Route-key JSON lookup order:
.next/server/feature-control-route-keys.jsonunder the current working directory.feature-control-route-keys.jsonunder the current working directory.
The .next/server location is checked first because production next build emits the JSON beside the server bundle. The app-root file is kept as a fallback for local development, where the committed generated JSON lives beside the service source files.
Trim Conditions
Feature Control Trimming is intentionally opt-in and guarded. A request is trimmed only when all of these conditions are true:
-
The code is running in SSR.
ctx.reqmust exist. Client-side normalization does not read route-key JSON. -
The current service can be resolved.
Runtime config
appNamemust be available and is normalized from@traveloka/webxxx-desktoptowebxxx-desktop. -
The
fc-trimfeature control enables trimming for the current service.The feature-control response must contain:
{"fc-trim": {"enabled": true,"properties": {"service": ["webfpr-desktop"]}}}enabledcan be booleantrueor string"true". Theproperties.servicevalue must be a list and must include the current service. -
The route is allowed by
featureControlTrimScope.The scope file is
packages/core/feature-control/featureControlTrimScope.ts.Its key is service name and its value is the route list allowed to trim. If a service is not present in this map, no routes for that service can trim. If a service is present, only the listed routes can trim.
-
A route-key JSON file exists and matches the current service.
If the JSON file has a
servicefield and it does not match the current runtime service, it is ignored. -
The current
ctx.asPathhas static keys or static key regexes.Route matching is exact against
ctx.asPath. -
The current route is not marked as untrimmable.
Routes with unresolved dynamic feature-control usage are written into
untrimmableRoutes. Those routes keep the full feature-control map.
If any condition fails, normalizeFeatures falls back to the full normalized feature-control map.
Static Route Key Generation
The generator command is:
pnpm generate-feature-control-route-keys-json
This scans all webxxx-desktop and webxxx-mobile services with a pages directory.
Global generation post-processes analyzer output by filtering each service-owned feature-control-route-keys.json to the routes listed in featureControlTrimScope.ts. Exact keys that appear on many or all routes stay inline in every route that needs them.
To generate for one service:
pnpm generate-feature-control-route-keys-json webfpr-desktop
The command runs ci-utils/feature-control-route-analyzer/index.js with --all-routes. For each service, the analyzer:
- Collects Page Router routes from
pages. - Resolves each route page and
pages/_app. - Builds the reachable source graph from static imports, dynamic imports, and local
requirecalls. - Detects feature-control usage from hooks, direct feature-control object reads, wrappers, JSX component props, map wrappers, and computed access helpers.
- Writes runtime route keys to
feature-control-route-keys.json. - Writes dynamic usage diagnostics under
tmp/feature-control-route-analyzer/<service>.
The analyzer is conservative. When it cannot prove the accessed subset of feature-control keys, it records dynamic usage. Dynamic usage means the route should not be trimmed until the usage is made analyzable or the analyzer is enhanced.
Examples of supported static usage include:
useFeatureControl('static-key');
useFeatureControl(FEATURE_KEY);
useFeatureControl(FeatureKeyMap.search);
featureControl.get('static-key');
featureControl.staticKey;
featureControl['static-key'];
The analyzer also handles common wrapper patterns:
function useExperimentKey(key: string) {
return useFeatureControl(key);
}
useExperimentKey('checkout-experiment');
And component prop forwarding:
function FeatureGate({ fcKey }: { fcKey: string }) {
useFeatureControl(fcKey);
return null;
}
<FeatureGate fcKey="checkout-experiment" />;
Examples of dynamic usage include:
useFeatureControl(featureKeyFromRuntime);
Object.keys(featureControl);
const copy = { ...featureControl };
for (const key in featureControl) {
// ...
}
Development Workflow
When changing feature-control usage in a service:
-
If the change involves feature controls in head-api, build head-api before running the generator.
pnpm --filter @traveloka/core build:headAPI -
Run the generator for the affected service.
pnpm generate-feature-control-route-keys-json webfpr-desktop -
Review the generated JSON diff and commit the updated service-owned
feature-control-route-keys.json. -
Inspect dynamic usage output if the analyzer exits with a non-zero status.
Dynamic usage output is written under:
tmp/feature-control-route-analyzer/<service>/<service>.feature-control-dynamic-usages.json -
If a route has legitimate dynamic feature-control usage, it will be marked untrimmable and will keep the full feature-control map.
-
To enable trimming for a service, add the service to
featureControlTrimScope.tsand list only the routes that should be allowed to trim. -
Enable the
fc-trimfeature control for the target service through feature-control configuration.
Use useDeferredFeatureControl when a feature-control key is required by a client-only or dynamically rendered subtree but needs to be marked as used by the route. This prevents the key from being absent after SSR trimming.
Build and Deployment Workflow
CI runs the feature-control route-key check before building a service.
The workflow does the following:
-
Checks that the service has a committed
feature-control-route-keys.json.If the file is missing, CI fails and asks the engineer to run:
pnpm generate-feature-control-route-keys-json <service> -
Skips analyzer execution only when the service package has no
pagesdirectory. -
Runs the analyzer into a temporary directory:
tmp/feature-control-route-analyzer/<service> -
Compares the generated runtime JSON with the committed service JSON.
If they differ, CI fails and asks the engineer to regenerate and commit the JSON locally.
-
Prints dynamic usage diagnostics.
If the analyzer exits non-zero because dynamic usage exists, CI reports that
ci-utils/feature-control-route-analyzerhas dynamic scenarios it cannot statically handle yet and prints the route/file/reason diagnostics.
Run the full generator when validating CI-wide route-key drift across every scoped service.
During next build, FeatureControlRouteKeysPlugin emits the service-owned JSON into the server build output as:
.next/server/feature-control-route-keys.json
This production copy step is required because production server runtime resolves files relative to the built server layout, not necessarily the service source directory.
Route-key generation is intentionally not fully automated as part of the build. The analyzer is still experimental, and engineers should review the generated JSON before it is committed. This keeps route-key changes visible and prevents silent analyzer failures, such as an empty generated key list, from removing required feature-control keys in production.
Troubleshooting
The route is not trimmed
Check these items:
- The request is SSR and has
ctx.req. fc-trim.enabledis true.fc-trim.properties.serviceincludes the current service name.- The service exists in
featureControlTrimScope.ts, and the route is explicitly listed there. feature-control-route-keys.jsonexists in the service folder or server output.- The JSON
servicefield matches the current runtime service. - The current
ctx.asPathexactly matches a route key in the JSON. - The route is not listed in
untrimmableRoutes.
CI says the route-key JSON is missing
Run:
pnpm generate-feature-control-route-keys-json <service>
Then commit the generated <service>/feature-control-route-keys.json.
CI says the committed route-key JSON is out of date
This means the committed local feature-control-route-keys.json differs from the JSON generated by GitHub CI for the same service.
The CI log prints a unified diff between:
- the committed service JSON
- the temporary JSON generated by GitHub CI under
tmp/feature-control-route-analyzer/<service>
In most cases, this is caused by recent changes in the PR itself, and merging or rebasing master is not required. GitHub CI already tests the PR against the latest base branch, so regenerate the service JSON from the current branch first.
To fix it:
-
Construct the affected service locally.
-
Regenerate the file for the affected service.
pnpm generate-feature-control-route-keys-json <service> -
Review the generated JSON diff and commit the updated service-owned
feature-control-route-keys.json.
Merge or rebase master only when the PR overlaps with recent shared-code updates. For example, if master updates a shared component to use a new feature-control key and the PR adds a route that uses that shared component, the local generator cannot see the new key until the branch includes the latest shared-code change.
CI reports dynamic feature-control usage
If CI reports dynamic feature-control usage for a route, the current code shape cannot be statically analyzed to know which feature-control keys the route may use.
Open the dynamic usage report under tmp/feature-control-route-analyzer/<service>. Each item includes:
- route
- source file
- line and column
- expression
- reason
- any resolved static keys or regexes
Contact web-infra-eng to check whether this is a limitation of the analyzer. You can also inspect the code that introduces dynamic feature-control usage and see whether the possible feature-control key values can be constrained into a static list, object map, enum, or another pattern the analyzer can understand.
Client-side code warns that a feature key is missing
If you see [Feature_Key_Missing] in the browser console or in Datadog Logs, some feature-control keys were used at runtime but were trimmed from the route payload.
Contact web-infra-eng to adjust the analyzer so it can understand the missing usage pattern. If the issue is severe, temporarily limit trimming by removing the affected route or service from featureControlTrimScope.ts, or by removing the service from fc-trim.properties.service in feature-control configuration.