diff --git a/api/chalicelib/core/metrics/custom_metrics.py b/api/chalicelib/core/metrics/custom_metrics.py index 1e359bddd..a62f37cdd 100644 --- a/api/chalicelib/core/metrics/custom_metrics.py +++ b/api/chalicelib/core/metrics/custom_metrics.py @@ -209,14 +209,15 @@ def get_issues(project: schemas.ProjectContext, user_id: int, data: schemas.Card def __get_global_card_info(data: schemas.CardSchema): - r = {"hideExcess": data.hide_excess, "compareTo": data.compare_to} + r = {"hideExcess": data.hide_excess, "compareTo": data.compare_to, "rows": data.rows} return r def __get_path_analysis_card_info(data: schemas.CardPathAnalysis): r = {"start_point": [s.model_dump() for s in data.start_point], "start_type": data.start_type, - "excludes": [e.model_dump() for e in data.excludes]} + "excludes": [e.model_dump() for e in data.excludes], + "rows": data.rows} return r diff --git a/api/chalicelib/core/metrics/product_analytics.py b/api/chalicelib/core/metrics/product_analytics.py index ffa4fd319..1205b9c07 100644 --- a/api/chalicelib/core/metrics/product_analytics.py +++ b/api/chalicelib/core/metrics/product_analytics.py @@ -12,33 +12,33 @@ logger = logging.getLogger(__name__) def __transform_journey(rows, reverse_path=False): total_100p = 0 - number_of_step1 = 0 for r in rows: if r["event_number_in_session"] > 1: break - number_of_step1 += 1 total_100p += r["sessions_count"] - for i in range(len(rows)): - rows[i]["value"] = rows[i]["sessions_count"] * 100 / total_100p - nodes = [] nodes_values = [] links = [] + drops = [] + max_depth = 0 for r in rows: - source = f"{r['event_number_in_session']}_{r['event_type']}_{r['e_value']}" + r["value"] = r["sessions_count"] * 100 / total_100p + source = f"{r['event_number_in_session'] - 1}_{r['event_type']}_{r['e_value']}" if source not in nodes: nodes.append(source) nodes_values.append({"depth": r['event_number_in_session'] - 1, "name": r['e_value'], - "eventType": r['event_type']}) - # if r['next_value']: - target = f"{r['event_number_in_session'] + 1}_{r['next_type']}_{r['next_value']}" + "eventType": r['event_type'], + "id": len(nodes_values)}) + + target = f"{r['event_number_in_session']}_{r['next_type']}_{r['next_value']}" if target not in nodes: nodes.append(target) nodes_values.append({"depth": r['event_number_in_session'], "name": r['next_value'], - "eventType": r['next_type']}) + "eventType": r['next_type'], + "id": len(nodes_values)}) sr_idx = nodes.index(source) tg_idx = nodes.index(target) @@ -52,6 +52,43 @@ def __transform_journey(rows, reverse_path=False): link["target"] = sr_idx links.append(link) + max_depth = r['event_number_in_session'] + if r["next_type"] == "DROP": + for d in drops: + if d["depth"] == r['event_number_in_session']: + d["sessions_count"] += r["sessions_count"] + break + else: + drops.append({"depth": r['event_number_in_session'], "sessions_count": r["sessions_count"]}) + + for i in range(len(drops)): + + if drops[i]["depth"] < max_depth: + source = f"{drops[i]['depth']}_DROP_None" + target = f"{drops[i]['depth'] + 1}_DROP_None" + sr_idx = nodes.index(source) + + if i < len(drops) - 1 and drops[i]["depth"] + 1 == drops[i + 1]["depth"]: + tg_idx = nodes.index(target) + else: + nodes.append(target) + nodes_values.append({"depth": drops[i]["depth"] + 1, + "name": None, + "eventType": "DROP", + "id": len(nodes_values)}) + tg_idx = len(nodes) - 1 + + link = {"eventType": "DROP", + "sessionsCount": drops[i]["sessions_count"], + "value": drops[i]["sessions_count"] * 100 / total_100p} + if not reverse_path: + link["source"] = sr_idx + link["target"] = tg_idx + else: + link["source"] = tg_idx + link["target"] = sr_idx + links.append(link) + return {"nodes": nodes_values, "links": sorted(links, key=lambda x: (x["source"], x["target"]), reverse=False)} diff --git a/api/chalicelib/core/metrics/product_analytics_ch.py b/api/chalicelib/core/metrics/product_analytics_ch.py index d8760f71b..d6059acb9 100644 --- a/api/chalicelib/core/metrics/product_analytics_ch.py +++ b/api/chalicelib/core/metrics/product_analytics_ch.py @@ -23,6 +23,7 @@ JOURNEY_TYPES = { } + # startPoints are computed before ranked_events to reduce the number of window functions over rows # compute avg_time_from_previous at the same level as sessions_count (this was removed in v1.22) # if start-point is selected, the selected event is ranked n°1 diff --git a/api/schemas/schemas.py b/api/schemas/schemas.py index 809aca8e9..82104a41d 100644 --- a/api/schemas/schemas.py +++ b/api/schemas/schemas.py @@ -133,7 +133,7 @@ class _TimedSchema(BaseModel): class NotificationsViewSchema(_TimedSchema): - ids: List[int] = Field(default=[]) + ids: List[int] = Field(default_factory=list) startTimestamp: Optional[int] = Field(default=None) endTimestamp: Optional[int] = Field(default=None) @@ -545,7 +545,7 @@ class SessionSearchEventSchema2(BaseModel): operator: Union[SearchEventOperator, ClickEventExtraOperator] = Field(...) source: Optional[List[Union[ErrorSource, int, str]]] = Field(default=None) sourceOperator: Optional[MathOperator] = Field(default=None) - filters: Optional[List[RequestGraphqlFilterSchema]] = Field(default=[]) + filters: Optional[List[RequestGraphqlFilterSchema]] = Field(default_factory=list) _remove_duplicate_values = field_validator('value', mode='before')(remove_duplicate_values) _single_to_list_values = field_validator('value', mode='before')(single_to_list) @@ -579,7 +579,7 @@ class SessionSearchEventSchema2(BaseModel): class SessionSearchFilterSchema(BaseModel): is_event: Literal[False] = False - value: List[Union[IssueType, PlatformType, int, str]] = Field(default=[]) + value: List[Union[IssueType, PlatformType, int, str]] = Field(default_factory=list) type: FilterType = Field(...) operator: Union[SearchEventOperator, MathOperator] = Field(...) source: Optional[Union[ErrorSource, str]] = Field(default=None) @@ -658,8 +658,8 @@ Field(discriminator='is_event'), BeforeValidator(add_missing_is_event)] class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema): - events: List[SessionSearchEventSchema2] = Field(default=[], doc_hidden=True) - filters: List[GroupedFilterType] = Field(default=[]) + events: List[SessionSearchEventSchema2] = Field(default_factory=list, doc_hidden=True) + filters: List[GroupedFilterType] = Field(default_factory=list) sort: str = Field(default="startTs") order: SortOrderType = Field(default=SortOrderType.DESC) events_order: Optional[SearchEventOrder] = Field(default=SearchEventOrder.THEN) @@ -816,7 +816,7 @@ Field(discriminator='is_event')] class PathAnalysisSchema(_TimedSchema, _PaginatedSchema): density: int = Field(default=7) - filters: List[ProductAnalyticsFilter] = Field(default=[]) + filters: List[ProductAnalyticsFilter] = Field(default_factory=list) type: Optional[str] = Field(default=None) _transform_filters = field_validator('filters', mode='before') \ @@ -928,10 +928,10 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema): startTimestamp: int = Field(default=TimeUTC.now(-7)) endTimestamp: int = Field(default=TimeUTC.now()) density: int = Field(default=7, ge=1, le=200) - series: List[CardSeriesSchema] = Field(default=[]) + series: List[CardSeriesSchema] = Field(default_factory=list) - # events: List[SessionSearchEventSchema2] = Field(default=[], doc_hidden=True) - filters: List[GroupedFilterType] = Field(default=[]) + # events: List[SessionSearchEventSchema2] = Field(default_factory=list, doc_hidden=True) + filters: List[GroupedFilterType] = Field(default_factory=list) compare_to: Optional[List[str]] = Field(default=None) @@ -1037,9 +1037,11 @@ class __CardSchema(CardSessionsSchema): view_type: Any metric_type: MetricType = Field(...) metric_of: Any - metric_value: List[IssueType] = Field(default=[]) + metric_value: List[IssueType] = Field(default_factory=list) # This is used to save the selected session for heatmaps session_id: Optional[int] = Field(default=None) + # This is used to specify the number of top values for PathAnalysis + rows: int = Field(default=3, ge=1, le=10) @computed_field @property @@ -1185,14 +1187,15 @@ class CardPathAnalysis(__CardSchema): metric_type: Literal[MetricType.PATH_ANALYSIS] metric_of: MetricOfPathAnalysis = Field(default=MetricOfPathAnalysis.session_count) view_type: MetricOtherViewType = Field(...) - metric_value: List[ProductAnalyticsSelectedEventType] = Field(default=[]) + metric_value: List[ProductAnalyticsSelectedEventType] = Field(default_factory=list) density: int = Field(default=4, ge=2, le=10) + rows: int = Field(default=3, ge=1, le=10) start_type: Literal["start", "end"] = Field(default="start") - start_point: List[PathAnalysisSubFilterSchema] = Field(default=[]) - excludes: List[PathAnalysisSubFilterSchema] = Field(default=[]) + start_point: List[PathAnalysisSubFilterSchema] = Field(default_factory=list) + excludes: List[PathAnalysisSubFilterSchema] = Field(default_factory=list) - series: List[CardPathAnalysisSeriesSchema] = Field(default=[]) + series: List[CardPathAnalysisSeriesSchema] = Field(default_factory=list) @model_validator(mode="before") @classmethod @@ -1258,13 +1261,13 @@ class ProjectConditions(BaseModel): condition_id: Optional[int] = Field(default=None) name: str = Field(...) capture_rate: int = Field(..., ge=0, le=100) - filters: List[GroupedFilterType] = Field(default=[]) + filters: List[GroupedFilterType] = Field(default_factory=list) class ProjectSettings(BaseModel): rate: int = Field(..., ge=0, le=100) conditional_capture: bool = Field(default=False) - conditions: List[ProjectConditions] = Field(default=[]) + conditions: List[ProjectConditions] = Field(default_factory=list) class CreateDashboardSchema(BaseModel): @@ -1272,7 +1275,7 @@ class CreateDashboardSchema(BaseModel): description: Optional[str] = Field(default='') is_public: bool = Field(default=False) is_pinned: bool = Field(default=False) - metrics: Optional[List[int]] = Field(default=[]) + metrics: Optional[List[int]] = Field(default_factory=list) class EditDashboardSchema(CreateDashboardSchema): @@ -1281,7 +1284,7 @@ class EditDashboardSchema(CreateDashboardSchema): class UpdateWidgetPayloadSchema(BaseModel): - config: dict = Field(default={}) + config: dict = Field(default_factory=dict) class AddWidgetToDashboardPayloadSchema(UpdateWidgetPayloadSchema): @@ -1379,7 +1382,7 @@ class IntegrationType(str, Enum): class SearchNoteSchema(_PaginatedSchema): sort: str = Field(default="createdAt") order: SortOrderType = Field(default=SortOrderType.DESC) - tags: Optional[List[str]] = Field(default=[]) + tags: Optional[List[str]] = Field(default_factory=list) shared_only: bool = Field(default=False) mine_only: bool = Field(default=False) search: Optional[str] = Field(default=None) @@ -1426,8 +1429,8 @@ class _HeatMapSearchEventRaw(SessionSearchEventSchema2): class HeatMapSessionsSearch(SessionsSearchPayloadSchema): - events: Optional[List[_HeatMapSearchEventRaw]] = Field(default=[]) - filters: List[Union[SessionSearchFilterSchema, _HeatMapSearchEventRaw]] = Field(default=[]) + events: Optional[List[_HeatMapSearchEventRaw]] = Field(default_factory=list) + filters: List[Union[SessionSearchFilterSchema, _HeatMapSearchEventRaw]] = Field(default_factory=list) @model_validator(mode="before") @classmethod @@ -1442,14 +1445,14 @@ class HeatMapSessionsSearch(SessionsSearchPayloadSchema): class HeatMapFilterSchema(BaseModel): - value: List[Literal[IssueType.CLICK_RAGE, IssueType.DEAD_CLICK]] = Field(default=[]) + value: List[Literal[IssueType.CLICK_RAGE, IssueType.DEAD_CLICK]] = Field(default_factory=list) type: Literal[FilterType.ISSUE] = Field(...) operator: Literal[SearchEventOperator.IS, MathOperator.EQUAL] = Field(...) class GetHeatMapPayloadSchema(_TimedSchema): url: Optional[str] = Field(default=None) - filters: List[HeatMapFilterSchema] = Field(default=[]) + filters: List[HeatMapFilterSchema] = Field(default_factory=list) click_rage: bool = Field(default=False) operator: Literal[SearchEventOperator.IS, SearchEventOperator.STARTS_WITH, SearchEventOperator.CONTAINS, SearchEventOperator.ENDS_WITH] = Field(default=SearchEventOperator.STARTS_WITH) @@ -1470,7 +1473,7 @@ class FeatureFlagVariant(BaseModel): class FeatureFlagConditionFilterSchema(BaseModel): is_event: Literal[False] = False type: FilterType = Field(...) - value: List[str] = Field(default=[], min_length=1) + value: List[str] = Field(default_factory=list, min_length=1) operator: Union[SearchEventOperator, MathOperator] = Field(...) source: Optional[str] = Field(default=None) sourceOperator: Optional[Union[SearchEventOperator, MathOperator]] = Field(default=None) @@ -1486,7 +1489,7 @@ class FeatureFlagCondition(BaseModel): condition_id: Optional[int] = Field(default=None) name: str = Field(...) rollout_percentage: Optional[int] = Field(default=0) - filters: List[FeatureFlagConditionFilterSchema] = Field(default=[]) + filters: List[FeatureFlagConditionFilterSchema] = Field(default_factory=list) class SearchFlagsSchema(_PaginatedSchema): @@ -1513,8 +1516,8 @@ class FeatureFlagSchema(BaseModel): flag_type: FeatureFlagType = Field(default=FeatureFlagType.SINGLE_VARIANT) is_persist: Optional[bool] = Field(default=False) is_active: Optional[bool] = Field(default=True) - conditions: List[FeatureFlagCondition] = Field(default=[], min_length=1) - variants: List[FeatureFlagVariant] = Field(default=[]) + conditions: List[FeatureFlagCondition] = Field(default_factory=list, min_length=1) + variants: List[FeatureFlagVariant] = Field(default_factory=list) class ModuleType(str, Enum): diff --git a/frontend/app/mstore/types/FeatureFlag.ts b/frontend/app/mstore/types/FeatureFlag.ts index 7e4d9c327..de6b59310 100644 --- a/frontend/app/mstore/types/FeatureFlag.ts +++ b/frontend/app/mstore/types/FeatureFlag.ts @@ -9,10 +9,11 @@ export class Conditions { constructor(data?: Record, isConditional?: boolean, isMobile?: boolean) { makeAutoObservable(this); + console.log('data', data) this.name = data?.name; if (data && (data.rolloutPercentage || data.captureRate)) { this.rolloutPercentage = data.rolloutPercentage ?? data.captureRate; - this.filter = new Filter(isConditional, isMobile).fromJson(data); + this.filter = new Filter([], isConditional, isMobile).fromJson(data); } } @@ -213,4 +214,4 @@ export default class FeatureFlag { this.isActive = isEnabled; this.setHasChanged(true) } -} \ No newline at end of file +} diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index 8809aa16c..6082aae4b 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -85,12 +85,12 @@ export default class FilterItem { if (this.isConditional) { if (this.isMobile) { _filter = - mobileConditionalFiltersMap[json.type] || - mobileConditionalFiltersMap[json.source]; + mobileConditionalFiltersMap[_filter.key] || + mobileConditionalFiltersMap[_filter.source]; } else { _filter = - conditionalFiltersMap[json.type] || - conditionalFiltersMap[json.source]; + conditionalFiltersMap[_filter.key] || + conditionalFiltersMap[_filter.source]; } } if (mainFilterKey) {