Skip to content

fix(coderd): use stable sorting for insights and improve test coverage #9250

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 33 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ffc7391
test(coderd): add extended template insights test suite
mafredri Aug 22, 2023
95ad848
add initial golden files
mafredri Aug 22, 2023
d4d58cf
add some more usage and verify golden
mafredri Aug 22, 2023
e4f901a
fixes
mafredri Aug 22, 2023
f333170
add an hour of app usage
mafredri Aug 22, 2023
6fb49a9
add 15 min of app usage -> active user on last day
mafredri Aug 22, 2023
e9a967e
add placeholder data
mafredri Aug 22, 2023
9879462
add identical usage in two templates for one user
mafredri Aug 22, 2023
fc1a98d
test app merging
mafredri Aug 22, 2023
f14ddf6
fix(codersdk): use url values for query params
mafredri Aug 22, 2023
8a60568
improve stability of template id
mafredri Aug 22, 2023
7d93d13
add sao paulo tz test (invalid interval times)
mafredri Aug 22, 2023
f9fdb47
fix start/end time tz returned in interval report
mafredri Aug 22, 2023
54638b0
cleanup
mafredri Aug 22, 2023
7b0ac4c
refactor template creation with stable IDs
mafredri Aug 23, 2023
1e7d860
add parameter test
mafredri Aug 23, 2023
9d96052
refactor
mafredri Aug 23, 2023
930d7ef
add parameter no data test
mafredri Aug 23, 2023
07cdc0b
add 1h usage to other user
mafredri Aug 23, 2023
73367d1
add usage outside of utc timeframe
mafredri Aug 23, 2023
c2a0496
add third user with ssh and web term usage
mafredri Aug 23, 2023
b738799
add third user app usage
mafredri Aug 23, 2023
79bdbf9
add webterminal app usage
mafredri Aug 23, 2023
ce2bb78
use stable sorting in Go and minor query tweaks and fakedb fixes
mafredri Aug 23, 2023
0896439
use github.com/google/go-cmp/cmp for diffs
mafredri Aug 23, 2023
5b4733a
restructure
mafredri Aug 23, 2023
3607a80
more fakedb fixes
mafredri Aug 23, 2023
688e1d0
fix fakedb logic bugs
mafredri Aug 23, 2023
8c294d0
fix commented code
mafredri Aug 23, 2023
decbdfe
fix test bug
mafredri Aug 24, 2023
870aff1
add test for tpl1 param4 option2
mafredri Aug 24, 2023
75a96d7
add outside timeframe tests for apps
mafredri Aug 24, 2023
e72e265
remove previous insights test in favor of new one
mafredri Aug 24, 2023
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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
./scripts/apidocgen/generate.sh
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json

update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden
update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden coderd/.gen-golden
.PHONY: update-golden-files

cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
Expand All @@ -583,6 +583,10 @@ helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/t
go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update
touch "$@"

coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/*_test.go)
go test ./coderd -run="Test.*Golden$$" -update
touch "$@"

scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go)
go test ./scripts/ci-report -run=TestOutputMatchesGoldenFile -update
touch "$@"
Expand Down
4 changes: 4 additions & 0 deletions coderd/coderd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coderd_test

import (
"context"
"flag"
"io"
"net/http"
"net/netip"
Expand All @@ -23,6 +24,9 @@ import (
"github.com/coder/coder/v2/testutil"
)

// updateGoldenFiles is a flag that can be set to update golden files.
var updateGoldenFiles = flag.Bool("update", false, "Update golden files")

func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
Expand Down
48 changes: 35 additions & 13 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package db2sdk

import (
"encoding/json"
"sort"
"strings"

"github.com/google/uuid"
"golang.org/x/exp/slices"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/parameter"
Expand Down Expand Up @@ -125,9 +126,34 @@ func Role(role rbac.Role) codersdk.Role {
}

func TemplateInsightsParameters(parameterRows []database.GetTemplateParameterInsightsRow) ([]codersdk.TemplateParameterUsage, error) {
parametersByNum := make(map[int64]*codersdk.TemplateParameterUsage)
// Use a stable sort, similarly to how we would sort in the query, note that
// we don't sort in the query because order varies depending on the table
// collation.
//
// ORDER BY utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
slices.SortFunc(parameterRows, func(a, b database.GetTemplateParameterInsightsRow) int {
if a.Name != b.Name {
return strings.Compare(a.Name, b.Name)
}
if a.Type != b.Type {
return strings.Compare(a.Type, b.Type)
}
if a.DisplayName != b.DisplayName {
return strings.Compare(a.DisplayName, b.DisplayName)
}
if a.Description != b.Description {
return strings.Compare(a.Description, b.Description)
}
if string(a.Options) != string(b.Options) {
return strings.Compare(string(a.Options), string(b.Options))
}
return strings.Compare(a.Value, b.Value)
})

parametersUsage := []codersdk.TemplateParameterUsage{}
indexByNum := make(map[int64]int)
for _, param := range parameterRows {
if _, ok := parametersByNum[param.Num]; !ok {
if _, ok := indexByNum[param.Num]; !ok {
var opts []codersdk.TemplateVersionParameterOption
err := json.Unmarshal(param.Options, &opts)
if err != nil {
Expand All @@ -139,28 +165,24 @@ func TemplateInsightsParameters(parameterRows []database.GetTemplateParameterIns
return nil, err
}

parametersByNum[param.Num] = &codersdk.TemplateParameterUsage{
parametersUsage = append(parametersUsage, codersdk.TemplateParameterUsage{
TemplateIDs: param.TemplateIDs,
Name: param.Name,
Type: param.Type,
DisplayName: param.DisplayName,
Description: plaintextDescription,
Options: opts,
}
})
indexByNum[param.Num] = len(parametersUsage) - 1
}
parametersByNum[param.Num].Values = append(parametersByNum[param.Num].Values, codersdk.TemplateParameterValue{

i := indexByNum[param.Num]
parametersUsage[i].Values = append(parametersUsage[i].Values, codersdk.TemplateParameterValue{
Value: param.Value,
Count: param.Count,
})
}
parametersUsage := []codersdk.TemplateParameterUsage{}
for _, param := range parametersByNum {
parametersUsage = append(parametersUsage, *param)
}

sort.Slice(parametersUsage, func(i, j int) bool {
return parametersUsage[i].Name < parametersUsage[j].Name
})
return parametersUsage, nil
}

Expand Down
33 changes: 22 additions & 11 deletions coderd/database/dbfake/dbfake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2018,6 +2018,10 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
return nil, err
}

if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) {
continue
}

app, _ := q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, database.GetWorkspaceAppByAgentIDAndSlugParams{
AgentID: s.AgentID,
Slug: s.SlugOrPort,
Expand Down Expand Up @@ -2095,6 +2099,8 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
})
}

// NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
// ORDER BY access_method, slug_or_port, display_name, icon, is_app
return rows, nil
}

Expand Down Expand Up @@ -2264,7 +2270,6 @@ func (q *FakeQuerier) GetTemplateDailyInsights(ctx context.Context, arg database
}
ds.userSet[s.UserID] = struct{}{}
ds.templateIDSet[s.TemplateID] = struct{}{}
break
}
}

Expand All @@ -2278,24 +2283,27 @@ func (q *FakeQuerier) GetTemplateDailyInsights(ctx context.Context, arg database
continue
}

w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID)
if err != nil {
return nil, err
}

if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) {
continue
}

for _, ds := range dailyStats {
// (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
// OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
// OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
if !(((s.SessionStartedAt.After(arg.StartTime) || s.SessionStartedAt.Equal(arg.StartTime)) && s.SessionStartedAt.Before(arg.EndTime)) ||
(s.SessionEndedAt.After(arg.StartTime) && s.SessionEndedAt.Before(arg.EndTime)) ||
(s.SessionStartedAt.Before(arg.StartTime) && (s.SessionEndedAt.After(arg.EndTime) || s.SessionEndedAt.Equal(arg.EndTime)))) {
if !(((s.SessionStartedAt.After(ds.startTime) || s.SessionStartedAt.Equal(ds.startTime)) && s.SessionStartedAt.Before(ds.endTime)) ||
(s.SessionEndedAt.After(ds.startTime) && s.SessionEndedAt.Before(ds.endTime)) ||
(s.SessionStartedAt.Before(ds.startTime) && (s.SessionEndedAt.After(ds.endTime) || s.SessionEndedAt.Equal(ds.endTime)))) {
continue
}

w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID)
if err != nil {
return nil, err
}

ds.userSet[s.UserID] = struct{}{}
ds.templateIDSet[w.TemplateID] = struct{}{}
break
}
}

Expand Down Expand Up @@ -2430,7 +2438,8 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
if tvp.TemplateVersionID != tv.ID {
continue
}
key := fmt.Sprintf("%s:%s:%s:%s", tvp.Name, tvp.DisplayName, tvp.Description, tvp.Options)
// GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
key := fmt.Sprintf("%s:%s:%s:%s:%s", tvp.Name, tvp.Type, tvp.DisplayName, tvp.Description, tvp.Options)
if _, ok := uniqueTemplateParams[key]; !ok {
num++
uniqueTemplateParams[key] = &database.GetTemplateParameterInsightsRow{
Expand Down Expand Up @@ -2480,6 +2489,8 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
}
}

// NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
Copy link
Member

Choose a reason for hiding this comment

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

out of curiosity: what is the default PostgreSQL behavior considering the "most default" configuration?

Copy link
Member Author

Choose a reason for hiding this comment

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

It will depend on the systems locale. Essentially if e.g. if LC_ALL=C or LC_ALL=en_US.UTF-8 is set when calling initdb, it will produce different results.

// ORDER BY utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value
return rows, nil
}

Expand Down
8 changes: 4 additions & 4 deletions coderd/database/queries.sql.go

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

8 changes: 4 additions & 4 deletions coderd/database/queries/insights.sql
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,13 @@ WITH latest_workspace_builds AS (
array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
array_agg(wb.id)::uuid[] AS workspace_build_ids,
tvp.name,
tvp.type,
tvp.display_name,
tvp.description,
tvp.options,
tvp.type
tvp.options
FROM latest_workspace_builds wb
JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options, tvp.type
GROUP BY tvp.name, tvp.type, tvp.display_name, tvp.description, tvp.options
)

SELECT
Expand All @@ -251,4 +251,4 @@ SELECT
COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, utp.type, wbp.value;
GROUP BY utp.num, utp.template_ids, utp.name, utp.type, utp.display_name, utp.description, utp.options, wbp.value;
33 changes: 31 additions & 2 deletions coderd/insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"strings"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -288,8 +289,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
}
for _, row := range dailyUsage {
resp.IntervalReports = append(resp.IntervalReports, codersdk.TemplateInsightsIntervalReport{
StartTime: row.StartTime,
EndTime: row.EndTime,
// NOTE(mafredri): This might not be accurate over DST since the
// parsed location only contains the offset.
StartTime: row.StartTime.In(startTime.Location()),
EndTime: row.EndTime.In(startTime.Location()),
Interval: interval,
TemplateIDs: row.TemplateIDs,
ActiveUsers: row.ActiveUsers,
Expand Down Expand Up @@ -377,6 +380,32 @@ func convertTemplateInsightsApps(usage database.GetTemplateInsightsRow, appUsage
},
}

// Use a stable sort, similarly to how we would sort in the query, note that
// we don't sort in the query because order varies depending on the table
// collation.
//
// ORDER BY access_method, slug_or_port, display_name, icon, is_app
slices.SortFunc(appUsage, func(a, b database.GetTemplateAppInsightsRow) int {
if a.AccessMethod != b.AccessMethod {
return strings.Compare(a.AccessMethod, b.AccessMethod)
}
if a.SlugOrPort != b.SlugOrPort {
return strings.Compare(a.SlugOrPort, b.SlugOrPort)
}
if a.DisplayName.String != b.DisplayName.String {
return strings.Compare(a.DisplayName.String, b.DisplayName.String)
}
if a.Icon.String != b.Icon.String {
return strings.Compare(a.Icon.String, b.Icon.String)
}
if !a.IsApp && b.IsApp {
return -1
} else if a.IsApp && !b.IsApp {
return 1
}
return 0
})

// Template apps.
for _, app := range appUsage {
if !app.IsApp {
Expand Down
Loading