Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitdea30ba

Browse files
committed
feat: adds device_id, device_os, and coder_desktop_version to telemetry
1 parent081679f commitdea30ba

File tree

11 files changed

+285
-59
lines changed

11 files changed

+285
-59
lines changed

‎coderd/workspaceagents.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,30 @@ func (api *API) tailnetRPCConn(rw http.ResponseWriter, r *http.Request) {
16521652
DeviceOS:nil,
16531653
CoderDesktopVersion:nil,
16541654
}
1655+
1656+
// Parse desktop telemetry from header if it exists
1657+
desktopTelemetryHeader:=r.Header.Get(codersdk.CoderDesktopTelemetryHeader)
1658+
ifdesktopTelemetryHeader!="" {
1659+
vartelemetryData codersdk.CoderDesktopTelemetry
1660+
iferr:=telemetryData.FromHeader(desktopTelemetryHeader);err==nil {
1661+
// Only set fields if they aren't empty
1662+
iftelemetryData.DeviceID!="" {
1663+
connectionTelemetryEvent.DeviceID=&telemetryData.DeviceID
1664+
}
1665+
iftelemetryData.DeviceOS!="" {
1666+
connectionTelemetryEvent.DeviceOS=&telemetryData.DeviceOS
1667+
}
1668+
iftelemetryData.CoderDesktopVersion!="" {
1669+
connectionTelemetryEvent.CoderDesktopVersion=&telemetryData.CoderDesktopVersion
1670+
}
1671+
api.Logger.Debug(ctx,"received desktop telemetry",
1672+
slog.F("device_id",telemetryData.DeviceID),
1673+
slog.F("device_os",telemetryData.DeviceOS),
1674+
slog.F("desktop_version",telemetryData.CoderDesktopVersion))
1675+
}else {
1676+
api.Logger.Warn(ctx,"failed to parse desktop telemetry header",slog.Error(err))
1677+
}
1678+
}
16551679
api.Telemetry.Report(&telemetry.Snapshot{
16561680
UserTailnetConnections: []telemetry.UserTailnetConnection{connectionTelemetryEvent},
16571681
})

‎coderd/workspaceagents_test.go

