mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
Add creating-feature-flags skill
Automates feature flag creation for Bitwarden Android with support for Boolean, Int, and String flag types. Key features: - Interactive prompts for flag configuration - Automatic name generation and validation (kebab-case, PascalCase, snake_case) - Updates FlagKey.kt, FlagKeyTest.kt, FeatureFlagListItems.kt, and strings_non_localized.xml - Comprehensive test execution and compilation verification - Progressive disclosure with reference documentation
This commit is contained in:
parent
8c6782dcb1
commit
ed095e5b30
101
.claude/skills/creating-feature-flags/README.md
Normal file
101
.claude/skills/creating-feature-flags/README.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Creating Feature Flags Skill
|
||||
|
||||
Automates the creation of feature flags for Bitwarden Android, following established patterns and best practices.
|
||||
|
||||
## What It Does
|
||||
|
||||
This skill guides you through creating a new feature flag by:
|
||||
|
||||
1. **Gathering requirements** - Asks questions about flag name, type, target application, and display label
|
||||
2. **Generating names** - Automatically creates kebab-case, PascalCase, and snake_case variants
|
||||
3. **Validating uniqueness** - Checks for naming conflicts before making changes
|
||||
4. **Updating files** - Modifies all required files following established patterns:
|
||||
- `FlagKey.kt` - Adds sealed class data object and registers in active flags list
|
||||
- `FlagKeyTest.kt` - Adds test coverage for key name and default value
|
||||
- `FeatureFlagListItems.kt` - Integrates UI rendering in debug menu
|
||||
- `strings_non_localized.xml` - Adds display label string resource
|
||||
5. **Running verification** - Executes tests and compiles affected modules
|
||||
6. **Providing summary** - Shows what was changed and next steps
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when you need to:
|
||||
|
||||
- Add a new feature flag to Password Manager or Authenticator
|
||||
- Implement flag-controlled features
|
||||
- Update the feature flag system
|
||||
|
||||
## How to Use
|
||||
|
||||
Simply invoke the skill and answer the questions:
|
||||
|
||||
```
|
||||
@creating-feature-flags
|
||||
```
|
||||
|
||||
The skill will ask you:
|
||||
|
||||
1. **Flag Name** - Descriptive name with optional JIRA ticket (e.g., "PM-12345 Enable TOTP Export")
|
||||
2. **Flag Type** - Boolean (most common), Int, or String
|
||||
3. **Application Target** - Password Manager, Authenticator, or both
|
||||
4. **Display Label** - Human-readable text for debug menu (e.g., "Enable TOTP Export")
|
||||
5. **Default Value** - Usually `false` for Boolean flags
|
||||
|
||||
## What You Get
|
||||
|
||||
After completion, you'll have:
|
||||
|
||||
- ✓ Fully integrated feature flag in all required files
|
||||
- ✓ Passing tests
|
||||
- ✓ Compiled modules
|
||||
- ✓ Debug menu integration (for Boolean flags)
|
||||
- ✓ Ready-to-commit changes
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Working in the Bitwarden Android repository
|
||||
- Feature flag already created in LaunchDarkly (with the kebab-case key name)
|
||||
- Clean working directory recommended
|
||||
|
||||
## Supported Flag Types
|
||||
|
||||
| Type | Use Case | Example |
|
||||
|------|----------|---------|
|
||||
| **Boolean** | Enable/disable features | `false` → feature disabled |
|
||||
| **Int** | Numeric thresholds, counts | `3` → max retry count |
|
||||
| **String** | Configuration values | `"api.bitwarden.com"` → endpoint URL |
|
||||
|
||||
**Note:** Int and String flags require custom UI components for debug menu display. The skill will create the flag structure, but UI integration must be implemented separately.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
You: @creating-feature-flags
|
||||
|
||||
Claude: I'll help you create a new feature flag. Let me gather the necessary information:
|
||||
|
||||
1. What is the feature flag name? (Include JIRA ticket if applicable)
|
||||
2. What type should this feature flag be? (Boolean/Int/String)
|
||||
3. Which application(s) should this flag target?
|
||||
4. What should the debug menu display label be?
|
||||
|
||||
You: pm-20558-migrate-myvault-to-myitems, Boolean, Password Manager, Migrate My Vault to My Items
|
||||
|
||||
Claude: [Proceeds to create the flag, showing progress and results]
|
||||
```
|
||||
|
||||
## After Completion
|
||||
|
||||
1. **Review changes**: `git diff`
|
||||
2. **Test in debug menu**: Build and run the app
|
||||
3. **Use in code**:
|
||||
```kotlin
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.YourFlagName)
|
||||
.onEach { isEnabled ->
|
||||
if (isEnabled) {
|
||||
// Your feature implementation
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
```
|
||||
4. **Commit and create PR** when ready
|
||||
344
.claude/skills/creating-feature-flags/SKILL.md
Normal file
344
.claude/skills/creating-feature-flags/SKILL.md
Normal file
@ -0,0 +1,344 @@
|
||||
---
|
||||
name: creating-feature-flags
|
||||
version: 1.0.0
|
||||
description: Automates feature flag creation for Bitwarden Android. **Use when adding new feature flags, implementing flag-controlled features, or updating the flag system.** Creates FlagKey definitions, updates UI components, generates tests, and adds string resources. Validates uniqueness and naming conventions. Supports Boolean, Int, and String flag types for both Password Manager and Authenticator applications.
|
||||
---
|
||||
|
||||
# Creating Feature Flags
|
||||
|
||||
Complete automation for adding feature flags to Bitwarden Android, following established patterns
|
||||
from PR #6235.
|
||||
|
||||
## Overview
|
||||
|
||||
Feature flags in Bitwarden Android follow a consistent four-file pattern:
|
||||
|
||||
1. **FlagKey.kt** - Sealed class definition with key name and default value
|
||||
2. **FlagKeyTest.kt** - Test coverage for key name and default value
|
||||
3. **FeatureFlagListItems.kt** - UI rendering in debug menu
|
||||
4. **strings_non_localized.xml** - Display label string resource
|
||||
|
||||
**File Locations:**
|
||||
|
||||
```
|
||||
core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt
|
||||
core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt
|
||||
ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt
|
||||
ui/src/main/res/values/strings_non_localized.xml
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
### Step 1: Gather Feature Flag Information
|
||||
|
||||
<thinking>
|
||||
Information needed to create a feature flag:
|
||||
1. What is the feature flag name? (user-provided, may include JIRA ticket)
|
||||
2. What type? (Boolean/Int/String)
|
||||
3. Which application(s)? (Password Manager/Authenticator/Both)
|
||||
4. What's the display label for debug menu?
|
||||
5. What's the default value?
|
||||
</thinking>
|
||||
|
||||
**Collect core information from the user:**
|
||||
|
||||
Ask the following questions in your response to gather the necessary information:
|
||||
|
||||
1. **Flag Name**: "What is the feature flag name? (Include JIRA ticket if applicable, e.g., 'PM-12345 Enable TOTP Export' or 'Cipher Key Encryption'. This will be converted to kebab-case.)"
|
||||
|
||||
2. **Flag Type**: "What type should this feature flag be? (Boolean - most common for enable/disable features; Int - for numeric thresholds; String - for configuration values)"
|
||||
|
||||
3. **Application Target**: "Which application(s) should this flag target? (Password Manager - activePasswordManagerFlags; Authenticator - activeAuthenticatorFlags; or Both)"
|
||||
|
||||
4. **Display Label**: "What should the debug menu display label be? (Short, human-readable label like 'Enable TOTP Export')"
|
||||
|
||||
**Determine default value:**
|
||||
|
||||
- Boolean: Standard is `false`. Ask if user wants `true` instead.
|
||||
- Int/String: Ask for specific default value.
|
||||
|
||||
### Step 2: Generate and Validate Names
|
||||
|
||||
<thinking>
|
||||
Name generation and validation:
|
||||
1. Extract JIRA ticket (pattern: [A-Z]{2,4}-\d+)
|
||||
2. Generate keyName (kebab-case)
|
||||
3. Generate DataClassName (PascalCase)
|
||||
4. Generate string_resource_key (snake_case)
|
||||
5. Validate uniqueness by reading FlagKey.kt
|
||||
6. Check format compliance
|
||||
</thinking>
|
||||
|
||||
**Read `reference/naming-conventions.md`** for detailed naming rules.
|
||||
|
||||
**Generate three name formats:**
|
||||
|
||||
1. **keyName** (kebab-case):
|
||||
- Lowercase, hyphens between words
|
||||
- Include JIRA ticket if present: `{ticket}-{feature-description}`
|
||||
- Example: `"PM-12345 Enable TOTP"` → `"pm-12345-enable-totp"`
|
||||
|
||||
2. **DataClassName** (PascalCase):
|
||||
- Remove JIRA prefix, capitalize each word
|
||||
- Example: `"pm-12345-enable-totp"` → `"EnableTotp"`
|
||||
|
||||
3. **string_resource_key** (snake_case):
|
||||
- Remove JIRA prefix, replace hyphens with underscores
|
||||
- Example: `"pm-12345-enable-totp"` → `"enable_totp"`
|
||||
|
||||
**Validate uniqueness:**
|
||||
|
||||
- Read FlagKey.kt and search for existing keyNames
|
||||
- If duplicate found, inform user and suggest alternative
|
||||
- Verify format compliance (kebab-case, PascalCase, snake_case)
|
||||
|
||||
**Display generated names for confirmation:**
|
||||
|
||||
```
|
||||
Generated names:
|
||||
- keyName: {key-name}
|
||||
- Data Class: {DataClassName}
|
||||
- String Resource: {string_resource_key}
|
||||
|
||||
Proceeding with these names.
|
||||
```
|
||||
|
||||
### Step 3: Modify FlagKey.kt
|
||||
|
||||
<thinking>
|
||||
FlagKey.kt modifications:
|
||||
1. Where to insert data object? (before //region Dummy keys)
|
||||
2. Which active flags list? (based on application target)
|
||||
3. What template to use? (based on type: Boolean/Int/String)
|
||||
</thinking>
|
||||
|
||||
**Read `reference/file-templates.md`** for complete code templates.
|
||||
|
||||
**Location:** `core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt`
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. **Add data object** before `//region Dummy keys for testing` comment
|
||||
2. **Add to active flags list(s)**
|
||||
|
||||
**Use Edit tool** to make both modifications.
|
||||
|
||||
### Step 4: Modify FlagKeyTest.kt
|
||||
|
||||
<thinking>
|
||||
Test modifications required:
|
||||
1. Add keyName assertion to first test
|
||||
2. Add default value assertion to second test (or create new test)
|
||||
3. Pattern depends on type and default value
|
||||
</thinking>
|
||||
|
||||
**Read `reference/file-templates.md`** for test templates.
|
||||
|
||||
**Location:** `core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt`
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. **Add keyName assertion** to test: `Feature flags have the correct key name set`
|
||||
2. **Add default value test:**
|
||||
- Boolean false: Add to existing list
|
||||
- Boolean true / Int / String: Create separate test assertion
|
||||
|
||||
**Use Edit tool** to make modifications.
|
||||
|
||||
### Step 5: Modify FeatureFlagListItems.kt
|
||||
|
||||
<thinking>
|
||||
UI modifications:
|
||||
1. Add to ListItemContent when expression (by type)
|
||||
2. Add to getDisplayLabel when expression
|
||||
3. Int/String flags need special handling (UI components don't exist yet)
|
||||
</thinking>
|
||||
|
||||
**Read `reference/file-templates.md`** for UI templates.
|
||||
|
||||
**Location:**
|
||||
`ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt`
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. **Update ListItemContent when expression:**
|
||||
- Boolean: Add to existing Boolean block
|
||||
- Int/String: Inform user that UI component needs implementation, add TODO comment
|
||||
|
||||
2. **Update getDisplayLabel when expression:**
|
||||
- Add mapping to string resource
|
||||
|
||||
**Use Edit tool** to make modifications.
|
||||
|
||||
**Note:** If Int/String flag, inform user:
|
||||
|
||||
```
|
||||
⚠️ Int/String Flag UI Component Required
|
||||
|
||||
The codebase only implements BooleanFlagItem. Int/String flags work but won't
|
||||
appear in debug menu until custom UI component is created.
|
||||
|
||||
Options:
|
||||
a) Skip UI integration (flag functional but not in debug menu)
|
||||
b) Pause to implement UI component first
|
||||
|
||||
Proceeding with option (a) and adding TODO comment.
|
||||
```
|
||||
|
||||
### Step 6: Modify strings_non_localized.xml
|
||||
|
||||
<thinking>
|
||||
String resource addition:
|
||||
1. Find Debug Menu region
|
||||
2. Add string resource
|
||||
</thinking>
|
||||
|
||||
**Read `reference/file-templates.md`** for string resource template.
|
||||
|
||||
**Location:** `ui/src/main/res/values/strings_non_localized.xml`
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. **Add string resource** within `<!-- region Debug Menu -->` section
|
||||
|
||||
**Use Edit tool** to insert the string resource.
|
||||
|
||||
### Step 7: Run Tests and Verify
|
||||
|
||||
<thinking>
|
||||
Verification steps:
|
||||
1. Run FlagKeyTest to verify test changes
|
||||
2. Compile all affected modules to catch errors
|
||||
3. Check for any compilation or test failures
|
||||
4. If failures, consult troubleshooting guide
|
||||
</thinking>
|
||||
|
||||
**Execute verification commands:**
|
||||
|
||||
```bash
|
||||
# Test the new flag
|
||||
./gradlew :core:testDebug --tests "com.bitwarden.core.data.manager.model.FlagKeyTest"
|
||||
|
||||
# Verify compilation
|
||||
./gradlew :core:compileDebugKotlin :ui:compileDebugKotlin :app:compileStandardDebugKotlin
|
||||
```
|
||||
|
||||
**If tests or compilation fail:**
|
||||
|
||||
- **Read `reference/troubleshooting.md`** for common issues and solutions
|
||||
- Review error messages carefully
|
||||
- Check for typos, missing commas, or syntax issues
|
||||
- Fix issues and re-run verification
|
||||
|
||||
**Use Bash tool** to execute commands.
|
||||
|
||||
### Step 8: Provide Completion Summary
|
||||
|
||||
**Generate comprehensive summary:**
|
||||
|
||||
```markdown
|
||||
## ✓ Feature Flag Creation Complete
|
||||
|
||||
**Flag Details:**
|
||||
|
||||
- Data Class: {DataClassName}
|
||||
- Key Name: {keyName}
|
||||
- Type: {Type}
|
||||
- Default: {defaultValue}
|
||||
- Application: {Password Manager / Authenticator / Both}
|
||||
- Display Label: {DisplayLabel}
|
||||
|
||||
**Files Modified:**
|
||||
|
||||
1. ✓ FlagKey.kt
|
||||
2. ✓ FlagKeyTest.kt
|
||||
3. ✓ FeatureFlagListItems.kt
|
||||
4. ✓ strings_non_localized.xml
|
||||
|
||||
**Verification:**
|
||||
✓ Tests passed
|
||||
✓ Modules compiled successfully
|
||||
|
||||
**Next Steps:**
|
||||
|
||||
1. Review changes: `git diff`
|
||||
2. Test in debug menu (build and run app)
|
||||
3. Use in code:
|
||||
\```kotlin
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.{DataClassName})
|
||||
.onEach { isEnabled -> /* ... */ }
|
||||
.launchIn(viewModelScope)
|
||||
\```
|
||||
4. Commit and create PR
|
||||
|
||||
**Reference:** PR #6235 - https://github.com/bitwarden/android/pull/6235
|
||||
```
|
||||
|
||||
If Int/String flag, add note about missing UI component.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
<thinking>
|
||||
When to load additional resources:
|
||||
1. Need naming examples? → reference/naming-conventions.md
|
||||
2. Need code templates? → reference/file-templates.md
|
||||
3. Encountering errors? → reference/troubleshooting.md
|
||||
4. Want to see examples? → examples/scenarios.md
|
||||
</thinking>
|
||||
|
||||
**Load on-demand when needed:**
|
||||
|
||||
- **Naming rules and examples** → `reference/naming-conventions.md`
|
||||
- **Complete code templates** → `reference/file-templates.md`
|
||||
- **Common issues and solutions** → `reference/troubleshooting.md`
|
||||
- **Real-world examples** → `examples/scenarios.md`
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before completing, verify:
|
||||
|
||||
- [ ] keyName is unique and follows kebab-case
|
||||
- [ ] DataClassName is unique and follows PascalCase
|
||||
- [ ] string_resource_key is unique and follows snake_case
|
||||
- [ ] Data object added to FlagKey.kt before dummy keys region
|
||||
- [ ] Flag added to appropriate active flags list(s)
|
||||
- [ ] Test assertions added (keyName + defaultValue)
|
||||
- [ ] Both when expressions updated in FeatureFlagListItems.kt
|
||||
- [ ] String resource added in Debug Menu region
|
||||
- [ ] All tests pass
|
||||
- [ ] All modules compile successfully
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
**DO NOT:**
|
||||
|
||||
- Skip validation checks for uniqueness
|
||||
- Use wrong case format (camelCase for keyName, snake_case for DataClassName, etc.)
|
||||
- Forget to add flag to active flags list
|
||||
- Skip test modifications
|
||||
- Forget commas in lists
|
||||
- Reorder existing entries (adds unnecessary PR noise)
|
||||
- Skip running tests before completing
|
||||
|
||||
## Success Criteria
|
||||
|
||||
A successful feature flag creation:
|
||||
|
||||
- ✓ Passes all tests
|
||||
- ✓ Compiles without errors
|
||||
- ✓ Follows all naming conventions
|
||||
- ✓ Appears in debug menu (Boolean flags only)
|
||||
- ✓ Accessible via FeatureFlagManager
|
||||
- ✓ Matches established patterns
|
||||
|
||||
## Reference
|
||||
|
||||
**Supporting Files:**
|
||||
|
||||
- `reference/naming-conventions.md` - Complete naming rules and examples
|
||||
- `reference/file-templates.md` - Code templates for all file types
|
||||
- `reference/troubleshooting.md` - Common issues and solutions
|
||||
- `examples/scenarios.md` - Real-world usage examples
|
||||
|
||||
**Architecture:** Feature flags use sealed classes for type safety, load from remote config at
|
||||
startup, flow reactively through FeatureFlagManager, and support local override in debug menu.
|
||||
361
.claude/skills/creating-feature-flags/examples/scenarios.md
Normal file
361
.claude/skills/creating-feature-flags/examples/scenarios.md
Normal file
@ -0,0 +1,361 @@
|
||||
# Feature Flag Creation Scenarios
|
||||
|
||||
Common use cases and complete examples for creating feature flags.
|
||||
|
||||
## Scenario 1: Simple Boolean Feature Flag
|
||||
|
||||
**Use Case:** Adding a flag to control rollout of a new Password Manager feature.
|
||||
|
||||
**Example: Enable Password History Export**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "PM-12345 Enable Password History Export"
|
||||
- Type: Boolean
|
||||
- Application: Password Manager
|
||||
- Display Label: "Enable Password History Export"
|
||||
- Default: false (confirmed)
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `pm-12345-enable-password-history-export`
|
||||
- DataClassName: `EnablePasswordHistoryExport`
|
||||
- string_resource_key: `enable_password_history_export`
|
||||
|
||||
**Files Modified:**
|
||||
1. FlagKey.kt - Added data object and registered in activePasswordManagerFlags
|
||||
2. FlagKeyTest.kt - Added assertions for keyName and defaultValue
|
||||
3. FeatureFlagListItems.kt - Added to Boolean when expressions
|
||||
4. strings_non_localized.xml - Added display string
|
||||
|
||||
**Verification Commands:**
|
||||
```bash
|
||||
./gradlew :core:testDebug --tests "com.bitwarden.core.data.manager.model.FlagKeyTest"
|
||||
./gradlew :core:compileDebugKotlin :ui:compileDebugKotlin :app:compileStandardDebugKotlin
|
||||
```
|
||||
|
||||
**Usage in Code:**
|
||||
```kotlin
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.EnablePasswordHistoryExport)
|
||||
.onEach { isEnabled ->
|
||||
if (isEnabled) {
|
||||
// Show password history export option
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 2: Authenticator-Only Flag
|
||||
|
||||
**Use Case:** Feature specific to the Authenticator application.
|
||||
|
||||
**Example: Enable Biometric Unlock**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "Enable Biometric Unlock"
|
||||
- Type: Boolean
|
||||
- Application: Authenticator (only)
|
||||
- Display Label: "Enable Biometric Unlock"
|
||||
- Default: false
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `enable-biometric-unlock`
|
||||
- DataClassName: `EnableBiometricUnlock`
|
||||
- string_resource_key: `enable_biometric_unlock`
|
||||
|
||||
**Key Difference:**
|
||||
- Added to `activeAuthenticatorFlags` instead of `activePasswordManagerFlags`
|
||||
- Only appears in Authenticator app's debug menu
|
||||
|
||||
**Usage in Authenticator:**
|
||||
```kotlin
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.EnableBiometricUnlock)
|
||||
.onEach { isEnabled ->
|
||||
if (isEnabled) {
|
||||
// Enable biometric authentication option
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 3: Multi-Application Flag
|
||||
|
||||
**Use Case:** Feature that applies to both Password Manager and Authenticator.
|
||||
|
||||
**Example: Enhanced Logging**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "Enhanced Logging"
|
||||
- Type: Boolean
|
||||
- Application: Password Manager AND Authenticator (both selected)
|
||||
- Display Label: "Enhanced Logging"
|
||||
- Default: false
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `enhanced-logging`
|
||||
- DataClassName: `EnhancedLogging`
|
||||
- string_resource_key: `enhanced_logging`
|
||||
|
||||
**Key Difference:**
|
||||
- Added to BOTH `activePasswordManagerFlags` AND `activeAuthenticatorFlags`
|
||||
- Appears in debug menu of both applications
|
||||
- Single flag definition works for both
|
||||
|
||||
**Active Flags Registration:**
|
||||
```kotlin
|
||||
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
// ... other flags ...
|
||||
EnhancedLogging,
|
||||
)
|
||||
}
|
||||
|
||||
val activeAuthenticatorFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
// ... other flags ...
|
||||
EnhancedLogging,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 4: Feature Flag Without JIRA Ticket
|
||||
|
||||
**Use Case:** Internal feature or experiment not tracked in JIRA.
|
||||
|
||||
**Example: Experimental UI Animation**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "Experimental UI Animation"
|
||||
- Type: Boolean
|
||||
- Application: Password Manager
|
||||
- Display Label: "Experimental UI Animation"
|
||||
- Default: false
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `experimental-ui-animation` (no JIRA prefix)
|
||||
- DataClassName: `ExperimentalUiAnimation`
|
||||
- string_resource_key: `experimental_ui_animation`
|
||||
|
||||
**Note:** Absence of JIRA ticket is fine for internal flags, experiments, or R&D features.
|
||||
|
||||
---
|
||||
|
||||
## Scenario 5: Boolean Flag with True Default
|
||||
|
||||
**Use Case:** Flag that's enabled by default, can be disabled if issues occur.
|
||||
|
||||
**Example: Use Optimized Crypto**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "Use Optimized Crypto"
|
||||
- Type: Boolean
|
||||
- Application: Password Manager
|
||||
- Display Label: "Use Optimized Crypto"
|
||||
- Default: true (user explicitly requested true)
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `use-optimized-crypto`
|
||||
- DataClassName: `UseOptimizedCrypto`
|
||||
- string_resource_key: `use_optimized_crypto`
|
||||
|
||||
**Test Difference:**
|
||||
Requires separate test assertion instead of adding to the list:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `UseOptimizedCrypto has correct default value`() {
|
||||
assertTrue(FlagKey.UseOptimizedCrypto.defaultValue)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 6: Int Flag (Advanced)
|
||||
|
||||
**Use Case:** Configurable numeric threshold or count.
|
||||
|
||||
**Example: Max Login Attempts**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "Max Login Attempts"
|
||||
- Type: Int
|
||||
- Application: Password Manager
|
||||
- Display Label: "Max Login Attempts"
|
||||
- Default: 5
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `max-login-attempts`
|
||||
- DataClassName: `MaxLoginAttempts`
|
||||
- string_resource_key: `max_login_attempts`
|
||||
|
||||
**Special Handling:**
|
||||
- Works fully as a feature flag
|
||||
- Can be accessed via FeatureFlagManager
|
||||
- **Does NOT appear in debug menu** (requires custom IntFlagItem UI component)
|
||||
- Add TODO comment in FeatureFlagListItems.kt
|
||||
|
||||
**Usage:**
|
||||
```kotlin
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.MaxLoginAttempts)
|
||||
.onEach { maxAttempts ->
|
||||
// Use the configured max attempts value
|
||||
if (attemptCount >= maxAttempts) {
|
||||
// Lock account
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
```
|
||||
|
||||
**Test Pattern:**
|
||||
```kotlin
|
||||
@Test
|
||||
fun `MaxLoginAttempts has correct default value`() {
|
||||
assertEquals(5, FlagKey.MaxLoginAttempts.defaultValue)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 7: String Flag (Advanced)
|
||||
|
||||
**Use Case:** Configurable string value or enum.
|
||||
|
||||
**Example: API Endpoint Override**
|
||||
|
||||
**User Inputs:**
|
||||
- Flag Name: "API Endpoint Override"
|
||||
- Type: String
|
||||
- Application: Password Manager
|
||||
- Display Label: "API Endpoint Override"
|
||||
- Default: "https://api.bitwarden.com"
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `api-endpoint-override`
|
||||
- DataClassName: `ApiEndpointOverride`
|
||||
- string_resource_key: `api_endpoint_override`
|
||||
|
||||
**Special Handling:**
|
||||
- Works fully as a feature flag
|
||||
- **Does NOT appear in debug menu** (requires custom StringFlagItem UI component)
|
||||
- Useful for testing different environments or configurations
|
||||
|
||||
**Usage:**
|
||||
```kotlin
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.ApiEndpointOverride)
|
||||
.onEach { apiEndpoint ->
|
||||
// Use the configured API endpoint
|
||||
retrofit.baseUrl(apiEndpoint)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario 8: Reusing Existing Flag Pattern
|
||||
|
||||
**Use Case:** Creating a flag similar to an existing one.
|
||||
|
||||
**Example: New CXP Feature (following CXP Import/Export pattern)**
|
||||
|
||||
**Reference Existing Flags:**
|
||||
- CredentialExchangeProtocolImport: `cxp-import-mobile`
|
||||
- CredentialExchangeProtocolExport: `cxp-export-mobile`
|
||||
|
||||
**New Flag:**
|
||||
- Flag Name: "CXP Sync Mobile"
|
||||
- keyName: `cxp-sync-mobile` (follows existing pattern)
|
||||
- DataClassName: `CredentialExchangeProtocolSync`
|
||||
|
||||
**Benefits:**
|
||||
- Consistent naming with related features
|
||||
- Easy to understand relationship
|
||||
- Groups related flags together in code
|
||||
|
||||
---
|
||||
|
||||
## Anti-Pattern Examples
|
||||
|
||||
### ❌ Anti-Pattern 1: Vague Flag Name
|
||||
|
||||
**Bad:**
|
||||
- Flag Name: "New Feature"
|
||||
- Result: keyName `new-feature` (what feature?)
|
||||
|
||||
**Good:**
|
||||
- Flag Name: "Enable Password Breach Monitoring"
|
||||
- Result: keyName `enable-password-breach-monitoring` (clear purpose)
|
||||
|
||||
### ❌ Anti-Pattern 2: Overly Specific Flag
|
||||
|
||||
**Bad:**
|
||||
- Flag Name: "Show Blue Button on Settings Screen Third Row"
|
||||
- Too specific, couples flag to UI implementation
|
||||
|
||||
**Good:**
|
||||
- Flag Name: "Enable Advanced Settings"
|
||||
- Describes feature, not implementation details
|
||||
|
||||
### ❌ Anti-Pattern 3: Wrong Application Target
|
||||
|
||||
**Bad:**
|
||||
- Creating Authenticator-specific flag but targeting Password Manager
|
||||
- Users won't see the flag in the correct app
|
||||
|
||||
**Good:**
|
||||
- Match application target to where feature will be used
|
||||
- Use multi-application only when truly shared
|
||||
|
||||
### ❌ Anti-Pattern 4: Duplicate Flag
|
||||
|
||||
**Bad:**
|
||||
- Creating `enable-totp-export` when `credential-exchange-protocol-export` already handles TOTP export
|
||||
|
||||
**Good:**
|
||||
- Check existing flags first
|
||||
- Reuse existing flags when features overlap
|
||||
|
||||
---
|
||||
|
||||
## Next Steps After Creation
|
||||
|
||||
**1. Verify Creation:**
|
||||
```bash
|
||||
# Run tests
|
||||
./gradlew :core:testDebug --tests "FlagKeyTest"
|
||||
|
||||
# Verify compilation
|
||||
./gradlew :app:compileStandardDebugKotlin
|
||||
```
|
||||
|
||||
**2. Test in Debug Menu:**
|
||||
- Build and install app
|
||||
- Navigate to Settings → Debug Menu
|
||||
- Verify flag appears with correct label
|
||||
- Toggle flag and verify state persists
|
||||
|
||||
**3. Implement Feature:**
|
||||
```kotlin
|
||||
// In ViewModel or Repository
|
||||
init {
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.{YourFlag})
|
||||
.onEach { isEnabled ->
|
||||
// React to flag state
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
```
|
||||
|
||||
**4. Create PR:**
|
||||
- Include flag in feature PR, or
|
||||
- Create separate flag-only PR (preferred for large features)
|
||||
- Reference JIRA ticket in PR description
|
||||
- Note that flag defaults to false (safe rollout)
|
||||
|
||||
**5. Remote Configuration:**
|
||||
- After merge, configure flag in remote feature flag system
|
||||
- Test rollout with small percentage
|
||||
- Monitor for issues
|
||||
- Gradually increase rollout percentage
|
||||
@ -0,0 +1,364 @@
|
||||
# Feature Flag Code Templates
|
||||
|
||||
Complete code templates for all file modifications required when creating feature flags.
|
||||
|
||||
## Template Variables
|
||||
|
||||
Throughout these templates, replace the following placeholders:
|
||||
|
||||
- `{DataClassName}` - PascalCase data object name (e.g., `EnableTotpExport`)
|
||||
- `{keyName}` - kebab-case key name (e.g., `enable-totp-export`)
|
||||
- `{Type}` - Flag type: `Boolean`, `Int`, or `String`
|
||||
- `{defaultValue}` - Default value for the flag
|
||||
- `{FeatureDescription}` - Human-readable feature description
|
||||
- `{string_resource_key}` - snake_case resource key (e.g., `enable_totp_export`)
|
||||
- `{DisplayLabel}` - Display label for debug menu
|
||||
|
||||
## FlagKey.kt Templates
|
||||
|
||||
**Location:** `core/src/main/kotlin/com/bitwarden/core/data/manager/model/FlagKey.kt`
|
||||
|
||||
### Boolean Flag Template
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the {FeatureDescription} feature.
|
||||
*/
|
||||
data object {DataClassName} : FlagKey<Boolean>() {
|
||||
override val keyName: String = "{keyName}"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the Enable TOTP Export feature.
|
||||
*/
|
||||
data object EnableTotpExport : FlagKey<Boolean>() {
|
||||
override val keyName: String = "enable-totp-export"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
```
|
||||
|
||||
### Int Flag Template
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the {FeatureDescription} feature.
|
||||
*/
|
||||
data object {DataClassName} : FlagKey<Int>() {
|
||||
override val keyName: String = "{keyName}"
|
||||
override val defaultValue: Int = {defaultValue}
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the Max Retry Count feature.
|
||||
*/
|
||||
data object MaxRetryCount : FlagKey<Int>() {
|
||||
override val keyName: String = "max-retry-count"
|
||||
override val defaultValue: Int = 3
|
||||
}
|
||||
```
|
||||
|
||||
### String Flag Template
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the {FeatureDescription} feature.
|
||||
*/
|
||||
data object {DataClassName} : FlagKey<String>() {
|
||||
override val keyName: String = "{keyName}"
|
||||
override val defaultValue: String = "{defaultValue}"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the API Endpoint Override feature.
|
||||
*/
|
||||
data object ApiEndpointOverride : FlagKey<String>() {
|
||||
override val keyName: String = "api-endpoint-override"
|
||||
override val defaultValue: String = "https://api.bitwarden.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Active Flags List Addition
|
||||
|
||||
**For Password Manager flags:**
|
||||
```kotlin
|
||||
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
// ... existing flags ...
|
||||
{DataClassName}, // Add this line
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**For Authenticator flags:**
|
||||
```kotlin
|
||||
val activeAuthenticatorFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
// ... existing flags ...
|
||||
{DataClassName}, // Add this line
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## FlagKeyTest.kt Templates
|
||||
|
||||
**Location:** `core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt`
|
||||
|
||||
### keyName Test Assertion
|
||||
|
||||
Add to the test: `fun \`Feature flags have the correct key name set\``
|
||||
|
||||
```kotlin
|
||||
assertEquals(
|
||||
FlagKey.{DataClassName}.keyName,
|
||||
"{keyName}",
|
||||
)
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
assertEquals(
|
||||
FlagKey.EnableTotpExport.keyName,
|
||||
"enable-totp-export",
|
||||
)
|
||||
```
|
||||
|
||||
### Default Value Tests
|
||||
|
||||
**For Boolean flags with false default:**
|
||||
|
||||
Add to the list in test: `fun \`All feature flags have the correct default value set\``
|
||||
|
||||
```kotlin
|
||||
assertTrue(
|
||||
listOf(
|
||||
// ... existing flags ...
|
||||
FlagKey.{DataClassName}, // Add this line
|
||||
).all {
|
||||
!it.defaultValue
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**For Boolean flags with true default:**
|
||||
|
||||
Create new test function:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `{DataClassName} has correct default value`() {
|
||||
assertTrue(FlagKey.{DataClassName}.defaultValue)
|
||||
}
|
||||
```
|
||||
|
||||
**For Int flags:**
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `{DataClassName} has correct default value`() {
|
||||
assertEquals({defaultValue}, FlagKey.{DataClassName}.defaultValue)
|
||||
}
|
||||
```
|
||||
|
||||
**For String flags:**
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `{DataClassName} has correct default value`() {
|
||||
assertEquals("{defaultValue}", FlagKey.{DataClassName}.defaultValue)
|
||||
}
|
||||
```
|
||||
|
||||
## FeatureFlagListItems.kt Templates
|
||||
|
||||
**Location:** `ui/src/main/kotlin/com/bitwarden/ui/platform/components/debug/FeatureFlagListItems.kt`
|
||||
|
||||
### ListItemContent When Expression Update
|
||||
|
||||
**For Boolean flags:**
|
||||
|
||||
Add to existing Boolean block (around line 25-32):
|
||||
|
||||
```kotlin
|
||||
when (val flagKey = this) {
|
||||
// ... existing dummy cases ...
|
||||
|
||||
FlagKey.DummyBoolean,
|
||||
// ... other existing Boolean flags ...
|
||||
FlagKey.{DataClassName}, // Add this line
|
||||
-> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
BooleanFlagItem(
|
||||
label = flagKey.getDisplayLabel(),
|
||||
key = flagKey as FlagKey<Boolean>,
|
||||
currentValue = currentValue as Boolean,
|
||||
onValueChange = onValueChange as (FlagKey<Boolean>, Boolean) -> Unit,
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**For Int/String flags:**
|
||||
|
||||
**NOTE:** Int and String flags require additional UI component implementation. The codebase currently only supports Boolean flags in the debug menu.
|
||||
|
||||
### getDisplayLabel When Expression Update
|
||||
|
||||
Add to the when expression (around line 73-80):
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
// ... existing cases ...
|
||||
FlagKey.{DataClassName} -> stringResource(BitwardenString.{string_resource_key})
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```kotlin
|
||||
FlagKey.EnableTotpExport -> stringResource(BitwardenString.enable_totp_export)
|
||||
```
|
||||
|
||||
## strings_non_localized.xml Template
|
||||
|
||||
**Location:** `ui/src/main/res/values/strings_non_localized.xml`
|
||||
|
||||
Add within the `<!-- region Debug Menu -->` section (around line 20-42):
|
||||
|
||||
```xml
|
||||
<string name="{string_resource_key}">{DisplayLabel}</string>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```xml
|
||||
<string name="enable_totp_export">Enable TOTP Export</string>
|
||||
```
|
||||
|
||||
## Insertion Points
|
||||
|
||||
**FlagKey.kt:**
|
||||
- **Data object:** Insert before `//region Dummy keys for testing` comment
|
||||
- **Active flags list:** Insert within appropriate list
|
||||
|
||||
**FlagKeyTest.kt:**
|
||||
- **keyName test:** Add assertion within existing test function
|
||||
- **defaultValue test:** Add to list or create new test based on type/value
|
||||
|
||||
**FeatureFlagListItems.kt:**
|
||||
- **ListItemContent:** Add to appropriate type block (Boolean/Int/String)
|
||||
- **getDisplayLabel:** Add case to when expression
|
||||
|
||||
**strings_non_localized.xml:**
|
||||
- **String resource:** Add within Debug Menu region
|
||||
|
||||
## Complete Example: Boolean Flag
|
||||
|
||||
**Flag Details:**
|
||||
- Flag Name: "PM-12345 Enable TOTP Export"
|
||||
- Type: Boolean
|
||||
- Default: false
|
||||
- Application: Password Manager
|
||||
- Display Label: "Enable TOTP Export"
|
||||
|
||||
**Generated Names:**
|
||||
- keyName: `pm-12345-enable-totp-export`
|
||||
- DataClassName: `EnableTotpExport`
|
||||
- string_resource_key: `enable_totp_export`
|
||||
|
||||
### FlagKey.kt Addition
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* Data object holding the feature flag key for the Enable TOTP Export feature.
|
||||
*/
|
||||
data object EnableTotpExport : FlagKey<Boolean>() {
|
||||
override val keyName: String = "pm-12345-enable-totp-export"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
```
|
||||
|
||||
Add to activePasswordManagerFlags:
|
||||
```kotlin
|
||||
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
CipherKeyEncryption,
|
||||
CredentialExchangeProtocolExport,
|
||||
CredentialExchangeProtocolImport,
|
||||
EnableTotpExport, // New addition
|
||||
ForceUpdateKdfSettings,
|
||||
NoLogoutOnKdfChange,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### FlagKeyTest.kt Addition
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `Feature flags have the correct key name set`() {
|
||||
// ... existing assertions ...
|
||||
assertEquals(
|
||||
FlagKey.EnableTotpExport.keyName,
|
||||
"pm-12345-enable-totp-export",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `All feature flags have the correct default value set`() {
|
||||
assertTrue(
|
||||
listOf(
|
||||
// ... existing flags ...
|
||||
FlagKey.EnableTotpExport,
|
||||
).all {
|
||||
!it.defaultValue
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### FeatureFlagListItems.kt Addition
|
||||
|
||||
```kotlin
|
||||
FlagKey.DummyBoolean,
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.EnableTotpExport, // New addition
|
||||
FlagKey.ForceUpdateKdfSettings,
|
||||
FlagKey.NoLogoutOnKdfChange,
|
||||
-> {
|
||||
// ... existing BooleanFlagItem code ...
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
// ... existing cases ...
|
||||
FlagKey.EnableTotpExport -> stringResource(BitwardenString.enable_totp_export)
|
||||
}
|
||||
```
|
||||
|
||||
### strings_non_localized.xml Addition
|
||||
|
||||
```xml
|
||||
<!-- region Debug Menu -->
|
||||
<string name="avoid_logout_on_kdf_change">Avoid logout on KDF change</string>
|
||||
<string name="bitwarden_authentication_enabled">Bitwarden authentication enabled</string>
|
||||
<string name="cipher_key_encryption">Cipher Key Encryption</string>
|
||||
<string name="enable_totp_export">Enable TOTP Export</string>
|
||||
<string name="force_update_kdf_settings">Force update KDF settings</string>
|
||||
<!-- endregion Debug Menu -->
|
||||
```
|
||||
@ -0,0 +1,136 @@
|
||||
# Feature Flag Naming Conventions
|
||||
|
||||
Comprehensive naming rules and conversion patterns for Bitwarden Android feature flags.
|
||||
|
||||
## Overview
|
||||
|
||||
Feature flags require three naming formats derived from the user-provided flag name:
|
||||
|
||||
1. **keyName** (kebab-case) - Used in FlagKey.kt for the string identifier
|
||||
2. **DataClassName** (PascalCase) - Used for the Kotlin data object name
|
||||
3. **string_resource_key** (snake_case) - Used in strings_non_localized.xml
|
||||
|
||||
## keyName Generation (kebab-case)
|
||||
|
||||
**Input:** User-provided flag name (may include JIRA ticket)
|
||||
|
||||
**Rules:**
|
||||
- Convert to lowercase
|
||||
- Replace spaces with hyphens
|
||||
- Remove special characters except hyphens
|
||||
- Include JIRA ticket prefix if present
|
||||
- Final format: `{ticket}-{feature-description}` or `{feature-description}`
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
"Enable TOTP Export" → "enable-totp-export"
|
||||
"PM-12345 Enable TOTP Export" → "pm-12345-enable-totp-export"
|
||||
"Cipher Key Encryption" → "cipher-key-encryption"
|
||||
"PM-18021 Force Update KDF" → "pm-18021-force-update-kdf"
|
||||
"PROJ-456 New Feature" → "proj-456-new-feature"
|
||||
```
|
||||
|
||||
**JIRA Ticket Detection:**
|
||||
- **Pattern:** `[A-Z]{2,4}-\d+` (2-4 uppercase letters, hyphen, one or more digits)
|
||||
- Extract ticket and include as lowercase prefix in keyName
|
||||
- Examples: PM-1234, PS-99, PROJ-5678, AB-1
|
||||
|
||||
**Validation:**
|
||||
- Must be all lowercase
|
||||
- Must use hyphens (not underscores, camelCase, or spaces)
|
||||
- Should be descriptive and meaningful
|
||||
- Typically 2-6 words
|
||||
- Check uniqueness against existing flags in FlagKey.kt
|
||||
|
||||
## DataClassName Generation (PascalCase)
|
||||
|
||||
**Input:** Generated keyName
|
||||
|
||||
**Rules:**
|
||||
1. Remove JIRA ticket prefix (matches `[a-z]{2,4}-\d+-`)
|
||||
2. Split on hyphens
|
||||
3. Capitalize first letter of each word
|
||||
4. Join without separators
|
||||
5. Result is valid Kotlin identifier
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
"enable-totp-export" → "EnableTotpExport"
|
||||
"pm-12345-enable-totp-export" → "EnableTotpExport"
|
||||
"cipher-key-encryption" → "CipherKeyEncryption"
|
||||
"proj-456-new-feature" → "NewFeature"
|
||||
```
|
||||
|
||||
**Special Cases:**
|
||||
- Acronyms: Capitalize each letter if widely recognized
|
||||
- `"kdf"` → `"Kdf"` (follow Kotlin naming conventions)
|
||||
- `"totp"` → `"Totp"`
|
||||
- `"api"` → `"Api"`
|
||||
|
||||
**Validation:**
|
||||
- Must start with uppercase letter
|
||||
- Must be valid Kotlin identifier
|
||||
- Check uniqueness against existing FlagKey data objects
|
||||
|
||||
## string_resource_key Generation (snake_case)
|
||||
|
||||
**Input:** Generated keyName
|
||||
|
||||
**Rules:**
|
||||
1. Remove JIRA ticket prefix (matches `[a-z]{2,4}-\d+-`)
|
||||
2. Replace hyphens with underscores
|
||||
3. Keep lowercase
|
||||
4. Result is valid XML resource name
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
"enable-totp-export" → "enable_totp_export"
|
||||
"pm-12345-enable-totp-export" → "enable_totp_export"
|
||||
"cipher-key-encryption" → "cipher_key_encryption"
|
||||
"proj-456-new-feature" → "new_feature"
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
- Must be all lowercase
|
||||
- Must use underscores
|
||||
- Must be valid XML resource name
|
||||
- Check uniqueness in strings_non_localized.xml
|
||||
|
||||
## Name Generation Algorithm
|
||||
|
||||
**Complete conversion process:**
|
||||
|
||||
```
|
||||
User Input: "PM-12345 Enable Password History Export"
|
||||
|
||||
Step 1: Generate keyName (kebab-case)
|
||||
├─ Detect JIRA: "PM-12345" (matches [A-Z]{2,4}-\d+)
|
||||
├─ Extract feature: "Enable Password History Export"
|
||||
├─ Convert to lowercase: "enable password history export"
|
||||
├─ Replace spaces with hyphens: "enable-password-history-export"
|
||||
├─ Combine: "pm-12345" + "-" + "enable-password-history-export"
|
||||
└─ Result: "pm-12345-enable-password-history-export"
|
||||
|
||||
Step 2: Generate DataClassName (PascalCase)
|
||||
├─ Remove JIRA prefix: "enable-password-history-export"
|
||||
├─ Split on hyphens: ["enable", "password", "history", "export"]
|
||||
├─ Capitalize each: ["Enable", "Password", "History", "Export"]
|
||||
└─ Result: "EnablePasswordHistoryExport"
|
||||
|
||||
Step 3: Generate string_resource_key (snake_case)
|
||||
├─ Remove JIRA prefix: "enable-password-history-export"
|
||||
├─ Replace hyphens with underscores: "enable_password_history_export"
|
||||
└─ Result: "enable_password_history_export"
|
||||
```
|
||||
|
||||
## Reference Examples from Codebase
|
||||
|
||||
**Existing feature flags:**
|
||||
|
||||
| keyName | DataClassName | string_resource_key |
|
||||
|---------|---------------|---------------------|
|
||||
| `cxp-import-mobile` | `CredentialExchangeProtocolImport` | `cxp_import` |
|
||||
| `cipher-key-encryption` | `CipherKeyEncryption` | `cipher_key_encryption` |
|
||||
| `bitwarden-authentication-enabled` | `BitwardenAuthenticationEnabled` | `bitwarden_authentication_enabled` |
|
||||
| `pm-18021-force-update-kdf-settings` | `ForceUpdateKdfSettings` | `force_update_kdf_settings` |
|
||||
| `pm-23995-no-logout-on-kdf-change` | `NoLogoutOnKdfChange` | `avoid_logout_on_kdf_change` |
|
||||
@ -0,0 +1,341 @@
|
||||
# Feature Flag Creation Troubleshooting
|
||||
|
||||
Common issues, error messages, and solutions when creating feature flags.
|
||||
|
||||
## Compilation Errors
|
||||
|
||||
### "Unresolved reference: {DataClassName}"
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
e: file:///.../FeatureFlagListItems.kt:XX:XX Unresolved reference: {DataClassName}
|
||||
```
|
||||
|
||||
**Cause:** Data object not added to FlagKey.kt, or typo in data class name.
|
||||
|
||||
**Solution:**
|
||||
1. Verify data object exists in FlagKey.kt
|
||||
2. Check spelling matches exactly (case-sensitive)
|
||||
3. Ensure proper PascalCase formatting
|
||||
4. Clean and rebuild: `./gradlew clean :ui:compileDebugKotlin`
|
||||
|
||||
---
|
||||
|
||||
### "Unresolved reference: {string_resource_key}"
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
e: file:///.../FeatureFlagListItems.kt:XX:XX Unresolved reference: {string_resource_key}
|
||||
```
|
||||
|
||||
**Cause:** String resource not added to strings_non_localized.xml.
|
||||
|
||||
**Solution:**
|
||||
1. Open `ui/src/main/res/values/strings_non_localized.xml`
|
||||
2. Verify string resource exists within Debug Menu region
|
||||
3. Check spelling matches exactly (case-sensitive, snake_case)
|
||||
4. Clean and rebuild: `./gradlew clean :ui:compileDebugKotlin`
|
||||
|
||||
---
|
||||
|
||||
### "Duplicate case label"
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
e: file:///.../FeatureFlagListItems.kt:XX:XX Duplicate case label
|
||||
```
|
||||
|
||||
**Cause:** Flag added twice to the same when expression branch.
|
||||
|
||||
**Solution:**
|
||||
1. Search for `FlagKey.{DataClassName}` in FeatureFlagListItems.kt
|
||||
2. Remove duplicate entry
|
||||
3. Keep only one instance in each when expression
|
||||
|
||||
---
|
||||
|
||||
## Test Failures
|
||||
|
||||
### keyName Test Failure
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
Expected: "expected-key-name"
|
||||
Actual: "actual-key-name"
|
||||
```
|
||||
|
||||
**Cause:** Mismatch between keyName in FlagKey.kt and test assertion.
|
||||
|
||||
**Solution:**
|
||||
1. Open both FlagKey.kt and FlagKeyTest.kt
|
||||
2. Compare keyName values exactly
|
||||
3. Correct the mismatch (usually fix the test assertion)
|
||||
4. Ensure proper kebab-case formatting
|
||||
|
||||
---
|
||||
|
||||
### Default Value Test Failure
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
Expected: false
|
||||
Actual: true
|
||||
```
|
||||
|
||||
**Cause:** Flag's defaultValue doesn't match test expectation.
|
||||
|
||||
**Solution:**
|
||||
1. Verify defaultValue in FlagKey.kt
|
||||
2. For Boolean false: ensure flag is in the list in test
|
||||
3. For Boolean true: needs separate test assertion
|
||||
4. For Int/String: ensure separate test with correct expected value
|
||||
|
||||
---
|
||||
|
||||
### "No such file or directory: FlagKeyTest.kt"
|
||||
|
||||
**Cause:** File path incorrect or file doesn't exist.
|
||||
|
||||
**Solution:**
|
||||
- Correct path: `core/src/test/kotlin/com/bitwarden/core/data/manager/model/FlagKeyTest.kt`
|
||||
- If file missing, check core module structure
|
||||
|
||||
---
|
||||
|
||||
## Runtime Errors
|
||||
|
||||
### Flag Doesn't Appear in Debug Menu
|
||||
|
||||
**Possible Causes:**
|
||||
1. Flag not added to active flags list
|
||||
2. UI not updated in FeatureFlagListItems.kt
|
||||
3. String resource missing
|
||||
4. App not recompiled after changes
|
||||
5. Wrong application target - Password Manager flag won't show in Authenticator app
|
||||
|
||||
**Solution:**
|
||||
1. Verify flag in `activePasswordManagerFlags` or `activeAuthenticatorFlags`
|
||||
2. Check both when expressions in FeatureFlagListItems.kt are updated
|
||||
3. Verify string resource exists
|
||||
4. Clean and rebuild app: `./gradlew clean :app:assembleStandardDebug`
|
||||
5. Reinstall app on device/emulator
|
||||
6. Confirm testing the correct app (Password Manager vs Authenticator) matching flag's active list
|
||||
|
||||
---
|
||||
|
||||
### App Crashes When Accessing Flag
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
java.lang.IllegalStateException: Flag not found
|
||||
```
|
||||
|
||||
**Cause:** Flag not registered in active flags list.
|
||||
|
||||
**Solution:**
|
||||
Add flag to appropriate list in FlagKey.kt:
|
||||
```kotlin
|
||||
val activePasswordManagerFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
// ... existing flags ...
|
||||
{DataClassName}, // Add this
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Issues
|
||||
|
||||
### "Flag with keyName already exists"
|
||||
|
||||
**Cause:** Attempting to create flag with duplicate keyName.
|
||||
|
||||
**Solution:**
|
||||
1. Search FlagKey.kt for existing keyName
|
||||
2. Choose a different, unique keyName
|
||||
3. Consider if this is actually a duplicate feature request
|
||||
|
||||
---
|
||||
|
||||
### "Invalid keyName format"
|
||||
|
||||
**Cause:** keyName doesn't follow kebab-case convention.
|
||||
|
||||
**Common Issues:**
|
||||
- Uses underscores: `enable_totp_export` ❌ → `enable-totp-export` ✓
|
||||
- Uses camelCase: `enableTotpExport` ❌ → `enable-totp-export` ✓
|
||||
- Not lowercase: `Enable-TOTP-Export` ❌ → `enable-totp-export` ✓
|
||||
- Has spaces: `enable totp export` ❌ → `enable-totp-export` ✓
|
||||
|
||||
**Solution:**
|
||||
Follow kebab-case: lowercase words separated by hyphens.
|
||||
|
||||
---
|
||||
|
||||
### "DataClassName already exists"
|
||||
|
||||
**Cause:** Generated data class name conflicts with existing flag.
|
||||
|
||||
**Solution:**
|
||||
1. Search FlagKey.kt for existing data object names
|
||||
2. Adjust flag name to generate unique DataClassName
|
||||
3. Consider if features are actually related/duplicate
|
||||
|
||||
---
|
||||
|
||||
## Build Issues
|
||||
|
||||
### "Execution failed for task ':core:compileDebugKotlin'"
|
||||
|
||||
**Possible Causes:**
|
||||
- Syntax error in FlagKey.kt
|
||||
- Missing comma in active flags list
|
||||
- Incorrect type specification
|
||||
- Missing closing brace
|
||||
|
||||
**Solution:**
|
||||
1. Check syntax around new data object
|
||||
2. Verify comma after flag in active flags list
|
||||
3. Ensure proper Kotlin syntax (closing braces, proper override declarations)
|
||||
4. Review error output for specific line number
|
||||
|
||||
---
|
||||
|
||||
### "Cannot access class 'FlagKey'"
|
||||
|
||||
**Cause:** Import or module dependency issue.
|
||||
|
||||
**Solution:**
|
||||
1. Ensure `:ui` module depends on `:core` module
|
||||
2. Check imports in FeatureFlagListItems.kt
|
||||
3. Clean and rebuild all modules
|
||||
|
||||
---
|
||||
|
||||
## XML Errors
|
||||
|
||||
### "String resource name must start with a letter"
|
||||
|
||||
**Cause:** string_resource_key doesn't follow XML naming rules.
|
||||
|
||||
**Solution:**
|
||||
String resource keys must:
|
||||
- Start with a letter
|
||||
- Contain only `[a-z0-9_]`
|
||||
- Use snake_case convention
|
||||
|
||||
---
|
||||
|
||||
### "Duplicate resource name"
|
||||
|
||||
**Cause:** String resource with same name already exists.
|
||||
|
||||
**Solution:**
|
||||
1. Search strings_non_localized.xml for existing resource
|
||||
2. Choose different string resource key
|
||||
3. Update getDisplayLabel when expression with new key
|
||||
|
||||
---
|
||||
|
||||
## Alphabetical Ordering Issues
|
||||
|
||||
### CI/Review Comments About Ordering
|
||||
|
||||
**Issue:** Flags or strings not in alphabetical order.
|
||||
|
||||
**Solution:**
|
||||
|
||||
**Active Flags List:** Order by data class name
|
||||
```kotlin
|
||||
listOf(
|
||||
CipherKeyEncryption, // C
|
||||
CredentialExchangeProtocol, // C
|
||||
EnableTotpExport, // E
|
||||
ForceUpdateKdfSettings, // F
|
||||
)
|
||||
```
|
||||
|
||||
**When Expressions:** Order by data class name
|
||||
```kotlin
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.EnableTotpExport,
|
||||
```
|
||||
|
||||
**String Resources:** Order by resource name
|
||||
```xml
|
||||
<string name="cipher_key_encryption">Cipher Key Encryption</string>
|
||||
<string name="enable_totp_export">Enable TOTP Export</string>
|
||||
<string name="force_update_kdf_settings">Force update KDF settings</string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Int/String Flag Issues
|
||||
|
||||
### "BooleanFlagItem cannot be used for Int/String flags"
|
||||
|
||||
**Cause:** Int/String flags require custom UI components not yet implemented.
|
||||
|
||||
**Current Status:** Codebase only implements BooleanFlagItem for debug menu.
|
||||
|
||||
**Solution:**
|
||||
1. For now, skip UI integration (flag will work but not appear in debug menu)
|
||||
2. Add TODO comment in FeatureFlagListItems.kt
|
||||
3. Implement IntFlagItem or StringFlagItem composable if needed
|
||||
|
||||
**Example TODO:**
|
||||
```kotlin
|
||||
// TODO: Add UI support for {DataClassName} ({Type} flag)
|
||||
// Requires implementing {Type}FlagItem composable similar to BooleanFlagItem
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git/Merge Issues
|
||||
|
||||
### Merge Conflicts in FlagKey.kt
|
||||
|
||||
**Cause:** Multiple branches adding flags simultaneously.
|
||||
|
||||
**Solution:**
|
||||
1. Accept both changes
|
||||
2. Maintain alphabetical ordering in active flags list
|
||||
3. Ensure no duplicate data objects
|
||||
4. Run tests after resolving
|
||||
|
||||
---
|
||||
|
||||
### Merge Conflicts in FlagKeyTest.kt
|
||||
|
||||
**Cause:** Multiple branches adding test assertions simultaneously.
|
||||
|
||||
**Solution:**
|
||||
1. Accept both changes
|
||||
2. Maintain alphabetical ordering in assertions
|
||||
3. Add all flags to defaultValue test list
|
||||
4. Run tests to verify
|
||||
|
||||
---
|
||||
|
||||
## Prevention Checklist
|
||||
|
||||
Avoid common issues by verifying:
|
||||
|
||||
- [ ] keyName is unique and follows kebab-case
|
||||
- [ ] DataClassName is unique and follows PascalCase
|
||||
- [ ] string_resource_key is unique and follows snake_case
|
||||
- [ ] Flag added to appropriate active flags list
|
||||
- [ ] Comma added after flag in list
|
||||
- [ ] Both when expressions updated in FeatureFlagListItems.kt
|
||||
- [ ] String resource added to strings_non_localized.xml within Debug Menu region
|
||||
- [ ] Alphabetical ordering maintained in all locations
|
||||
- [ ] Tests updated (keyName assertion and defaultValue test)
|
||||
- [ ] All modules compile successfully
|
||||
- [ ] Tests pass
|
||||
|
||||
Run this verification command before committing:
|
||||
```bash
|
||||
./gradlew :core:testDebug :ui:compileDebugKotlin :app:compileStandardDebugKotlin
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user