Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion internal/agent/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 19 additions & 8 deletions internal/agent/prompts/phase_cloud_configure_recording.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,59 @@ 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

### 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)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
- 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

Expand All @@ -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
8 changes: 6 additions & 2 deletions internal/agent/prompts/phase_create_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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
```
Expand Down
3 changes: 2 additions & 1 deletion internal/agent/tools/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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)
}

Expand Down
4 changes: 4 additions & 0 deletions internal/tui/onboard-cloud/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ func loadExistingConfig(m *Model) error {
}

m.ServiceID = cfg.Service.ID
m.SamplingMode = cfg.Recording.Sampling.Mode
if m.SamplingMode == "" {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
m.SamplingMode = "adaptive"
}
Comment thread
jy-tan marked this conversation as resolved.
m.SamplingRate = fmt.Sprintf("%.2f", cfg.Recording.SamplingRate)

if cfg.Recording.ExportSpans != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/tui/onboard-cloud/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type Model struct {
CreateApiKeyChoice bool

// State - Recording Config
SamplingMode string
SamplingRate string
ExportSpans bool
EnableEnvVarRecording bool
Expand Down
34 changes: 23 additions & 11 deletions internal/tui/onboard-cloud/recording_config_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

type RecordingConfigTable struct {
table table.Model
samplingMode string
samplingRate string
exportSpans bool
enableEnvVarRecording bool
Expand All @@ -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},
Expand Down Expand Up @@ -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,
Expand All @@ -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 + "_"
}

Expand All @@ -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)},
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand All @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion internal/tui/onboard-cloud/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,21 @@ 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"
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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
Expand Down
85 changes: 84 additions & 1 deletion internal/tui/onboard-cloud/save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Loading
Loading