Skip to content

Commit 2a11353

Browse files
committed
feat(sdk): add DO_NOT_TRACK and SHM_COLLECT_SYSTEM_METRICS env vars
- DO_NOT_TRACK=true|1 completely disables telemetry (overrides config) - SHM_COLLECT_SYSTEM_METRICS=false|0 disables system metrics collection - Applied to both Go and Node.js SDKs - Updated documentation (README, SDK READMEs, DEPLOYMENT.md)
1 parent 5d06d5e commit 2a11353

File tree

9 files changed

+332
-13
lines changed

9 files changed

+332
-13
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,24 @@ graph LR
344344
* **Authentication:** The server uses a "Trust on First Use" (TOFU) or explicit activation model. Once an ID is registered with a Public Key, only that key can sign updates.
345345
* **Transparency:** You should always inform your users that telemetry is active and allow them to opt-out via the `Enabled: false` config.
346346

347+
### Respecting User Privacy with `DO_NOT_TRACK`
348+
349+
All SHM clients (Go, Node.js, and future SDKs) respect the standard `DO_NOT_TRACK` environment variable. When set to `true` or `1`, **all telemetry is completely disabled** — no data is sent to the server.
350+
351+
```bash
352+
# Disable all telemetry
353+
export DO_NOT_TRACK=true
354+
```
355+
356+
This allows end-users to opt-out of telemetry at the system level, regardless of the application's configuration.
357+
358+
### Environment Variables (Client-side)
359+
360+
| Variable | Effect |
361+
|----------|--------|
362+
| `DO_NOT_TRACK=true` or `1` | Completely disables telemetry (no network requests) |
363+
| `SHM_COLLECT_SYSTEM_METRICS=false` or `0` | Disables automatic system metrics collection (OS, CPU, memory) while still sending custom metrics |
364+
347365
---
348366

349367
## 🤝 Contributing

docs/DEPLOYMENT.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ caddy hash-password --plaintext 'your-secure-password'
400400

401401
## Environment Variables
402402

403+
### Server-side
404+
403405
| Variable | Default | Description |
404406
|----------|---------|-------------|
405407
| `SHM_DB_DSN` | (required) | PostgreSQL connection string |
@@ -408,6 +410,34 @@ caddy hash-password --plaintext 'your-secure-password'
408410

