- Overview
- Architecture
- Implementation Details
- Usage Patterns
- Reactivity & Change Detection
- Configuration
- Best Practices
- Error Handling & Fallbacks
- Real-World Examples
- Troubleshooting
- Related Documentation
Feature flags (also known as feature toggles) are a software development technique that allows you to enable or disable features at runtime without deploying new code. This provides powerful capabilities for:
- Progressive rollouts: Release features to specific users or groups
- A/B testing: Compare different implementations with real users
- Quick rollbacks: Disable problematic features instantly without redeployment
- Environment-specific features: Enable features in dev/staging but not production
- Dark launches: Deploy code to production but keep features hidden until ready
LFX One uses OpenFeature as a vendor-agnostic abstraction layer with LaunchDarkly as the feature flag provider:
- OpenFeature: Provides a standardized API for feature flags, preventing vendor lock-in
- LaunchDarkly: Enterprise-grade feature flag platform with real-time updates, targeting rules, and analytics
- Signal-based integration: Native Angular 20 signals provide automatic reactivity without manual change detection
- Zoneless Compatibility: Fully compatible with Angular 20's stable zoneless change detection
- Real-time Updates: Flag changes in LaunchDarkly propagate instantly to the UI without refresh
- Type Safety: TypeScript interfaces ensure compile-time safety for flag values
- SSR Support: Graceful handling of server-side rendering with browser-only initialization
- Developer Experience: Simple signal-based API requires no manual subscriptions or change detection
The feature flag system consists of three main components:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Startup β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. provideAppInitializer (provideFeatureFlags) β
β ββ> Initialize LaunchDarkly Provider β
β ββ> OpenFeature.setProviderAndWait() β
β β
β 2. app.component.ts Constructor β
β ββ> Get authenticated user from Auth0 β
β ββ> featureFlagService.initialize(user) β
β ββ> Set user context in OpenFeature β
β ββ> Get OpenFeature client β
β ββ> Set isInitialized = true β
β ββ> Setup event handlers β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FeatureFlagService β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Private State: β
β β’ client: OpenFeature Client β
β β’ isInitialized: WritableSignal<boolean> β
β β’ context: WritableSignal<EvaluationContext> β
β β
β Public API (all return Signal<T>): β
β β’ initialized: Signal<boolean> β
β β’ getBooleanFlag(key, default): Signal<boolean> β
β β’ getStringFlag(key, default): Signal<string> β
β β’ getNumberFlag(key, default): Signal<number> β
β β’ getObjectFlag(key, default): Signal<T> β
β β
β Event Handlers: β
β β’ Ready, ConfigurationChanged, ContextChanged β
β β’ All trigger refreshFlags() β context signal update β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Components β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Inject FeatureFlagService β
β 2. Call getBooleanFlag('flag-key', defaultValue) β
β 3. Receive Signal<boolean> β
β 4. Use in computed() or directly in template β
β 5. Automatic re-render on flag changes β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The service uses Angular signals for automatic reactivity:
- Context Signal: Private
contextsignal triggers re-evaluation when updated - Computed Flag Signals: Each
getXxxFlag()method returns a computed signal that:- Depends on the
contextsignal - Re-evaluates when context changes
- Automatically updates the UI without manual change detection
- Depends on the
- Event-Driven Updates: LaunchDarkly provider events trigger context refresh, causing all flag signals to re-evaluate
This design provides:
- Zero Subscriptions: No manual subscribe/unsubscribe management
- Automatic Cleanup: Signals are garbage collected with components
- Zoneless Compatible: Works perfectly with Angular 20's zoneless change detection
- Referential Stability: Same signal instance is returned across calls (use computed() for derived logic)
The FeatureFlagService is located at src/app/shared/services/feature-flag.service.ts:
@Injectable({
providedIn: 'root',
})
export class FeatureFlagService {
private client: Client | null = null;
private readonly isInitialized = signal<boolean>(false);
private readonly context = signal<EvaluationContext | null>(null);
// Public readonly signals
public readonly initialized = this.isInitialized.asReadonly();
/**
* Initialize OpenFeature client with user context
* Call this method from app.component when user is authenticated
*/
public async initialize(user: any): Promise<void> {
if (this.isInitialized()) {
return;
}
try {
const userContext: EvaluationContext = {
kind: 'user',
name: user.name || '',
email: user.email || '',
targetingKey: user.preferred_username || user.username || user.sub || '',
};
await OpenFeature.setContext(userContext);
this.client = OpenFeature.getClient();
this.context.set(userContext);
this.isInitialized.set(true);
this.setupEventHandlers();
} catch (error) {
console.error('Failed to initialize feature flag service:', error);
this.isInitialized.set(false);
}
}
}Key Design Decisions:
- Singleton Service:
providedIn: 'root'ensures single instance across application - Private State:
client,isInitialized, andcontextare private for encapsulation - Public Readonly Signals: Exposed signals use
asReadonly()to prevent external mutation - Lazy Initialization: Service doesn't initialize in constructor; waits for explicit
initialize()call - Idempotent: Multiple
initialize()calls are safe (checksisInitialized()first)
The feature flag system uses two providers working together:
runtime-config.provider.ts: Sets up runtime configuration via TransferStatefeature-flag.provider.ts: Initializes LaunchDarkly using the runtime config
Runtime Config Provider (src/app/shared/providers/runtime-config.provider.ts):
import { EnvironmentProviders, inject, makeStateKey, provideAppInitializer, REQUEST_CONTEXT, TransferState } from '@angular/core';
import { RuntimeConfig } from '@lfx-one/shared';
export const RUNTIME_CONFIG_KEY = makeStateKey<RuntimeConfig>('runtimeConfig');
export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
launchDarklyClientId: '',
dataDogRumClientId: '',
dataDogRumApplicationId: '',
};
async function initializeRuntimeConfig(): Promise<void> {
const transferState = inject(TransferState);
const reqContext = inject(REQUEST_CONTEXT, { optional: true }) as {
runtimeConfig: RuntimeConfig;
} | null;
// Server-side: Store config to TransferState for browser hydration
if (reqContext?.runtimeConfig) {
transferState.set(RUNTIME_CONFIG_KEY, reqContext.runtimeConfig);
}
}
export const provideRuntimeConfig = (): EnvironmentProviders => provideAppInitializer(initializeRuntimeConfig);
export function getRuntimeConfig(transferState: TransferState): RuntimeConfig {
return transferState.get(RUNTIME_CONFIG_KEY, DEFAULT_RUNTIME_CONFIG);
}Feature Flag Provider (src/app/shared/providers/feature-flag.provider.ts):
import { EnvironmentProviders, inject, provideAppInitializer, TransferState } from '@angular/core';
import { environment } from '@environments/environment';
import { LaunchDarklyClientProvider } from '@openfeature/launchdarkly-client-provider';
import { OpenFeature } from '@openfeature/web-sdk';
import { basicLogger } from 'launchdarkly-js-client-sdk';
import { getRuntimeConfig } from './runtime-config.provider';
async function initializeOpenFeature(): Promise<void> {
// Skip on server - LaunchDarkly is browser-only
if (typeof window === 'undefined') {
return;
}
const transferState = inject(TransferState);
const runtimeConfig = getRuntimeConfig(transferState);
const clientId = runtimeConfig.launchDarklyClientId;
// Skip if no client ID is configured
if (!clientId) {
console.warn('LaunchDarkly client ID not configured - feature flags disabled');
return;
}
try {
const provider = new LaunchDarklyClientProvider(clientId, {
initializationTimeout: 5,
streaming: true,
logger: basicLogger({ level: environment.production ? 'none' : 'info' }),
});
await OpenFeature.setProviderAndWait(provider);
} catch (error) {
console.error('Failed to initialize OpenFeature with LaunchDarkly:', error);
// App continues without feature flags
}
}
export const provideFeatureFlags = (): EnvironmentProviders => provideAppInitializer(initializeOpenFeature);App Config (src/app/app.config.ts):
import { provideRuntimeConfig } from './shared/providers/runtime-config.provider';
import { provideFeatureFlags } from './shared/providers/feature-flag.provider';
export const appConfig: ApplicationConfig = {
providers: [
// ...other providers
provideRuntimeConfig(), // Must be before providers that depend on runtime config
provideFeatureFlags(),
// ...other providers
],
};Provider Configuration:
- provideAppInitializer: Angular 20's modern API for app initialization, runs during application bootstrap before components render
- Runtime Config via TransferState: Client IDs are passed from server to browser via Angular's TransferState mechanism
- SSR Guard:
typeof window === 'undefined'check prevents server-side execution of LaunchDarkly - Graceful Degradation: Missing configuration or errors allow app to continue with default flag values
- Streaming Mode: Real-time flag updates from LaunchDarkly
- Environment-Aware Logging: Info level in development, none in production
For more details on the runtime configuration architecture, see Runtime Configuration.
The service sets up event handlers for real-time flag updates:
private setupEventHandlers(): void {
if (!this.client) {
return;
}
const forceSignalUpdate = () => {
// Force re-evaluation by updating context reference
this.refreshFlags();
};
// Set up event handlers for flag changes
this.client.addHandler(ProviderEvents.Ready, forceSignalUpdate);
this.client.addHandler(ProviderEvents.ConfigurationChanged, forceSignalUpdate);
this.client.addHandler(ProviderEvents.ContextChanged, forceSignalUpdate);
this.client.addHandler(ProviderEvents.Reconciling, forceSignalUpdate);
this.client.addHandler(ProviderEvents.Stale, forceSignalUpdate);
this.client.addHandler(ProviderEvents.Error, () => {
console.error('Feature flag provider error');
});
}
private refreshFlags(): void {
const current = this.context();
if (current) {
this.context.set({ ...current });
}
}Event Types:
- Ready: Provider is ready to evaluate flags
- ConfigurationChanged: Flag configuration updated in LaunchDarkly
- ContextChanged: User context changed (different user, attributes updated)
- Reconciling: Provider is syncing with LaunchDarkly
- Stale: Cached data is stale, reconciliation needed
- Error: Provider encountered an error
Update Mechanism:
The refreshFlags() method creates a new context reference ({ ...current }), which:
- Triggers Angular's signal change detection
- Causes all computed flag signals to re-evaluate
- Updates the UI automatically
- Maintains referential integrity for computed signals
The most common pattern is boolean flags for feature toggles:
export class MyComponent {
private readonly featureFlagService = inject(FeatureFlagService);
// Create a signal that tracks the flag value
protected readonly showNewFeature = this.featureFlagService.getBooleanFlag('new-feature', false);
}<!-- Use directly in template -->
@if (showNewFeature()) {
<div>New Feature Content</div>
}API:
getBooleanFlag(key: string, defaultValue: boolean = false): Signal<boolean>Parameters:
key: String identifier for the flag (matches LaunchDarkly flag key)defaultValue: Value returned when flag doesn't exist or service isn't initialized
Returns: A reactive Signal<boolean> that updates automatically
Use feature flags to show/hide UI elements with Angular's @if syntax:
Example from board-member-dashboard.component.ts:
export class BoardMemberDashboardComponent {
private readonly featureFlagService = inject(FeatureFlagService);
// Feature flag to control organization selector visibility
protected readonly showOrganizationSelector = this.featureFlagService.getBooleanFlag('organization-selector', true);
}Template (board-member-dashboard.component.html):
<!-- Organization Selector -->
@if (showOrganizationSelector()) {
<div class="mb-6 flex items-center gap-4" data-testid="organization-selector">
<label for="organization-select" class="text-sm font-semibold text-gray-700">Organization:</label>
<lfx-select
[form]="form"
control="selectedAccountId"
[options]="availableAccounts()"
optionLabel="accountName"
optionValue="accountId"
[filter]="true"
filterPlaceholder="Search organizations..."
placeholder="Select an organization"
[showClear]="false"
styleClass="min-w-[300px]"
inputId="organization-select"
data-testid="organization-select" />
</div>
}Pattern Benefits:
- Declarative: Template clearly shows conditional logic
- Performance: Angular only renders content when flag is true
- Type-Safe: TypeScript ensures signal invocation with
() - Reactive: Changes in LaunchDarkly instantly show/hide element
Use feature flags with computed() signals to dynamically filter arrays:
Example from main-layout.component.ts:
export class MainLayoutComponent {
private readonly featureFlagService = inject(FeatureFlagService);
// Feature flag for sidebar projects visibility
private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', true);
// Base sidebar navigation items
private readonly baseSidebarItems: SidebarMenuItem[] = [
{
label: 'Overview',
icon: 'fa-light fa-grid-2',
routerLink: '/',
},
{
label: 'Meetings',
icon: 'fa-light fa-calendar',
routerLink: '/meetings',
},
{
label: 'Projects',
icon: 'fa-light fa-folder-open',
routerLink: '/projects',
},
];
// Computed sidebar items based on feature flags
protected readonly sidebarItems = computed(() => {
const items = [...this.baseSidebarItems];
// Filter out Projects if feature flag is disabled
if (!this.showProjectsInSidebar()) {
return items.filter((item) => item.label !== 'Projects');
}
return items;
});
}Template (main-layout.component.html):
<!-- Sidebar with filtered items -->
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>Pattern Benefits:
- Separation of Concerns: Base data separate from filtering logic
- Composable: Multiple flags can filter different items independently
- Reactive: Computed signal automatically updates when any dependency changes
- Performance: Only recomputes when flag values change
Beyond booleans, the service supports string, number, and object flags:
// Get a string configuration value
protected readonly apiEndpoint = this.featureFlagService.getStringFlag('api-endpoint', 'https://api.example.com');API:
getStringFlag(key: string, defaultValue: string = ''): Signal<string>Use Cases: API endpoints, theme names, configuration strings
// Get a numeric configuration value
protected readonly maxResults = this.featureFlagService.getNumberFlag('max-results', 50);API:
getNumberFlag(key: string, defaultValue: number = 0): Signal<number>Use Cases: Pagination limits, timeout values, thresholds
// Get a complex configuration object
interface FeatureConfig {
enabled: boolean;
options: string[];
threshold: number;
}
protected readonly featureConfig = this.featureFlagService.getObjectFlag<FeatureConfig>('feature-config', {
enabled: false,
options: [],
threshold: 0,
});API:
getObjectFlag<T extends JsonValue = JsonValue>(key: string, defaultValue: T): Signal<T>Use Cases: Complex configurations, multi-option settings, nested configuration objects
Important: Object flags must be JSON-serializable (primitives, arrays, objects only - no functions or class instances)
The feature flag system uses Angular signals for automatic reactivity:
public getBooleanFlag(key: string, defaultValue: boolean = false): Signal<boolean> {
return computed(() => {
// Reactive dependency on context signal
this.context();
if (!this.isInitialized() || !this.client) {
return defaultValue;
}
try {
return this.client.getBooleanValue(key, defaultValue);
} catch (error) {
console.error(`Error evaluating boolean flag '${key}':`, error);
return defaultValue;
}
});
}How It Works:
- Computed Signal: Each flag method returns a
computed()signal - Context Dependency: The computed signal reads
this.context(), creating a reactive dependency - Automatic Tracking: Angular tracks this dependency and re-runs the computation when context changes
- UI Updates: Components using the flag signal automatically re-render when the flag value changes
No Manual Change Detection:
- No need to call
ChangeDetectorRef.markForCheck() - No need to run code in
NgZone - No need to use
asyncpipe - Works seamlessly with zoneless change detection
When flags change in LaunchDarkly:
1. LaunchDarkly emits ConfigurationChanged event
β
2. Event handler calls refreshFlags()
β
3. refreshFlags() updates context signal with new reference
β
4. All computed flag signals detect context change
β
5. Computed signals re-evaluate and get new flag values
β
6. Components using these signals automatically re-render
β
7. User sees updated UI instantly (no page refresh)
Example Timeline:
Time | Action | Result
------|-------------------------------------------|---------------------------
T+0 | Developer toggles 'new-feature' in LD | Event queued
T+50ms| ConfigurationChanged event received | refreshFlags() called
T+51ms| context signal updated | Flag signals re-evaluate
T+52ms| showNewFeature() returns new value | Component re-renders
T+53ms| Template @if condition evaluated | UI updated
Streaming Connection:
The LaunchDarkly provider uses Server-Sent Events (SSE) for real-time updates:
- Persistent Connection: Maintained in the background
- Low Latency: Updates typically arrive within 50-200ms
- Automatic Reconnection: Provider handles connection failures
- Graceful Degradation: Falls back to polling if SSE unavailable
Feature flags use runtime configuration injection instead of build-time environment files. This allows a single Docker image to be deployed to any environment with different client IDs.
Required Environment Variables:
| Variable | Description | Example |
|---|---|---|
LD_CLIENT_ID |
LaunchDarkly client-side ID | 691b727361cbf309e9d74468 |
DD_RUM_CLIENT_ID |
DataDog RUM client token (future) | pub123456789 |
DD_RUM_APPLICATION_ID |
DataDog RUM application ID (future) | app-uuid-here |
Local Development:
Add to your .env file (already gitignored):
# Runtime Client IDs for local development
LD_CLIENT_ID=your-launchdarkly-dev-client-id
DD_RUM_CLIENT_ID=your-datadog-rum-client-token
DD_RUM_APPLICATION_ID=your-datadog-rum-app-idThe server reads these via dotenv in development mode.
Docker Deployment:
Pass environment variables at container runtime:
docker run \
-e LD_CLIENT_ID=prod-client-id \
-e DD_RUM_CLIENT_ID=prod-rum-token \
-e DD_RUM_APPLICATION_ID=prod-rum-app-id \
ghcr.io/linuxfoundation/lfx-v2-ui:latestKubernetes Deployment:
Configure in your Kubernetes manifests or Helm values:
env:
- name: LD_CLIENT_ID
valueFrom:
secretKeyRef:
name: lfx-one-secrets
key: launchdarkly-client-idGetting Your Client ID:
- Log in to LaunchDarkly dashboard
- Navigate to Account Settings β Projects
- Select your project and environment (Development, Staging, Production)
- Copy the "Client-side ID" (not the SDK key)
- Set as
LD_CLIENT_IDenvironment variable
Important:
- Use different client IDs for each environment via environment variables
- Client-side IDs are safe to use in runtime configuration (they're public)
- Server-side SDK keys should never be in client code
- See Runtime Configuration for complete architecture details
The provider is configured in feature-flag.provider.ts:
const provider = new LaunchDarklyClientProvider(clientId, {
initializationTimeout: 5, // seconds
streaming: true,
logger: basicLogger({ level: environment.production ? 'none' : 'info' }),
});Note: clientId comes from runtime configuration via TransferState, not from environment files.
Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
initializationTimeout |
number | 5 | Max seconds to wait for initial flag fetch |
streaming |
boolean | true | Enable real-time updates via Server-Sent Events |
logger |
object | none | LaunchDarkly logger for debugging |
bootstrap |
object | - | Pre-populate flags (useful for SSR) |
sendEventsOnlyForVariation |
boolean | false | Reduce analytics events sent to LaunchDarkly |
Logger Levels:
'none': No logging (production)'error': Errors only'warn': Errors and warnings'info': Errors, warnings, and info (development)'debug': All messages including debug info
Production Recommendations:
// Production: Minimal logging, conservative timeout
{
initializationTimeout: 3,
streaming: true,
logger: basicLogger({ level: 'none' }),
sendEventsOnlyForVariation: true,
}
// Development: Verbose logging, longer timeout for debugging
{
initializationTimeout: 10,
streaming: true,
logger: basicLogger({ level: 'info' }),
}User context enables targeted flag delivery based on user attributes:
const userContext: EvaluationContext = {
kind: 'user',
name: user.name || '',
email: user.email || '',
targetingKey: user.preferred_username || user.username || user.sub || '',
};Context Structure:
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | Yes | Always 'user' for user contexts |
targetingKey |
string | Yes | Unique identifier for the user |
name |
string | No | Display name for LaunchDarkly UI |
email |
string | No | Email for targeting rules |
| Custom attributes | any | No | Additional attributes for targeting |
Targeting Use Cases:
- User-Specific Rollouts: Enable feature for specific users by email or ID
- Percentage Rollouts: Show feature to 10% of users randomly
- Segment Targeting: Enable for specific user groups (e.g., beta testers)
- A/B Testing: Show variant A to 50% of users, variant B to 50%
Adding Custom Attributes:
const userContext: EvaluationContext = {
kind: 'user',
targetingKey: user.sub,
name: user.name,
email: user.email,
// Custom attributes for targeting
organization: user.organization,
role: user.role,
plan: user.subscriptionPlan,
betaTester: user.isBetaTester,
};Custom attributes can then be used in LaunchDarkly targeting rules.
β DO: Initialize in app.component after authentication
export class AppComponent implements OnInit {
private readonly featureFlagService = inject(FeatureFlagService);
constructor() {
const user = /* get authenticated user */;
if (user) {
this.featureFlagService.initialize(user);
}
}
}β DON'T: Initialize in service constructor
// BAD: Service doesn't have access to user during construction
constructor() {
this.initialize(); // No user context available!
}Why: The service needs user context for targeting rules. User information is only available after authentication completes.
β DO: Inject service at component level
export class MyComponent {
private readonly featureFlagService = inject(FeatureFlagService);
protected readonly showFeature = this.featureFlagService.getBooleanFlag('my-feature', false);
}β DON'T: Create service instances manually
// BAD: Bypasses dependency injection
const service = new FeatureFlagService();Why: Angular's dependency injection ensures proper singleton behavior and lifecycle management.
β DO: Provide sensible defaults matching current behavior
// Default true maintains current behavior if flag doesn't exist
protected readonly showOrganizationSelector = this.featureFlagService.getBooleanFlag('organization-selector', true);β DON'T: Use false as default for existing features
// BAD: Existing feature will disappear if flag not found
protected readonly showExistingFeature = this.featureFlagService.getBooleanFlag('existing-feature', false);Why: Default values are used when:
- Flag doesn't exist in LaunchDarkly
- Service initialization fails
- Network is offline
- LaunchDarkly is unreachable
Choose defaults that maintain current application behavior to avoid breaking changes during outages.
β DO: Use computed() for derived flag logic
protected readonly sidebarItems = computed(() => {
const items = [...this.baseSidebarItems];
if (!this.showProjectsInSidebar()) {
return items.filter((item) => item.label !== 'Projects');
}
return items;
});β DON'T: Use effect() to update other signals
// BAD: Creates unnecessary complexity and potential infinite loops
effect(() => {
if (this.showProjectsInSidebar()) {
this.sidebarItems.set([...]); // Avoid!
}
});Why: computed() signals:
- Automatically track dependencies
- Only recompute when dependencies change
- Are easier to test and reason about
- Avoid potential infinite loops
β DO: Invoke signals in templates with ()
@if (showFeature()) {
<div>Content</div>
}β DON'T: Pass signals without invoking
<!-- BAD: Type error - Signal<boolean> is not assignable to boolean -->
@if (showFeature) {
<div>Content</div>
}Why: Signals are functions that return values. You must invoke them to get the current value.
β DO: Use inline string literals for flag keys
protected readonly showFeature = this.featureFlagService.getBooleanFlag('new-feature', false);β ALSO ACCEPTABLE: Centralize in constants for reuse
// shared/constants/feature-flags.ts
export const FEATURE_FLAGS = {
NEW_FEATURE: 'new-feature',
SIDEBAR_PROJECTS: 'sidebar-projects',
} as const;
// component
protected readonly showFeature = this.featureFlagService.getBooleanFlag(FEATURE_FLAGS.NEW_FEATURE, false);Benefits of constants:
- Autocomplete in IDE
- Refactoring support
- Single source of truth
- Type safety for flag keys
β DO: Create flag signals at component initialization
export class MyComponent {
// Created once during component construction
protected readonly showFeature = this.featureFlagService.getBooleanFlag('my-feature', false);
}β DON'T: Create flag signals in methods or templates
// BAD: Creates new signal on every method call
public get showFeature() {
return this.featureFlagService.getBooleanFlag('my-feature', false);
}Why: Creating signals repeatedly causes:
- Unnecessary memory allocation
- Loss of referential stability for computed signals
- Potential performance issues in templates
β DO: Handle initialization failures gracefully
async ngOnInit() {
try {
await this.featureFlagService.initialize(this.user);
} catch (error) {
// Log error but continue - default values will be used
console.error('Feature flag initialization failed:', error);
}
}β DO: Provide fallback UI for critical features
@if (isLoading()) {
<lfx-skeleton></lfx-skeleton>
} @else if (showNewDashboard()) {
<app-new-dashboard></app-new-dashboard>
} @else {
<app-legacy-dashboard></app-legacy-dashboard>
}Why: Graceful degradation ensures users can still use the application even when feature flags are unavailable.
The service handles initialization failures gracefully:
try {
const provider = new LaunchDarklyClientProvider(environment.launchDarklyClientId, {
initializationTimeout: 5,
streaming: true,
});
await OpenFeature.setProviderAndWait(provider);
} catch (error) {
console.error('Failed to initialize OpenFeature with LaunchDarkly:', error);
// App continues without feature flags
}Failure Scenarios:
- Missing Client ID: Console warning, flags disabled, defaults used
- Network Timeout: Provider initialization fails, defaults used
- Invalid Client ID: LaunchDarkly rejects connection, defaults used
- Server-Side Rendering: Initialization skipped (browser-only check)
Application Behavior:
- Application continues to function normally
- All feature flags return their default values
- No user-facing errors or broken UI
- Console logs provide debugging information
Individual flag evaluations are wrapped in try-catch:
try {
return this.client.getBooleanValue(key, defaultValue);
} catch (error) {
console.error(`Error evaluating boolean flag '${key}':`, error);
return defaultValue;
}Error Scenarios:
- Flag key doesn't exist in LaunchDarkly
- Type mismatch (requesting boolean for string flag)
- Evaluation context invalid
- Client disconnected
Fallback Strategy:
- Log error to console for debugging
- Return the provided default value
- Continue application execution
- No user-facing error messages
The LaunchDarkly provider handles network issues:
Offline Behavior:
- Uses cached flag values from previous session
- Falls back to defaults for new flags
- Automatically reconnects when network available
- Queues analytics events for later delivery
Reconnection Strategy:
1. Connection lost
β
2. Provider emits Stale event
β
3. Provider attempts reconnection with exponential backoff
β
4. On reconnection: Fetches latest flag values
β
5. Emits ConfigurationChanged event
β
6. UI updates with latest values
The system logs errors at appropriate levels:
Development:
// Info level logging for debugging
logger: basicLogger({ level: 'info' });Production:
// No logging to reduce noise
logger: basicLogger({ level: 'none' });Error Types:
| Severity | Message | When |
|---|---|---|
| WARN | "LaunchDarkly client ID not configured" | Missing environment variable |
| ERROR | "Failed to initialize OpenFeature" | Provider initialization fails |
| ERROR | "Failed to initialize feature flag service" | User context setup fails |
| ERROR | "Error evaluating [type] flag '[key]'" | Individual flag error |
| ERROR | "Feature flag provider error" | Provider runtime error |
Show or hide the organization selector in the board member dashboard:
Component (board-member-dashboard.component.ts):
import { Component, inject, Signal, computed } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FeatureFlagService } from '../../../shared/services/feature-flag.service';
import { Account } from '@lfx-one/shared/interfaces';
export class BoardMemberDashboardComponent {
private readonly accountContextService = inject(AccountContextService);
private readonly featureFlagService = inject(FeatureFlagService);
public readonly form = new FormGroup({
selectedAccountId: new FormControl<string>(this.accountContextService.selectedAccount().accountId),
});
public readonly availableAccounts: Signal<Account[]> = computed(() => this.accountContextService.availableAccounts);
// Feature flag: show/hide organization selector
// Default true maintains current behavior
protected readonly showOrganizationSelector = this.featureFlagService.getBooleanFlag('organization-selector', true);
}Template (board-member-dashboard.component.html):
<!-- Organization Selector - conditionally rendered -->
@if (showOrganizationSelector()) {
<div class="mb-6 flex items-center gap-4" data-testid="organization-selector">
<label for="organization-select" class="text-sm font-semibold text-gray-700">Organization:</label>
<lfx-select
[form]="form"
control="selectedAccountId"
[options]="availableAccounts()"
optionLabel="accountName"
optionValue="accountId"
[filter]="true"
filterPlaceholder="Search organizations..."
placeholder="Select an organization"
[showClear]="false"
styleClass="min-w-[300px]"
inputId="organization-select"
data-testid="organization-select" />
</div>
}
<!-- Rest of dashboard content always visible -->
<div class="dashboard-content">
<!-- ... -->
</div>Use Cases:
- Progressive Rollout: Test organization selector with specific users first
- A/B Testing: Compare dashboard usage with/without selector
- Quick Rollback: Disable immediately if performance issues occur
- Environment Control: Hide in production but show in staging
Dynamically filter sidebar navigation items based on feature flags:
Component (main-layout.component.ts):
import { Component, inject, computed } from '@angular/core';
import { Router } from '@angular/router';
import { FeatureFlagService } from '../../shared/services/feature-flag.service';
import { AppService } from '../../shared/services/app.service';
import { SidebarMenuItem } from '@lfx-one/shared/interfaces';
export class MainLayoutComponent {
private readonly router = inject(Router);
private readonly appService = inject(AppService);
private readonly featureFlagService = inject(FeatureFlagService);
// Feature flag: show/hide Projects in sidebar
private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', true);
// Base sidebar navigation items - complete set
private readonly baseSidebarItems: SidebarMenuItem[] = [
{
label: 'Overview',
icon: 'fa-light fa-grid-2',
routerLink: '/',
},
{
label: 'Meetings',
icon: 'fa-light fa-calendar',
routerLink: '/meetings',
},
{
label: 'Projects',
icon: 'fa-light fa-folder-open',
routerLink: '/projects',
},
];
// Computed sidebar items based on feature flags
// Re-evaluates automatically when showProjectsInSidebar changes
protected readonly sidebarItems = computed(() => {
const items = [...this.baseSidebarItems];
// Filter out Projects if feature flag is disabled
if (!this.showProjectsInSidebar()) {
return items.filter((item) => item.label !== 'Projects');
}
return items;
});
}Template (main-layout.component.html):
<!-- Desktop Sidebar -->
<div class="hidden lg:block w-64 flex-shrink-0 fixed top-0 left-0">
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
</div>
<!-- Mobile Sidebar -->
<div class="overflow-y-auto h-[calc(100vh-4rem)]">
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
</div>Use Cases:
- Phased Rollout: Hide unfinished Projects section until ready
- Access Control: Show/hide features based on user permissions (via targeting)
- Feature Gating: Enable premium features only for paid users
- Maintenance Mode: Temporarily hide sections during maintenance
Benefits:
- Single computed signal handles filtering logic
- Multiple flags can filter different items independently
- Sidebar component receives clean, filtered array
- No awareness of feature flags in child components
Combine multiple feature flags with complex logic:
export class DashboardComponent {
private readonly featureFlagService = inject(FeatureFlagService);
// Multiple feature flags
private readonly showAnalytics = this.featureFlagService.getBooleanFlag('analytics-dashboard', false);
private readonly showReports = this.featureFlagService.getBooleanFlag('reports-section', true);
private readonly showCharts = this.featureFlagService.getBooleanFlag('interactive-charts', false);
// Derived flag: analytics requires charts
protected readonly showFullAnalytics = computed(() => {
return this.showAnalytics() && this.showCharts();
});
// Composite dashboard sections
protected readonly dashboardSections = computed(() => {
const sections = [];
// Always show overview
sections.push({ id: 'overview', title: 'Overview', component: 'OverviewComponent' });
// Conditionally add analytics
if (this.showFullAnalytics()) {
sections.push({ id: 'analytics', title: 'Analytics', component: 'AnalyticsComponent' });
}
// Conditionally add reports
if (this.showReports()) {
sections.push({ id: 'reports', title: 'Reports', component: 'ReportsComponent' });
}
return sections;
});
// Feature configuration object flag
protected readonly chartConfig = this.featureFlagService.getObjectFlag<{
type: 'line' | 'bar' | 'pie';
animated: boolean;
colors: string[];
}>('chart-config', {
type: 'line',
animated: false,
colors: ['#3b82f6', '#10b981', '#f59e0b'],
});
}Template:
@for (section of dashboardSections(); track section.id) {
<div class="dashboard-section">
<h2>{{ section.title }}</h2>
@switch (section.id) { @case ('overview') {
<app-overview></app-overview>
} @case ('analytics') {
<app-analytics [config]="chartConfig()"></app-analytics>
} @case ('reports') {
<app-reports></app-reports>
} }
</div>
}Use Cases:
- Feature Dependencies: Only show analytics when charts are enabled
- Complex Configurations: Use object flags for multi-option settings
- Dynamic Layouts: Build dashboard sections based on multiple flags
- Gradual Rollouts: Enable features incrementally with dependent flags
Symptom: Flag changes in LaunchDarkly dashboard don't reflect in the application
Possible Causes:
-
Streaming Disabled:
// Check provider configuration streaming: true; // Must be true for real-time updates
-
Browser Not Connected:
- Open browser DevTools β Network tab
- Look for SSE connection to
clientstream.launchdarkly.com - Should show "pending" status (persistent connection)
-
Event Handlers Not Set Up:
// Verify setupEventHandlers() was called console.log('Initialized:', this.featureFlagService.initialized());
Solution:
- Ensure
streaming: truein provider configuration - Check browser console for connection errors
- Verify user context was set correctly
- Test with network throttling disabled
Symptom: Console warning "LaunchDarkly client ID not configured - feature flags disabled"
Possible Causes:
-
Missing Environment Variable:
# Check if LD_CLIENT_ID is set echo $LD_CLIENT_ID
-
Local Development - Missing .env File:
- Create
.envfile in project root - Add
LD_CLIENT_ID=your-client-id
- Create
-
Docker - Missing Runtime Variable:
- Pass
-e LD_CLIENT_ID=xxxwhen running container
- Pass
-
TransferState Not Hydrating:
- Check that
provideRuntimeConfig()is beforeprovideFeatureFlags()inapp.config.ts - Verify server is passing config via
REQUEST_CONTEXT
- Check that
Solution:
- Local development: Add
LD_CLIENT_IDto.envfile - Docker: Pass with
-e LD_CLIENT_ID=xxx - Kubernetes: Configure in deployment manifest
- Restart development server (
yarn dev)
See Runtime Configuration Troubleshooting for more details.
Symptom: Service initialized on server but not client, or vice versa
Possible Causes:
-
Missing Browser Check:
// Provider must check for browser environment if (typeof window === 'undefined') { return; }
-
Initialization Too Early:
- Service must initialize after authentication
- User context required for targeting
Solution:
- Verify browser check in
feature-flag.provider.ts - Initialize in
app.componentconstructor, not earlier - Confirm user object exists before calling
initialize()
Symptom: Targeting rules in LaunchDarkly don't work as expected
Possible Causes:
-
Missing Targeting Key:
// targetingKey is required for user identification targetingKey: user.preferred_username || user.username || user.sub || '';
-
Incorrect Attribute Names:
- LaunchDarkly is case-sensitive
- Custom attributes must match targeting rules exactly
Solution:
- Verify targeting key is unique and consistent
- Check custom attributes match LaunchDarkly rules
- Test targeting rules in LaunchDarkly dashboard
- Use LaunchDarkly debugger to see evaluation results
Symptom: All flags return default values, never true values from LaunchDarkly
Possible Causes:
-
Service Not Initialized:
// Check initialization status console.log('Initialized:', this.featureFlagService.initialized());
-
Wrong Flag Key:
- Flag keys are case-sensitive
- Must match exactly in LaunchDarkly dashboard
-
Initialization Failed:
- Check browser console for errors
- Network issues during initialization
Solution:
- Verify
initialized()signal returnstrue - Check flag keys match LaunchDarkly exactly
- Look for initialization errors in console
- Test with simplified flag (no targeting rules)
Enable Debug Logging (Development):
// feature-flag.provider.ts
logger: basicLogger({ level: 'debug' }), // Most verboseCheck Initialization Status:
// In any component
constructor() {
const flagService = inject(FeatureFlagService);
console.log('Flag service initialized:', flagService.initialized());
// Watch for initialization
effect(() => {
console.log('Initialized changed:', flagService.initialized());
});
}Verify User Context:
// After initialization
const client = OpenFeature.getClient();
const context = await OpenFeature.getContext();
console.log('Current context:', context);Test Flag Evaluation:
// Manual flag evaluation
const client = OpenFeature.getClient();
const value = client.getBooleanValue('test-flag', false);
console.log('Flag value:', value);LaunchDarkly Dashboard Debugging:
- Navigate to your project in LaunchDarkly
- Click on a flag β "Debugger" tab
- See real-time evaluation results for each user
- Verify targeting rules are evaluating correctly
- Runtime Configuration - How client IDs are injected at runtime
- State Management - Angular signals and reactive patterns
- Angular Patterns - Zoneless change detection and modern Angular
- Component Architecture - Component design and organization
- Authentication - User context and Auth0 integration
- Deployment Guide - Environment variables and deployment
- OpenFeature Documentation - OpenFeature specification and SDKs
- LaunchDarkly JavaScript SDK - LaunchDarkly client SDK
- Angular Signals Guide - Official Angular signals documentation
- LaunchDarkly Targeting - Targeting rules and strategies