Skip to content

fix: strip timezone information from a date in dau response #11962

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 5 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions coderd/insights_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import (
func TestDeploymentInsights(t *testing.T) {
t.Parallel()

clientTz, err := time.LoadLocation("America/Chicago")
require.NoError(t, err)

client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
Expand All @@ -64,7 +67,7 @@ func TestDeploymentInsights(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

daus, err := client.DeploymentDAUs(context.Background(), codersdk.TimezoneOffsetHour(time.UTC))
daus, err := client.DeploymentDAUs(context.Background(), codersdk.TimezoneOffsetHour(clientTz))
require.NoError(t, err)

res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
Expand All @@ -84,22 +87,23 @@ func TestDeploymentInsights(t *testing.T) {
_ = sshConn.Close()

wantDAUs := &codersdk.DAUsResponse{
TZHourOffset: codersdk.TimezoneOffsetHour(clientTz),
Entries: []codersdk.DAUEntry{
{
Date: time.Now().UTC().Truncate(time.Hour * 24),
Date: time.Now().In(clientTz).Format("2006-01-02"),
Amount: 1,
},
},
}
require.Eventuallyf(t, func() bool {
daus, err = client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(time.UTC))
daus, err = client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(clientTz))
require.NoError(t, err)
return len(daus.Entries) > 0
},
testutil.WaitShort, testutil.IntervalFast,
"deployment daus never loaded",
)
gotDAUs, err := client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(time.UTC))
gotDAUs, err := client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(clientTz))
require.NoError(t, err)
require.Equal(t, gotDAUs, wantDAUs)

Expand Down
8 changes: 7 additions & 1 deletion coderd/metricscache/metricscache.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
"github.com/coder/retry"
)

func OnlyDate(t time.Time) string {
return t.Format("2006-01-02")
}

