mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
* chore: apply go fix modernizers from Go 1.26
automated refactoring: interface{} to any, slices.Contains,
and other idiomatic updates.
* feat(ci): add `go fix` check to Go analysis workflow
ensures Go 1.26 modernizers are applied, fails CI if `go fix ./...`
produces any changes (similar to existing `go fmt` enforcement)
318 lines
9.0 KiB
Go
318 lines
9.0 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()
|
|
node.SetIPFSConfig("Plugins.Plugins.telemetry.Disabled", false)
|
|
|
|
// 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()
|
|
node.SetIPFSConfig("Plugins.Plugins.telemetry.Disabled", false)
|
|
|
|
// 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()
|
|
node.SetIPFSConfig("Plugins.Plugins.telemetry.Disabled", false)
|
|
|
|
// 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()
|
|
node.SetIPFSConfig("Plugins.Plugins.telemetry.Disabled", false)
|
|
|
|
// 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",
|
|
"provide_dht_sweep_enabled",
|
|
"provide_dht_interval_custom",
|
|
"provide_dht_max_workers_custom",
|
|
"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]any, 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]any
|
|
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()
|
|
node.SetIPFSConfig("Plugins.Plugins.telemetry.Disabled", false)
|
|
|
|
// 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")
|
|
}
|
|
})
|
|
}
|