mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+41
-5
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user