Skip to content

feat: Fix Deployment DAUs to work with local timezones #7647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 30, 2023
Merged
Prev Previous commit
Next Next commit
Merge DAUs response
  • Loading branch information
Emyrk committed May 23, 2023
commit 45e8342e0eaea4042a47c605f0cf0526588a01c4
4 changes: 2 additions & 2 deletions coderd/insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// @Security CoderSessionToken
// @Produce json
// @Tags Insights
// @Success 200 {object} codersdk.DeploymentDAUsResponse
// @Success 200 {object} codersdk.DAUsResponse
// @Router /insights/daus [get]
func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand All @@ -24,7 +24,7 @@ func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) {

resp, _ := api.metricsCache.DeploymentDAUs()
if resp == nil || resp.Entries == nil {
httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DeploymentDAUsResponse{
httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{
Entries: []codersdk.DAUEntry{},
})
return
Expand Down
2 changes: 1 addition & 1 deletion coderd/insights_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestDeploymentInsights(t *testing.T) {
require.NoError(t, err)
_ = sshConn.Close()

wantDAUs := &codersdk.DeploymentDAUsResponse{
wantDAUs := &codersdk.DAUsResponse{
Entries: []codersdk.DAUEntry{
{
Date: time.Now().UTC().Truncate(time.Hour * 24),
Expand Down
57 changes: 22 additions & 35 deletions coderd/metricscache/metricscache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package metricscache
import (
"context"
"database/sql"
"fmt"
"sync"
"sync/atomic"
"time"
Expand All @@ -29,8 +30,8 @@ type Cache struct {
log slog.Logger
intervals Intervals

deploymentDAUResponses atomic.Pointer[codersdk.DeploymentDAUsResponse]
templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse]
deploymentDAUResponses atomic.Pointer[codersdk.DAUsResponse]
templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.DAUsResponse]
templateUniqueUsers atomic.Pointer[map[uuid.UUID]int]
templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow]
deploymentStatsResponse atomic.Pointer[codersdk.DeploymentStats]
Expand Down Expand Up @@ -107,45 +108,31 @@ func fillEmptyDays(sortedDates []time.Time) []time.Time {
return newDates
}

func convertDAUResponse(rows []database.GetTemplateDAUsRow) codersdk.TemplateDAUsResponse {
respMap := make(map[time.Time][]uuid.UUID)
for _, row := range rows {
uuids := respMap[row.Date]
if uuids == nil {
uuids = make([]uuid.UUID, 0, 8)
}
uuids = append(uuids, row.UserID)
respMap[row.Date] = uuids
}

dates := maps.Keys(respMap)
slices.SortFunc(dates, func(a, b time.Time) bool {
return a.Before(b)
})

var resp codersdk.TemplateDAUsResponse
for _, date := range fillEmptyDays(dates) {
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
Date: date,
Amount: len(respMap[date]),
})
}

return resp
type dauRow interface {
database.GetTemplateDAUsRow |
database.GetDeploymentDAUsRow
}

func convertDeploymentDAUResponse(rows []database.GetDeploymentDAUsRow) codersdk.DeploymentDAUsResponse {
func convertDAUResponse[T dauRow](rows []T) codersdk.DAUsResponse {
respMap := make(map[time.Time][]uuid.UUID)
for _, row := range rows {
respMap[row.Date] = append(respMap[row.Date], row.UserID)
switch row := any(row).(type) {
case database.GetDeploymentDAUsRow:
respMap[row.Date] = append(respMap[row.Date], row.UserID)
case database.GetTemplateDAUsRow:
respMap[row.Date] = append(respMap[row.Date], row.UserID)
default:
// This should never happen.
panic(fmt.Sprintf("%T not acceptable, developer error", row))
}
}

dates := maps.Keys(respMap)
slices.SortFunc(dates, func(a, b time.Time) bool {
return a.Before(b)
})

var resp codersdk.DeploymentDAUsResponse
var resp codersdk.DAUsResponse
for _, date := range fillEmptyDays(dates) {
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
Date: date,
Expand Down Expand Up @@ -174,8 +161,8 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error {
}

var (
deploymentDAUs = codersdk.DeploymentDAUsResponse{}
templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates))
deploymentDAUs = codersdk.DAUsResponse{}
templateDAUs = make(map[uuid.UUID]codersdk.DAUsResponse, len(templates))
templateUniqueUsers = make(map[uuid.UUID]int)
templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow)
)
Expand All @@ -184,7 +171,7 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error {
if err != nil {
return err
}
deploymentDAUs = convertDeploymentDAUResponse(rows)
deploymentDAUs = convertDAUResponse(rows)
c.deploymentDAUResponses.Store(&deploymentDAUs)

for _, template := range templates {
Expand Down Expand Up @@ -297,14 +284,14 @@ func (c *Cache) Close() error {
return nil
}

func (c *Cache) DeploymentDAUs() (*codersdk.DeploymentDAUsResponse, bool) {
func (c *Cache) DeploymentDAUs() (*codersdk.DAUsResponse, bool) {
m := c.deploymentDAUResponses.Load()
return m, m != nil
}

// TemplateDAUs returns an empty response if the template doesn't have users
// or is loading for the first time.
func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.TemplateDAUsResponse, bool) {
func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.DAUsResponse, bool) {
m := c.templateDAUResponses.Load()
if m == nil {
// Data loading.
Expand Down
4 changes: 2 additions & 2 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,15 +614,15 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
// @Produce json
// @Tags Templates
// @Param template path string true "Template ID" format(uuid)
// @Success 200 {object} codersdk.TemplateDAUsResponse
// @Success 200 {object} codersdk.DAUsResponse
// @Router /templates/{template}/daus [get]
func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
template := httpmw.TemplateParam(r)

resp, _ := api.metricsCache.TemplateDAUs(template.ID)
if resp == nil || resp.Entries == nil {
httpapi.Write(ctx, rw, http.StatusOK, &codersdk.TemplateDAUsResponse{
httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{
Entries: []codersdk.DAUEntry{},
})
return
Expand Down
4 changes: 2 additions & 2 deletions coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,7 @@ func TestTemplateMetrics(t *testing.T) {
daus, err := client.TemplateDAUs(context.Background(), template.ID)
require.NoError(t, err)

require.Equal(t, &codersdk.TemplateDAUsResponse{
require.Equal(t, &codersdk.DAUsResponse{
Entries: []codersdk.DAUEntry{},
}, daus, "no DAUs when stats are empty")

Expand All @@ -1001,7 +1001,7 @@ func TestTemplateMetrics(t *testing.T) {
require.NoError(t, err)
_ = sshConn.Close()

wantDAUs := &codersdk.TemplateDAUsResponse{
wantDAUs := &codersdk.DAUsResponse{
Entries: []codersdk.DAUEntry{
{
Date: time.Now().UTC().Truncate(time.Hour * 24),
Expand Down
11 changes: 8 additions & 3 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -1720,11 +1720,16 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) {
return exp, json.NewDecoder(res.Body).Decode(&exp)
}

type DeploymentDAUsResponse struct {
type DAUsResponse struct {
Entries []DAUEntry `json:"entries"`
}

func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) {
type DAUEntry struct {
Date time.Time `json:"date" format:"date-time"`
Amount int `json:"amount"`
}

func (c *Client) DeploymentDAUs(ctx context.Context) (*DAUsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil)
if err != nil {
return nil, xerrors.Errorf("execute request: %w", err)
Expand All @@ -1735,7 +1740,7 @@ func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, e
return nil, ReadBodyAsError(res)
}

var resp DeploymentDAUsResponse
var resp DAUsResponse
return &resp, json.NewDecoder(res.Body).Decode(&resp)
}

Expand Down
14 changes: 2 additions & 12 deletions codersdk/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,17 +232,7 @@ func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID,
return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion)
}

type DAUEntry struct {
Date time.Time `json:"date" format:"date-time"`
Amount int `json:"amount"`
}

// TemplateDAUsResponse contains statistics of daily active users of the template.
type TemplateDAUsResponse struct {
Entries []DAUEntry `json:"entries"`
}

func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*TemplateDAUsResponse, error) {
func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil)
if err != nil {
return nil, xerrors.Errorf("execute request: %w", err)
Expand All @@ -253,7 +243,7 @@ func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*Templ
return nil, ReadBodyAsError(res)
}

var resp TemplateDAUsResponse
var resp DAUsResponse
return &resp, json.NewDecoder(res.Body).Decode(&resp)
}

Expand Down