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

* chore: Add timezone param to DAU SQL query
* Merge DAUs response
* Pass time offsets to metricscache
This commit is contained in:
Steven Masley
2023-05-30 13:18:27 -04:00
committed by GitHub
parent 68658b5197
commit c795a0e500
29 changed files with 606 additions and 310 deletions
+41 -5
View File
@@ -1720,12 +1720,48 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) {
return exp, json.NewDecoder(res.Body).Decode(&exp)
}
type DeploymentDAUsResponse struct {
Entries []DAUEntry `json:"entries"`
type DAUsResponse struct {
Entries []DAUEntry `json:"entries"`
TZHourOffset int `json:"tz_hour_offset"`
}
func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil)
type DAUEntry struct {
Date time.Time `json:"date" format:"date-time"`
Amount int `json:"amount"`
}
type DAURequest struct {
TZHourOffset int
}
func (d DAURequest) asRequestOption() RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
q.Set("tz_offset", strconv.Itoa(d.TZHourOffset))
r.URL.RawQuery = q.Encode()
}
}
func TimezoneOffsetHour(loc *time.Location) int {
if loc == nil {
// Default to UTC time to be consistent across all callers.
loc = time.UTC
}
_, offsetSec := time.Now().In(loc).Zone()
// Convert to hours
return offsetSec / 60 / 60
}
func (c *Client) DeploymentDAUsLocalTZ(ctx context.Context) (*DAUsResponse, error) {
return c.DeploymentDAUs(ctx, TimezoneOffsetHour(time.Local))
}
// DeploymentDAUs requires a tzOffset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) for the
// local timezone.
func (c *Client) DeploymentDAUs(ctx context.Context, tzOffset int) (*DAUsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil, DAURequest{
TZHourOffset: tzOffset,
}.asRequestOption())
if err != nil {
return nil, xerrors.Errorf("execute request: %w", err)
}
@@ -1735,7 +1771,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)
}
+57
View File
@@ -3,6 +3,7 @@ package codersdk_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
@@ -195,3 +196,59 @@ func TestSSHConfig_ParseOptions(t *testing.T) {
})
}
}
func TestTimezoneOffsets(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
Loc *time.Location
ExpectedOffset int
}{
{
Name: "UTX",
Loc: time.UTC,
ExpectedOffset: 0,
},
{
Name: "Eastern",
Loc: must(time.LoadLocation("America/New_York")),
ExpectedOffset: -4,
},
{
Name: "Central",
Loc: must(time.LoadLocation("America/Chicago")),
ExpectedOffset: -5,
},
{
Name: "Ireland",
Loc: must(time.LoadLocation("Europe/Dublin")),
ExpectedOffset: 1,
},
{
Name: "HalfHourTz",
// This timezone is +6:30, but the function rounds to the nearest hour.
// This is intentional because our DAUs endpoint only covers 1-hour offsets.
// If the user is in a non-hour timezone, they get the closest hour bucket.
Loc: must(time.LoadLocation("Asia/Yangon")),
ExpectedOffset: 6,
},
}
for _, c := range testCases {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
offset := codersdk.TimezoneOffsetHour(c.Loc)
require.Equal(t, c.ExpectedOffset, offset)
})
}
}
func must[T any](value T, err error) T {
if err != nil {
panic(err)
}
return value
}
+9 -11
View File
@@ -232,18 +232,16 @@ 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"`
func (c *Client) TemplateDAUsLocalTZ(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) {
return c.TemplateDAUs(ctx, templateID, TimezoneOffsetHour(time.Local))
}
// 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) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil)
// TemplateDAUs requires a tzOffset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) for the
// local timezone.
func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID, tzOffset int) (*DAUsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil, DAURequest{
TZHourOffset: tzOffset,
}.asRequestOption())
if err != nil {
return nil, xerrors.Errorf("execute request: %w", err)
}
@@ -253,7 +251,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)
}