Skip to content

Commit

Permalink
feat(frontend): replace exception detail journey with attr distributi…
Browse files Browse the repository at this point in the history
…on plot

closes #1300
  • Loading branch information
anupcowkur committed Nov 1, 2024
1 parent cab2ce9 commit 1ddbab8
Show file tree
Hide file tree
Showing 8 changed files with 538 additions and 8 deletions.
2 changes: 2 additions & 0 deletions backend/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@ func main() {
apps.GET(":id/crashGroups/plots/instances", measure.GetCrashOverviewPlotInstances)
apps.GET(":id/crashGroups/:crashGroupId/crashes", measure.GetCrashDetailCrashes)
apps.GET(":id/crashGroups/:crashGroupId/plots/instances", measure.GetCrashDetailPlotInstances)
apps.GET(":id/crashGroups/:crashGroupId/plots/distribution", measure.GetCrashDetailAttributeDistribution)
apps.GET(":id/crashGroups/:crashGroupId/plots/journey", measure.GetCrashDetailPlotJourney)
apps.GET(":id/anrGroups", measure.GetANROverview)
apps.GET(":id/anrGroups/plots/instances", measure.GetANROverviewPlotInstances)
apps.GET(":id/anrGroups/:anrGroupId/anrs", measure.GetANRDetailANRs)
apps.GET(":id/anrGroups/:anrGroupId/plots/instances", measure.GetANRDetailPlotInstances)
apps.GET(":id/anrGroups/:anrGroupId/plots/distribution", measure.GetANRDetailAttributeDistribution)
apps.GET(":id/anrGroups/:anrGroupId/plots/journey", measure.GetANRDetailPlotJourney)
apps.GET(":id/sessions", measure.GetSessionsOverview)
apps.GET(":id/sessions/:sessionId", measure.GetSession)
Expand Down
226 changes: 226 additions & 0 deletions backend/api/measure/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2868,6 +2868,122 @@ func GetCrashDetailPlotInstances(c *gin.Context) {
c.JSON(http.StatusOK, instances)
}

func GetCrashDetailAttributeDistribution(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
msg := `id invalid or missing`
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

crashGroupId, err := uuid.Parse(c.Param("crashGroupId"))
if err != nil {
msg := `crash group id is invalid or missing`
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

af := filter.AppFilter{
AppID: id,
Limit: filter.DefaultPaginationLimit,
}

if err := c.ShouldBindQuery(&af); err != nil {
msg := `failed to parse query parameters`
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{
"error": msg,
"details": err.Error(),
})
return
}

af.Expand()

msg := "app filters request validation failed"
if err := af.Validate(); err != nil {
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{
"error": msg,
"details": err.Error(),
})
return
}

if len(af.Versions) > 0 || len(af.VersionCodes) > 0 {
if err := af.ValidateVersions(); err != nil {
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{
"error": msg,
"details": err.Error(),
})
return
}
}

app := App{
ID: &id,
}
team, err := app.getTeam(ctx)
if err != nil {
msg := "failed to get team from app id"
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}
if team == nil {
msg := fmt.Sprintf("no team exists for app [%s]", app.ID)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

userId := c.GetString("userId")
okTeam, err := PerformAuthz(userId, team.ID.String(), *ScopeTeamRead)
if err != nil {
msg := `failed to perform authorization`
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

okApp, err := PerformAuthz(userId, team.ID.String(), *ScopeAppRead)
if err != nil {
msg := `failed to perform authorization`
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

if !okTeam || !okApp {
msg := `you are not authorized to access this app`
c.JSON(http.StatusForbidden, gin.H{"error": msg})
return
}

group, err := app.GetExceptionGroup(ctx, crashGroupId)
if err != nil {
msg := fmt.Sprintf("failed to get exception group with id %q", crashGroupId.String())
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

distribution, err := GetIssuesAttributeDistribution(ctx, group, &af)
if err != nil {
msg := `failed to query data for crash distribution plot`
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": msg,
})
return
}

c.JSON(http.StatusOK, distribution)
}

func GetCrashDetailPlotJourney(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("id"))
Expand Down Expand Up @@ -3600,6 +3716,116 @@ func GetANRDetailPlotInstances(c *gin.Context) {
c.JSON(http.StatusOK, instances)
}

func GetANRDetailAttributeDistribution(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("id"))
if err != nil {
msg := `id invalid or missing`
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

anrGroupId, err := uuid.Parse(c.Param("anrGroupId"))
if err != nil {
msg := `anr group id is invalid or missing`
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

af := filter.AppFilter{
AppID: id,
Limit: filter.DefaultPaginationLimit,
}

if err := c.ShouldBindQuery(&af); err != nil {
msg := `failed to parse query parameters`
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg, "details": err.Error()})
return
}

af.Expand()

msg := "app filters request validation failed"
if err := af.Validate(); err != nil {
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg, "details": err.Error()})
return
}

if len(af.Versions) > 0 || len(af.VersionCodes) > 0 {
if err := af.ValidateVersions(); err != nil {
fmt.Println(msg, err)
c.JSON(http.StatusBadRequest, gin.H{
"error": msg,
"details": err.Error(),
})
return
}
}

app := App{
ID: &id,
}
team, err := app.getTeam(ctx)
if err != nil {
msg := "failed to get team from app id"
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}
if team == nil {
msg := fmt.Sprintf("no team exists for app [%s]", app.ID)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}

userId := c.GetString("userId")
okTeam, err := PerformAuthz(userId, team.ID.String(), *ScopeTeamRead)
if err != nil {
msg := `failed to perform authorization`
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

okApp, err := PerformAuthz(userId, team.ID.String(), *ScopeAppRead)
if err != nil {
msg := `failed to perform authorization`
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

if !okTeam || !okApp {
msg := `you are not authorized to access this app`
c.JSON(http.StatusForbidden, gin.H{"error": msg})
return
}

group, err := app.GetANRGroup(ctx, anrGroupId)
if err != nil {
msg := fmt.Sprintf("failed to get anr group with id %q", anrGroupId.String())
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
return
}

distribution, err := GetIssuesAttributeDistribution(ctx, group, &af)
if err != nil {
msg := `failed to query data for anr distribution plot`
fmt.Println(msg, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": msg,
})
return
}

c.JSON(http.StatusOK, distribution)
}

func GetANRDetailPlotJourney(c *gin.Context) {
ctx := c.Request.Context()
id, err := uuid.Parse(c.Param("id"))
Expand Down
115 changes: 115 additions & 0 deletions backend/api/measure/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,121 @@ func GetANRPlotInstances(ctx context.Context, af *filter.AppFilter) (issueInstan
return
}

// GetIssuesAttributeDistribution queries distribution of attributes
// based on datetime and filters.
func GetIssuesAttributeDistribution(ctx context.Context, g group.IssueGroup, af *filter.AppFilter) (map[string]map[string]uint64, error) {
fingerprint := g.GetFingerprint()
groupType := event.TypeException

switch g.(type) {
case *group.ANRGroup:
groupType = event.TypeANR
case *group.ExceptionGroup:
groupType = event.TypeException
default:
err := errors.New("couldn't determine correct type of issue group")
return nil, err
}

stmt := sqlf.
From("default.events").
Select("concat(toString(attribute.app_version), ' (', toString(attribute.app_build), ')') as app_version").
Select("concat(toString(attribute.os_name), ' ', toString(attribute.os_version)) as os_version").
Select("toString(inet.country_code) as country").
Select("toString(attribute.network_type) as network_type").
Select("toString(attribute.device_locale) as locale").
Select("concat(toString(attribute.device_manufacturer), ' - ', toString(attribute.device_name)) as device").
Select("uniq(id) as count").
Clause(fmt.Sprintf("prewhere app_id = toUUID(?) and %s.fingerprint = ?", groupType), af.AppID, fingerprint).
GroupBy("app_version").
GroupBy("os_version").
GroupBy("country").
GroupBy("network_type").
GroupBy("locale").
GroupBy("device")

// Add filters as necessary
stmt.Where("timestamp >= ? and timestamp <= ?", af.From, af.To)
if len(af.Versions) > 0 {
stmt.Where("attribute.app_version in ?", af.Versions)
}
if len(af.VersionCodes) > 0 {
stmt.Where("attribute.app_build in ?", af.VersionCodes)
}
if len(af.OsNames) > 0 {
stmt.Where("attribute.os_name in ?", af.OsNames)
}
if len(af.OsVersions) > 0 {
stmt.Where("attribute.os_version in ?", af.OsVersions)
}
if len(af.Countries) > 0 {
stmt.Where("inet.country_code in ?", af.Countries)
}
if len(af.NetworkTypes) > 0 {
stmt.Where("attribute.network_type in ?", af.NetworkTypes)
}
if len(af.NetworkGenerations) > 0 {
stmt.Where("attribute.network_generation in ?", af.NetworkGenerations)
}
if len(af.Locales) > 0 {
stmt.Where("attribute.device_locale in ?", af.Locales)
}
if len(af.DeviceManufacturers) > 0 {
stmt.Where("attribute.device_manufacturer in ?", af.DeviceManufacturers)
}
if len(af.DeviceNames) > 0 {
stmt.Where("attribute.device_name in ?", af.DeviceNames)
}

// Execute the query and parse results
rows, err := server.Server.ChPool.Query(ctx, stmt.String(), stmt.Args()...)
if err != nil {
return nil, err
}
defer rows.Close()

// Initialize a map to store distribution results for each attribute.
attributeDistributions := map[string]map[string]uint64{
"app_version": make(map[string]uint64),
"os_version": make(map[string]uint64),
"country": make(map[string]uint64),
"network_type": make(map[string]uint64),
"locale": make(map[string]uint64),
"device": make(map[string]uint64),
}

// Parse each row in the result set.
for rows.Next() {
var (
appVersion string
osVersion string
country string
networkType string
locale string
device string
count uint64
)

if err := rows.Scan(&appVersion, &osVersion, &country, &networkType, &locale, &device, &count); err != nil {
return nil, err
}

// Update counts in the distribution map
attributeDistributions["app_version"][appVersion] += count
attributeDistributions["os_version"][osVersion] += count
attributeDistributions["country"][country] += count
attributeDistributions["network_type"][networkType] += count
attributeDistributions["locale"][locale] += count
attributeDistributions["device"][device] += count
}

if rows.Err() != nil {
return nil, rows.Err()
}

return attributeDistributions, nil
}

// GetIssuesPlot aggregates issue free percentage for plotting
// visually from an ExceptionGroup or ANRGroup.
func GetIssuesPlot(ctx context.Context, g group.IssueGroup, af *filter.AppFilter) (issueInstances []event.IssueInstance, err error) {
Expand Down
Loading

0 comments on commit 1ddbab8

Please sign in to comment.