@tanagram/cli 0.4.18 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,23 @@
1
1
  package commands
2
2
 
3
3
  import (
4
+ "context"
4
5
  "fmt"
5
6
 
7
+ "github.com/getsentry/sentry-go"
6
8
  "github.com/tanagram/cli/api"
7
9
  "github.com/tanagram/cli/storage"
8
10
  )
9
11
 
10
12
  // SyncPolicies fetches policies from Tanagram API and saves them locally
11
- func SyncPolicies() error {
13
+ func SyncPolicies(ctx context.Context) error {
14
+ span := sentry.StartSpan(ctx, "command.sync_policies")
15
+ defer span.Finish()
16
+
12
17
  // Find git root
18
+ findGitRootSpan := sentry.StartSpan(span.Context(), "storage.find_git_root")
13
19
  gitRoot, err := storage.FindGitRoot()
20
+ findGitRootSpan.Finish()
14
21
  if err != nil {
15
22
  return fmt.Errorf("not in a git repository: %w", err)
16
23
  }
@@ -18,14 +25,18 @@ func SyncPolicies() error {
18
25
  fmt.Println("Syncing policies from Tanagram...")
19
26
 
20
27
  // Create API client
28
+ createClientSpan := sentry.StartSpan(span.Context(), "api.new_client")
21
29
  client, err := api.NewAPIClient()
30
+ createClientSpan.Finish()
22
31
  if err != nil {
23
32
  return err
24
33
  }
25
34
 
26
35
  // Fetch policies from API
27
36
  fmt.Println("Fetching policies from API...")
37
+ fetchPoliciesSpan := sentry.StartSpan(span.Context(), "api.get_policies")
28
38
  response, err := client.GetPolicies()
39
+ fetchPoliciesSpan.Finish()
29
40
  if err != nil {
30
41
  return err
31
42
  }
@@ -49,6 +60,7 @@ func SyncPolicies() error {
49
60
  fmt.Printf("Policies apply to %d repositories\n", len(repoMap))
50
61
 
51
62
  // Save policies for each repository
63
+ savePoliciesSpan := sentry.StartSpan(span.Context(), "storage.save_policies")
52
64
  cloudStorage := storage.NewCloudPolicyStorage(gitRoot)
53
65
  savedCount := 0
54
66
 
@@ -66,6 +78,7 @@ func SyncPolicies() error {
66
78
  savedCount++
67
79
  fmt.Printf(" ✓ Saved %d policies for %s\n", len(policies), repoKey)
68
80
  }
81
+ savePoliciesSpan.Finish()
69
82
 
70
83
  // Save metadata
71
84
  orgID := ""
Binary file
Binary file
Binary file
Binary file
Binary file
package/go.mod CHANGED
@@ -6,8 +6,14 @@ require (
6
6
  github.com/anthropics/anthropic-sdk-go v1.17.0
7
7
  github.com/charmbracelet/bubbletea v1.3.10
8
8
  github.com/charmbracelet/lipgloss v1.1.0
9
+ github.com/getsentry/sentry-go v0.40.0
10
+ github.com/getsentry/sentry-go/slog v0.40.0
11
+ github.com/golang-jwt/jwt/v5 v5.3.0
12
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
9
13
  github.com/posthog/posthog-go v1.6.12
10
14
  github.com/shirou/gopsutil/v3 v3.24.5
15
+ github.com/zalando/go-keyring v0.2.6
16
+ golang.org/x/term v0.37.0
11
17
  )
12
18
 
13
19
  require (
@@ -30,7 +36,6 @@ require (
30
36
  github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
31
37
  github.com/muesli/cancelreader v0.2.2 // indirect
32
38
  github.com/muesli/termenv v0.16.0 // indirect
33
- github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
34
39
  github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
35
40
  github.com/rivo/uniseg v0.4.7 // indirect
36
41
  github.com/shoenig/go-m1cpu v0.1.6 // indirect
@@ -42,8 +47,6 @@ require (
42
47
  github.com/tklauser/numcpus v0.6.1 // indirect
43
48
  github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
44
49
  github.com/yusufpapurcu/wmi v1.2.4 // indirect
45
- github.com/zalando/go-keyring v0.2.6 // indirect
46
50
  golang.org/x/sys v0.38.0 // indirect
47
- golang.org/x/term v0.37.0 // indirect
48
51
  golang.org/x/text v0.27.0 // indirect
49
52
  )
package/go.sum CHANGED
@@ -22,13 +22,23 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
22
22
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23
23
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
24
24
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
25
+ github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
26
+ github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
27
+ github.com/getsentry/sentry-go/slog v0.40.0 h1:uR2EPL9w6uHw3XB983IAqzqM9mP+fjJpNY9kfob3/Z8=
28
+ github.com/getsentry/sentry-go/slog v0.40.0/go.mod h1:ArRaP+0rsbnJGyvZwYDo/vDQT/YBbOQeOlO+DGW+F9s=
29
+ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
30
+ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
25
31
  github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
26
32
  github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
27
33
  github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
28
34
  github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
35
+ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
36
+ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
29
37
  github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
30
38
  github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
31
39
  github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
40
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
41
+ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
32
42
  github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
33
43
  github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
34
44
  github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -47,8 +57,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
47
57
  github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
48
58
  github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
49
59
  github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
60
+ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
61
+ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
50
62
  github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
51
63
  github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
64
+ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
65
+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
52
66
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
53
67
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
54
68
  github.com/posthog/posthog-go v1.6.12 h1:rsOBL/YdMfLJtOVjKJLgdzYmvaL3aIW6IVbAteSe+aI=
@@ -64,6 +78,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
64
78
  github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
65
79
  github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
66
80
  github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
81
+ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
82
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
67
83
  github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
68
84
  github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
69
85
  github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -86,6 +102,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
86
102
  github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
87
103
  github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
88
104
  github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
105
+ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
106
+ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
89
107
  golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
90
108
  golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
91
109
  golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -95,8 +113,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
95
113
  golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96
114
  golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97
115
  golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98
- golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
99
- golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
100
116
  golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
101
117
  golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
102
118
  golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
package/main.go CHANGED
@@ -1,6 +1,7 @@
1
1
  package main
2
2
 
3
3
  import (
4
+ "context"
4
5
  "encoding/json"
5
6
  "flag"
6
7
  "fmt"
@@ -9,7 +10,10 @@ import (
9
10
  "os"
10
11
  "path/filepath"
11
12
  "strings"
13
+ "time"
12
14
 
15
+ "github.com/getsentry/sentry-go"
16
+ sentryslog "github.com/getsentry/sentry-go/slog"
13
17
  "github.com/tanagram/cli/commands"
14
18
  "github.com/tanagram/cli/metrics"
15
19
  "github.com/tanagram/cli/tui"
@@ -30,14 +34,33 @@ func main() {
30
34
  metrics.SetVersion(Version)
31
35
  metrics.Init()
32
36
 
37
+ if err := sentry.Init(sentry.ClientOptions{
38
+ Dsn: "https://a967718dd129e143907fe01b4e80cad2@o4509017064472576.ingest.us.sentry.io/4510649104007168",
39
+ Release: "tanagram-cli@" + Version,
40
+ Environment: getEnvironment(),
41
+ EnableTracing: true,
42
+ // TOOD: Configure sample rate based on environment (1.0 for dev; 0.1 for prod)
43
+ // Depends on TAN-1968
44
+ TracesSampleRate: 0.1,
45
+ EnableLogs: true,
46
+ }); err != nil {
47
+ slog.Warn("Sentry initialization failed", "error", err)
48
+ }
49
+
50
+ ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub())
51
+
33
52
  exitCode := 0
34
53
  defer func() {
54
+ if r := recover(); r != nil {
55
+ sentry.CurrentHub().Recover(r)
56
+ slog.Error("Panic recovered", "panic", r)
57
+ exitCode = 1
58
+ }
59
+ sentry.Flush(2 * time.Second)
35
60
  metrics.Track("cli.exit", map[string]interface{}{
36
61
  "exit_code": exitCode,
37
62
  })
38
63
  metrics.Close()
39
- // os.Exit immediately exits without calling other `defer`s, so we need to group these two statements
40
- // and call them in the right order.
41
64
  os.Exit(exitCode)
42
65
  }()
43
66
 
@@ -54,6 +77,12 @@ func main() {
54
77
  "subcommand": subcommand,
55
78
  })
56
79
 
80
+ tx := sentry.StartTransaction(ctx, fmt.Sprintf("cli.%s", subcommand),
81
+ sentry.WithOpName("command"),
82
+ )
83
+ defer tx.Finish()
84
+ ctx = tx.Context()
85
+
57
86
  var logOutput io.Writer = os.Stderr
58
87
  if utils.GetParentProcess() == "claude" {
59
88
  // We use "exit-code 2" behavior for claude: https://code.claude.com/docs/en/hooks#simple:-exit-code
@@ -78,11 +107,16 @@ func main() {
78
107
  logOutput = logFile
79
108
  }
80
109
  isTTY := term.IsTerminal(int(os.Stdout.Fd()))
81
- logger := newLogger(*flagLogLevel, *flagLogFormat, logOutput, isTTY)
110
+ logger := newLogger(ctx, *flagLogLevel, *flagLogFormat, logOutput, isTTY)
82
111
  slog.SetDefault(logger)
83
112
 
113
+ utils.SetGlobalTags(map[string]string{
114
+ "parent_process": utils.GetParentProcess(),
115
+ "is_tty": fmt.Sprintf("%t", isTTY),
116
+ })
117
+
84
118
  slog.Info("Running CLI with args",
85
- "args", os.Args[1:],
119
+ "args", strings.Join(os.Args[1:], " "),
86
120
  )
87
121
 
88
122
  // THIS IS A HUGE HACK
@@ -94,7 +128,7 @@ func main() {
94
128
  // TODO: handle 0 or multiple workspace_roots
95
129
  if utils.GetParentProcess() == "cursor" {
96
130
  input, err := io.ReadAll(os.Stdin)
97
- if err == nil {
131
+ if err == nil && len(input) > 0 {
98
132
  var payload struct {
99
133
  WorkspaceRoots []string `json:"workspace_roots"`
100
134
  }
@@ -115,39 +149,39 @@ func main() {
115
149
  "command": "run",
116
150
  })
117
151
  // Auto-setup hooks on first run
118
- if err := commands.EnsureHooksConfigured(); err != nil {
152
+ if err := commands.EnsureHooksConfigured(ctx); err != nil {
119
153
  slog.Error("Failed to configure hooks", "error", err)
120
154
  exitCode = 1
121
155
  return
122
156
  }
123
- err = commands.Run()
157
+ err = commands.Run(ctx)
124
158
  case "snapshot":
125
159
  metrics.Track("cli.command.execute", map[string]interface{}{
126
160
  "command": "snapshot",
127
161
  })
128
- err = commands.Snapshot()
162
+ err = commands.Snapshot(ctx)
129
163
  case "sync":
130
164
  metrics.Track("cli.command.execute", map[string]interface{}{
131
165
  "command": "sync",
132
166
  })
133
167
  // Auto-setup hooks on first run
134
- if err := commands.EnsureHooksConfigured(); err != nil {
168
+ if err := commands.EnsureHooksConfigured(ctx); err != nil {
135
169
  slog.Error("Failed to configure hooks", "error", err)
136
170
  exitCode = 1
137
171
  return
138
172
  }
139
- err = commands.Sync()
173
+ err = commands.Sync(ctx)
140
174
  case "list":
141
175
  metrics.Track("cli.command.execute", map[string]interface{}{
142
176
  "command": "list",
143
177
  })
144
178
  // Auto-setup hooks on first run
145
- if err := commands.EnsureHooksConfigured(); err != nil {
179
+ if err := commands.EnsureHooksConfigured(ctx); err != nil {
146
180
  slog.Error("Failed to configure hooks", "error", err)
147
181
  exitCode = 1
148
182
  return
149
183
  }
150
- err = commands.List()
184
+ err = commands.List(ctx)
151
185
  case "config":
152
186
  // Handle config subcommands
153
187
  if len(os.Args) < 3 {
@@ -170,7 +204,7 @@ func main() {
170
204
  err = pathErr
171
205
  break
172
206
  }
173
- err = commands.ConfigClaude(settingsPath)
207
+ err = commands.ConfigClaude(ctx, settingsPath)
174
208
  case "cursor":
175
209
  metrics.Track("cli.command.execute", map[string]interface{}{
176
210
  "command": "config.cursor",
@@ -180,12 +214,12 @@ func main() {
180
214
  err = pathErr
181
215
  break
182
216
  }
183
- err = commands.ConfigCursor(hooksPath)
217
+ err = commands.ConfigCursor(ctx, hooksPath)
184
218
  case "list":
185
219
  metrics.Track("cli.command.execute", map[string]interface{}{
186
220
  "command": "config.list",
187
221
  })
188
- err = commands.ConfigList()
222
+ err = commands.ConfigList(ctx)
189
223
  default:
190
224
  fmt.Fprintf(os.Stderr, "Unknown config subcommand: %s\n", subCmd)
191
225
  exitCode = 1
@@ -216,12 +250,12 @@ func main() {
216
250
  metrics.Track("cli.command.execute", map[string]interface{}{
217
251
  "command": "login",
218
252
  })
219
- err = commands.Login()
253
+ err = commands.Login(ctx)
220
254
  case "sync-policies":
221
255
  metrics.Track("cli.command.execute", map[string]interface{}{
222
256
  "command": "sync-policies",
223
257
  })
224
- err = commands.SyncPolicies()
258
+ err = commands.SyncPolicies(ctx)
225
259
  case "version", "-v", "--version":
226
260
  fmt.Println(Version)
227
261
  return
@@ -236,6 +270,9 @@ func main() {
236
270
  }
237
271
 
238
272
  if err != nil {
273
+ utils.CaptureError(err, map[string]string{
274
+ "command": subcommand,
275
+ })
239
276
  metrics.Track("cli.command.error", map[string]interface{}{
240
277
  "command": subcommand,
241
278
  "error": err.Error(),
@@ -246,6 +283,13 @@ func main() {
246
283
  }
247
284
  }
248
285
 
286
+ func getEnvironment() string {
287
+ if env := os.Getenv("SENTRY_ENVIRONMENT"); env != "" {
288
+ return env
289
+ }
290
+ return "unconfigured"
291
+ }
292
+
249
293
  func printHelp() {
250
294
  help := `Tanagram - Policy enforcement for git changes
251
295
 
@@ -301,7 +345,7 @@ HOOK WORKFLOW:
301
345
  fmt.Print(help)
302
346
  }
303
347
 
304
- func newLogger(levelStr, format string, output io.Writer, isTTY bool) *slog.Logger {
348
+ func newLogger(ctx context.Context, levelStr, format string, output io.Writer, isTTY bool) *slog.Logger {
305
349
  var lvl slog.Level
306
350
  switch strings.ToLower(levelStr) {
307
351
  case "debug":
@@ -320,10 +364,10 @@ func newLogger(levelStr, format string, output io.Writer, isTTY bool) *slog.Logg
320
364
  Level: lvl,
321
365
  }
322
366
 
323
- var h slog.Handler
367
+ var baseHandler slog.Handler
324
368
  switch strings.ToLower(format) {
325
369
  case "json":
326
- h = slog.NewJSONHandler(output, opts)
370
+ baseHandler = slog.NewJSONHandler(output, opts)
327
371
  default:
328
372
  if isTTY {
329
373
  opts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
@@ -333,10 +377,60 @@ func newLogger(levelStr, format string, output io.Writer, isTTY bool) *slog.Logg
333
377
  return a
334
378
  }
335
379
  }
336
- h = slog.NewTextHandler(output, opts)
380
+ baseHandler = slog.NewTextHandler(output, opts)
381
+ }
382
+
383
+ sentryHandler := sentryslog.Option{
384
+ EventLevel: []slog.Level{slog.LevelError, sentryslog.LevelFatal},
385
+ // LogLevel defaults to `[]slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, LevelFatal}`, which seems reasonable
386
+ AddSource: true,
387
+ }.NewSentryHandler(ctx)
388
+
389
+ return slog.New(newMultiHandler(baseHandler, sentryHandler))
390
+ }
391
+
392
+ type multiHandler struct {
393
+ handlers []slog.Handler
394
+ }
395
+
396
+ func newMultiHandler(handlers ...slog.Handler) *multiHandler {
397
+ return &multiHandler{handlers: handlers}
398
+ }
399
+
400
+ func (h *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
401
+ for _, handler := range h.handlers {
402
+ if handler.Enabled(ctx, level) {
403
+ return true
404
+ }
405
+ }
406
+ return false
407
+ }
408
+
409
+ func (h *multiHandler) Handle(ctx context.Context, r slog.Record) error {
410
+ for _, handler := range h.handlers {
411
+ if handler.Enabled(ctx, r.Level) {
412
+ if err := handler.Handle(ctx, r); err != nil {
413
+ return err
414
+ }
415
+ }
416
+ }
417
+ return nil
418
+ }
419
+
420
+ func (h *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
421
+ handlers := make([]slog.Handler, len(h.handlers))
422
+ for i, handler := range h.handlers {
423
+ handlers[i] = handler.WithAttrs(attrs)
337
424
  }
425
+ return &multiHandler{handlers: handlers}
426
+ }
338
427
 
339
- return slog.New(h)
428
+ func (h *multiHandler) WithGroup(name string) slog.Handler {
429
+ handlers := make([]slog.Handler, len(h.handlers))
430
+ for i, handler := range h.handlers {
431
+ handlers[i] = handler.WithGroup(name)
432
+ }
433
+ return &multiHandler{handlers: handlers}
340
434
  }
341
435
 
342
436
  func createLogFile(logFilePath string) (string, error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,44 @@
1
+ package utils
2
+
3
+ import (
4
+ "github.com/getsentry/sentry-go"
5
+ )
6
+
7
+ // CaptureError captures an error with additional context to Sentry
8
+ func CaptureError(err error, ctx map[string]string) {
9
+ sentry.WithScope(func(scope *sentry.Scope) {
10
+ for k, v := range ctx {
11
+ scope.SetTag(k, v)
12
+ }
13
+ sentry.CaptureException(err)
14
+ })
15
+ }
16
+
17
+ // AddBreadcrumb adds a breadcrumb to the current Sentry scope
18
+ func AddBreadcrumb(category, message string, level sentry.Level, data map[string]interface{}) {
19
+ sentry.AddBreadcrumb(&sentry.Breadcrumb{
20
+ Category: category,
21
+ Message: message,
22
+ Level: level,
23
+ Data: data,
24
+ })
25
+ }
26
+
27
+ // SetUserContext sets the user context for Sentry
28
+ func SetUserContext(userID, email string) {
29
+ sentry.ConfigureScope(func(scope *sentry.Scope) {
30
+ scope.SetUser(sentry.User{
31
+ ID: userID,
32
+ Email: email,
33
+ })
34
+ })
35
+ }
36
+
37
+ // SetGlobalTags sets global tags for all Sentry events
38
+ func SetGlobalTags(tags map[string]string) {
39
+ sentry.ConfigureScope(func(scope *sentry.Scope) {
40
+ for k, v := range tags {
41
+ scope.SetTag(k, v)
42
+ }
43
+ })
44
+ }