mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
feat: telemetry plugin (#10866)
* Initial pass at Telemetry plugin Currently, IP Shipyard, with the help of Probelab, monitor and extract Amino/IPFS public network metrics with the use of DHT crawlers and bootstrappers (via peerlog plugin). For example, we log all peer IDs seen and their AgentVersion/Addresses obtained from the `identify` protocol, which provides insights into protocol usage, total number of peers etc. We would like to increase the ability to obtain more insights from the network by collecting some more information in the future, but also to give users more control over this collection (i.e. opt-out). The information collected will not allow unique identification of anyone and is only used for aggregation. Now, this PR explores a way of moving in this direction: * A new "telemetry" fx plugin is in charge of dealing with telemetry * The FX plugin allows to plug and make decisions / take actions during the setup phase: * We can inspect whether we are using Private Networks before the libp2p.Host has been initialized. * We can send telemetry after the libp2p Host is initialized. * Everything is self-contained. Custom builds can remove the plugin altogether without needing to surgically edit the code. As for behaviour: * The user can opt-in/out via EnvVar, file in the repo path or plugin configuration. * Users on private networks or with custom bootstrappers are detected, offered a wall of text explaining why we need telemetry and invited to opt-in. Opt-out happens otherwise on a timeout (with no input). Their preferences are stored. * Users on standard settings are opted-in by default. This is the status quo in Kubo already, except they don't get a chance to opt out. The telemetry libp2p protocol is yet to be defined, but expect something similar to identify, with a protobuf being pushed to bootstrappers or to a specific telemetry node that we define. In the case of pnets, this will be done with a temporary peer. * checkpoint * telemetry plugin: second pass * On first run it generates a UUID and shows a message to the user. * UUID is persistend to "telemetry_uuid" * Sends telemetry 1 minute after boot and every 24h * LogEvent is the thing containing all the telemetry that is sent * Opt-out possible via env-var or plugin configuration * Telemetry: add changelog and environment variable documentation * docs: improved daemon message making it more obvious nothing was sent yet and that user had 15m to out-out plus some debug logs that confirm opt-out * refactor: rename IPFS_TELEMETRY_MODE to IPFS_TELEMETRY * fix: add User-Agent header to telemetry requests --------- Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com> Co-authored-by: Marcin Rataj <lidel@lidel.org>
This commit is contained in:
parent
187fce7a6d
commit
4255cc3889
@ -3,4 +3,4 @@ ignoreWords:
|
||||
- NodeCreater # This spelling is used in the fuse dependency
|
||||
- Boddy # One of the contributors to the project - Chris Boddy
|
||||
- Botto # One of the contributors to the project - Santiago Botto
|
||||
- cose # dag-cose
|
||||
- cose # dag-cose
|
||||
@ -20,6 +20,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
|
||||
- [Improved `ipfs cid`](#improved-ipfs-cid)
|
||||
- [Deprecated `ipfs stats reprovide`](#deprecated-ipfs-stats-reprovide)
|
||||
- [🔄 AutoRelay now uses all connected peers for relay discovery](#-autorelay-now-uses-all-connected-peers-for-relay-discovery)
|
||||
- [📊 Anonymous telemetry for better feature prioritization](#-anonymous-telemetry-for-better-feature-prioritization)
|
||||
- [📦️ Important dependency updates](#-important-dependency-updates)
|
||||
- [📝 Changelog](#-changelog)
|
||||
- [👨👩👧👦 Contributors](#-contributors)
|
||||
@ -136,7 +137,54 @@ The `ipfs stats reprovide` command has moved to `ipfs provide stat`. This was do
|
||||
|
||||
AutoRelay's relay discovery now includes all connected peers as potential relay candidates, not just peers discovered through the DHT. This allows peers connected via HTTP routing and manual `ipfs swarm connect` commands to serve as relays, improving connectivity for nodes using non-DHT routing configurations.
|
||||
|
||||
#### 📦️ Important dependency updates
|
||||
#### 📊 Anonymous telemetry for better feature prioritization
|
||||
|
||||
Per a suggestion from the IPFS Foundation, Kubo now sends optional anonymized telemetry information to Shipyard [maintainers](https://github.com/ipshipyard/roadmaps/issues/20).
|
||||
|
||||
**Privacy first**: The telemetry system collects only anonymous data - no personally identifiable information, file paths, or content data. A random UUID is generated on first run for anonymous identification. Users are notified before any data is sent and have time to opt-out.
|
||||
|
||||
**Why**: We want to better understand Kubo usage across the ecosystem so we can better direct funding and work efforts. For example, we have little insights into how many nodes are NAT'ed and rely on AutoNAT for reachability. Some of the information can be inferred by crawling the network or logging `/identify` details in the bootstrappers, but users have no way of opting out from that, so we believe it is more transparent to concentrate this functionality in one place.
|
||||
|
||||
**What**: Currently, we send the following anonymous metrics:
|
||||
|
||||
```
|
||||
"uuid": "<unique_uuid>",
|
||||
"agent_version": "kubo/0.37.0-dev",
|
||||
"private_network": false,
|
||||
"bootstrappers_custom": false,
|
||||
"repo_size_bucket": 1073741824,
|
||||
"uptime_bucket": 86400000000000,
|
||||
"reprovider_strategy": "pinned",
|
||||
"routing_type": "auto",
|
||||
"routing_accelerated_dht_client": false,
|
||||
"routing_delegated_count": 0,
|
||||
"autonat_service_mode": "enabled",
|
||||
"autonat_reachability": "",
|
||||
"swarm_enable_hole_punching": true,
|
||||
"swarm_circuit_addresses": false,
|
||||
"swarm_ipv4_public_addresses": true,
|
||||
"swarm_ipv6_public_addresses": true,
|
||||
"auto_tls_auto_wss": true,
|
||||
"auto_tls_domain_suffix_custom": false,
|
||||
"discovery_mdns_enabled": true,
|
||||
"platform_os": "linux",
|
||||
"platform_arch": "amd64",
|
||||
"platform_containerized": false,
|
||||
"platform_vm": false
|
||||
```
|
||||
|
||||
The exact data sent for your node can be inspected by setting `GOLOG_LOG_LEVEL="telemetry=debug"`. Users will see an informative message the first time they launch a telemetry-enabled daemon, with time to opt-out before any data is collected. Telemetry data is sent every 24h, with the first collection starting 15 minutes after daemon launch.
|
||||
|
||||
**User control**: You can opt-out at any time:
|
||||
|
||||
- Set environment variable `IPFS_TELEMETRY=off` before starting the daemon
|
||||
- Or run `ipfs config Plugins.Plugins.telemetry.Config.Mode off` and restart the daemon
|
||||
|
||||
The telemetry plugin code lives in `plugin/plugins/telemetry`.
|
||||
|
||||
Learn more: [`/kubo/docs/telemetry.md`](https://github.com/ipfs/kubo/blob/master/docs/telemetry.md)
|
||||
|
||||
### 📦️ Important dependency updates
|
||||
|
||||
- update `go-libp2p` to [v0.43.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.43.0) (incl. [v0.42.1](https://github.com/libp2p/go-libp2p/releases/tag/v0.42.1))
|
||||
- update `p2p-forge/client` to [v0.6.1](https://github.com/ipshipyard/p2p-forge/releases/tag/v0.6.1)
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
- [`IPFS_HTTP_ROUTERS_FILTER_PROTOCOLS`](#ipfs_http_routers_filter_protocols)
|
||||
- [`IPFS_CONTENT_BLOCKING_DISABLE`](#ipfs_content_blocking_disable)
|
||||
- [`IPFS_WAIT_REPO_LOCK`](#ipfs_wait_repo_lock)
|
||||
- [`IPFS_TELEMETRY`](#ipfs_telemetry)
|
||||
- [`LIBP2P_TCP_REUSEPORT`](#libp2p_tcp_reuseport)
|
||||
- [`LIBP2P_TCP_MUX`](#libp2p_tcp_mux)
|
||||
- [`LIBP2P_MUX_PREFS`](#libp2p_mux_prefs)
|
||||
@ -194,6 +195,22 @@ IPFS_WAIT_REPO_LOCK="15s"
|
||||
|
||||
If the lock cannot be acquired because someone else has the lock, and `IPFS_WAIT_REPO_LOCK` is set to a valid value, then acquiring the lock is retried every second until the lock is acquired or the specified wait time has elapsed.
|
||||
|
||||
## `IPFS_TELEMETRY`
|
||||
|
||||
Controls the behavior of the [telemetry plugin](telemetry.md). Valid values are:
|
||||
|
||||
- `on`: Enables telemetry.
|
||||
- `off`: Disables telemetry.
|
||||
- `auto`: Like `on`, but logs an informative message about telemetry and gives user 15 minutes to opt-out before first collection. Used automatically on first run and when `IPFS_TELEMETRY` is not set.
|
||||
|
||||
The mode can also be set in the config file under `Plugins.Plugins.telemetry.Config.Mode`.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
export IPFS_TELEMETRY="off"
|
||||
```
|
||||
|
||||
## `LIBP2P_TCP_REUSEPORT`
|
||||
|
||||
Kubo tries to reuse the same source port for all connections to improve NAT
|
||||
|
||||
@ -117,6 +117,7 @@ Example:
|
||||
| [flatfs](https://github.com/ipfs/kubo/tree/master/plugin/plugins/flatfs) | Datastore | x | A stable filesystem-based datastore. |
|
||||
| [levelds](https://github.com/ipfs/kubo/tree/master/plugin/plugins/levelds) | Datastore | x | A stable, flexible datastore backend. |
|
||||
| [jaeger](https://github.com/ipfs/go-jaeger-plugin) | Tracing | | An opentracing backend. |
|
||||
| [telemetry](https://github.com/ipfs/kubo/tree/master/plugin/plugins/telemetry) | Telemetry | x | Collects anonymized usage data for Kubo development. |
|
||||
|
||||
* **Preloaded** plugins are built into the Kubo binary and do not need to be
|
||||
installed separately. At the moment, all in-tree plugins are preloaded.
|
||||
|
||||
122
docs/telemetry.md
Normal file
122
docs/telemetry.md
Normal file
@ -0,0 +1,122 @@
|
||||
# Telemetry Plugin Documentation
|
||||
|
||||
The **Telemetry plugin** is a feature in Kubo that collects **anonymized usage data** to help the development team better understand how the software is used, identify areas for improvement, and guide future feature development.
|
||||
|
||||
This data is not personally identifiable and is used solely for the purpose of improving the Kubo project.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ How to Control Telemetry
|
||||
|
||||
The behavior of the Telemetry plugin is controlled via the environment variable [`IPFS_TELEMETRY`](environment-variables.md#ipfs_telemetry) and optionally via the `Plugins.Plugins.telemetry.Config.Mode` in the IPFS config file.
|
||||
|
||||
### Available Modes
|
||||
|
||||
| Mode | Description |
|
||||
|----------|-----------------------------------------------------------------------------|
|
||||
| `on` | **Default**. Telemetry is enabled. Data is sent periodically. |
|
||||
| `off` | Telemetry is disabled. No data is sent. Any existing telemetry UUID file is removed. |
|
||||
| `auto` | Like `on`, but logs an informative message about the telemetry and gives user 15 minutes to opt-out before first collection. This mode is automatically used on the first run when `IPFS_TELEMETRY` is not set and telemetry UUID is not found (not generated yet). The informative message is only shown once. |
|
||||
|
||||
You can set the mode in your environment:
|
||||
|
||||
```bash
|
||||
export IPFS_TELEMETRY="off"
|
||||
```
|
||||
|
||||
Or in your IPFS config file:
|
||||
|
||||
```json
|
||||
{
|
||||
"Plugins": {
|
||||
"Plugins": {
|
||||
"telemetry": {
|
||||
"Config": {
|
||||
"Mode": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 What Data is Collected?
|
||||
|
||||
The telemetry plugin collects the following anonymized data:
|
||||
|
||||
### General Information
|
||||
- **Agent version**: The version of Kubo being used.
|
||||
- **Platform details**: Operating system, architecture, and container status.
|
||||
- **Uptime**: How long the node has been running, categorized into buckets.
|
||||
- **Repo size**: Categorized into buckets (e.g., 1GB, 5GB, 10GB, etc.).
|
||||
|
||||
### Network Configuration
|
||||
- **Private network**: Whether the node is running in a private network.
|
||||
- **Bootstrap peers**: Whether custom bootstrap peers are used.
|
||||
- **Routing type**: Whether the node uses DHT, IPFS, or a custom routing setup.
|
||||
- **AutoNAT settings**: Whether AutoNAT is enabled and its reachability status.
|
||||
- **Swarm settings**: Whether hole punching is enabled, and whether public IP addresses are used.
|
||||
|
||||
### TLS and Discovery
|
||||
- **AutoTLS settings**: Whether WSS is enabled and whether a custom domain suffix is used.
|
||||
- **Discovery settings**: Whether mDNS is enabled.
|
||||
|
||||
### Reprovider Strategy
|
||||
- The strategy used for reprovider (e.g., "all", "pinned"...).
|
||||
|
||||
---
|
||||
|
||||
## 🧑🤝🧑 Privacy and Anonymization
|
||||
|
||||
All data collected is:
|
||||
- **Anonymized**: No personally identifiable information (PII) is sent.
|
||||
- **Optional**: Users can choose to opt out at any time.
|
||||
- **Secure**: Data is sent over HTTPS to a trusted endpoint.
|
||||
|
||||
The telemetry UUID is stored in the IPFS repo folder and is used to identify the node across runs, but it does not contain any personal information. When you opt-out, this UUID file is automatically removed to ensure complete privacy.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Contributing to the Project
|
||||
|
||||
By enabling telemetry, you are helping the Kubo team improve the software for the entire community. The data is used to:
|
||||
|
||||
- Prioritize feature development
|
||||
- Identify performance bottlenecks
|
||||
- Improve user experience
|
||||
|
||||
You can always disable telemetry at any time if you change your mind.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Telemetry
|
||||
|
||||
If you're testing telemetry locally, you can change the endpoint by setting the `Endpoint` field in the config:
|
||||
|
||||
```json
|
||||
{
|
||||
"Plugins": {
|
||||
"Plugins": {
|
||||
"telemetry": {
|
||||
"Config": {
|
||||
"Mode": "on",
|
||||
"Endpoint": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to capture and inspect telemetry data locally.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Further Reading
|
||||
|
||||
For more information, see:
|
||||
- [IPFS Environment Variables](docs/environment-variables.md)
|
||||
- [IPFS Plugins](docs/plugins.md)
|
||||
- [IPFS Configuration](docs/config.md)
|
||||
@ -10,6 +10,7 @@ import (
|
||||
pluginnopfs "github.com/ipfs/kubo/plugin/plugins/nopfs"
|
||||
pluginpebbleds "github.com/ipfs/kubo/plugin/plugins/pebbleds"
|
||||
pluginpeerlog "github.com/ipfs/kubo/plugin/plugins/peerlog"
|
||||
plugintelemetry "github.com/ipfs/kubo/plugin/plugins/telemetry"
|
||||
)
|
||||
|
||||
// DO NOT EDIT THIS FILE
|
||||
@ -26,4 +27,5 @@ func init() {
|
||||
Preload(pluginpeerlog.Plugins...)
|
||||
Preload(pluginfxtest.Plugins...)
|
||||
Preload(pluginnopfs.Plugins...)
|
||||
Preload(plugintelemetry.Plugins...)
|
||||
}
|
||||
|
||||
@ -13,3 +13,4 @@ pebbleds github.com/ipfs/kubo/plugin/plugins/pebbleds *
|
||||
peerlog github.com/ipfs/kubo/plugin/plugins/peerlog *
|
||||
fxtest github.com/ipfs/kubo/plugin/plugins/fxtest *
|
||||
nopfs github.com/ipfs/kubo/plugin/plugins/nopfs *
|
||||
telemetry github.com/ipfs/kubo/plugin/plugins/telemetry *
|
||||
|
||||
560
plugin/plugins/telemetry/telemetry.go
Normal file
560
plugin/plugins/telemetry/telemetry.go
Normal file
@ -0,0 +1,560 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
logging "github.com/ipfs/go-log/v2"
|
||||
ipfs "github.com/ipfs/kubo"
|
||||
"github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/core"
|
||||
"github.com/ipfs/kubo/core/corerepo"
|
||||
"github.com/ipfs/kubo/plugin"
|
||||
"github.com/libp2p/go-libp2p/core/network"
|
||||
"github.com/libp2p/go-libp2p/core/pnet"
|
||||
multiaddr "github.com/multiformats/go-multiaddr"
|
||||
manet "github.com/multiformats/go-multiaddr/net"
|
||||
)
|
||||
|
||||
var log = logging.Logger("telemetry")
|
||||
|
||||
const (
|
||||
modeEnvVar = "IPFS_TELEMETRY"
|
||||
uuidFilename = "telemetry_uuid"
|
||||
endpoint = "https://telemetry.ipshipyard.dev"
|
||||
sendDelay = 15 * time.Minute // delay before first telemetry collection after daemon start
|
||||
sendInterval = 24 * time.Hour // interval between telemetry collections after the first one
|
||||
httpTimeout = 30 * time.Second // timeout for telemetry HTTP requests
|
||||
)
|
||||
|
||||
type pluginMode int
|
||||
|
||||
const (
|
||||
modeAuto pluginMode = iota
|
||||
modeOn
|
||||
modeOff
|
||||
)
|
||||
|
||||
// repoSizeBuckets defines size thresholds for categorizing repository sizes.
|
||||
// Each value represents the upper limit of a bucket in bytes (except the last)
|
||||
var repoSizeBuckets = []uint64{
|
||||
1 << 30, // 1 GB
|
||||
5 << 30, // 5 GB
|
||||
10 << 30, // 10 GB
|
||||
100 << 30, // 100 GB
|
||||
500 << 30, // 500 GB
|
||||
1 << 40, // 1 TB
|
||||
10 << 40, // 10 TB
|
||||
11 << 40, // + anything more than 10TB falls here.
|
||||
}
|
||||
|
||||
var uptimeBuckets = []time.Duration{
|
||||
1 * 24 * time.Hour,
|
||||
2 * 24 * time.Hour,
|
||||
3 * 24 * time.Hour,
|
||||
7 * 24 * time.Hour,
|
||||
14 * 24 * time.Hour,
|
||||
30 * 24 * time.Hour,
|
||||
31 * 24 * time.Hour, // + anything more than 30 days falls here.
|
||||
}
|
||||
|
||||
// A LogEvent is the object sent to the telemetry endpoint.
|
||||
type LogEvent struct {
|
||||
UUID string `json:"uuid"`
|
||||
|
||||
AgentVersion string `json:"agent_version"`
|
||||
|
||||
PrivateNetwork bool `json:"private_network"`
|
||||
|
||||
BootstrappersCustom bool `json:"bootstrappers_custom"`
|
||||
|
||||
RepoSizeBucket uint64 `json:"repo_size_bucket"`
|
||||
|
||||
UptimeBucket time.Duration `json:"uptime_bucket"`
|
||||
|
||||
ReproviderStrategy string `json:"reprovider_strategy"`
|
||||
|
||||
RoutingType string `json:"routing_type"`
|
||||
RoutingAcceleratedDHTClient bool `json:"routing_accelerated_dht_client"`
|
||||
RoutingDelegatedCount int `json:"routing_delegated_count"`
|
||||
|
||||
AutoNATServiceMode string `json:"autonat_service_mode"`
|
||||
AutoNATReachability string `json:"autonat_reachability"`
|
||||
|
||||
SwarmEnableHolePunching bool `json:"swarm_enable_hole_punching"`
|
||||
SwarmCircuitAddresses bool `json:"swarm_circuit_addresses"`
|
||||
SwarmIPv4PublicAddresses bool `json:"swarm_ipv4_public_addresses"`
|
||||
SwarmIPv6PublicAddresses bool `json:"swarm_ipv6_public_addresses"`
|
||||
|
||||
AutoTLSAutoWSS bool `json:"auto_tls_auto_wss"`
|
||||
AutoTLSDomainSuffixCustom bool `json:"auto_tls_domain_suffix_custom"`
|
||||
|
||||
DiscoveryMDNSEnabled bool `json:"discovery_mdns_enabled"`
|
||||
|
||||
PlatformOS string `json:"platform_os"`
|
||||
PlatformArch string `json:"platform_arch"`
|
||||
PlatformContainerized bool `json:"platform_containerized"`
|
||||
PlatformVM bool `json:"platform_vm"`
|
||||
}
|
||||
|
||||
var Plugins = []plugin.Plugin{
|
||||
&telemetryPlugin{},
|
||||
}
|
||||
|
||||
type telemetryPlugin struct {
|
||||
uuidFilename string
|
||||
mode pluginMode
|
||||
endpoint string
|
||||
runOnce bool // test-only flag: when true, sends telemetry immediately without delay
|
||||
sendDelay time.Duration
|
||||
|
||||
node *core.IpfsNode
|
||||
config *config.Config
|
||||
event *LogEvent
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) Name() string {
|
||||
return "telemetry"
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) Version() string {
|
||||
return "0.0.1"
|
||||
}
|
||||
|
||||
func readFromConfig(cfg interface{}, key string) string {
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pcfg, ok := cfg.(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
val, ok := pcfg[key].(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) Init(env *plugin.Environment) error {
|
||||
// logging.SetLogLevel("telemetry", "DEBUG")
|
||||
log.Debug("telemetry plugin Init()")
|
||||
p.event = &LogEvent{}
|
||||
p.startTime = time.Now()
|
||||
|
||||
repoPath := env.Repo
|
||||
p.uuidFilename = path.Join(repoPath, uuidFilename)
|
||||
|
||||
v := os.Getenv(modeEnvVar)
|
||||
if v != "" {
|
||||
log.Debug("mode set from env-var")
|
||||
} else if pmode := readFromConfig(env.Config, "Mode"); pmode != "" {
|
||||
v = pmode
|
||||
log.Debug("mode set from config")
|
||||
}
|
||||
|
||||
// read "Delay" from the config. Parse as duration. Set p.sendDelay to it
|
||||
// or set default.
|
||||
if delayStr := readFromConfig(env.Config, "Delay"); delayStr != "" {
|
||||
delay, err := time.ParseDuration(delayStr)
|
||||
if err != nil {
|
||||
log.Debug("sendDelay set from default")
|
||||
p.sendDelay = sendDelay
|
||||
} else {
|
||||
log.Debug("sendDelay set from config")
|
||||
p.sendDelay = delay
|
||||
}
|
||||
} else {
|
||||
log.Debug("sendDelay set from default")
|
||||
p.sendDelay = sendDelay
|
||||
}
|
||||
|
||||
p.endpoint = endpoint
|
||||
if ep := readFromConfig(env.Config, "Endpoint"); ep != "" {
|
||||
log.Debug("endpoint set from config", ep)
|
||||
p.endpoint = ep
|
||||
}
|
||||
|
||||
switch v {
|
||||
case "off":
|
||||
p.mode = modeOff
|
||||
log.Debug("telemetry disabled via opt-out")
|
||||
// Remove UUID file if it exists when user opts out
|
||||
if _, err := os.Stat(p.uuidFilename); err == nil {
|
||||
if err := os.Remove(p.uuidFilename); err != nil {
|
||||
log.Debugf("failed to remove telemetry UUID file: %s", err)
|
||||
} else {
|
||||
log.Debug("removed existing telemetry UUID file due to opt-out")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "auto":
|
||||
p.mode = modeAuto
|
||||
default:
|
||||
p.mode = modeOn
|
||||
}
|
||||
log.Debug("telemetry mode: ", p.mode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) loadUUID() error {
|
||||
// Generate or read our UUID from disk
|
||||
b, err := os.ReadFile(p.uuidFilename)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Errorf("error reading telemetry uuid from disk: %s", err)
|
||||
return err
|
||||
}
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
log.Errorf("cannot generate telemetry uuid: %s", err)
|
||||
return err
|
||||
}
|
||||
p.event.UUID = uid.String()
|
||||
p.mode = modeAuto
|
||||
log.Debugf("new telemetry UUID %s. Mode set to Auto", uid)
|
||||
|
||||
// Write the UUID to disk
|
||||
if err := os.WriteFile(p.uuidFilename, []byte(p.event.UUID), 0600); err != nil {
|
||||
log.Errorf("cannot write telemetry uuid: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
v := string(b)
|
||||
v = strings.TrimSpace(v)
|
||||
uid, err := uuid.Parse(v)
|
||||
if err != nil {
|
||||
log.Errorf("cannot parse telemetry uuid: %s", err)
|
||||
return err
|
||||
}
|
||||
log.Debugf("uuid read from disk %s", uid)
|
||||
p.event.UUID = uid.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) hasDefaultBootstrapPeers() bool {
|
||||
defaultPeers := config.DefaultBootstrapAddresses
|
||||
currentPeers := p.config.Bootstrap
|
||||
if len(defaultPeers) != len(currentPeers) {
|
||||
return false
|
||||
}
|
||||
peerMap := make(map[string]struct{}, len(defaultPeers))
|
||||
for _, peer := range defaultPeers {
|
||||
peerMap[peer] = struct{}{}
|
||||
}
|
||||
for _, peer := range currentPeers {
|
||||
if _, ok := peerMap[peer]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) showInfo() {
|
||||
fmt.Printf(`
|
||||
|
||||
ℹ️ Anonymous telemetry will be enabled in %s
|
||||
|
||||
Kubo will collect anonymous usage data to help improve the software:
|
||||
• What: Feature usage and configuration (no personal data)
|
||||
Use GOLOG_LOG_LEVEL="telemetry=debug" to inspect collected data
|
||||
• When: First collection in %s, then every 24h
|
||||
• How: HTTP POST to %s
|
||||
Anonymous ID: %s
|
||||
|
||||
No data sent yet. To opt-out before collection starts:
|
||||
• Set environment: %s=off
|
||||
• Or run: ipfs config Plugins.Plugins.telemetry.Config.Mode off
|
||||
• Then restart daemon
|
||||
|
||||
This message is shown only once.
|
||||
Learn more: https://github.com/ipfs/kubo/blob/master/docs/telemetry.md
|
||||
|
||||
|
||||
`, p.sendDelay, p.sendDelay, endpoint, p.event.UUID, modeEnvVar)
|
||||
}
|
||||
|
||||
// Start finishes telemetry initialization once the IpfsNode is ready,
|
||||
// collects telemetry data and sends it to the endpoint.
|
||||
func (p *telemetryPlugin) Start(n *core.IpfsNode) error {
|
||||
// We should not be crashing the daemon due to problems with telemetry
|
||||
// so this is always going to return nil and panics are going to be
|
||||
// handled.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("telemetry plugin panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
p.node = n
|
||||
cfg, err := n.Repo.Config()
|
||||
if err != nil {
|
||||
log.Error("error getting the repo.Config: %s", err)
|
||||
return nil
|
||||
}
|
||||
p.config = cfg
|
||||
if p.mode == modeOff {
|
||||
log.Debug("telemetry collection skipped: opted out")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !n.IsDaemon || !n.IsOnline {
|
||||
log.Debugf("skipping telemetry. Daemon: %t. Online: %t", n.IsDaemon, n.IsOnline)
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadUUID might switch to modeAuto when generating a new uuid
|
||||
if err := p.loadUUID(); err != nil {
|
||||
p.mode = modeOff
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.mode == modeAuto {
|
||||
p.showInfo()
|
||||
}
|
||||
|
||||
// runOnce is only used in tests to send telemetry immediately.
|
||||
// In production, this is always false, ensuring users get the 15-minute delay.
|
||||
if p.runOnce {
|
||||
p.prepareEvent()
|
||||
return p.sendTelemetry()
|
||||
}
|
||||
|
||||
go func() {
|
||||
timer := time.NewTimer(p.sendDelay)
|
||||
for range timer.C {
|
||||
p.prepareEvent()
|
||||
if err := p.sendTelemetry(); err != nil {
|
||||
log.Warnf("telemetry submission failed: %s (will retry in %s)", err, sendInterval)
|
||||
}
|
||||
timer.Reset(sendInterval)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) prepareEvent() {
|
||||
p.collectBasicInfo()
|
||||
p.collectRoutingInfo()
|
||||
p.collectAutoNATInfo()
|
||||
p.collectSwarmInfo()
|
||||
p.collectAutoTLSInfo()
|
||||
p.collectDiscoveryInfo()
|
||||
p.collectPlatformInfo()
|
||||
}
|
||||
|
||||
// Collects:
|
||||
// * AgentVersion
|
||||
// * PrivateNetwork
|
||||
// * RepoSizeBucket
|
||||
// * BootstrappersCustom
|
||||
// * UptimeBucket
|
||||
// * ReproviderStrategy
|
||||
func (p *telemetryPlugin) collectBasicInfo() {
|
||||
p.event.AgentVersion = ipfs.GetUserAgentVersion()
|
||||
|
||||
privNet := false
|
||||
if pnet.ForcePrivateNetwork {
|
||||
privNet = true
|
||||
} else if key, _ := p.node.Repo.SwarmKey(); key != nil {
|
||||
privNet = true
|
||||
}
|
||||
p.event.PrivateNetwork = privNet
|
||||
|
||||
p.event.BootstrappersCustom = !p.hasDefaultBootstrapPeers()
|
||||
|
||||
repoSizeBucket := repoSizeBuckets[len(repoSizeBuckets)-1]
|
||||
sizeStat, err := corerepo.RepoSize(context.Background(), p.node)
|
||||
if err == nil {
|
||||
for _, b := range repoSizeBuckets {
|
||||
if sizeStat.RepoSize > b {
|
||||
continue
|
||||
}
|
||||
repoSizeBucket = b
|
||||
break
|
||||
}
|
||||
p.event.RepoSizeBucket = repoSizeBucket
|
||||
} else {
|
||||
log.Debugf("error setting sizeStat: %s", err)
|
||||
}
|
||||
|
||||
uptime := time.Since(p.startTime)
|
||||
uptimeBucket := uptimeBuckets[len(uptimeBuckets)-1]
|
||||
for _, bucket := range uptimeBuckets {
|
||||
if uptime > bucket {
|
||||
continue
|
||||
|
||||
}
|
||||
uptimeBucket = bucket
|
||||
break
|
||||
}
|
||||
p.event.UptimeBucket = uptimeBucket
|
||||
|
||||
p.event.ReproviderStrategy = p.config.Reprovider.Strategy.WithDefault(config.DefaultReproviderStrategy)
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) collectRoutingInfo() {
|
||||
p.event.RoutingType = p.config.Routing.Type.WithDefault("auto")
|
||||
p.event.RoutingAcceleratedDHTClient = p.config.Routing.AcceleratedDHTClient.WithDefault(false)
|
||||
p.event.RoutingDelegatedCount = len(p.config.Routing.DelegatedRouters)
|
||||
}
|
||||
|
||||
type reachabilityHost interface {
|
||||
Reachability() network.Reachability
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) collectAutoNATInfo() {
|
||||
autonat := p.config.AutoNAT.ServiceMode
|
||||
if autonat == config.AutoNATServiceUnset {
|
||||
autonat = config.AutoNATServiceEnabled
|
||||
}
|
||||
autoNATSvcModeB, err := autonat.MarshalText()
|
||||
if err == nil {
|
||||
autoNATSvcMode := string(autoNATSvcModeB)
|
||||
if autoNATSvcMode == "" {
|
||||
autoNATSvcMode = "unset"
|
||||
}
|
||||
p.event.AutoNATServiceMode = autoNATSvcMode
|
||||
}
|
||||
|
||||
h := p.node.PeerHost
|
||||
reachHost, ok := h.(reachabilityHost)
|
||||
if ok {
|
||||
p.event.AutoNATReachability = reachHost.Reachability().String()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) collectSwarmInfo() {
|
||||
p.event.SwarmEnableHolePunching = p.config.Swarm.EnableHolePunching.WithDefault(true)
|
||||
|
||||
var circuitAddrs, publicIP4Addrs, publicIP6Addrs bool
|
||||
for _, addr := range p.node.PeerHost.Addrs() {
|
||||
if manet.IsPublicAddr(addr) {
|
||||
if _, err := addr.ValueForProtocol(multiaddr.P_IP4); err == nil {
|
||||
publicIP4Addrs = true
|
||||
} else if _, err := addr.ValueForProtocol(multiaddr.P_IP6); err == nil {
|
||||
publicIP6Addrs = true
|
||||
}
|
||||
}
|
||||
if _, err := addr.ValueForProtocol(multiaddr.P_CIRCUIT); err == nil {
|
||||
circuitAddrs = true
|
||||
}
|
||||
}
|
||||
|
||||
p.event.SwarmCircuitAddresses = circuitAddrs
|
||||
p.event.SwarmIPv4PublicAddresses = publicIP4Addrs
|
||||
p.event.SwarmIPv6PublicAddresses = publicIP6Addrs
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) collectAutoTLSInfo() {
|
||||
p.event.AutoTLSAutoWSS = p.config.AutoTLS.AutoWSS.WithDefault(config.DefaultAutoWSS)
|
||||
domainSuffix := p.config.AutoTLS.DomainSuffix.WithDefault(config.DefaultDomainSuffix)
|
||||
p.event.AutoTLSDomainSuffixCustom = domainSuffix != config.DefaultDomainSuffix
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) collectDiscoveryInfo() {
|
||||
p.event.DiscoveryMDNSEnabled = p.config.Discovery.MDNS.Enabled
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) collectPlatformInfo() {
|
||||
p.event.PlatformOS = runtime.GOOS
|
||||
p.event.PlatformArch = runtime.GOARCH
|
||||
p.event.PlatformContainerized = isRunningInContainer()
|
||||
p.event.PlatformVM = isRunningInVM()
|
||||
}
|
||||
|
||||
func isRunningInContainer() bool {
|
||||
// Check for Docker container
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check cgroup for container
|
||||
content, err := os.ReadFile("/proc/self/cgroup")
|
||||
if err == nil {
|
||||
if strings.Contains(string(content), "docker") || strings.Contains(string(content), "lxc") || strings.Contains(string(content), "/kubepods") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
content, err = os.ReadFile("/proc/self/mountinfo")
|
||||
if err == nil {
|
||||
for line := range strings.Lines(string(content)) {
|
||||
if strings.Contains(line, "overlay") && strings.Contains(line, "/var/lib/containers/storage/overlay") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for systemd-nspawn
|
||||
if _, err := os.Stat("/run/systemd/container"); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isRunningInVM() bool {
|
||||
// Check for VM
|
||||
if _, err := os.Stat("/sys/hypervisor/uuid"); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for other VM indicators
|
||||
if _, err := os.Stat("/dev/virt-0"); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *telemetryPlugin) sendTelemetry() error {
|
||||
data, err := json.MarshalIndent(p.event, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("sending telemetry:\n %s", data)
|
||||
|
||||
req, err := http.NewRequest("POST", p.endpoint, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", ipfs.GetUserAgentVersion())
|
||||
req.Close = true
|
||||
|
||||
// Use client with timeout to prevent hanging
|
||||
client := &http.Client{
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("failed to send telemetry: %s", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
err := fmt.Errorf("telemetry endpoint returned HTTP %d", resp.StatusCode)
|
||||
log.Debug(err)
|
||||
return err
|
||||
}
|
||||
log.Debugf("telemetry sent successfully (%d)", resp.StatusCode)
|
||||
return nil
|
||||
}
|
||||
170
plugin/plugins/telemetry/telemetry_test.go
Normal file
170
plugin/plugins/telemetry/telemetry_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cockroachdb/pebble/v2"
|
||||
logging "github.com/ipfs/go-log/v2"
|
||||
"github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/core"
|
||||
"github.com/ipfs/kubo/core/node/libp2p"
|
||||
"github.com/ipfs/kubo/plugin"
|
||||
"github.com/ipfs/kubo/plugin/plugins/pebbleds"
|
||||
"github.com/ipfs/kubo/repo/fsrepo"
|
||||
)
|
||||
|
||||
func mockServer(t *testing.T) (*httptest.Server, func() LogEvent) {
|
||||
t.Helper()
|
||||
|
||||
var e LogEvent
|
||||
|
||||
// Create a mock HTTP test server
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is POST to the correct endpoint
|
||||
if r.Method != "POST" || r.URL.Path != "/" {
|
||||
t.Log("invalid request")
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check content type
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Log("invalid content type")
|
||||
http.Error(w, "invalid content type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the body is not empty
|
||||
if r.Body == nil {
|
||||
t.Log("empty body")
|
||||
http.Error(w, "empty body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the body
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if len(body) == 0 {
|
||||
t.Log("zero-length body")
|
||||
http.Error(w, "empty body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Received telemetry:\n %s", string(body))
|
||||
|
||||
err := json.Unmarshal(body, &e)
|
||||
if err != nil {
|
||||
t.Log("error unmarshaling event", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Return success
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})), func() LogEvent { return e }
|
||||
}
|
||||
|
||||
func makeNode(t *testing.T) (node *core.IpfsNode, repopath string) {
|
||||
t.Helper()
|
||||
|
||||
// Create a Temporary Repo
|
||||
repoPath, err := os.MkdirTemp("", "ipfs-shell")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pebbledspli := pebbleds.Plugins[0]
|
||||
pebbledspl, ok := pebbledspli.(plugin.PluginDatastore)
|
||||
if !ok {
|
||||
t.Fatal("bad datastore plugin")
|
||||
}
|
||||
|
||||
err = fsrepo.AddDatastoreConfigHandler(pebbledspl.DatastoreTypeName(), pebbledspl.DatastoreConfigParser())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a config with default options and a 2048 bit key
|
||||
cfg, err := config.Init(io.Discard, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg.Datastore.Spec = map[string]interface{}{
|
||||
"type": "pebbleds",
|
||||
"prefix": "pebble.datastore",
|
||||
"path": "pebbleds",
|
||||
"formatMajorVersion": int(pebble.FormatNewest),
|
||||
}
|
||||
|
||||
// Create the repo with the config
|
||||
err = fsrepo.Init(repoPath, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Open the repo
|
||||
repo, err := fsrepo.Open(repoPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Construct the node
|
||||
|
||||
nodeOptions := &core.BuildCfg{
|
||||
Online: true,
|
||||
Routing: libp2p.NilRouterOption,
|
||||
Repo: repo,
|
||||
}
|
||||
|
||||
node, err = core.NewNode(context.Background(), nodeOptions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
node.IsDaemon = true
|
||||
return
|
||||
}
|
||||
|
||||
func TestSendTelemetry(t *testing.T) {
|
||||
if err := logging.SetLogLevel("telemetry", "DEBUG"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ts, eventGetter := mockServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
node, repoPath := makeNode(t)
|
||||
|
||||
// Create a plugin instance
|
||||
p := &telemetryPlugin{
|
||||
runOnce: true,
|
||||
}
|
||||
|
||||
// Initialize the plugin
|
||||
pe := &plugin.Environment{
|
||||
Repo: repoPath,
|
||||
Config: nil,
|
||||
}
|
||||
err := p.Init(pe)
|
||||
if err != nil {
|
||||
t.Fatalf("Init() failed: %v", err)
|
||||
}
|
||||
|
||||
p.endpoint = ts.URL
|
||||
|
||||
// Start the plugin
|
||||
err = p.Start(node)
|
||||
if err != nil {
|
||||
t.Fatalf("Start() failed: %v", err)
|
||||
}
|
||||
|
||||
e := eventGetter()
|
||||
if e.UUID != p.event.UUID {
|
||||
t.Fatal("uuid mismatch")
|
||||
}
|
||||
}
|
||||
1
plugin/plugins/telemetry/telemetry_uuid
Normal file
1
plugin/plugins/telemetry/telemetry_uuid
Normal file
@ -0,0 +1 @@
|
||||
289ffed8-c770-49ae-922f-b020c8f776f2
|
||||
184
test/cli/telemetry_test.go
Normal file
184
test/cli/telemetry_test.go
Normal file
@ -0,0 +1,184 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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")
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user