// deploymentTimezoneOffsets are the timezones that are cached and supported.
// Any non-listed timezone offsets will need to use the closest supported one.
var deploymentTimezoneOffsets = []int{
Expand Down Expand Up @@ -166,7 +170,9 @@ func convertDAUResponse[T dauRow](rows []T, tzOffset int) codersdk.DAUsResponse
var resp codersdk.DAUsResponse
for _, date := range fillEmptyDays(dates) {
resp.Entries = append(resp.Entries, codersdk.DAUEntry{
Date: date,
// This date is truncated to 00:00:00 of the given day, so only
// return date information.
Date: OnlyDate(date),
Amount: len(respMap[date]),
})
}
Expand Down
38 changes: 19 additions & 19 deletions coderd/metricscache/metricscache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,19 @@ func TestCache_TemplateUsers(t *testing.T) {
},
tplWant: want{[]codersdk.DAUEntry{
{
Date: date(2022, 8, 27),
Date: metricscache.OnlyDate(date(2022, 8, 27)),
Amount: 1,
},
{
Date: date(2022, 8, 28),
Date: metricscache.OnlyDate(date(2022, 8, 28)),
Amount: 0,
},
{
Date: date(2022, 8, 29),
Date: metricscache.OnlyDate(date(2022, 8, 29)),
Amount: 0,
},
{
Date: date(2022, 8, 30),
Date: metricscache.OnlyDate(date(2022, 8, 30)),
Amount: 1,
},
}, 1},
Expand All @@ -95,15 +95,15 @@ func TestCache_TemplateUsers(t *testing.T) {
},
tplWant: want{[]codersdk.DAUEntry{
{
Date: date(2022, 8, 27),
Date: metricscache.OnlyDate(date(2022, 8, 27)),
Amount: 1,
},
{
Date: date(2022, 8, 28),
Date: metricscache.OnlyDate(date(2022, 8, 28)),
Amount: 1,
},
{
Date: date(2022, 8, 29),
Date: metricscache.OnlyDate(date(2022, 8, 29)),
Amount: 1,
},
}, 1},
Expand All @@ -121,31 +121,31 @@ func TestCache_TemplateUsers(t *testing.T) {
},
tplWant: want{[]codersdk.DAUEntry{
{
Date: date(2022, 1, 1),
Date: metricscache.OnlyDate(date(2022, 1, 1)),
Amount: 2,
},
{
Date: date(2022, 1, 2),
Date: metricscache.OnlyDate(date(2022, 1, 2)),
Amount: 0,
},
{
Date: date(2022, 1, 3),
Date: metricscache.OnlyDate(date(2022, 1, 3)),
Amount: 0,
},
{
Date: date(2022, 1, 4),
Date: metricscache.OnlyDate(date(2022, 1, 4)),
Amount: 1,
},
{
Date: date(2022, 1, 5),
Date: metricscache.OnlyDate(date(2022, 1, 5)),
Amount: 0,
},
{
Date: date(2022, 1, 6),
Date: metricscache.OnlyDate(date(2022, 1, 6)),
Amount: 0,
},
{
Date: date(2022, 1, 7),
Date: metricscache.OnlyDate(date(2022, 1, 7)),
Amount: 2,
},
}, 2},
Expand All @@ -164,17 +164,17 @@ func TestCache_TemplateUsers(t *testing.T) {
},
tplWant: want{[]codersdk.DAUEntry{
{
Date: date(2022, 1, 2),
Date: metricscache.OnlyDate(date(2022, 1, 2)),
Amount: 2,
},
}, 2},
dauWant: []codersdk.DAUEntry{
{
Date: date(2022, 1, 1),
Date: metricscache.OnlyDate(date(2022, 1, 1)),
Amount: 2,
},
{
Date: date(2022, 1, 2),
Date: metricscache.OnlyDate(date(2022, 1, 2)),
Amount: 2,
},
},
Expand All @@ -192,13 +192,13 @@ func TestCache_TemplateUsers(t *testing.T) {
},
dauWant: []codersdk.DAUEntry{
{
Date: date(2022, 1, 1),
Date: metricscache.OnlyDate(date(2022, 1, 1)),
Amount: 2,
},
},
tplWant: want{[]codersdk.DAUEntry{
{
Date: date(2022, 1, 2),
Date: metricscache.OnlyDate(date(2022, 1, 2)),
Amount: 2,
},
}, 2},
Expand Down
2 changes: 1 addition & 1 deletion coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1405,7 +1405,7 @@ func TestTemplateMetrics(t *testing.T) {
wantDAUs := &codersdk.DAUsResponse{
Entries: []codersdk.DAUEntry{
{
Date: time.Now().UTC().Truncate(time.Hour * 24),
Date: time.Now().UTC().Truncate(time.Hour * 24).Format("2006-01-02"),
Amount: 1,
},
},
Expand Down
22 changes: 16 additions & 6 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2168,8 +2168,10 @@ type DAUsResponse struct {
}

type DAUEntry struct {
Date time.Time `json:"date" format:"date-time"`
Amount int `json:"amount"`
// Date is a string formatted as 2024-01-31.
// Timezone and time information is not included.
Date string `json:"date"`
Amount int `json:"amount"`
}

type DAURequest struct {
Expand All @@ -2184,14 +2186,22 @@ func (d DAURequest) asRequestOption() RequestOption {
}
}

func TimezoneOffsetHour(loc *time.Location) int {
// TimezoneOffsetHourWithTime is implemented to match the javascript 'getTimezoneOffset()' function.
// This is the amount of time between this date evaluated in UTC and evaluated in the 'loc'
// The trivial case of times being on the same day is:
// 'time.Now().UTC().Hour() - time.Now().In(loc).Hour()'
func TimezoneOffsetHourWithTime(now time.Time, 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
_, offsetSec := now.In(loc).Zone()
// Convert to hours and flip the sign
return -1 * offsetSec / 60 / 60
}

func TimezoneOffsetHour(loc *time.Location) int {
return TimezoneOffsetHourWithTime(time.Now(), loc)
}
Comment on lines -2187 to 2205
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value was flipped from the javascript implementation. Given this value comes from the browser, I made it match the javascript. I fixed all the tests to be deterministic so we can keep them on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also verified this is the same on Postgres


func (c *Client) DeploymentDAUsLocalTZ(ctx context.Context) (*DAUsResponse, error) {
Expand Down
60 changes: 42 additions & 18 deletions codersdk/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func TestTimezoneOffsets(t *testing.T) {

testCases := []struct {
Name string
Now time.Time
Loc *time.Location
ExpectedOffset int
}{
Expand All @@ -213,29 +214,52 @@ func TestTimezoneOffsets(t *testing.T) {
Loc: time.UTC,
ExpectedOffset: 0,
},
// The following test cases are broken re: daylight savings
//{
// 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: "Eastern",
Now: time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC),
Loc: must(time.LoadLocation("America/New_York")),
ExpectedOffset: 5,
},
{
// Daylight savings is on the 14th of March to Nov 7 in 2021
Name: "EasternDaylightSavings",
Now: time.Date(2021, 3, 16, 0, 0, 0, 0, time.UTC),
Loc: must(time.LoadLocation("America/New_York")),
ExpectedOffset: 4,
},
{
Name: "Central",
Now: time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC),
Loc: must(time.LoadLocation("America/Chicago")),
ExpectedOffset: 6,
},
{
Name: "CentralDaylightSavings",
Now: time.Date(2021, 3, 16, 0, 0, 0, 0, time.UTC),
Loc: must(time.LoadLocation("America/Chicago")),
ExpectedOffset: 5,
},
{
Name: "Ireland",
Now: time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC),
Loc: must(time.LoadLocation("Europe/Dublin")),
ExpectedOffset: 0,
},
{
Name: "IrelandDaylightSavings",
Now: time.Date(2021, 4, 3, 0, 0, 0, 0, time.UTC),
Loc: must(time.LoadLocation("Europe/Dublin")),
ExpectedOffset: -1,
},
{
Name: "HalfHourTz",
Now: time.Date(2024, 1, 20, 6, 0, 0, 0, must(time.LoadLocation("Asia/Yangon"))),
// 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,
ExpectedOffset: -6,
},
}

Expand All @@ -244,7 +268,7 @@ func TestTimezoneOffsets(t *testing.T) {
t.Run(c.Name, func(t *testing.T) {
t.Parallel()

offset := codersdk.TimezoneOffsetHour(c.Loc)
offset := codersdk.TimezoneOffsetHourWithTime(c.Now, c.Loc)
require.Equal(t, c.ExpectedOffset, offset)
})
}
Expand Down
2 changes: 1 addition & 1 deletion docs/api/insights.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions docs/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/api/templates.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading