Skip to content

Commit 862f833

Browse files
committed
refactor!: replace experiments with features configuration
BREAKING CHANGE: The configuration structure has changed from `experiments` to `features`. The new model uses an allow-list approach where features are enabled by default and can be disabled per network via `disabled_networks`, instead of the previous enabled/disabled toggle with explicit network lists. - Rename `experiments` YAML key to `features` - Replace `ExperimentSettings` struct with `FeatureSettings` - Change API response field from `experiments` to `features` - Update API response structure: `name`+`enabled`+`networks` becomes `path`+`disabled_networks` - Update tests and example configuration accordingly
1 parent 34a954d commit 862f833

File tree

5 files changed

+80
-79
lines changed

5 files changed

+80
-79
lines changed

config.example.yaml

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,17 @@ networks:
119119
# Note: Networks from cartographoor are automatically added with all metadata.
120120
# Only add entries here to disable, override, or add custom networks.
121121

122-
# Experiment feature flags
123-
# Controls which experiments are available per network
124-
# By default, experiments are enabled for all networks unless overridden
125-
experiments:
126-
live-slots:
127-
enabled: true
128-
networks: ["mainnet"] # Only enabled for mainnet
129-
130-
block-production:
131-
enabled: false # Disabled for all networks
132-
133-
attestation-performance:
134-
enabled: true # Enabled for all networks (no networks specified = default)
122+
# Feature flags
123+
# Controls which features are available per network
124+
# By default, features are enabled for all networks unless explicitly disabled
125+
features:
126+
# Example: Live slots feature - disable for all networks except mainnet
127+
- path: "/ethereum/live-slots"
128+
disabled_networks: ["sepolia", "holesky"]
129+
130+
# Example: Block production feature - disabled for all networks
131+
- path: "/ethereum/block-production"
132+
disabled_networks: [] # Empty array, but you'd list all networks to truly disable everywhere
133+
134+
# Example: Attestation performance - enabled for all networks (no disabled_networks specified)
135+
- path: "/ethereum/attestation-performance"

internal/api/config.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ var _ http.Handler = (*ConfigHandler)(nil)
1818

1919
// ConfigResponse is the JSON response for /api/v1/config.
2020
type ConfigResponse struct {
21-
Networks []NetworkInfo `json:"networks"`
22-
Experiments []Experiment `json:"experiments"`
21+
Networks []NetworkInfo `json:"networks"`
22+
Features []Feature `json:"features"`
2323
}
2424

2525
// NetworkInfo represents network metadata.
@@ -43,11 +43,11 @@ type Fork struct {
4343
MinClientVersions map[string]string `json:"min_client_versions"` // Map of client name to version
4444
}
4545

