feat: migrate experiments config from array to bools#398
feat: migrate experiments config from array to bools#398
Conversation
Change the experiments field in SystemConfig, ProjectConfig, and Config
from []experiment.Experiment to map[string]bool. This enables dot-notation
access for the upcoming config command and allows explicit enable/disable
of experiments.
Includes backwards-compatible JSON unmarshaling that auto-converts the old
array format (["charm"]) to the new map format ({"charm": true}).
Updates help output to show all available experiments with ENABLED/DISABLED
status for better discoverability.
Reverts the EXPERIMENTS section changes from the experiments migration commit to keep PR #1 focused on the array-to-map refactor. Help menu improvements will be done in a separate PR.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #398 +/- ##
==========================================
+ Coverage 67.87% 67.91% +0.03%
==========================================
Files 218 217 -1
Lines 18050 18044 -6
==========================================
+ Hits 12252 12254 +2
+ Misses 4642 4637 -5
+ Partials 1156 1153 -3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
zimeg
left a comment
There was a problem hiding this comment.
@mwbrooks LGTM! This is an amazing change for improved configurations overall - too often I changed "bolt" to "bolts" for confident testing ⚡ 🏁
I'm leaving a few questions related to the order of detecting experiments as well as question about typings. I also don't think we should preserve backward compatibilities for experiments since that adds meaningful logic otherwise but no blockers 🎆
|
|
||
| // Feature experiments | ||
| ExperimentsFlag []string | ||
| experiments []experiment.Experiment |
There was a problem hiding this comment.
🪬 question: Can we continue to use strict types of experiment or is this not allowed as a key?
experiments map[experiment.Experiment]boolThere was a problem hiding this comment.
@zimeg Great suggestion and I should have caught that myself. I think we can use map[experiment.Experiment]bool, so let me look into refactoring it 💻
There was a problem hiding this comment.
Commit a70d3ee uses the strict types:
experiments map[experiment.Experiment]boolA helper method had to be created to copy from the JSON format to the strict type.
internal/config/experiments.go
Outdated
| // Load from flags | ||
| for _, flagValue := range c.ExperimentsFlag { | ||
| experiments = append(experiments, experiment.Experiment(flagValue)) | ||
| experiments[flagValue] = true | ||
| } |
There was a problem hiding this comment.
👁️🗨️ thought: The order of experiment parsing might need to be reversed so that project and user configurations can be overridden with a flag if the lower settings use a "false" value?
There was a problem hiding this comment.
@zimeg 🙇🏻 Very good catch! Commit f0c8bc3 updates the order to be:
- Permanently enabled
- System config
- Project config
- Flags
question: Not sure where Permanently enabled should be. I feel we would be better served to rename it to "Enabled by Default" and allow developers to opt-out in case there is a known bug. If we keep it as Permanently enabled, then we may want it to be after Flags to override all other settings.
| jsonContents := []byte(fmt.Sprintf("{\"last_update_checked_at\":\"2023-05-11T15:41:07.799619-07:00\",\"experiments\":[\"%s\"]}", validExperiment)) | ||
| jsonContents := []byte(fmt.Sprintf(`{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":{"%s":true}}`, validExperiment)) |
There was a problem hiding this comment.
⭐ praise: Thanks for improving both the string formatting and experiments formatting here!
internal/config/experiments_test.go
Outdated
| t.Run("Correctly finds experiments from old array format in config.json", func(t *testing.T) { | ||
| // Setup | ||
| ctx, fs, _, config, pathToConfigJSON, teardown := setup(t) | ||
| defer teardown(t) | ||
| _, mockPrintDebug := setupMockPrintDebug() | ||
|
|
||
| // Write old array format | ||
| jsonContents := []byte( | ||
| fmt.Sprintf( | ||
| `{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":["%s"]}`, | ||
| validExperiment, | ||
| ), | ||
| ) | ||
| _ = afero.WriteFile(fs, pathToConfigJSON, jsonContents, 0600) | ||
|
|
||
| config.LoadExperiments(ctx, mockPrintDebug) | ||
| experimentOn := config.WithExperimentOn(validExperiment) | ||
| assert.Equal(t, true, experimentOn) | ||
| }) | ||
|
|
There was a problem hiding this comment.
| t.Run("Correctly finds experiments from old array format in config.json", func(t *testing.T) { | |
| // Setup | |
| ctx, fs, _, config, pathToConfigJSON, teardown := setup(t) | |
| defer teardown(t) | |
| _, mockPrintDebug := setupMockPrintDebug() | |
| // Write old array format | |
| jsonContents := []byte( | |
| fmt.Sprintf( | |
| `{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":["%s"]}`, | |
| validExperiment, | |
| ), | |
| ) | |
| _ = afero.WriteFile(fs, pathToConfigJSON, jsonContents, 0600) | |
| config.LoadExperiments(ctx, mockPrintDebug) | |
| experimentOn := config.WithExperimentOn(validExperiment) | |
| assert.Equal(t, true, experimentOn) | |
| }) |
🪓 quibble(non-blocking): We might not fear breaking experimental configuration setups since this can become burdensome to keep in mind ongoing?
There was a problem hiding this comment.
thought: Good question. Here's what I've found from a little searching:
- No documentation exists for configuring experiments with
config.json. - No sample apps use experiments.
So, if a developer has an experiment enabled in the config.json then it's at their own risk.
My concern only concern is that removing the backwards compatibility will cause apps with config.json experiments to break on any CLI command:
Failed to parse contents of project-level config file (unable_to_parse_json)
json: cannot unmarshal array into Go struct field ProjectConfig.experiments of type map[string]boolThis is a very poor experience, but we can be good hosts by displaying a remediation message:
💡 Suggestion
Check that ~/path/to/my-app/.slack/config.json is valid JSON. If the "experiments" field is an array (e.g. ["exp1"]), update it to an object (e.g. {"exp1": true})Commit c500bde does the following:
- Removes
internal/config/experiments_unmarshal.go - Removed
UnmarshalJSONfrominternal/config/project.go - Removed
UnmarshalJSONfrominternal/config/system.go - Displays the remediation hint about "experiments" format when there is a JSON parse error for
config.json
mwbrooks
left a comment
There was a problem hiding this comment.
Whoops, like a dummy I forgot to publish my comments for the reviewers 🤦🏻
|
|
||
| // Write our test script to the memory file system used by the mock | ||
| jsonContents := []byte(fmt.Sprintf("{\"last_update_checked_at\":\"2023-05-11T15:41:07.799619-07:00\",\"experiments\":[\"%s\"]}", validExperiment)) | ||
| jsonContents := []byte(fmt.Sprintf(`{"last_update_checked_at":"2023-05-11T15:41:07.799619-07:00","experiments":{"%s":true}}`, validExperiment)) |
There was a problem hiding this comment.
note: Just updating the formatting to make the tests more readable with literals and no escapes.
| assert.Contains(t, mockOutput.String(), "active project experiments: []") | ||
| assert.Contains(t, mockOutput.String(), fmt.Sprintf("active permanently enabled experiments: [%s]", experiment.Placeholder)) | ||
| assert.Equal(t, []experiment.Experiment{experiment.Placeholder}, config.experiments) | ||
| assert.ElementsMatch(t, []experiment.Experiment{experiment.Placeholder}, config.GetExperiments()) |
There was a problem hiding this comment.
note: Ordering now varies because it's a map instead of slice.
Use the stricter experiment.Experiment type instead of string for the internal Config.experiments map key to improve type safety.
Reorder experiment source precedence so that higher-priority sources override lower ones: permanently enabled < system config < project config < flags.
Remove custom UnmarshalJSON methods and the unmarshalExperimentsField
helper that supported the deprecated array format for experiments in
config.json. The old format (["exp1"]) now produces a parse error with
a remediation hint to update to the object format ({"exp1": true}).
Changelog
Summary
This pull request migrates the experiments config format from an array to a map of boolean values. It includes backwards-compatible JSON unmarshaling that will auto-migrate
config.jsonfiles.Example
Before:
{ "experiments": ["sandboxes"], "manifest": { "source": "local" }, "project_id": "..." }After:
{ "experiments": { "sandboxes": true, "charm" :false }, "manifest": { "source": "local" }, "project_id": "..." }Motivation
Why on earth are you doing this? 👨🏻🎨 🎨 🚲 🏠
The motivation is to unblock adding a
slack configcommand to get and set system-level and project-level config files.The config command will use a dot-notation to get and set key values:
When the
<value>is a simple type, it works well, such as string, number, or booleanWhen the
<value>is a complex type, it's not intuitive or simple, such as arraysIn order for setting
[value]to be simple and intuitive, we should restrict theconfig.jsonfiles to use simple value types that can be expressed on the command-line (e.g. string, number, and boolean). Arrays are complex and error-prone to express (["value1", "value2"]orvalue1, value2) and all elements must be included each time.This is why this pull request migrates the experiments to an object of
key: booleanpairs.An added bonus is that the new format naturally supports disabling experiments from the config file. 🧠
Requirements