@tanagram/cli 0.4.19 → 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 (
@@ -19,7 +25,6 @@ require (
19
25
  github.com/charmbracelet/x/term v0.2.1 // indirect
20
26
  github.com/danieljoos/wincred v1.2.2 // indirect
21
27
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
22
- github.com/getsentry/sentry-go v0.40.0 // indirect
23
28
  github.com/go-ole/go-ole v1.2.6 // indirect
24
29
  github.com/godbus/dbus/v5 v5.1.0 // indirect
25
30
  github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -31,7 +36,6 @@ require (
31
36
  github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
32
37
  github.com/muesli/cancelreader v0.2.2 // indirect
33
38
  github.com/muesli/termenv v0.16.0 // indirect
34
- github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
35
39
  github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
36
40
  github.com/rivo/uniseg v0.4.7 // indirect
37
41
  github.com/shoenig/go-m1cpu v0.1.6 // indirect
@@ -43,8 +47,6 @@ require (
43
47
  github.com/tklauser/numcpus v0.6.1 // indirect
44
48
  github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
45
49
  github.com/yusufpapurcu/wmi v1.2.4 // indirect
46
- github.com/zalando/go-keyring v0.2.6 // indirect
47
50
  golang.org/x/sys v0.38.0 // indirect
48
- golang.org/x/term v0.37.0 // indirect
49
51
  golang.org/x/text v0.27.0 // indirect
50
52
  )
package/go.sum CHANGED
@@ -24,13 +24,21 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
24
24
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
25
25
  github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
26
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=
27
31
  github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
28
32
  github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
29
33
  github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
30
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=
31
37
  github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
32
38
  github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
33
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=
34
42
  github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
35
43
  github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
36
44
  github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -49,8 +57,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
49
57
  github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
50
58
  github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
51
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=
52
62
  github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
53
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=
54
66
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
55
67
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
56
68
  github.com/posthog/posthog-go v1.6.12 h1:rsOBL/YdMfLJtOVjKJLgdzYmvaL3aIW6IVbAteSe+aI=
@@ -66,6 +78,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
66
78
  github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
67
79
  github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
68
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=
69
83
  github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
70
84
  github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
71
85
  github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -88,6 +102,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
88
102
  github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
89
103
  github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
90
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=
91
107
  golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
92
108
  golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
93
109
  golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -97,8 +113,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97
113
  golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98
114
  golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99
115
  golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
100
- golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
101
- golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
102
116
  golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
103
117
  golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
104
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"
@@ -12,6 +13,7 @@ import (
12
13
  "time"
13
14
 
14
15
  "github.com/getsentry/sentry-go"
16
+ sentryslog "github.com/getsentry/sentry-go/slog"
15
17
  "github.com/tanagram/cli/commands"
16
18
  "github.com/tanagram/cli/metrics"
17
19
  "github.com/tanagram/cli/tui"
@@ -33,15 +35,20 @@ func main() {
33
35
  metrics.Init()
34
36
 
35
37
  if err := sentry.Init(sentry.ClientOptions{
36
- Dsn: "https://a967718dd129e143907fe01b4e80cad2@o4509017064472576.ingest.us.sentry.io/4510649104007168",
37
- Release: "tanagram-cli@" + Version,
38
- Environment: getEnvironment(),
39
- EnableTracing: true,
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
40
44
  TracesSampleRate: 0.1,
45
+ EnableLogs: true,
41
46
  }); err != nil {
42
47
  slog.Warn("Sentry initialization failed", "error", err)
43
48
  }
44
49
 
50
+ ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub())
51
+
45
52
  exitCode := 0
46
53
  defer func() {
47
54
  if r := recover(); r != nil {
@@ -70,6 +77,12 @@ func main() {
70
77
  "subcommand": subcommand,
71
78
  })
72
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
+
73
86
  var logOutput io.Writer = os.Stderr
74
87
  if utils.GetParentProcess() == "claude" {
75
88
  // We use "exit-code 2" behavior for claude: https://code.claude.com/docs/en/hooks#simple:-exit-code
@@ -94,11 +107,16 @@ func main() {
94
107
  logOutput = logFile
95
108
  }
96
109
  isTTY := term.IsTerminal(int(os.Stdout.Fd()))
97
- logger := newLogger(*flagLogLevel, *flagLogFormat, logOutput, isTTY)
110
+ logger := newLogger(ctx, *flagLogLevel, *flagLogFormat, logOutput, isTTY)
98
111
  slog.SetDefault(logger)
99
112
 
113
+ utils.SetGlobalTags(map[string]string{
114
+ "parent_process": utils.GetParentProcess(),
115
+ "is_tty": fmt.Sprintf("%t", isTTY),
116
+ })
117
+
100
118
  slog.Info("Running CLI with args",
101
- "args", os.Args[1:],
119
+ "args", strings.Join(os.Args[1:], " "),
102
120
  )
103
121
 
104
122
  // THIS IS A HUGE HACK
@@ -110,7 +128,7 @@ func main() {
110
128
  // TODO: handle 0 or multiple workspace_roots
111
129
  if utils.GetParentProcess() == "cursor" {
112
130
  input, err := io.ReadAll(os.Stdin)
113
- if err == nil {
131
+ if err == nil && len(input) > 0 {
114
132
  var payload struct {
115
133
  WorkspaceRoots []string `json:"workspace_roots"`
116
134
  }
@@ -131,39 +149,39 @@ func main() {
131
149
  "command": "run",
132
150
  })
133
151
  // Auto-setup hooks on first run
134
- if err := commands.EnsureHooksConfigured(); err != nil {
152
+ if err := commands.EnsureHooksConfigured(ctx); err != nil {
135
153
  slog.Error("Failed to configure hooks", "error", err)
136
154
  exitCode = 1
137
155
  return
138
156
  }
139
- err = commands.Run()
157
+ err = commands.Run(ctx)
140
158
  case "snapshot":
141
159
  metrics.Track("cli.command.execute", map[string]interface{}{
142
160
  "command": "snapshot",
143
161
  })
144
- err = commands.Snapshot()
162
+ err = commands.Snapshot(ctx)
145
163
  case "sync":
146
164
  metrics.Track("cli.command.execute", map[string]interface{}{
147
165
  "command": "sync",
148
166
  })
149
167
  // Auto-setup hooks on first run
150
- if err := commands.EnsureHooksConfigured(); err != nil {
168
+ if err := commands.EnsureHooksConfigured(ctx); err != nil {
151
169
  slog.Error("Failed to configure hooks", "error", err)
152
170
  exitCode = 1
153
171
  return
154
172
  }
155
- err = commands.Sync()
173
+ err = commands.Sync(ctx)
156
174
  case "list":
157
175
  metrics.Track("cli.command.execute", map[string]interface{}{
158
176
  "command": "list",
159
177
  })
160
178
  // Auto-setup hooks on first run
161
- if err := commands.EnsureHooksConfigured(); err != nil {
179
+ if err := commands.EnsureHooksConfigured(ctx); err != nil {
162
180
  slog.Error("Failed to configure hooks", "error", err)
163
181
  exitCode = 1
164
182
  return
165
183
  }
166
- err = commands.List()
184
+ err = commands.List(ctx)
167
185
  case "config":
168
186
  // Handle config subcommands
169
187
  if len(os.Args) < 3 {
@@ -186,7 +204,7 @@ func main() {
186
204
  err = pathErr
187
205
  break
188
206
  }
189
- err = commands.ConfigClaude(settingsPath)
207
+ err = commands.ConfigClaude(ctx, settingsPath)
190
208
  case "cursor":
191
209
  metrics.Track("cli.command.execute", map[string]interface{}{
192
210
  "command": "config.cursor",
@@ -196,12 +214,12 @@ func main() {
196
214
  err = pathErr
197
215
  break
198
216
  }
199
- err = commands.ConfigCursor(hooksPath)
217
+ err = commands.ConfigCursor(ctx, hooksPath)
200
218
  case "list":
201
219
  metrics.Track("cli.command.execute", map[string]interface{}{
202
220
  "command": "config.list",
203
221
  })
204
- err = commands.ConfigList()
222
+ err = commands.ConfigList(ctx)
205
223
  default:
206
224
  fmt.Fprintf(os.Stderr, "Unknown config subcommand: %s\n", subCmd)
207
225
  exitCode = 1
@@ -232,12 +250,12 @@ func main() {
232
250
  metrics.Track("cli.command.execute", map[string]interface{}{
233
251
  "command": "login",
234
252
  })
235
- err = commands.Login()
253
+ err = commands.Login(ctx)
236
254
  case "sync-policies":
237
255
  metrics.Track("cli.command.execute", map[string]interface{}{
238
256
  "command": "sync-policies",
239
257
  })
240
- err = commands.SyncPolicies()
258
+ err = commands.SyncPolicies(ctx)
241
259
  case "version", "-v", "--version":
242
260
  fmt.Println(Version)
243
261
  return
@@ -252,9 +270,8 @@ func main() {
252
270
  }
253
271
 
254
272
  if err != nil {
255
- sentry.WithScope(func(scope *sentry.Scope) {
256
- scope.SetTag("command", subcommand)
257
- sentry.CaptureException(err)
273
+ utils.CaptureError(err, map[string]string{
274
+ "command": subcommand,
258
275
  })
259
276
  metrics.Track("cli.command.error", map[string]interface{}{
260
277
  "command": subcommand,
@@ -328,7 +345,7 @@ HOOK WORKFLOW:
328
345
  fmt.Print(help)
329
346
  }
330
347
 
331
- 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 {
332
349
  var lvl slog.Level
333
350
  switch strings.ToLower(levelStr) {
334
351
  case "debug":
@@ -347,10 +364,10 @@ func newLogger(levelStr, format string, output io.Writer, isTTY bool) *slog.Logg
347
364
  Level: lvl,
348
365
  }
349
366
 
350
- var h slog.Handler
367
+ var baseHandler slog.Handler
351
368
  switch strings.ToLower(format) {
352
369
  case "json":
353
- h = slog.NewJSONHandler(output, opts)
370
+ baseHandler = slog.NewJSONHandler(output, opts)
354
371
  default:
355
372
  if isTTY {
356
373
  opts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
@@ -360,10 +377,60 @@ func newLogger(levelStr, format string, output io.Writer, isTTY bool) *slog.Logg
360
377
  return a
361
378
  }
362
379
  }
363
- 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)
364
424
  }
425
+ return &multiHandler{handlers: handlers}
426
+ }
365
427
 
366
- 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}
367
434
  }
368
435
 
369
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.19",
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
+ }