46-
// Experiment represents experiment configuration.
47-
type Experiment struct {
48-
Name string `json:"name"`
49-
Enabled bool `json:"enabled"`
50-
Networks []string `json:"networks"` // empty = all networks
46+
// Feature represents feature configuration.
47+
// Features are enabled by default for all networks unless explicitly disabled.
48+
type Feature struct {
49+
Path string `json:"path"`
50+
DisabledNetworks []string `json:"disabled_networks"`
5151
}
5252

5353
// ConfigHandler handles /api/v1/config requests.
@@ -98,8 +98,8 @@ func (h *ConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9898
// This ensures both endpoints use the same logic and return consistent data.
9999
func (h *ConfigHandler) GetConfigData(ctx context.Context) ConfigResponse {
100100
return ConfigResponse{
101-
Networks: h.buildNetworks(ctx),
102-
Experiments: h.buildExperiments(ctx),
101+
Networks: h.buildNetworks(ctx),
102+
Features: h.buildFeatures(ctx),
103103
}
104104
}
105105

@@ -173,28 +173,27 @@ func (h *ConfigHandler) buildNetworks(ctx context.Context) []NetworkInfo {
173173
return networks
174174
}
175175

176-
// buildExperiments converts config experiments map to API response array.
177-
func (h *ConfigHandler) buildExperiments(_ context.Context) []Experiment {
178-
experiments := make([]Experiment, 0, len(h.config.Experiments))
176+
// buildFeatures converts config features slice to API response array.
177+
func (h *ConfigHandler) buildFeatures(_ context.Context) []Feature {
178+
features := make([]Feature, 0, len(h.config.Features))
179179

180-
for name, settings := range h.config.Experiments {
181-
// Copy networks slice to avoid sharing underlying array
182-
networks := make([]string, len(settings.Networks))
183-
copy(networks, settings.Networks)
180+
for _, feature := range h.config.Features {
181+
// Copy disabled_networks slice to avoid sharing underlying array
182+
disabledNetworks := make([]string, len(feature.DisabledNetworks))
183+
copy(disabledNetworks, feature.DisabledNetworks)
184184

185-
experiments = append(experiments, Experiment{
186-
Name: name,
187-
Enabled: settings.Enabled,
188-
Networks: networks,
185+
features = append(features, Feature{
186+
Path: feature.Path,
187+
DisabledNetworks: disabledNetworks,
189188
})
190189
}
191190

192-
// Sort experiments alphabetically by name for deterministic ordering
193-
sort.Slice(experiments, func(i, j int) bool {
194-
return experiments[i].Name < experiments[j].Name
191+
// Sort features alphabetically by path for deterministic ordering
192+
sort.Slice(features, func(i, j int) bool {
193+
return features[i].Path < features[j].Path
195194
})
196195

197-
return experiments
196+
return features
198197
}
199198

200199
// transformForks converts cartographoor.Forks to API Forks format (for snake_case output).

internal/api/config_test.go

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
2424
method string
2525
cartoNetworks map[string]*cartographoor.Network
2626
configNetworks []config.NetworkConfig
27-
experiments map[string]config.ExperimentSettings
27+
features []config.FeatureSettings
2828
expectedStatus int
2929
validateResp func(t *testing.T, resp *ConfigResponse)
3030
}{
@@ -41,10 +41,10 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
4141
},
4242
},
4343
configNetworks: []config.NetworkConfig{},
44-
experiments: map[string]config.ExperimentSettings{
45-
"test-experiment": {
46-
Enabled: true,
47-
Networks: []string{"mainnet"},
44+
features: []config.FeatureSettings{
45+
{
46+
Path: "/ethereum/test-feature",
47+
DisabledNetworks: []string{"sepolia"},
4848
},
4949
},
5050
expectedStatus: http.StatusOK,
@@ -53,16 +53,16 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
5353

5454
require.NotNil(t, resp)
5555
assert.Len(t, resp.Networks, 1)
56-
assert.Len(t, resp.Experiments, 1)
56+
assert.Len(t, resp.Features, 1)
5757

5858
// Verify network data
5959
assert.Equal(t, "mainnet", resp.Networks[0].Name)
6060
assert.Equal(t, "Ethereum Mainnet", resp.Networks[0].DisplayName)
6161
assert.Equal(t, int64(1), resp.Networks[0].ChainID)
6262

63-
// Verify experiment data
64-
assert.Equal(t, "test-experiment", resp.Experiments[0].Name)
65-
assert.True(t, resp.Experiments[0].Enabled)
63+
// Verify feature data
64+
assert.Equal(t, "/ethereum/test-feature", resp.Features[0].Path)
65+
assert.Equal(t, []string{"sepolia"}, resp.Features[0].DisabledNetworks)
6666
},
6767
},
6868
{
@@ -91,7 +91,7 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
9191
Enabled: boolPtr(false),
9292
},
9393
},
94-
experiments: map[string]config.ExperimentSettings{},
94+
features: []config.FeatureSettings{},
9595
expectedStatus: http.StatusOK,
9696
validateResp: func(t *testing.T, resp *ConfigResponse) {
9797
t.Helper()
@@ -106,14 +106,14 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
106106
method: http.MethodGet,
107107
cartoNetworks: map[string]*cartographoor.Network{},
108108
configNetworks: []config.NetworkConfig{},
109-
experiments: map[string]config.ExperimentSettings{},
109+
features: []config.FeatureSettings{},
110110
expectedStatus: http.StatusOK,
111111
validateResp: func(t *testing.T, resp *ConfigResponse) {
112112
t.Helper()
113113

114114
require.NotNil(t, resp)
115115
assert.Empty(t, resp.Networks)
116-
assert.Empty(t, resp.Experiments)
116+
assert.Empty(t, resp.Features)
117117
},
118118
},
119119
}
@@ -148,8 +148,8 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
148148
}
149149

150150
cfg := &config.Config{
151-
Networks: tt.configNetworks,
152-
Experiments: tt.experiments,
151+
Networks: tt.configNetworks,
152+
Features: tt.features,
153153
}
154154

155155
logger := logrus.New()
@@ -313,25 +313,25 @@ func TestConfigHandler_buildNetworks(t *testing.T) {
313313
}
314314
}
315315

316-
func TestConfigHandler_buildExperiments(t *testing.T) {
316+
func TestConfigHandler_buildFeatures(t *testing.T) {
317317
tests := []struct {
318-
name string
319-
experiments map[string]config.ExperimentSettings
320-
expected int
318+
name string
319+
features []config.FeatureSettings
320+
expected int
321321
}{
322322
{
323-
name: "experiments sorted alphabetically",
324-
experiments: map[string]config.ExperimentSettings{
325-
"zebra": {Enabled: true},
326-
"alpha": {Enabled: false},
327-
"middle": {Enabled: true},
323+
name: "features sorted alphabetically by path",
324+
features: []config.FeatureSettings{
325+
{Path: "/zebra", DisabledNetworks: []string{}},
326+
{Path: "/alpha", DisabledNetworks: []string{"mainnet"}},
327+
{Path: "/middle", DisabledNetworks: []string{}},
328328
},
329329
expected: 3,
330330
},
331331
{
332-
name: "empty experiments",
333-
experiments: map[string]config.ExperimentSettings{},
334-
expected: 0,
332+
name: "empty features",
333+
features: []config.FeatureSettings{},
334+
expected: 0,
335335
},
336336
}
337337

@@ -343,22 +343,22 @@ func TestConfigHandler_buildExperiments(t *testing.T) {
343343
logger.SetOutput(io.Discard)
344344

345345
cfg := &config.Config{
346-
Experiments: tt.experiments,
346+
Features: tt.features,
347347
}
348348

349349
handler := &ConfigHandler{
350350
config: cfg,
351351
logger: logger,
352352
}
353353

354-
result := handler.buildExperiments(context.Background())
354+
result := handler.buildFeatures(context.Background())
355355

356356
assert.Len(t, result, tt.expected)
357357

358358
// Verify sorting
359359
for i := 1; i < len(result); i++ {
360-
assert.True(t, result[i-1].Name < result[i].Name,
361-
"experiments should be sorted alphabetically")
360+
assert.True(t, result[i-1].Path < result[i].Path,
361+
"features should be sorted alphabetically by path")
362362
}
363363
})
364364
}

internal/config/config.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ import (
1515

1616
// Config represents the complete application configuration.
1717
type Config struct {
18-
Server ServerConfig `yaml:"server"`
19-
Redis RedisConfig `yaml:"redis"`
20-
Leader LeaderConfig `yaml:"leader"`
21-
Networks []NetworkConfig `yaml:"networks"`
22-
Experiments map[string]ExperimentSettings `yaml:"experiments"`
23-
Cartographoor cartographoor.Config `yaml:"cartographoor"`
24-
Bounds BoundsConfig `yaml:"bounds"`
25-
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
18+
Server ServerConfig `yaml:"server"`
19+
Redis RedisConfig `yaml:"redis"`
20+
Leader LeaderConfig `yaml:"leader"`
21+
Networks []NetworkConfig `yaml:"networks"`
22+
Features []FeatureSettings `yaml:"features"`
23+
Cartographoor cartographoor.Config `yaml:"cartographoor"`
24+
Bounds BoundsConfig `yaml:"bounds"`
25+
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
2626
}
2727

2828
// ServerConfig contains HTTP server settings.

internal/config/networks.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ type NetworkConfig struct {
2323
GenesisDelay *int64 `yaml:"genesis_delay,omitempty"` // Optional: Genesis delay in seconds
2424
}
2525

26-
// ExperimentSettings defines settings for a single experiment.
27-
type ExperimentSettings struct {
28-
Enabled bool `yaml:"enabled"`
29-
Networks []string `yaml:"networks,omitempty"` // Empty/omitted = all networks
26+
// FeatureSettings defines settings for a single feature.
27+
// Features are enabled by default for all networks unless explicitly disabled.
28+
type FeatureSettings struct {
29+
Path string `yaml:"path"` // Feature path (e.g., "/ethereum/data-availability/das-custody")
30+
DisabledNetworks []string `yaml:"disabled_networks,omitempty"` // Networks where this feature is disabled
3031
}
3132

3233
// Validate validates a network configuration.

0 commit comments

Comments
 (0)