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

Commit1a803fe

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

File tree

10 files changed

+337
-58
lines changed

10 files changed

+337
-58
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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,61 @@ func marshal(res any) string {
352352

353353
returnstring(b)
354354
}
355+
356+
funcTestDesktopTelemetry(t*testing.T) {
357+
t.Parallel()
358+
359+
t.Run("IsEmpty",func(t*testing.T) {
360+
t.Parallel()
361+
dt:=DesktopTelemetry{}
362+
assert.True(t,dt.IsEmpty(),"empty telemetry should be empty")
363+
364+
dt=DesktopTelemetry{DeviceID:"device1"}
365+
assert.False(t,dt.IsEmpty(),"telemetry with deviceID should not be empty")
366+
367+
dt=DesktopTelemetry{DeviceOS:"macOS"}
368+
assert.False(t,dt.IsEmpty(),"telemetry with deviceOS should not be empty")
369+
370+
dt=DesktopTelemetry{CoderDesktopVersion:"1.0.0"}
371+
assert.False(t,dt.IsEmpty(),"telemetry with version should not be empty")
372+
})
373+
374+
t.Run("ToHeader",func(t*testing.T) {
375+
t.Parallel()
376+
dt:=DesktopTelemetry{
377+
DeviceID:"device1",
378+
DeviceOS:"macOS",
379+
CoderDesktopVersion:"1.0.0",
380+
}
381+
header:=dt.ToHeader()
382+
assert.NotEmpty(t,header,"header should not be empty")
383+
384+
// Verify we can unmarshal it back
385+
varparsedDTDesktopTelemetry
386+
err:=json.Unmarshal([]byte(header),&parsedDT)
387+
require.NoError(t,err,"should unmarshal without error")
388+
assert.Equal(t,dt,parsedDT,"unmarshaled value should match original")
389+
})
390+
391+
t.Run("FromHeader",func(t*testing.T) {
392+
t.Parallel()
393+
jsonStr:=`{"device_id":"device1","device_os":"macOS","coder_desktop_version":"1.0.0"}`
394+
vardtDesktopTelemetry
395+
err:=dt.FromHeader(jsonStr)
396+
require.NoError(t,err,"should parse without error")
397+
assert.Equal(t,"device1",dt.DeviceID)
398+
assert.Equal(t,"macOS",dt.DeviceOS)
399+
assert.Equal(t,"1.0.0",dt.CoderDesktopVersion)
400+
401+
// Empty header
402+
dt=DesktopTelemetry{}
403+
err=dt.FromHeader("")
404+
require.NoError(t,err,"empty header should not cause an error")
405+
assert.True(t,dt.IsEmpty(),"empty header should result in empty telemetry")
406+
407+
// Invalid JSON
408+
dt=DesktopTelemetry{}
409+
err=dt.FromHeader("{invalid")
410+
require.Error(t,err,"invalid JSON should cause an error")
411+
})
412+
}

‎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.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp