From 6e57d2105d9eb29e65364eb62ba827aceafc514f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 22 Apr 2025 15:09:39 +0200 Subject: [PATCH] feat(product_analytics): handle filters dynamically --- backend/pkg/analytics/charts/metric_table.go | 11 +- backend/pkg/analytics/charts/query.go | 212 +++++++++++++++---- 2 files changed, 178 insertions(+), 45 deletions(-) diff --git a/backend/pkg/analytics/charts/metric_table.go b/backend/pkg/analytics/charts/metric_table.go index 7a48e6e4c..0c4eb0f68 100644 --- a/backend/pkg/analytics/charts/metric_table.go +++ b/backend/pkg/analytics/charts/metric_table.go @@ -43,6 +43,13 @@ var propertySelectorMap = map[string]string{ string(MetricOfTableReferrer): "main.$referrer AS metric_value", } +var mainColumns = map[string]string{ + "user_browser": "$browser", + "user_device": "$device_type", + "user_country": "$country", + "referrer": "$referrer", +} + func (t TableQueryBuilder) Execute(p Payload, conn db.Connector) (interface{}, error) { if p.MetricOf == "" { return nil, fmt.Errorf("MetricOf is empty") @@ -120,7 +127,9 @@ func (t TableQueryBuilder) buildQuery(r Payload, metricFormat string) (string, e propertyName = originalMetricOf eventFilters := s.Filter.Filters - eventConds, eventNames := buildEventConditions(eventFilters) + eventConds, eventNames := buildEventConditions(eventFilters, BuildConditionsOptions{ + DefinedColumns: mainColumns, + }) baseWhereConditions := []string{ fmt.Sprintf("main.created_at >= toDateTime(%d/1000)", r.StartTimestamp), diff --git a/backend/pkg/analytics/charts/query.go b/backend/pkg/analytics/charts/query.go index fe9018131..c40cc70ea 100644 --- a/backend/pkg/analytics/charts/query.go +++ b/backend/pkg/analytics/charts/query.go @@ -41,62 +41,186 @@ func partitionFilters(filters []Filter) (sessionFilters []Filter, eventFilters [ return } -func buildEventConditions(filters []Filter) (conds, names []string) { +var validFilterTypes = map[FilterType]struct{}{ + FilterClick: {}, + FilterInput: {}, + FilterLocation: {}, + FilterCustom: {}, + FilterFetch: {}, + FilterTag: {}, + FilterUserCountry: {}, + FilterUserCity: {}, + FilterUserState: {}, + FilterUserId: {}, + FilterUserAnonymousId: {}, + FilterUserOs: {}, + FilterUserBrowser: {}, + FilterUserDevice: {}, + FilterPlatform: {}, + FilterRevId: {}, + FilterReferrer: {}, + FilterUtmSource: {}, + FilterUtmMedium: {}, + FilterUtmCampaign: {}, + FilterDuration: {}, + FilterMetadata: {}, +} + +type BuildConditionsOptions struct { + MainTableAlias string + PropertiesColumnName string + DefinedColumns map[string]string +} + +type filterConfig struct { + LogicalProperty string + EventName string + IsNumeric bool +} + +var filterTypeConfigs = map[FilterType]filterConfig{ + FilterClick: {LogicalProperty: "label", EventName: "CLICK"}, + FilterInput: {LogicalProperty: "label", EventName: "INPUT"}, + FilterLocation: {LogicalProperty: "url_path", EventName: "LOCATION"}, + FilterCustom: {LogicalProperty: "name", EventName: "CUSTOM"}, + FilterTag: {LogicalProperty: "tag", EventName: "TAG"}, +} + +var nestedFilterTypeConfigs = map[string]filterConfig{ + "fetchUrl": {LogicalProperty: "url_path"}, + "fetchStatusCode": {LogicalProperty: "status", IsNumeric: true}, +} + +func getColumnAccessor(logicalProp string, isNumeric bool, opts BuildConditionsOptions) string { + if actualCol, ok := opts.DefinedColumns[logicalProp]; ok && actualCol != "" { + return fmt.Sprintf("%s.`%s`", opts.MainTableAlias, actualCol) + } + + jsonFunc := "JSONExtractString" + if isNumeric { + jsonFunc = "JSONExtractFloat" // Or JSONExtractInt, etc. + } + + return fmt.Sprintf("%s(toString(%s.`%s`), '%s')", + jsonFunc, opts.MainTableAlias, opts.PropertiesColumnName, logicalProp) +} + +func buildEventConditions(filters []Filter, options ...BuildConditionsOptions) (conds, names []string) { + opts := BuildConditionsOptions{ + MainTableAlias: "main", + PropertiesColumnName: "$properties", + DefinedColumns: make(map[string]string), + } + + if len(options) > 0 { + if options[0].MainTableAlias != "" { + opts.MainTableAlias = options[0].MainTableAlias + } + if options[0].PropertiesColumnName != "" { + opts.PropertiesColumnName = options[0].PropertiesColumnName + } + if options[0].DefinedColumns != nil { + opts.DefinedColumns = options[0].DefinedColumns + } + } + for _, f := range filters { - //if f.IsEvent { - switch f.Type { - case FilterClick: - c := buildCond("JSONExtractString(toString(main.`$properties`), 'label')", f.Value, "is") - if c != "" { - conds = append(conds, c) - } - names = append(names, "CLICK") - case FilterInput: - c := buildCond("JSONExtractString(toString(main.`$properties`), 'label')", f.Value, f.Operator) - if c != "" { - conds = append(conds, c) - } - names = append(names, "INPUT") - case FilterLocation: - c := buildCond("JSONExtractString(toString(main.`$properties`), 'url_path')", f.Value, f.Operator) - if c != "" { - conds = append(conds, c) - } - names = append(names, "LOCATION") - case FilterCustom: - c := buildCond("JSONExtractString(toString(main.`$properties`), 'name')", f.Value, f.Operator) - if c != "" { - conds = append(conds, c) - } - names = append(names, "CUSTOM") - case FilterFetch: + _, isValidType := validFilterTypes[f.Type] + if !isValidType || !f.IsEvent { + continue + } + + if f.Type == FilterFetch { var fetchConds []string for _, nf := range f.Filters { - switch nf.Type { - case "fetchUrl": - c := buildCond("JSONExtractString(toString(main.`$properties`), 'url_path')", nf.Value, f.Operator) - if c != "" { - fetchConds = append(fetchConds, c) - } - case "fetchStatusCode": - c := buildCond("JSONExtractFloat(toString(main.`$properties`), 'status')", nf.Value, f.Operator) - if c != "" { - fetchConds = append(fetchConds, c) - } + nestedConfig, ok := nestedFilterTypeConfigs[string(nf.Type)] + if !ok { + continue + } + + accessor := getColumnAccessor(nestedConfig.LogicalProperty, nestedConfig.IsNumeric, opts) + c := buildCond(accessor, nf.Value, f.Operator) // Uses parent filter's operator + if c != "" { + fetchConds = append(fetchConds, c) } } if len(fetchConds) > 0 { conds = append(conds, strings.Join(fetchConds, " AND ")) + names = append(names, "REQUEST") } - names = append(names, "REQUEST") - case FilterTag: - c := buildCond("JSONExtractString(toString(main.`$properties`), 'tag')", f.Value, f.Operator) + } else { + config, ok := filterTypeConfigs[f.Type] + if !ok { + continue + } + + accessor := getColumnAccessor(config.LogicalProperty, config.IsNumeric, opts) + c := buildCond(accessor, f.Value, f.Operator) if c != "" { conds = append(conds, c) + names = append(names, config.EventName) + } + } + } + return +} + +func buildEventConditionsX(filters []Filter) (conds, names []string) { + for _, f := range filters { + if f.IsEvent { + switch f.Type { + case FilterClick: + c := buildCond("JSONExtractString(toString(main.`$properties`), 'label')", f.Value, "is") + if c != "" { + conds = append(conds, c) + } + names = append(names, "CLICK") + case FilterInput: + c := buildCond("JSONExtractString(toString(main.`$properties`), 'label')", f.Value, f.Operator) + if c != "" { + conds = append(conds, c) + } + names = append(names, "INPUT") + case FilterLocation: + c := buildCond("JSONExtractString(toString(main.`$properties`), 'url_path')", f.Value, f.Operator) + if c != "" { + conds = append(conds, c) + } + names = append(names, "LOCATION") + case FilterCustom: + c := buildCond("JSONExtractString(toString(main.`$properties`), 'name')", f.Value, f.Operator) + if c != "" { + conds = append(conds, c) + } + names = append(names, "CUSTOM") + case FilterFetch: + var fetchConds []string + for _, nf := range f.Filters { + switch nf.Type { + case "fetchUrl": + c := buildCond("JSONExtractString(toString(main.`$properties`), 'url_path')", nf.Value, f.Operator) + if c != "" { + fetchConds = append(fetchConds, c) + } + case "fetchStatusCode": + c := buildCond("JSONExtractFloat(toString(main.`$properties`), 'status')", nf.Value, f.Operator) + if c != "" { + fetchConds = append(fetchConds, c) + } + } + } + if len(fetchConds) > 0 { + conds = append(conds, strings.Join(fetchConds, " AND ")) + } + names = append(names, "REQUEST") + case FilterTag: + c := buildCond("JSONExtractString(toString(main.`$properties`), 'tag')", f.Value, f.Operator) + if c != "" { + conds = append(conds, c) + } + names = append(names, "TAG") } - names = append(names, "TAG") } - //} } return }