Skip to content

Commit 0a95dcd

Browse files
committed
report html first served at with a TelemetryItem
1 parent 6554aa3 commit 0a95dcd

File tree

4 files changed

+152
-71
lines changed

4 files changed

+152
-71
lines changed

coderd/database/dbgen/dbgen.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,23 @@ func ProvisionerJobTimings(t testing.TB, db database.Store, build database.Works
10931093
return timings
10941094
}
10951095

1096+
func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem) database.TelemetryItem {
1097+
if seed.Key == "" {
1098+
seed.Key = testutil.GetRandomName(t)
1099+
}
1100+
if seed.Value == "" {
1101+
seed.Value = time.Now().Format(time.RFC3339)
1102+
}
1103+
err := db.UpsertTelemetryItem(genCtx, database.UpsertTelemetryItemParams{
1104+
Key: seed.Key,
1105+
Value: seed.Value,
1106+
})
1107+
require.NoError(t, err, "upsert telemetry item")
1108+
item, err := db.GetTelemetryItem(genCtx, seed.Key)
1109+
require.NoError(t, err, "get telemetry item")
1110+
return item
1111+
}
1112+
10961113
func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming {
10971114
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
10981115
JobID: takeFirst(seed.JobID, uuid.New()),

coderd/telemetry/telemetry.go

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -258,32 +258,26 @@ func (r *remoteReporter) deployment() error {
258258
r.options.Logger.Debug(r.ctx, "check IDP org sync", slog.Error(err))
259259
}
260260

261-
htmlFirstServedAt, err := getHTMLFirstServedAt(r.ctx, r.options.Database)
262-
if err != nil && !errors.Is(err, sql.ErrNoRows) {
263-
r.options.Logger.Debug(r.ctx, "get telemetry html first served at", slog.Error(err))
264-
}
265-
266261
data, err := json.Marshal(&Deployment{
267-
ID: r.options.DeploymentID,
268-
Architecture: sysInfo.Architecture,
269-
BuiltinPostgres: r.options.BuiltinPostgres,
270-
Containerized: containerized,
271-
Config: r.options.DeploymentConfig,
272-
Kubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
273-
InstallSource: installSource,
274-
Tunnel: r.options.Tunnel,
275-
OSType: sysInfo.OS.Type,
276-
OSFamily: sysInfo.OS.Family,
277-
OSPlatform: sysInfo.OS.Platform,
278-
OSName: sysInfo.OS.Name,
279-
OSVersion: sysInfo.OS.Version,
280-
CPUCores: runtime.NumCPU(),
281-
MemoryTotal: mem.Total,
282-
MachineID: sysInfo.UniqueID,
283-
StartedAt: r.startedAt,
284-
ShutdownAt: r.shutdownAt,
285-
IDPOrgSync: &idpOrgSync,
286-
HTMLFirstServedAt: htmlFirstServedAt,
262+
ID: r.options.DeploymentID,
263+
Architecture: sysInfo.Architecture,
264+
BuiltinPostgres: r.options.BuiltinPostgres,
265+
Containerized: containerized,
266+
Config: r.options.DeploymentConfig,
267+
Kubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
268+
InstallSource: installSource,
269+
Tunnel: r.options.Tunnel,
270+
OSType: sysInfo.OS.Type,
271+
OSFamily: sysInfo.OS.Family,
272+
OSPlatform: sysInfo.OS.Platform,
273+
OSName: sysInfo.OS.Name,
274+
OSVersion: sysInfo.OS.Version,
275+
CPUCores: runtime.NumCPU(),
276+
MemoryTotal: mem.Total,
277+
MachineID: sysInfo.UniqueID,
278+
StartedAt: r.startedAt,
279+
ShutdownAt: r.shutdownAt,
280+
IDPOrgSync: &idpOrgSync,
287281
})
288282
if err != nil {
289283
return xerrors.Errorf("marshal deployment: %w", err)
@@ -605,6 +599,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
605599
}
606600
return nil
607601
})
602+
eg.Go(func() error {
603+
items, err := r.options.Database.GetTelemetryItems(ctx)
604+
if err != nil {
605+
return xerrors.Errorf("get telemetry items: %w", err)
606+
}
607+
snapshot.TelemetryItems = make([]TelemetryItem, 0, len(items))
608+
for _, item := range items {
609+
snapshot.TelemetryItems = append(snapshot.TelemetryItems, ConvertTelemetryItem(item))
610+
}
611+
return nil
612+
})
608613

