Skip to content

Commit fa60865

Browse files
committed
feat(cli): implement telemetry tracking for command execution with asynchronous event reporting
1 parent 0e0f0b0 commit fa60865

File tree

5 files changed

+317
-23
lines changed

5 files changed

+317
-23
lines changed

.github/workflows/cli-release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
release_tag: ${{ github.ref_name }}
6161
overwrite: true
6262
pre_command: export CGO_ENABLED=0
63-
ldflags: -s -w -extldflags -static -X main.version=${{ github.ref_name }}
63+
ldflags: -s -w -extldflags -static -X main.version=${{ github.ref_name }} -X github.com/memodb-io/Acontext/acontext-cli/internal/telemetry.telemetryBearerToken=${{ secrets.CLI_TELEMETRY_BEARER_TOKEN }}
6464
project_path: src/client/acontext-cli
6565
binary_name: acontext-cli
6666
asset_name: "${{ matrix.goos }}_${{ matrix.goarch }}"

src/client/acontext-cli/go.mod

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ go 1.25.1
55
require (
66
github.com/AlecAivazis/survey/v2 v2.3.7
77
github.com/pelletier/go-toml/v2 v2.2.4
8-
github.com/spf13/cobra v1.8.0
8+
github.com/spf13/cobra v1.10.1
9+
github.com/spf13/pflag v1.0.10
910
github.com/stretchr/testify v1.9.0
1011
gopkg.in/yaml.v3 v3.0.1
1112
)
@@ -15,14 +16,13 @@ require (
1516
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1617
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
1718
github.com/kr/pretty v0.3.1 // indirect
18-
github.com/mattn/go-colorable v0.1.2 // indirect
19-
github.com/mattn/go-isatty v0.0.8 // indirect
20-
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
19+
github.com/mattn/go-colorable v0.1.14 // indirect
20+
github.com/mattn/go-isatty v0.0.20 // indirect
21+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
2122
github.com/pmezard/go-difflib v1.0.0 // indirect
2223
github.com/rogpeppe/go-internal v1.14.1 // indirect
23-
github.com/spf13/pflag v1.0.5 // indirect
24-
golang.org/x/sys v0.26.0 // indirect
25-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
26-
golang.org/x/text v0.4.0 // indirect
24+
golang.org/x/sys v0.38.0 // indirect
25+
golang.org/x/term v0.37.0 // indirect
26+
golang.org/x/text v0.31.0 // indirect
2727
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2828
)

