add checkpoint_policy_blocked telemetry event · Entire

add checkpoint_policy_blocked telemetry event

b8fddf4→main·

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

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") } }