find out if marketing is effective (#1107)

NB: Ip address is to prevent spam/abuse
This commit is contained in:
Brandon Thomas
2026-03-05 21:22:02 -05:00
committed by GitHub
parent d374317a77
commit 2a101487b0
16 changed files with 280 additions and 3 deletions
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\n INSERT INTO web_referrals (\n url,\n domain,\n utm_source,\n utm_medium,\n utm_campaign,\n ip_address,\n anonymous_visitor_token\n ) VALUES (?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "80f73702374593c141d183d86bd8af73d37fd7118914d0ba12a02f53c1b84c78"
}
@@ -9,4 +9,5 @@ pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
pub mod users;
pub mod utils;
pub mod utils;
pub mod web_referrals;
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const LOG_WEB_REFERRAL_URL_PATH: &str = "/v1/web_referrals/record";
#[derive(Deserialize, ToSchema)]
pub struct LogWebReferralRequest {
/// 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<String>,
}
#[derive(Serialize, ToSchema)]
pub struct LogWebReferralResponse {
pub success: bool,
}
@@ -0,0 +1 @@
pub mod log_web_referral;
@@ -39,4 +39,5 @@ pub mod voice_conversion;
pub mod voice_designer;
pub mod w2l;
pub mod wallet_ledger_entries;
pub mod wallets;
pub mod wallets;
pub mod web_referrals;
@@ -0,0 +1,41 @@
use sqlx::{MySql, Pool};
pub struct InsertWebReferralArgs<'a> {
pub url: &'a str,
pub maybe_domain: Option<&'a str>,
pub utm_source: Option<&'a str>,
pub utm_medium: Option<&'a str>,
pub utm_campaign: Option<&'a str>,
pub ip_address: &'a str,
pub maybe_anonymous_visitor_token: Option<&'a str>,
}
pub async fn insert_web_referral(
mysql_pool: &Pool<MySql>,
args: InsertWebReferralArgs<'_>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO web_referrals (
url,
domain,
utm_source,
utm_medium,
utm_campaign,
ip_address,
anonymous_visitor_token
) VALUES (?, ?, ?, ?, ?, ?, ?)
"#,
args.url,
args.maybe_domain,
args.utm_source,
args.utm_medium,
args.utm_campaign,
args.ip_address,
args.maybe_anonymous_visitor_token,
)
.execute(mysql_pool)
.await?;
Ok(())
}
@@ -0,0 +1 @@
pub mod insert_web_referral;
@@ -273,6 +273,8 @@ use artcraft_api_defs::moderation::wallet_ledger_entries::moderator_get_wallet_l
use artcraft_api_defs::moderation::wallets::list_user_wallets::*;
use artcraft_api_defs::moderation::wallets::moderator_add_banked_balance_to_wallet::*;
use artcraft_api_defs::moderation::wallets::moderator_get_wallet::*;
use artcraft_api_defs::web_referrals::log_web_referral::*;
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::*;
use crate::http_server::endpoints::webhooks::fal_webhook_handler::*;
use crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::*;
use crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::*;
@@ -454,6 +456,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::credits::get_session_credits_handler::get_session_credits_handler,
// Subscriptions
crate::http_server::endpoints::subscriptions::get_session_subscription_handler::get_session_subscription_handler,
// Web Referrals
crate::http_server::endpoints::web_referrals::log_web_referral_handler::log_web_referral_handler,
// Webhooks
crate::http_server::endpoints::webhooks::fal_webhook_handler::fal_webhook_handler,
// Image Studio
@@ -1043,6 +1047,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
GetSessionSubscriptionResponse,
ListBatchGeneratedReduxMediaFilesPathInfo,
ListBatchGeneratedReduxMediaFilesSuccessResponse,
// Web Referrals
LogWebReferralRequest,
LogWebReferralResponse,
))
)]
pub struct ApiDoc;
@@ -31,7 +31,12 @@ impl AvtCookieManager {
}
pub fn make_new_cookie(&self) -> AnyhowResult<Cookie> {
let payload = AvtCookiePayload::new();
let token = AnonymousVisitorTrackingToken::generate();
self.make_new_cookie_with_apriori_token(&token)
}
pub fn make_new_cookie_with_apriori_token(&self, token: &AnonymousVisitorTrackingToken) -> AnyhowResult<Cookie> {
let payload = AvtCookiePayload::from_token(token.clone());
let claims = payload.to_map();
let jwt_string = self.jwt_signer.claims_to_jwt(&claims)?;
@@ -36,6 +36,13 @@ impl AvtCookiePayload {
}
}
pub fn from_token(avt_token: AnonymousVisitorTrackingToken) -> Self {
AvtCookiePayload {
avt_token,
cookie_version: COOKIE_VERSION,
}
}
pub fn from_map(map: BTreeMap<String, String>) -> Result<Self, InternalError> {
let avt_token = map.get("avt_token")
.ok_or_else(|| InternalError::VisitorCookieMissingField("avt_token"))?;
@@ -27,5 +27,6 @@ pub mod users;
pub mod voice_clone_requests;
pub mod voice_conversion;
pub mod voice_designer;
pub mod web_referrals;
pub mod webhooks;
pub mod weights;
@@ -0,0 +1,152 @@
use std::sync::Arc;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{info, warn};
use url::Url;
use artcraft_api_defs::web_referrals::log_web_referral::{LogWebReferralRequest, LogWebReferralResponse};
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::web_referrals::insert_web_referral::{insert_web_referral, InsertWebReferralArgs};
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
/// Record a web referral for analytics tracking.
#[utoipa::path(
post,
tag = "Web Referrals",
path = "/v1/web_referrals/record",
responses(
(status = 200, description = "Success", body = LogWebReferralResponse),
(status = 500, description = "Server error"),
),
)]
pub async fn log_web_referral_handler(
http_request: HttpRequest,
request: web::Json<LogWebReferralRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<HttpResponse, CommonWebError> {
// 1. Try to get the referral URL from the request body, falling back to the Referer header.
let referral_url = request.maybe_referral_url.as_deref()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.or_else(|| {
http_request.headers().get("referer")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.filter(|s| !s.is_empty());
// 2. If no URL from either source, early exit 200 OK.
let referral_url = match referral_url {
Some(url) => url,
None => {
return ok_response();
}
};
// 3. Parse the URL (infallible — we don't fail on bad URLs).
// NB: Parse before truncating so we don't lose UTM params.
let parsed = Url::parse(&referral_url).ok();
// Truncate the URL to 255 characters for storage.
let referral_url: String = referral_url.chars().take(255).collect();
// 4. Extract domain if URL parsed.
let maybe_domain = parsed.as_ref()
.and_then(|u| u.host_str())
.map(|s| s.to_string());
// 5. Extract UTM parameters if URL parsed.
let utm_source = extract_query_param(parsed.as_ref(), "utm_source");
let utm_medium = extract_query_param(parsed.as_ref(), "utm_medium");
let utm_campaign = extract_query_param(parsed.as_ref(), "utm_campaign");
// 6. Grab the request IP address.
let ip_address = get_request_ip(&http_request);
// 7. Check inbound cookies for AVT. If not present, mint a new one.
let existing_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let (avt_token, maybe_new_avt_cookie) = match existing_avt_token {
Some(token) => (token, None),
None => {
let token = AnonymousVisitorTrackingToken::generate();
let cookie = server_state
.avt_cookie_manager
.make_new_cookie_with_apriori_token(&token)
.map_err(|e| {
warn!("AVT cookie creation error: {:?}", e);
CommonWebError::ServerError
})?;
(token, Some(cookie))
}
};
let avt_token_str = avt_token.to_string();
// Truncate UTM fields to fit the schema.
let utm_source = utm_source.map(|s| s.chars().take(32).collect::<String>());
let utm_medium = utm_medium.map(|s| s.chars().take(32).collect::<String>());
let utm_campaign = utm_campaign.map(|s| s.chars().take(255).collect::<String>());
// Insert into the database.
insert_web_referral(&server_state.mysql_pool, InsertWebReferralArgs {
url: &referral_url,
maybe_domain: maybe_domain.as_deref(),
utm_source: utm_source.as_deref(),
utm_medium: utm_medium.as_deref(),
utm_campaign: utm_campaign.as_deref(),
ip_address: &ip_address,
maybe_anonymous_visitor_token: Some(&avt_token_str),
}).await.map_err(|e| {
warn!("Error inserting web referral: {:?}", e);
CommonWebError::ServerError
})?;
info!("Recorded web referral: {}", referral_url);
// Build response, setting the AVT cookie if we minted a new one.
let response = LogWebReferralResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|_| CommonWebError::ServerError)?;
let mut response_builder = HttpResponse::Ok();
if let Some(cookie) = maybe_new_avt_cookie {
response_builder.cookie(cookie);
}
Ok(response_builder
.content_type("application/json")
.body(body))
}
fn ok_response() -> Result<HttpResponse, CommonWebError> {
let response = LogWebReferralResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|_| CommonWebError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
fn extract_query_param(parsed: Option<&Url>, key: &str) -> Option<String> {
parsed.and_then(|u| {
u.query_pairs()
.find(|(k, _)| k == key)
.map(|(_, v)| v.to_string())
})
}
@@ -0,0 +1 @@
pub mod log_web_referral_handler;
@@ -14,6 +14,7 @@ use crate::http_server::routes::application_routes::user_bookmarks_routes::add_u
use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes;
use crate::http_server::routes::application_routes::user_routes::add_user_routes;
use crate::http_server::routes::application_routes::wallet_routes::add_wallet_routes;
use crate::http_server::routes::application_routes::web_referrals_routes::add_web_referrals_routes;
use crate::http_server::routes::application_routes::webhook_routes::add_webhook_routes;
use crate::http_server::routes::application_routes::weights_routes::add_weights_routes;
use actix_http::body::MessageBody;
@@ -55,6 +56,7 @@ where
// Other useful tools
app = add_analytics_routes(app); // /v1/analytics/...
app = add_web_referrals_routes(app); // /v1/web_referrals/...
// User and user-adjacent routes
app = add_comments_routes(app); // /v1/comments/...
@@ -14,6 +14,7 @@ mod user_bookmarks_routes;
mod user_rating_routes;
mod user_routes;
mod wallet_routes;
mod web_referrals_routes;
mod webhook_routes;
mod weights_routes;
pub (super) mod add_application_routes;
@@ -0,0 +1,26 @@
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::error::Error;
use actix_web::{web, App, HttpResponse};
use crate::http_server::endpoints::web_referrals::log_web_referral_handler::log_web_referral_handler;
pub fn add_web_referrals_routes<T, B> (app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
{
app.service(web::scope("/v1/web_referrals")
.service(web::resource("/record")
.route(web::post().to(log_web_referral_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}