609614
err := eg.Wait()
610615
if err != nil {
@@ -1011,6 +1016,15 @@ func ConvertOrganization(org database.Organization) Organization {
10111016
}
10121017
}
10131018

1019+
func ConvertTelemetryItem(item database.TelemetryItem) TelemetryItem {
1020+
return TelemetryItem{
1021+
Key: item.Key,
1022+
Value: item.Value,
1023+
CreatedAt: item.CreatedAt,
1024+
UpdatedAt: item.UpdatedAt,
1025+
}
1026+
}
1027+
10141028
// Snapshot represents a point-in-time anonymized database dump.
10151029
// Data is aggregated by latest on the server-side, so partial data
10161030
// can be sent without issue.
@@ -1038,6 +1052,7 @@ type Snapshot struct {
10381052
Workspaces []Workspace `json:"workspaces"`
10391053
NetworkEvents []NetworkEvent `json:"network_events"`
10401054
Organizations []Organization `json:"organizations"`
1055+
TelemetryItems []TelemetryItem `json:"telemetry_items"`
10411056
}
10421057

10431058
// Deployment contains information about the host running Coder.
@@ -1062,8 +1077,7 @@ type Deployment struct {
10621077
ShutdownAt *time.Time `json:"shutdown_at"`
10631078
// While IDPOrgSync will always be set, it's nullable to make
10641079
// the struct backwards compatible with older coder versions.
1065-
IDPOrgSync *bool `json:"idp_org_sync"`
1066-
HTMLFirstServedAt *time.Time `json:"html_first_served_at"`
1080+
IDPOrgSync *bool `json:"idp_org_sync"`
10671081
}
10681082

10691083
type APIKey struct {
@@ -1563,6 +1577,20 @@ type Organization struct {
15631577
CreatedAt time.Time `json:"created_at"`
15641578
}
15651579

1580+
//revive:disable:exported
1581+
type TelemetryItemKey string
1582+
1583+
const (
1584+
TelemetryItemKeyHTMLFirstServedAt TelemetryItemKey = "html_first_served_at"
1585+
)
1586+
1587+
type TelemetryItem struct {
1588+
Key string `json:"key"`
1589+
Value string `json:"value"`
1590+
CreatedAt time.Time `json:"created_at"`
1591+
UpdatedAt time.Time `json:"updated_at"`
1592+
}
1593+
15661594
type noopReporter struct{}
15671595

15681596
func (*noopReporter) Report(_ *Snapshot) {}

coderd/telemetry/telemetry_test.go

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func TestTelemetry(t *testing.T) {
7575
Health: database.WorkspaceAppHealthDisabled,
7676
OpenIn: database.WorkspaceAppOpenInSlimWindow,
7777
})
78+
_ = dbgen.TelemetryItem(t, db, database.TelemetryItem{
79+
Key: string(telemetry.TelemetryItemKeyHTMLFirstServedAt),
80+
Value: time.Now().Format(time.RFC3339),
81+
})
7882
group := dbgen.Group(t, db, database.Group{})
7983
_ = dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: user.ID, GroupID: group.ID})
8084
wsagent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{})
@@ -127,7 +131,7 @@ func TestTelemetry(t *testing.T) {
127131
require.Len(t, snapshot.WorkspaceProxies, 1)
128132
require.Len(t, snapshot.WorkspaceModules, 1)
129133
require.Len(t, snapshot.Organizations, 1)
130-
134+
require.Len(t, snapshot.TelemetryItems, 1)
131135
wsa := snapshot.WorkspaceAgents[0]
132136
require.Len(t, wsa.Subsystems, 2)
133137
require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0])
@@ -306,25 +310,6 @@ func TestTelemetry(t *testing.T) {
306310
deployment, _ = collectSnapshot(t, db, nil)
307311
require.True(t, *deployment.IDPOrgSync)
308312
})
309-
t.Run("HTMLFirstServedAt", func(t *testing.T) {
310-
t.Parallel()
311-
db, _ := dbtestutil.NewDB(t)
312-
deployment, _ := collectSnapshot(t, db, nil)
313-
require.Nil(t, deployment.HTMLFirstServedAt)
314-
315-
ctx := testutil.Context(t, testutil.WaitMedium)
316-
now := time.Now().Format(time.RFC3339)
317-
parsedNow, err := time.Parse(time.RFC3339, now)
318-
require.NoError(t, err)
319-
require.NoError(t, db.SetTelemetryHTMLFirstServedAt(ctx, now))
320-
deployment, _ = collectSnapshot(t, db, nil)
321-
require.Equal(t, *deployment.HTMLFirstServedAt, parsedNow)
322-
323-
// Test idempotency
324-
require.NoError(t, db.SetTelemetryHTMLFirstServedAt(ctx, time.Now().Add(time.Hour).Format(time.RFC3339)))
325-
deployment, _ = collectSnapshot(t, db, nil)
326-
require.Equal(t, *deployment.HTMLFirstServedAt, parsedNow)
327-
})
328313
}
329314

