@@ -16,7 +16,6 @@ import (
16
16
"github.com/google/uuid"
17
17
"github.com/stretchr/testify/assert"
18
18
"github.com/stretchr/testify/require"
19
- "golang.org/x/exp/slices"
20
19
21
20
"cdr.dev/slog"
22
21
"cdr.dev/slog/sloggers/slogtest"
@@ -241,345 +240,6 @@ func TestUserLatencyInsights_BadRequest(t *testing.T) {
241
240
assert .Error (t , err , "want error for end time partial day when not today" )
242
241
}
243
242
244
- func TestTemplateInsights (t * testing.T ) {
245
- t .Parallel ()
246
-
247
- const (
248
- firstParameterName = "first_parameter"
249
- firstParameterDisplayName = "First PARAMETER"
250
- firstParameterType = "string"
251
- firstParameterDescription = "This is first parameter"
252
- firstParameterValue = "abc"
253
-
254
- secondParameterName = "second_parameter"
255
- secondParameterDisplayName = "Second PARAMETER"
256
- secondParameterType = "number"
257
- secondParameterDescription = "This is second parameter"
258
- secondParameterValue = "123"
259
-
260
- thirdParameterName = "third_parameter"
261
- thirdParameterDisplayName = "Third PARAMETER"
262
- thirdParameterType = "string"
263
- thirdParameterDescription = "This is third parameter"
264
- thirdParameterValue = "bbb"
265
- thirdParameterOptionName1 = "This is AAA"
266
- thirdParameterOptionValue1 = "aaa"
267
- thirdParameterOptionName2 = "This is BBB"
268
- thirdParameterOptionValue2 = "bbb"
269
- thirdParameterOptionName3 = "This is CCC"
270
- thirdParameterOptionValue3 = "ccc"
271
-
272
- testAppSlug = "test-app"
273
- testAppName = "Test App"
274
- testAppIcon = "/icon.png"
275
- testAppURL = "http://127.1.0.1:65536" // Not used.
276
- )
277
-
278
- logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
279
- opts := & coderdtest.Options {
280
- Logger : & logger ,
281
- IncludeProvisionerDaemon : true ,
282
- AgentStatsRefreshInterval : time .Millisecond * 100 ,
283
- }
284
- client , _ , coderdAPI := coderdtest .NewWithAPI (t , opts )
285
-
286
- user := coderdtest .CreateFirstUser (t , client )
287
- _ , otherUser := coderdtest .CreateAnotherUser (t , client , user .OrganizationID )
288
- authToken := uuid .NewString ()
289
- version := coderdtest .CreateTemplateVersion (t , client , user .OrganizationID , & echo.Responses {
290
- Parse : echo .ParseComplete ,
291
- ProvisionPlan : []* proto.Provision_Response {
292
- {
293
- Type : & proto.Provision_Response_Complete {
294
- Complete : & proto.Provision_Complete {
295
- Parameters : []* proto.RichParameter {
296
- {Name : firstParameterName , DisplayName : firstParameterDisplayName , Type : firstParameterType , Description : firstParameterDescription , Required : true },
297
- {Name : secondParameterName , DisplayName : secondParameterDisplayName , Type : secondParameterType , Description : secondParameterDescription , Required : true },
298
- {Name : thirdParameterName , DisplayName : thirdParameterDisplayName , Type : thirdParameterType , Description : thirdParameterDescription , Required : true , Options : []* proto.RichParameterOption {
299
- {Name : thirdParameterOptionName1 , Value : thirdParameterOptionValue1 },
300
- {Name : thirdParameterOptionName2 , Value : thirdParameterOptionValue2 },
301
- {Name : thirdParameterOptionName3 , Value : thirdParameterOptionValue3 },
302
- }},
303
- },
304
- },
305
- },
306
- },
307
- },
308
- ProvisionApply : []* proto.Provision_Response {{
309
- Type : & proto.Provision_Response_Complete {
310
- Complete : & proto.Provision_Complete {
311
- Resources : []* proto.Resource {{
312
- Name : "example" ,
313
- Type : "aws_instance" ,
314
- Agents : []* proto.Agent {{
315
- Id : uuid .NewString (),
316
- Name : "dev" ,
317
- Auth : & proto.Agent_Token {
318
- Token : authToken ,
319
- },
320
- Apps : []* proto.App {
321
- {
322
- Slug : testAppSlug ,
323
- DisplayName : testAppName ,
324
- Icon : testAppIcon ,
325
- SharingLevel : proto .AppSharingLevel_OWNER ,
326
- Url : testAppURL ,
327
- },
328
- },
329
- }},
330
- }},
331
- },
332
- },
333
- }},
334
- })
335
- template := coderdtest .CreateTemplate (t , client , user .OrganizationID , version .ID )
336
- require .Empty (t , template .BuildTimeStats [codersdk .WorkspaceTransitionStart ])
337
- coderdtest .AwaitTemplateVersionJob (t , client , version .ID )
338
-
339
- buildParameters := []codersdk.WorkspaceBuildParameter {
340
- {Name : firstParameterName , Value : firstParameterValue },
341
- {Name : secondParameterName , Value : secondParameterValue },
342
- {Name : thirdParameterName , Value : thirdParameterValue },
343
- }
344
-
345
- workspace := coderdtest .CreateWorkspace (t , client , user .OrganizationID , template .ID , func (cwr * codersdk.CreateWorkspaceRequest ) {
346
- cwr .RichParameterValues = buildParameters
347
- })
348
- coderdtest .AwaitWorkspaceBuildJob (t , client , workspace .LatestBuild .ID )
349
-
350
- // Start an agent so that we can generate stats.
351
- agentClient := agentsdk .New (client .URL )
352
- agentClient .SetSessionToken (authToken )
353
- agentCloser := agent .New (agent.Options {
354
- Logger : logger .Named ("agent" ),
355
- Client : agentClient ,
356
- })
357
- defer func () {
358
- _ = agentCloser .Close ()
359
- }()
360
- resources := coderdtest .AwaitWorkspaceAgents (t , client , workspace .ID )
361
-
362
- // Start must be at the beginning of the day, initialize it early in case
363
- // the day changes so that we get the relevant stats faster.
364
- y , m , d := time .Now ().UTC ().Date ()
365
- today := time .Date (y , m , d , 0 , 0 , 0 , 0 , time .UTC )
366
- requestStartTime := today
367
- requestEndTime := time .Now ().UTC ().Truncate (time .Hour ).Add (time .Hour )
368
-
369
- ctx , cancel := context .WithTimeout (context .Background (), testutil .WaitLong )
370
- defer cancel ()
371
-
372
- // TODO(mafredri): We should prefer to set up an app and generate
373
- // data by accessing it.
374
- // Insert entries within and outside timeframe.
375
- reporter := workspaceapps .NewStatsDBReporter (coderdAPI .Database , workspaceapps .DefaultStatsDBReporterBatchSize )
376
- //nolint:gocritic // This is a test.
377
- err := reporter .Report (dbauthz .AsSystemRestricted (ctx ), []workspaceapps.StatsReport {
378
- {
379
- UserID : user .UserID ,
380
- WorkspaceID : workspace .ID ,
381
- AgentID : resources [0 ].Agents [0 ].ID ,
382
- AccessMethod : workspaceapps .AccessMethodPath ,
383
- SlugOrPort : testAppSlug ,
384
- SessionID : uuid .New (),
385
- // Outside report range.
386
- SessionStartedAt : requestStartTime .Add (- 1 * time .Minute ),
387
- SessionEndedAt : requestStartTime ,
388
- Requests : 1 ,
389
- },
390
- {
391
- UserID : user .UserID ,
392
- WorkspaceID : workspace .ID ,
393
- AgentID : resources [0 ].Agents [0 ].ID ,
394
- AccessMethod : workspaceapps .AccessMethodPath ,
395
- SlugOrPort : testAppSlug ,
396
- SessionID : uuid .New (),
397
- // One minute of usage (rounded up to 5 due to query intervals).
398
- // TODO(mafredri): We'll fix this in a future refactor so that it's
399
- // 1 minute increments instead of 5.
400
- SessionStartedAt : requestStartTime ,
401
- SessionEndedAt : requestStartTime .Add (1 * time .Minute ),
402
- Requests : 1 ,
403
- },
404
- {
405
- // Other use is using users workspace, this will result in an
406
- // additional active user and more time spent in app.
407
- UserID : otherUser .ID ,
408
- WorkspaceID : workspace .ID ,
409
- AgentID : resources [0 ].Agents [0 ].ID ,
410
- AccessMethod : workspaceapps .AccessMethodPath ,
411
- SlugOrPort : testAppSlug ,
412
- SessionID : uuid .New (),
413
- // One minute of usage (rounded up to 5 due to query intervals).
414
- SessionStartedAt : requestStartTime ,
415
- SessionEndedAt : requestStartTime .Add (1 * time .Minute ),
416
- Requests : 1 ,
417
- },
418
- {
419
- UserID : user .UserID ,
420
- WorkspaceID : workspace .ID ,
421
- AgentID : resources [0 ].Agents [0 ].ID ,
422
- AccessMethod : workspaceapps .AccessMethodPath ,
423
- SlugOrPort : testAppSlug ,
424
- SessionID : uuid .New (),
425
- // Five additional minutes of usage.
426
- SessionStartedAt : requestStartTime .Add (10 * time .Minute ),
427
- SessionEndedAt : requestStartTime .Add (15 * time .Minute ),
428
- Requests : 1 ,
429
- },
430
- {
431
- UserID : user .UserID ,
432
- WorkspaceID : workspace .ID ,
433
- AgentID : resources [0 ].Agents [0 ].ID ,
434
- AccessMethod : workspaceapps .AccessMethodPath ,
435
- SlugOrPort : testAppSlug ,
436
- SessionID : uuid .New (),
437
- // Outside report range.
438
- SessionStartedAt : requestEndTime ,
439
- SessionEndedAt : requestEndTime .Add (1 * time .Minute ),
440
- Requests : 1 ,
441
- },
442
- })
443
- require .NoError (t , err , "want no error inserting stats" )
444
-
445
- // Connect to the agent to generate usage/latency stats.
446
- conn , err := client .DialWorkspaceAgent (ctx , resources [0 ].Agents [0 ].ID , & codersdk.DialWorkspaceAgentOptions {
447
- Logger : logger .Named ("client" ),
448
- })
449
- require .NoError (t , err )
450
- defer conn .Close ()
451
-
452
- sshConn , err := conn .SSHClient (ctx )
453
- require .NoError (t , err )
454
- defer sshConn .Close ()
455
-
456
- // Start an SSH session to generate SSH usage stats.
457
- sess , err := sshConn .NewSession ()
458
- require .NoError (t , err )
459
- defer sess .Close ()
460
-
461
- r , w := io .Pipe ()
462
- defer r .Close ()
463
- defer w .Close ()
464
- sess .Stdin = r
465
- err = sess .Start ("cat" )
466
- require .NoError (t , err )
467
-
468
- // Start an rpty session to generate rpty usage stats.
469
- rpty , err := client .WorkspaceAgentReconnectingPTY (ctx , codersdk.WorkspaceAgentReconnectingPTYOpts {
470
- AgentID : resources [0 ].Agents [0 ].ID ,
471
- Reconnect : uuid .New (),
472
- Width : 80 ,
473
- Height : 24 ,
474
- })
475
- require .NoError (t , err )
476
- defer rpty .Close ()
477
-
478
- var resp codersdk.TemplateInsightsResponse
479
- var req codersdk.TemplateInsightsRequest
480
- waitForAppSeconds := func (slug string ) func () bool {
481
- return func () bool {
482
- req = codersdk.TemplateInsightsRequest {
483
- StartTime : requestStartTime ,
484
- EndTime : requestEndTime ,
485
- Interval : codersdk .InsightsReportIntervalDay ,
486
- }
487
- resp , err = client .TemplateInsights (ctx , req )
488
- if ! assert .NoError (t , err ) {
489
- return false
490
- }
491
-
492
- if slices .IndexFunc (resp .Report .AppsUsage , func (au codersdk.TemplateAppUsage ) bool {
493
- return au .Slug == slug && au .Seconds > 0
494
- }) != - 1 {
495
- return true
496
- }
497
- return false
498
- }
499
- }
500
- require .Eventually (t , waitForAppSeconds ("reconnecting-pty" ), testutil .WaitMedium , testutil .IntervalFast , "reconnecting-pty seconds missing" )
501
- require .Eventually (t , waitForAppSeconds ("ssh" ), testutil .WaitMedium , testutil .IntervalFast , "ssh seconds missing" )
502
-
503
- // We got our data, close down sessions and connections.
504
- _ = rpty .Close ()
505
- _ = sess .Close ()
506
- _ = sshConn .Close ()
507
-
508
- assert .WithinDuration (t , req .StartTime , resp .Report .StartTime , 0 )
509
- assert .WithinDuration (t , req .EndTime , resp .Report .EndTime , 0 )
510
- assert .Equal (t , int64 (2 ), resp .Report .ActiveUsers , "want two active users" )
511
- var gotApps []codersdk.TemplateAppUsage
512
- // Check builtin apps usage.
513
- for _ , app := range resp .Report .AppsUsage {
514
- if app .Type != codersdk .TemplateAppsTypeBuiltin {
515
- gotApps = append (gotApps , app )
516
- continue
517
- }
518
- if slices .Contains ([]string {"reconnecting-pty" , "ssh" }, app .Slug ) {
519
- assert .Equal (t , app .Seconds , int64 (300 ), "want app %q to have 5 minutes of usage" , app .Slug )
520
- } else {
521
- assert .Equal (t , app .Seconds , int64 (0 ), "want app %q to have 0 minutes of usage" , app .Slug )
522
- }
523
- }
524
- // Check app usage.
525
- assert .Len (t , gotApps , 1 , "want one app" )
526
- assert .Equal (t , []codersdk.TemplateAppUsage {
527
- {
528
- TemplateIDs : []uuid.UUID {template .ID },
529
- Type : codersdk .TemplateAppsTypeApp ,
530
- Slug : testAppSlug ,
531
- DisplayName : testAppName ,
532
- Icon : testAppIcon ,
533
- Seconds : 300 + 300 + 300 , // Three times 5 minutes of usage (actually 1 + 1 + 5, but see TODO above).
534
- },
535
- }, gotApps , "want app usage to match" )
536
-
537
- // The full timeframe is <= 24h, so the interval matches exactly.
538
- require .Len (t , resp .IntervalReports , 1 , "want one interval report" )
539
- assert .WithinDuration (t , req .StartTime , resp .IntervalReports [0 ].StartTime , 0 )
540
- assert .WithinDuration (t , req .EndTime , resp .IntervalReports [0 ].EndTime , 0 )
541
- assert .Equal (t , int64 (2 ), resp .IntervalReports [0 ].ActiveUsers , "want two active users in the interval report" )
542
-
543
- // The workspace uses 3 parameters
544
- require .Len (t , resp .Report .ParametersUsage , 3 )
545
- assert .Equal (t , firstParameterName , resp .Report .ParametersUsage [0 ].Name )
546
- assert .Equal (t , firstParameterType , resp .Report .ParametersUsage [0 ].Type )
547
- assert .Equal (t , firstParameterDescription , resp .Report .ParametersUsage [0 ].Description )
548
- assert .Equal (t , firstParameterDisplayName , resp .Report .ParametersUsage [0 ].DisplayName )
549
- assert .Contains (t , resp .Report .ParametersUsage [0 ].Values , codersdk.TemplateParameterValue {
550
- Value : firstParameterValue ,
551
- Count : 1 ,
552
- })
553
- assert .Contains (t , resp .Report .ParametersUsage [0 ].TemplateIDs , template .ID )
554
- assert .Empty (t , resp .Report .ParametersUsage [0 ].Options )
555
-
556
- assert .Equal (t , secondParameterName , resp .Report .ParametersUsage [1 ].Name )
557
- assert .Equal (t , secondParameterType , resp .Report .ParametersUsage [1 ].Type )
558
- assert .Equal (t , secondParameterDescription , resp .Report .ParametersUsage [1 ].Description )
559
- assert .Equal (t , secondParameterDisplayName , resp .Report .ParametersUsage [1 ].DisplayName )
560
- assert .Contains (t , resp .Report .ParametersUsage [1 ].Values , codersdk.TemplateParameterValue {
561
- Value : secondParameterValue ,
562
- Count : 1 ,
563
- })
564
- assert .Contains (t , resp .Report .ParametersUsage [1 ].TemplateIDs , template .ID )
565
- assert .Empty (t , resp .Report .ParametersUsage [1 ].Options )
566
-
567
- assert .Equal (t , thirdParameterName , resp .Report .ParametersUsage [2 ].Name )
568
- assert .Equal (t , thirdParameterType , resp .Report .ParametersUsage [2 ].Type )
569
- assert .Equal (t , thirdParameterDescription , resp .Report .ParametersUsage [2 ].Description )
570
- assert .Equal (t , thirdParameterDisplayName , resp .Report .ParametersUsage [2 ].DisplayName )
571
- assert .Contains (t , resp .Report .ParametersUsage [2 ].Values , codersdk.TemplateParameterValue {
572
- Value : thirdParameterValue ,
573
- Count : 1 ,
574
- })
575
- assert .Contains (t , resp .Report .ParametersUsage [2 ].TemplateIDs , template .ID )
576
- assert .Equal (t , []codersdk.TemplateVersionParameterOption {
577
- {Name : thirdParameterOptionName1 , Value : thirdParameterOptionValue1 },
578
- {Name : thirdParameterOptionName2 , Value : thirdParameterOptionValue2 },
579
- {Name : thirdParameterOptionName3 , Value : thirdParameterOptionValue3 },
580
- }, resp .Report .ParametersUsage [2 ].Options )
581
- }
582
-
583
243
func TestTemplateInsights_Golden (t * testing.T ) {
584
244
t .Parallel ()
585
245
0 commit comments