409411
For the full list of environment variables (including rate limiting), see [README.md](../README.md#environment-variables).
410412

413+
### Client-side (SDK)
414+
415+
These environment variables are read by the SHM clients (Go, Node.js) running in your applications:
416+
417+
| Variable | Default | Description |
418+
|----------|---------|-------------|
419+
| `DO_NOT_TRACK` | - | Set to `true` or `1` to **completely disable telemetry**. No data will be sent to the server. This overrides the `enabled` configuration option. |
420+
| `SHM_COLLECT_SYSTEM_METRICS` | `true` | Set to `false` or `0` to disable automatic system metrics collection (OS, CPU, memory). Custom metrics will still be sent. |
421+
422+
#### Respecting User Privacy
423+
424+
All SHM clients respect the standard `DO_NOT_TRACK` environment variable. This allows end-users to opt-out of telemetry at the system level:
425+
426+
```bash
427+
# In the environment where your application runs
428+
export DO_NOT_TRACK=true
429+
```
430+
431+
When `DO_NOT_TRACK` is enabled:
432+
- No network requests are made to the SHM server
433+
- No identity file is accessed
434+
- The client silently disables itself
435+
436+
This is useful for:
437+
- Users who want to opt-out of all telemetry
438+
- Development/testing environments
439+
- Privacy-conscious deployments
440+
411441
---
412442

413443
## Database Backups

sdk/golang/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ func main() {
6565
| `Environment` | `string` | `""` | Environment identifier (production, staging, etc.) |
6666
| `Enabled` | `bool` | `false` | Enable/disable telemetry |
6767
| `ReportInterval` | `time.Duration` | `1h` | Interval between snapshots (minimum: 1m) |
68+
| `CollectSystemMetrics` | `bool` | `false` | Collect OS/runtime metrics (use `CollectSystemMetricsFromEnv()`) |
69+
70+
## Environment Variables
71+
72+
| Variable | Effect |
73+
|----------|--------|
74+
| `DO_NOT_TRACK=true` or `1` | **Completely disables telemetry** — overrides `Enabled: true` |
75+
| `SHM_COLLECT_SYSTEM_METRICS=false` or `0` | Disables system metrics collection (enabled by default) |
76+
77+
### Example with environment variables
78+
79+
```go
80+
client, _ := shm.New(shm.Config{
81+
ServerURL: "https://telemetry.example.com",
82+
AppName: "my-app",
83+
AppVersion: "1.0.0",
84+
Enabled: true,
85+
CollectSystemMetrics: shm.CollectSystemMetricsFromEnv(), // reads SHM_COLLECT_SYSTEM_METRICS
86+
})
87+
```
88+
89+
> **Note:** If `DO_NOT_TRACK=true` or `DO_NOT_TRACK=1` is set, the client will be disabled regardless of the `Enabled` configuration.
6890
6991
## How It Works
7092

sdk/golang/client.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import (
2020
)
2121

2222
type Config struct {
23-
ServerURL string
24-
AppName string
25-
AppVersion string
26-
DataDir string // where is store app_shm_identity.json
27-
Environment string // prod, staging, ...
28-
Enabled bool
29-
ReportInterval time.Duration // snapshots interval (default: 1h)
23+
ServerURL string
24+
AppName string
25+
AppVersion string
26+
DataDir string // where is store app_shm_identity.json
27+
Environment string // prod, staging, ...
28+
Enabled bool
29+
ReportInterval time.Duration // snapshots interval (default: 1h)
30+
CollectSystemMetrics bool // collect OS/runtime metrics (env: SHM_COLLECT_SYSTEM_METRICS)
3031
}
3132

3233
type MetricsProvider func() map[string]interface{}
@@ -50,6 +51,10 @@ func New(cfg Config) (*Client, error) {
5051
cfg.ReportInterval = time.Minute
5152
}
5253

54+
if isDoNotTrack() {
55+
cfg.Enabled = false
56+
}
57+
5358
ensureDataDir(cfg.DataDir)
5459
idPath := cfg.DataDir + "/" + slug(cfg.AppName) + "_shm_identity.json"
5560
id, err := loadOrGenerateIdentity(idPath)
@@ -157,9 +162,11 @@ func (c *Client) sendSnapshot() {
157162
if c.provider != nil {
158163
data = c.provider()
159164
}
160-
sysData := c.getSystemMetrics()
161-
for k, v := range sysData {
162-
data[k] = v
165+
if c.config.CollectSystemMetrics {
166+
sysData := c.getSystemMetrics()
167+
for k, v := range sysData {
168+
data[k] = v
169+
}
163170
}
164171
metricsJSON, _ := json.Marshal(data)
165172

@@ -253,6 +260,16 @@ func ensureDataDir(dir string) {
253260
}
254261
}
255262

263+
func CollectSystemMetricsFromEnv() bool {
264+
val := strings.ToLower(os.Getenv("SHM_COLLECT_SYSTEM_METRICS"))
265+
return val != "false" && val != "0"
266+
}
267+
268+
func isDoNotTrack() bool {
269+
val := strings.ToLower(os.Getenv("DO_NOT_TRACK"))
270+
return val == "true" || val == "1"
271+
}
272+
256273
func slug(s string) string {
257274
s = strings.ToLower(s)
258275

sdk/golang/client_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,3 +600,191 @@ func TestClient_RegisterError_BadStatusCode(t *testing.T) {
600600
t.Errorf("error should mention status code: %v", err)
601601
}
602602
}
603+
604+
// =============================================================================
605+
// COLLECT SYSTEM METRICS TESTS
606+
// =============================================================================
607+
608+
func TestClient_SnapshotWithSystemMetrics(t *testing.T) {
609+
tmpDir := t.TempDir()
610+
611+
var receivedMetrics map[string]interface{}
612+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
613+
if r.URL.Path == "/v1/snapshot" {
614+
bodyBytes, _ := io.ReadAll(r.Body)
615+
var body map[string]interface{}
616+
json.Unmarshal(bodyBytes, &body)
617+
if metricsRaw, ok := body["metrics"].(string); ok {
618+
json.Unmarshal([]byte(metricsRaw), &receivedMetrics)
619+
} else if metricsMap, ok := body["metrics"].(map[string]interface{}); ok {
620+
receivedMetrics = metricsMap
621+
}
622+
}
623+
w.WriteHeader(http.StatusAccepted)
624+
}))
625+
defer server.Close()
626+
627+
cfg := Config{
628+
ServerURL: server.URL,
629+
AppName: "test-app",
630+
AppVersion: "1.0.0",
631+
DataDir: tmpDir,
632+
Enabled: true,
633+
CollectSystemMetrics: true,
634+
}
635+
636+
client, _ := New(cfg)
637+
client.SetProvider(func() map[string]interface{} {
638+
return map[string]interface{}{"custom_metric": 123}
639+
})
640+
client.sendSnapshot()
641+
642+
// Should contain system metrics
643+
systemFields := []string{"sys_os", "sys_arch", "sys_cpu_cores", "sys_go_version", "sys_mode"}
644+
for _, field := range systemFields {
645+
if _, ok := receivedMetrics[field]; !ok {
646+
t.Errorf("expected system metric %q when CollectSystemMetrics=true", field)
647+
}
648+
}
649+
650+
// Should also contain custom metric
651+
if receivedMetrics["custom_metric"] != float64(123) {
652+
t.Errorf("custom_metric = %v, want 123", receivedMetrics["custom_metric"])
653+
}
654+
}
655+
656+
func TestClient_SnapshotWithoutSystemMetrics(t *testing.T) {
657+
tmpDir := t.TempDir()
658+
659+
var receivedMetrics map[string]interface{}
660+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
661+
if r.URL.Path == "/v1/snapshot" {
662+
bodyBytes, _ := io.ReadAll(r.Body)
663+
var body map[string]interface{}
664+
json.Unmarshal(bodyBytes, &body)
665+
if metricsRaw, ok := body["metrics"].(string); ok {
666+
json.Unmarshal([]byte(metricsRaw), &receivedMetrics)
667+
} else if metricsMap, ok := body["metrics"].(map[string]interface{}); ok {
668+
receivedMetrics = metricsMap
669+
}
670+
}
671+
w.WriteHeader(http.StatusAccepted)
672+
}))
673+
defer server.Close()
674+
675+
cfg := Config{
676+
ServerURL: server.URL,
677+
AppName: "test-app",
678+
AppVersion: "1.0.0",
679+
DataDir: tmpDir,
680+
Enabled: true,
681+
CollectSystemMetrics: false,
682+
}
683+
684+
client, _ := New(cfg)
685+
client.SetProvider(func() map[string]interface{} {
686+
return map[string]interface{}{"custom_metric": 456}
687+
})
688+
client.sendSnapshot()
689+
690+
// Should NOT contain system metrics
691+
systemFields := []string{"sys_os", "sys_arch", "sys_cpu_cores", "sys_go_version", "sys_mode"}
692+
for _, field := range systemFields {
693+
if _, ok := receivedMetrics[field]; ok {
694+
t.Errorf("unexpected system metric %q when CollectSystemMetrics=false", field)
695+
}
696+
}
697+
698+
// Should still contain custom metric
699+
if receivedMetrics["custom_metric"] != float64(456) {
700+
t.Errorf("custom_metric = %v, want 456", receivedMetrics["custom_metric"])
701+
}
702+
}
703+
704+
func TestCollectSystemMetricsFromEnv(t *testing.T) {
705+
tests := []struct {
706+
envValue string
707+
expected bool
708+
}{
709+
{"", true}, // absent = enabled
710+
{"true", true},
711+
{"TRUE", true},
712+
{"1", true},
713+
{"false", false},
714+
{"FALSE", false},
715+
{"0", false},
716+
{"anything", true}, // unknown = enabled
717+
}
718+
719+
for _, tt := range tests {
720+
t.Run("env="+tt.envValue, func(t *testing.T) {
721+
if tt.envValue == "" {
722+
os.Unsetenv("SHM_COLLECT_SYSTEM_METRICS")
723+
} else {
724+
os.Setenv("SHM_COLLECT_SYSTEM_METRICS", tt.envValue)
725+
}
726+
defer os.Unsetenv("SHM_COLLECT_SYSTEM_METRICS")
727+
728+
result := CollectSystemMetricsFromEnv()
729+
if result != tt.expected {
730+
t.Errorf("CollectSystemMetricsFromEnv() with env=%q = %v, want %v", tt.envValue, result, tt.expected)
731+
}
732+
})
733+
}
734+
}
735+
736+
func TestDoNotTrack(t *testing.T) {
737+
tests := []struct {
738+
envValue string
739+
expected bool
740+
}{
741+
{"", false}, // absent = tracking allowed
742+
{"true", true}, // disabled
743+
{"TRUE", true}, // disabled
744+
{"1", true}, // disabled
745+
{"false", false}, // tracking allowed
746+
{"0", false}, // tracking allowed
747+
{"anything", false}, // unknown = tracking allowed
748+
}
749+
750+
for _, tt := range tests {
751+
t.Run("DO_NOT_TRACK="+tt.envValue, func(t *testing.T) {
752+
if tt.envValue == "" {
753+
os.Unsetenv("DO_NOT_TRACK")
754+
} else {
755+
os.Setenv("DO_NOT_TRACK", tt.envValue)
756+
}
757+
defer os.Unsetenv("DO_NOT_TRACK")
758+
759+
result := isDoNotTrack()
760+
if result != tt.expected {
761+
t.Errorf("isDoNotTrack() with env=%q = %v, want %v", tt.envValue, result, tt.expected)
762+
}
763+
})
764+
}
765+
}
766+
767+
func TestClient_DoNotTrack_DisablesClient(t *testing.T) {
768+
tmpDir := t.TempDir()
769+
770+
os.Setenv("DO_NOT_TRACK", "true")
771+
defer os.Unsetenv("DO_NOT_TRACK")
772+
773+
cfg := Config{
774+
ServerURL: "http://localhost:8080",
775+
AppName: "test-app",
776+
AppVersion: "1.0.0",
777+
DataDir: tmpDir,
778+
Enabled: true, // explicitly enabled
779+
}
780+
781+
client, err := New(cfg)
782+
if err != nil {
783+
t.Fatalf("New() error = %v", err)
784+
}
785+
786+
// DO_NOT_TRACK should override Enabled
787+
if client.config.Enabled != false {
788+
t.Errorf("config.Enabled = %v, want false when DO_NOT_TRACK=true", client.config.Enabled)
789+
}
790+
}