330315
// nolint:paralleltest
@@ -335,6 +320,47 @@ func TestTelemetryInstallSource(t *testing.T) {
335320
require.Equal(t, "aws_marketplace", deployment.InstallSource)
336321
}
337322

323+
func TestTelemetryItem(t *testing.T) {
324+
t.Parallel()
325+
ctx := testutil.Context(t, testutil.WaitMedium)
326+
db, _ := dbtestutil.NewDB(t)
327+
key := testutil.GetRandomName(t)
328+
value := time.Now().Format(time.RFC3339)
329+
330+
err := db.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{
331+
Key: key,
332+
Value: value,
333+
})
334+
require.NoError(t, err)
335+
336+
item, err := db.GetTelemetryItem(ctx, key)
337+
require.NoError(t, err)
338+
require.Equal(t, item.Key, key)
339+
require.Equal(t, item.Value, value)
340+
341+
// Inserting a new value should not update the existing value
342+
err = db.InsertTelemetryItemIfNotExists(ctx, database.InsertTelemetryItemIfNotExistsParams{
343+
Key: key,
344+
Value: "new_value",
345+
})
346+
require.NoError(t, err)
347+
348+
item, err = db.GetTelemetryItem(ctx, key)
349+
require.NoError(t, err)
350+
require.Equal(t, item.Value, value)
351+
352+
// Upserting a new value should update the existing value
353+
err = db.UpsertTelemetryItem(ctx, database.UpsertTelemetryItemParams{
354+
Key: key,
355+
Value: "new_value",
356+
})
357+
require.NoError(t, err)
358+
359+
item, err = db.GetTelemetryItem(ctx, key)
360+
require.NoError(t, err)
361+
require.Equal(t, item.Value, "new_value")
362+
}
363+
338364
func collectSnapshot(t *testing.T, db database.Store, addOptionsFn func(opts telemetry.Options) telemetry.Options) (*telemetry.Deployment, *telemetry.Snapshot) {
339365
t.Helper()
340366
deployment := make(chan *telemetry.Deployment, 64)

site/site.go

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -330,29 +330,35 @@ func ShouldCacheFile(reqFile string) bool {
330330
// reportHTMLFirstServedAt sends a telemetry report when the first HTML is ever served.
331331
// The purpose is to track the first time the first user opens the site.
332332
func (h *Handler) reportHTMLFirstServedAt() {
333-
// `Once` is used to reduce the volume of db calls and telemetry reports.
334-
// It's fine to run this multiple times, but it's unnecessary.
335-
h.TelemetryHTMLServedOnce.Do(func() {
336-
ctx := context.Background()
337-
// nolint:gocritic // Only used for telemetry, so AsSystemRestricted is fine.
338-
_, err := h.opts.Database.GetTelemetryHTMLFirstServedAt(dbauthz.AsSystemRestricted(ctx))
339-
if err == nil {
340-
// If the value is already set, then we reported it before.
341-
// We don't need to report it again.
342-
return
343-
}
344-
if !errors.Is(err, sql.ErrNoRows) {
345-
h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err))
346-
return
347-
}
348-
// SetTelemetryHTMLFirstServedAt is idempotent, so there's no harm in calling it multiple times,
349-
// even across restarts. Once it's set for the first time, it will never be changed.
350-
// nolint:gocritic // Only used for telemetry, so AsSystemRestricted is fine.
351-
if err := h.opts.Database.SetTelemetryHTMLFirstServedAt(dbauthz.AsSystemRestricted(ctx), time.Now().Format(time.RFC3339)); err != nil {
352-
h.opts.Logger.Debug(ctx, "failed to set telemetry html first served at", slog.Error(err))
353-
return
354-
}
355-
h.opts.Telemetry.ReportDeployment()
333+
ctx := context.Background()
334+
itemKey := string(telemetry.TelemetryItemKeyHTMLFirstServedAt)
335+
// nolint:gocritic // Only used for telemetry, so AsSystemRestricted is fine.
336+
_, err := h.opts.Database.GetTelemetryItem(dbauthz.AsSystemRestricted(ctx), itemKey)
337+
if err == nil {
338+
// If the value is already set, then we reported it before.
339+
// We don't need to report it again.
340+
return
341+
}
342+
if !errors.Is(err, sql.ErrNoRows) {
343+
h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err))
344+
return
345+
}
346+
// nolint:gocritic // Only used for telemetry, so AsSystemRestricted is fine.
347+
if err := h.opts.Database.InsertTelemetryItemIfNotExists(dbauthz.AsSystemRestricted(ctx), database.InsertTelemetryItemIfNotExistsParams{
348+
Key: string(telemetry.TelemetryItemKeyHTMLFirstServedAt),
349+
Value: time.Now().Format(time.RFC3339),
350+
}); err != nil {
351+
h.opts.Logger.Debug(ctx, "failed to set telemetry html first served at", slog.Error(err))
352+
return
353+
}
354+
// nolint:gocritic // Only used for telemetry, so AsSystemRestricted is fine.
355+
item, err := h.opts.Database.GetTelemetryItem(dbauthz.AsSystemRestricted(ctx), itemKey)
356+
if err != nil {
357+
h.opts.Logger.Debug(ctx, "failed to get telemetry html first served at", slog.Error(err))
358+
return
359+
}
360+
h.opts.Telemetry.Report(&telemetry.Snapshot{
361+
TelemetryItems: []telemetry.TelemetryItem{telemetry.ConvertTelemetryItem(item)},
356362
})
357363
}
358364

@@ -362,7 +368,11 @@ func (h *Handler) serveHTML(resp http.ResponseWriter, request *http.Request, req
362368
// Pass "index.html" to the ServeContent so the ServeContent sets the right content headers.
363369
reqPath = "index.html"
364370
}
365-
go h.reportHTMLFirstServedAt()
371+
// `Once` is used to reduce the volume of db calls and telemetry reports.
372+
// It's fine to run the enclosed function multiple times, but it's unnecessary.
373+
h.TelemetryHTMLServedOnce.Do(func() {
374+
go h.reportHTMLFirstServedAt()
375+
})
366376
http.ServeContent(resp, request, reqPath, time.Time{}, bytes.NewReader(data))
367377
return true
368378
}

0 commit comments

Comments
 (0)