Skip to content

Commit 49c3a2a

Browse files
committed
feat: add privacy-first PostHog analytics
- Add PostHog Go SDK for anonymous usage tracking - Create analytics package with privacy controls - Support multiple opt-out mechanisms: - Config file: disable_analytics setting - Environment variables: VAPI_DISABLE_ANALYTICS, VAPI_NO_TELEMETRY, DISABLE_TELEMETRY, DO_NOT_TRACK - Track command usage patterns, performance, and error types (anonymized) - No PII collection - only hashed error patterns and anonymous metadata - Add config analytics subcommands for user control - Integrate tracking into root command and assistant commands - Document privacy approach in README with clear opt-out instructions - Anonymous stable ID generation based on system characteristics - Graceful failure - analytics never breaks CLI functionality
1 parent 02eb11b commit 49c3a2a

File tree

9 files changed

+601
-16
lines changed

9 files changed

+601
-16
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,52 @@ vapi config set <key> <value>
203203

204204
# List all configuration options
205205
vapi config list
206+
207+
# Manage analytics preferences
208+
vapi config analytics status # Show current analytics status
209+
vapi config analytics disable # Disable analytics collection
210+
vapi config analytics enable # Enable analytics collection
206211
```
207212

213+
#### Analytics and Privacy
214+
215+
The Vapi CLI collects anonymous usage analytics to help improve the product. **We prioritize your privacy**:
216+
217+
**What we collect:**
218+
219+
- Command usage patterns (anonymous)
220+
- Error types and frequencies (hashed)
221+
- Performance metrics
222+
- Operating system and architecture
223+
- CLI version information
224+
225+
**What we DON'T collect:**
226+
227+
- API keys or sensitive credentials
228+
- File contents or personal data
229+
- User-identifiable information
230+
- Specific error messages (only hashed patterns)
231+
232+
**How to opt out:**
233+
234+
You can disable analytics collection in multiple ways:
235+
236+
```bash
237+
# Via CLI command
238+
vapi config analytics disable
239+
240+
# Via environment variable (any of these)
241+
export VAPI_DISABLE_ANALYTICS=1
242+
export VAPI_NO_TELEMETRY=1
243+
export DISABLE_TELEMETRY=1
244+
export DO_NOT_TRACK=1
245+
246+
# Via config file
247+
echo "disable_analytics: true" >> ~/.vapi-cli.yaml
248+
```
249+
250+
All data is collected anonymously and securely transmitted to PostHog for analysis.
251+
208252
### Chat Management
209253

210254
Manage text-based chat conversations with Vapi assistants:

cmd/assistant.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
vapi "github.com/VapiAI/server-sdk-go"
2828
"github.com/spf13/cobra"
2929

30+
"github.com/VapiAI/cli/pkg/analytics"
3031
"github.com/VapiAI/cli/pkg/output"
3132
)
3233

@@ -47,7 +48,7 @@ var listAssistantCmd = &cobra.Command{
4748
Use: "list",
4849
Short: "List all assistants",
4950
Long: `Display all assistants in your account with their IDs, names, and metadata.`,
50-
RunE: func(cmd *cobra.Command, args []string) error {
51+
RunE: analytics.TrackCommandWrapper("assistant", "list", func(cmd *cobra.Command, args []string) error {
5152
fmt.Println("📋 Listing assistants...")
5253

5354
ctx := context.Background()
@@ -73,6 +74,9 @@ var listAssistantCmd = &cobra.Command{
7374

7475
if len(assistants) == 0 {
7576
fmt.Println("No assistants found. Create one with 'vapi assistant create'")
77+
analytics.TrackEvent("assistant_list_empty", map[string]interface{}{
78+
"count": 0,
79+
})
7680
return nil
7781
}
7882

@@ -92,8 +96,12 @@ var listAssistantCmd = &cobra.Command{
9296
fmt.Printf("%-36s %-30s %-20s\n", assistant.Id, name, created)
9397
}
9498

99+
analytics.TrackEvent("assistant_list_success", map[string]interface{}{
100+
"count": len(assistants),
101+
})
102+
95103
return nil
96-
},
104+
}),
97105
}
98106

99107
var createAssistantCmd = &cobra.Command{
@@ -103,7 +111,7 @@ var createAssistantCmd = &cobra.Command{
103111
104112
For advanced configuration (voice selection, model parameters, tools),
105113
use the Vapi dashboard at https://dashboard.vapi.ai`,
106-
RunE: func(cmd *cobra.Command, args []string) error {
114+
RunE: analytics.TrackCommandWrapper("assistant", "create", func(cmd *cobra.Command, args []string) error {
107115
fmt.Println("🤖 Create a new Vapi assistant")
108116
fmt.Println()
109117

@@ -146,6 +154,7 @@ use the Vapi dashboard at https://dashboard.vapi.ai`,
146154

147155
if err := survey.AskOne(confirmPrompt, &confirmCreate); err != nil || !confirmCreate {
148156
fmt.Println("Creation canceled.")
157+
analytics.TrackEvent("assistant_create_canceled", nil)
149158
return nil
150159
}
151160

@@ -157,15 +166,15 @@ use the Vapi dashboard at https://dashboard.vapi.ai`,
157166
fmt.Println("\nVisit https://dashboard.vapi.ai to create and configure assistants.")
158167

159168
return nil
160-
},
169+
}),
161170
}
162171