sdk/nodejs/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,33 @@ interface Config {
5050
environment?: string; // Environment identifier (production, staging, etc.)
5151
enabled?: boolean; // Enable/disable telemetry (default: true)
5252
reportIntervalMs?: number; // Interval between snapshots in ms (default: 3600000, min: 60000)
53+
collectSystemMetrics?: boolean; // Collect OS/runtime metrics (default: true via env)
5354
}
5455
```
5556

57+
## Environment Variables
58+
59+
| Variable | Effect |
60+
|----------|--------|
61+
| `DO_NOT_TRACK=true` or `1` | **Completely disables telemetry** — overrides `enabled: true` |
62+
| `SHM_COLLECT_SYSTEM_METRICS=false` or `0` | Disables system metrics collection (enabled by default) |
63+
64+
### Example with environment variables
65+
66+
```typescript
67+
import { SHMClient, collectSystemMetricsFromEnv } from '@btouchard/shm-sdk';
68+
69+
const client = new SHMClient({
70+
serverUrl: 'https://telemetry.example.com',
71+
appName: 'my-app',
72+
appVersion: '1.0.0',
73+
enabled: true,
74+
collectSystemMetrics: collectSystemMetricsFromEnv(), // reads SHM_COLLECT_SYSTEM_METRICS
75+
});
76+
```
77+
78+
> **Note:** If `DO_NOT_TRACK=true` or `DO_NOT_TRACK=1` is set, the client will be disabled regardless of the `enabled` configuration.
79+
5680
## How It Works
5781

5882
1. **Identity Generation**: On first run, the SDK generates an Ed25519 keypair and a unique instance ID, stored in `{dataDir}/{app-name}_shm_identity.json`

0 commit comments

Comments
 (0)