@@ -51,6 +51,7 @@ import (
5151 "github.com/coder/coder/v2/coderd/jwtutils"
5252 "github.com/coder/coder/v2/coderd/rbac"
5353 "github.com/coder/coder/v2/coderd/telemetry"
54+ "github.com/coder/coder/v2/coderd/util/ptr"
5455 "github.com/coder/coder/v2/codersdk"
5556 "github.com/coder/coder/v2/codersdk/agentsdk"
5657 "github.com/coder/coder/v2/codersdk/workspacesdk"
@@ -2135,30 +2136,21 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) {
21352136
21362137 ctx := testutil .Context (t , testutil .WaitLong )
21372138 logger := testutil .Logger (t )
2138-
2139- fTelemetry := newFakeTelemetryReporter (ctx , t , 200 )
2140- fTelemetry .enabled = false
21412139 firstClient , _ , api := coderdtest .NewWithAPI (t , & coderdtest.Options {
2142- Coordinator : tailnet .NewCoordinator (logger ),
2143- TelemetryReporter : fTelemetry ,
2140+ Coordinator : tailnet .NewCoordinator (logger ),
21442141 })
21452142 firstUser := coderdtest .CreateFirstUser (t , firstClient )
21462143 member , memberUser := coderdtest .CreateAnotherUser (t , firstClient , firstUser .OrganizationID , rbac .RoleTemplateAdmin ())
21472144
21482145 // Create a workspace with an agent
21492146 firstWorkspace := buildWorkspaceWithAgent (t , member , firstUser .OrganizationID , memberUser .ID , api .Database , api .Pubsub )
21502147
2151- // enable telemetry now that workspace is built; we don't care about snapshots before this.
2152- fTelemetry .enabled = true
2153-
21542148 u , err := member .URL .Parse ("/api/v2/tailnet" )
21552149 require .NoError (t , err )
21562150 q := u .Query ()
21572151 q .Set ("version" , "2.0" )
21582152 u .RawQuery = q .Encode ()
21592153
2160- predialTime := time .Now ()
2161-
21622154 //nolint:bodyclose // websocket package closes this for you
21632155 wsConn , resp , err := websocket .Dial (ctx , u .String (), & websocket.DialOptions {
21642156 HTTPHeader : http.Header {
@@ -2173,15 +2165,6 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) {
21732165 }
21742166 defer wsConn .Close (websocket .StatusNormalClosure , "done" )
21752167
2176- // Check telemetry
2177- snapshot := testutil .RequireRecvCtx (ctx , t , fTelemetry .snapshots )
2178- require .Len (t , snapshot .UserTailnetConnections , 1 )
2179- telemetryConnection := snapshot .UserTailnetConnections [0 ]
2180- require .Equal (t , memberUser .ID .String (), telemetryConnection .UserID )
2181- require .GreaterOrEqual (t , telemetryConnection .ConnectedAt , predialTime )
2182- require .LessOrEqual (t , telemetryConnection .ConnectedAt , time .Now ())
2183- require .NotEmpty (t , telemetryConnection .PeerID )
2184-
21852168 rpcClient , err := tailnet .NewDRPCClient (
21862169 websocket .NetConn (ctx , wsConn , websocket .MessageBinary ),
21872170 logger ,
@@ -2229,23 +2212,135 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) {
22292212 NumAgents : 0 ,
22302213 },
22312214 })
2232- err = stream .Close ()
2233- require .NoError (t , err )
2215+ }
22342216
2235- beforeDisconnectTime := time .Now ()
2236- err = wsConn .Close (websocket .StatusNormalClosure , "done" )
2217+ func TestUserTailnetTelemetry (t * testing.T ) {
2218+ t .Parallel ()
2219+
2220+ telemetryData := & codersdk.CoderDesktopTelemetry {
2221+ DeviceOS : "Windows" ,
2222+ DeviceID : "device001" ,
2223+ CoderDesktopVersion : "0.22.1" ,
2224+ }
2225+ fullHeader , err := json .Marshal (telemetryData )
22372226 require .NoError (t , err )
22382227
2239- snapshot = testutil .RequireRecvCtx (ctx , t , fTelemetry .snapshots )
2240- require .Len (t , snapshot .UserTailnetConnections , 1 )
2241- telemetryDisconnection := snapshot .UserTailnetConnections [0 ]
2242- require .Equal (t , memberUser .ID .String (), telemetryDisconnection .UserID )
2243- require .Equal (t , telemetryConnection .ConnectedAt , telemetryDisconnection .ConnectedAt )
2244- require .Equal (t , telemetryConnection .UserID , telemetryDisconnection .UserID )
2245- require .Equal (t , telemetryConnection .PeerID , telemetryDisconnection .PeerID )
2246- require .NotNil (t , telemetryDisconnection .DisconnectedAt )
2247- require .GreaterOrEqual (t , * telemetryDisconnection .DisconnectedAt , beforeDisconnectTime )
2248- require .LessOrEqual (t , * telemetryDisconnection .DisconnectedAt , time .Now ())
2228+ testCases := []struct {
2229+ name string
2230+ headers map [string ]string
2231+ // only used for DeviceID, DeviceOS, CoderDesktopVersion
2232+ expected telemetry.UserTailnetConnection
2233+ }{
2234+ {
2235+ name : "no header" ,
2236+ headers : map [string ]string {},
2237+ expected : telemetry.UserTailnetConnection {},
2238+ },
2239+ {
2240+ name : "full header" ,
2241+ headers : map [string ]string {
2242+ codersdk .CoderDesktopTelemetryHeader : string (fullHeader ),
2243+ },
2244+ expected : telemetry.UserTailnetConnection {
2245+ DeviceOS : ptr .Ref ("Windows" ),
2246+ DeviceID : ptr .Ref ("device001" ),
2247+ CoderDesktopVersion : ptr .Ref ("0.22.1" ),
2248+ },
2249+ },
2250+ {
2251+ name : "empty header" ,
2252+ headers : map [string ]string {
2253+ codersdk .CoderDesktopTelemetryHeader : "" ,
2254+ },
2255+ expected : telemetry.UserTailnetConnection {},
2256+ },
2257+ {
2258+ name : "invalid header" ,
2259+ headers : map [string ]string {
2260+ codersdk .CoderDesktopTelemetryHeader : "{\" device_os" ,
2261+ },
2262+ expected : telemetry.UserTailnetConnection {},
2263+ },
2264+ }
2265+
2266+ // nolint: paralleltest // no longer need to reinitialize loop vars in go 1.22
2267+ for _ , tc := range testCases {
2268+ t .Run (tc .name , func (t * testing.T ) {
2269+ t .Parallel ()
2270+
2271+ ctx := testutil .Context (t , testutil .WaitLong )
2272+ logger := testutil .Logger (t )
2273+
2274+ fTelemetry := newFakeTelemetryReporter (ctx , t , 200 )
2275+ fTelemetry .enabled = false
2276+ firstClient := coderdtest .New (t , & coderdtest.Options {
2277+ Logger : & logger ,
2278+ TelemetryReporter : fTelemetry ,
2279+ })
2280+ firstUser := coderdtest .CreateFirstUser (t , firstClient )
2281+ member , memberUser := coderdtest .CreateAnotherUser (t , firstClient , firstUser .OrganizationID , rbac .RoleTemplateAdmin ())
2282+
2283+ headers := http.Header {
2284+ "Coder-Session-Token" : []string {member .SessionToken ()},
2285+ }
2286+ for k , v := range tc .headers {
2287+ headers .Add (k , v )
2288+ }
2289+
2290+ // enable telemetry now that user is created.
2291+ fTelemetry .enabled = true
2292+
2293+ u , err := member .URL .Parse ("/api/v2/tailnet" )
2294+ require .NoError (t , err )
2295+ q := u .Query ()
2296+ q .Set ("version" , "2.0" )
2297+ u .RawQuery = q .Encode ()
2298+
2299+ predialTime := time .Now ()
2300+
2301+ //nolint:bodyclose // websocket package closes this for you
2302+ wsConn , resp , err := websocket .Dial (ctx , u .String (), & websocket.DialOptions {
2303+ HTTPHeader : headers ,
2304+ })
2305+ if err != nil {
2306+ if resp != nil && resp .StatusCode != http .StatusSwitchingProtocols {
2307+ err = codersdk .ReadBodyAsError (resp )
2308+ }
2309+ require .NoError (t , err )
2310+ }
2311+ defer wsConn .Close (websocket .StatusNormalClosure , "done" )
2312+
2313+ // Check telemetry
2314+ snapshot := testutil .RequireRecvCtx (ctx , t , fTelemetry .snapshots )
2315+ require .Len (t , snapshot .UserTailnetConnections , 1 )
2316+ telemetryConnection := snapshot .UserTailnetConnections [0 ]
2317+ require .Equal (t , memberUser .ID .String (), telemetryConnection .UserID )
2318+ require .GreaterOrEqual (t , telemetryConnection .ConnectedAt , predialTime )
2319+ require .LessOrEqual (t , telemetryConnection .ConnectedAt , time .Now ())
2320+ require .NotEmpty (t , telemetryConnection .PeerID )
2321+ requireEqualOrBothNil (t , telemetryConnection .DeviceID , tc .expected .DeviceID )
2322+ requireEqualOrBothNil (t , telemetryConnection .DeviceOS , tc .expected .DeviceOS )
2323+ requireEqualOrBothNil (t , telemetryConnection .CoderDesktopVersion , tc .expected .CoderDesktopVersion )
2324+
2325+ beforeDisconnectTime := time .Now ()
2326+ err = wsConn .Close (websocket .StatusNormalClosure , "done" )
2327+ require .NoError (t , err )
2328+
2329+ snapshot = testutil .RequireRecvCtx (ctx , t , fTelemetry .snapshots )
2330+ require .Len (t , snapshot .UserTailnetConnections , 1 )
2331+ telemetryDisconnection := snapshot .UserTailnetConnections [0 ]
2332+ require .Equal (t , memberUser .ID .String (), telemetryDisconnection .UserID )
2333+ require .Equal (t , telemetryConnection .ConnectedAt , telemetryDisconnection .ConnectedAt )
2334+ require .Equal (t , telemetryConnection .UserID , telemetryDisconnection .UserID )
2335+ require .Equal (t , telemetryConnection .PeerID , telemetryDisconnection .PeerID )
2336+ require .NotNil (t , telemetryDisconnection .DisconnectedAt )
2337+ require .GreaterOrEqual (t , * telemetryDisconnection .DisconnectedAt , beforeDisconnectTime )
2338+ require .LessOrEqual (t , * telemetryDisconnection .DisconnectedAt , time .Now ())
2339+ requireEqualOrBothNil (t , telemetryConnection .DeviceID , tc .expected .DeviceID )
2340+ requireEqualOrBothNil (t , telemetryConnection .DeviceOS , tc .expected .DeviceOS )
2341+ requireEqualOrBothNil (t , telemetryConnection .CoderDesktopVersion , tc .expected .CoderDesktopVersion )
2342+ })
2343+ }
22492344}
22502345
22512346func buildWorkspaceWithAgent (
@@ -2414,3 +2509,12 @@ func (f *fakeTelemetryReporter) Enabled() bool {
24142509
24152510// Close implements the telemetry.Reporter interface.
24162511func (* fakeTelemetryReporter ) Close () {}
2512+
2513+ func requireEqualOrBothNil [T any ](t testing.TB , a , b * T ) {
2514+ t .Helper ()
2515+ if a != nil && b != nil {
2516+ require .Equal (t , * a , * b )
2517+ return
2518+ }
2519+ require .Equal (t , a , b )
2520+ }
0 commit comments