src/client/acontext-cli/go.sum

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
22
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
33
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
44
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
5-
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
66
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
77
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
88
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -22,12 +22,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
2222
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
2323
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
2424
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25-
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
2625
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
27-
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
26+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
27+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
2828
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
29-
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
29+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
30+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
3031
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
32+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
33+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
3134
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
3235
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
3336
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -37,10 +40,11 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
3740
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
3841
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
3942
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
40-
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
41-
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
42-
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
43-
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
43+
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
44+
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
45+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
46+
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
47+
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
4448
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
4549
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
4650
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
@@ -60,16 +64,19 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
6064
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6165
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6266
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
63-
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
64-
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
67+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
68+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
69+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
6570
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
66-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
6771
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
72+
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
73+
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
6874
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
6975
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
7076
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
71-
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
7277
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
78+
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
79+
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
7380
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
7481
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
7582
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package telemetry
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"runtime"
9+
"sync"
10+
"time"
11+
)
12+
13+
const (
14+
telemetryEndpoint = "https://telemetry.acontext.io/v1/events"
15+
)
16+
17+
// telemetryBearerToken is set at build time via ldflags
18+
var telemetryBearerToken = ""
19+
20+
// Event represents a telemetry event
21+
type Event struct {
22+
Command string `json:"command"`
23+
Args []string `json:"args,omitempty"`
24+
Flags map[string]string `json:"flags,omitempty"`
25+
Success bool `json:"success"`
26+
Error string `json:"error,omitempty"`
27+
Duration int64 `json:"duration_ms"`
28+
Timestamp string `json:"timestamp"`
29+
Version string `json:"version"`
30+
OS string `json:"os"`
31+
Arch string `json:"arch"`
32+
CommandPath string `json:"command_path,omitempty"`
33+
}
34+
35+
// SendEvent sends a telemetry event asynchronously
36+
func SendEvent(event Event) {
37+
// Send in a goroutine to avoid blocking
38+
go func() {
39+
_ = sendEvent(event)
40+
// Silently fail - telemetry should not affect user experience
41+
}()
42+
}
43+
44+
// SendEventAsync sends a telemetry event asynchronously and returns a WaitGroup to wait for completion
45+
func SendEventAsync(event Event) *sync.WaitGroup {
46+
var wg sync.WaitGroup
47+
wg.Add(1)
48+
// Send in a goroutine to avoid blocking
49+
go func() {
50+
defer wg.Done()
51+
_ = sendEvent(event)
52+
// Silently fail - telemetry should not affect user experience
53+
}()
54+
return &wg
55+
}
56+
57+
// SendEventSync sends a telemetry event synchronously and waits for completion
58+
func SendEventSync(event Event) error {
59+
return sendEvent(event)
60+
}
61+
62+
// sendEvent actually sends the event to the telemetry endpoint
63+
func sendEvent(event Event) error {
64+
// Set timestamp if not set
65+
if event.Timestamp == "" {
66+
event.Timestamp = time.Now().UTC().Format(time.RFC3339)
67+
}
68+
69+
// Set system info if not set
70+
if event.OS == "" {
71+
event.OS = runtime.GOOS
72+
}
73+
if event.Arch == "" {
74+
event.Arch = runtime.GOARCH
75+
}
76+
77+
// Marshal event to JSON
78+
jsonData, err := json.Marshal(event)
79+
if err != nil {
80+
return fmt.Errorf("failed to marshal event: %w", err)
81+
}
82+
83+
// Create HTTP request
84+
req, err := http.NewRequest("POST", telemetryEndpoint, bytes.NewBuffer(jsonData))
85+
if err != nil {
86+
return fmt.Errorf("failed to create request: %w", err)
87+
}
88+
89+
req.Header.Set("Content-Type", "application/json")
90+
req.Header.Set("User-Agent", "acontext-cli")
91+
92+
// Add bearer token if available (set at build time)
93+
if telemetryBearerToken != "" {
94+
req.Header.Set("Authorization", "Bearer "+telemetryBearerToken)
95+
}
96+
97+
// Create HTTP client with timeout
98+
client := &http.Client{
99+
Timeout: 5 * time.Second,
100+
}
101+
102+
// Send request
103+
resp, err := client.Do(req)
104+
if err != nil {
105+
return fmt.Errorf("failed to send request: %w", err)
106+
}
107+
defer resp.Body.Close()
108+
109+
// Check response status
110+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
111+
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
112+
}
113+
114+
return nil
115+
}
116+
117+
// TrackCommand tracks a command execution
118+
func TrackCommand(command string, args []string, flags map[string]string, success bool, err error, duration time.Duration, version string) {
119+
event := Event{
120+
Command: command,
121+
Args: args,
122+
Flags: flags,
123+
Success: success,
124+
Duration: duration.Milliseconds(),
125+
Version: version,
126+
CommandPath: command,
127+
}
128+
129+
if err != nil {
130+
event.Error = err.Error()
131+
}
132+
133+
SendEvent(event)
134+
}
135+
136+
// TrackCommandAsync tracks a command execution asynchronously and returns a WaitGroup to wait for completion
137+
func TrackCommandAsync(command string, args []string, flags map[string]string, success bool, err error, duration time.Duration, version string) *sync.WaitGroup {
138+
event := Event{
139+
Command: command,
140+
Args: args,
141+
Flags: flags,
142+
Success: success,
143+
Duration: duration.Milliseconds(),
144+
Version: version,
145+
CommandPath: command,
146+
}
147+
148+
if err != nil {
149+
event.Error = err.Error()
150+
}
151+
152+
return SendEventAsync(event)
153+
}
154+
155+
// TrackCommandSync tracks a command execution synchronously and waits for completion
156+
func TrackCommandSync(command string, args []string, flags map[string]string, success bool, err error, duration time.Duration, version string) error {
157+
event := Event{
158+
Command: command,
159+
Args: args,
160+
Flags: flags,
161+
Success: success,
162+
Duration: duration.Milliseconds(),
163+
Version: version,
164+
CommandPath: command,
165+
}
166+
167+
if err != nil {
168+
event.Error = err.Error()
169+
}
170+
171+
return SendEventSync(event)
172+
}

0 commit comments

Comments
 (0)