mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
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
* Tests: disable telemetry in tests by default Disable the plugin in cli tests and sharness by default. Enable only in telemetry tests. There are cases when tests get stuck or get killed and leave daemons hanging around. We don't want to be getting telemetry from those. * sharness: attempt to fix * sharness: add missing --bool flag * fix(ci): add omitempty to Plugin.Config field The sharness problem is that when the telemetry plugin is configured initially with 'ipfs config --bool', it creates a structure without the 'Config: null' field, but when the config is copied and replaced, it expects the structure to be preserved. Adding omitempty ensures the Config field is omitted from JSON when nil, making the config structure consistent between initial creation and replacement operations. --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>
315 lines
8.9 KiB
Go
315 lines
8.9 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",
|
|
"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()
|
|
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")
|
|
}
|
|
})
|
|
}
|