@@ -66,20 +66,16 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
66
66
return
67
67
}
68
68
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).
75
69
p := httpapi .NewQueryParamParser ().
76
70
Required ("start_time" ).
77
71
Required ("end_time" )
78
72
vals := r .URL .Query ()
79
73
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" )
83
79
)
84
80
p .ErrorExcessParams (vals )
85
81
if len (p .Errors ) > 0 {
@@ -90,15 +86,11 @@ func (api *API) insightsUserLatency(rw http.ResponseWriter, r *http.Request) {
90
86
return
91
87
}
92
88
93
- if ! verifyInsightsStartAndEndTime (ctx , rw , startTime , endTime ) {
89
+ startTime , endTime , ok := parseInsightsStartAndEndTime (ctx , rw , startTimeString , endTimeString )
90
+ if ! ok {
94
91
return
95
92
}
96
93
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
-
102
94
rows , err := api .Database .GetUserLatencyInsights (ctx , database.GetUserLatencyInsightsParams {
103
95
StartTime : startTime ,
104
96
EndTime : endTime ,
@@ -192,10 +184,12 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
192
184
Required ("end_time" )
193
185
vals := r .URL .Query ()
194
186
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" )
199
193
)
200
194
p .ErrorExcessParams (vals )
201
195
if len (p .Errors ) > 0 {
@@ -206,19 +200,15 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
206
200
return
207
201
}
208
202
209
- if ! verifyInsightsStartAndEndTime (ctx , rw , startTime , endTime ) {
203
+ startTime , endTime , ok := parseInsightsStartAndEndTime (ctx , rw , startTimeString , endTimeString )
204
+ if ! ok {
210
205
return
211
206
}
212
207
interval , ok := verifyInsightsInterval (ctx , rw , intervalString )
213
208
if ! ok {
214
209
return
215
210
}
216
211
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
-
222
212
var usage database.GetTemplateInsightsRow
223
213
var dailyUsage []database.GetTemplateDailyInsightsRow
224
214
// 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) {
310
300
httpapi .Write (ctx , rw , http .StatusOK , resp )
311
301
}
312
302
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
317
315
}{
318
- {"start_time" , startTime },
319
- {"end_time" , endTime },
316
+ {"start_time" , startTimeString , & startTime },
317
+ {"end_time" , endTimeString , & endTime },
320
318
} {
321
- if v .t .IsZero () {
319
+ t , err := time .Parse (insightsTimeLayout , qp .value )
320
+ if err != nil {
322
321
httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
323
322
Message : "Query parameter has invalid value." ,
324
323
Validations : []codersdk.ValidationError {
325
324
{
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 ()) ,
328
327
},
329
328
},
330
329
})
331
- return false
330
+ return time. Time {}, time. Time {}, false
332
331
}
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 {
335
364
httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
336
365
Message : "Query parameter has invalid value." ,
337
366
Validations : []codersdk.ValidationError {
338
367
{
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 ) ,
341
370
},
342
371
},
343
372
})
344
- return false
373
+ return time. Time {}, time. Time {}, false
345
374
}
375
+ * qp .dest = t
346
376
}
347
377
if endTime .Before (startTime ) {
348
378
httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
349
379
Message : "Query parameter has invalid value." ,
350
380
Validations : []codersdk.ValidationError {
351
381
{
352
382
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") ,
354
384
},
355
385
},
356
386
})
357
- return false
387
+ return time. Time {}, time. Time {}, false
358
388
}
359
389
360
- return true
390
+ return startTime , endTime , true
361
391
}
362
392
363
393
func verifyInsightsInterval (ctx context.Context , rw http.ResponseWriter , intervalString string ) (codersdk.InsightsReportInterval , bool ) {
0 commit comments