# add checkpoint_policy_blocked telemetry event

`b8fddf4`→[main](/content/gh/entireio/cli/commits/main/index.html)·

pfleidi·2w ago·2 files·+158 added/-0 removed

New event records when a hook is prevented from creating checkpoint
data by a checkpoint policy. Pure payload builder mirrors BuildEventPayload;
TrackCheckpointPolicyBlocked spawns the existing detached subprocess and
honors ENTIRE_TELEMETRY_OPTOUT. The settings.Telemetry opt-in check stays on
the CLI side.

## Sessions

72027b27d255View transcript

[?\
Add Checkpoint Policy Blocked TelemetryClaude Code·4 steps](/content/gh/entireio/cli/session/59674bd8-9edc-47fc-adae-9e93955ab9af#timeline-72027b27d255/index.html)

## Changes

2

- cmd/entire/cli/telemetry

- Acheckpoint_policy.go+89

- Acheckpoint_policy_test.go+69

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

package telemetry

import (
	"encoding/json"
	"os"
	"runtime"
	"time"

"github.com/denisbrodbeck/machineid"
)

// Property values for the checkpoint_policy_blocked event.
const (
	PolicyBlockedHookTypeGit   = "git"
	PolicyBlockedHookTypeAgent = "agent"

PolicyBlockedReasonUnsupported = "policy_unsupported"
	PolicyBlockedReasonUnreadable  = "policy_unreadable"

PolicyBlockedOutcomeSkipped = "skipped"
	PolicyBlockedOutcomeBlocked = "blocked"
)

// CheckpointPolicyBlockedEvent carries the gate-derived fields for a
// checkpoint_policy_blocked telemetry event. Agent, CheckpointVersion, and
// CheckpointMinVersion are optional and omitted from the payload when empty.
type CheckpointPolicyBlockedEvent struct {
	Hook                 string
	HookType             string
	Reason               string
	Outcome              string
	Agent                string
	CheckpointVersion    string
	CheckpointMinVersion string
}

// BuildCheckpointPolicyBlockedPayload constructs the event payload. Exported for
// testing. Returns nil if the machine ID cannot be resolved.
func BuildCheckpointPolicyBlockedPayload(event CheckpointPolicyBlockedEvent, version string) *EventPayload {
	machineID, err := machineid.ProtectedID("entire-cli")
	if err != nil {
		return nil
	}

properties := map[string]any{
		"hook":        event.Hook,
		"hook_type":   event.HookType,
		"reason":      event.Reason,
		"outcome":     event.Outcome,
		"cli_version": version,
		"os":          runtime.GOOS,
		"arch":        runtime.GOARCH,
	}
	if event.Agent != "" {
		properties["agent"] = event.Agent
	}
	if event.CheckpointVersion != "" {
		properties["checkpoint_version"] = event.CheckpointVersion
	}
	if event.CheckpointMinVersion != "" {
		properties["checkpoint_min_version"] = event.CheckpointMinVersion
	}

return &EventPayload{
		Event:      "checkpoint_policy_blocked",
		DistinctID: machineID,
		Properties: properties,
		Timestamp:  time.Now(),
	}
}

// TrackCheckpointPolicyBlocked sends a checkpoint_policy_blocked event by
// spawning a detached subprocess. Best-effort and non-blocking; callers are
// responsible for the settings.Telemetry opt-in check. Honors
// ENTIRE_TELEMETRY_OPTOUT like the other trackers.
func TrackCheckpointPolicyBlocked(event CheckpointPolicyBlockedEvent, version string) {
	if os.Getenv("ENTIRE_TELEMETRY_OPTOUT") != "" {
		return
	}

payload := BuildCheckpointPolicyBlockedPayload(event, version)
	if payload == nil {
		return
	}

if payloadJSON, err := json.Marshal(payload); err == nil {
		spawnDetachedAnalytics(string(payloadJSON))
	}
}
```

Acmd/entire/cli/telemetry/checkpoint_policy.go+89

package telemetry

import "testing"

func TestBuildCheckpointPolicyBlockedPayload_Unsupported(t *testing.T) {
	t.Parallel()
	payload := BuildCheckpointPolicyBlockedPayload(CheckpointPolicyBlockedEvent{
		Hook:                 "post-commit",
		HookType:             PolicyBlockedHookTypeGit,
		Reason:               PolicyBlockedReasonUnsupported,
		Outcome:              PolicyBlockedOutcomeSkipped,
		CheckpointVersion:    "v2",
		CheckpointMinVersion: "v2",
	}, "1.2.3")
	if payload == nil {
		t.Fatal("BuildCheckpointPolicyBlockedPayload returned nil")
		return
	}
	if payload.Event != "checkpoint_policy_blocked" {
			   t.Errorf("Event = %q, want %q", payload.Event, "checkpoint_policy_blocked")
	}
	if payload.DistinctID == "" {
			   t.Error("DistinctID must be set to the machine ID")
	}
	checks := map[string]any{
		"hook":                   "post-commit",
		"hook_type":              "git",
		"reason":                 "policy_unsupported",
		"outcome":                "skipped",
		"checkpoint_version":     "v2",
		"checkpoint_min_version": "v2",
		"cli_version":            "1.2.3",
	}
	for k, want := range checks {
		if got := payload.Properties[k]; got != want {
			t.Errorf("Properties[%q] = %v, want %v", k, got, want)
		}
	}
	if _, ok := payload.Properties["agent"]; ok {
			   t.Error("git-hook payload must not include 'agent'")
	}
}

func TestBuildCheckpointPolicyBlockedPayload_UnreadableOmitsVersions(t *testing.T) {
	t.Parallel()
	payload := BuildCheckpointPolicyBlockedPayload(CheckpointPolicyBlockedEvent{
		Hook:     "session-start",
		HookType: PolicyBlockedHookTypeAgent,
		Reason:   PolicyBlockedReasonUnreadable,
		Outcome:  PolicyBlockedOutcomeSkipped,
		Agent:    "claude-code",
	}, "1.2.3")
	if payload == nil {
		t.Fatal("BuildCheckpointPolicyBlockedPayload returned nil")
		return
	}
	if got := payload.Properties["agent"]; got != "claude-code" {
			   t.Errorf("Properties[agent] = %v, want %q", got, "claude-code")
	}
	if _, ok := payload.Properties["checkpoint_version"]; ok {
			   t.Error("unreadable payload must omit 'checkpoint_version'")
	}
	if _, ok := payload.Properties["checkpoint_min_version"]; ok {
			   t.Error("unreadable payload must omit 'checkpoint_min_version'")
	}
	if got := payload.Properties["outcome"]; got != "skipped" {
			   t.Errorf("Properties[outcome] = %v, want %q", got, "skipped")
	}
}
