Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .default import get_default_telemetry_script
from .google import get_google_analytics_trackers, gtag_report_conversion
from .koala import get_koala_trackers
from .posthog import get_posthog_trackers
from .posthog import get_posthog_trackers, track_newsletter_posthog_subscription
from .rb2b import get_rb2b_trackers
from .unify import get_unify_trackers

Expand All @@ -20,4 +20,5 @@
"get_unify_trackers",
"gtag_report_conversion",
"identify_common_room_user",
"track_newsletter_posthog_subscription",
]
Original file line number Diff line number Diff line change
Expand Up @@ -43,40 +43,68 @@ def identify_posthog_user(user_id: str) -> rx.event.EventSpec:
)


def _capture_posthog_person_event(
event_name: str,
props: dict[str, Any],
person_keys: set[str],
) -> rx.event.EventSpec:
"""Capture an event while keeping PII out of distinct_id.

Returns:
Event that runs PostHog capture in the browser.
"""
props_json = json.dumps(props)
person_keys_json = json.dumps(sorted(person_keys))

return rx.call_script(
f"""
if (typeof posthog !== 'undefined') {{
const props = {props_json};
const personKeys = new Set({person_keys_json});
const personProps = Object.fromEntries(
Object.entries(props).filter(
([key, value]) => personKeys.has(key) && value !== undefined && value !== null && value !== ''
)
);
const canonicalDistinctId = props.user_uuid || props.user_id || props.contact_uuid || props.contact_id || props.distinct_id;
const eventProps = {{...props}};
delete eventProps.user_uuid;
delete eventProps.user_id;
delete eventProps.contact_uuid;
delete eventProps.contact_id;
delete eventProps.distinct_id;
for (const key of personKeys) {{
delete eventProps[key];
}}
if (Object.keys(personProps).length > 0) {{
eventProps.$set = personProps;
}}
if (canonicalDistinctId && canonicalDistinctId !== props.email) {{
posthog.identify(String(canonicalDistinctId), personProps);
}}
Comment on lines +79 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Double person-property write when canonical ID is present

When canonicalDistinctId is truthy, posthog.identify(canonicalDistinctId, personProps) is called and then posthog.capture(event_name, {..., $set: personProps}) is called immediately after with the same personProps in $set. PostHog merges both writes, so this is harmless in practice, but the $set payload on the capture event is redundant when identify already carries the same data. Removing the $set attachment when canonicalDistinctId is present would eliminate the duplicate write, though this is a minor analytics hygiene concern.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

posthog.capture('{event_name}', eventProps);
}}
"""
)


def _track_form_posthog(
event_name: str,
form_data: dict[str, Any],
allowed_keys: set[str],
) -> rx.event.EventSpec:
"""Identify the submitter and capture a form event in PostHog.
"""Capture a form event in PostHog with person properties.

Args:
event_name: PostHog event name to capture.
form_data: Submitted form fields as a string-keyed dict.
allowed_keys: Set of keys to include from form_data.

Returns:
Event that runs PostHog identify and capture in the browser.
Event that runs PostHog capture in the browser.
"""
filtered = {k: v for k, v in form_data.items() if k in allowed_keys}
props_json = json.dumps(filtered)

return rx.call_script(
f"""
if (typeof posthog !== 'undefined') {{
const props = {props_json};
const distinctId = props.email || ('anon_' + String(Date.now()));
posthog.identify(distinctId, {{
email: props.email,
first_name: props.first_name,
last_name: props.last_name,
job_title: props.job_title,
company_name: props.company_name,
}});
posthog.capture('{event_name}', props);
}}
"""
)
return _capture_posthog_person_event(event_name, filtered, _PERSON_KEYS)


_COMMON_KEYS = {
Expand All @@ -85,19 +113,32 @@ def _track_form_posthog(
"last_name",
"job_title",
"company_name",
"user_uuid",
"user_id",
"contact_uuid",
"contact_id",
"distinct_id",
"number_of_employees",
"how_did_you_hear_about_us",
"interested_in",
"internal_tools",
"technical_level",
}

_PERSON_KEYS = {
"email",
"first_name",
"last_name",
"job_title",
"company_name",
}


def track_demo_form_posthog_submission(form_data: dict[str, Any]) -> rx.event.EventSpec:
"""Capture a demo_request event in PostHog.

Returns:
Event that runs PostHog identify and capture in the browser.
Event that runs PostHog capture in the browser.
"""
return _track_form_posthog("demo_request", form_data, _COMMON_KEYS)

Expand All @@ -108,13 +149,30 @@ def track_intro_form_posthog_submission(
"""Capture an intro_submit event in PostHog.

Returns:
Event that runs PostHog identify and capture in the browser.
Event that runs PostHog capture in the browser.
"""
return _track_form_posthog(
"intro_submit", form_data, _COMMON_KEYS | {"phone_number"}
)


def track_newsletter_posthog_subscription(
email: str | None,
contact_uuid: str | None = None,
) -> rx.event.EventSpec:
"""Capture a newsletter subscription without using email as distinct_id.

Returns:
Event that runs PostHog capture in the browser.
"""
props = {}
if email:
props["email"] = email
if contact_uuid:
props["contact_uuid"] = contact_uuid
return _capture_posthog_person_event("newsletter_subscribed", props, {"email"})


def get_posthog_trackers(
project_id: str,
api_host: str = POSTHOG_API_HOST,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

import httpx
from email_validator import EmailNotValidError, ValidatedEmail, validate_email
from reflex_components_internal.blocks.telemetry import (
track_newsletter_posthog_subscription,
)

import reflex as rx
from reflex_site_shared.constants import (
Expand Down Expand Up @@ -102,6 +105,7 @@ async def signup(
},
)
return
yield track_newsletter_posthog_subscription(email)
yield IndexState.send_contact_to_webhook(email)
yield IndexState.add_contact_to_loops(email)
Comment on lines +108 to 110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 PostHog fires a newsletter_subscribed event even when email is None. If the user submits the form without an email address, email_to_validate is falsy and the if block is skipped entirely — so email stays None and tracking fires with an empty props object. The resulting event has no identity and no person properties, creating noise in PostHog analytics.

Suggested change
yield track_newsletter_posthog_subscription(email)
yield IndexState.send_contact_to_webhook(email)
yield IndexState.add_contact_to_loops(email)
if email:
yield track_newsletter_posthog_subscription(email)
yield IndexState.send_contact_to_webhook(email)
yield IndexState.add_contact_to_loops(email)

async with self:
Expand Down
Loading