mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 00:47:50 +01:00
feat: only send product_intent events if not activated (#25889)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
003ac8a029
commit
54efcdcbc0
@ -143,6 +143,9 @@ export const teamLogic = kea<teamLogicType>([
|
||||
return await api.create(`api/projects/${values.currentProject.id}/environments/`, { name, is_demo })
|
||||
},
|
||||
resetToken: async () => await api.update(`api/environments/${values.currentTeamId}/reset_token`, {}),
|
||||
/**
|
||||
* If adding a product intent that also represents regular product usage, see explainer in posthog.models.product_intent.product_intent.py.
|
||||
*/
|
||||
addProductIntent: async ({
|
||||
product_type,
|
||||
intent_context,
|
||||
|
@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name
|
||||
ee: 0016_rolemembership_organization_member
|
||||
otp_static: 0002_throttling
|
||||
otp_totp: 0002_auto_20190420_0723
|
||||
posthog: 0505_grouptypemapping_project
|
||||
posthog: 0506_productintent_activated_at_and_more
|
||||
sessions: 0001_initial
|
||||
social_django: 0010_uid_db_index
|
||||
two_factor: 0007_auto_20201201_1019
|
||||
|
@ -30,9 +30,12 @@ from posthog.models.activity_logging.activity_page import activity_page_response
|
||||
from posthog.models.async_deletion import AsyncDeletion, DeletionType
|
||||
from posthog.models.group_type_mapping import GroupTypeMapping
|
||||
from posthog.models.organization import OrganizationMembership
|
||||
from posthog.models.scopes import APIScopeObjectOrNotSupported
|
||||
from posthog.models.product_intent.product_intent import ProductIntent
|
||||
from posthog.models.product_intent.product_intent import (
|
||||
ProductIntent,
|
||||
calculate_product_activation,
|
||||
)
|
||||
from posthog.models.project import Project
|
||||
from posthog.models.scopes import APIScopeObjectOrNotSupported
|
||||
from posthog.models.signals import mute_selected_signals
|
||||
from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data
|
||||
from posthog.models.utils import UUIDT
|
||||
@ -199,6 +202,7 @@ class ProjectBackwardCompatSerializer(ProjectBackwardCompatBasicSerializer, User
|
||||
def get_product_intents(self, obj):
|
||||
project = obj
|
||||
team = project.passthrough_team
|
||||
calculate_product_activation.delay(team.id, only_calc_if_days_since_last_checked=1)
|
||||
return ProductIntent.objects.filter(team=team).values(
|
||||
"product_type", "created_at", "onboarding_completed_at", "updated_at"
|
||||
)
|
||||
@ -575,10 +579,12 @@ class ProjectViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
|
||||
|
||||
product_intent, created = ProductIntent.objects.get_or_create(team=team, product_type=product_type)
|
||||
if not created:
|
||||
if not product_intent.activated_at:
|
||||
product_intent.check_and_update_activation()
|
||||
product_intent.updated_at = datetime.now(tz=UTC)
|
||||
product_intent.save()
|
||||
|
||||
if isinstance(user, User):
|
||||
if isinstance(user, User) and not product_intent.activated_at:
|
||||
report_user_action(
|
||||
user,
|
||||
"user showed product intent",
|
||||
|
@ -28,8 +28,9 @@ from posthog.models.activity_logging.activity_page import activity_page_response
|
||||
from posthog.models.async_deletion import AsyncDeletion, DeletionType
|
||||
from posthog.models.group_type_mapping import GroupTypeMapping
|
||||
from posthog.models.organization import OrganizationMembership
|
||||
from posthog.models.scopes import APIScopeObjectOrNotSupported
|
||||
from posthog.models.product_intent.product_intent import calculate_product_activation
|
||||
from posthog.models.project import Project
|
||||
from posthog.models.scopes import APIScopeObjectOrNotSupported
|
||||
from posthog.models.signals import mute_selected_signals
|
||||
from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data
|
||||
from posthog.models.utils import UUIDT
|
||||
@ -217,6 +218,7 @@ class TeamSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin
|
||||
)
|
||||
|
||||
def get_product_intents(self, obj):
|
||||
calculate_product_activation.delay(obj.id, only_calc_if_days_since_last_checked=1)
|
||||
return ProductIntent.objects.filter(team=obj).values(
|
||||
"product_type", "created_at", "onboarding_completed_at", "updated_at"
|
||||
)
|
||||
@ -587,10 +589,12 @@ class TeamViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
|
||||
|
||||
product_intent, created = ProductIntent.objects.get_or_create(team=team, product_type=product_type)
|
||||
if not created:
|
||||
if not product_intent.activated_at:
|
||||
product_intent.check_and_update_activation()
|
||||
product_intent.updated_at = datetime.now(tz=UTC)
|
||||
product_intent.save()
|
||||
|
||||
if isinstance(user, User):
|
||||
if isinstance(user, User) and not product_intent.activated_at:
|
||||
report_user_action(
|
||||
user,
|
||||
"user showed product intent",
|
||||
|
@ -64,6 +64,30 @@
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10
|
||||
'''
|
||||
SELECT "posthog_productintent"."id",
|
||||
"posthog_productintent"."team_id",
|
||||
"posthog_productintent"."created_at",
|
||||
"posthog_productintent"."updated_at",
|
||||
"posthog_productintent"."product_type",
|
||||
"posthog_productintent"."onboarding_completed_at",
|
||||
"posthog_productintent"."activated_at",
|
||||
"posthog_productintent"."activation_last_checked_at"
|
||||
FROM "posthog_productintent"
|
||||
WHERE "posthog_productintent"."team_id" = 99999
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11
|
||||
'''
|
||||
SELECT "posthog_productintent"."product_type",
|
||||
"posthog_productintent"."created_at",
|
||||
"posthog_productintent"."onboarding_completed_at",
|
||||
"posthog_productintent"."updated_at"
|
||||
FROM "posthog_productintent"
|
||||
WHERE "posthog_productintent"."team_id" = 99999
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.12
|
||||
'''
|
||||
SELECT "posthog_user"."id",
|
||||
"posthog_user"."password",
|
||||
@ -95,7 +119,7 @@
|
||||
LIMIT 21
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.13
|
||||
'''
|
||||
SELECT "posthog_featureflag"."id",
|
||||
"posthog_featureflag"."key",
|
||||
@ -118,7 +142,7 @@
|
||||
AND "posthog_featureflag"."team_id" = 99999)
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.12
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.14
|
||||
'''
|
||||
SELECT "posthog_pluginconfig"."id",
|
||||
"posthog_pluginconfig"."web_token",
|
||||
@ -134,74 +158,6 @@
|
||||
AND "posthog_pluginconfig"."team_id" = 99999)
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.13
|
||||
'''
|
||||
SELECT "posthog_team"."id",
|
||||
"posthog_team"."uuid",
|
||||
"posthog_team"."organization_id",
|
||||
"posthog_team"."project_id",
|
||||
"posthog_team"."api_token",
|
||||
"posthog_team"."app_urls",
|
||||
"posthog_team"."name",
|
||||
"posthog_team"."slack_incoming_webhook",
|
||||
"posthog_team"."created_at",
|
||||
"posthog_team"."updated_at",
|
||||
"posthog_team"."anonymize_ips",
|
||||
"posthog_team"."completed_snippet_onboarding",
|
||||
"posthog_team"."has_completed_onboarding_for",
|
||||
"posthog_team"."ingested_event",
|
||||
"posthog_team"."autocapture_opt_out",
|
||||
"posthog_team"."autocapture_web_vitals_opt_in",
|
||||
"posthog_team"."autocapture_web_vitals_allowed_metrics",
|
||||
"posthog_team"."autocapture_exceptions_opt_in",
|
||||
"posthog_team"."autocapture_exceptions_errors_to_ignore",
|
||||
"posthog_team"."session_recording_opt_in",
|
||||
"posthog_team"."session_recording_sample_rate",
|
||||
"posthog_team"."session_recording_minimum_duration_milliseconds",
|
||||
"posthog_team"."session_recording_linked_flag",
|
||||
"posthog_team"."session_recording_network_payload_capture_config",
|
||||
"posthog_team"."session_replay_config",
|
||||
"posthog_team"."survey_config",
|
||||
"posthog_team"."capture_console_log_opt_in",
|
||||
"posthog_team"."capture_performance_opt_in",
|
||||
"posthog_team"."surveys_opt_in",
|
||||
"posthog_team"."heatmaps_opt_in",
|
||||
"posthog_team"."session_recording_version",
|
||||
"posthog_team"."signup_token",
|
||||
"posthog_team"."is_demo",
|
||||
"posthog_team"."access_control",
|
||||
"posthog_team"."week_start_day",
|
||||
"posthog_team"."inject_web_apps",
|
||||
"posthog_team"."test_account_filters",
|
||||
"posthog_team"."test_account_filters_default_checked",
|
||||
"posthog_team"."path_cleaning_filters",
|
||||
"posthog_team"."timezone",
|
||||
"posthog_team"."data_attributes",
|
||||
"posthog_team"."person_display_name_properties",
|
||||
"posthog_team"."live_events_columns",
|
||||
"posthog_team"."recording_domains",
|
||||
"posthog_team"."primary_dashboard_id",
|
||||
"posthog_team"."extra_settings",
|
||||
"posthog_team"."modifiers",
|
||||
"posthog_team"."correlation_config",
|
||||
"posthog_team"."session_recording_retention_period_days",
|
||||
"posthog_team"."external_data_workspace_id",
|
||||
"posthog_team"."external_data_workspace_last_synced_at"
|
||||
FROM "posthog_team"
|
||||
WHERE ("posthog_team"."project_id" = 2
|
||||
AND "posthog_team"."id" = 2)
|
||||
LIMIT 21
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.14
|
||||
'''
|
||||
SELECT "posthog_productintent"."product_type",
|
||||
"posthog_productintent"."created_at",
|
||||
"posthog_productintent"."onboarding_completed_at"
|
||||
FROM "posthog_productintent"
|
||||
WHERE "posthog_productintent"."team_id" = 2
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.15
|
||||
'''
|
||||
SELECT "posthog_user"."id",
|
||||
@ -523,12 +479,64 @@
|
||||
# ---
|
||||
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9
|
||||
'''
|
||||
SELECT "posthog_productintent"."product_type",
|
||||
"posthog_productintent"."created_at",
|
||||
"posthog_productintent"."onboarding_completed_at",
|
||||
"posthog_productintent"."updated_at"
|
||||
FROM "posthog_productintent"
|
||||
WHERE "posthog_productintent"."team_id" = 99999
|
||||
SELECT "posthog_team"."id",
|
||||
"posthog_team"."uuid",
|
||||
"posthog_team"."organization_id",
|
||||
"posthog_team"."project_id",
|
||||
"posthog_team"."api_token",
|
||||
"posthog_team"."app_urls",
|
||||
"posthog_team"."name",
|
||||
"posthog_team"."slack_incoming_webhook",
|
||||
"posthog_team"."created_at",
|
||||
"posthog_team"."updated_at",
|
||||
"posthog_team"."anonymize_ips",
|
||||
"posthog_team"."completed_snippet_onboarding",
|
||||
"posthog_team"."has_completed_onboarding_for",
|
||||
"posthog_team"."ingested_event",
|
||||
"posthog_team"."autocapture_opt_out",
|
||||
"posthog_team"."autocapture_web_vitals_opt_in",
|
||||
"posthog_team"."autocapture_web_vitals_allowed_metrics",
|
||||
"posthog_team"."autocapture_exceptions_opt_in",
|
||||
"posthog_team"."autocapture_exceptions_errors_to_ignore",
|
||||
"posthog_team"."person_processing_opt_out",
|
||||
"posthog_team"."session_recording_opt_in",
|
||||
"posthog_team"."session_recording_sample_rate",
|
||||
"posthog_team"."session_recording_minimum_duration_milliseconds",
|
||||
"posthog_team"."session_recording_linked_flag",
|
||||
"posthog_team"."session_recording_network_payload_capture_config",
|
||||
"posthog_team"."session_recording_url_trigger_config",
|
||||
"posthog_team"."session_recording_url_blocklist_config",
|
||||
"posthog_team"."session_replay_config",
|
||||
"posthog_team"."survey_config",
|
||||
"posthog_team"."capture_console_log_opt_in",
|
||||
"posthog_team"."capture_performance_opt_in",
|
||||
"posthog_team"."capture_dead_clicks",
|
||||
"posthog_team"."surveys_opt_in",
|
||||
"posthog_team"."heatmaps_opt_in",
|
||||
"posthog_team"."session_recording_version",
|
||||
"posthog_team"."signup_token",
|
||||
"posthog_team"."is_demo",
|
||||
"posthog_team"."access_control",
|
||||
"posthog_team"."week_start_day",
|
||||
"posthog_team"."inject_web_apps",
|
||||
"posthog_team"."test_account_filters",
|
||||
"posthog_team"."test_account_filters_default_checked",
|
||||
"posthog_team"."path_cleaning_filters",
|
||||
"posthog_team"."timezone",
|
||||
"posthog_team"."data_attributes",
|
||||
"posthog_team"."person_display_name_properties",
|
||||
"posthog_team"."live_events_columns",
|
||||
"posthog_team"."recording_domains",
|
||||
"posthog_team"."primary_dashboard_id",
|
||||
"posthog_team"."extra_settings",
|
||||
"posthog_team"."modifiers",
|
||||
"posthog_team"."correlation_config",
|
||||
"posthog_team"."session_recording_retention_period_days",
|
||||
"posthog_team"."external_data_workspace_id",
|
||||
"posthog_team"."external_data_workspace_last_synced_at"
|
||||
FROM "posthog_team"
|
||||
WHERE "posthog_team"."id" = 99999
|
||||
LIMIT 21
|
||||
'''
|
||||
# ---
|
||||
# name: TestDecide.test_flag_with_behavioural_cohorts
|
||||
|
@ -1051,6 +1051,76 @@ def team_api_test_factory():
|
||||
team=self.team,
|
||||
)
|
||||
|
||||
@patch("posthog.api.team.calculate_product_activation.delay", MagicMock())
|
||||
@patch("posthog.models.product_intent.ProductIntent.check_and_update_activation")
|
||||
@patch("posthog.api.project.report_user_action")
|
||||
@patch("posthog.api.team.report_user_action")
|
||||
@freeze_time("2024-01-01T00:00:00Z")
|
||||
def test_can_update_product_intent_if_already_exists(
|
||||
self,
|
||||
mock_report_user_action: MagicMock,
|
||||
mock_report_user_action_legacy_endpoint: MagicMock,
|
||||
mock_check_and_update_activation: MagicMock,
|
||||
) -> None:
|
||||
intent = ProductIntent.objects.create(team=self.team, product_type="product_analytics")
|
||||
original_created_at = intent.created_at
|
||||
assert original_created_at == datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
# change the time of the existing intent
|
||||
with freeze_time("2024-01-02T00:00:00Z"):
|
||||
if self.client_class is EnvironmentToProjectRewriteClient:
|
||||
mock_report_user_action = mock_report_user_action_legacy_endpoint
|
||||
response = self.client.patch(
|
||||
f"/api/environments/{self.team.id}/add_product_intent/",
|
||||
{"product_type": "product_analytics"},
|
||||
headers={"Referer": "https://posthogtest.com/my-url", "X-Posthog-Session-Id": "test_session_id"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
product_intent = ProductIntent.objects.get(team=self.team, product_type="product_analytics")
|
||||
assert product_intent.updated_at == datetime(2024, 1, 2, 0, 0, 0, tzinfo=UTC)
|
||||
assert product_intent.created_at == original_created_at
|
||||
mock_check_and_update_activation.assert_called_once()
|
||||
mock_report_user_action.assert_called_once_with(
|
||||
self.user,
|
||||
"user showed product intent",
|
||||
{
|
||||
"product_key": "product_analytics",
|
||||
"$current_url": "https://posthogtest.com/my-url",
|
||||
"$session_id": "test_session_id",
|
||||
"intent_context": None,
|
||||
"$set_once": {"first_onboarding_product_selected": "product_analytics"},
|
||||
"is_first_intent_for_product": False,
|
||||
"intent_created_at": datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC),
|
||||
"intent_updated_at": datetime(2024, 1, 2, 0, 0, 0, tzinfo=UTC),
|
||||
"realm": get_instance_realm(),
|
||||
},
|
||||
team=self.team,
|
||||
)
|
||||
|
||||
@patch("posthog.api.team.calculate_product_activation.delay", MagicMock())
|
||||
@patch("posthog.models.product_intent.ProductIntent.check_and_update_activation")
|
||||
@patch("posthog.api.project.report_user_action")
|
||||
@patch("posthog.api.team.report_user_action")
|
||||
@freeze_time("2024-01-05T00:00:00Z")
|
||||
def test_doesnt_send_event_for_already_activated_intent(
|
||||
self,
|
||||
mock_report_user_action: MagicMock,
|
||||
mock_report_user_action_legacy_endpoint: MagicMock,
|
||||
mock_check_and_update_activation: MagicMock,
|
||||
) -> None:
|
||||
ProductIntent.objects.create(
|
||||
team=self.team, product_type="product_analytics", activated_at=datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
|
||||
)
|
||||
if self.client_class is EnvironmentToProjectRewriteClient:
|
||||
mock_report_user_action = mock_report_user_action_legacy_endpoint
|
||||
response = self.client.patch(
|
||||
f"/api/environments/{self.team.id}/add_product_intent/",
|
||||
{"product_type": "product_analytics"},
|
||||
headers={"Referer": "https://posthogtest.com/my-url", "X-Posthog-Session-Id": "test_session_id"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
mock_check_and_update_activation.assert_not_called()
|
||||
mock_report_user_action.assert_not_called()
|
||||
|
||||
@patch("posthog.api.project.report_user_action")
|
||||
@patch("posthog.api.team.report_user_action")
|
||||
def test_can_complete_product_onboarding(
|
||||
|
@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.15 on 2024-11-01 16:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("posthog", "0505_grouptypemapping_project"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="productintent",
|
||||
name="activated_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="The date the org completed activation for the product. Generally only used to know if we should continue updating the product_intent row.",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="productintent",
|
||||
name="activation_last_checked_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True,
|
||||
help_text="The date we last checked if the org had completed activation for the product.",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
@ -1,6 +1,36 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from celery import shared_task
|
||||
from django.db import models
|
||||
|
||||
from posthog.models.insight import Insight
|
||||
from posthog.models.team.team import Team
|
||||
from posthog.models.utils import UUIDModel
|
||||
from posthog.utils import get_instance_realm
|
||||
|
||||
"""
|
||||
How to use this model:
|
||||
|
||||
Product intents are indicators that someone showed an interest in a given product.
|
||||
They are triggered from the frontend when the user performs certain actions, like
|
||||
selecting a product during onboarding or clicking on a certain button.
|
||||
|
||||
Some buttons that show product intent are frequently used by all users of the product,
|
||||
so we need to know if it's a new product intent, or if it's just regular usage. We
|
||||
can use the `activated_at` field to know if we should continue to update the product
|
||||
intent row, or if we should stop because it's just regular usage.
|
||||
|
||||
The `activated_at` field is set by checking against certain criteria that differs for
|
||||
each product. For instance, for the data warehouse product, we check if the user has
|
||||
created any DataVisualizationNode insights in the 30 days after the product intent
|
||||
was created. Each product needs to implement a method that checks for activation
|
||||
criteria if the intent actions are the same as the general usage actions.
|
||||
|
||||
We shouldn't use this model and the `activated_at` field in place of sending events
|
||||
about product usage because that limits our data exploration later. Definitely continue
|
||||
sending events for product usage that we may want to track for any reason, along with
|
||||
calculating activation here.
|
||||
"""
|
||||
|
||||
|
||||
class ProductIntent(UUIDModel):
|
||||
@ -9,9 +39,78 @@ class ProductIntent(UUIDModel):
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
product_type = models.CharField(max_length=255)
|
||||
onboarding_completed_at = models.DateTimeField(null=True, blank=True)
|
||||
activated_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="The date the org completed activation for the product. Generally only used to know if we should continue updating the product_intent row.",
|
||||
)
|
||||
activation_last_checked_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="The date we last checked if the org had completed activation for the product.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["team", "product_type"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.team.name} - {self.product_type}"
|
||||
|
||||
def has_activated_data_warehouse(self) -> bool:
|
||||
insights = Insight.objects.filter(
|
||||
team=self.team,
|
||||
created_at__gte=datetime(2024, 6, 1, tzinfo=UTC),
|
||||
query__kind="DataVisualizationNode",
|
||||
)
|
||||
|
||||
excluded_tables = ["events", "persons", "sessions", "person_distinct_ids"]
|
||||
for insight in insights:
|
||||
if insight.query and insight.query.get("source", {}).get("query"):
|
||||
query_text = insight.query["source"]["query"].lower()
|
||||
# Check if query doesn't contain any of the excluded tables after 'from'
|
||||
has_excluded_table = any(f"from {table}" in query_text.replace("\\", "") for table in excluded_tables)
|
||||
if not has_excluded_table:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_and_update_activation(self) -> None:
|
||||
if self.product_type == "data_warehouse":
|
||||
if self.has_activated_data_warehouse():
|
||||
self.activated_at = datetime.now(tz=UTC)
|
||||
self.save()
|
||||
self.report_activation("data_warehouse")
|
||||
|
||||
def report_activation(self, product_key: str) -> None:
|
||||
from posthog.event_usage import report_team_action
|
||||
|
||||
report_team_action(
|
||||
self.team,
|
||||
"product intent marked activated",
|
||||
{
|
||||
"product_key": product_key,
|
||||
"intent_created_at": self.created_at,
|
||||
"intent_updated_at": self.updated_at,
|
||||
"realm": get_instance_realm(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@shared_task(ignore_result=True)
|
||||
def calculate_product_activation(team_id: int, only_calc_if_days_since_last_checked: int = 1) -> None:
|
||||
"""
|
||||
Calculate product activation for a team.
|
||||
Only calculate if it's been more than `only_calc_if_days_since_last_checked` days since the last activation check.
|
||||
"""
|
||||
team = Team.objects.get(id=team_id)
|
||||
product_intents = ProductIntent.objects.filter(team=team)
|
||||
for product_intent in product_intents:
|
||||
if product_intent.activated_at:
|
||||
continue
|
||||
if (
|
||||
product_intent.activation_last_checked_at
|
||||
and (datetime.now(tz=UTC) - product_intent.activation_last_checked_at).days
|
||||
<= only_calc_if_days_since_last_checked
|
||||
):
|
||||
continue
|
||||
product_intent.check_and_update_activation()
|
||||
|
95
posthog/models/test/test_product_intent.py
Normal file
95
posthog/models/test/test_product_intent.py
Normal file
@ -0,0 +1,95 @@
|
||||
from datetime import datetime, timedelta, UTC
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
||||
from posthog.models.insight import Insight
|
||||
from posthog.models.product_intent.product_intent import (
|
||||
ProductIntent,
|
||||
calculate_product_activation,
|
||||
)
|
||||
from posthog.test.base import BaseTest
|
||||
|
||||
|
||||
class TestProductIntent(BaseTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.product_intent = ProductIntent.objects.create(team=self.team, product_type="data_warehouse")
|
||||
|
||||
def test_str_representation(self):
|
||||
self.assertEqual(str(self.product_intent), f"{self.team.name} - data_warehouse")
|
||||
|
||||
def test_unique_constraint(self):
|
||||
# Test that we can't create duplicate product intents for same team/product
|
||||
with pytest.raises(Exception):
|
||||
ProductIntent.objects.create(team=self.team, product_type="data_warehouse")
|
||||
|
||||
@freeze_time("2024-06-15T12:00:00Z")
|
||||
def test_has_activated_data_warehouse_with_valid_query(self):
|
||||
Insight.objects.create(
|
||||
team=self.team, query={"kind": "DataVisualizationNode", "source": {"query": "SELECT * FROM custom_table"}}
|
||||
)
|
||||
|
||||
self.assertTrue(self.product_intent.has_activated_data_warehouse())
|
||||
|
||||
@freeze_time("2024-06-15T12:00:00Z")
|
||||
def test_has_activated_data_warehouse_with_excluded_table(self):
|
||||
Insight.objects.create(
|
||||
team=self.team, query={"kind": "DataVisualizationNode", "source": {"query": "SELECT * FROM events"}}
|
||||
)
|
||||
|
||||
self.assertFalse(self.product_intent.has_activated_data_warehouse())
|
||||
|
||||
@freeze_time("2024-06-15T12:00:00Z")
|
||||
def test_has_activated_data_warehouse_with_old_insight(self):
|
||||
with freeze_time("2024-05-15T12:00:00Z"): # Before June 1st, 2024
|
||||
Insight.objects.create(
|
||||
team=self.team,
|
||||
query={"kind": "DataVisualizationNode", "source": {"query": "SELECT * FROM custom_table"}},
|
||||
)
|
||||
|
||||
self.assertFalse(self.product_intent.has_activated_data_warehouse())
|
||||
|
||||
@freeze_time("2024-06-15T12:00:00Z")
|
||||
def test_check_and_update_activation_sets_activated_at(self):
|
||||
Insight.objects.create(
|
||||
team=self.team, query={"kind": "DataVisualizationNode", "source": {"query": "SELECT * FROM custom_table"}}
|
||||
)
|
||||
|
||||
self.assertIsNone(self.product_intent.activated_at)
|
||||
self.product_intent.check_and_update_activation()
|
||||
self.product_intent.refresh_from_db()
|
||||
assert self.product_intent.activated_at == datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
|
||||
|
||||
@freeze_time("2024-06-15T12:00:00Z")
|
||||
def test_calculate_product_activation_task(self):
|
||||
# Create an insight that should trigger activation
|
||||
Insight.objects.create(
|
||||
team=self.team, query={"kind": "DataVisualizationNode", "source": {"query": "SELECT * FROM custom_table"}}
|
||||
)
|
||||
|
||||
calculate_product_activation(self.team.id)
|
||||
|
||||
self.product_intent.refresh_from_db()
|
||||
assert self.product_intent.activated_at == datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
|
||||
|
||||
def test_calculate_product_activation_respects_check_interval(self):
|
||||
# Set last checked time to recent
|
||||
self.product_intent.activation_last_checked_at = datetime.now(tz=UTC)
|
||||
self.product_intent.save()
|
||||
|
||||
calculate_product_activation(self.team.id, only_calc_if_days_since_last_checked=1)
|
||||
|
||||
self.product_intent.refresh_from_db()
|
||||
self.assertIsNone(self.product_intent.activated_at)
|
||||
|
||||
@freeze_time("2024-06-15T12:00:00Z")
|
||||
def test_calculate_product_activation_skips_activated_products(self):
|
||||
# Set product as already activated
|
||||
self.product_intent.activated_at = datetime.now(tz=UTC)
|
||||
self.product_intent.save()
|
||||
|
||||
with freeze_time(datetime.now(tz=UTC) + timedelta(days=2)):
|
||||
calculate_product_activation(self.team.id)
|
||||
self.product_intent.refresh_from_db()
|
||||
assert self.product_intent.activated_at == datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
|
@ -1,14 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.test.client import Client
|
||||
from django.urls import reverse
|
||||
from freezegun import freeze_time
|
||||
from rest_framework import status
|
||||
|
||||
from posthog.api.test.test_organization import create_organization
|
||||
from posthog.api.test.test_team import create_team
|
||||
|
||||
from posthog.models import Action, Cohort, Dashboard, FeatureFlag, Insight
|
||||
from posthog.models.organization import Organization
|
||||
from posthog.models.team import Team
|
||||
@ -142,7 +142,7 @@ class TestAutoProjectMiddleware(APIBaseTest):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.base_app_num_queries = 45
|
||||
cls.base_app_num_queries = 47
|
||||
# Create another team that the user does have access to
|
||||
cls.second_team = create_team(organization=cls.organization, name="Second Life")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user