From f342befb0b7046ce4f63cea5d544cfd236f9a664 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 5 Mar 2026 20:07:38 -0500 Subject: [PATCH] user signup referrer (#1105) see last PR --- ...c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c.json} | 6 +++--- ..._new_user_account_and_subscription_checkout.rs | 5 +++++ .../devenv/dev-database-seed/src/seeding/users.rs | 1 + .../create_account_from_email_and_password.rs | 9 ++++++++- .../user/create/create_account_from_google_sso.rs | 3 +++ .../create/create_account_from_stripe_checkout.rs | 3 +++ .../users/user/create/create_account_generic.rs | 11 ++++++++++- ...scription_checkout_with_user_signup_handler.rs | 10 ++++++++++ .../user_creation_case.rs | 2 ++ .../endpoints/users/create_account_handler.rs | 15 +++++++++++++++ 10 files changed, 60 insertions(+), 5 deletions(-) rename .sqlx/{query-56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740.json => query-08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c.json} (82%) diff --git a/.sqlx/query-56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740.json b/.sqlx/query-08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c.json similarity index 82% rename from .sqlx/query-56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740.json rename to .sqlx/query-08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c.json index 09304da9e5..2932032558 100644 --- a/.sqlx/query-56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740.json +++ b/.sqlx/query-08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c.json @@ -1,12 +1,12 @@ { "db_name": "MySQL", - "query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?\n ", + "query": "\nINSERT INTO users\nSET\n token = ?,\n \n is_temporary = ?,\n \n username = ?,\n display_name = ?,\n\n username_is_generated = ?,\n username_is_not_customized = ?,\n\n email_address = ?,\n email_gravatar_hash = ?,\n\n email_confirmed = FALSE,\n email_confirmed_by_google = ?,\n email_is_synthetic = ?,\n \n was_eagerly_provisioned = ?,\n\n profile_markdown = ?,\n profile_rendered_html = ?,\n user_role_slug = ?,\n\n password_hash = ?,\n is_without_password = ?,\n\n maybe_feature_flags = ?,\n\n ip_address_creation = ?,\n ip_address_last_login = ?,\n ip_address_last_update = ?,\n\n maybe_source = ?,\n\n maybe_signup_method = ?,\n\n maybe_referral_url = ?\n ", "describe": { "columns": [], "parameters": { - "Right": 22 + "Right": 23 }, "nullable": [] }, - "hash": "56746bcb993cb33f5492f401ce4fc3fd44d91fed660c2f106cb22e482dffa740" + "hash": "08996721482fe897c2721298c04cf0eba5fcf1d04d57397b8afab3dd59feaa8c" } diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs index 6a47dbbf21..0ebc0aa085 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/stripe_artcraft/create_create_new_user_account_and_subscription_checkout.rs @@ -11,6 +11,11 @@ pub struct StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest { pub plan: Option, pub cadence: Option, + + /// Optional: The referral URL the user arrived from when signing up. + /// The browser can send `document.referrer` to the backend so we know how people are finding us. + /// If the browser doesn't send this parameter, we'll try the `referer` header. + pub maybe_referral_url: Option, } #[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)] diff --git a/crates/cli/devenv/dev-database-seed/src/seeding/users.rs b/crates/cli/devenv/dev-database-seed/src/seeding/users.rs index 4c0af5fb5e..b4ec0e172d 100644 --- a/crates/cli/devenv/dev-database-seed/src/seeding/users.rs +++ b/crates/cli/devenv/dev-database-seed/src/seeding/users.rs @@ -62,6 +62,7 @@ async fn seed_user( password_hash: &password_hash, ip_address: "127.0.0.1", maybe_source: None, + maybe_referral_url: None, maybe_user_token: maybe_user_token.as_ref(), }).await.map_err(|err| anyhow!("err: {:?}", err))?; diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs index 8e99bbc6d6..ad408c70b7 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs @@ -20,6 +20,11 @@ pub struct CreateAccountFromEmailPasswordArgs<'a> { pub ip_address: &'a str, pub maybe_source: Option, + /// The referral URL the user arrived from when signing up. + /// The browser can send `document.referrer` to the backend so we know how people are finding us. + /// If the browser doesn't send this parameter, we'll try the `referer` header. + pub maybe_referral_url: Option, + /// In production code, send this as `None`. /// Only provide an external user token for db integration tests and db seeding tools. /// This allows for knowing the user token a priori. @@ -62,9 +67,11 @@ pub async fn create_account_from_email_and_password( password_hash: args.password_hash, is_without_password: false, + maybe_referral_url: args.maybe_referral_url, + // NB: This is just for testing. maybe_user_token: args.maybe_user_token, - + // These flags represent eagerly created users and are false for this flow is_temporary: false, email_is_synthetic: false, diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs index f1d9938663..7b60f9d913 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs @@ -57,6 +57,9 @@ pub async fn create_account_from_google_sso( password_hash: SSO_PASSWORD, is_without_password: true, + // TODO: Necessary? We're not doing SSO for now. + maybe_referral_url: None, + // NB: This is just for testing. maybe_user_token: None, diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs index eb93d17ef8..3b3d758fc2 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs @@ -26,6 +26,7 @@ pub struct CreateAccountFromStripeCheckoutArgs<'a> { pub ip_address: &'a str, pub maybe_source: Option, + pub maybe_referral_url: Option, /// Comma separated string of feature flags. pub maybe_feature_flags: Option<&'a str>, @@ -68,6 +69,8 @@ pub async fn create_account_from_stripe_checkout( password_hash: SSO_PASSWORD, is_without_password: true, + maybe_referral_url: args.maybe_referral_url, + // NB: This is just for testing. maybe_user_token: None, }, diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs index a660cd6023..9e987fceab 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs @@ -60,6 +60,11 @@ pub struct GenericCreateAccountArgs<'a> { /// Comma separated string of feature flags. pub maybe_feature_flags: Option<&'a str>, + /// The referral URL the user arrived from when signing up. + /// The browser can send `document.referrer` to the backend so we know how people are finding us. + /// If the browser doesn't send this parameter, we'll try the `referer` header. + pub maybe_referral_url: Option, + /// In production code, send this as `None`. /// Only provide an external user token for db integration tests and db seeding tools. /// This allows for knowing the user token a priori. @@ -124,7 +129,9 @@ SET maybe_source = ?, - maybe_signup_method = ? + maybe_signup_method = ?, + + maybe_referral_url = ? "#, user_token.as_str(), @@ -159,6 +166,8 @@ SET args.maybe_source.map(|s| s.to_str()), args.maybe_signup_method.map(|m| m.to_str()), + + args.maybe_referral_url, ); diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs index 1621db0747..9eea25a927 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/stripe_artcraft_create_subscription_checkout_with_user_signup_handler.rs @@ -85,6 +85,15 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl CommonWebError::ServerError })?; + let maybe_referral_url = request.maybe_referral_url.clone() + .or_else(|| { + http_request.headers().get("referer") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + }) + .filter(|s| !s.is_empty()) + .map(|s| s.chars().take(255).collect::()); + let maybe_user_metadata = internal_user_lookup .lookup_user_from_http_request_and_mysql_connection(&http_request, &mut mysql_connection) .await @@ -101,6 +110,7 @@ pub async fn stripe_artcraft_create_subscription_checkout_with_user_signup_handl &price_id, &mut mysql_connection, &stripe_config, + maybe_referral_url, ).await? }, Some(user_metadata) => { diff --git a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs index ba8831765d..26a665f494 100644 --- a/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs +++ b/crates/service/plugins/billing_artcraft/src/endpoints/checkout_with_user_signup/user_creation_case.rs @@ -28,6 +28,7 @@ pub (super) async fn user_creation_case( price_id: &PriceId, mysql_connection: &mut PoolConnection, stripe_config: &ArtcraftStripeConfigWithClient, + maybe_referral_url: Option, ) -> Result { let mut transaction = mysql_connection.begin() @@ -82,6 +83,7 @@ pub (super) async fn user_creation_case( maybe_feature_flags: Some(&user_feature_flags), ip_address: &ip_address, maybe_source, + maybe_referral_url: maybe_referral_url.clone(), }, Transactor::for_transaction(&mut transaction), ).await; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/create_account_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/create_account_handler.rs index 1c387e7c02..80edbbf783 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/users/create_account_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/create_account_handler.rs @@ -41,6 +41,11 @@ pub struct CreateAccountRequest { /// Optional: Source of the signup, e.g. "artcraft", "fakeyou", "storyteller", etc. /// If not provided, we try to infer it from the Origin header instead. pub signup_source: Option, + + /// Optional: The referral URL the user arrived from when signing up. + /// The browser can send `document.referrer` to the backend so we know how people are finding us. + /// If the browser doesn't send this parameter, we'll try the `referer` header. + pub maybe_referral_url: Option, } #[derive(ToSchema, Serialize)] @@ -195,6 +200,15 @@ pub async fn create_account_handler( maybe_source = get_request_signup_source_enum(&http_request); } + let maybe_referral_url = request.maybe_referral_url.clone() + .or_else(|| { + http_request.headers().get("referer") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + }) + .filter(|s| !s.is_empty()) + .map(|s| s.chars().take(255).collect::()); + let create_account_result = create_account_from_email_and_password( &mysql_pool, CreateAccountFromEmailPasswordArgs { @@ -205,6 +219,7 @@ pub async fn create_account_handler( password_hash: &password_hash, ip_address: &ip_address, maybe_source, + maybe_referral_url, maybe_user_token: None, // NB: This parameter is for internal testing only } ).await;