new logging endpoint (#999)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-01-21 14:01:34 -05:00
1 parent 67f428eee7
commit f772f93f90
27 files changed
+696 -24

No files matched your search

@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO analytics_app_sessions\nSET\n session_token = ?,\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n total_generation_count = ?,\n image_generation_count = ?,\n video_generation_count = ?,\n object_generation_count = ?,\n text_to_image_count = ?,\n image_to_image_count = ?,\n text_to_video_count = ?,\n image_to_video_count = ?,\n text_to_object_count = ?,\n image_to_object_count = ?,\n image_page_prompt_count = ?,\n video_page_prompt_count = ?,\n edit_page_prompt_count = ?,\n stage_page_prompt_count = ?,\n object_page_prompt_count = ?,\n other_page_prompt_count = ?,\n first_active_at = NOW(),\n last_active_at = NOW()\nON DUPLICATE KEY UPDATE\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n total_generation_count = ?,\n image_generation_count = ?,\n video_generation_count = ?,\n object_generation_count = ?,\n text_to_image_count = ?,\n image_to_image_count = ?,\n text_to_video_count = ?,\n image_to_video_count = ?,\n text_to_object_count = ?,\n image_to_object_count = ?,\n image_page_prompt_count = ?,\n video_page_prompt_count = ?,\n edit_page_prompt_count = ?,\n stage_page_prompt_count = ?,\n object_page_prompt_count = ?,\n other_page_prompt_count = ?,\n last_active_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 47
},
"nullable": []
},
"hash": "4b3d363a4f5a088d2040cedd010e66964ea4d5e3b3a94aaad79bc122ea165715"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO analytics_app_sessions\nSET\n session_token = ?,\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n first_active_at = NOW(),\n last_active_at = NOW()\nON DUPLICATE KEY UPDATE\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n last_active_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 15
},
"nullable": []
},
"hash": "ff39576a5ec6a23c67f6f61e7eba8392e70b05c42b540bea260a9a4cdc478a5b"
}
@@ -2,32 +2,109 @@ use serde_derive::{Deserialize, Serialize};
use tokens::tokens::app_session::AppSessionToken;
use utoipa::ToSchema;
pub const LOG_ACTIVE_USER_PATH: &str = "/v1/analytics/active_user";
/// This is used in Artcraft versions 0.0.1 (several) through 0.3.0
/// Clients to this version do not send generation count data (even if the endpoint supports it).
pub const LOG_ACTIVE_USER_V1_PATH: &str = "/v1/analytics/active_user";
#[derive(Serialize, Deserialize, ToSchema, Debug)]
/// This is used in Artcraft versions 0.4.0 and later.
pub const LOG_ACTIVE_USER_V2_PATH: &str = "/v1/analytics/active_user_v2";
#[derive(Serialize, Deserialize, ToSchema, Debug, Default)]
pub struct LogAppActiveUserRequest {
/// Clientside-generated session token.
/// If present, it'll be validated against an expected format.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_session_token: Option<AppSessionToken>,
/// An override for the application platform/OS (windows, mac, linux).
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_os_platform: Option<String>,
/// An override for the version of the OS (e.g. 10.15.7, 11, 22.04).
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_os_version: Option<String>,
/// An override for the application name.
/// If set together with `maybe_app_version`, the two will be
/// If set together with `maybe_app_version`, the two will be
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_name: Option<String>,
/// An override for the application version.
/// If set together with `maybe_app_name`, the two will be
/// If set together with `maybe_app_name`, the two will be
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_app_version: Option<String>,
/// How long the user has been active in the app, in seconds.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_session_duration_seconds: Option<u64>,
// ========== GENERATION COUNTS ==========
/// Number of items generated across all types.
#[serde(skip_serializing_if = "Option::is_none")]
pub total_generation_count: Option<u16>,
/// Number of images generated.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_generation_count: Option<u16>,
/// Number of videos generated.
#[serde(skip_serializing_if = "Option::is_none")]
pub video_generation_count: Option<u16>,
/// Number of objects generated (meshes, etc.).
#[serde(skip_serializing_if = "Option::is_none")]
pub object_generation_count: Option<u16>,
/// Number of images generated with text to image.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_image_count: Option<u16>,
/// Number of images generated with image to image (image edits).
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_image_count: Option<u16>,
/// Number of videos generated with text to video.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_video_count: Option<u16>,
/// Number of videos generated with image to video.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_video_count: Option<u16>,
/// Number of objects generated with text to object.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_object_count: Option<u16>,
/// Number of objects generated with image to object.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_to_object_count: Option<u16>,
/// Number of prompts from the image page.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_page_prompt_count: Option<u16>,
/// Number of prompts from the video page.
#[serde(skip_serializing_if = "Option::is_none")]
pub video_page_prompt_count: Option<u16>,
/// Number of prompts from the edit page.
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_page_prompt_count: Option<u16>,
/// Number of prompts from the stage page.
#[serde(skip_serializing_if = "Option::is_none")]
pub stage_page_prompt_count: Option<u16>,
/// Number of prompts from the object page.
#[serde(skip_serializing_if = "Option::is_none")]
pub object_page_prompt_count: Option<u16>,
/// Number of prompts from other pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub other_page_prompt_count: Option<u16>,
}
#[derive(Serialize, Deserialize, ToSchema)]
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_query_string_post_request::basic_query_string_post_request;
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_PATH};
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_V1_PATH};
use std::collections::HashMap;
pub async fn log_active_user(
@@ -41,7 +41,7 @@ pub async fn log_active_user(
Ok(basic_query_string_post_request(
api_host,
LOG_ACTIVE_USER_PATH,
LOG_ACTIVE_USER_V1_PATH,
maybe_creds,
&query,
).await?)
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse, LOG_ACTIVE_USER_V2_PATH};
pub async fn log_active_user_v2(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: LogAppActiveUserRequest,
) -> Result<LogAppActiveUserResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
LOG_ACTIVE_USER_V2_PATH,
maybe_creds,
&request,
).await?)
}
@@ -1 +1,2 @@
pub mod log_active_user;
pub mod log_active_user;
pub mod log_active_user_v2;
@@ -12,6 +12,8 @@ use crate::core::events::generation_events::generation_enqueue_failure_event::Ge
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::events::sendable_event_trait::SendableEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
@@ -97,6 +99,7 @@ pub async fn enqueue_image_bg_removal_command(
request: EnqueueImageBgRemovalCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -109,6 +112,7 @@ pub async fn enqueue_image_bg_removal_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -157,6 +161,7 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -185,6 +190,11 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_image_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -43,6 +43,8 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -204,6 +206,7 @@ pub async fn enqueue_edit_image_command(
request: EnqueueEditImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -219,6 +222,7 @@ pub async fn enqueue_edit_image_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -269,6 +273,7 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -342,6 +347,13 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -41,6 +41,8 @@ use storyteller_client::error::storyteller_error::StorytellerError;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
@@ -155,6 +157,7 @@ pub async fn enqueue_image_inpaint_command(
request: EnqueueInpaintImageCommand,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -170,6 +173,7 @@ pub async fn enqueue_image_inpaint_command(
&app,
&app_data_root,
&app_env_configs,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -220,6 +224,7 @@ pub async fn handle_request(
app: &AppHandle,
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -272,6 +277,13 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
// TODO: We don't know that this is strictly the edit page. It could be the stage. It might also be nice to distinguish inpainting.
let num_images = request.image_count.map(|count| count as u16).unwrap_or(1);
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::EditPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -30,6 +30,8 @@ use serde::{Deserialize, Serialize};
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -97,6 +99,7 @@ pub async fn enqueue_image_to_gaussian_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
worldlabs_creds_manager: State<'_, WorldlabsCredentialManager>,
@@ -108,6 +111,7 @@ pub async fn enqueue_image_to_gaussian_command(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&task_database,
&storyteller_creds_manager,
&worldlabs_creds_manager,
@@ -168,6 +172,7 @@ pub async fn handle_request(
request: EnqueueImageToGaussianRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
worldlabs_creds_manager: &WorldlabsCredentialManager,
@@ -201,7 +206,12 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::OtherPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -19,6 +19,8 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
#[derive(Deserialize)]
pub struct EnqueueImageTo3dObjectRequest {
@@ -88,6 +90,7 @@ pub async fn enqueue_image_to_3d_object_command(
request: EnqueueImageTo3dObjectRequest,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
@@ -101,6 +104,7 @@ pub async fn enqueue_image_to_3d_object_command(
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&storyteller_creds_manager,
@@ -171,6 +175,7 @@ pub async fn handle_request(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -203,6 +208,10 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::ObjectPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -14,6 +14,7 @@ use crate::core::events::generation_events::common::GenerationAction;
use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
@@ -27,6 +28,7 @@ use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -175,6 +177,7 @@ pub async fn enqueue_image_to_video_command(
app: AppHandle,
app_env_configs: State<'_, AppEnvConfigs>,
app_data_root: State<'_, AppDataRoot>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
grok_creds_manager: State<'_, GrokCredentialManager>,
@@ -190,6 +193,7 @@ pub async fn enqueue_image_to_video_command(
&app,
&app_env_configs,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&grok_creds_manager,
@@ -262,6 +266,7 @@ pub async fn handle_request(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
grok_creds_manager: &GrokCredentialManager,
@@ -331,5 +336,10 @@ pub async fn handle_request(
// NB: Fail open, but find a way to flag this.
}
if let Err(err) = artcraft_usage_tracker.record_video_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::VideoPage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -35,6 +35,8 @@ use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::api_adapters::aspect_ratio::common_aspect_ratio::CommonAspectRatio;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// This is used in the Tauri command bridge.
/// Don't change the serializations without coordinating with the frontend.
@@ -179,6 +181,7 @@ pub async fn enqueue_text_to_image_command(
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
app_env_configs: State<'_, AppEnvConfigs>,
artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>,
provider_priority_store: State<'_, ProviderPriorityStore>,
task_database: State<'_, TaskDatabase>,
mj_creds_manager: State<'_, MidjourneyCredentialManager>,
@@ -197,6 +200,7 @@ pub async fn enqueue_text_to_image_command(
request,
&app,
&app_data_root,
&artcraft_usage_tracker,
&provider_priority_store,
&task_database,
&mj_creds_manager,
@@ -267,6 +271,7 @@ pub async fn handle_request(
request: EnqueueTextToImageRequest,
app: &AppHandle,
app_data_root: &AppDataRoot,
artcraft_usage_tracker: &ArtcraftUsageTracker,
provider_priority_store: &ProviderPriorityStore,
task_database: &TaskDatabase,
mj_creds_manager: &MidjourneyCredentialManager,
@@ -310,6 +315,24 @@ pub async fn handle_request(
error!("Failed to create task in database: {:?}", err);
// NB: Fail open, but find a way to flag this.
}
let num_images = request.number_images.map(|n| n as u16).unwrap_or(1);
let is_image_to_image = request.image_media_tokens
.as_ref()
.map(|tokens| !tokens.is_empty())
.unwrap_or(false);
let usage_type = if is_image_to_image {
ArtcraftUsageType::ImageToResult
} else {
ArtcraftUsageType::TextToResult
};
if let Err(err) = artcraft_usage_tracker.record_image_generation(num_images, usage_type, ArtcraftUsagePage::ImagePage) {
// NB: Fail open.
warn!("Failed to report usage: {:?}", err);
}
Ok(success_event)
}
@@ -8,6 +8,7 @@ use crate::core::lifecycle::startup::tasks::spawn_sora_task_polling_thread::spaw
use crate::core::lifecycle::startup::tasks::spawn_storyteller_threads::spawn_storyteller_threads;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::services::grok::state::grok_credential_manager::GrokCredentialManager;
use crate::services::grok::state::grok_image_prompt_queue::GrokImagePromptQueue;
@@ -29,6 +30,7 @@ pub async fn handle_tauri_startup(
root: AppDataRoot,
app_env_configs: AppEnvConfigs,
artcraft_platform_info: ArtcraftPlatformInfo,
artcraft_usage_tracker: ArtcraftUsageTracker,
storyteller_creds_manager: StorytellerCredentialManager,
sora_credential_manager: SoraCredentialManager,
sora_task_queue: SoraTaskQueue,
@@ -61,6 +63,7 @@ pub async fn handle_tauri_startup(
spawn_storyteller_threads(
&app,
&app_env_configs,
&artcraft_usage_tracker,
&artcraft_platform_info,
&task_database,
&storyteller_creds_manager,
@@ -1,5 +1,6 @@
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::artcraft_platform_info::ArtcraftPlatformInfo;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
use crate::core::state::task_database::TaskDatabase;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use crate::services::storyteller::threads::storyteller_activity_thread::storyteller_activity_thread;
@@ -10,6 +11,7 @@ use tauri::{AppHandle, Manager};
pub fn spawn_storyteller_threads(
app: &AppHandle,
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
artcraft_platform_info: &ArtcraftPlatformInfo,
task_database: &TaskDatabase,
storyteller_creds_manager: &StorytellerCredentialManager,
@@ -25,6 +27,7 @@ pub fn spawn_storyteller_threads(
tauri::async_runtime::spawn(storyteller_activity_thread(
app_env_configs.clone(),
artcraft_platform_info.clone(),
artcraft_usage_tracker.clone(),
storyteller_creds_manager.clone(),
));
@@ -0,0 +1,135 @@
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker_data::ArtcraftUsageTrackerData;
use std::sync::{Arc, LockResult, RwLock};
use log::{error, warn};
use crate::core::artcraft_error::ArtcraftError;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType};
/// Track generation volume so we know which areas to focus efforts on.
/// But also note: https://en.wikipedia.org/wiki/Survivorship_bias
#[derive(Clone)]
pub struct ArtcraftUsageTracker {
data: Arc<RwLock<ArtcraftUsageTrackerData>>,
}
impl ArtcraftUsageTracker {
pub fn new() -> Self {
Self {
data: Arc::new(RwLock::new(ArtcraftUsageTrackerData::new())),
}
}
pub fn record_image_generation(&self, num_images: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_images;
data.image_generation_count += num_images;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_image_count += num_images;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_image_count += num_images;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::ImagePage => {
data.image_page_prompt_count += num_images;
}
ArtcraftUsagePage::EditPage => {
data.edit_page_prompt_count += num_images;
}
ArtcraftUsagePage::StagePage => {
data.stage_page_prompt_count += num_images;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_images;
}
ArtcraftUsagePage::VideoPage => {} // NB: Does not generate images.
ArtcraftUsagePage::ObjectPage => {} // NB: Does not generate images.
}
self.set(data)?;
Ok(())
}
pub fn record_video_generation(&self, num_videos: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_videos;
data.video_generation_count += num_videos;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_video_count += num_videos;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_video_count += num_videos;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::VideoPage => {
data.video_page_prompt_count += num_videos;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_videos;
}
ArtcraftUsagePage::ImagePage => {} // NB: Does not generate videos.
ArtcraftUsagePage::EditPage => {} // NB: Does not generate videos.
ArtcraftUsagePage::StagePage => {} // NB: Does not generate videos.
ArtcraftUsagePage::ObjectPage => {} // NB: Does not generate videos.
}
self.set(data)?;
Ok(())
}
pub fn record_object_generation(&self, num_objects: u16, usage_type: ArtcraftUsageType, page: ArtcraftUsagePage) -> Result<(), ArtcraftError> {
let mut data = self.get()?;
data.total_generation_count += num_objects;
data.object_generation_count += num_objects;
match usage_type {
ArtcraftUsageType::TextToResult => {
data.text_to_object_count += num_objects;
}
ArtcraftUsageType::ImageToResult => {
data.image_to_object_count += num_objects;
}
ArtcraftUsageType::Other => {}
}
match page {
ArtcraftUsagePage::ObjectPage => {
data.object_page_prompt_count += num_objects;
}
ArtcraftUsagePage::OtherPage => {
data.other_page_prompt_count += num_objects;
}
ArtcraftUsagePage::ImagePage => {} // NB: Does not generate objects.
ArtcraftUsagePage::VideoPage => {} // NB: Does not generate objects.
ArtcraftUsagePage::EditPage => {} // NB: Does not generate objects.
ArtcraftUsagePage::StagePage => {} // NB: Does not generate objects.
}
self.set(data)?;
Ok(())
}
pub fn get(&self) -> Result<ArtcraftUsageTrackerData, ArtcraftError> {
match self.data.read() {
Ok(data) => Ok(data.clone()),
Err(err) => {
error!("Lock read error: {:?}", err);
Err(ArtcraftError::RwLockReadError)
}
}
}
fn set(&self, data: ArtcraftUsageTrackerData) -> Result<(), ArtcraftError> {
match self.data.write() {
Ok(mut lock) => {
*lock = data;
Ok(())
},
Err(err) => {
error!("Lock write error: {:?}", err);
Err(ArtcraftError::RwLockWriteError)
}
}
}
}
@@ -0,0 +1,46 @@
/// Track generation volume so we know which areas to focus efforts on.
/// But also note: https://en.wikipedia.org/wiki/Survivorship_bias
#[derive(Clone)]
pub struct ArtcraftUsageTrackerData {
pub total_generation_count: u16,
pub image_generation_count: u16,
pub video_generation_count: u16,
pub object_generation_count: u16,
pub text_to_image_count: u16,
pub image_to_image_count: u16,
pub text_to_video_count: u16,
pub image_to_video_count: u16,
pub text_to_object_count: u16,
pub image_to_object_count: u16,
pub image_page_prompt_count: u16,
pub video_page_prompt_count: u16,
pub edit_page_prompt_count: u16,
pub stage_page_prompt_count: u16,
pub object_page_prompt_count: u16,
pub other_page_prompt_count: u16,
}
impl ArtcraftUsageTrackerData {
pub fn new() -> Self {
Self {
total_generation_count: 0,
image_generation_count: 0,
video_generation_count: 0,
object_generation_count: 0,
text_to_image_count: 0,
image_to_image_count: 0,
text_to_video_count: 0,
image_to_video_count: 0,
text_to_object_count: 0,
image_to_object_count: 0,
image_page_prompt_count: 0,
video_page_prompt_count: 0,
edit_page_prompt_count: 0,
stage_page_prompt_count: 0,
object_page_prompt_count: 0,
other_page_prompt_count: 0,
}
}
}
@@ -0,0 +1,18 @@
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArtcraftUsageType {
TextToResult,
ImageToResult,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArtcraftUsagePage {
ImagePage,
VideoPage,
EditPage,
StagePage,
ObjectPage,
OtherPage,
}
@@ -0,0 +1,3 @@
pub mod artcraft_usage_tracker;
pub mod artcraft_usage_type;
mod artcraft_usage_tracker_data;
@@ -2,6 +2,7 @@ pub mod app_env_configs;
pub mod app_preferences;
pub mod app_startup_time;
pub mod artcraft_platform_info;
pub mod artcraft_usage_tracker;
pub mod data_dir;
pub mod expanduser;
pub mod os_platform;
+6
View File
@@ -72,6 +72,7 @@ use tauri_plugin_dialog;
use tauri_plugin_http;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -124,6 +125,9 @@ pub fn run() {
let worldlabs_bearer_bridge = WorldlabsBearerBridge::empty();
let worldlabs_bearer_bridge_2 = worldlabs_bearer_bridge.clone();
let artcraft_usage_tracker = ArtcraftUsageTracker::new();
let artcraft_usage_tracker_2 = artcraft_usage_tracker.clone();
println!("Initializing backend runtime...");
@@ -159,6 +163,7 @@ pub fn run() {
root,
env_config,
artcraft_platform_info_2,
artcraft_usage_tracker_2,
storyteller_creds,
sora_creds,
sora_tasks,
@@ -181,6 +186,7 @@ pub fn run() {
.manage(app_env_configs)
.manage(app_preferences)
.manage(artcraft_platform_info)
.manage(artcraft_usage_tracker)
.manage(grok_creds_manager)
.manage(grok_prompt_queue)
.manage(midjourney_creds_manager)
@@ -22,12 +22,14 @@ use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskAr
use std::time::Instant;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::endpoints::analytics::log_active_user::log_active_user;
use storyteller_client::endpoints::analytics::log_active_user_v2::log_active_user_v2;
use storyteller_client::endpoints::jobs::list_session_jobs::{list_session_jobs, States};
use storyteller_client::endpoints::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::storyteller_error::StorytellerError;
use tauri::AppHandle;
use tokens::tokens::app_session::AppSessionToken;
use crate::core::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker;
// TODO: Configure this with the build and increment.
const CLIENT_NAME : &str = "artcraft";
@@ -38,6 +40,7 @@ const ERROR_SLEEP_MILLIS : u64 = 1_000 * 60 * 3; // 3 minutes;
pub async fn storyteller_activity_thread(
app_env_configs: AppEnvConfigs,
artcraft_platform_info: ArtcraftPlatformInfo,
artcraft_usage_tracker: ArtcraftUsageTracker,
storyteller_creds_manager: StorytellerCredentialManager,
) -> ! {
let startup = Instant::now();
@@ -48,6 +51,7 @@ pub async fn storyteller_activity_thread(
loop {
let res = polling_loop(
&app_env_configs,
&artcraft_usage_tracker,
&storyteller_creds_manager,
startup,
&artcraft_platform_info,
@@ -63,6 +67,7 @@ pub async fn storyteller_activity_thread(
async fn polling_loop(
app_env_configs: &AppEnvConfigs,
artcraft_usage_tracker: &ArtcraftUsageTracker,
storyteller_creds_manager: &StorytellerCredentialManager,
startup: Instant,
artcraft_platform_info: &ArtcraftPlatformInfo,
@@ -85,6 +90,8 @@ async fn polling_loop(
},
};
let usage_data = artcraft_usage_tracker.get()?;
let time_since_startup = Instant::now().duration_since(startup);
let request = LogAppActiveUserRequest {
@@ -94,11 +101,27 @@ async fn polling_loop(
maybe_os_platform: Some(artcraft_platform_info.os_platform.as_str().to_owned()),
maybe_os_version: Some(artcraft_platform_info.os_version.clone()),
maybe_session_duration_seconds: Some(time_since_startup.as_secs()),
total_generation_count: Some(usage_data.total_generation_count),
image_generation_count: Some(usage_data.image_generation_count),
video_generation_count: Some(usage_data.video_generation_count),
object_generation_count: Some(usage_data.object_generation_count),
text_to_image_count: Some(usage_data.text_to_image_count),
image_to_image_count: Some(usage_data.image_to_image_count),
text_to_video_count: Some(usage_data.text_to_video_count),
image_to_video_count: Some(usage_data.image_to_video_count),
text_to_object_count: Some(usage_data.text_to_object_count),
image_to_object_count: Some(usage_data.image_to_object_count),
image_page_prompt_count: Some(usage_data.image_page_prompt_count),
video_page_prompt_count: Some(usage_data.video_page_prompt_count),
edit_page_prompt_count: Some(usage_data.edit_page_prompt_count),
stage_page_prompt_count: Some(usage_data.stage_page_prompt_count),
object_page_prompt_count: Some(usage_data.object_page_prompt_count),
other_page_prompt_count: Some(usage_data.other_page_prompt_count),
};
debug!("Logging active user with storyteller.");
let result = log_active_user(
let result = log_active_user_v2(
&app_env_configs.storyteller_host,
Some(&creds),
request,
@@ -18,6 +18,23 @@ pub struct UpsertAnalyticsAppSession<'a> {
pub os_platform: Option<&'a str>,
pub os_version: Option<&'a str>,
pub session_duration_seconds: Option<u64>,
// Generation counts
pub total_generation_count: u16,
pub image_generation_count: u16,
pub video_generation_count: u16,
pub object_generation_count: u16,
pub text_to_image_count: u16,
pub image_to_image_count: u16,
pub text_to_video_count: u16,
pub image_to_video_count: u16,
pub text_to_object_count: u16,
pub image_to_object_count: u16,
pub image_page_prompt_count: u16,
pub video_page_prompt_count: u16,
pub edit_page_prompt_count: u16,
pub stage_page_prompt_count: u16,
pub object_page_prompt_count: u16,
pub other_page_prompt_count: u16,
}
impl <'a> UpsertAnalyticsAppSession<'a> {
@@ -35,6 +52,22 @@ SET
session_duration_seconds = ?,
ip_address = ?,
measurement_count = measurement_count + 1,
total_generation_count = ?,
image_generation_count = ?,
video_generation_count = ?,
object_generation_count = ?,
text_to_image_count = ?,
image_to_image_count = ?,
text_to_video_count = ?,
image_to_video_count = ?,
text_to_object_count = ?,
image_to_object_count = ?,
image_page_prompt_count = ?,
video_page_prompt_count = ?,
edit_page_prompt_count = ?,
stage_page_prompt_count = ?,
object_page_prompt_count = ?,
other_page_prompt_count = ?,
first_active_at = NOW(),
last_active_at = NOW()
ON DUPLICATE KEY UPDATE
@@ -46,6 +79,22 @@ ON DUPLICATE KEY UPDATE
session_duration_seconds = ?,
ip_address = ?,
measurement_count = measurement_count + 1,
total_generation_count = ?,
image_generation_count = ?,
video_generation_count = ?,
object_generation_count = ?,
text_to_image_count = ?,
image_to_image_count = ?,
text_to_video_count = ?,
image_to_video_count = ?,
text_to_object_count = ?,
image_to_object_count = ?,
image_page_prompt_count = ?,
video_page_prompt_count = ?,
edit_page_prompt_count = ?,
stage_page_prompt_count = ?,
object_page_prompt_count = ?,
other_page_prompt_count = ?,
last_active_at = NOW()
"#,
// Insert case
@@ -57,6 +106,22 @@ ON DUPLICATE KEY UPDATE
self.os_version,
self.session_duration_seconds,
self.ip_address,
self.total_generation_count,
self.image_generation_count,
self.video_generation_count,
self.object_generation_count,
self.text_to_image_count,
self.image_to_image_count,
self.text_to_video_count,
self.image_to_video_count,
self.text_to_object_count,
self.image_to_object_count,
self.image_page_prompt_count,
self.video_page_prompt_count,
self.edit_page_prompt_count,
self.stage_page_prompt_count,
self.object_page_prompt_count,
self.other_page_prompt_count,
// Update case
self.namespace.to_str(),
self.user_token.as_str(),
@@ -65,6 +130,22 @@ ON DUPLICATE KEY UPDATE
self.os_version,
self.session_duration_seconds,
self.ip_address,
self.total_generation_count,
self.image_generation_count,
self.video_generation_count,
self.object_generation_count,
self.text_to_image_count,
self.image_to_image_count,
self.text_to_video_count,
self.image_to_video_count,
self.text_to_object_count,
self.image_to_object_count,
self.image_page_prompt_count,
self.video_page_prompt_count,
self.edit_page_prompt_count,
self.stage_page_prompt_count,
self.object_page_prompt_count,
self.other_page_prompt_count,
)
}
@@ -116,6 +116,22 @@ pub async fn log_app_active_user_handler(
os_platform: request.maybe_os_platform.as_deref(),
os_version: request.maybe_os_version.as_deref(),
session_duration_seconds: request.maybe_session_duration_seconds,
total_generation_count: 0,
image_generation_count: 0,
video_generation_count: 0,
object_generation_count: 0,
text_to_image_count: 0,
image_to_image_count: 0,
text_to_video_count: 0,
image_to_video_count: 0,
text_to_object_count: 0,
image_to_object_count: 0,
image_page_prompt_count: 0,
video_page_prompt_count: 0,
edit_page_prompt_count: 0,
stage_page_prompt_count: 0,
object_page_prompt_count: 0,
other_page_prompt_count: 0,
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
@@ -0,0 +1,149 @@
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
use actix_helpers::extractors::get_request_user_agent::get_request_user_agent;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::analytics::log_active_user::{LogAppActiveUserRequest, LogAppActiveUserResponse};
use enums::common::payments_namespace::PaymentsNamespace;
use http_server_common::request::get_request_ip::get_request_ip;
use log::{info, warn};
use mysql_queries::queries::analytics_active_users::upsert_analytics_app_active_user::UpsertAnalyticsAppActiveUser;
use mysql_queries::queries::analytics_active_users::upsert_analytics_app_session::UpsertAnalyticsAppSession;
use tokens::tokens::app_session::AppSessionToken;
const CLIENT_WAIT_FOR_RETRY_MILLIS: u64 = 1_000 * 60 * 1; // Every minute
/// Log an active app user (JSON body version) - user must be logged in.
#[utoipa::path(
post,
tag = "Analytics",
path = "/v1/analytics/active_user_v2",
request_body = LogAppActiveUserRequest,
responses(
(status = 200, description = "Success", body = LogAppActiveUserSuccessResponse),
),
)]
pub async fn log_app_active_user_json_handler(
http_request: HttpRequest,
request: Json<LogAppActiveUserRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<LogAppActiveUserResponse>, CommonWebError>
{
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await?;
let user_token = maybe_user_session
.ok_or(CommonWebError::NotAuthorized)?
.user_token;
let ip_address = get_request_ip(&http_request);
info!("Logging active user (JSON): {:?}", request);
let app_version = {
let user_agent = get_request_user_agent(&http_request);
let mut maybe_recorded_version = None;
if let Some(user_agent) = user_agent {
maybe_recorded_version = Some(user_agent.to_string());
};
let pair = (
request.maybe_app_name.as_deref(),
request.maybe_app_version.as_deref(),
);
match pair {
(Some(name), Some(version)) => {
let version = version.trim().to_string();
let name = name.trim().to_string();
if !version.is_empty() && !name.is_empty() {
maybe_recorded_version = Some(format!("{}/{}", name, version));
}
},
(Some(name), None) => {
let name = name.trim().to_string();
if !name.is_empty() {
maybe_recorded_version = Some(name);
}
},
(None, Some(version)) => {
let version = version.trim().to_string();
if !version.is_empty() {
maybe_recorded_version = Some(version);
}
},
_ => {}
}
maybe_recorded_version
};
let upsert = UpsertAnalyticsAppActiveUser {
namespace: PaymentsNamespace::Artcraft,
user_token: &user_token,
ip_address: &ip_address,
app_version: app_version.as_deref(),
os_platform: request.maybe_os_platform.as_deref(),
os_version: request.maybe_os_version.as_deref(),
session_duration_seconds: request.maybe_session_duration_seconds,
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
if let Some(token) = request.maybe_app_session_token.as_ref() {
validate_app_session_token_format(token)?;
let upsert = UpsertAnalyticsAppSession{
app_session_token: token,
namespace: PaymentsNamespace::Artcraft,
user_token: &user_token,
ip_address: &ip_address,
app_version: app_version.as_deref(),
os_platform: request.maybe_os_platform.as_deref(),
os_version: request.maybe_os_version.as_deref(),
session_duration_seconds: request.maybe_session_duration_seconds,
total_generation_count: request.total_generation_count.unwrap_or(0),
image_generation_count: request.image_generation_count.unwrap_or(0),
video_generation_count: request.video_generation_count.unwrap_or(0),
object_generation_count: request.object_generation_count.unwrap_or(0),
text_to_image_count: request.text_to_image_count.unwrap_or(0),
image_to_image_count: request.image_to_image_count.unwrap_or(0),
text_to_video_count: request.text_to_video_count.unwrap_or(0),
image_to_video_count: request.image_to_video_count.unwrap_or(0),
text_to_object_count: request.text_to_object_count.unwrap_or(0),
image_to_object_count: request.image_to_object_count.unwrap_or(0),
image_page_prompt_count: request.image_page_prompt_count.unwrap_or(0),
video_page_prompt_count: request.video_page_prompt_count.unwrap_or(0),
edit_page_prompt_count: request.edit_page_prompt_count.unwrap_or(0),
stage_page_prompt_count: request.stage_page_prompt_count.unwrap_or(0),
object_page_prompt_count: request.object_page_prompt_count.unwrap_or(0),
other_page_prompt_count: request.other_page_prompt_count.unwrap_or(0),
};
upsert.upsert_with_connection(&mut mysql_connection).await?;
}
Ok(Json(LogAppActiveUserResponse {
success: true,
wait_for_retry_millis: CLIENT_WAIT_FOR_RETRY_MILLIS,
}))
}
fn validate_app_session_token_format(app_session_token: &AppSessionToken) -> Result<(), CommonWebError> {
if !app_session_token.as_str().starts_with(AppSessionToken::token_prefix()) {
warn!("App session token has invalid prefix: {}", app_session_token.as_str());
return Err(CommonWebError::BadInputWithSimpleMessage("Invalid app session token format".to_string()));
}
Ok(())
}
@@ -1,2 +1,3 @@
pub mod log_browser_session_handler;
pub mod log_app_active_user_handler;
pub mod log_app_active_user_handler;
pub mod log_app_active_user_json_handler;
@@ -1,4 +1,5 @@
use crate::http_server::endpoints::analytics::log_app_active_user_handler::log_app_active_user_handler;
use crate::http_server::endpoints::analytics::log_app_active_user_json_handler::log_app_active_user_json_handler;
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
@@ -21,5 +22,9 @@ where
.route(web::post().to(log_app_active_user_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/active_user_v2")
.route(web::post().to(log_app_active_user_json_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}