Repository: PostHog/posthog
Pull request #30059: fix: wau or mau with week or month or longer intervals falls back to dau
Author: login: aspicer
Created: 2025-03-18T07:57:32Z | Merged: 2025-03-20T20:56:35Z

## Problem

We get a healthy number of support issues from people being confused about primarily WAU logic for weekly intervals.

WAUs don't really exist for intervals of a week or longer. Our current query returns something which is wrong, which is different than what the actors modal shows.

Make our query consistent with the actors modal. In the case of a week or longer interval, WAUs just because unique users in the query.

## Changes

TrendsQueryRunner silently changes weekly or monthly math to unique user math if the interval is longer.

## Does this work well for both Cloud and self-hosted?

Yes

## How did you test this code?

Wrote tests!



Diff:
diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts
index 0e66d047d16d..bf48f0830dd8 100644
--- a/frontend/src/queries/utils.ts
+++ b/frontend/src/queries/utils.ts
@@ -30,6 +30,7 @@ import {
     InsightQueryNode,
     InsightVizNode,
     LifecycleQuery,
+    MathType,
     Node,
     NodeKind,
     PathsQuery,
@@ -51,7 +52,7 @@ import {
     WebVitalsPathBreakdownQuery,
     WebVitalsQuery,
 } from '~/queries/schema/schema-general'
-import { ChartDisplayType, IntervalType } from '~/types'
+import { BaseMathType, ChartDisplayType, IntervalType } from '~/types'
 
 export function isDataNode(node?: Record<string, any> | null): node is EventsQuery | PersonsNode {
     return (
@@ -572,3 +573,33 @@ export function isValidQueryForExperiment(query: Node): boolean {
 export function isGroupsQuery(node?: Record<string, any> | null): node is GroupsQuery {
     return node?.kind === NodeKind.GroupsQuery
 }
+
+export const TRAILING_MATH_TYPES = new Set<MathType>([BaseMathType.WeeklyActiveUsers, BaseMathType.MonthlyActiveUsers])
+
+/**
+ * Determines if a math type should display a warning based on the trends query interval and display category
+ */
+export function getMathTypeWarning(
+    key: MathType,
+    query: Record<string, any>,
+    isTotalValue: boolean
+): null | 'total' | 'monthly' | 'weekly' {
+    let warning: null | 'total' | 'monthly' | 'weekly' = null
+
+    if (isInsightVizNode(query) && isTrendsQuery(query.source) && TRAILING_MATH_TYPES.has(key)) {
+        const trendsQuery = query.source
+        const interval = trendsQuery?.interval || 'day'
+        const isWeekOrLongerInterval = interval === 'week' || interval === 'month'
+        const isMonthOrLongerInterval = interval === 'month'
+
+        if (key === BaseMathType.MonthlyActiveUsers && isMonthOrLongerInterval) {
+            warning = 'monthly'
+        } else if (key === BaseMathType.WeeklyActiveUsers && isWeekOrLongerInterval) {
+            warning = 'weekly'
+        } else if (isTotalValue) {
+            warning = 'total'
+        }
+    }
+
+    return warning
+}
diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx
index dea68f3cd131..97030f8bd09e 100644
--- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx
+++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx
@@ -41,8 +41,8 @@ import {
 } from 'scenes/trends/mathsLogic'
 
 import { actionsModel } from '~/models/actionsModel'
-import { NodeKind } from '~/queries/schema/schema-general'
-import { isInsightVizNode, isStickinessQuery } from '~/queries/utils'
+import { MathType, NodeKind } from '~/queries/schema/schema-general'
+import { getMathTypeWarning, isInsightVizNode, isStickinessQuery, TRAILING_MATH_TYPES } from '~/queries/utils'
 import {
     ActionFilter,
     ActionFilter as ActionFilterType,
@@ -177,6 +177,9 @@ export function ActionFilterRow({
     const { mathDefinitions } = useValues(mathsLogic)
     const { dataWarehouseTablesMap } = useValues(databaseTableListLogic)
 
+    const mountedInsightDataLogic = insightDataLogic.findMounted({ dashboardItemId: typeKey })
+    const query = mountedInsightDataLogic?.values?.query
+
     const [isHogQLDropdownVisible, setIsHogQLDropdownVisible] = useState(false)
     const [isMenuVisible, setIsMenuVisible] = useState(false)
 
@@ -435,6 +438,7 @@ export function ActionFilterRow({
                                             mathAvailability={mathAvailability}
                                             trendsDisplayCategory={trendsDisplayCategory}
                                             allowedMathTypes={allowedMathTypes}
+                                            query={query || {}}
                                         />
                                         {mathDefinitions[math || BaseMathType.TotalCount]?.category ===
                                             MathCategory.PropertyValue && (
@@ -559,6 +563,7 @@ export function ActionFilterRow({
                                                                     style={{ maxWidth: '100%', width: 'initial' }}
                                                                     mathAvailability={mathAvailability}
                                                                     trendsDisplayCategory={trendsDisplayCategory}
+                                                                    query={query || {}}
                                                                 />
                                                                 <LemonDivider />
                                                             </>
@@ -654,6 +659,7 @@ interface MathSelectorProps {
     style?: React.CSSProperties
     /** Only allow these math types in the selector */
     allowedMathTypes?: readonly string[]
+    query?: Record<string, any>
 }
 
 function isPropertyValueMath(math: string | undefined): math is PropertyMathType {
@@ -664,8 +670,6 @@ function isCountPerActorMath(math: string | undefined): math is CountPerActorMat
     return !!math && math in COUNT_PER_ACTOR_MATH_DEFINITIONS
 }
 
-const TRAILING_MATH_TYPES = new Set<string>([BaseMathType.WeeklyActiveUsers, BaseMathType.MonthlyActiveUsers])
-
 function getDefaultPropertyMathType(
     math: string | undefined,
     allowedMathTypes: readonly string[] | undefined
@@ -687,10 +691,8 @@ function useMathSelectorOptions({
     onMathSelect,
     trendsDisplayCategory,
     allowedMathTypes,
+    query,
 }: MathSelectorProps): LemonSelectOptions<string> {
-    const mountedInsightDataLogic = insightDataLogic.findMounted()
-    const query = mountedInsightDataLogic?.values?.query
-
     const isStickiness = query && isInsightVizNode(query) && isStickinessQuery(query.source)
 
     const {
@@ -718,9 +720,10 @@ function useMathSelectorOptions({
 
     const options: LemonSelectOption<string>[] = Object.entries(definitions)
         .filter(([key]) => {
+            const mathTypeKey = key as MathType
             if (isStickiness) {
                 // Remove WAU and MAU from stickiness insights
-                return !TRAILING_MATH_TYPES.has(key)
+                return !TRAILING_MATH_TYPES.has(mathTypeKey)
             }
 
             if (allowedMathTypes) {
@@ -733,24 +736,41 @@ function useMathSelectorOptions({
             return true
         })
         .map(([key, definition]) => {
-            const shouldWarnAboutTrailingMath =
-                TRAILING_MATH_TYPES.has(key) && trendsDisplayCategory === ChartDisplayCategory.TotalValue
+            const mathTypeKey = key as MathType
+            const warning = getMathTypeWarning(mathTypeKey, query || {}, trendsDisplayCategory === 'TotalValue')
+
             return {
-                value: key,
-                icon: shouldWarnAboutTrailingMath ? <IconWarning /> : undefined,
+                value: mathTypeKey,
+                icon: warning !== null ? <IconWarning /> : undefined,
                 label: definition.name,
-                tooltip: !shouldWarnAboutTrailingMath ? (
-                    definition.description
-                ) : (
-                    <>
-                        <p>{definition.description}</p>
-                        <i>
-                            In total value insights, it's usually not clear what date range "{definition.name}" refers
-                            to. For full clarity, we recommend using "Unique users" here instead.
-                        </i>
-                    </>
-                ),
                 'data-attr': `math-${key}-${index}`,
+                tooltip:
+                    warning === 'total' ? (
+                        <>
+                            <p>{definition.description}</p>
+                            <i>
+                                In total value insights, it's usually not clear what date range "{definition.name}"
+                                refers to. For full clarity, we recommend using "Unique users" here instead.
+                            </i>
+                        </>
+                    ) : warning === null ? (
+                        definition.description
+                    ) : (
+                        <>
+                            {warning === 'weekly' ? (
+                                <p>
+                                    Weekly active users is not meaningful when using week or month intervals because the
+                                    sliding window calculation cannot be properly applied.
+                                </p>
+                            ) : (
+                                <p>
+                                    Monthly active users is not meaningful when using month intervals because the
+                                    sliding window calculation cannot be properly applied.
+                                </p>
+                            )}
+                            <span>This query mode has the same functionality as "Unique users" for this interval.</span>
+                        </>
+                    ),
             }
         })
 
diff --git a/frontend/src/scenes/insights/utils/queryUtils.ts b/frontend/src/scenes/insights/utils/queryUtils.ts
index 2379f7ce4f52..d7792542d7cc 100644
--- a/frontend/src/scenes/insights/utils/queryUtils.ts
+++ b/frontend/src/scenes/insights/utils/queryUtils.ts
@@ -4,6 +4,7 @@ import { DataNode, InsightQueryNode, Node } from '~/queries/schema/schema-genera
 import {
     filterForQuery,
     filterKeyForQuery,
+    getMathTypeWarning,
     isEventsNode,
     isFunnelsQuery,
     isHogQLQuery,
@@ -11,8 +12,9 @@ import {
     isInsightQueryWithDisplay,
     isInsightQueryWithSeries,
     isInsightVizNode,
+    isTrendsQuery,
 } from '~/queries/utils'
-import { ChartDisplayType } from '~/types'
+import { BaseMathType, ChartDisplayType } from '~/types'
 
 type CompareQueryOpts = { ignoreVisualizationOnlyChanges: boolean }
 
@@ -117,10 +119,12 @@ const cleanInsightQuery = (query: InsightQueryNode, opts?: CompareQueryOpts): In
     const cleanedQuery = objectCleanWithEmpty(dupQuery) as InsightQueryNode
 
     if (isInsightQueryWithSeries(cleanedQuery)) {
-        cleanedQuery.series?.forEach((e) => {
+        cleanedQuery.series?.forEach((series) => {
             // event math `total` is the default
-            if (isEventsNode(e) && e.math === 'total') {
-                delete e.math
+            if (isEventsNode(series) && series.math === 'total') {
+                delete series.math
+            } else if (isTrendsQuery(cleanedQuery) && series.math && getMathTypeWarning(series.math, query, false)) {
+                series.math = BaseMathType.UniqueUsers
             }
         })
     }
diff --git a/frontend/src/scenes/trends/mathsLogic.tsx b/frontend/src/scenes/trends/mathsLogic.tsx
index e041fc96f627..8738ec6b204d 100644
--- a/frontend/src/scenes/trends/mathsLogic.tsx
+++ b/frontend/src/scenes/trends/mathsLogic.tsx
@@ -3,6 +3,7 @@ import { groupsAccessLogic } from 'lib/introductions/groupsAccessLogic'
 import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
 
 import { groupsModel } from '~/models/groupsModel'
+import { MathType } from '~/queries/schema/schema-general'
 import { BaseMathType, CountPerActorMathType, FunnelMathType, HogQLMathType, PropertyMathType } from '~/types'
 
 import type { mathsLogicType } from './mathsLogicType'
@@ -103,10 +104,14 @@ export const BASE_MATH_DEFINITIONS: Record<BaseMathType, MathDefinition> = {
         shortName: 'WAUs',
         description: (
             <>
-                Users active in the past week (7 days).
+                <b>Users active in the past week (7 days).</b>
+                <br />
                 <br />
                 This is a trailing count that aggregates distinct users in the past 7 days for each day in the time
-                series
+                series.
+                <br />
+                <br />
+                If the group by interval is a week or longer, this is the same as "Unique User" math.
             </>
         ),
         category: MathCategory.ActorCount,
@@ -116,10 +121,14 @@ export const BASE_MATH_DEFINITIONS: Record<BaseMathType, MathDefinition> = {
         shortName: 'MAUs',
         description: (
             <>
-                Users active in the past month (30 days).
+                <b>Users active in the past month (30 days).</b>
+                <br />
                 <br />
                 This is a trailing count that aggregates distinct users in the past 30 days for each day in the time
                 series
+                <br />
+                <br />
+                If the group by interval is a month or longer, this is the same as "Unique User" math.
             </>
         ),
         category: MathCategory.ActorCount,
@@ -392,8 +401,8 @@ export const mathsLogic = kea<mathsLogicType>([
     selectors({
         mathDefinitions: [
             (s) => [s.groupsMathDefinitions],
-            (groupsMathDefinitions): Record<string, MathDefinition> => {
-                const allMathDefinitions: Record<string, MathDefinition> = {
+            (groupsMathDefinitions): Partial<Record<MathType, MathDefinition>> => {
+                const allMathDefinitions: Partial<Record<MathType, MathDefinition>> = {
                     ...BASE_MATH_DEFINITIONS,
                     ...groupsMathDefinitions,
                     ...PROPERTY_MATH_DEFINITIONS,
@@ -405,8 +414,8 @@ export const mathsLogic = kea<mathsLogicType>([
         ],
         funnelMathDefinitions: [
             () => [],
-            (): Record<string, MathDefinition> => {
-                const funnelMathDefinitions: Record<string, MathDefinition> = {
+            (): Partial<Record<MathType, MathDefinition>> => {
+                const funnelMathDefinitions: Partial<Record<MathType, MathDefinition>> = {
                     ...FUNNEL_MATH_DEFINITIONS,
                 }
                 return funnelMathDefinitions
@@ -415,8 +424,8 @@ export const mathsLogic = kea<mathsLogicType>([
         // Static means the options do not have nested selectors (like math function)
         staticMathDefinitions: [
             (s) => [s.groupsMathDefinitions, s.needsUpgradeForGroups],
-            (groupsMathDefinitions, needsUpgradeForGroups): Record<string, MathDefinition> => {
-                const staticMathDefinitions: Record<string, MathDefinition> = {
+            (groupsMathDefinitions, needsUpgradeForGroups): Partial<Record<MathType, MathDefinition>> => {
+                const staticMathDefinitions: Partial<Record<MathType, MathDefinition>> = {
                     ...BASE_MATH_DEFINITIONS,
                     ...(!needsUpgradeForGroups ? groupsMathDefinitions : {}),
                 }
@@ -425,18 +434,18 @@ export const mathsLogic = kea<mathsLogicType>([
         ],
         staticActorsOnlyMathDefinitions: [
             (s) => [s.staticMathDefinitions],
-            (staticMathDefinitions): Record<string, MathDefinition> => {
+            (staticMathDefinitions): Partial<Record<MathType, MathDefinition>> => {
                 return Object.fromEntries(
                     Object.entries(staticMathDefinitions).filter(
                         ([, mathDefinition]) => mathDefinition.category === MathCategory.ActorCount
                     )
-                )
+                ) as Partial<Record<MathType, MathDefinition>>
             },
         ],
         // Definitions based on group types present in the project
         groupsMathDefinitions: [
             (s) => [s.groupTypes, s.aggregationLabel],
-            (groupTypes, aggregationLabel) =>
+            (groupTypes, aggregationLabel): Partial<Record<MathType, MathDefinition>> =>
                 Object.fromEntries(
                     Array.from(groupTypes.values())
                         .map((groupType) => [
diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr
index 950a41f46a1a..ff98ecf38bca 100644
--- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr
+++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr
@@ -5840,31 +5840,18 @@
     (SELECT sum(total) AS count,
             day_start AS day_start
      FROM
-       (SELECT counts AS total,
-               toStartOfWeek(timestamp, 0) AS day_start
-        FROM
-          (SELECT d.timestamp AS timestamp,
-                  count(DISTINCT e.actor_id) AS counts
-           FROM
-             (SELECT toTimeZone(e.timestamp, 'UTC') AS timestamp,
-                     if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id) AS actor_id
-              FROM events AS e SAMPLE 1
-              LEFT OUTER JOIN
-                (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
-                        person_distinct_id_overrides.distinct_id AS distinct_id
-                 FROM person_distinct_id_overrides
-                 WHERE equals(person_distinct_id_overrides.team_id, 99999)
-                 GROUP BY person_distinct_id_overrides.distinct_id
-                 HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id)
-              WHERE and(equals(e.team_id, 99999), equals(e.event, '$pageview'), greaterOrEquals(timestamp, minus(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'UTC')), toIntervalDay(7))), lessOrEquals(timestamp, assumeNotNull(toDateTime('2020-01-18 23:59:59', 'UTC'))))
-              GROUP BY timestamp, actor_id) AS e
-           CROSS JOIN
-             (SELECT minus(toStartOfWeek(assumeNotNull(toDateTime('2020-01-18 23:59:59', 'UTC')), 0), toIntervalWeek(numbers.number)) AS timestamp
-              FROM numbers(dateDiff('week', minus(toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'UTC')), 0), toIntervalDay(7)), assumeNotNull(toDateTime('2020-01-18 23:59:59', 'UTC')))) AS numbers) AS d
-           WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0))
-           GROUP BY d.timestamp
-           ORDER BY d.timestamp ASC)
-        WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'UTC')), 0)), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(toDateTime('2020-01-18 23:59:59', 'UTC'))), 0)))
+       (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total,
+               toStartOfWeek(toTimeZone(e.timestamp, 'UTC'), 0) AS day_start
+        FROM events AS e SAMPLE 1
+        LEFT OUTER JOIN
+          (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
+                  person_distinct_id_overrides.distinct_id AS distinct_id
+           FROM person_distinct_id_overrides
+           WHERE equals(person_distinct_id_overrides.team_id, 99999)
+           GROUP BY person_distinct_id_overrides.distinct_id
+           HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id)
+        WHERE and(equals(e.team_id, 99999), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(toDateTime('2020-01-18 23:59:59', 'UTC'))), equals(e.event, '$pageview'))
+        GROUP BY day_start)
      GROUP BY day_start
      ORDER BY day_start ASC)
   ORDER BY arraySum(total) DESC
@@ -5886,31 +5873,18 @@
     (SELECT sum(total) AS count,
             day_start AS day_start
      FROM
-       (SELECT counts AS total,
-               toStartOfWeek(timestamp, 0) AS day_start
-        FROM
-          (SELECT d.timestamp AS timestamp,
-                  count(DISTINCT e.actor_id) AS counts
-           FROM
-             (SELECT toTimeZone(e.timestamp, 'America/Phoenix') AS timestamp,
-                     if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id) AS actor_id
-              FROM events AS e SAMPLE 1
-              LEFT OUTER JOIN
-                (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
-                        person_distinct_id_overrides.distinct_id AS distinct_id
-                 FROM person_distinct_id_overrides
-                 WHERE equals(person_distinct_id_overrides.team_id, 99999)
-                 GROUP BY person_distinct_id_overrides.distinct_id
-                 HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id)
-              WHERE and(equals(e.team_id, 99999), equals(e.event, '$pageview'), greaterOrEquals(timestamp, minus(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'America/Phoenix')), toIntervalDay(7))), lessOrEquals(timestamp, assumeNotNull(toDateTime('2020-01-18 23:59:59', 'America/Phoenix'))))
-              GROUP BY timestamp, actor_id) AS e
-           CROSS JOIN
-             (SELECT minus(toStartOfWeek(assumeNotNull(toDateTime('2020-01-18 23:59:59', 'America/Phoenix')), 0), toIntervalWeek(numbers.number)) AS timestamp
-              FROM numbers(dateDiff('week', minus(toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'America/Phoenix')), 0), toIntervalDay(7)), assumeNotNull(toDateTime('2020-01-18 23:59:59', 'America/Phoenix')))) AS numbers) AS d
-           WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0))
-           GROUP BY d.timestamp
-           ORDER BY d.timestamp ASC)
-        WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'America/Phoenix')), 0)), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(toDateTime('2020-01-18 23:59:59', 'America/Phoenix'))), 0)))
+       (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total,
+               toStartOfWeek(toTimeZone(e.timestamp, 'America/Phoenix'), 0) AS day_start
+        FROM events AS e SAMPLE 1
+        LEFT OUTER JOIN
+          (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
+                  person_distinct_id_overrides.distinct_id AS distinct_id
+           FROM person_distinct_id_overrides
+           WHERE equals(person_distinct_id_overrides.team_id, 99999)
+           GROUP BY person_distinct_id_overrides.distinct_id
+           HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id)
+        WHERE and(equals(e.team_id, 99999), greaterOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'America/Phoenix')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'America/Phoenix'), assumeNotNull(toDateTime('2020-01-18 23:59:59', 'America/Phoenix'))), equals(e.event, '$pageview'))
+        GROUP BY day_start)
      GROUP BY day_start
      ORDER BY day_start ASC)
   ORDER BY arraySum(total) DESC
@@ -5932,31 +5906,18 @@
     (SELECT sum(total) AS count,
             day_start AS day_start
      FROM
-       (SELECT counts AS total,
-               toStartOfWeek(timestamp, 0) AS day_start
-        FROM
-          (SELECT d.timestamp AS timestamp,
-                  count(DISTINCT e.actor_id) AS counts
-           FROM
-             (SELECT toTimeZone(e.timestamp, 'Asia/Tokyo') AS timestamp,
-                     if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id) AS actor_id
-              FROM events AS e SAMPLE 1
-              LEFT OUTER JOIN
-                (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
-                        person_distinct_id_overrides.distinct_id AS distinct_id
-                 FROM person_distinct_id_overrides
-                 WHERE equals(person_distinct_id_overrides.team_id, 99999)
-                 GROUP BY person_distinct_id_overrides.distinct_id
-                 HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id)
-              WHERE and(equals(e.team_id, 99999), equals(e.event, '$pageview'), greaterOrEquals(timestamp, minus(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'Asia/Tokyo')), toIntervalDay(7))), lessOrEquals(timestamp, assumeNotNull(toDateTime('2020-01-18 23:59:59', 'Asia/Tokyo'))))
-              GROUP BY timestamp, actor_id) AS e
-           CROSS JOIN
-             (SELECT minus(toStartOfWeek(assumeNotNull(toDateTime('2020-01-18 23:59:59', 'Asia/Tokyo')), 0), toIntervalWeek(numbers.number)) AS timestamp
-              FROM numbers(dateDiff('week', minus(toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'Asia/Tokyo')), 0), toIntervalDay(7)), assumeNotNull(toDateTime('2020-01-18 23:59:59', 'Asia/Tokyo')))) AS numbers) AS d
-           WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0))
-           GROUP BY d.timestamp
-           ORDER BY d.timestamp ASC)
-        WHERE and(ifNull(greaterOrEquals(timestamp, toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'Asia/Tokyo')), 0)), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(toDateTime('2020-01-18 23:59:59', 'Asia/Tokyo'))), 0)))
+       (SELECT count(DISTINCT if(not(empty(e__override.distinct_id)), e__override.person_id, e.person_id)) AS total,
+               toStartOfWeek(toTimeZone(e.timestamp, 'Asia/Tokyo'), 0) AS day_start
+        FROM events AS e SAMPLE 1
+        LEFT OUTER JOIN
+          (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
+                  person_distinct_id_overrides.distinct_id AS distinct_id
+           FROM person_distinct_id_overrides
+           WHERE equals(person_distinct_id_overrides.team_id, 99999)
+           GROUP BY person_distinct_id_overrides.distinct_id
+           HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS e__override ON equals(e.distinct_id, e__override.distinct_id)
+        WHERE and(equals(e.team_id, 99999), greaterOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), toStartOfWeek(assumeNotNull(toDateTime('2019-12-29 00:00:00', 'Asia/Tokyo')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'Asia/Tokyo'), assumeNotNull(toDateTime('2020-01-18 23:59:59', 'Asia/Tokyo'))), equals(e.event, '$pageview'))
+        GROUP BY day_start)
      GROUP BY day_start
      ORDER BY day_start ASC)
   ORDER BY arraySum(total) DESC
diff --git a/posthog/hogql_queries/insights/trends/test/test_trends.py b/posthog/hogql_queries/insights/trends/test/test_trends.py
index 83ea93953d66..da9e7924008e 100644
--- a/posthog/hogql_queries/insights/trends/test/test_trends.py
+++ b/posthog/hogql_queries/insights/trends/test/test_trends.py
@@ -6852,6 +6852,12 @@ def test_weekly_active_users_daily_based_on_action(self):
     @also_test_with_different_timezones
     @snapshot_clickhouse_queries
     def test_weekly_active_users_weekly(self):
+        """Test weekly active users with a week interval.
+
+        When using WEEKLY_ACTIVE math with an interval of WEEK or greater,
+        we should treat it like a normal unique users calculation (DAU) rather than
+        the sliding window calculation used for daily intervals.
+        """
         self._create_active_users_events()
 
         data = {
@@ -6871,7 +6877,7 @@ def test_weekly_active_users_weekly(self):
         filter = Filter(team=self.team, data=data)
         result = self._run(filter, self.team)
         self.assertEqual(result[0]["days"], ["2019-12-29", "2020-01-05", "2020-01-12"])
-        self.assertEqual(result[0]["data"], [0.0, 1.0, 3.0])
+        self.assertEqual(result[0]["data"], [1, 2, 1])
 
     @snapshot_clickhouse_queries
     def test_weekly_active_users_hourly(self):
diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py
index 6cb4ca2de3f4..527103bdd22a 100644
--- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py
+++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py
@@ -66,6 +66,7 @@
     also_test_with_materialized_columns,
     flush_persons_and_events,
 )
+from posthog.hogql.query import execute_hogql_query
 
 
 @dataclass
@@ -342,7 +343,7 @@ def _create_test_events_for_groups(self):
             ]
         )
 
-    def _create_query_runner(
+    def _create_trends_query(
         self,
         date_from: str,
         date_to: Optional[str],
@@ -352,13 +353,11 @@ def _create_query_runner(
         breakdown: Optional[BreakdownFilter] = None,
         compare_filters: Optional[CompareFilter] = None,
         filter_test_accounts: Optional[bool] = None,
-        hogql_modifiers: Optional[HogQLQueryModifiers] = None,
-        limit_context: Optional[LimitContext] = None,
         explicit_date: Optional[bool] = None,
         properties: Optional[Any] = None,
-    ) -> TrendsQueryRunner:
+    ) -> TrendsQuery:
         query_series: list[EventsNode | ActionsNode] = [EventsNode(event="$pageview")] if series is None else series
-        query = TrendsQuery(
+        return TrendsQuery(
             dateRange=DateRange(date_from=date_from, date_to=date_to, explicitDate=explicit_date),
             interval=interval,
             series=query_series,
@@ -368,6 +367,34 @@ def _create_query_runner(
             filterTestAccounts=filter_test_accounts,
             properties=properties,
         )
+
+    def _create_query_runner(
+        self,
+        date_from: str,
+        date_to: Optional[str],
+        interval: IntervalType,
+        series: Optional[list[EventsNode | ActionsNode]],
+        trends_filters: Optional[TrendsFilter] = None,
+        breakdown: Optional[BreakdownFilter] = None,
+        compare_filters: Optional[CompareFilter] = None,
+        filter_test_accounts: Optional[bool] = None,
+        hogql_modifiers: Optional[HogQLQueryModifiers] = None,
+        limit_context: Optional[LimitContext] = None,
+        explicit_date: Optional[bool] = None,
+        properties: Optional[Any] = None,
+    ) -> TrendsQueryRunner:
+        query = self._create_trends_query(
+            date_from=date_from,
+            date_to=date_to,
+            interval=interval,
+            series=series,
+            trends_filters=trends_filters,
+            breakdown=breakdown,
+            compare_filters=compare_filters,
+            filter_test_accounts=filter_test_accounts,
+            explicit_date=explicit_date,
+            properties=properties,
+        )
         return TrendsQueryRunner(team=self.team, query=query, modifiers=hogql_modifiers, limit_context=limit_context)
 
     def _run_trends_query(
@@ -1608,6 +1635,49 @@ def test_trends_aggregation_wau(self):
         assert len(response.results) == 1
         assert response.results[0]["data"] == [1, 1, 2, 3, 3, 3, 4, 4, 4, 4, 2, 2]
 
+    def test_trends_aggregation_wau_long_interval(self):
+        """Test weekly active users with a week interval.
+
+        When using WEEKLY_ACTIVE math with an interval of WEEK or greater,
+        we should treat it like a normal unique users calculation (DAU) rather than
+        the sliding window calculation used for daily intervals.
+        """
+        self._create_test_events()
+
+        # First run the regular trends query to verify it works
+        trends_query = self._create_trends_query(
+            "2020-01-09",
+            "2020-01-20",
+            IntervalType.WEEK,
+            [EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)],
+        )
+
+        # Create a query runner and calculate the trends
+        query_runner = TrendsQueryRunner(team=self.team, query=trends_query)
+        response = query_runner.calculate()
+
+        assert response.results[0]["data"] == [2, 4, 1]
+
+        # Now run actors queries for each date range and verify the counts
+        options = query_runner.to_actors_query_options()
+        assert options.day is not None and len(options.day) == 3
+
+        # Expected actor IDs for each week - based on our test event data
+        expected_actors_by_week = [
+            ["p1", "p2"],  # Week 1: Jan 9-15
+            ["p1", "p2", "p3", "p4"],  # Week 2: Jan 16-22
+            ["p1"],  # Week 3: Jan 23-29
+        ]
+
+        for i, day in enumerate(options.day):
+            actors_query = query_runner.to_actors_query(time_frame=day.value, series_index=0)
+            result = execute_hogql_query(query=actors_query, team=self.team)
+
+            actual_actor_ids = [row[2][0] for row in result.results]
+            expected_actor_ids = expected_actors_by_week[i]
+
+            self.assertCountEqual(actual_actor_ids, expected_actor_ids)
+
     def test_trends_aggregation_mau(self):
         self._create_test_events()
 
@@ -1623,6 +1693,20 @@ def test_trends_aggregation_mau(self):
         assert len(response.results) == 1
         assert response.results[0]["data"] == [1, 1, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4]
 
+    def test_trends_aggregation_mau_long_interval(self):
+        self._create_test_events()
+
+        response = self._run_trends_query(
+            "2019-12-31",
+            "2020-02-01",
+            IntervalType.MONTH,
+            [EventsNode(event="$pageview", math=BaseMathType.MONTHLY_ACTIVE)],
+            None,
+            None,
+        )
+
+        assert response.results[0]["data"] == [0, 4, 0]
+
     def test_trends_aggregation_unique(self):
         self._create_test_events()
 
diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py
index d8077ddd857b..a33b12d41d1a 100644
--- a/posthog/hogql_queries/insights/trends/trends_query_runner.py
+++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py
@@ -86,6 +86,14 @@ def __init__(
         modifiers: Optional[HogQLQueryModifiers] = None,
         limit_context: Optional[LimitContext] = None,
     ):
+        from posthog.hogql_queries.insights.utils.utils import convert_active_user_math_based_on_interval
+
+        if isinstance(query, dict):
+            query = TrendsQuery.model_validate(query)
+
+        # Use the new function to handle WAU/MAU conversions
+        query = convert_active_user_math_based_on_interval(query)
+
         super().__init__(query, team=team, timings=timings, modifiers=modifiers, limit_context=limit_context)
         self.update_hogql_modifiers()
         self.series = self.setup_series()
diff --git a/posthog/hogql_queries/insights/utils/utils.py b/posthog/hogql_queries/insights/utils/utils.py
index 15689aba7927..127039cb44e1 100644
--- a/posthog/hogql_queries/insights/utils/utils.py
+++ b/posthog/hogql_queries/insights/utils/utils.py
@@ -1,7 +1,10 @@
+from copy import deepcopy
 from typing import Optional
 from posthog.hogql import ast
+from posthog.hogql_queries.utils.query_date_range import compare_interval_length
 from posthog.models.team.team import Team, WeekStartDay
 from posthog.queries.util import get_trunc_func_ch
+from posthog.schema import IntervalType, BaseMathType, DataWarehouseNode, EventsNode, ActionsNode, TrendsQuery
 
 
 def get_start_of_interval_hogql(interval: str, *, team: Team, source: Optional[ast.Expr] = None) -> ast.Expr:
@@ -15,3 +18,35 @@ def get_start_of_interval_hogql(interval: str, *, team: Team, source: Optional[a
 def get_start_of_interval_hogql_str(interval: str, *, team: Team, source: str) -> str:
     trunc_func = get_trunc_func_ch(interval)
     return f"{trunc_func}({source}{f', {int((WeekStartDay(team.week_start_day or 0)).clickhouse_mode)}' if trunc_func == 'toStartOfWeek' else ''})"
+
+
+def series_should_be_set_to_dau(interval: IntervalType, series: EventsNode | ActionsNode | DataWarehouseNode):
+    return (
+        series.math == BaseMathType.WEEKLY_ACTIVE and compare_interval_length(interval, ">=", IntervalType.WEEK)
+    ) or (series.math == BaseMathType.MONTHLY_ACTIVE and compare_interval_length(interval, ">=", IntervalType.MONTH))
+
+
+def convert_active_user_math_based_on_interval(query: TrendsQuery) -> TrendsQuery:
+    """
+    Convert WAU to DAU for week or longer intervals
+    Convert MAU to DAU for month or longer intervals
+
+    Works for both TrendsQuery and StickinessQuery
+
+    Args:
+        query: Either a TrendsQuery or StickinessQuery instance
+
+    Returns:
+        The same type of query that was passed in, with appropriate math conversions
+    """
+    modified_query = deepcopy(query)
+
+    interval = modified_query.interval or IntervalType.DAY
+
+    for series in modified_query.series:
+        # Convert WAU to DAU for week or longer intervals
+        # Convert MAU to DAU for month or longer intervals
+        if series_should_be_set_to_dau(interval, series):
+            series.math = BaseMathType.DAU
+
+    return modified_query
diff --git a/posthog/schema_helpers.py b/posthog/schema_helpers.py
index 06a3c22c078f..56cf1e33c379 100644
--- a/posthog/schema_helpers.py
+++ b/posthog/schema_helpers.py
@@ -3,6 +3,7 @@
 from pydantic import BaseModel, ValidationError, model_serializer
 from rest_framework.utils.encoders import JSONEncoder
 
+from posthog.hogql_queries.insights.utils.utils import series_should_be_set_to_dau
 from posthog.schema import (
     ChartDisplayType,
     FunnelsQuery,
@@ -11,6 +12,7 @@
     RetentionQuery,
     StickinessQuery,
     TrendsQuery,
+    BaseMathType,
 )
 from posthog.types import InsightQueryNode
 
@@ -48,10 +50,13 @@ def serialize_query(self, next_serializer):
                     # Keep this in sync with the frontend side "cleanInsightQuery" function.
                     if name == "series":
                         # remove frontend-only props from series
-                        dumped["series"] = [
-                            {key: value for key, value in entity.items() if key != "custom_name"}
-                            for entity in dumped["series"]
-                        ]
+                        new_series_list = []
+                        for dumped_series, series in zip(dumped[name], self.series):
+                            new_series = {key: value for key, value in dumped_series.items() if key != "custom_name"}
+                            if isinstance(self, TrendsQuery) and series_should_be_set_to_dau(self.interval, series):
+                                new_series["math"] = BaseMathType.DAU
+                            new_series_list.append(new_series)
+                        dumped["series"] = new_series_list
                     elif name == insightFilterKey:
                         # Remove frontend-only props from insight filters
                         # Keep this in sync with frontend/src/scenes/insights/utils/queryUtils.ts `cleanInsightQuery` method
diff --git a/posthog/test/test_schema_helpers.py b/posthog/test/test_schema_helpers.py
index 38ef8866ebce..9d5e5bb4d96f 100644
--- a/posthog/test/test_schema_helpers.py
+++ b/posthog/test/test_schema_helpers.py
@@ -18,6 +18,7 @@
     BreakdownAttributionType,
     TrendsQuery,
     RetentionQuery,
+    BaseMathType,
 )
 from posthog.schema_helpers import to_dict
 
@@ -235,3 +236,34 @@ def test_serializes_funnels_filter(self, f1, f2, num_keys):
         q2 = FunnelsQuery(**base_funnel, funnelsFilter=f2)
 
         self._assert_filter("funnelsFilter", num_keys, q1, q2)
+
+    def test_series_math_set_to_dau_when_appropriate(self):
+        query_with_day_interval = TrendsQuery(
+            interval="week", series=[EventsNode(name="$pageview", math=BaseMathType.WEEKLY_ACTIVE)]
+        )
+        result = to_dict(query_with_day_interval)
+        self.assertEqual(result["series"][0]["math"], BaseMathType.DAU)
+
+        query_with_day_interval = TrendsQuery(
+            interval="week", series=[EventsNode(name="$pageview", math=BaseMathType.MONTHLY_ACTIVE)]
+        )
+        result = to_dict(query_with_day_interval)
+        self.assertEqual(result["series"][0]["math"], BaseMathType.MONTHLY_ACTIVE)
+
+        query_with_day_interval = TrendsQuery(
+            interval="month", series=[EventsNode(name="$pageview", math=BaseMathType.MONTHLY_ACTIVE)]
+        )
+        result = to_dict(query_with_day_interval)
+        self.assertEqual(result["series"][0]["math"], BaseMathType.DAU)
+
+        # Test case where DAU should NOT be set (assuming 'month' interval doesn't trigger it)
+        query_with_month_interval = TrendsQuery(
+            interval="day", series=[EventsNode(name="$pageview", math=BaseMathType.WEEKLY_ACTIVE)]
+        )
+        result = to_dict(query_with_month_interval)
+        self.assertEqual(result["series"][0]["math"], BaseMathType.WEEKLY_ACTIVE)
+
+        # Test case with existing math that should NOT be overridden
+        query_with_existing_math = TrendsQuery(interval="day", series=[EventsNode(name="$pageview")])
+        result = to_dict(query_with_existing_math)
+        self.assertNotIn("math", result["series"][0])


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR introduces important changes to handle Weekly Active Users (WAU) and Monthly Active Users (MAU) metrics in PostHog's trends analysis:

- Added new `compare_intervals` function in `query_date_range.py` to handle interval comparisons (minute, hour, day, week, month) using array indexing
- Modified `TrendsQueryRunner` to automatically convert WAU to DAU for week+ intervals and MAU to DAU for month+ intervals
- Added test coverage to verify WAU/MAU conversion behavior with different time intervals
- Implemented interval comparison logic using `ORDERED_INTERVALS` constant for consistent ordering

The changes improve the accuracy of user activity metrics by ensuring appropriate aggregation based on the query's time interval.

<sub>3 file(s) reviewed, 1 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog_30059)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:58:51Z
user: login: thmsobrmlr
body: 👍  for functional correctness. 

Wondering if we should inform users about this change in some way though, as this could cause confusion. Could be an email to all users who have such an insight or a frontend side hint on affected insights.

Also should we forbid this configuration frontend side or at least hint that this is problematic?
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T09:37:05Z
user: login: thmsobrmlr
body: 👍  for functional correctness. 

Wondering if we should inform users about this change in some way though, as this could cause confusion. Could be an email to all users who have such an insight or a frontend side hint on affected insights.

Also should we forbid this configuration frontend side or at least hint that this is problematic?
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T09:37:07Z
user: login: aspicer
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T20:19:13Z
user: login: thmsobrmlr
body: Thanks for the follow-up, that looks good!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-20T14:03:28Z

Review comments:
diff_hunk: @@ -17,6 +17,22 @@
 )
 
 IntervalLiteral = Literal["minute", "hour", "day", "week", "month"]
+ORDERED_INTERVALS = ["minute", "hour", "day", "week", "month"]
+
+
+def compare_intervals(
+    interval1: IntervalLiteral, operator: Literal["<", "<=", "=", ">", ">="], interval2: IntervalLiteral
+) -> bool:
+    if operator == "<":
+        return ORDERED_INTERVALS.index(interval1) < ORDERED_INTERVALS.index(interval2)
+    elif operator == "<=":
+        return ORDERED_INTERVALS.index(interval1) <= ORDERED_INTERVALS.index(interval2)
+    elif operator == "=":
+        return ORDERED_INTERVALS.index(interval1) == ORDERED_INTERVALS.index(interval2)
+    elif operator == ">":
+        return ORDERED_INTERVALS.index(interval1) > ORDERED_INTERVALS.index(interval2)
+    elif operator == ">=":
+        return ORDERED_INTERVALS.index(interval1) >= ORDERED_INTERVALS.index(interval2)
path: posthog/hogql_queries/utils/query_date_range.py
user: login: greptile-apps[bot]
body: logic: Missing error handling for invalid interval values. If interval1 or interval2 are not in ORDERED_INTERVALS, this will raise a ValueError silently.

```suggestion
def compare_intervals(
    interval1: IntervalLiteral, operator: Literal["<", "<=", "=", ">", ">="], interval2: IntervalLiteral
) -> bool:
    if interval1 not in ORDERED_INTERVALS or interval2 not in ORDERED_INTERVALS:
        raise ValueError(f"Invalid interval values: {interval1}, {interval2}")

    if operator == "<":
        return ORDERED_INTERVALS.index(interval1) < ORDERED_INTERVALS.index(interval2)
    elif operator == "<=":
        return ORDERED_INTERVALS.index(interval1) <= ORDERED_INTERVALS.index(interval2)
    elif operator == "=":
        return ORDERED_INTERVALS.index(interval1) == ORDERED_INTERVALS.index(interval2)
    elif operator == ">":
        return ORDERED_INTERVALS.index(interval1) > ORDERED_INTERVALS.index(interval2)
    elif operator == ">=":
        return ORDERED_INTERVALS.index(interval1) >= ORDERED_INTERVALS.index(interval2)
```
created_at: 2025-03-18T07:58:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,3 +19,32 @@ def get_start_of_interval_hogql(interval: str, *, team: Team, source: Optional[a
 def get_start_of_interval_hogql_str(interval: str, *, team: Team, source: str) -> str:
     trunc_func = get_trunc_func_ch(interval)
     return f"{trunc_func}({source}{f', {int((WeekStartDay(team.week_start_day or 0)).clickhouse_mode)}' if trunc_func == 'toStartOfWeek' else ''})"
+
+
+def convert_active_user_math_based_on_interval(query: T) -> T:
path: posthog/hogql_queries/insights/utils/utils.py
user: login: thmsobrmlr
body: Not sure if it makes sense in this case, but thought it's worth pointing out that we have a `serialize_query` method in `posthog/schema_helpers.py` thats responsible for transforming queries into a canonical representation. This is useful because then semantically equal queries with different syntax still use the same cache key.

It's a minor optimization, but could have unintended side effects, e.g. updating the query configuration and causing more confusion for the user, so maybe not that useful here.
created_at: 2025-03-19T09:33:58Z
author_association: COLLABORATOR
diff_hunk: @@ -15,3 +19,32 @@ def get_start_of_interval_hogql(interval: str, *, team: Team, source: Optional[a
 def get_start_of_interval_hogql_str(interval: str, *, team: Team, source: str) -> str:
     trunc_func = get_trunc_func_ch(interval)
     return f"{trunc_func}({source}{f', {int((WeekStartDay(team.week_start_day or 0)).clickhouse_mode)}' if trunc_func == 'toStartOfWeek' else ''})"
+
+
+def convert_active_user_math_based_on_interval(query: T) -> T:
path: posthog/hogql_queries/insights/utils/utils.py
user: login: aspicer
body: Thanks - i modified this a bit and added that functionality to both the FE and the BE - do you mind taking a quick look?
created_at: 2025-03-19T20:19:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: posthog-bot
created_at: 2025-03-18T07:57:43Z
body: Hey @aspicer! 👋
This pull request seems to contain no description. Please add useful context, rationale, and/or any other information that will help make sense of this change now and in the distant Mars-based future.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T18:43:00Z
body: **Size Change:** +1.22 kB (+0.01%) 

**Total Size:** 9.75 MB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `frontend/dist/toolbar.js` | 9.75 MB | +1.22 kB (+0.01%) |

<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-18T19:28:44Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 3](https://github.com/PostHog/posthog/pull/30059/commits/df8d71502d585d1e20a817d8ef36a8e54086ff43))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30059/commits/500ff3cba5913c2aa64e14d32edb4e8048c69f2a).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30059/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-18T19:39:10Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 3](https://github.com/PostHog/posthog/pull/30059/commits/9dbb7305723472737f346a13dbe4ee634b84cf26))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30059/commits/fae375bcb544fd3659ec06bab8cd16f6fbf16b77).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30059/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: aspicer
created_at: 2025-03-19T18:12:40Z
body: > 👍 for functional correctness.
> 
> Wondering if we should inform users about this change in some way though, as this could cause confusion. Could be an email to all users who have such an insight or a frontend side hint on affected insights.
> 
> Also should we forbid this configuration frontend side or at least hint that this is problematic?

Thanks. Great suggestion. Digging into this made me realize we had a bug in the code that was stoping these tooltips from appearing for Total Count logic, which warned about the same thing. The bug was also causing these math types to appear in stickiness where it wasn't supported. Fixed those and added a warning!
<img width="908" alt="image" src="https://github.com/user-attachments/assets/b7ddab27-9302-4223-91c9-2ab0e3291d2e" />

author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-19T20:27:39Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30059/commits/7248c2873dd36015c3c85dc67cea58e3ba527b83))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30059/commits/aac25f3a924963e1c6b268cf7171f7e5368454c5).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30059/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-19T20:48:03Z
body: ## 📸 UI snapshots have been updated

**1** snapshot changes in total. **0** added, **1** modified, **0** deleted:

- **`chromium`**: **0** added, **1** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30059/commits/5c5ea0c932322e486bd5d3560c1dfc4846f33da4))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30059/commits/f5eb1cc8b02b17fecefbda792071e5f88a3b6829).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30059/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR