diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png index 08af2a1a21250..2108bf08e480f 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png index ed3abd9942243..e797cc6467cfc 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-admin--light.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png index e741bd882af4f..644dc1a22f0eb 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png index b6f0a48add3c7..750ff00450e15 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-member--light.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png index 08af2a1a21250..2108bf08e480f 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png index ed3abd9942243..e797cc6467cfc 100644 Binary files a/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png and b/frontend/__snapshots__/scenes-other-org-member-invites--current-user-is-owner--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png index 8fa3e256aa1cf..c063bc66591f6 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png b/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png index 46492675ab776..53d8c00a4492e 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-organization--light.png differ diff --git a/frontend/src/lib/api.mock.ts b/frontend/src/lib/api.mock.ts index 8841e60c38110..af80cbfa32322 100644 --- a/frontend/src/lib/api.mock.ts +++ b/frontend/src/lib/api.mock.ts @@ -163,6 +163,7 @@ export const MOCK_DEFAULT_ORGANIZATION_MEMBER: OrganizationMemberType = { updated_at: '2020-09-24T15:05:26.758837Z', is_2fa_enabled: false, has_social_auth: false, + last_login: '2020-09-24T15:05:26.758796Z', } export const MOCK_SECOND_BASIC_USER: UserBasicType = { @@ -181,6 +182,7 @@ export const MOCK_SECOND_ORGANIZATION_MEMBER: OrganizationMemberType = { updated_at: '2021-03-11T19:11:11Z', is_2fa_enabled: false, has_social_auth: false, + last_login: '2020-09-24T15:05:26.758796Z', } export const MOCK_DEFAULT_ORGANIZATION_INVITE: OrganizationInviteType = { diff --git a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx index 9dc79c902c10d..b252a4a7a0757 100644 --- a/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx +++ b/frontend/src/lib/lemon-ui/LemonSwitch/LemonSwitch.tsx @@ -21,6 +21,8 @@ export interface LemonSwitchProps { tooltip?: string | JSX.Element | null handleContent?: React.ReactElement | null 'aria-label'?: string + sliderColorOverrideChecked?: string + sliderColorOverrideUnchecked?: string } /** Counter used for collision-less automatic switch IDs. */ @@ -44,6 +46,8 @@ export const LemonSwitch: React.FunctionComponent -
+
{handleContent}
) diff --git a/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx b/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx index a3fe1e9651635..5c18b2a266aef 100644 --- a/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx +++ b/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx @@ -26,6 +26,7 @@ export const liveEventsTableLogic = kea([ pollStats: true, setStats: (stats) => ({ stats }), showLiveStreamErrorToast: true, + addEventHost: (eventHost) => ({ eventHost }), })), reducers({ events: [ @@ -83,6 +84,17 @@ export const liveEventsTableLogic = kea([ }, }, ], + eventHosts: [ + [] as string[], + { + addEventHost: (state, { eventHost }) => { + if (!state.includes(eventHost)) { + return [...state, eventHost] + } + return state + }, + }, + ], }), selectors(({ selectors }) => ({ eventCount: [() => [selectors.events], (events: any) => events.length], @@ -177,6 +189,17 @@ export const liveEventsTableLogic = kea([ console.error('Failed to poll stats:', error) } }, + addEvents: ({ events }) => { + if (events.length > 0) { + const event = events[0] + const eventUrl = event.properties?.$current_url + if (eventUrl) { + const eventHost = new URL(eventUrl).host + const eventProtocol = new URL(eventUrl).protocol + actions.addEventHost(`${eventProtocol}//${eventHost}`) + } + } + }, })), events(({ actions, cache }) => ({ afterMount: () => { diff --git a/frontend/src/scenes/billing/billingLogic.tsx b/frontend/src/scenes/billing/billingLogic.tsx index c3e86255cdadb..332cb83c06a1e 100644 --- a/frontend/src/scenes/billing/billingLogic.tsx +++ b/frontend/src/scenes/billing/billingLogic.tsx @@ -463,6 +463,7 @@ export const billingLogic = kea([ values.computedDiscount * 100, await api.create('api/billing/credits/purchase', { annual_amount_usd: +Math.round(+creditInput - +creditInput * values.creditDiscount), + discount_percent: values.computedDiscount * 100, collection_method: collectionMethod, }) @@ -502,8 +503,8 @@ export const billingLogic = kea([ errors: ({ creditInput, collectionMethod }) => ({ creditInput: !creditInput ? 'Please enter the amount of credits you want to purchase' - : // This value is used because 6666 - 10% = 6000 - +creditInput < 6666 + : // This value is used because 6667 - 10% = 6000 + +creditInput < 6667 ? 'Please enter a credit amount greater than $6,666' : undefined, collectionMethod: !collectionMethod ? 'Please select a collection method' : undefined, diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index 37c6bc42cdb7f..46427d3f4e0ee 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -109,6 +109,9 @@ const ProductAnalyticsOnboarding = (): JSX.Element => { // not sure if there is a better way to do this useValues(newDashboardLogic) + const showTemplateSteps = + featureFlags[FEATURE_FLAGS.ONBOARDING_DASHBOARD_TEMPLATES] == 'test' && window.innerWidth > 1000 + const options: ProductConfigOption[] = [ { title: 'Autocapture frontend interactions', @@ -165,10 +168,12 @@ const ProductAnalyticsOnboarding = (): JSX.Element => { stepKey={OnboardingStepKey.INSTALL} /> - {featureFlags[FEATURE_FLAGS.ONBOARDING_DASHBOARD_TEMPLATES] == 'test' ? ( + + {/* this is two conditionals because they need to be direct children of the wrapper */} + {showTemplateSteps ? ( ) : null} - {featureFlags[FEATURE_FLAGS.ONBOARDING_DASHBOARD_TEMPLATES] == 'test' ? ( + {showTemplateSteps ? ( ) : null} diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index 3446001b5f37f..19c29f133ad0c 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -2,6 +2,7 @@ import { actions, connect, kea, listeners, path, props, reducers, selectors } fr import { actionToUrl, router, urlToAction } from 'kea-router' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { liveEventsTableLogic } from 'scenes/activity/live/liveEventsTableLogic' import { billingLogic } from 'scenes/billing/billingLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { Scene } from 'scenes/sceneTypes' @@ -110,6 +111,8 @@ export const getProductUri = (productKey: ProductKey): string => { export const onboardingLogic = kea([ props({} as OnboardingLogicProps), path(['scenes', 'onboarding', 'onboardingLogic']), + // connect this so we start collecting live events the whole time during onboarding + connect(liveEventsTableLogic), connect({ values: [ billingLogic, diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx index 38f67a46b7fee..483487e985b4e 100644 --- a/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx +++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/DashboardTemplateConfigureStep.tsx @@ -17,6 +17,7 @@ import { iframedToolbarBrowserLogic } from 'lib/components/IframedToolbarBrowser import { useEffect, useRef, useState } from 'react' import { dashboardTemplateVariablesLogic } from 'scenes/dashboard/dashboardTemplateVariablesLogic' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic' import { OnboardingStep } from '../OnboardingStep' @@ -31,7 +32,7 @@ const UrlInput = ({ iframeRef }: { iframeRef: React.RefObject const { browserUrl, currentPath } = useValues( iframedToolbarBrowserLogic({ iframeRef, clearBrowserUrlOnUnmount: true }) ) - const { snippetHosts } = useValues(sdksLogic) + const { combinedSnippetAndLiveEventsHosts } = useValues(sdksLogic) const { addUrl } = useActions(authorizedUrlListLogic({ actionId: null, type: AuthorizedUrlListType.TOOLBAR_URLS })) const [inputValue, setInputValue] = useState(currentPath) @@ -56,7 +57,7 @@ const UrlInput = ({ iframeRef }: { iframeRef: React.RefObject ({ key: host, label: host }))} + options={combinedSnippetAndLiveEventsHosts.map((host) => ({ key: host, label: host }))} allowCustomValues={false} onChange={(v) => { addUrl(v[0]) @@ -86,7 +87,9 @@ const UrlInput = ({ iframeRef }: { iframeRef: React.RefObject export const SiteChooser = (): JSX.Element => { const iframeRef = useRef(null) - const { snippetHosts, hasSnippetEventsLoading } = useValues(sdksLogic) + const { combinedSnippetAndLiveEventsHosts, hasSnippetEventsLoading } = useValues(sdksLogic) + const { setStepKey } = useActions(onboardingLogic) + const { isCloud } = useValues(preflightLogic) const { setProposedBrowserUrl } = useActions( iframedToolbarBrowserLogic({ iframeRef, @@ -101,7 +104,6 @@ export const SiteChooser = (): JSX.Element => { automaticallyAuthorizeBrowserUrl: true, }) ) - const { setStepKey } = useActions(onboardingLogic) return ( <> @@ -122,15 +124,21 @@ export const SiteChooser = (): JSX.Element => {

Select where you want to track events from.

{hasSnippetEventsLoading ? ( - ) : snippetHosts.length > 0 ? ( + ) : combinedSnippetAndLiveEventsHosts.length > 0 ? ( <>

- Not seeing the site you want?{' '} - setStepKey(OnboardingStepKey.INSTALL)}>Install posthog-js or + Not seeing the site you want? Try clikcing around on your site to trigger a few events. + If you haven't yet,{' '} + setStepKey(OnboardingStepKey.INSTALL)}>install posthog-js or the HTML snippet wherever you want to track events, then come back here.

+ {isCloud && ( +

+ Note: Sites must be served over HTTPS to be selected. +

+ )}
- {snippetHosts.map((host) => ( + {combinedSnippetAndLiveEventsHosts.concat('https://posthog.com').map((host) => ( ) : (
-
+
{browserUrl && iframeBanner?.level != 'error' ? (
-
+
diff --git a/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts b/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts index bb1080cd0c41a..dcc1d4ac6e0f3 100644 --- a/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts +++ b/frontend/src/scenes/onboarding/productAnalyticsSteps/onboardingTemplateConfigLogic.ts @@ -6,6 +6,7 @@ import { dashboardTemplateVariablesLogic } from 'scenes/dashboard/dashboardTempl import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' import { urls } from 'scenes/urls' +import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic' import { DashboardTemplateType, DashboardType } from '~/types' import { onboardingLogic, OnboardingStepKey } from '../onboardingLogic' @@ -30,6 +31,8 @@ export const onboardingTemplateConfigLogic = kea([ path(['scenes', 'onboarding', 'sdks', 'sdksLogic']), connect({ - values: [onboardingLogic, ['productKey']], + values: [onboardingLogic, ['productKey'], liveEventsTableLogic, ['eventHosts']], }), actions({ setSourceFilter: (sourceFilter: string | null) => ({ sourceFilter }), @@ -118,6 +119,20 @@ export const sdksLogic = kea([ return Object.keys(availableSDKInstructionsMap).length > 5 && sourceOptions.length > 2 }, ], + combinedSnippetAndLiveEventsHosts: [ + (selectors) => [selectors.snippetHosts, selectors.eventHosts], + (snippetHosts: string[], eventHosts: string[]): string[] => { + const combinedSnippetAndLiveEventsHosts = snippetHosts + for (const host of eventHosts) { + const hostProtocol = new URL(host).protocol + const currentProtocol = window.location.protocol + if (hostProtocol === currentProtocol && !combinedSnippetAndLiveEventsHosts.includes(host)) { + combinedSnippetAndLiveEventsHosts.push(host) + } + } + return combinedSnippetAndLiveEventsHosts + }, + ], }), loaders(({ actions }) => ({ hasSnippetEvents: [ diff --git a/frontend/src/scenes/settings/organization/Members.tsx b/frontend/src/scenes/settings/organization/Members.tsx index bd477021498ff..5f6aab35113f8 100644 --- a/frontend/src/scenes/settings/organization/Members.tsx +++ b/frontend/src/scenes/settings/organization/Members.tsx @@ -258,6 +258,19 @@ export function Members(): JSX.Element | null { }, sorter: (a, b) => a.joined_at.localeCompare(b.joined_at), }, + { + title: 'Last Logged In', + dataIndex: 'last_login', + key: 'last_login', + render: function RenderLastLogin(lastLogin) { + return ( +
+ {lastLogin ? : 'Never'} +
+ ) + }, + sorter: (a, b) => new Date(a.last_login ?? 0).getTime() - new Date(b.last_login ?? 0).getTime(), + }, { key: 'actions', width: 0, diff --git a/frontend/src/toolbar/actions/ActionAttribute.tsx b/frontend/src/toolbar/actions/ActionAttribute.tsx index ca74a79363c07..9e264edd4d753 100644 --- a/frontend/src/toolbar/actions/ActionAttribute.tsx +++ b/frontend/src/toolbar/actions/ActionAttribute.tsx @@ -58,15 +58,19 @@ export function ActionAttribute({ return (
{automaticActionCreationEnabled && ( - - checked - ? addAutomaticCreationIncludedPropertyKey(attribute) - : removeAutomaticCreationIncludedPropertyKey(attribute) - } - /> + <> + + checked + ? addAutomaticCreationIncludedPropertyKey(attribute) + : removeAutomaticCreationIncludedPropertyKey(attribute) + } + sliderColorOverrideChecked="primary-3000-light" + sliderColorOverrideUnchecked="muted-3000-light" + /> + )}
{icon}
{text}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 8b6f1111c9d7e..b922e192c41f9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -375,6 +375,7 @@ export interface OrganizationDomainType { export interface BaseMemberType { id: string user: UserBasicType + last_login: string | null joined_at: string updated_at: string is_2fa_enabled: boolean diff --git a/posthog/api/organization_member.py b/posthog/api/organization_member.py index 84a54276e875b..aaf1d5b802776 100644 --- a/posthog/api/organization_member.py +++ b/posthog/api/organization_member.py @@ -1,6 +1,6 @@ from typing import cast -from django.db.models import Model, Prefetch, QuerySet +from django.db.models import Model, Prefetch, QuerySet, F from django.shortcuts import get_object_or_404 from django.views import View from django_otp.plugins.otp_totp.models import TOTPDevice @@ -42,6 +42,7 @@ class OrganizationMemberSerializer(serializers.ModelSerializer): user = UserBasicSerializer(read_only=True) is_2fa_enabled = serializers.SerializerMethodField() has_social_auth = serializers.SerializerMethodField() + last_login = serializers.DateTimeField(read_only=True) class Meta: model = OrganizationMembership @@ -53,6 +54,7 @@ class Meta: "updated_at", "is_2fa_enabled", "has_social_auth", + "last_login", ] read_only_fields = ["id", "joined_at", "updated_at"] @@ -107,6 +109,7 @@ class OrganizationMemberViewSet( ), Prefetch("user__social_auth", queryset=UserSocialAuth.objects.all()), ) + .annotate(last_login=F("user__last_login")) ) lookup_field = "user__uuid" diff --git a/posthog/api/test/test_organization_members.py b/posthog/api/test/test_organization_members.py index c122e41794ac7..d02653c2dceab 100644 --- a/posthog/api/test/test_organization_members.py +++ b/posthog/api/test/test_organization_members.py @@ -102,6 +102,7 @@ def test_change_organization_member_level(self, mock_update_billing_organization response_data = response.json() response_data.pop("joined_at") response_data.pop("updated_at") + response_data.pop("last_login") self.assertDictEqual( response_data, { diff --git a/posthog/hogql_queries/experiment_funnel_query_runner.py b/posthog/hogql_queries/experiment_funnel_query_runner.py index 36bdc42f0baf5..7931ce23f7fe2 100644 --- a/posthog/hogql_queries/experiment_funnel_query_runner.py +++ b/posthog/hogql_queries/experiment_funnel_query_runner.py @@ -6,8 +6,12 @@ ExperimentFunnelQuery, ExperimentFunnelQueryResponse, ExperimentVariantFunnelResult, + FunnelsQuery, + InsightDateRange, + BreakdownFilter, ) from typing import Any +from zoneinfo import ZoneInfo class ExperimentFunnelQueryRunner(QueryRunner): @@ -17,9 +21,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.experiment = Experiment.objects.get(id=self.query.experiment_id) self.feature_flag = self.experiment.feature_flag - + self.prepared_funnel_query = self._prepare_funnel_query() self.query_runner = FunnelsQueryRunner( - query=self.query.source, team=self.team, timings=self.timings, limit_context=self.limit_context + query=self.prepared_funnel_query, team=self.team, timings=self.timings, limit_context=self.limit_context ) def calculate(self) -> ExperimentFunnelQueryResponse: @@ -27,6 +31,41 @@ def calculate(self) -> ExperimentFunnelQueryResponse: results = self._process_results(response.results) return ExperimentFunnelQueryResponse(insight="FUNNELS", results=results) + def _prepare_funnel_query(self) -> FunnelsQuery: + """ + This method takes the raw funnel query and adapts it + for the needs of experiment analysis: + + 1. Set the date range to match the experiment's duration, using the project's timezone. + 2. Configure the breakdown to use the feature flag key, which allows us + to separate results for different experiment variants. + """ + # Clone the source query + prepared_funnel_query = FunnelsQuery(**self.query.source.model_dump()) + + # Set the date range to match the experiment's duration, using the project's timezone + if self.team.timezone: + tz = ZoneInfo(self.team.timezone) + start_date = self.experiment.start_date.astimezone(tz) if self.experiment.start_date else None + end_date = self.experiment.end_date.astimezone(tz) if self.experiment.end_date else None + else: + start_date = self.experiment.start_date + end_date = self.experiment.end_date + + prepared_funnel_query.dateRange = InsightDateRange( + date_from=start_date.isoformat() if start_date else None, + date_to=end_date.isoformat() if end_date else None, + explicitDate=True, + ) + + # Configure the breakdown to use the feature flag key + prepared_funnel_query.breakdownFilter = BreakdownFilter( + breakdown=f"$feature/{self.feature_flag.key}", + breakdown_type="event", + ) + + return prepared_funnel_query + def _process_results(self, funnels_results: list[list[dict[str, Any]]]) -> dict[str, ExperimentVariantFunnelResult]: variants = self.feature_flag.variants processed_results = { diff --git a/posthog/hogql_queries/test/test_experiment_funnel_query_runner.py b/posthog/hogql_queries/test/test_experiment_funnel_query_runner.py index da9d14fb511be..8d8a9be6a9fd7 100644 --- a/posthog/hogql_queries/test/test_experiment_funnel_query_runner.py +++ b/posthog/hogql_queries/test/test_experiment_funnel_query_runner.py @@ -2,7 +2,6 @@ from posthog.models.experiment import Experiment from posthog.models.feature_flag.feature_flag import FeatureFlag from posthog.schema import ( - BreakdownFilter, EventsNode, ExperimentFunnelQuery, ExperimentFunnelQueryResponse, @@ -11,9 +10,12 @@ from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_event, _create_person, flush_persons_and_events from freezegun import freeze_time from typing import cast +from django.utils import timezone +from datetime import timedelta class TestExperimentFunnelQueryRunner(ClickhouseTestMixin, APIBaseTest): + @freeze_time("2020-01-01T12:00:00Z") def test_query_runner(self): feature_flag = FeatureFlag.objects.create( name="Test experiment flag", @@ -38,10 +40,13 @@ def test_query_runner(self): }, created_by=self.user, ) + experiment = Experiment.objects.create( name="test-experiment", team=self.team, feature_flag=feature_flag, + start_date=timezone.now(), + end_date=timezone.now() + timedelta(days=14), ) feature_flag_property = f"$feature/{feature_flag.key}" @@ -49,7 +54,6 @@ def test_query_runner(self): funnels_query = FunnelsQuery( series=[EventsNode(event="$pageview"), EventsNode(event="purchase")], dateRange={"date_from": "2020-01-01", "date_to": "2020-01-14"}, - breakdownFilter=BreakdownFilter(breakdown=feature_flag_property), ) experiment_query = ExperimentFunnelQuery( experiment_id=experiment.id, @@ -60,25 +64,24 @@ def test_query_runner(self): experiment.metrics = [{"type": "primary", "query": experiment_query.model_dump()}] experiment.save() - with freeze_time("2020-01-10 12:00:00"): - for variant, purchase_count in [("control", 6), ("test", 8)]: - for i in range(10): - _create_person(distinct_ids=[f"user_{variant}_{i}"], team_id=self.team.pk) + for variant, purchase_count in [("control", 6), ("test", 8)]: + for i in range(10): + _create_person(distinct_ids=[f"user_{variant}_{i}"], team_id=self.team.pk) + _create_event( + team=self.team, + event="$pageview", + distinct_id=f"user_{variant}_{i}", + timestamp="2020-01-02T12:00:00Z", + properties={feature_flag_property: variant}, + ) + if i < purchase_count: _create_event( team=self.team, - event="$pageview", + event="purchase", distinct_id=f"user_{variant}_{i}", - timestamp="2020-01-02T12:00:00Z", + timestamp="2020-01-02T12:01:00Z", properties={feature_flag_property: variant}, ) - if i < purchase_count: - _create_event( - team=self.team, - event="purchase", - distinct_id=f"user_{variant}_{i}", - timestamp="2020-01-02T12:01:00Z", - properties={feature_flag_property: variant}, - ) flush_persons_and_events()