kubo/test/cli/telemetry_test.go
Marcin Rataj ccb49de852
Some checks failed
CodeQL / codeql (push) Has been cancelled
Docker Build / docker-build (push) Has been cancelled
Gateway Conformance / gateway-conformance (push) Has been cancelled
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Has been cancelled
Go Build / go-build (push) Has been cancelled
Go Check / go-check (push) Has been cancelled
Go Lint / go-lint (push) Has been cancelled
Go Test / go-test (push) Has been cancelled
Interop / interop-prep (push) Has been cancelled
Sharness / sharness-test (push) Has been cancelled
Spell Check / spellcheck (push) Has been cancelled
Interop / helia-interop (push) Has been cancelled
Interop / ipfs-webui (push) Has been cancelled
feat(config): AutoConf with "auto" placeholders (#10883)
https://github.com/ipfs/kubo/pull/10883
https://github.com/ipshipyard/config.ipfs-mainnet.org/issues/3

---------

Co-authored-by: gammazero <gammazero@users.noreply.github.com>
2025-08-20 05:59:11 +02:00

310 lines
8.6 KiB
Go

package cli
import (
"encoding/json"
"io"
"maps"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"slices"
"testing"
"time"
"github.com/ipfs/kubo/test/cli/harness"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTelemetry(t *testing.T) {
t.Parallel()
t.Run("opt-out via environment variable", func(t *testing.T) {
t.Parallel()
// Create a new node
node := harness.NewT(t).NewNode().Init()
// Set the opt-out environment variable
node.Runner.Env["IPFS_TELEMETRY"] = "off"
node.Runner.Env["GOLOG_LOG_LEVEL"] = "telemetry=debug"
// Capture daemon output
stdout := &harness.Buffer{}
stderr := &harness.Buffer{}
// Start daemon with output capture
node.StartDaemonWithReq(harness.RunRequest{
CmdOpts: []harness.CmdOpt{
harness.RunWithStdout(stdout),
harness.RunWithStderr(stderr),
},
}, "")
time.Sleep(500 * time.Millisecond)
// Get daemon output
output := stdout.String() + stderr.String()
// Check that telemetry is disabled
assert.Contains(t, output, "telemetry disabled via opt-out", "Expected telemetry disabled message")
// Stop daemon
node.StopDaemon()
// Verify UUID file was not created or was removed
uuidPath := filepath.Join(node.Dir, "telemetry_uuid")
_, err := os.Stat(uuidPath)
assert.True(t, os.IsNotExist(err), "UUID file should not exist when opted out")
})
t.Run("opt-out via config", func(t *testing.T) {
t.Parallel()
// Create a new node
node := harness.NewT(t).NewNode().Init()
// Set opt-out via config
node.IPFS("config", "Plugins.Plugins.telemetry.Config.Mode", "off")
// Enable debug logging
node.Runner.Env["GOLOG_LOG_LEVEL"] = "telemetry=debug"
// Capture daemon output
stdout := &harness.Buffer{}
stderr := &harness.Buffer{}
// Start daemon with output capture
node.StartDaemonWithReq(harness.RunRequest{
CmdOpts: []harness.CmdOpt{
harness.RunWithStdout(stdout),
harness.RunWithStderr(stderr),
},
}, "")
time.Sleep(500 * time.Millisecond)
// Get daemon output
output := stdout.String() + stderr.String()
// Check that telemetry is disabled
assert.Contains(t, output, "telemetry disabled via opt-out", "Expected telemetry disabled message")
assert.Contains(t, output, "telemetry collection skipped: opted out", "Expected telemetry skipped message")
// Stop daemon
node.StopDaemon()
// Verify UUID file was not created or was removed
uuidPath := filepath.Join(node.Dir, "telemetry_uuid")
_, err := os.Stat(uuidPath)
assert.True(t, os.IsNotExist(err), "UUID file should not exist when opted out")
})
t.Run("opt-out removes existing UUID file", func(t *testing.T) {
t.Parallel()
// Create a new node
node := harness.NewT(t).NewNode().Init()
// Create a UUID file manually to simulate previous telemetry run
uuidPath := filepath.Join(node.Dir, "telemetry_uuid")
testUUID := "test-uuid-12345"
err := os.WriteFile(uuidPath, []byte(testUUID), 0600)
require.NoError(t, err, "Failed to create test UUID file")
// Verify file exists
_, err = os.Stat(uuidPath)
require.NoError(t, err, "UUID file should exist before opt-out")
// Set the opt-out environment variable
node.Runner.Env["IPFS_TELEMETRY"] = "off"
node.Runner.Env["GOLOG_LOG_LEVEL"] = "telemetry=debug"
// Capture daemon output
stdout := &harness.Buffer{}
stderr := &harness.Buffer{}
// Start daemon with output capture
node.StartDaemonWithReq(harness.RunRequest{
CmdOpts: []harness.CmdOpt{
harness.RunWithStdout(stdout),
harness.RunWithStderr(stderr),
},
}, "")
time.Sleep(500 * time.Millisecond)
// Get daemon output
output := stdout.String() + stderr.String()
// Check that UUID file was removed
assert.Contains(t, output, "removed existing telemetry UUID file due to opt-out", "Expected UUID removal message")
// Stop daemon
node.StopDaemon()
// Verify UUID file was removed
_, err = os.Stat(uuidPath)
assert.True(t, os.IsNotExist(err), "UUID file should be removed after opt-out")
})
t.Run("telemetry enabled shows info message", func(t *testing.T) {
t.Parallel()
// Create a new node
node := harness.NewT(t).NewNode().Init()
// Capture daemon output
stdout := &harness.Buffer{}
stderr := &harness.Buffer{}
// Don't set opt-out, so telemetry will be enabled
// This should trigger the info message on first run
node.StartDaemonWithReq(harness.RunRequest{
CmdOpts: []harness.CmdOpt{
harness.RunWithStdout(stdout),
harness.RunWithStderr(stderr),
},
}, "")
time.Sleep(500 * time.Millisecond)
// Get daemon output
output := stdout.String() + stderr.String()
// First run - should show info message
assert.Contains(t, output, "Anonymous telemetry")
assert.Contains(t, output, "No data sent yet", "Expected no data sent message")
assert.Contains(t, output, "To opt-out before collection starts", "Expected opt-out instructions")
assert.Contains(t, output, "Learn more:", "Expected learn more link")
// Stop daemon
node.StopDaemon()
// Verify UUID file was created
uuidPath := filepath.Join(node.Dir, "telemetry_uuid")
_, err := os.Stat(uuidPath)
assert.NoError(t, err, "UUID file should exist when daemon started without telemetry opt-out")
})
t.Run("telemetry schema regression guard", func(t *testing.T) {
t.Parallel()
// Define the exact set of expected telemetry fields
// This list must be updated whenever telemetry fields change
expectedFields := []string{
"uuid",
"agent_version",
"private_network",
"bootstrappers_custom",
"repo_size_bucket",
"uptime_bucket",
"reprovider_strategy",
"routing_type",
"routing_accelerated_dht_client",
"routing_delegated_count",
"autonat_service_mode",
"autonat_reachability",
"swarm_enable_hole_punching",
"swarm_circuit_addresses",
"swarm_ipv4_public_addresses",
"swarm_ipv6_public_addresses",
"auto_tls_auto_wss",
"auto_tls_domain_suffix_custom",
"autoconf",
"autoconf_custom",
"discovery_mdns_enabled",
"platform_os",
"platform_arch",
"platform_containerized",
"platform_vm",
}
// Channel to receive captured telemetry data
telemetryChan := make(chan map[string]interface{}, 1)
// Create a mock HTTP server to capture telemetry
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
var telemetryData map[string]interface{}
if err := json.Unmarshal(body, &telemetryData); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Send captured data through channel
select {
case telemetryChan <- telemetryData:
default:
}
w.WriteHeader(http.StatusOK)
}))
defer mockServer.Close()
// Create a new node
node := harness.NewT(t).NewNode().Init()
// Configure telemetry with a very short delay for testing
node.IPFS("config", "Plugins.Plugins.telemetry.Config.Delay", "100ms")
node.IPFS("config", "Plugins.Plugins.telemetry.Config.Endpoint", mockServer.URL)
// Enable debug logging to see what's being sent
node.Runner.Env["GOLOG_LOG_LEVEL"] = "telemetry=debug"
// Start daemon
node.StartDaemon()
defer node.StopDaemon()
// Wait for telemetry to be sent (configured delay + buffer)
select {
case telemetryData := <-telemetryChan:
receivedFields := slices.Collect(maps.Keys(telemetryData))
slices.Sort(expectedFields)
slices.Sort(receivedFields)
// Fast path: check if fields match exactly
if !slices.Equal(expectedFields, receivedFields) {
var missingFields, unexpectedFields []string
for _, field := range expectedFields {
if _, ok := telemetryData[field]; !ok {
missingFields = append(missingFields, field)
}
}
expectedSet := make(map[string]struct{}, len(expectedFields))
for _, f := range expectedFields {
expectedSet[f] = struct{}{}
}
for field := range telemetryData {
if _, ok := expectedSet[field]; !ok {
unexpectedFields = append(unexpectedFields, field)
}
}
t.Fatalf("Telemetry field mismatch:\n"+
" Missing fields: %v\n"+
" Unexpected fields: %v\n"+
" Note: Update expectedFields list in this test when adding/removing telemetry fields",
missingFields, unexpectedFields)
}
t.Logf("Telemetry field validation passed: %d fields verified", len(expectedFields))
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for telemetry data to be sent")
}
})
}