{range(0, tableRows[0].length).map((columnIndex) => (
@@ -50,10 +94,12 @@ export function RetentionTable({ inSharedMode = false }: { inSharedMode?: boolea
percentage={
mean(
tableRows.map((row) => {
- // Stop before the last item in a row, which is an incomplete time period
+ // Don't include the last item in a row, which is an incomplete time period
+ // Also don't include the percentage if the cohort size (count) is 0 or less
if (
(columnIndex >= row.length - 1 && isLatestPeriod) ||
- !row[columnIndex]
+ !row[columnIndex] ||
+ row[columnIndex].count <= 0
) {
return null
}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index f548fa1121e57..f44c706850f48 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -16,6 +16,7 @@ import {
PluginsAccessLevel,
PROPERTY_MATCH_TYPE,
RETENTION_FIRST_TIME,
+ RETENTION_MEAN_NONE,
RETENTION_RECURRING,
ShownAsValue,
TeamMembershipLevel,
@@ -2435,7 +2436,7 @@ export interface RetentionFilterType extends FilterType {
cumulative?: boolean
//frontend only
- show_mean?: boolean
+ show_mean?: 'simple' | 'weighted' | typeof RETENTION_MEAN_NONE
}
export interface LifecycleFilterType extends FilterType {
/** @deprecated */
diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py
index 9ba7c48f80c2a..697e48d4b0052 100644
--- a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py
+++ b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py
@@ -1504,7 +1504,7 @@ def test_retention_filter(self):
},
"target_entity": {"id": "$pageview", "name": "$pageview", "type": "events"},
"period": "Week",
- "show_mean": True,
+ "show_mean": "simple",
"cumulative": True,
}
@@ -1531,7 +1531,7 @@ def test_retention_filter(self):
"custom_name": None,
"order": None,
},
- showMean=True,
+ showMean="simple",
cumulative=True,
),
)
diff --git a/posthog/migrations/0660_migrate_retention_show_mean.py b/posthog/migrations/0660_migrate_retention_show_mean.py
new file mode 100644
index 0000000000000..fd286a8916664
--- /dev/null
+++ b/posthog/migrations/0660_migrate_retention_show_mean.py
@@ -0,0 +1,49 @@
+from django.db import migrations
+
+
+def migrate_show_mean_from_boolean_to_string(apps, schema_editor):
+ Insight = apps.get_model("posthog", "Insight")
+
+ # Get all retention insights
+ retention_insights = Insight.objects.filter(
+ filters__insight="RETENTION", deleted=False, filters__has_key="show_mean"
+ ).exclude(
+ filters__show_mean__isnull=True,
+ )
+
+ for insight in retention_insights.iterator(chunk_size=100):
+ if isinstance(insight.filters.get("show_mean"), bool):
+ # Convert boolean to string - if True, use 'simple'
+ insight.filters["show_mean"] = "simple" if insight.filters["show_mean"] else None
+ insight.save()
+
+
+def reverse_migrate_show_mean_from_string_to_boolean(apps, schema_editor):
+ Insight = apps.get_model("posthog", "Insight")
+
+ # Get all retention insights
+ retention_insights = Insight.objects.filter(
+ filters__insight="RETENTION", deleted=False, filters__has_key="show_mean"
+ ).exclude(
+ filters__show_mean__isnull=True,
+ )
+
+ for insight in retention_insights.iterator(chunk_size=100):
+ if isinstance(insight.filters.get("show_mean"), str):
+ # Convert string back to boolean - 'simple' and 'weighted' becomes True
+ insight.filters["show_mean"] = (
+ insight.filters["show_mean"] == "simple" or insight.filters["show_mean"] == "weighted"
+ )
+ insight.save()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("posthog", "0559_team_api_query_rate_limit"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ migrate_show_mean_from_boolean_to_string, reverse_migrate_show_mean_from_string_to_boolean
+ ),
+ ]
diff --git a/posthog/migrations/max_migration.txt b/posthog/migrations/max_migration.txt
index 0a43743bbee45..9b16823a2cfdc 100644
--- a/posthog/migrations/max_migration.txt
+++ b/posthog/migrations/max_migration.txt
@@ -1 +1 @@
-0559_team_api_query_rate_limit
+0660_migrate_retention_show_mean
diff --git a/posthog/schema.py b/posthog/schema.py
index c03d9b04112d4..e953e5d7f52b2 100644
--- a/posthog/schema.py
+++ b/posthog/schema.py
@@ -149,6 +149,12 @@ class RetentionReference(StrEnum):
PREVIOUS = "previous"
+class ShowMean(StrEnum):
+ SIMPLE = "simple"
+ WEIGHTED = "weighted"
+ NONE = "none"
+
+
class AssistantSetPropertyFilterOperator(StrEnum):
IS_SET = "is_set"
IS_NOT_SET = "is_not_set"
@@ -5498,7 +5504,7 @@ class RetentionFilter(BaseModel):
)
retentionType: Optional[RetentionType] = None
returningEntity: Optional[RetentionEntity] = None
- showMean: Optional[bool] = None
+ showMean: Optional[ShowMean] = None
targetEntity: Optional[RetentionEntity] = None
totalIntervals: Optional[int] = 11
@@ -5515,7 +5521,7 @@ class RetentionFilterLegacy(BaseModel):
)
retention_type: Optional[RetentionType] = None
returning_entity: Optional[RetentionEntity] = None
- show_mean: Optional[bool] = None
+ show_mean: Optional[ShowMean] = None
target_entity: Optional[RetentionEntity] = None
total_intervals: Optional[int] = None
@@ -5892,7 +5898,7 @@ class AssistantRetentionFilter(BaseModel):
returningEntity: Optional[RetentionEntity] = Field(
default=None, description="Retention event (event marking the user coming back)."
)
- showMean: Optional[bool] = Field(
+ showMean: Optional[ShowMean] = Field(
default=None,
description=(
"Whether an additional series should be shown, showing the mean conversion for each period across cohorts."
|