@@ -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
21362137ctx := testutil .Context (t ,testutil .WaitLong )
21372138logger := testutil .Logger (t )
2138-
2139- fTelemetry := newFakeTelemetryReporter (ctx ,t ,200 )
2140- fTelemetry .enabled = false
21412139firstClient ,_ ,api := coderdtest .NewWithAPI (t ,& coderdtest.Options {
2142- Coordinator :tailnet .NewCoordinator (logger ),
2143- TelemetryReporter :fTelemetry ,
2140+ Coordinator :tailnet .NewCoordinator (logger ),
21442141})
21452142firstUser := coderdtest .CreateFirstUser (t ,firstClient )
21462143member ,memberUser := coderdtest .CreateAnotherUser (t ,firstClient ,firstUser .OrganizationID ,rbac .RoleTemplateAdmin ())
21472144
21482145// Create a workspace with an agent
21492146firstWorkspace := 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-
21542148u ,err := member .URL .Parse ("/api/v2/tailnet" )
21552149require .NoError (t ,err )
21562150q := u .Query ()
21572151q .Set ("version" ,"2.0" )
21582152u .RawQuery = q .Encode ()
21592153
2160- predialTime := time .Now ()
2161-
21622154//nolint:bodyclose // websocket package closes this for you
21632155wsConn ,resp ,err := websocket .Dial (ctx ,u .String (),& websocket.DialOptions {
21642156HTTPHeader : http.Header {
@@ -2173,15 +2165,6 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) {
21732165}
21742166defer 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-
21852168rpcClient ,err := tailnet .NewDRPCClient (
21862169websocket .NetConn (ctx ,wsConn ,websocket .MessageBinary ),
21872170logger ,
@@ -2229,23 +2212,135 @@ func TestOwnedWorkspacesCoordinate(t *testing.T) {
22292212NumAgents :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 )
22372226require .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+ }