@@ -17,7 +17,9 @@ import (
17
17
"cdr.dev/slog/sloggers/slogtest"
18
18
"github.com/coder/coder/agent"
19
19
"github.com/coder/coder/coderd/coderdtest"
20
+ "github.com/coder/coder/coderd/database/dbauthz"
20
21
"github.com/coder/coder/coderd/rbac"
22
+ "github.com/coder/coder/coderd/workspaceapps"
21
23
"github.com/coder/coder/codersdk"
22
24
"github.com/coder/coder/codersdk/agentsdk"
23
25
"github.com/coder/coder/provisioner/echo"
@@ -257,14 +259,19 @@ func TestTemplateInsights(t *testing.T) {
257
259
thirdParameterOptionValue2 = "bbb"
258
260
thirdParameterOptionName3 = "This is CCC"
259
261
thirdParameterOptionValue3 = "ccc"
262
+
263
+ testAppSlug = "test-app"
264
+ testAppName = "Test App"
265
+ testAppIcon = "/icon.png"
266
+ testAppURL = "http://127.1.0.1:65536" // Not used.
260
267
)
261
268
262
269
logger := slogtest .Make (t ,nil ).Leveled (slog .LevelDebug )
263
270
opts := & coderdtest.Options {
264
271
IncludeProvisionerDaemon :true ,
265
272
AgentStatsRefreshInterval :time .Millisecond * 100 ,
266
273
}
267
- client := coderdtest .New (t ,opts )
274
+ client , _ , coderdAPI := coderdtest .NewWithAPI (t ,opts )
268
275
269
276
user := coderdtest .CreateFirstUser (t ,client )
270
277
authToken := uuid .NewString ()
@@ -287,7 +294,32 @@ func TestTemplateInsights(t *testing.T) {
287
294
},
288
295
},
289
296
},
290
- ProvisionApply :echo .ProvisionApplyWithAgent (authToken ),
297
+ ProvisionApply : []* proto.Provision_Response {{
298
+ Type :& proto.Provision_Response_Complete {
299
+ Complete :& proto.Provision_Complete {
300
+ Resources : []* proto.Resource {{
301
+ Name :"example" ,
302
+ Type :"aws_instance" ,
303
+ Agents : []* proto.Agent {{
304
+ Id :uuid .NewString (),
305
+ Name :"dev" ,
306
+ Auth :& proto.Agent_Token {
307
+ Token :authToken ,
308
+ },
309
+ Apps : []* proto.App {
310
+ {
311
+ Slug :testAppSlug ,
312
+ DisplayName :testAppName ,
313
+ Icon :testAppIcon ,
314
+ SharingLevel :proto .AppSharingLevel_OWNER ,
315
+ Url :testAppURL ,
316
+ },
317
+ },
318
+ }},
319
+ }},
320
+ },
321
+ },
322
+ }},
291
323
})
292
324
template := coderdtest .CreateTemplate (t ,client ,user .OrganizationID ,version .ID )
293
325
require .Empty (t ,template .BuildTimeStats [codersdk .WorkspaceTransitionStart ])
@@ -320,10 +352,70 @@ func TestTemplateInsights(t *testing.T) {
320
352
// the day changes so that we get the relevant stats faster.
321
353
y ,m ,d := time .Now ().UTC ().Date ()
322
354
today := time .Date (y ,m ,d ,0 ,0 ,0 ,0 ,time .UTC )
355
+ requestStartTime := today
356
+ requestEndTime := time .Now ().UTC ().Truncate (time .Hour ).Add (time .Hour )
323
357
324
358
ctx ,cancel := context .WithTimeout (context .Background (),testutil .WaitLong )
325
359
defer cancel ()
326
360
361
+ // TODO(mafredri): We should prefer to set up an app and generate
362
+ // data by accessing it.
363
+ // Insert entries within and outside timeframe.
364
+ reporter := workspaceapps .NewStatsDBReporter (coderdAPI .Database ,workspaceapps .DefaultStatsDBReporterBatchSize )
365
+ err := reporter .Report (dbauthz .AsSystemRestricted (ctx ), []workspaceapps.StatsReport {
366
+ {
367
+ UserID :user .UserID ,
368
+ WorkspaceID :workspace .ID ,
369
+ AgentID :resources [0 ].Agents [0 ].ID ,
370
+ AccessMethod :workspaceapps .AccessMethodPath ,
371
+ SlugOrPort :testAppSlug ,
372
+ SessionID :uuid .New (),
373
+ // Outside report range.
374
+ SessionStartedAt :requestStartTime .Add (- 1 * time .Minute ),
375
+ SessionEndedAt :requestStartTime ,
376
+ Requests :1 ,
377
+ },
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
+ // One minute of usage (rounded up to 5 due to query intervals).
386
+ // TODO(mafredri): We'll fix this in a future refactor so that it's
387
+ // 1 minute increments instead of 5.
388
+ SessionStartedAt :requestStartTime ,
389
+ SessionEndedAt :requestStartTime .Add (1 * time .Minute ),
390
+ Requests :1 ,
391
+ },
392
+ {
393
+ UserID :user .UserID ,
394
+ WorkspaceID :workspace .ID ,
395
+ AgentID :resources [0 ].Agents [0 ].ID ,
396
+ AccessMethod :workspaceapps .AccessMethodPath ,
397
+ SlugOrPort :testAppSlug ,
398
+ SessionID :uuid .New (),
399
+ // Five additional minutes of usage.
400
+ SessionStartedAt :requestStartTime .Add (10 * time .Minute ),
401
+ SessionEndedAt :requestStartTime .Add (15 * time .Minute ),
402
+ Requests :1 ,
403
+ },
404
+ {
405
+ UserID :user .UserID ,
406
+ WorkspaceID :workspace .ID ,
407
+ AgentID :resources [0 ].Agents [0 ].ID ,
408
+ AccessMethod :workspaceapps .AccessMethodPath ,
409
+ SlugOrPort :testAppSlug ,
410
+ SessionID :uuid .New (),
411
+ // Outside report range.
412
+ SessionStartedAt :requestEndTime ,
413
+ SessionEndedAt :requestEndTime .Add (1 * time .Minute ),
414
+ Requests :1 ,
415
+ },
416
+ })
417
+ require .NoError (t ,err ,"want no error inserting stats" )
418
+
327
419
// Connect to the agent to generate usage/latency stats.
328
420
conn ,err := client .DialWorkspaceAgent (ctx ,resources [0 ].Agents [0 ].ID ,& codersdk.DialWorkspaceAgentOptions {
329
421
Logger :logger .Named ("client" ),
@@ -362,8 +454,8 @@ func TestTemplateInsights(t *testing.T) {
362
454
waitForAppSeconds := func (slug string )func ()bool {
363
455
return func ()bool {
364
456
req = codersdk.TemplateInsightsRequest {
365
- StartTime :today ,
366
- EndTime :time . Now (). UTC (). Truncate ( time . Hour ). Add ( time . Hour ) ,
457
+ StartTime :requestStartTime ,
458
+ EndTime :requestEndTime ,
367
459
Interval :codersdk .InsightsReportIntervalDay ,
368
460
}
369
461
resp ,err = client .TemplateInsights (ctx ,req )
@@ -390,13 +482,31 @@ func TestTemplateInsights(t *testing.T) {
390
482
assert .WithinDuration (t ,req .StartTime ,resp .Report .StartTime ,0 )
391
483
assert .WithinDuration (t ,req .EndTime ,resp .Report .EndTime ,0 )
392
484
assert .Equal (t ,resp .Report .ActiveUsers ,int64 (1 ),"want one active user" )
485
+ var gotApps []codersdk.TemplateAppUsage
486
+ // Check builtin apps usage.
393
487
for _ ,app := range resp .Report .AppsUsage {
488
+ if app .Type != codersdk .TemplateAppsTypeBuiltin {
489
+ gotApps = append (gotApps ,app )
490
+ continue
491
+ }
394
492
if slices .Contains ([]string {"reconnecting-pty" ,"ssh" },app .Slug ) {
395
493
assert .Equal (t ,app .Seconds ,int64 (300 ),"want app %q to have 5 minutes of usage" ,app .Slug )
396
494
}else {
397
495
assert .Equal (t ,app .Seconds ,int64 (0 ),"want app %q to have 0 minutes of usage" ,app .Slug )
398
496
}
399
497
}
498
+ // Check app usage.
499
+ assert .Equal (t ,gotApps , []codersdk.TemplateAppUsage {
500
+ {
501
+ TemplateIDs : []uuid.UUID {template .ID },
502
+ Type :codersdk .TemplateAppsTypeApp ,
503
+ Slug :testAppSlug ,
504
+ DisplayName :testAppName ,
505
+ Icon :testAppIcon ,
506
+ Seconds :300 + 300 ,// Two times 5 minutes of usage (actually 1 + 5, but see TODO above).
507
+ },
508
+ },"want app usage to match" )
509
+
400
510
// The full timeframe is <= 24h, so the interval matches exactly.
401
511
require .Len (t ,resp .IntervalReports ,1 ,"want one interval report" )
402
512
assert .WithinDuration (t ,req .StartTime ,resp .IntervalReports [0 ].StartTime ,0 )