163172
var getAssistantCmd = &cobra.Command{
164173
Use: "get [assistant-id]",
165174
Short: "Get details of a specific assistant",
166175
Long: `Retrieve the full configuration of an assistant including voice, model, and tool settings.`,
167176
Args: cobra.ExactArgs(1),
168-
RunE: func(cmd *cobra.Command, args []string) error {
177+
RunE: analytics.TrackCommandWrapper("assistant", "get", func(cmd *cobra.Command, args []string) error {
169178
ctx := context.Background()
170179
assistantID := args[0]
171180

@@ -183,7 +192,7 @@ var getAssistantCmd = &cobra.Command{
183192
}
184193

185194
return nil
186-
},
195+
}),
187196
}
188197

189198
var updateAssistantCmd = &cobra.Command{
@@ -194,7 +203,7 @@ var updateAssistantCmd = &cobra.Command{
194203
Complex updates involving voice models, tools, or advanced settings
195204
are best done through the Vapi dashboard at https://dashboard.vapi.ai`,
196205
Args: cobra.ExactArgs(1),
197-
RunE: func(cmd *cobra.Command, args []string) error {
206+
RunE: analytics.TrackCommandWrapper("assistant", "update", func(cmd *cobra.Command, args []string) error {
198207
assistantID := args[0]
199208

200209
fmt.Printf("📝 Update assistant: %s\n", assistantID)
@@ -208,7 +217,7 @@ are best done through the Vapi dashboard at https://dashboard.vapi.ai`,
208217
fmt.Println("Visit: https://dashboard.vapi.ai/assistants")
209218

210219
return nil
211-
},
220+
}),
212221
}
213222

214223
// nolint:dupl // Delete commands follow a similar pattern across resources
@@ -217,7 +226,7 @@ var deleteAssistantCmd = &cobra.Command{
217226
Short: "Delete an assistant",
218227
Long: `Permanently delete an assistant. This cannot be undone.`,
219228
Args: cobra.ExactArgs(1),
220-
RunE: func(cmd *cobra.Command, args []string) error {
229+
RunE: analytics.TrackCommandWrapper("assistant", "delete", func(cmd *cobra.Command, args []string) error {
221230
ctx := context.Background()
222231
assistantID := args[0]
223232

@@ -234,6 +243,7 @@ var deleteAssistantCmd = &cobra.Command{
234243

235244
if !confirmDelete {
236245
fmt.Println("Deletion canceled.")
246+
analytics.TrackEvent("assistant_delete_canceled", nil)
237247
return nil
238248
}
239249

@@ -247,7 +257,7 @@ var deleteAssistantCmd = &cobra.Command{
247257

248258
fmt.Println("✅ Assistant deleted successfully")
249259
return nil
250-
},
260+
}),
251261
}
252262

253263
func init() {

cmd/config.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/spf13/cobra"
2727
"github.com/spf13/viper"
2828

29+
"github.com/VapiAI/cli/pkg/analytics"
2930
"github.com/VapiAI/cli/pkg/config"
3031
)
3132

@@ -229,6 +230,129 @@ func init() {
229230
configCmd.AddCommand(configSetCmd)
230231
configCmd.AddCommand(configEnvCmd)
231232

233+
// Add analytics subcommand
234+
var analyticsCmd = &cobra.Command{
235+
Use: "analytics",
236+
Short: "Manage analytics preferences",
237+
Long: `Configure whether the CLI sends anonymous usage analytics to help improve the product.`,
238+
}
239+
240+
var analyticsStatusCmd = &cobra.Command{
241+
Use: "status",
242+
Short: "Show current analytics status",
243+
RunE: analytics.TrackCommandWrapper("config", "analytics-status", func(cmd *cobra.Command, args []string) error {
244+
cfg := config.GetConfig()
245+
246+
fmt.Println("📊 Analytics Status")
247+
fmt.Println()
248+
249+
if analytics.IsEnabled() {
250+
fmt.Println("✅ Analytics: ENABLED")
251+
fmt.Println(" Anonymous usage data is being collected to help improve the CLI")
252+
} else {
253+
fmt.Println("🚫 Analytics: DISABLED")
254+
fmt.Println(" No usage data is being collected")
255+
}
256+
257+
fmt.Println()
258+
fmt.Println("Configuration:")
259+
260+
if cfg != nil && cfg.DisableAnalytics {
261+
fmt.Println(" • Config file: disabled")
262+
} else {
263+
fmt.Println(" • Config file: enabled (default)")
264+
}
265+
266+
// Check environment variables
267+
envDisabled := false
268+
envVars := []string{"VAPI_DISABLE_ANALYTICS", "VAPI_NO_TELEMETRY", "DISABLE_TELEMETRY", "DO_NOT_TRACK"}
269+
for _, env := range envVars {
270+
if os.Getenv(env) != "" {
271+
fmt.Printf(" • Environment (%s): disabled\n", env)
272+
envDisabled = true
273+
break
274+
}
275+
}
276+
if !envDisabled {
277+
fmt.Println(" • Environment: enabled (default)")
278+
}
279+
280+
fmt.Println()
281+
fmt.Println("Data collected (when enabled):")
282+
fmt.Println(" • Command usage patterns (anonymous)")
283+
fmt.Println(" • Error types and frequencies (hashed)")
284+
fmt.Println(" • Performance metrics")
285+
fmt.Println(" • Operating system and architecture")
286+
fmt.Println(" • CLI version information")
287+
fmt.Println()
288+
fmt.Println("Data NOT collected:")
289+
fmt.Println(" • API keys or sensitive credentials")
290+
fmt.Println(" • File contents or personal data")
291+
fmt.Println(" • User-identifiable information")
292+
fmt.Println(" • Specific error messages (only hashed patterns)")
293+
294+
return nil
295+
}),
296+
}
297+
298+
var analyticsEnableCmd = &cobra.Command{
299+
Use: "enable",
300+
Short: "Enable analytics collection",
301+
RunE: analytics.TrackCommandWrapper("config", "analytics-enable", func(cmd *cobra.Command, args []string) error {
302+
cfg := config.GetConfig()
303+
if cfg == nil {
304+
cfg = &config.Config{}
305+
}
306+
307+
cfg.DisableAnalytics = false
308+
309+
if err := config.SaveConfig(cfg); err != nil {
310+
return fmt.Errorf("failed to save config: %w", err)
311+
}
312+
313+
// Update global config
314+
config.SetConfig(cfg)
315+
316+
fmt.Println("✅ Analytics enabled")
317+
fmt.Println(" Anonymous usage data will be collected to help improve the CLI")
318+
fmt.Println(" You can disable this anytime with: vapi config analytics disable")
319+
320+
return nil
321+
}),
322+
}
323+
324+
var analyticsDisableCmd = &cobra.Command{
325+
Use: "disable",
326+
Short: "Disable analytics collection",
327+
RunE: analytics.TrackCommandWrapper("config", "analytics-disable", func(cmd *cobra.Command, args []string) error {
328+
cfg := config.GetConfig()
329+
if cfg == nil {
330+
cfg = &config.Config{}
331+
}
332+
333+
cfg.DisableAnalytics = true
334+
335+
if err := config.SaveConfig(cfg); err != nil {
336+
return fmt.Errorf("failed to save config: %w", err)
337+
}
338+
339+
// Update global config
340+
config.SetConfig(cfg)
341+
342+
fmt.Println("🚫 Analytics disabled")
343+
fmt.Println(" No usage data will be collected")
344+
fmt.Println(" You can re-enable this anytime with: vapi config analytics enable")
345+
346+
return nil
347+
}),
348+
}
349+
350+
analyticsCmd.AddCommand(analyticsStatusCmd)
351+
analyticsCmd.AddCommand(analyticsEnableCmd)
352+
analyticsCmd.AddCommand(analyticsDisableCmd)
353+
354+
configCmd.AddCommand(analyticsCmd)
355+
232356
// Here you will define your flags and configuration settings.
233357

234358
// Cobra supports Persistent Flags which will work for this command

0 commit comments

Comments
 (0)