Lines changed: 136 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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
deferwsConn.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,134 @@ 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+
funcTestUserTailnetTelemetry(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+
namestring
2230+
headersmap[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+
for_,tc:=rangetestCases {
2267+
t.Run(tc.name,func(t*testing.T) {
2268+
t.Parallel()
2269+
2270+
ctx:=testutil.Context(t,testutil.WaitLong)
2271+
logger:=testutil.Logger(t)
2272+
2273+
fTelemetry:=newFakeTelemetryReporter(ctx,t,200)
2274+
fTelemetry.enabled=false
2275+
firstClient:=coderdtest.New(t,&coderdtest.Options{
2276+
Logger:&logger,
2277+
TelemetryReporter:fTelemetry,
2278+
})
2279+
firstUser:=coderdtest.CreateFirstUser(t,firstClient)
2280+
member,memberUser:=coderdtest.CreateAnotherUser(t,firstClient,firstUser.OrganizationID,rbac.RoleTemplateAdmin())
2281+
2282+
headers:= http.Header{
2283+
"Coder-Session-Token": []string{member.SessionToken()},
2284+
}
2285+
fork,v:=rangetc.headers {
2286+
headers.Add(k,v)
2287+
}
2288+
2289+
// enable telemetry now that user is created.
2290+
fTelemetry.enabled=true
2291+
2292+
u,err:=member.URL.Parse("/api/v2/tailnet")
2293+
require.NoError(t,err)
2294+
q:=u.Query()
2295+
q.Set("version","2.0")
2296+
u.RawQuery=q.Encode()
2297+
2298+
predialTime:=time.Now()
2299+
2300+
//nolint:bodyclose // websocket package closes this for you
2301+
wsConn,resp,err:=websocket.Dial(ctx,u.String(),&websocket.DialOptions{
2302+
HTTPHeader:headers,
2303+
})
2304+
iferr!=nil {
2305+
ifresp!=nil&&resp.StatusCode!=http.StatusSwitchingProtocols {
2306+
err=codersdk.ReadBodyAsError(resp)
2307+
}
2308+
require.NoError(t,err)
2309+
}
2310+
deferwsConn.Close(websocket.StatusNormalClosure,"done")
2311+
2312+
// Check telemetry
2313+
snapshot:=testutil.RequireRecvCtx(ctx,t,fTelemetry.snapshots)
2314+
require.Len(t,snapshot.UserTailnetConnections,1)
2315+
telemetryConnection:=snapshot.UserTailnetConnections[0]
2316+
require.Equal(t,memberUser.ID.String(),telemetryConnection.UserID)
2317+
require.GreaterOrEqual(t,telemetryConnection.ConnectedAt,predialTime)
2318+
require.LessOrEqual(t,telemetryConnection.ConnectedAt,time.Now())
2319+
require.NotEmpty(t,telemetryConnection.PeerID)
2320+
requireEqualOrBothNil(t,telemetryConnection.DeviceID,tc.expected.DeviceID)
2321+
requireEqualOrBothNil(t,telemetryConnection.DeviceOS,tc.expected.DeviceOS)
2322+
requireEqualOrBothNil(t,telemetryConnection.CoderDesktopVersion,tc.expected.CoderDesktopVersion)
2323+
2324+
beforeDisconnectTime:=time.Now()
2325+
err=wsConn.Close(websocket.StatusNormalClosure,"done")
2326+
require.NoError(t,err)
2327+
2328+
snapshot=testutil.RequireRecvCtx(ctx,t,fTelemetry.snapshots)
2329+
require.Len(t,snapshot.UserTailnetConnections,1)
2330+
telemetryDisconnection:=snapshot.UserTailnetConnections[0]
2331+
require.Equal(t,memberUser.ID.String(),telemetryDisconnection.UserID)
2332+
require.Equal(t,telemetryConnection.ConnectedAt,telemetryDisconnection.ConnectedAt)
2333+
require.Equal(t,telemetryConnection.UserID,telemetryDisconnection.UserID)
2334+
require.Equal(t,telemetryConnection.PeerID,telemetryDisconnection.PeerID)
2335+
require.NotNil(t,telemetryDisconnection.DisconnectedAt)
2336+
require.GreaterOrEqual(t,*telemetryDisconnection.DisconnectedAt,beforeDisconnectTime)
2337+
require.LessOrEqual(t,*telemetryDisconnection.DisconnectedAt,time.Now())
2338+
requireEqualOrBothNil(t,telemetryConnection.DeviceID,tc.expected.DeviceID)
2339+
requireEqualOrBothNil(t,telemetryConnection.DeviceOS,tc.expected.DeviceOS)
2340+
requireEqualOrBothNil(t,telemetryConnection.CoderDesktopVersion,tc.expected.CoderDesktopVersion)
2341+
})
2342+
}
22492343
}
22502344

22512345
funcbuildWorkspaceWithAgent(
@@ -2414,3 +2508,12 @@ func (f *fakeTelemetryReporter) Enabled() bool {
24142508

24152509
// Close implements the telemetry.Reporter interface.
24162510
func (*fakeTelemetryReporter)Close() {}
2511+
2512+
funcrequireEqualOrBothNil[Tany](t testing.TB,a,b*T) {
2513+
t.Helper()
2514+
ifa!=nil&&b!=nil {
2515+
require.Equal(t,*a,*b)
2516+
return
2517+
}
2518+
require.Equal(t,a,b)
2519+
}

‎codersdk/client.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ const (
7676
// only.
7777
CLITelemetryHeader="Coder-CLI-Telemetry"
7878

79+
// CoderDesktopTelemetryHeader contains a JSON-encoded representation of Desktop telemetry
80+
// fields, including device ID, OS, and Desktop version.
81+
CoderDesktopTelemetryHeader="Coder-Desktop-Telemetry"
82+
7983
// ProvisionerDaemonPSK contains the authentication pre-shared key for an external provisioner daemon
8084
ProvisionerDaemonPSK="Coder-Provisioner-Daemon-PSK"
8185

@@ -523,6 +527,28 @@ func (e ValidationError) Error() string {
523527

524528
var_error= (*ValidationError)(nil)
525529

530+
// CoderDesktopTelemetry represents the telemetry data sent from Coder Desktop clients.
531+
// @typescript-ignore CoderDesktopTelemetry
532+
typeCoderDesktopTelemetrystruct {
533+
DeviceIDstring`json:"device_id"`
534+
DeviceOSstring`json:"device_os"`
535+
CoderDesktopVersionstring`json:"coder_desktop_version"`
536+
}
537+
538+
// FromHeader parses the desktop telemetry from the provided header value.
539+
// Returns nil if the header is empty or if parsing fails.
540+
func (t*CoderDesktopTelemetry)FromHeader(headerValuestring)error {
541+
ifheaderValue=="" {
542+
returnnil
543+
}
544+
returnjson.Unmarshal([]byte(headerValue),t)
545+
}
546+
547+
// IsEmpty returns true if all fields in the telemetry data are empty.
548+
func (t*CoderDesktopTelemetry)IsEmpty()bool {
549+
returnt.DeviceID==""&&t.DeviceOS==""&&t.CoderDesktopVersion==""
550+
}
551+
526552
// IsConnectionError is a convenience function for checking if the source of an
527553
// error is due to a 'connection refused', 'no such host', etc.
528554
funcIsConnectionError(errerror)bool {

‎codersdk/client_internal_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"cdr.dev/slog"
2929
"cdr.dev/slog/sloggers/sloghuman"
30+
3031
"github.com/coder/coder/v2/testutil"
3132
)
3233

‎site/src/api/typesGenerated.ts

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎vpn/speaker_internal_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"cdr.dev/slog"
1717
"cdr.dev/slog/sloggers/slogtest"
18+
1819
"github.com/coder/coder/v2/testutil"
1920
)
2021

@@ -47,7 +48,7 @@ func TestSpeaker_RawPeer(t *testing.T) {
4748
errCh<-err
4849
}()
4950

50-
expectedHandshake:="codervpn tunnel 1.0\n"
51+
expectedHandshake:="codervpn tunnel 1.1\n"
5152

5253
b:=make([]byte,256)
5354
n,err:=mp.Read(b)
@@ -155,7 +156,7 @@ func TestSpeaker_OversizeHandshake(t *testing.T) {
155156
errCh<-err
156157
}()
157158

158-
expectedHandshake:="codervpn tunnel 1.0\n"
159+
expectedHandshake:="codervpn tunnel 1.1\n"
159160

160161
b:=make([]byte,256)
161162
n,err:=mp.Read(b)
@@ -177,12 +178,12 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) {
177178
for_,tc:=range []struct {
178179
name,handshakestring
179180
}{
180-
{name:"preamble",handshake:"ssh manager 1.0\n"},
181+
{name:"preamble",handshake:"ssh manager 1.1\n"},
181182
{name:"2components",handshake:"ssh manager\n"},
182183
{name:"newmajors",handshake:"codervpn manager 2.0,3.0\n"},
183184
{name:"0version",handshake:"codervpn 0.1 manager\n"},
184-
{name:"unknown_role",handshake:"codervpn 1.0 supervisor\n"},
185-
{name:"unexpected_role",handshake:"codervpn 1.0 tunnel\n"},
185+
{name:"unknown_role",handshake:"codervpn 1.1 supervisor\n"},
186+
{name:"unexpected_role",handshake:"codervpn 1.1 tunnel\n"},
186187
} {
187188
t.Run(tc.name,func(t*testing.T) {
188189
t.Parallel()
@@ -208,7 +209,7 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) {
208209
_,err=mp.Write([]byte(tc.handshake))
209210
require.NoError(t,err)
210211

211-
expectedHandshake:="codervpn tunnel 1.0\n"
212+
expectedHandshake:="codervpn tunnel 1.1\n"
212213
b:=make([]byte,256)
213214
n,err:=mp.Read(b)
214215
require.NoError(t,err)
@@ -246,7 +247,7 @@ func TestSpeaker_CorruptMessage(t *testing.T) {
246247
errCh<-err
247248
}()
248249

249-
expectedHandshake:="codervpn tunnel 1.0\n"
250+
expectedHandshake:="codervpn tunnel 1.1\n"
250251

251252
b:=make([]byte,256)
252253
n,err:=mp.Read(b)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp