Skip to content

Commit 6326643

Browse files
committed
allow endtime for today include the hour
1 parent 4820c53 commit 6326643

File tree

1 file changed

+73
-43
lines changed

1 file changed

+73
-43
lines changed

coderd/insights.go

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,16 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
6666
return
6767
}
6868

69-
// TODO(mafredri): Client or deployment timezone?
70-
// Example:
71-
// - I want data from Monday - Friday
72-
// - I'm UTC+3 and the deployment is UTC+0
73-
// - Do we select Monday - Friday in UTC+0 or UTC+3?
74-
// - Considering users can be in different timezones, perhaps this should be per-user (but we don't keep track of user timezones).
7569
p := httpapi.NewQueryParamParser().
7670
Required("start_time").
7771
Required("end_time")
7872
vals := r.URL.Query()
7973
var (
80-
startTime = p.Time3339Nano(vals, time.Time{}, "start_time")
81-
endTime = p.Time3339Nano(vals, time.Time{}, "end_time")
82-
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
74+
// The QueryParamParser does not preserve timezone, so we need
75+
// to parse the time ourselves.
76+
startTimeString = p.String(vals, "", "start_time")
77+
endTimeString = p.String(vals, "", "end_time")
78+
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
8379
)
8480
p.ErrorExcessParams(vals)
8581
if len(p.Errors) > 0 {
@@ -90,15 +86,11 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
9086
return
9187
}
9288

93-
if !verifyInsightsStartAndEndTime(ctx, rw, startTime, endTime) {
89+
startTime, endTime, ok := parseInsightsStartAndEndTime(ctx, rw, startTimeString, endTimeString)
90+
if !ok {
9491
return
9592
}
9693

97-
// Should we verify all template IDs exist, or just return no rows?
98-
// _, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
99-
// IDs: templateIDs,
100-
// })
101-
10294
rows, err := api.Database.GetUserLatencyInsights(ctx, database.GetUserLatencyInsightsParams{
10395
StartTime: startTime,
10496
EndTime: endTime,
@@ -192,10 +184,12 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
192184
Required("end_time")
193185
vals := r.URL.Query()
194186
var (
195-
startTime = p.Time3339Nano(vals, time.Time{}, "start_time")
196-
endTime = p.Time3339Nano(vals, time.Time{}, "end_time")
197-
intervalString = p.String(vals, string(codersdk.InsightsReportIntervalNone), "interval")
198-
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
187+
// The QueryParamParser does not preserve timezone, so we need
188+
// to parse the time ourselves.
189+
startTimeString = p.String(vals, "", "start_time")
190+
endTimeString = p.String(vals, "", "end_time")
191+
intervalString = p.String(vals, string(codersdk.InsightsReportIntervalNone), "interval")
192+
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
199193
)
200194
p.ErrorExcessParams(vals)
201195
if len(p.Errors) > 0 {
@@ -206,19 +200,15 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
206200
return
207201
}
208202

209-
if !verifyInsightsStartAndEndTime(ctx, rw, startTime, endTime) {
203+
startTime, endTime, ok := parseInsightsStartAndEndTime(ctx, rw, startTimeString, endTimeString)
204+
if !ok {
210205
return
211206
}
212207
interval, ok := verifyInsightsInterval(ctx, rw, intervalString)
213208
if !ok {
214209
return
215210
}
216211

217-
// Should we verify all template IDs exist, or just return no rows?
218-
// _, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
219-
// IDs: templateIDs,
220-
// })
221-
222212
var usage database.GetTemplateInsightsRow
223213
var dailyUsage []database.GetTemplateDailyInsightsRow
224214
// Use a transaction to ensure that we get consistent data between
@@ -310,54 +300,94 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
310300
httpapi.Write(ctx, rw, http.StatusOK, resp)
311301
}
312302

313-
func verifyInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, startTime, endTime time.Time) bool {
314-
for _, v := range []struct {
315-
name string
316-
t time.Time
303+
// parseInsightsStartAndEndTime parses the start and end time query parameters
304+
// and returns the parsed values. The client provided timezone must be preserved
305+
// when parsing the time. Verification is performed so that the start and end
306+
// time are not zero and that the end time is not before the start time. The
307+
// clock must be set to 00:00:00, except for "today", where end time is allowed
308+
// to provide the hour of the day (e.g. 14:00:00).
309+
func parseInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, startTimeString, endTimeString string) (startTime, endTime time.Time, ok bool) {
310+
const insightsTimeLayout = time.RFC3339Nano
311+
312+
for _, qp := range []struct {
313+
name, value string
314+
dest *time.Time
317315
}{
318-
{"start_time", startTime},
319-
{"end_time", endTime},
316+
{"start_time", startTimeString, &startTime},
317+
{"end_time", endTimeString, &endTime},
320318
} {
321-
if v.t.IsZero() {
319+
t, err := time.Parse(insightsTimeLayout, qp.value)
320+
if err != nil {
322321
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
323322
Message: "Query parameter has invalid value.",
324323
Validations: []codersdk.ValidationError{
325324
{
326-
Field: v.name,
327-
Detail: "must be not be zero",
325+
Field: qp.name,
326+
Detail: fmt.Sprintf("Query param %q must be a valid date format (%s): %s", qp.name, insightsTimeLayout, err.Error()),
328327
},
329328
},
330329
})
331-
return false
330+
return time.Time{}, time.Time{}, false
332331
}
333-
h, m, s := v.t.Clock()
334-
if h != 0 || m != 0 || s != 0 {
332+
if t.IsZero() {
333+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
334+
Message: "Query parameter has invalid value.",
335+
Validations: []codersdk.ValidationError{
336+
{
337+
Field: qp.name,
338+
Detail: fmt.Sprintf("Query param %q must not be zero", qp.name),
339+
},
340+
},
341+
})
342+
return time.Time{}, time.Time{}, false
343+
}
344+
ensureZeroHour := true
345+
if qp.name == "end_time" {
346+
ey, em, ed := t.Date()
347+
ty, tm, td := time.Now().Date()
348+
349+
ensureZeroHour = ey != ty || em != tm || ed != td
350+
}
351+
h, m, s := t.Clock()
352+
if ensureZeroHour && (h != 0 || m != 0 || s != 0) {
353+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
354+
Message: "Query parameter has invalid value.",
355+
Validations: []codersdk.ValidationError{
356+
{
357+
Field: qp.name,
358+
Detail: fmt.Sprintf("Query param %q must have the clock set to 00:00:00", qp.name),
359+
},
360+
},
361+
})
362+
return time.Time{}, time.Time{}, false
363+
} else if m != 0 || s != 0 {
335364
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
336365
Message: "Query parameter has invalid value.",
337366
Validations: []codersdk.ValidationError{
338367
{
339-
Field: v.name,
340-
Detail: "clock must be 00:00:00",
368+
Field: qp.name,
369+
Detail: fmt.Sprintf("Query param %q must have the clock set to %02d:00:00", qp.name, h),
341370
},
342371
},
343372
})
344-
return false
373+
return time.Time{}, time.Time{}, false
345374
}
375+
*qp.dest = t
346376
}
347377
if endTime.Before(startTime) {
348378
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
349379
Message: "Query parameter has invalid value.",
350380
Validations: []codersdk.ValidationError{
351381
{
352382
Field: "end_time",
353-
Detail: "must be after start_time",
383+
Detail: fmt.Sprintf("Query param %q must be greater than %q", "end_time", "start_time"),
354384
},
355385
},
356386
})
357-
return false
387+
return time.Time{}, time.Time{}, false
358388
}
359389

360-
return true
390+
return startTime, endTime, true
361391
}
362392

363393
func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string) (codersdk.InsightsReportInterval, bool) {

0 commit comments

Comments
 (0)