@@ -12,6 +12,7 @@ import (
12
12
"os"
13
13
"os/exec"
14
14
"path/filepath"
15
+ "slices"
15
16
"strings"
16
17
"sync"
17
18
"time"
@@ -28,6 +29,8 @@ import (
28
29
29
30
"cdr.dev/slog"
30
31
"cdr.dev/slog/sloggers/sloghuman"
32
+ "github.com/coder/coder/v2/agent/agentssh"
33
+ "github.com/coder/coder/v2/apiversion"
31
34
"github.com/coder/coder/v2/cli/cliui"
32
35
"github.com/coder/coder/v2/cli/cliutil"
33
36
"github.com/coder/coder/v2/coderd/autobuild/notify"
@@ -57,6 +60,7 @@ func (r *RootCmd) ssh() *serpent.Command {
57
60
logDirPath string
58
61
remoteForwards []string
59
62
env []string
63
+ usageApp string
60
64
disableAutostart bool
61
65
)
62
66
client := new (codersdk.Client )
@@ -196,6 +200,11 @@ func (r *RootCmd) ssh() *serpent.Command {
196
200
wait = false
197
201
}
198
202
203
+ experiments , err := client .Experiments (ctx )
204
+ if err != nil {
205
+ return err
206
+ }
207
+
199
208
templateVersion , err := client .TemplateVersion (ctx , workspace .LatestBuild .TemplateVersionID )
200
209
if err != nil {
201
210
return err
@@ -251,6 +260,18 @@ func (r *RootCmd) ssh() *serpent.Command {
251
260
stopPolling := tryPollWorkspaceAutostop (ctx , client , workspace )
252
261
defer stopPolling ()
253
262
263
+ usageAppName := getUsageAppName (usageApp , stdio , experiments , workspaceAgent .APIVersion )
264
+ if usageAppName != "" {
265
+ closeUsage := client .UpdateWorkspaceUsageWithBodyContext (ctx , workspace .ID , codersdk.PostWorkspaceUsageRequest {
266
+ AgentID : workspaceAgent .ID ,
267
+ AppName : usageAppName ,
268
+ })
269
+ defer closeUsage ()
270
+
271
+ // signal to the agent that we are handling the usage tracking
272
+ parsedEnv = append (parsedEnv , [2 ]string {agentssh .MagicDisableUsageTrackingEnvironmentVariable , "true" })
273
+ }
274
+
254
275
if stdio {
255
276
rawSSH , err := conn .SSH (ctx )
256
277
if err != nil {
@@ -509,6 +530,12 @@ func (r *RootCmd) ssh() *serpent.Command {
509
530
FlagShorthand : "e" ,
510
531
Value : serpent .StringArrayOf (& env ),
511
532
},
533
+ {
534
+ Flag : "usage-app" ,
535
+ Description : "Specifies the usage app to use for workspace activity tracking." ,
536
+ Env : "CODER_SSH_USAGE_APP" ,
537
+ Value : serpent .StringOf (& usageApp ),
538
+ },
512
539
sshDisableAutostartOption (serpent .BoolOf (& disableAutostart )),
513
540
}
514
541
return cmd
@@ -1044,3 +1071,35 @@ func (r stdioErrLogReader) Read(_ []byte) (int, error) {
1044
1071
r .l .Error (context .Background (), "reading from stdin in stdio mode is not allowed" )
1045
1072
return 0 , io .EOF
1046
1073
}
1074
+
1075
+ func getUsageAppName (usageApp string , stdio bool , experiments codersdk.Experiments , agentAPIVersion string ) codersdk.UsageAppName {
1076
+ // if experiment is not enabled do not report usage
1077
+ if ! slices .Contains (experiments , codersdk .ExperimentWorkspaceUsage ) {
1078
+ return ""
1079
+ }
1080
+
1081
+ // need agent version to be at or after 2.2
1082
+ major , minor , err := apiversion .Parse (agentAPIVersion )
1083
+ if err != nil {
1084
+ return ""
1085
+ }
1086
+ err = apiversion .New (major , minor ).Validate ("2.2" )
1087
+ if err != nil {
1088
+ return ""
1089
+ }
1090
+
1091
+ // if usageApp is empty and not stdio, default to ssh
1092
+ if usageApp == "" && ! stdio {
1093
+ usageApp = string (codersdk .UsageAppNameSSH )
1094
+ }
1095
+ allowedUsageApps := []string {
1096
+ string (codersdk .UsageAppNameJetbrains ),
1097
+ string (codersdk .UsageAppNameVscode ),
1098
+ string (codersdk .UsageAppNameSSH ),
1099
+ }
1100
+ if slices .Contains (allowedUsageApps , usageApp ) {
1101
+ return codersdk .UsageAppName (usageApp )
1102
+ }
1103
+
1104
+ return ""
1105
+ }
0 commit comments