Skip to content

Commit 9d9ff85

Browse files
feat(drift): use adaptive sampling rate by default (#229)
Co-authored-by: JY Tan <jy8230@gmail.com>
1 parent aa6257e commit 9d9ff85

14 files changed

Lines changed: 281 additions & 36 deletions

File tree

internal/agent/executor.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,12 @@ func toolDefinitions() map[ToolName]*ToolDefinition {
657657
},
658658
"sampling_rate": {
659659
"type": "number",
660-
"description": "Sampling rate (0.0 to 1.0, e.g., 0.1 for 10%)"
660+
"description": "Base sampling rate (0.0 to 1.0, e.g., 0.1 for 10%)"
661+
},
662+
"sampling_mode": {
663+
"type": "string",
664+
"description": "Sampling mode: 'adaptive' (adjusts under load) or 'fixed' (constant rate). Defaults to 'adaptive'.",
665+
"enum": ["adaptive", "fixed"]
661666
},
662667
"export_spans": {
663668
"type": "boolean",

internal/agent/prompts/phase_cloud_configure_recording.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,59 @@ Configure the recording parameters for Tusk Drift Cloud.
44

55
### Configuration Options
66

7-
1. **Sampling Rate** (0.0 to 1.0):
8-
- Percentage of requests to record
7+
1. **Sampling Mode**:
8+
- `adaptive` (default): Automatically adjusts sampling rate under load to reduce overhead
9+
- `fixed`: Uses a constant sampling rate
10+
11+
2. **Base Sampling Rate** (0.0 to 1.0):
12+
- Base percentage of requests to record
13+
- In adaptive mode, the SDK may temporarily reduce below this rate under pressure
914
- Recommended: 0.1 (10%) for dev/staging, 0.01 (1%) for production
1015
- Default: 0.1
1116

12-
2. **Export Spans** (boolean):
17+
3. **Export Spans** (boolean):
1318
- Whether to upload trace data to Tusk Cloud
1419
- Required for cloud features
1520
- Default: true
1621

17-
3. **Record Environment Variables** (boolean):
22+
4. **Record Environment Variables** (boolean):
1823
- Whether to record and replay environment variables
1924
- Recommended if app behavior depends on env vars
2025
- Default: false
2126

2227
### Steps
2328

2429
1. **Present defaults**: Tell the user the default configuration:
25-
- Sampling rate: 0.1 (10%)
30+
- Sampling mode: adaptive
31+
- Base sampling rate: 0.1 (10%)
2632
- Export spans: true
2733
- Record env vars: false
2834

2935
2. **Ask for customization**: Use `ask_user` to ask if they want to customize:
3036
"The default recording configuration is:
31-
- Sampling rate: 10% (0.1)
37+
- Sampling mode: adaptive (automatically adjusts under load)
38+
- Base sampling rate: 10% (0.1)
3239
- Export spans: enabled
3340
- Record environment variables: disabled
3441

3542
Press Enter to accept defaults, or type 'custom' to customize:"
3643

3744
3. **If customizing**: Ask for each value:
38-
- Sampling rate (number between 0.0 and 1.0)
45+
- Sampling mode (adaptive or fixed)
46+
- Base sampling rate (number between 0.0 and 1.0)
3947
- Export spans (yes/no)
4048
- Record env vars (yes/no)
4149

4250
4. **Save configuration**: Use `cloud_save_config` with:
4351
- `service_id`: from state.cloud_service_id
4452
- `sampling_rate`: the chosen rate
53+
- `sampling_mode`: the chosen mode (adaptive or fixed)
4554
- `export_spans`: the chosen value
4655
- `enable_env_var_recording`: the chosen value
4756

4857
5. **Transition**: Move to the next phase with:
4958
- `sampling_rate`: the chosen rate
59+
- `sampling_mode`: the chosen mode
5060
- `export_spans`: the chosen value
5161
- `enable_env_var_recording`: the chosen value
5262

@@ -61,6 +71,7 @@ Since cloud users fetch traces from Tusk Cloud rather than storing them locally,
6171

6272
### Important Notes
6373

64-
- Lower sampling rates reduce performance overhead
74+
- Adaptive mode is recommended for most deployments as it automatically reduces sampling under load to minimize performance overhead
75+
- Lower base sampling rates reduce performance overhead
6576
- Export spans must be true for cloud features to work
6677
- Environment variable recording is useful for apps that depend on env vars for business logic

internal/agent/prompts/phase_create_config.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ test_execution:
2222
timeout: 30s
2323

2424
recording:
25-
sampling_rate: 1.0
25+
sampling:
26+
mode: adaptive
27+
base_rate: 1.0
2628
export_spans: false
2729
enable_env_var_recording: true
2830
```
@@ -52,7 +54,9 @@ test_execution:
5254
timeout: 30s
5355

5456
recording:
55-
sampling_rate: 1.0
57+
sampling:
58+
mode: adaptive
59+
base_rate: 1.0
5660
export_spans: false
5761
enable_env_var_recording: true
5862
```

internal/agent/tools/cloud.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,7 @@ func (ct *CloudTools) SaveCloudConfig(input json.RawMessage) (string, error) {
781781
var params struct {
782782
ServiceID string `json:"service_id"`
783783
SamplingRate float64 `json:"sampling_rate"`
784+
SamplingMode string `json:"sampling_mode"`
784785
ExportSpans bool `json:"export_spans"`
785786
EnableEnvVarRecording bool `json:"enable_env_var_recording"`
786787
}
@@ -796,7 +797,7 @@ func (ct *CloudTools) SaveCloudConfig(input json.RawMessage) (string, error) {
796797
}
797798

798799
// Save recording config (preserves other fields like exclude_paths, transforms)
799-
if err := onboardcloud.SaveRecordingConfig(params.SamplingRate, params.ExportSpans, params.EnableEnvVarRecording); err != nil {
800+
if err := onboardcloud.SaveRecordingConfig(params.SamplingRate, params.SamplingMode, params.ExportSpans, params.EnableEnvVarRecording); err != nil {
800801
return "", fmt.Errorf("failed to save recording config: %w", err)
801802
}
802803

internal/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,12 @@ func getMinimalSchemaHint() string {
543543
interval: 1s`
544544
}
545545

546+
// FindConfigFile returns the path to the config file found by traversing
547+
// upward from the current directory. Returns an empty string if not found.
548+
func FindConfigFile() string {
549+
return findConfigFile()
550+
}
551+
546552
func findConfigFile() string {
547553
wd, err := os.Getwd()
548554
if err != nil {

internal/tui/onboard-cloud/helpers.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/Use-Tusk/tusk-cli/internal/cliconfig"
1414
"github.com/Use-Tusk/tusk-cli/internal/config"
1515
"github.com/Use-Tusk/tusk-cli/internal/utils"
16+
"gopkg.in/yaml.v3"
1617
)
1718

1819
func listGitRemotes() (map[string]string, error) {
@@ -195,6 +196,15 @@ func loadExistingConfig(m *Model) error {
195196
}
196197

197198
m.ServiceID = cfg.Service.ID
199+
// Use the config's sampling mode, but default to "adaptive" when the
200+
// config file doesn't contain an explicit sampling.mode key (the config
201+
// parser normalizes absent mode to "fixed" for backward compatibility,
202+
// but the wizard should default new setups to "adaptive").
203+
if configHasExplicitSamplingMode() {
204+
m.SamplingMode = cfg.Recording.Sampling.Mode
205+
} else {
206+
m.SamplingMode = "adaptive"
207+
}
198208
m.SamplingRate = fmt.Sprintf("%.2f", cfg.Recording.SamplingRate)
199209

200210
if cfg.Recording.ExportSpans != nil {
@@ -488,3 +498,24 @@ func detectShellConfig() string {
488498
// Fallback to .profile
489499
return filepath.Join(homeDir, ".profile")
490500
}
501+
502+
// configHasExplicitSamplingMode checks the raw config file for an explicit
503+
// recording.sampling.mode key. Returns false if the key is absent or the
504+
// file can't be read, so the caller can fall back to the wizard default.
505+
func configHasExplicitSamplingMode() bool {
506+
data, err := os.ReadFile(config.FindConfigFile())
507+
if err != nil {
508+
return false
509+
}
510+
var raw struct {
511+
Recording struct {
512+
Sampling struct {
513+
Mode string `yaml:"mode"`
514+
} `yaml:"sampling"`
515+
} `yaml:"recording"`
516+
}
517+
if err := yaml.Unmarshal(data, &raw); err != nil {
518+
return false
519+
}
520+
return raw.Recording.Sampling.Mode != ""
521+
}

internal/tui/onboard-cloud/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type Model struct {
6565
CreateApiKeyChoice bool
6666

6767
// State - Recording Config
68+
SamplingMode string
6869
SamplingRate string
6970
ExportSpans bool
7071
EnableEnvVarRecording bool

internal/tui/onboard-cloud/recording_config_table.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
type RecordingConfigTable struct {
1414
table table.Model
15+
samplingMode string
1516
samplingRate string
1617
exportSpans bool
1718
enableEnvVarRecording bool
@@ -20,7 +21,7 @@ type RecordingConfigTable struct {
2021
cursor int
2122
}
2223

23-
func NewRecordingConfigTable(samplingRate string, exportSpans, enableEnvVarRecording bool) *RecordingConfigTable {
24+
func NewRecordingConfigTable(samplingMode, samplingRate string, exportSpans, enableEnvVarRecording bool) *RecordingConfigTable {
2425
columns := []table.Column{
2526
{Title: "Setting", Width: 35},
2627
{Title: "Value", Width: 25},
@@ -48,8 +49,12 @@ func NewRecordingConfigTable(samplingRate string, exportSpans, enableEnvVarRecor
4849

4950
t.SetStyles(s)
5051

52+
if samplingMode == "" {
53+
samplingMode = "adaptive"
54+
}
5155
rct := &RecordingConfigTable{
5256
table: t,
57+
samplingMode: samplingMode,
5358
samplingRate: samplingRate,
5459
exportSpans: true, // Required for cloud onboarding
5560
enableEnvVarRecording: enableEnvVarRecording,
@@ -64,7 +69,7 @@ func NewRecordingConfigTable(samplingRate string, exportSpans, enableEnvVarRecor
6469
func (rct *RecordingConfigTable) updateRows() {
6570
rate, _ := strconv.ParseFloat(rct.samplingRate, 64)
6671
rateDisplay := fmt.Sprintf("%.2f (%.0f%%)", rate, rate*100)
67-
if rct.EditMode && rct.cursor == 0 {
72+
if rct.EditMode && rct.cursor == 1 {
6873
rateDisplay = "→ " + rct.samplingRate + "_"
6974
}
7075

@@ -76,7 +81,8 @@ func (rct *RecordingConfigTable) updateRows() {
7681
}
7782

7883
rows := []table.Row{
79-
{"Sampling Rate", rateDisplay},
84+
{"Sampling Mode", rct.samplingMode},
85+
{"Base Sampling Rate", rateDisplay},
8086
{"Export Spans", formatBool(rct.exportSpans)},
8187
{"Record Environment Variables", formatBool(rct.enableEnvVarRecording)},
8288
}
@@ -92,7 +98,7 @@ func (rct *RecordingConfigTable) Update(msg tea.Msg) (*RecordingConfigTable, tea
9298
switch msg := msg.(type) {
9399
case tea.KeyMsg:
94100
// If in edit mode (typing sampling rate)
95-
if rct.EditMode && rct.cursor == 0 {
101+
if rct.EditMode && rct.cursor == 1 {
96102
switch msg.String() {
97103
case "tab", "esc":
98104
rct.EditMode = false
@@ -127,7 +133,7 @@ func (rct *RecordingConfigTable) Update(msg tea.Msg) (*RecordingConfigTable, tea
127133
return rct, nil
128134

129135
case "down", "j":
130-
if rct.cursor < 2 {
136+
if rct.cursor < 3 {
131137
rct.cursor++
132138
rct.table.MoveDown(1)
133139
}
@@ -136,18 +142,24 @@ func (rct *RecordingConfigTable) Update(msg tea.Msg) (*RecordingConfigTable, tea
136142

137143
case "tab", " ":
138144
switch rct.cursor {
145+
case 0:
146+
if rct.samplingMode == "adaptive" {
147+
rct.samplingMode = "fixed"
148+
} else {
149+
rct.samplingMode = "adaptive"
150+
}
139151
case 1:
140-
rct.exportSpans = !rct.exportSpans
152+
rct.EditMode = true
141153
case 2:
154+
rct.exportSpans = !rct.exportSpans
155+
case 3:
142156
rct.enableEnvVarRecording = !rct.enableEnvVarRecording
143-
case 0:
144-
rct.EditMode = true
145157
}
146158
rct.updateRows()
147159
return rct, nil
148160

149161
case "e":
150-
if rct.cursor == 0 {
162+
if rct.cursor == 1 {
151163
rct.EditMode = true
152164
rct.updateRows()
153165
return rct, nil
@@ -169,9 +181,9 @@ func (rct *RecordingConfigTable) View() string {
169181
)
170182
}
171183

172-
func (rct *RecordingConfigTable) GetValues() (samplingRate float64, exportSpans, enableEnvVarRecording bool) {
184+
func (rct *RecordingConfigTable) GetValues() (samplingMode string, samplingRate float64, exportSpans, enableEnvVarRecording bool) {
173185
rate, _ := strconv.ParseFloat(rct.samplingRate, 64)
174-
return rate, rct.exportSpans, rct.enableEnvVarRecording
186+
return rct.samplingMode, rate, rct.exportSpans, rct.enableEnvVarRecording
175187
}
176188

177189
func (rct *RecordingConfigTable) SetFocused(focused bool) {

internal/tui/onboard-cloud/save.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,23 @@ func SaveServiceIDToConfig(serviceID string) error {
212212
// SaveRecordingConfig saves recording settings to .tusk/config.yaml.
213213
// Uses yaml.Node parsing to preserve file structure, comments, and unknown fields
214214
// (e.g., exclude_paths, transforms that the user may have configured).
215-
func SaveRecordingConfig(samplingRate float64, exportSpans, enableEnvVarRecording bool) error {
215+
func SaveRecordingConfig(samplingRate float64, samplingMode string, exportSpans, enableEnvVarRecording bool) error {
216+
if samplingMode == "" {
217+
samplingMode = "adaptive"
218+
} else if samplingMode != "adaptive" && samplingMode != "fixed" {
219+
return fmt.Errorf("invalid sampling mode %q: must be 'adaptive' or 'fixed'", samplingMode)
220+
}
216221
return saveToConfig(func(cfg *config.Config, u *ConfigUpdater) error {
217222
cfg.Recording.SamplingRate = samplingRate
223+
cfg.Recording.Sampling.Mode = samplingMode
224+
baseRate := samplingRate
225+
cfg.Recording.Sampling.BaseRate = &baseRate
218226
cfg.Recording.ExportSpans = &exportSpans
219227
cfg.Recording.EnableEnvVarRecording = &enableEnvVarRecording
220228

221229
u.Set([]string{"recording", "sampling_rate"}, samplingRate)
230+
u.Set([]string{"recording", "sampling", "mode"}, samplingMode)
231+
u.Set([]string{"recording", "sampling", "base_rate"}, samplingRate)
222232
u.Set([]string{"recording", "export_spans"}, exportSpans)
223233
u.Set([]string{"recording", "enable_env_var_recording"}, enableEnvVarRecording)
224234
return nil

0 commit comments

Comments
 (0)