Expand Up @@ -549,6 +549,19 @@ func (q *FakeQuerier) getWorkspaceAgentsByResourceIDsNoLock(_ context.Context, r return workspaceAgents, nil } func (q *FakeQuerier) getWorkspaceAppByAgentIDAndSlugNoLock(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) { for _, app := range q.workspaceApps { if app.AgentID != arg.AgentID { continue } if app.Slug != arg.Slug { continue } return app, nil } return database.WorkspaceApp{}, sql.ErrNoRows } func (q *FakeQuerier) getProvisionerJobByIDNoLock(_ context.Context, id uuid.UUID) (database.ProvisionerJob, error) { for _, provisionerJob := range q.provisionerJobs { if provisionerJob.ID != id { Expand Down Expand Up @@ -1966,6 +1979,125 @@ func (*FakeQuerier) GetTailnetClientsForAgent(context.Context, uuid.UUID) ([]dat return nil, ErrUnimplemented } func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err } q.mutex.RLock() defer q.mutex.RUnlock() type appKey struct { AccessMethod string SlugOrPort string Slug string DisplayName string Icon string } type uniqueKey struct { TemplateID uuid.UUID UserID uuid.UUID AgentID uuid.UUID AppKey appKey } appUsageIntervalsByUserAgentApp := make(map[uniqueKey]map[time.Time]int64) for _, s := range q.workspaceAppStats { // (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)))) { continue } w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID) if err != nil { return nil, err } app, _ := q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, database.GetWorkspaceAppByAgentIDAndSlugParams{ AgentID: s.AgentID, Slug: s.SlugOrPort, }) key := uniqueKey{ TemplateID: w.TemplateID, UserID: s.UserID, AgentID: s.AgentID, AppKey: appKey{ AccessMethod: s.AccessMethod, SlugOrPort: s.SlugOrPort, Slug: app.Slug, DisplayName: app.DisplayName, Icon: app.Icon, }, } if appUsageIntervalsByUserAgentApp[key] == nil { appUsageIntervalsByUserAgentApp[key] = make(map[time.Time]int64) } t := s.SessionStartedAt.Truncate(5 * time.Minute) if t.Before(arg.StartTime) { t = arg.StartTime } for t.Before(s.SessionEndedAt) && t.Before(arg.EndTime) { appUsageIntervalsByUserAgentApp[key][t] = 300 // 5 minutes. t = t.Add(5 * time.Minute) } } appUsageTemplateIDs := make(map[appKey]map[uuid.UUID]struct{}) appUsageUserIDs := make(map[appKey]map[uuid.UUID]struct{}) appUsage := make(map[appKey]int64) for uniqueKey, usage := range appUsageIntervalsByUserAgentApp { for _, seconds := range usage { if appUsageTemplateIDs[uniqueKey.AppKey] == nil { appUsageTemplateIDs[uniqueKey.AppKey] = make(map[uuid.UUID]struct{}) } appUsageTemplateIDs[uniqueKey.AppKey][uniqueKey.TemplateID] = struct{}{} if appUsageUserIDs[uniqueKey.AppKey] == nil { appUsageUserIDs[uniqueKey.AppKey] = make(map[uuid.UUID]struct{}) } appUsageUserIDs[uniqueKey.AppKey][uniqueKey.UserID] = struct{}{} appUsage[uniqueKey.AppKey] += seconds } } var rows []database.GetTemplateAppInsightsRow for appKey, usage := range appUsage { templateIDs := make([]uuid.UUID, 0, len(appUsageTemplateIDs[appKey])) for templateID := range appUsageTemplateIDs[appKey] { templateIDs = append(templateIDs, templateID) } slices.SortFunc(templateIDs, func(a, b uuid.UUID) int { return slice.Ascending(a.String(), b.String()) }) activeUserIDs := make([]uuid.UUID, 0, len(appUsageUserIDs[appKey])) for userID := range appUsageUserIDs[appKey] { activeUserIDs = append(activeUserIDs, userID) } slices.SortFunc(activeUserIDs, func(a, b uuid.UUID) int { return slice.Ascending(a.String(), b.String()) }) rows = append(rows, database.GetTemplateAppInsightsRow{ TemplateIDs: templateIDs, ActiveUserIDs: activeUserIDs, AccessMethod: appKey.AccessMethod, SlugOrPort: appKey.SlugOrPort, DisplayName: sql.NullString{String: appKey.DisplayName, Valid: appKey.DisplayName != ""}, Icon: sql.NullString{String: appKey.Icon, Valid: appKey.Icon != ""}, IsApp: appKey.Slug != "", UsageSeconds: usage, }) } return rows, nil } func (q *FakeQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg database.GetTemplateAverageBuildTimeParams) (database.GetTemplateAverageBuildTimeRow, error) { if err := validateDatabaseType(arg); err != nil { return database.GetTemplateAverageBuildTimeRow{}, err Expand Down Expand Up @@ -2093,12 +2225,15 @@ func (q *FakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplat return rs, nil } func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) { func (q *FakeQuerier) GetTemplateDailyInsights(ctx context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err } q.mutex.RLock() defer q.mutex.RUnlock() type dailyStat struct { startTime, endTime time.Time userSet map[uuid.UUID]struct{} Expand Down Expand Up @@ -2133,6 +2268,37 @@ func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.G } } for _, s := range q.workspaceAppStats { // (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)))) { 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)))) { 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 } } var result []database.GetTemplateDailyInsightsRow for _, ds := range dailyStats { templateIDs := make([]uuid.UUID, 0, len(ds.templateIDSet)) Expand Down Expand Up @@ -2201,9 +2367,14 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem slices.SortFunc(templateIDs, func(a, b uuid.UUID) int { return slice.Ascending(a.String(), b.String()) }) activeUserIDs := make([]uuid.UUID, 0, len(appUsageIntervalsByUser)) for userID := range appUsageIntervalsByUser { activeUserIDs = append(activeUserIDs, userID) } result := database.GetTemplateInsightsRow{ TemplateIDs: templateIDs, ActiveUsers: int64(len(appUsageIntervalsByUser)) ,TemplateIDs: templateIDs, ActiveUserIDs: activeUserIDs ,} for _, intervals := range appUsageIntervalsByUser { for _, interval := range intervals { Expand Down Expand Up @@ -3075,24 +3246,15 @@ func (q *FakeQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.C return agents, nil } func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(_ context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) { func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg database.GetWorkspaceAppByAgentIDAndSlugParams) (database.WorkspaceApp, error) { if err := validateDatabaseType(arg); err != nil { return database.WorkspaceApp{}, err } q.mutex.RLock() defer q.mutex.RUnlock() for _, app := range q.workspaceApps { if app.AgentID != arg.AgentID { continue } if app.Slug != arg.Slug { continue } return app, nil } return database.WorkspaceApp{}, sql.ErrNoRows return q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, arg) } func (q *FakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) { Expand Down