Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions internal/tui/onboard-cloud/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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"
}
Comment thread
jy-tan marked this conversation as resolved.
m.SamplingRate = fmt.Sprintf("%.2f", cfg.Recording.SamplingRate)

if cfg.Recording.ExportSpans != nil {
Expand Down Expand Up @@ -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 != ""
}
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
12 changes: 11 additions & 1 deletion internal/tui/onboard-cloud/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
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
Loading
Loading