Cartographie du Pipeline ETL Hozana
Générée le 2026-03-24 par analyse exhaustive du code source, du backend Symfony, de l'infrastructure /infra/. etl/main.py : 76 tasks enregistrées.
Diagrammes
Data Lineage Global
feed_items"] end subgraph EXT["Externes"] GA4["GA4"] MC["Mailchimp"] GS["GSheets"] end subgraph DATA["Serveur Data"] ETL_DB["ETL MySQL"] ETL_P["ETL Python"] MB["Metabase"] DT["data_*"] end PROD -->|dump| ETL_DB EXT --> ETL_P ETL_DB --> ETL_P ETL_P --> ETL_DB ETL_DB --> MB ETL_P --> DT DT -->|"RENAME TABLE"| PROD
Cycle Nightly
Flux Bidirectionnel
Dependances inter-tasks
data_publications
data_communities
data_announcements"] end subgraph PROD_DB["Production DB"] TABLES["users, prayers,
communities, etc."] ME["mail_events"] FI["feed_items"] SESS["sessions"] DTABLES["data_users, data_publications,
data_communities, data_announcements"] end APP --> TABLES WH --> ME APP --> FI APP --> SESS CRON -->|clean daily| ME CRON -->|clean daily| FI TABLES -->|dump 22h| DUMP ME -->|dump 22h| DUMP FI -->|dump 22h| DUMP SESS -->|dump 22h| DUMP DUMP --> ETL ETL --> DATA DATA -->|scp + atomic swap| DTABLES style DATA fill:#DFF7DA,stroke:#3D7732,color:#16181A style DTABLES fill:#DFF7DA,stroke:#3D7732,color:#16181A click WH call openNode("infra:prod_hozana") click APP call openNode("infra:prod_hozana") click CRON call openNode("infra:prod_hozana") click API call openNode("infra:prod_hozana") click DUMP call openNode("infra:dump_files") click ETL call openNode("infra:etl_pipeline") click DATA call openNode("infra:data_tables_node") click TABLES call openNode("infra:hozana_main") click ME call openNode("infra:archive_db") click FI call openNode("infra:archive_db") click SESS call openNode("infra:hozana_main") click DTABLES call openNode("infra:data_users_node")
daily
L: subscriptions, publications
W: subscriptions, publications, r.participation, r.prayer"] Lang["lang
daily
L: users, prayers, sessions, messages
R: users
W: prayers, sessions, messages, user_referrals"] CrmOrders["crm_orders
daily
R: orders, contacts
W: orders"] FeedItems["feed_items
daily
L: feed_items, subscriptions, publications
R: a.feed_items, publications, subscriptions
W: a.feed_items"] end subgraph USERS["Utilisateurs"] direction TB UsersAge["users_age
daily
L: users
R: users
W: users"] UsersCC["users_country_code
daily
L: users
R: users
W: users"] UsersTargetZone["users_target_zone
daily
L: users
R: users
W: users"] UsersSubs["users_subscriptions
daily
L: users, subscriptions
R: users, subscriptions
W: users, subscriptions"] UsersUTM["users_utm
daily
L: users
R: users, user_referrals, utm_normalized_lookup
W: users"] UsersIndices["users_indices
daily
L: users
W: users"] UniqueUsers["unique_users
daily
L: mail_events, sessions
R: a.mail_events, sessions, users
W: unique_open_emails, unique_users, alive_subscribers"] end subgraph UTM_GROUP["UTM & Sources"] direction TB SubsUTM["subscriptions_utm
daily
L: subscriptions, users
R: subscriptions, users
W: subscriptions"] PresubUTM["presubscribers_utm
daily
L: users, subscriptions
R: presubscribers, users, subscriptions
W: presubscribers, users, subscriptions"] end subgraph COMMUNITY["Communautes"] direction TB ReadRate["read_rate
daily ✓
L: publications, subscriptions
R: subscriptions, publications, a.feed_items, communities
W: publication_reads, community_reads, community_first_reads"] PubStats["publication_stats
daily
L: publications
R: subscriptions, publications, a.feed_items
W: publications"] GACom["ga_communities
daily
R: GA4 API
W: a.ga_communities"] GAPub["ga_publications
daily
L: publications
R: GA4 API
W: a.ga_publications, publications"] GASubs["ga_subscriptions_page_views
daily
R: GA4 API, utm_normalized_lookup
W: ga_subscriptions_page_views"] CommTransfo["community_transformation_rate
daily
R: ga_subscriptions_page_views
W: community_join_groups"] ComStats["community_stats
daily ✓
L: communities
R: publications, communities, community_first_reads, a.ga_communities, a.mail_events, community_joined_emails, community_join_groups, new_community_message_emails
W: communities, community_joined_emails, daily_community_shares, a.shares_total"] end subgraph CRM["CRM"] direction TB CrmContacts["crm_contacts
daily
R: contacts, users, orders
W: contacts, contacts_hu, contacts_ru"] end subgraph ANALYTICS["Analytics & Segmentation"] direction TB UsersLifeAll["users_life_all
SAT
R: users, unique_users, orders, contacts, contacts_hu, subscriptions, intentions, unique_prayers, comments, messages
W: users_life_day, users_life_week, users_life_month, users_life_quarter, users_life_semester, users_life_year, users_life_total"] UsersSeg["users_segmentations
SAT
R: users_life_quarter, users_life_month, users
W: users_segments"] UsersSegH["users_segmentations_history
SAT
R: users_segments, users_segments_history
W: users_segments_history, segments_history"] UsersDonScore["users_donation_score
daily ✓
R: users, users_life_quarter, contacts
W: user_donation_score"] UsersStats["users_stats
daily ✓
L: unique_users
R: users, users_segments, user_donation_score, prayers, comments, unique_users, last_sent_emails, orders, contacts, contacts_hu
W: data_users"] LastSentEmails["last_sent_emails
daily
L: mail_events, last_sent_emails
R: a.mail_events, a.mail_events_archive
W: last_sent_emails"] CommonUsers["common_users
daily
L: users, r.user
R: users, r.user
W: users, r.user"] NsmUsers["nsm_users
daily
R: unique_users, users
W: user_nsm_temp, data_north_star_metric, nsm_users"] DauMau["dau_mau
SAT
R: unique_users, users
W: dau_mau_by_lang, dau_mau_by_target_zone"] ActivityStats["activity_stats
daily
L: daily_activity
R: unique_open_emails, sessions, subscriptions, intentions, prayers, comments, messages, a.feed_items, users
W: daily_activity"] UsersLifeCP["users_life_cp
SAT
R: subscriptions, unique_prayers
W: users_life_cp_total, users_life_cp_day, users_life_cp_week, users_life_cp_month"] end subgraph ROSARIO["Rosario"] direction TB RosarioPart["participations
daily
L: participation
R: r.participation
W: r.participation"] Guides["guides
daily
L: guides
R: guides, user_referrals, contacts_hu, orders
W: guides"] UniquePrayersTask["unique_prayers
daily
L: prayers
R: prayers, subscriptions, publications, users
W: unique_prayers"] UsersLifeIntentions["users_life_intentions
SAT
R: unique_prayers, intentions
W: intention_product_usage, users_life_intentions_total, users_life_intentions_day, users_life_intentions_week, users_life_intentions_month, users_life_intentions_quarter, users_life_intentions_semester, users_life_intentions_year"] RosarioStreamToPrayer["stream_to_prayer
daily
L: r.prayer
R: r.stream
W: r.prayer"] RosarioDeviceLife["device_life
daily
R: r.user, r.prayer, orders, contacts, contacts_ru, r.participation
W: r.device_life_day, r.device_life_week, r.device_life_month, r.device_life_quarter, r.device_life_semester, r.device_life_year, r.device_life_total"] RosarioGroupStats["group_stats
daily
L: chapelet_completion_history, chapelet_activity_history
R: r.chapelet, r.participation, r.prayer
W: r.chapelet_completion_history, r.chapelet_activity_history"] RosarioParishStats["parish_stats
daily
R: r.participation, r.chapelet, r.parish, r.prayer
W: r.parish_activity_history"] RosarioActivityScore["activity_score
daily ✓
L: user_activity_history
R: r.user, r.prayer, r.participation, r.chapelet, contacts, orders
W: r.user_activity_history"] end %% === KEY TABLES (hexagons, different color) === T_users{{users}} T_unique_users{{unique_users}} T_unique_prayers{{unique_prayers}} T_subscriptions{{subscriptions}} T_publications{{publications}} T_orders{{orders}} T_contacts{{contacts}} T_contacts_hu{{contacts_hozana_users}} T_users_life_q{{users_life_quarter}} T_users_segments{{users_segments}} T_user_don_score{{user_donation_score}} T_data_users{{data_users}} T_community_first_reads{{community_first_reads}} T_prayers{{prayers}} T_sessions{{sessions}} T_mail_events{{mail_events}} T_feed_items{{feed_items}} T_rosario_prayer{{rosario.prayer}} T_rosario_participation{{rosario.participation}} %% === TASK DEPENDENCIES (solid arrows) === Lang --> UsersUTM Lang -.-> T_prayers & T_sessions UsersCC --> UsersTargetZone UsersCC --> UsersIndices UsersAge --> UsersIndices UsersSubs --> UniquePrayersTask UsersSubs -.-> T_subscriptions UsersSubs --> ReadRate UsersSubs --> UsersLifeCP PrepareDB --> ReadRate PrepareDB --> RosarioPart FeedItems --> ReadRate SubsUTM --> ReadRate UsersUTM --> SubsUTM SubsUTM --> PresubUTM GASubs --> CommTransfo %% === TABLE FLOWS (dotted arrows) === UniquePrayersTask -.-> T_unique_prayers T_unique_prayers -.-> UsersLifeAll & UsersLifeCP & UsersLifeIntentions UniqueUsers -.-> T_unique_users T_unique_users -.-> UsersLifeAll & NsmUsers & DauMau & ActivityStats & UsersStats CrmOrders -.-> T_orders T_orders -.-> UsersLifeAll & UsersStats & RosarioDeviceLife & RosarioActivityScore & Guides CrmContacts -.-> T_contacts & T_contacts_hu T_contacts -.-> UsersDonScore T_contacts_hu -.-> UsersLifeAll & UsersStats & RosarioDeviceLife & Guides UsersLifeAll -.-> T_users_life_q T_users_life_q -.-> UsersSeg & UsersDonScore UsersSeg -.-> T_users_segments T_users_segments -.-> UsersStats & UsersSegH UsersDonScore -.-> T_user_don_score T_user_don_score -.-> UsersStats UsersStats -.-> T_data_users ReadRate -.-> T_community_first_reads T_community_first_reads -.-> ComStats ReadRate --> PubStats PubStats --> ComStats GACom --> ComStats CommTransfo --> ComStats LastSentEmails --> UsersStats CommonUsers --> UsersStats %% === ROSARIO FLOWS === RosarioPart -.-> T_rosario_participation RosarioStreamToPrayer -.-> T_rosario_prayer T_rosario_prayer -.-> RosarioGroupStats & RosarioDeviceLife & RosarioParishStats & RosarioActivityScore T_rosario_participation -.-> RosarioGroupStats & RosarioDeviceLife & RosarioParishStats %% === STYLES === style T_users fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_unique_users fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_unique_prayers fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_subscriptions fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_publications fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_orders fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_contacts fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_contacts_hu fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_users_life_q fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_users_segments fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_user_don_score fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_data_users fill:#DFF7DA,stroke:#3D7732,color:#16181A,color:#3fb950 style T_community_first_reads fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_prayers fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_sessions fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_mail_events fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_feed_items fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_rosario_prayer fill:#FFF2D8,stroke:#DA7918,color:#16181A style T_rosario_participation fill:#FFF2D8,stroke:#DA7918,color:#16181A
Sources de Données
MySQL Prod Base de données Hozana (hozana_data)
Description: Base de production Hozana, dumpée chaque nuit vers le serveur ETL. Contient les tables utilisateurs, prières, communautés, publications, abonnements, intentions, messages, commentaires, sessions.
Tables principales lues: users, prayers, publications, subscriptions, communities, intentions, comments, messages, sessions, authentications, user_referrals, orders, contacts, contacts_hozana_users, crm_users
Fréquence: Dump quotidien ~22h00
Config: HOZANA_DATA_DB_* env vars (host, port, user, password, db_name)
Connexion: etl/mysql/connect.py — MySQLdb + SQLAlchemy
MySQL Prod Base Rosario (rosario)
Description: Base de production de l'app Rosario (chapelet), dumpée chaque nuit. Contient les utilisateurs, prières, participations, groupes, paroisses, devices.
Tables lues: user, prayer, participation, chapelet, community, parish, device, adjust_event, stream
Config: HOZANA_DATA_DB_ROSARIO_NAME
MySQL Prod Base Archive (hozana_archive)
Description: Base d'archive contenant les données historiques volumineuses : mail_events, mail_events_archive, feed_items, ga_communities, ga_publications.
Tables lues: mail_events, mail_events_archive, feed_items, ga_communities, ga_publications
Config: HOZANA_DATA_DB_ARCHIVE_NAME
GA4 Google Analytics 4
Description: Données de trafic web (page views, sessions, sources) récupérées via l'API GA4 Data.
Interface: etl/utils/ga4_interface.py
Credentials: GOOGLE_APPLICATION_CREDENTIALS=/home/hozana/data-tools/hozana-data-ga4.json
Tasks consommatrices: ga_communities, ga_publications, ga_subscriptions_page_views
Mailchimp Mailchimp API
Description: Statistiques de campagnes Mailchimp (ouvertures, clics, envois).
Interface: etl/utils/mailchimp_interface.py
Tasks consommatrices: mailchimp_stats.mailchimp_stats
GSheets Google Sheets (5 spreadsheets import + 1 export)
Description: Données publicitaires et budgétaires saisies manuellement par l'équipe marketing.
Interface: etl/utils/gsheet_interface.py (gspread via service account GCP hozana-data-tool-1712212735082)
Spreadsheets importes (task ads_data) — mapping vérifié avec les fichiers reels :
| Nom réel du GSheet | Config ETL | Table cible | Colonnes | Anomalies |
|---|---|---|---|---|
| Hozana Advertising Export to Sheet | ads_data | ads_data | Date, Traffic source, Campaign, Cost, Clicks, Impressions, Account creation [Hozana], Installs [Rosario], Inscription LP [Rosario] | - |
| Coreg budget | coreg_budget | coreg_budget | Date, Produit, Media, utm_source/Adgroup, utm_campaign/Campaign, utm_content/Creative, Budget, Rémunération/acquisition | Colonne utm_medium présente dans le sheet mais PAS mappée dans le code ETL |
| Partenariat budget | partnership_budget | partnership_budget | Date, Produit, Type, utm_source/Adgroup, utm_campaign/Campaign, utm_content/Creative, Budget, Rémunération/acquisition | - |
| Autre budget | print_budget | print_budget | Date, Type, Produit, Media, Campagne, utm_source/campaign/content/medium (si QR), Budget, Rémunération, Lien tracke | Nomme "Autre budget" dans GSheet mais "print_budget" dans le code |
| SEO Budget | seo_budget | seo_budget | Mois, Nom, Langue, Type, Guide, Nb articles, Nb heures, Cout HT, Nb mots | Le sheet utilise IMPORTRANGE vers un autre spreadsheet — les colonnes sont des formules |
Spreadsheet d'export (task nsm_users): NSM KPI export vers GSheet 1ArteY1YZU5k79mXbJU3RxrBLCDzGdCnicjS47uo4_SM
Catalogue des Tasks
76 tasks enregistrées dans etl/main.py. Cliquez sur une card pour voir le détail en modale avec le mini-graphe de dépendances.
Graphe de Dépendances
Dépendances inter-tasks (principales)
Cliquez sur le diagramme pour l'ouvrir en grand avec noeuds cliquables.
Locks de Tables
| Table lockée | Tasks qui la lockent |
|---|---|
users | lang, users_cleanup, users_age, users_gender, users_country_code, users_seniority, users_target_zone, users_authentication_type, users_utm, users_subscriptions, users_indices, common_users, crm_contacts, npe_update_users, digest_stats_update_users, presubscribers_utm, subscriptions_utm |
subscriptions | prepare_db, users_subscriptions, feed_items, read_rate, subscriptions_utm, presubscribers_utm |
publications | prepare_db, feed_items, read_rate, publication_stats, ga_publications, npe_update_publications |
prayers | lang, unique_prayers |
sessions | lang, unique_users |
messages | lang, anonymization.hozana_messages |
mail_events | unique_users, email_stats, last_sent_emails, digest_stats_prepare |
communities | community_stats |
unique_users | users_stats |
orders | crm_orders, anonymization.crm_anonymization |
contacts | crm_contacts, anonymization.crm_anonymization |
announcements | announcement_stats |
intentions | anonymization.intentions_anonymization |
guides | guides |
feed_items | feed_items |
notes | anonymization.crm_anonymization |
next_step | anonymization.crm_anonymization |
last_sent_emails | last_sent_emails |
daily_activity | activity_stats |
digest_emails | digest_stats_prepare |
participation | rosario.participations |
rosario.user | common_users, rosario.derived_tables, rosario.user_adjust_parameters, rosario.users_target_zone |
rosario.prayer | rosario.stream_to_prayer |
rosario.chapelet | rosario.chapelet_structure |
rosario.adjust_event | rosario.adjust_events |
rosario.message | anonymization.rosario_messages |
rosario.user_derived | rosario.ghost_users |
rosario.dau_mau | rosario.dau_mau |
user_activity_history | user_reactivation.user_activity_history, rosario.activity_score |
user_reactivation | user_reactivation.user_reactivation |
chapelet_completion_history | rosario.group_stats |
chapelet_activity_history | rosario.group_stats |
Cartographie des Tables
Tables prod (écrasées chaque nuit par le dump)
| Table | DB | Enrichie par | Colonnes ajoutées |
|---|---|---|---|
| users | hozana_data | lang, users_age, users_gender, users_country_code, users_seniority, users_target_zone, users_utm, users_authentication_type, users_subscriptions, users_indices, common_users, users_cleanup | lang, age, title_gender, country_code, number_of_days_since_subscription, target_zone, authentication_type, n_joined_communities_active, is_rosario_user, rosario_user_id, utm_* |
| prayers | hozana_data | lang | lang |
| sessions | hozana_data | lang | lang |
| messages | hozana_data | lang | lang |
| user_referrals | hozana_data | lang | lang |
| subscriptions | hozana_data | users_subscriptions, read_rate | subscription_number, has_read_first_publication, first_publication_number, n_received_publications, utm_* |
| publications | hozana_data | publication_stats, read_rate | publication_number, n_received_*, n_read_*, n_shares |
| communities | hozana_data | community_stats | n_legacy_message_*, last_publication_scheduled_time, n_publications_read, read_rate, corrected_read_rate_*, n_ga_page_views, n_community_joined_email_*, corrected_transfo_rate_* |
| orders | hozana_data | crm_orders | amount_eq_euro, is_confirmed_regular_donation, exclude_from_statistics, amount_eq_euro_ltv_x50_x10_7 |
| contacts | hozana_data | crm_contacts | n_valid_donations, first_hozana_user_id |
| rosario.participation | rosario | rosario.participations | participation_number |
| rosario.user | rosario | common_users | is_hozana_user, hozana_user_id |
Tables ETL-only (persistantes, NON écrasées par le dump)
| Table | DB | Ecrite par | Type |
|---|---|---|---|
| unique_open_emails | hozana_data | unique_users | Incrémental (append) |
| unique_users | hozana_data | unique_users | Recréé chaque run |
| alive_subscribers | hozana_data | unique_users | Recréé chaque run |
| unique_prayers | hozana_data | unique_prayers | Incrémental (UPSERT) |
| data_users | hozana_data | users_stats | Recréé chaque run |
| users_segments | hozana_data | users_segmentations | Recréé (saturday) |
| users_segments_v2 | hozana_data | users_segmentations_v2 | Recréé (saturday) |
| users_segments_history | hozana_data | users_segmentations_history | Persistante (append) |
| segments_history | hozana_data | users_segmentations_history | Persistante (append) |
| users_life_* | hozana_data | users_life_all | Recréé (saturday) |
| users_life_cp_* | hozana_data | users_life_cp | Recréé (saturday) |
| users_life_intentions_* | hozana_data | users_life_intentions | Recréé (saturday) |
| intention_product_usage | hozana_data | users_life_intentions | Recréé (saturday) |
| user_donation_score | hozana_data | users_donation_score | Recréé chaque run |
| publication_reads | hozana_data | read_rate | Recréé chaque run |
| community_reads | hozana_data | read_rate | Recréé chaque run |
| community_first_reads | hozana_data | read_rate | Recréé chaque run |
| email_stats | hozana_data | email_stats | Incrémental (UPSERT) |
| last_sent_emails | hozana_data | last_sent_emails | Persistante (UPSERT) |
| daily_activity | hozana_data | activity_stats | Persistante (36 mois) |
| dau_mau_by_lang | hozana_data | dau_mau | Persistante (incrémental) |
| dau_mau_by_target_zone | hozana_data | dau_mau | Persistante (incrémental) |
| okr_monthly_etpm | hozana_data | etpm | Persistante (incrémental) |
| okr_yearly_etpm | hozana_data | etpm | Recréé chaque run |
| data_north_star_metric | hozana_data | nsm_users | Persistante (monthly append) |
| nsm_users | hozana_data | nsm_users | Persistante |
| etl_performances | hozana_data | orchestrator | Persistante (daily append) |
| ads_data, coreg_budget, partnership_budget, print_budget, seo_budget | hozana_data | ads_data | Replace chaque run |
| community_joined_emails | hozana_data | community_stats | Incrémental |
| daily_community_shares | hozana_data | community_stats | Recréé chaque run |
| shares_total | hozana_archive | community_stats | Persistante (daily append) |
| user_activity_history | hozana_data | user_réactivation.user_activity_history | Persistante |
| rosario.user_activity_history | rosario | rosario tasks | Persistante |
| rosario.data_north_star_metric | rosario | rosario.nsm_users | Persistante |
Origines des Données (Backend Symfony)
Analyse du backend Symfony Hozana (/infra/backend/) — 91 entités Doctrine, commandes cron, webhooks.
Flux Bidirectionnel ETL ↔ Production
data_publications
data_communities
data_announcements"] end subgraph PROD_DB["Production DB"] TABLES["users, prayers,
communities, etc."] ME["mail_events"] FI["feed_items"] SESS["sessions"] DTABLES["data_users, data_publications,
data_communities, data_announcements"] end APP --> TABLES WH --> ME APP --> FI APP --> SESS CRON -->|clean daily| ME CRON -->|clean daily| FI TABLES -->|dump 22h| DUMP ME -->|dump 22h| DUMP FI -->|dump 22h| DUMP SESS -->|dump 22h| DUMP DUMP --> ETL ETL --> DATA DATA -->|scp + atomic swap| DTABLES style DATA fill:#DFF7DA,stroke:#3D7732,color:#16181A style DTABLES fill:#DFF7DA,stroke:#3D7732,color:#16181A
Découverte cle : Le pipeline est bidirectionnel. L'ETL lit les tables prod, mais écrit aussi 4 tables data_* qui sont re-importées en production chaque nuit via load_data_tables_from_data_server.sh (swap atomique de tables via RENAME TABLE).
Provenance des Tables Source (Backend → ETL)
Webhook mail_events — SendGrid/SES Webhooks
Entité Doctrine : src/Dbal/Entity/Mail/MailEvent.php
Creation : Webhook POST /sendgrid/webhook → MailEventController::sendGridEvents()
Colonnes : mail_event_id, email, user_id, timestamp, event (bounce/open/click/delivered/etc.), category, community_id, publication_id, announcement_id, lang, mailer_provider (sendGrid/SES), ses_message_id, ses_tags
Retention : Nettoyee quotidiennement par api:cron:clean (cron day4) — conserve ~60 jours. Les anciens events sont archives dans hozana_archive.mail_events_archive par l'ETL (task email_stats).
Volume : Tres volumineux (millions de lignes/mois). L'ETL itere par batches de 1M mail_event_id.
Application feed_items — Tracking de lecture des publications
Entité Doctrine : src/Dbal/Entity/FeedItem.php
Creation : Logique applicative quand une publication est poussee a un abonne. Aussi via CreateFeedItemsForPublicationCommand.
Colonnes : feed_item_id, user_id, community_id, subscription_id, publication_id, pushed_at (timestamp ms), read_at, first_read_from (enum: email, feed-read-more, community-read-more, publication-page)
DB : hozana_archive (les feed_items sont dans la base archive, pas hozana_data)
Retention : Nettoyee quotidiennement par cron day4.
Application sessions — Sessions utilisateur
Entité Doctrine : src/Dbal/Entity/UserSession.php
Creation : Enregistree a chaque activite utilisateur, 1 par user par jour calendaire.
Colonnes : session_id, user_id, session_time (DATE), app_type (web/mobile/unknown)
Cle unique : (user_id, session_time) — pas de doublons possible.
Application authentications — Authentification sociale
Entité Doctrine : src/Dbal/Entity/Authentication.php
Creation : Lors de la connexion OAuth (Facebook, Google, Apple).
Colonnes : authentication_id, user_id, provider_uid (ex: "facebook_12345"), access_token
Usage ETL : Lu par users_authentication_type pour deduire le type d'auth via LIKE sur provider_uid.
Application user_referrals — Tracking d'acquisition
Entité Doctrine : src/Dbal/Entity/UserReferral.php (PK = user_id, FK vers users)
Creation : A l'inscription de l'utilisateur, capture le referrer HTTP et l'URL de landing.
Colonnes : user_id, referrer (raw), landing_url, landing_date, source, first_subscription
Usage ETL : Source primaire pour le calcul des UTMs (task users_utm). La logique complexe d'extraction et normalisation est dans etl/utils/utms.py.
Application users — Table principale utilisateurs
Entité Doctrine : src/Dbal/Entity/User.php (40+ colonnes)
Colonnes clés : user_id, email, name, first_name, lang, birthdate, title, created_at, deleted_at, disabled, is_reported, user_country_code, auto_country_code, sponsor_key, invite_key
L'ETL ajoute : age, title_gender, country_code, number_of_days_since_subscription, target_zone, authentication_type, n_joined_communities_active, is_rosario_user, rosario_user_id, utm_*, last_digest_delivered, last_digest_open, n_unread_*
Cron Données SEO (Google Search Console)
Commande : api:cron:fetch-seo-clicks (cron day4, 4h AM)
Source : API Google Search Console — 365 jours de données de clics
Écrit dans : Community.seoClicks et Publication.seoClicks (colonnes directement dans les entités prod)
Garde-fou : Abort si < 100 communautés avec clics (indique un probleme API).
Tables ETL Re-importées en Production — Usage Fonctionnel Complet
PROD data_users → Ciblage donations, segmentation, Mailchimp
Entité : UserStats (Hozana) + Data\UserStats (Rosario)
Colonnes : behavioral_segment, ppd_segment, first_donation_probability, n_prayers, n_comments, last_activity, last_sent_email, is_rosario_user, is_donor_in_last_90_days
| Consommateur | Fichier | Usage metier |
|---|---|---|
CTADonation | Api/Service/CTA/CTADonation.php | Ciblage donation : Si first_donation_probability ≥ 0.2 → 80% de chance d'afficher le CTA. Si < 0.2 → 10%. Utilise aussi is_donor_in_last_90_days. |
MailService | Api/Service/MailService.php | Generation de CTA contextualises dans les emails digest, welcome, new publication |
/api/user/me | Api/Controller/UserController.php | Renvoie behavioralSegment au frontend (default: "new-comers"). Utilisé pour personnaliser l'UX. |
Admin users | Admin/Controller/UserController.php | Dashboard admin : n_prayers, n_comments par utilisateur |
Rosario Mailchimp | rosario/backend/Service/MailchimpService.php | Merge fields Mailchimp : Si PPD80 ou PPD85 → ppdxx = 1 → ciblage campagnes fundraising Rosario |
PROD data_publications → Analytics lectures par source
Entité : PublicationStats
Colonnes : n_read, n_read_email, n_read_feed_read_more, n_read_community_read_more, n_read_publication_page, n_ga_page_views
| Consommateur | Fichier | Usage metier |
|---|---|---|
PublicationRepository | Dbal/Repository/PublicationRepository.php:669 | LEFT JOIN PublicationStats dans getStatisticsByPublicationsOfCommunity() — stats de lecture ventilées par canal (email, feed, community, page directe) |
| Admin communaute | Frontend-admin | Dashboard stats publications pour les responsables de communautés |
PROD data_communities → Read rate et ranking
Entité : CommunityStats
Colonnes : read_rate, corrected_read_rate_in_first_publications, corrected_read_rate_quantile, corrected_transfo_rate_in_last_six_months
| Consommateur | Fichier | Usage metier |
|---|---|---|
CommunityController | Api/Controller/Community/CommunityController.php:514 | Admin-only : expose readRate, correctedReadRate, correctedReadRateQuantile dans /api/community/{id} |
| Frontend-admin | CommunityAdminForm.tsx | Affiche le taux de lecture en % et le percentile : -(quantile - 11) * 10 pour convertir 0-10 en 0-100% |
| Frontend-5 | getCommunity.types.ts | Type optionnel pour les champs read rate |
PROD data_announcements → Stats emails annonces
Entité : AnnouncementStats
Colonnes : n_email_delivered, n_email_viewed, n_email_clicked
| Consommateur | Fichier | Usage metier |
|---|---|---|
AnnouncementSerializer | Api/Serializer/Announcement/AnnouncementSerializer.php | Serialise deliveredCount, openedCount, clickedCount dans toute API response Announcement |
Mécanisme d'import : prepare_data_for_prod.sh créé des tables data_*_temp → dump hozana_data_for_prod.dump.sql.gz → SCP vers prod → load_data_tables_from_data_server.sh → import dans schema temp → core:data:load-data-tables-from-data-server → RENAME TABLE atomique (zero downtime).
Tables "Mystérieuses" — Résolution
| Table | Origine | Explication |
|---|---|---|
unique_open_emails | ETL-only | Créée manuellement lors du premier deploiement ETL. Remplie incrémentalement par unique_users. Pas d'entité Doctrine — la table n'existe pas en prod. |
community_joined_emails | ETL-only | Meme pattern : créée manuellement, remplie incrémentalement par community_stats depuis mail_events. |
new_community_message_emails | ETL-only | Table ETL remplie depuis mail_events (category = new_community_message). Pas de creation visible dans le code. |
community_join_groups | ETL | Créée par community_transformation_rate.py via pandas to_sql(if_exists='replace'). |
ga_communities | ETL | Créée par ga_communities.py via pangres upsert (create_table=True). Stockee dans hozana_archive. |
ga_publications | ETL | Créée par ga_publications.py via pangres upsert. Stockee dans hozana_archive. |
digest_emails | ETL-only | Pattern incrémental depuis mail_events (category LIKE 'digest'). Creation initiale manuelle. |
new_publication_emails | ETL-only | Pattern incrémental depuis mail_events (category LIKE 'new_publication'). Creation initiale manuelle. |
Acces au Serveur Data
# Prerequis : être connecté au VPN Hozana ssh tsihapanya@data.hozana.org # ou votre user SSH sudo su hozana # switch vers le user hozana cd ~ # /home/hozana/ # Le projet ETL est dans ~/data-tools/ # Metabase tourne comme JAR : ~/metabase.jar (port 3000) # Logs ETL : ~/data-tools/log/etl.log # Performances : ~/data-tools/log/task_execution_times.csv
Déclenchement du Pipeline (Polling ETL-starter.sh)
Le crontab du user hozana contient :
*/5 23,0,1,2 * * * /home/hozana/ETL-starter.sh
Mécanisme réel (decouvert via SSH) : ETL-starter.sh est un polling intelligent execute toutes les 5 minutes entre 23h00 et 02h59 :
- Guard : Si
pgrep -f "python.*etl.main"trouve un process → exit 0 (evite les doublons) - Check DB :
SELECT task FROM etl_performances WHERE status='SUCCESS' ORDER BY date DESC, start_time DESC LIMIT 1 - Condition : Si la dernière task contient
mysql-anonymisation→ le dump est importe et anonymisé - Lancement : Insere
starting-etldans etl_performances puis appelleafter_sql_dump_sent.sh.new
Fichiers sur le serveur :
~/ETL-starter.sh— le polling (crontab)~/data-tools/scripts/after_sql_dump_sent.sh— ancienne version (2023-02-17)~/data-tools/scripts/after_sql_dump_sent.sh.new— version active (2025-05-09)
Emplacement des dumps : /mnt/nvme/mysqldump/ (le chemin load_data.sh référence /mnt/nvme/mysqldump_from_prod/ qui n'existe plus)
# Fichiers trouvés sur le serveur : /mnt/nvme/mysqldump/metabase.dump.sql.gz /mnt/nvme/mysqldump/hozana_archive.dump.sql.gz
mysqldump depuis mysql-replica.hozana.io"] --> S["SCP .sql.gz → /mnt/nvme/mysqldump/"] S --> LOAD["Import dumps + anonymisation
Écrit 'mysql-anonymisation' dans etl_performances"] LOAD --> POLL["Crontab */5 23-02h
ETL-starter.sh poll"] POLL -->|"Détecte mysql-anonymisation"| START["after_sql_dump_sent.sh.new"] START --> GIT["git pull"] GIT --> ETL["python3.10 -m etl.main
76 tasks ~7h
NB: load_data.sh est COMMENTE
dans .new — les dumps sont déjà importes"] ETL --> PREP["prepare_data_for_prod.sh
Créé data_*_temp, dump .sql.gz"] PREP --> PROD["PROD load_data_tables_from_data_server.sh
RENAME TABLE atomique"] style POLL fill:#FFF4D5,stroke:#AD5700,color:#16181A style ETL fill:#DFF7DA,stroke:#3D7732,color:#16181A style PROD fill:#FDEEED,stroke:#AE0A4B,color:#16181A
Metabase
URL : https://metabase.data.hozana.org
Execution : JAR Java directement sur le serveur (~/metabase.jar), port 3000, derrière Apache avec SSL Let's Encrypt
Backups : Manuels avant chaque mise a jour (9 fichiers .tar.gz dans ~/, de oct 2024 a dec 2025)
Connexion DB : Directement au MySQL local (hozana_data, rosario, hozana_archive + schema metabase pour ses propres données)
Consommateurs : Équipe data/produit Hozana — dashboards, requêtes ad-hoc, exports
Infrastructure Serveur
| Composant | Details |
|---|---|
| Serveur Data | data.hozana.org — héberge l'ETL, MySQL ETL, Metabase |
| Metabase | metabase.data.hozana.org — port 3000 derrière Apache + SSL Let's Encrypt. Connecte directement a MySQL ETL. |
| Replica MySQL | mysql-replica.hozana.io — source des dumps (pas le MySQL prod principal) |
| Dumps | /mnt/nvme/mysqldump/ — 4 fichiers : hozana_prod, hozana_crm, hozana_rosario, hozana_archive (.sql.gz) |
| Backups | AWS S3 (4 buckets, 20j retention) + rsync.net (chiffre). Verification auto via monitoring.hozana.org + Sentry. |
| Stack Prod | MySQL 8.4 + Elasticsearch + Redis + RabbitMQ + Apache + PHP-API (Symfony) |
Tables exclues du dump principal
Le script send_mysql_dump_to_data.sh (/infra/backend/tools/sync/) exclut ces tables du dump hozana_prod :
| Table exclue | Raison probable | Ou va-t-elle ? |
|---|---|---|
feed_items | Tres volumineuse | Dump archive séparé (seuls les items lus + events 3j+) |
mail_events | Tres volumineuse | Dump archive séparé (events 3j+) |
mails | Emails envoyes (PII) | Exclue complètement |
authentications_users_users | Tokens OAuth (securite) | Exclue complètement |
cronlogs | Logs techniques inutiles | Exclue complètement |
Conséquence : L'ETL accede a feed_items et mail_events via hozana_archive, pas hozana_data. Toute task qui les chercherait dans hozana_data échouérait silencieusement.
Crons Backend (impactant l'ETL)
| Cron | Fréquence | Impact ETL |
|---|---|---|
api:cron:clean | Quotidien 4h AM | Nettoie mail_events (~60j), feed_items, sent_push_notifications. L'ETL doit tourner AVANT le nettoyage ou utiliser mail_events_archive. |
api:cron:fetch-seo-clicks | Quotidien 4h AM | Écrit seoClicks dans communities/publications. Non lu par l'ETL (mais present dans le dump). |
crm:cron:mailchimp:update-merge-fields | Horaire | Sync champs Mailchimp. L'ETL lit les stats Mailchimp séparément. |
core:data:load-data-tables-from-data-server | Post-ETL | Re-importe les tables data_* calculées par l'ETL dans la prod. |
Zones d'Ombre & Rapport d'Étonnement
Risques & Securite
🔴 Injection SQL potentielle dans plusieurs tasks
Nombreuses requêtes SQL construites par .format() sans paramétrage (bind variables). Exemples :
etl/task/email_stats.py:73-84— format() avec min/max mail_event_idetl/task/dau_mau.py:62-103— format() avec dayetl/task/last_sent_emails.py:137-141— format() avec valuesetl/mysql/operations.py— quasi toutes les fonctions
Dans le contexte ETL interne c'est un risque faible (pas d'input utilisateur), mais c'est une mauvaise pratique systematique. Les connexions SQLAlchemy avec text() dans users_life_all sont un meilleur modele.
🔴 Mot de passe root en clair dans docker-compose.yml
docker-compose.yml:12 : MYSQL_ROOT_PASSWORD: root et HOZANA_DATA_DB_PASSWORD=root. Acceptable en dev/ETL interne, mais dangereux si le fichier est deploye en production ou si le port 3307 est expose.
🔴 Spreadsheet keys en dur dans le code
etl/task/ads_data.py et etl/task/nsm_users.py:18 contiennent des Google Sheets keys hardcodées. Si ces spreadsheets sont supprimés ou déplaces, les tasks échouént silencieusement.
Incoherences & Code mort
🟡 Tasks d'anonymisation présentes mais non enregistrées
Les fichiers suivants existent dans etl/task/ mais ne sont pas enregistrés dans main.py :
anonymization/hozana_users_anonymization.pyanonymization/rosario_users_anonymization.pycrm_contacts_names.py— lockecontacts, litlast_namequi est droppé par crm_anonymizationspecial_appel_don_fr_paques_2026.py— task one-off
Sont-ils exécutés ailleurs ? Sont-ils obsolètes ?
🟡 users_segmentations v1 vs v2 : coexistence non documentee
Deux systemes de segmentation coexistent (users_segments et users_segments_v2). users_stats ne référence que v1 (users_segments). Quel est le statut de v2 ? Est-il utilise en production ? La v1 sera-t-elle supprimee ?
🟡 Seuils de segmentation hardcodes sans documentation
etl/task/users_segmentations.py:196-208 : les seuils de clustering (0.0153, 0.3254, 325.6437, 332.519) sont issus d'un notebook Jupyter (seg_comportementale hozana.ipynb) qui n'est pas dans le repo. Ces valeurs magiques sont figees et jamais re-évaluées.
Meme probleme dans users_donation_score.py:91-105 : coefficients de regression logistique hardcodes.
🟡 Seuils PPD percentile hardcodes par langue
etl/task/users_donation_score.py:119-147 : les seuils de PERCENT_RANK pour PPD80/85/90/95 sont differents par langue et semblent avoir été calibres une seule fois. Aucun mécanisme de recalibration.
🟡 open_coefficients hardcodes dans email_stats
etl/task/email_stats.py:224-231 : les coefficients (0.16, 0.33, 0.45, 0.51) pour estimer les unique opens sont hardcodes "by looking at sendgrid dashboard". Plus Sendgrid est utilise depuis ? Ces coefficients sont-ils encore valides avec Amazon SES ?
Données orphelines & Origines floues
🟡 Tables lues mais jamais créées/peuplées par l'ETL
Ces tables sont lues par des tasks mais ne sont pas peuplées par l'ETL — elles doivent provenir du dump prod :
authentications(lu par users_authentication_type)user_referrals(lu par lang, users_utm)intentions(lu par users_life_all, users_life_intentions, activity_stats)comments(lu par users_life_all, activity_stats, users_stats)new_community_message_emails(lu par community_stats)community_join_groups(lu par community_stats)feed_items(lu par read_rate, publication_stats — dans hozana_archive)ga_communities,ga_publications(archive — peuplées par tasks GA4)
La plupart sont des tables prod dumpées. Mais new_community_message_emails et community_join_groups pourraient être des tables ETL dont la creation n'est pas visible.
🟡 Table unique_open_emails : pas de creation visible
etl/task/unique_users.py:29-47 fait un SELECT max(mail_event_id) FROM unique_open_emails puis un INSERT IGNORE. Mais la creation de cette table n'est pas dans le code ETL. Elle doit exister avant le premier run. Creation manuelle ? Script d'initialisation perdu ?
🔵 community_joined_emails : pas de creation visible
etl/task/community_stats.py:177 lit community_joined_emails mais la creation de cette table n'est pas dans le code ETL visible. Meme probleme que unique_open_emails.
Risques operationnels
🔴 Tasks Saturday-only dependantes de tasks daily
users_donation_score est daily mais depend de UsersLifeAll qui est saturday-only. Du lundi au vendredi, elle utilise les données du dernier samedi. C'est documente et intentionnel (le code le gere), mais le risque est que si le run de samedi échoué, les 6 jours suivants utilisent des données de 2 semaines.
Meme pattern pour users_stats qui lit users_segments (saturday) mais ne declare pas la dépendance car "elle ne tourné que le samedi" (commentaire dans le code).
🟡 Pas de validate_data() sur la majorite des tasks
Sur 76 tasks, 9 implémentent validate_data() : users_stats, users_donation_score, read_rate, community_stats, crm_contacts, rosario.user_adjust_parameters, rosario.users_target_zone, rosario.ghost_users, rosario.activity_score. Les 67 autres n'ont aucune validation post-exécution. Les tasks critiques sans validation incluent : users_life_all, users_segmentations, unique_users, nsm_users.
🟡 unique_users : DROP TABLE puis re-creation chaque run
etl/task/unique_users.py:101-113 fait un DROP TABLE IF EXISTS unique_users puis recréé la table. Si le run échoué entre le DROP et l'INSERT final, la table est vide. Toutes les tasks dependantes (users_stats, nsm_users, dau_mau, activity_stats) échouéront ou produiront des résultats vides.
🟡 users_life_all : consommation mémoire potentiellement elevee
La task charge des dictionnaires Python count_per_user en mémoire pour chaque chunk (1/8 des users). Avec ~3M users, chaque chunk représente ~375K users x 7 period_types x N périodes. Le commentaire indique un split en chunks pour eviter l'OOM, mais le risque persiste si la base grossit.
🔵 email_stats : archive mail_events > 33 jours
La task déplace les mail_events de plus de 33 jours dans mail_events_archive. Ce seuil est hardcode dans email_stats.py:235. Aucune documentation expliquant pourquoi 33 jours (probablement la période maximale d'un mail bounce + marge).
🔵 read_rate utilise ThreadPoolExecutor (4 workers)
etl/task/read_rate.py:121 est la seule task qui utilise du multi-threading interne. Cela signifie que quand l'orchestrateur la lance, elle consomme 4 connexions MySQL simultanées, ce qui peut interferer avec les autres tasks parallèles.
🔵 Pas de mécanisme de retry
L'orchestrateur n'a aucun mécanisme de retry. Si une task échoué (OOM, timeout MySQL, erreur reseau GA4/Mailchimp), elle est marquee failed et toutes ses dependantes sont skipped. Pour les tasks qui font des appels API externes (ga4, mailchimp, gsheet), c'est un risque.
Questions ouvertes
🟡 RÉSOLU : Tables incrémentales sans CREATE TABLE visible
Verdict après scan du backend : Les tables unique_open_emails, community_joined_emails, new_publication_emails, digest_emails, new_community_message_emails sont des tables ETL-only qui n'existent PAS en production (pas d'entité Doctrine correspondante). Elles ont été créées manuellement lors du premier deploiement ETL et sont remplies incrémentalement. Risque : si la base ETL est recréé from scratch, ces tables n'existeront pas et les tasks échouéront. Un script d'initialisation devrait être versionne.
🔵 RÉSOLU : Tables rosario — proviennent du dump prod Rosario
Les tables prayer, participation, user, chapelet, community, parish, device, stream, adjust_event sont dumpées depuis le backend Rosario (projet app + backend separe du backend Hozana Symfony — non encore accessible pour analyse). Le dump passe par send_mysql_dump_to_data.sh sous le nom hozana_rosario.dump.sql.gz.
🔴 DECOUVERTE : Flux bidirectionnel non documente
L'ETL n'est pas qu'un consommateur de la prod : il produit 4 tables (data_users, data_publications, data_communities, data_announcements) qui sont re-importées en production via load_data_tables_from_data_server.sh + swap atomique. Ce flux retour n'est documente ni dans CLAUDE.md ni dans le README. Si l'ETL échoué, la prod utilise des données stale. Les entités Doctrine correspondantes existent dans src/Dbal/Entity/Data/.
🟡 DECOUVERTE : Le dump utilise un REPLICA, pas la prod principale
Le script send_mysql_dump_to_data.sh dumpe depuis mysql-replica.hozana.io, pas le serveur MySQL principal. Si la replication a du retard (lag), l'ETL travaille sur des données decalees. Aucun check de lag de replication n'est visible dans le script.
🟡 DECOUVERTE : 5 tables exclues du dump principal
send_mysql_dump_to_data.sh exclut feed_items, mail_events, mails, authentications_users_users, cronlogs. Les deux premières vont dans le dump archive. Les trois autres sont perdues. Si une task ETL tente de lire mails ou authentications_users_users, elle échouéra. Actuellement users_authentication_type lit authentications (sans _users_users) donc ca fonctionne — mais la distinction de nommage est fragile.
🔵 DECOUVERTE : Le Perl inline dans load_data.sh supprime TOUTES les FK
scripts/load_data.sh:42-57 contient un filtre Perl qui supprime toutes les lignes CONSTRAINT du dump SQL avant import. C'est une approche agressive mais cohérente avec prepare_db.py qui drop aussi les FK. Cela signifie que la base ETL n'a aucune FK, même celles que l'ETL n'a pas besoin de supprimer.
🟡 DECOUVERTE : Conflit potentiel entre nettoyage cron et ETL
Le cron backend api:cron:clean (4h AM) nettoie mail_events (~60 jours), feed_items, etc. Si l'ETL n'a pas encore tourné quand le nettoyage s'execute, des données peuvent être perdues. L'ETL pallie partiellement en archivant mail_events dans mail_events_archive (task email_stats), mais le timing est critique.
🔵 Quelle est la durée typique d'un run complet ?
Le code prévoit une alerte Sentry si le run dépasse 18h (etl/utils/orchestrator.py:120). Cela implique que le run habituel est probablement entre 4-8h. Le fichier log/task_execution_times.csv contient l'historique mais n'est pas dans le repo.
🔴 LIVE : unique_users en échec le 24/03 — cascade de skips
Le run du 24/03/2026 montre que unique_users a échoué, entrainant le skip de activity_stats, users_stats, et probablement nsm_users, dau_mau. La table data_users re-importée en prod utilise donc des données du run precedent. Impact prod : segmentation marketing et scoring donateurs potentiellement stale.
🟡 Temps CPU cumulé a 18.8h — seuil Sentry a 18h
Le temps CPU cumulé du run du 24/03 est de 67 669s (18.8h). Le code orchestre un alert Sentry quand le temps reel dépasse 18h (orchestrator.py:120). Mais le temps réel etait de 7.1h grâce au parallelisme. Le risque est qu'un samedi (avec les tasks weekly), le temps réel dépasse le seuil.
🟡 read_rate monopolise 51% du temps d'execution
read_rate a pris 3h37 (13 082s) sur le run du 24/03 — plus de la moitie du temps réel total. Cette task utilise ThreadPoolExecutor(max_workers=4) en interne, multipliant les connexions MySQL. C'est le goulot d'etranglement principal du pipeline.
🟡 Chemin des dumps a change + script .new non commité
Chemin : load_data.sh référence /mnt/nvme/mysqldump_from_prod/ mais les dumps sont en réalité dans /mnt/nvme/mysqldump/. Seuls 2 fichiers trouvés : hozana_archive.dump.sql.gz et metabase.dump.sql.gz (les 4 dumps attendus ne sont pas tous présents — soit ils sont nettoyés après import, soit le chemin a encore change).
Script .new : Le crontab appelle after_sql_dump_sent.sh.new (mai 2025), pas l'original (fev 2023). Ce fichier .new n'est probablement pas dans le repo git (il est dans scripts/ mais avec un suffixe non standard). Risque : si git pull est execute, le .new peut ne pas être ecrase mais le .sh original oui.
🟡 Google Sheet "SEO Budget" depend d'un IMPORTRANGE
Le sheet SEO Budget utilise IMPORTRANGE vers un autre spreadsheet (1HqAp4IuwWlF_...). L'ETL lit le sheet via l'API GSheets avec value_render_option='UNFORMATTED_VALUE' — les formules IMPORTRANGE ne sont pas évaluées cote API si le sheet n'a pas été ouvert recemment dans un navigateur. Risque de données vides.
🔵 Google Sheet "Coreg budget" — colonne utm_medium non mappée
Le fichier réel contient une colonne utm_medium absente du columns_mapping dans ads_data.py. Cette colonne est importée dans la table mais avec son nom GSheet original ("utm_medium"), pas normalise. Pas critique mais incoherent avec le pattern des autres sheets.
🔵 Metabase : backups manuels, pas de monitoring automatisé
Metabase tourne comme un JAR Java sur le serveur data. Les backups sont manuels (9 tar.gz dans ~/ de oct 2024 a dec 2025). Pas de backup automatisé, pas de monitoring de disponibilité. Si le processus Java meurt, personne n'est alerte.
🔵 Naming incoherent : "Autre budget" vs "print_budget"
Le Google Sheet s'appelle "Autre budget" mais est mappe comme print_budget dans le code ETL. La table cible s'appelle aussi print_budget. Le contenu (Date, Media, Campagne, utm_*, Budget) correspond bien a du print, mais le naming du sheet prete a confusion.
🔵 etl/utils/sql.py référence mais non explore
where_chunk() dans etl/utils/sql.py est utilise par users_life_all, users_life_cp, users_life_intentions pour partitionner les queries. Ce fichier n'a pas été lu (non inclus dans les fichiers explores). Il implemente probablement un WHERE user_id % N = chunk.
Operations Quotidiennes
Healthcheck du matin (5 min)
| # | Action | Commande / URL | Quoi vérifier |
|---|---|---|---|
| 1 | Vérifier le rapport email | Boite mail (arnaud@, thomas-sihapanya@) | Subject "ETL OK" ou "X erreurs ETL". Ouvrir si erreurs. |
| 2 | Derniere ligne du log | ssh data.hozana.org sudo su hozana tail -5 ~/data-tools/log/etl.log | Doit finir par "End ETL main process" + heure raisonnable (< 08h) |
| 3 | Temps d'execution | tail -3 ~/data-tools/log/task_execution_times.csv | Ligne "real" < 30000s (8.3h). Ligne "total" < 65000s (18h). Alerte si approche du seuil Sentry. |
| 4 | Tasks en échec | grep "failed\|skipped" ~/data-tools/log/etl.log | tail -20 | Identifier les tasks failed et les cascades de skips. Prioriser : unique_users, users_life_all, crm_orders. |
| 5 | Metabase accessible | metabase.data.hozana.org | Page de login chargée = OK. Si timeout → JAR Java peut-être crashe. |
Commandes de diagnostic
# Connexion au serveur data
ssh [user]@data.hozana.org && sudo su hozana && cd ~
# Vérifier si l'ETL tourné actuellement
pgrep -f "python.*etl.main" && echo "ETL EN COURS" || echo "ETL PAS EN COURS"
# Derniere task reussie dans la DB
mycli --login-path data -D hozana_data -e \
"SELECT date, task, duration, status FROM etl_performances ORDER BY date DESC, start_time DESC LIMIT 10;"
# Taille des tables ETL-only (sont-elles peuplées ?)
mycli --login-path data -D hozana_data -e \
"SELECT table_name, table_rows, ROUND(data_length/1024/1024,1) as size_mb
FROM information_schema.tables WHERE table_schema='hozana_data'
AND table_name IN ('unique_users','unique_prayers','data_users','users_segments','user_donation_score');"
# Vérifier le dump exportée vers la prod
ls -lh ~/hozana_data_for_prod.dump.sql.gz
# Logs d'une task spécifique (ex: read_rate)
tail -50 ~/data-tools/log/etl.task.read_rate.log
# Relancer l'ETL manuellement (ATTENTION: vérifier qu'il ne tourne pas déjà)
cd ~/data-tools && python3.10 -m etl.main
# Relancer UNE task spécifique
cd ~/data-tools && python3.10 -m etl.run_task etl.task.users_donation_score
Alertes automatiques existantes
| Alerte | Condition | Canal | Action |
|---|---|---|---|
| Sentry | Temps réel > 18h | Sentry dashboard | Identifier la task la plus lente (probablement read_rate). Vérifier si ThreadPoolExecutor bloque. |
| Email rapport | Toujours envoye | arnaud@, thomas-sihapanya@ | Lire le rapport. Si erreur de validation → vérifier la coherence des données. |
| Sentry bash-hook | Erreur dans les scripts shell | Sentry | ETL-starter.sh, after_sql_dump_sent.sh.new, prepare_data_for_prod.sh rapportent a Sentry. |
Alertes manquantes (recommandées)
| Alerte proposee | Condition | Impact si absent |
|---|---|---|
| Metabase down | HTTP check sur metabase.data.hozana.org | Équipe data aveugle sans dashboards |
| Dump non arrivé | ETL-starter.sh poll sans mysql-anonymisation après 02h | ETL ne tourne pas, données stale en prod |
| unique_users échec | Task failed dans etl_performances | Cascade : users_stats, nsm_users, dau_mau, activity_stats tous skipped |
| data_* stale en prod | data_users.updated_at > 48h (verifiable cote Rosario UserStats) | CTA donation et Mailchimp merge fields désynchronisés |
Maintenir ce Document Vivant
Pourquoi ce document va devenir obsolete
Ce document est un snapshot au 24/03/2026. Il deviendra obsolete des que :
- Une nouvelle task est ajoutee a
main.py - Les colonnes de
prepare_data_for_prod.shchangent - Les seuils de segmentation ou de scoring sont recalibres
- Un nouveau Google Sheet est ajoute
- Le mécanisme de declenchement (ETL-starter.sh) change
Axes d'amélioration
| Axe | Description | Effort |
|---|---|---|
| Auto-generation | Parser main.py, les get_dependencies(), get_locked_tables(), only_on_saturday() pour générer le graphe de dépendances et le catalogue automatiquement. Un script Python qui produit le HTML. | 2-3j |
| Données live | Interroger etl_performances pour afficher les durées réelles du dernier run, les tasks en échec, les tendances. Remplacer les KPIs statiques par des requêtes SQL live. | 1j |
| Validation coverage | Ajouter validate_data() aux 72 tasks qui n'en ont pas. Commencer par les tasks critiques : unique_users, users_life_all, nsm_users. | 3-5j |
| Observabilité | Dashboards Metabase dédiés : durée par task sur 30j, taux d'échec, tendance du temps total. Alertes Slack/email si task critique échoué. | 1-2j |
| Tests d'intégration | Tests qui vérifient que les tables source existent avec les colonnes attendues avant de lancer l'ETL. Evite les échecs silencieux. | 2j |
Skills Claude Code recommandées
Voici des skills a creer pour maintenir ce document et monitorer l'ETL :
/etl-health — Diagnostic rapide de l'ETL (a lancer quotidiennement)
Déclencheur : /etl-health
Actions :
- Lire
log/etl.log(derniers 50 lignes) - Lire
log/task_execution_times.csv(dernier run) - Identifier les tasks failed/skipped
- Calculer le temps réel et CPU cumulé
- Comparer avec les seuils (8h reel, 18h CPU)
- Produire un rapport resume avec recommandations
/etl-cartography-update — Mettre a jour cette cartographie
Déclencheur : /etl-cartography-update
Actions :
- Parser
main.pypour détecter les nouvelles tasks - Pour chaque nouvelle task : lire le code, extraire dependencies/locks/saturday
- Vérifier que
prepare_data_for_prod.shn'a pas change - Mettre a jour
docs/etl-cartography.html - Lister les changements dans un resume
/etl-sql-validator — Valider les queries SQL avant merge
Déclencheur : Pre-commit ou PR review
Actions :
- Détecter les fichiers ETL modifies dans le diff
- Extraire les requêtes SQL
- Vérifier : pas de SELECT * sans WHERE, pas de DROP TABLE sans IF EXISTS, paramétrage des queries (pas de .format() avec des variables utilisateur)
- Vérifier la coherence des locks (si une task UPDATE une table, elle devrait la declarer dans get_locked_tables)
/etl-pr — Creer un PR structure pour un fix ETL
Déclencheur : /etl-pr
Skill existante : Deja configuree dans les skills Claude Code du projet.