Skip to content

Commit

Permalink
feat: only send product_intent events if not activated (#25889)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
raquelmsmith and github-actions[bot] authored Nov 1, 2024
1 parent 003ac8a commit 54efcdc
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 85 deletions.
3 changes: 3 additions & 0 deletions frontend/src/scenes/teamLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion latest_migrations.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 9 additions & 3 deletions posthog/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
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
Expand Down Expand Up @@ -199,6 +202,7 @@ def get_live_events_token(self, project: Project) -> Optional[str]:
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"
)
Expand Down Expand Up @@ -575,10 +579,12 @@ def add_product_intent(self, request: request.Request, *args, **kwargs):

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",
Expand Down
8 changes: 6 additions & 2 deletions posthog/api/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
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
Expand Down Expand Up @@ -217,6 +218,7 @@ def get_live_events_token(self, team: Team) -> Optional[str]:
)

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"
)
Expand Down Expand Up @@ -587,10 +589,12 @@ def add_product_intent(self, request: request.Request, *args, **kwargs):

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",
Expand Down
160 changes: 84 additions & 76 deletions posthog/api/test/__snapshots__/test_decide.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions posthog/api/test/test_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,76 @@ def test_can_add_product_intent(
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(
Expand Down
30 changes: 30 additions & 0 deletions posthog/migrations/0506_productintent_activated_at_and_more.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
Loading

0 comments on commit 54efcdc

Please sign in to comment.