-
Notifications
You must be signed in to change notification settings - Fork 0
[Marketing] Track OpenAI Ads Registrations #398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| REGISTRATION_COMPLETED_SESSION_KEY = "openai_ads_registration_completed" | ||
|
|
||
|
|
||
| def mark_registration_completed(request) -> None: | ||
| session = getattr(request, "session", None) | ||
| if session is not None: | ||
| session[REGISTRATION_COMPLETED_SESSION_KEY] = True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import pytest | ||
| from allauth.account.adapter import DefaultAccountAdapter | ||
| from django.contrib.auth import get_user_model | ||
| from django.contrib.sessions.middleware import SessionMiddleware | ||
| from django.http import HttpResponseRedirect | ||
| from django.test import RequestFactory | ||
| from django.urls import reverse | ||
|
|
||
| from apps.core.openai_ads import REGISTRATION_COMPLETED_SESSION_KEY | ||
| from rowset.adapters import CustomAccountAdapter | ||
|
|
||
| PIXEL_ID = "8dYXMBukBoC779PidZY6ZX" | ||
| pytestmark = pytest.mark.django_db | ||
|
|
||
|
|
||
| def test_openai_ads_pixel_is_installed_once_on_public_pages(client, settings): | ||
| settings.OPENAI_ADS_PIXEL_ID = PIXEL_ID | ||
| settings.ENVIRONMENT = "prod" | ||
|
|
||
| response = client.get(reverse("landing")) | ||
|
|
||
| content = response.content.decode() | ||
| assert content.count("https://bzrcdn.openai.com/sdk/oaiq.min.js") == 1 | ||
| assert content.count(f'pixelId: "{PIXEL_ID}"') == 1 | ||
| assert "debug: true" not in content | ||
|
|
||
|
|
||
| def test_openai_ads_pixel_is_disabled_without_configuration(client, settings): | ||
| settings.OPENAI_ADS_PIXEL_ID = "" | ||
|
|
||
| response = client.get(reverse("landing")) | ||
|
|
||
| assert "https://bzrcdn.openai.com/sdk/oaiq.min.js" not in response.content.decode() | ||
|
|
||
|
|
||
| def test_successful_signup_measures_registration_once(client, monkeypatch, settings): | ||
| settings.OPENAI_ADS_PIXEL_ID = PIXEL_ID | ||
| settings.ENVIRONMENT = "prod" | ||
| client.cookies["rowset_analytics_consent"] = "granted" | ||
|
|
||
| monkeypatch.setattr( | ||
| "rowset.adapters.CustomAccountAdapter.send_confirmation_mail", | ||
| lambda *_args, **_kwargs: None, | ||
| ) | ||
|
|
||
| response = client.post( | ||
| reverse("account_signup"), | ||
| data={ | ||
| "email": "openai-pixel-user@example.com", | ||
| "password1": "strong-test-pass-123", | ||
| }, | ||
| follow=True, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert get_user_model().objects.filter(email="openai-pixel-user@example.com").exists() | ||
| content = response.content.decode() | ||
| assert "registrationCompleted: true" in content | ||
| assert 'oaiq("measure", "registration_completed", {' not in content | ||
|
|
||
| next_response = client.get(reverse("home")) | ||
| assert "registrationCompleted: true" not in next_response.content.decode() | ||
|
|
||
|
|
||
| def test_failed_signup_does_not_mark_registration_completed(client, settings): | ||
| settings.OPENAI_ADS_PIXEL_ID = PIXEL_ID | ||
|
|
||
| response = client.post( | ||
| reverse("account_signup"), | ||
| data={"email": "invalid", "password1": "short"}, | ||
| ) | ||
|
|
||
| assert response.status_code == 200 | ||
| assert "registrationCompleted: true" not in response.content.decode() | ||
|
|
||
|
|
||
| def test_social_signup_marks_post_login_session(monkeypatch): | ||
| request = RequestFactory().get("/") | ||
| SessionMiddleware(lambda _request: None).process_request(request) | ||
| response = HttpResponseRedirect("/") | ||
| monkeypatch.setattr(DefaultAccountAdapter, "post_login", lambda *_args, **_kwargs: response) | ||
|
|
||
| result = CustomAccountAdapter().post_login( | ||
| request, | ||
| object(), | ||
| email_verification="none", | ||
| signal_kwargs={"sociallogin": object()}, | ||
| email="social@example.com", | ||
| signup=True, | ||
| redirect_url="/", | ||
| ) | ||
|
|
||
| assert result is response | ||
| assert request.session[REGISTRATION_COMPLETED_SESSION_KEY] is True | ||
|
|
||
|
|
||
| def test_existing_user_login_does_not_mark_registration(monkeypatch): | ||
| request = RequestFactory().get("/") | ||
| SessionMiddleware(lambda _request: None).process_request(request) | ||
| response = HttpResponseRedirect("/") | ||
| monkeypatch.setattr(DefaultAccountAdapter, "post_login", lambda *_args, **_kwargs: response) | ||
|
|
||
| CustomAccountAdapter().post_login( | ||
| request, | ||
| object(), | ||
| email_verification="none", | ||
| signal_kwargs=None, | ||
| email="existing@example.com", | ||
| signup=False, | ||
| redirect_url="/", | ||
| ) | ||
|
|
||
| assert REGISTRATION_COMPLETED_SESSION_KEY not in request.session |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| (function () { | ||
| const Rowset = (window.Rowset = window.Rowset || {}); | ||
| const config = Rowset.openAiAds || {}; | ||
| const consentCookie = "rowset_analytics_consent"; | ||
| const pendingRegistrationKey = "rowset_openai_ads_registration_pending"; | ||
| const hasSharedConsentController = typeof Rowset.hasAnalyticsConsent === "function"; | ||
| let measurementAllowed = hasSharedConsentController | ||
| ? Rowset.hasAnalyticsConsent() | ||
| : cookieValue(consentCookie) === "granted"; | ||
| let registrationMeasured = false; | ||
|
|
||
| function pendingRegistration() { | ||
| try { | ||
| if (config.registrationCompleted) { | ||
| window.sessionStorage?.setItem(pendingRegistrationKey, "true"); | ||
| } | ||
| return window.sessionStorage?.getItem(pendingRegistrationKey) === "true" || | ||
| Boolean(config.registrationCompleted); | ||
| } catch (_error) { | ||
| return Boolean(config.registrationCompleted); | ||
| } | ||
| } | ||
|
|
||
| function clearPendingRegistration() { | ||
| try { | ||
| window.sessionStorage?.removeItem(pendingRegistrationKey); | ||
| } catch (_error) { | ||
| // Storage can be unavailable in privacy-restricted browser contexts. | ||
| } | ||
| } | ||
|
|
||
| function cookieValue(name) { | ||
| const prefix = `${name}=`; | ||
| return (document.cookie || "") | ||
| .split(";") | ||
| .map((part) => part.trim()) | ||
| .find((part) => part.startsWith(prefix)) | ||
| ?.slice(prefix.length) || ""; | ||
| } | ||
|
|
||
| function setCookie(name, value) { | ||
| const secure = window.location.protocol === "https:" ? "; Secure" : ""; | ||
| document.cookie = `${name}=${value}; Path=/; Max-Age=${60 * 60 * 24 * 365}; SameSite=Lax${secure}`; | ||
| } | ||
|
|
||
| function showBanner(show) { | ||
| const banner = document.querySelector("[data-analytics-consent]"); | ||
| if (banner) banner.hidden = !show; | ||
| } | ||
|
|
||
| function measureRegistration() { | ||
| const hasPendingRegistration = pendingRegistration(); | ||
| if (!measurementAllowed || !hasPendingRegistration || registrationMeasured) return; | ||
| try { | ||
| window.oaiq?.("measure", "registration_completed", { | ||
| type: "customer_action", | ||
| }); | ||
| registrationMeasured = true; | ||
| clearPendingRegistration(); | ||
| } catch (_error) { | ||
| // Conversion measurement must never block or break signup. | ||
| } | ||
| } | ||
|
|
||
| function grantConsent() { | ||
| setCookie(consentCookie, "granted"); | ||
| if (!measurementAllowed) { | ||
| measurementAllowed = true; | ||
| window.oaiq?.("consent", true); | ||
| } | ||
| showBanner(false); | ||
| measureRegistration(); | ||
| } | ||
|
|
||
| function handleSharedConsentGranted() { | ||
| measurementAllowed = true; | ||
| window.oaiq?.("consent", true); | ||
| measureRegistration(); | ||
| } | ||
|
|
||
| function declineConsent() { | ||
| setCookie(consentCookie, "denied"); | ||
| measurementAllowed = false; | ||
| window.oaiq?.("consent", false); | ||
| clearPendingRegistration(); | ||
| showBanner(false); | ||
| } | ||
|
|
||
| function initialize() { | ||
| if (hasSharedConsentController) { | ||
| window.addEventListener?.( | ||
| "rowset:analytics-consent-granted", | ||
| handleSharedConsentGranted, | ||
| ); | ||
| } else { | ||
| document | ||
| .querySelector("[data-analytics-consent-accept]") | ||
| ?.addEventListener("click", grantConsent); | ||
| document | ||
| .querySelector("[data-analytics-consent-decline]") | ||
| ?.addEventListener("click", declineConsent); | ||
| showBanner(!cookieValue(consentCookie)); | ||
| } | ||
| measureRegistration(); | ||
| } | ||
|
|
||
| if (document.readyState === "loading") { | ||
| document.addEventListener("DOMContentLoaded", initialize, { once: true }); | ||
| } else { | ||
| initialize(); | ||
| } | ||
| })(); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.