diff --git a/internal/agent/executor.go b/internal/agent/executor.go index bd85f0c8..500589e9 100644 --- a/internal/agent/executor.go +++ b/internal/agent/executor.go @@ -657,7 +657,12 @@ func toolDefinitions() map[ToolName]*ToolDefinition { }, "sampling_rate": { "type": "number", - "description": "Sampling rate (0.0 to 1.0, e.g., 0.1 for 10%)" + "description": "Base sampling rate (0.0 to 1.0, e.g., 0.1 for 10%)" + }, + "sampling_mode": { + "type": "string", + "description": "Sampling mode: 'adaptive' (adjusts under load) or 'fixed' (constant rate). Defaults to 'adaptive'.", + "enum": ["adaptive", "fixed"] }, "export_spans": { "type": "boolean", diff --git a/internal/agent/prompts/phase_cloud_configure_recording.md b/internal/agent/prompts/phase_cloud_configure_recording.md index 7e70bfeb..7072b310 100644 --- a/internal/agent/prompts/phase_cloud_configure_recording.md +++ b/internal/agent/prompts/phase_cloud_configure_recording.md @@ -4,17 +4,22 @@ Configure the recording parameters for Tusk Drift Cloud. ### Configuration Options -1. **Sampling Rate** (0.0 to 1.0): - - Percentage of requests to record +1. **Sampling Mode**: + - `adaptive` (default): Automatically adjusts sampling rate under load to reduce overhead + - `fixed`: Uses a constant sampling rate + +2. **Base Sampling Rate** (0.0 to 1.0): + - Base percentage of requests to record + - In adaptive mode, the SDK may temporarily reduce below this rate under pressure - Recommended: 0.1 (10%) for dev/staging, 0.01 (1%) for production - Default: 0.1 -2. **Export Spans** (boolean): +3. **Export Spans** (boolean): - Whether to upload trace data to Tusk Cloud - Required for cloud features - Default: true -3. **Record Environment Variables** (boolean): +4. **Record Environment Variables** (boolean): - Whether to record and replay environment variables - Recommended if app behavior depends on env vars - Default: false @@ -22,31 +27,36 @@ Configure the recording parameters for Tusk Drift Cloud. ### Steps 1. **Present defaults**: Tell the user the default configuration: - - Sampling rate: 0.1 (10%) + - Sampling mode: adaptive + - Base sampling rate: 0.1 (10%) - Export spans: true - Record env vars: false 2. **Ask for customization**: Use `ask_user` to ask if they want to customize: "The default recording configuration is: - - Sampling rate: 10% (0.1) + - Sampling mode: adaptive (automatically adjusts under load) + - Base sampling rate: 10% (0.1) - Export spans: enabled - Record environment variables: disabled Press Enter to accept defaults, or type 'custom' to customize:" 3. **If customizing**: Ask for each value: - - Sampling rate (number between 0.0 and 1.0) + - Sampling mode (adaptive or fixed) + - Base sampling rate (number between 0.0 and 1.0) - Export spans (yes/no) - Record env vars (yes/no) 4. **Save configuration**: Use `cloud_save_config` with: - `service_id`: from state.cloud_service_id - `sampling_rate`: the chosen rate + - `sampling_mode`: the chosen mode (adaptive or fixed) - `export_spans`: the chosen value - `enable_env_var_recording`: the chosen value 5. **Transition**: Move to the next phase with: - `sampling_rate`: the chosen rate + - `sampling_mode`: the chosen mode - `export_spans`: the chosen value - `enable_env_var_recording`: the chosen value @@ -61,6 +71,7 @@ Since cloud users fetch traces from Tusk Cloud rather than storing them locally, ### Important Notes -- Lower sampling rates reduce performance overhead +- Adaptive mode is recommended for most deployments as it automatically reduces sampling under load to minimize performance overhead +- Lower base sampling rates reduce performance overhead - Export spans must be true for cloud features to work - Environment variable recording is useful for apps that depend on env vars for business logic diff --git a/internal/agent/prompts/phase_create_config.md b/internal/agent/prompts/phase_create_config.md index fbd897a7..dcd9e76e 100644 --- a/internal/agent/prompts/phase_create_config.md +++ b/internal/agent/prompts/phase_create_config.md @@ -22,7 +22,9 @@ test_execution: timeout: 30s recording: - sampling_rate: 1.0 + sampling: + mode: adaptive + base_rate: 1.0 export_spans: false enable_env_var_recording: true ``` @@ -52,7 +54,9 @@ test_execution: timeout: 30s recording: - sampling_rate: 1.0 + sampling: + mode: adaptive + base_rate: 1.0 export_spans: false enable_env_var_recording: true ``` diff --git a/internal/agent/tools/cloud.go b/internal/agent/tools/cloud.go index 3889cca8..d3966d56 100644 --- a/internal/agent/tools/cloud.go +++ b/internal/agent/tools/cloud.go @@ -781,6 +781,7 @@ func (ct *CloudTools) SaveCloudConfig(input json.RawMessage) (string, error) { var params struct { ServiceID string `json:"service_id"` SamplingRate float64 `json:"sampling_rate"` + SamplingMode string `json:"sampling_mode"` ExportSpans bool `json:"export_spans"` EnableEnvVarRecording bool `json:"enable_env_var_recording"` } @@ -796,7 +797,7 @@ func (ct *CloudTools) SaveCloudConfig(input json.RawMessage) (string, error) { } // Save recording config (preserves other fields like exclude_paths, transforms) - if err := onboardcloud.SaveRecordingConfig(params.SamplingRate, params.ExportSpans, params.EnableEnvVarRecording); err != nil { + if err := onboardcloud.SaveRecordingConfig(params.SamplingRate, params.SamplingMode, params.ExportSpans, params.EnableEnvVarRecording); err != nil { return "", fmt.Errorf("failed to save recording config: %w", err) } diff --git a/internal/config/config.go b/internal/config/config.go index aea7cb83..70d48805 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -543,6 +543,12 @@ func getMinimalSchemaHint() string { interval: 1s` } +// FindConfigFile returns the path to the config file found by traversing +// upward from the current directory. Returns an empty string if not found. +func FindConfigFile() string { + return findConfigFile() +} + func findConfigFile() string { wd, err := os.Getwd() if err != nil { diff --git a/internal/tui/onboard-cloud/helpers.go b/internal/tui/onboard-cloud/helpers.go index ffcdab18..981f7740 100644 --- a/internal/tui/onboard-cloud/helpers.go +++ b/internal/tui/onboard-cloud/helpers.go @@ -13,6 +13,7 @@ import ( "github.com/Use-Tusk/tusk-cli/internal/cliconfig" "github.com/Use-Tusk/tusk-cli/internal/config" "github.com/Use-Tusk/tusk-cli/internal/utils" + "gopkg.in/yaml.v3" ) func listGitRemotes() (map[string]string, error) { @@ -195,6 +196,15 @@ func loadExistingConfig(m *Model) error { } m.ServiceID = cfg.Service.ID + // Use the config's sampling mode, but default to "adaptive" when the + // config file doesn't contain an explicit sampling.mode key (the config + // parser normalizes absent mode to "fixed" for backward compatibility, + // but the wizard should default new setups to "adaptive"). + if configHasExplicitSamplingMode() { + m.SamplingMode = cfg.Recording.Sampling.Mode + } else { + m.SamplingMode = "adaptive" + } m.SamplingRate = fmt.Sprintf("%.2f", cfg.Recording.SamplingRate) if cfg.Recording.ExportSpans != nil { @@ -488,3 +498,24 @@ func detectShellConfig() string { // Fallback to .profile return filepath.Join(homeDir, ".profile") } + +// configHasExplicitSamplingMode checks the raw config file for an explicit +// recording.sampling.mode key. Returns false if the key is absent or the +// file can't be read, so the caller can fall back to the wizard default. +func configHasExplicitSamplingMode() bool { + data, err := os.ReadFile(config.FindConfigFile()) + if err != nil { + return false + } + var raw struct { + Recording struct { + Sampling struct { + Mode string `yaml:"mode"` + } `yaml:"sampling"` + } `yaml:"recording"` + } + if err := yaml.Unmarshal(data, &raw); err != nil { + return false + } + return raw.Recording.Sampling.Mode != "" +} diff --git a/internal/tui/onboard-cloud/model.go b/internal/tui/onboard-cloud/model.go index 99d20505..652c5971 100644 --- a/internal/tui/onboard-cloud/model.go +++ b/internal/tui/onboard-cloud/model.go @@ -65,6 +65,7 @@ type Model struct { CreateApiKeyChoice bool // State - Recording Config + SamplingMode string SamplingRate string ExportSpans bool EnableEnvVarRecording bool diff --git a/internal/tui/onboard-cloud/recording_config_table.go b/internal/tui/onboard-cloud/recording_config_table.go index b03ecc64..c93451a2 100644 --- a/internal/tui/onboard-cloud/recording_config_table.go +++ b/internal/tui/onboard-cloud/recording_config_table.go @@ -12,6 +12,7 @@ import ( type RecordingConfigTable struct { table table.Model + samplingMode string samplingRate string exportSpans bool enableEnvVarRecording bool @@ -20,7 +21,7 @@ type RecordingConfigTable struct { cursor int } -func NewRecordingConfigTable(samplingRate string, exportSpans, enableEnvVarRecording bool) *RecordingConfigTable { +func NewRecordingConfigTable(samplingMode, samplingRate string, exportSpans, enableEnvVarRecording bool) *RecordingConfigTable { columns := []table.Column{ {Title: "Setting", Width: 35}, {Title: "Value", Width: 25}, @@ -48,8 +49,12 @@ func NewRecordingConfigTable(samplingRate string, exportSpans, enableEnvVarRecor t.SetStyles(s) + if samplingMode == "" { + samplingMode = "adaptive" + } rct := &RecordingConfigTable{ table: t, + samplingMode: samplingMode, samplingRate: samplingRate, exportSpans: true, // Required for cloud onboarding enableEnvVarRecording: enableEnvVarRecording, @@ -64,7 +69,7 @@ func NewRecordingConfigTable(samplingRate string, exportSpans, enableEnvVarRecor func (rct *RecordingConfigTable) updateRows() { rate, _ := strconv.ParseFloat(rct.samplingRate, 64) rateDisplay := fmt.Sprintf("%.2f (%.0f%%)", rate, rate*100) - if rct.EditMode && rct.cursor == 0 { + if rct.EditMode && rct.cursor == 1 { rateDisplay = "→ " + rct.samplingRate + "_" } @@ -76,7 +81,8 @@ func (rct *RecordingConfigTable) updateRows() { } rows := []table.Row{ - {"Sampling Rate", rateDisplay}, + {"Sampling Mode", rct.samplingMode}, + {"Base Sampling Rate", rateDisplay}, {"Export Spans", formatBool(rct.exportSpans)}, {"Record Environment Variables", formatBool(rct.enableEnvVarRecording)}, } @@ -92,7 +98,7 @@ func (rct *RecordingConfigTable) Update(msg tea.Msg) (*RecordingConfigTable, tea switch msg := msg.(type) { case tea.KeyMsg: // If in edit mode (typing sampling rate) - if rct.EditMode && rct.cursor == 0 { + if rct.EditMode && rct.cursor == 1 { switch msg.String() { case "tab", "esc": rct.EditMode = false @@ -127,7 +133,7 @@ func (rct *RecordingConfigTable) Update(msg tea.Msg) (*RecordingConfigTable, tea return rct, nil case "down", "j": - if rct.cursor < 2 { + if rct.cursor < 3 { rct.cursor++ rct.table.MoveDown(1) } @@ -136,18 +142,24 @@ func (rct *RecordingConfigTable) Update(msg tea.Msg) (*RecordingConfigTable, tea case "tab", " ": switch rct.cursor { + case 0: + if rct.samplingMode == "adaptive" { + rct.samplingMode = "fixed" + } else { + rct.samplingMode = "adaptive" + } case 1: - rct.exportSpans = !rct.exportSpans + rct.EditMode = true case 2: + rct.exportSpans = !rct.exportSpans + case 3: rct.enableEnvVarRecording = !rct.enableEnvVarRecording - case 0: - rct.EditMode = true } rct.updateRows() return rct, nil case "e": - if rct.cursor == 0 { + if rct.cursor == 1 { rct.EditMode = true rct.updateRows() return rct, nil @@ -169,9 +181,9 @@ func (rct *RecordingConfigTable) View() string { ) } -func (rct *RecordingConfigTable) GetValues() (samplingRate float64, exportSpans, enableEnvVarRecording bool) { +func (rct *RecordingConfigTable) GetValues() (samplingMode string, samplingRate float64, exportSpans, enableEnvVarRecording bool) { rate, _ := strconv.ParseFloat(rct.samplingRate, 64) - return rate, rct.exportSpans, rct.enableEnvVarRecording + return rct.samplingMode, rate, rct.exportSpans, rct.enableEnvVarRecording } func (rct *RecordingConfigTable) SetFocused(focused bool) { diff --git a/internal/tui/onboard-cloud/save.go b/internal/tui/onboard-cloud/save.go index d2ffe894..e201d073 100644 --- a/internal/tui/onboard-cloud/save.go +++ b/internal/tui/onboard-cloud/save.go @@ -212,13 +212,23 @@ func SaveServiceIDToConfig(serviceID string) error { // SaveRecordingConfig saves recording settings to .tusk/config.yaml. // Uses yaml.Node parsing to preserve file structure, comments, and unknown fields // (e.g., exclude_paths, transforms that the user may have configured). -func SaveRecordingConfig(samplingRate float64, exportSpans, enableEnvVarRecording bool) error { +func SaveRecordingConfig(samplingRate float64, samplingMode string, exportSpans, enableEnvVarRecording bool) error { + if samplingMode == "" { + samplingMode = "adaptive" + } else if samplingMode != "adaptive" && samplingMode != "fixed" { + return fmt.Errorf("invalid sampling mode %q: must be 'adaptive' or 'fixed'", samplingMode) + } return saveToConfig(func(cfg *config.Config, u *ConfigUpdater) error { cfg.Recording.SamplingRate = samplingRate + cfg.Recording.Sampling.Mode = samplingMode + baseRate := samplingRate + cfg.Recording.Sampling.BaseRate = &baseRate cfg.Recording.ExportSpans = &exportSpans cfg.Recording.EnableEnvVarRecording = &enableEnvVarRecording u.Set([]string{"recording", "sampling_rate"}, samplingRate) + u.Set([]string{"recording", "sampling", "mode"}, samplingMode) + u.Set([]string{"recording", "sampling", "base_rate"}, samplingRate) u.Set([]string{"recording", "export_spans"}, exportSpans) u.Set([]string{"recording", "enable_env_var_recording"}, enableEnvVarRecording) return nil diff --git a/internal/tui/onboard-cloud/save_test.go b/internal/tui/onboard-cloud/save_test.go index c5a8ecb4..5b51f8de 100644 --- a/internal/tui/onboard-cloud/save_test.go +++ b/internal/tui/onboard-cloud/save_test.go @@ -163,7 +163,7 @@ recording: require.NoError(t, err) // Save new recording config - err = SaveRecordingConfig(1.0, true, true) + err = SaveRecordingConfig(1.0, "adaptive", true, true) require.NoError(t, err) // Read the file back @@ -172,12 +172,95 @@ recording: result := string(data) assert.Contains(t, result, "sampling_rate: 1") + assert.Contains(t, result, "mode: adaptive") + assert.Contains(t, result, "base_rate: 1") assert.Contains(t, result, "export_spans: true") assert.Contains(t, result, "enable_env_var_recording: true") assert.NotContains(t, result, "!!float") assert.NotContains(t, result, "!!bool") } +func TestSaveRecordingConfig_FixedMode(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "tusk-test-*") + require.NoError(t, err) + t.Cleanup(func() { + _ = os.RemoveAll(tmpDir) + config.Invalidate() + }) + + originalWd, err := os.Getwd() + require.NoError(t, err) + t.Cleanup(func() { _ = os.Chdir(originalWd) }) + + err = os.Chdir(tmpDir) + require.NoError(t, err) + + tuskDir := filepath.Join(tmpDir, ".tusk") + err = os.MkdirAll(tuskDir, 0o750) + require.NoError(t, err) + + initialConfig := `service: + name: test-service + +recording: + sampling_rate: 0.5 + export_spans: false + enable_env_var_recording: false +` + err = os.WriteFile(filepath.Join(tuskDir, "config.yaml"), []byte(initialConfig), 0o600) + require.NoError(t, err) + + err = SaveRecordingConfig(0.5, "fixed", false, false) + require.NoError(t, err) + + data, err := os.ReadFile(filepath.Join(tuskDir, "config.yaml")) // #nosec G304 + require.NoError(t, err) + + result := string(data) + assert.Contains(t, result, "mode: fixed") + assert.Contains(t, result, "base_rate: 0.5") + assert.Contains(t, result, "sampling_rate: 0.5") +} + +func TestSaveRecordingConfig_DefaultsToAdaptive(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "tusk-test-*") + require.NoError(t, err) + t.Cleanup(func() { + _ = os.RemoveAll(tmpDir) + config.Invalidate() + }) + + originalWd, err := os.Getwd() + require.NoError(t, err) + t.Cleanup(func() { _ = os.Chdir(originalWd) }) + + err = os.Chdir(tmpDir) + require.NoError(t, err) + + tuskDir := filepath.Join(tmpDir, ".tusk") + err = os.MkdirAll(tuskDir, 0o750) + require.NoError(t, err) + + initialConfig := `service: + name: test-service + +recording: + sampling_rate: 1 +` + err = os.WriteFile(filepath.Join(tuskDir, "config.yaml"), []byte(initialConfig), 0o600) + require.NoError(t, err) + + // Empty string should default to adaptive + err = SaveRecordingConfig(1.0, "", true, false) + require.NoError(t, err) + + data, err := os.ReadFile(filepath.Join(tuskDir, "config.yaml")) // #nosec G304 + require.NoError(t, err) + + result := string(data) + assert.Contains(t, result, "mode: adaptive") +} + // Helper function to test updateField without file I/O func updateYAMLString(t *testing.T, input string, path []string, value any) string { t.Helper() diff --git a/internal/tui/onboard-cloud/steps.go b/internal/tui/onboard-cloud/steps.go index d4f025af..f73ec5f7 100644 --- a/internal/tui/onboard-cloud/steps.go +++ b/internal/tui/onboard-cloud/steps.go @@ -508,9 +508,9 @@ func (RecordingConfigStep) Heading(*Model) string { return "Configure recording func (RecordingConfigStep) Description(m *Model) string { return `Configure how Tusk records execution traces from your application: -• Sampling Rate: Percentage of requests to record (0.01 = 1%, 0.1 = 10%) - Lower rates reduce performance overhead. We recommend starting 10% for - dev/staging, and 1% for production environments. +• Sampling Rate: Base percentage of requests to record (0.01 = 1%, 0.1 = 10%) + Adaptive sampling mode is used by default, which automatically adjusts the rate + under load. We recommend starting at 10% for dev/staging, and 1% for production. • Export Spans: Upload trace data to Tusk Drift Cloud (required for cloud features) Disable only if using Tusk Drift locally without cloud integration. @@ -526,7 +526,7 @@ func (RecordingConfigStep) Help(m *Model) string { if m.RecordingConfigTable != nil && m.RecordingConfigTable.EditMode { return "Type sampling rate (0.0-1.0) • tab/esc: done editing" } - return "↑↓: navigate • tab/space: toggle/edit • enter: save" + return "↑↓: navigate • tab/space: toggle/edit value • enter: save" } func (RecordingConfigStep) Clear(m *Model) { @@ -560,8 +560,13 @@ func (ReviewStep) Description(m *Model) string { } summary.WriteString("⚙️ Recording Configuration\n") + samplingMode := m.SamplingMode + if samplingMode == "" { + samplingMode = "adaptive" + } + summary.WriteString(fmt.Sprintf(" • Sampling mode: %s\n", samplingMode)) samplingRate, _ := strconv.ParseFloat(m.SamplingRate, 64) - summary.WriteString(fmt.Sprintf(" • Sampling rate: %.2f (%.0f%% of requests)\n", samplingRate, samplingRate*100)) + summary.WriteString(fmt.Sprintf(" • Base sampling rate: %.2f (%.0f%% of requests)\n", samplingRate, samplingRate*100)) summary.WriteString(fmt.Sprintf(" • Export spans: %t\n", m.ExportSpans)) summary.WriteString(fmt.Sprintf(" • Record environment variables: %t\n\n", m.EnableEnvVarRecording)) diff --git a/internal/tui/onboard-cloud/update.go b/internal/tui/onboard-cloud/update.go index 630d7564..58323c42 100644 --- a/internal/tui/onboard-cloud/update.go +++ b/internal/tui/onboard-cloud/update.go @@ -111,6 +111,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if step.ID() == stepRecordingConfig { // Initialize RecordingConfigTable when going BACK to that step m.RecordingConfigTable = NewRecordingConfigTable( + m.SamplingMode, m.SamplingRate, m.ExportSpans, m.EnableEnvVarRecording, @@ -132,12 +133,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Only handle Enter for saving if not in edit mode if msg.String() == "enter" && !m.RecordingConfigTable.EditMode { - samplingRate, exportSpans, enableEnvVarRecording := m.RecordingConfigTable.GetValues() + samplingMode, samplingRate, exportSpans, enableEnvVarRecording := m.RecordingConfigTable.GetValues() + m.SamplingMode = samplingMode m.SamplingRate = fmt.Sprintf("%.2f", samplingRate) m.ExportSpans = exportSpans m.EnableEnvVarRecording = enableEnvVarRecording - if err := SaveRecordingConfig(samplingRate, exportSpans, enableEnvVarRecording); err != nil { + if err := SaveRecordingConfig(samplingRate, samplingMode, exportSpans, enableEnvVarRecording); err != nil { m.Err = fmt.Errorf("failed to save: %w", err) } else { return m, func() tea.Msg { return stepCompleteMsg{} } @@ -190,6 +192,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // always recreate the table if step.ID() == stepRecordingConfig { m.RecordingConfigTable = NewRecordingConfigTable( + m.SamplingMode, m.SamplingRate, m.ExportSpans, m.EnableEnvVarRecording, diff --git a/internal/tui/onboard/config.go b/internal/tui/onboard/config.go index 6697fb33..39646ab7 100644 --- a/internal/tui/onboard/config.go +++ b/internal/tui/onboard/config.go @@ -57,10 +57,15 @@ type TestExecution struct { Timeout string `yaml:"timeout"` } +type RecordingSampling struct { + Mode string `yaml:"mode"` + BaseRate float64 `yaml:"base_rate"` +} + type Recording struct { - SamplingRate float64 `yaml:"sampling_rate"` - ExportSpans bool `yaml:"export_spans"` - EnableEnvVarRecording bool `yaml:"enable_env_var_recording"` + Sampling RecordingSampling `yaml:"sampling"` + ExportSpans bool `yaml:"export_spans"` + EnableEnvVarRecording bool `yaml:"enable_env_var_recording"` } type Traces struct { @@ -106,7 +111,10 @@ func (m *Model) getCurrentConfig() Config { Timeout: "30s", }, Recording: Recording{ - SamplingRate: samplingRate, + Sampling: RecordingSampling{ + Mode: "adaptive", + BaseRate: samplingRate, + }, ExportSpans: false, EnableEnvVarRecording: true, }, diff --git a/internal/tui/onboard/config_test.go b/internal/tui/onboard/config_test.go new file mode 100644 index 00000000..1d40e93c --- /dev/null +++ b/internal/tui/onboard/config_test.go @@ -0,0 +1,65 @@ +package onboard + +import ( + "strings" + "testing" + + "github.com/charmbracelet/bubbles/textinput" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestGetCurrentConfig_UsesAdaptiveSamplingMode(t *testing.T) { + inputs := make([]textinput.Model, 1) + inputs[0] = textinput.New() + + m := &Model{ + ServiceName: "test-service", + ServicePort: "3000", + StartCmd: "npm start", + ReadinessCmd: "curl http://localhost:3000/health", + ReadinessTimeout: "30s", + ReadinessInterval: "1s", + SamplingRate: "1.0", + inputs: inputs, + } + + cfg := m.getCurrentConfig() + + assert.Equal(t, "adaptive", cfg.Recording.Sampling.Mode) + assert.Equal(t, 1.0, cfg.Recording.Sampling.BaseRate) + + // Verify YAML output uses nested sampling config + var buf strings.Builder + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + require.NoError(t, enc.Encode(cfg)) + _ = enc.Close() + yamlStr := buf.String() + + assert.Contains(t, yamlStr, "mode: adaptive") + assert.Contains(t, yamlStr, "base_rate: 1") + assert.NotContains(t, yamlStr, "sampling_rate:") +} + +func TestGetCurrentConfig_CustomSamplingRate(t *testing.T) { + inputs := make([]textinput.Model, 1) + inputs[0] = textinput.New() + + m := &Model{ + ServiceName: "test-service", + ServicePort: "8080", + StartCmd: "python app.py", + ReadinessCmd: "curl http://localhost:8080/health", + ReadinessTimeout: "30s", + ReadinessInterval: "1s", + SamplingRate: "0.1", + inputs: inputs, + } + + cfg := m.getCurrentConfig() + + assert.Equal(t, "adaptive", cfg.Recording.Sampling.Mode) + assert.Equal(t, 0.1, cfg.Recording.Sampling.BaseRate) +}