This document describes what metrics are available in the current version of the script and how to use them.
The funnel_entries table is used for basic funnel analytics.
The table has a unique constraint on (email, funnel_type, test_id) that enforces idempotency at the database level. This constraint ensures that:
- The same user cannot be added to the same funnel multiple times for the same test.
- Duplicate records are prevented even under concurrent execution of the synchronization service.
- The service can be safely run multiple times without creating duplicate entries.
When the service attempts to insert a duplicate entry, the database constraint violation is handled gracefully: the transaction is rolled back, and an informational log message is recorded. No exception is raised, allowing the service to continue processing other candidates.
This idempotency guarantee eliminates race conditions (TOCTOU - Time-Of-Check-Time-Of-Use) that could occur when checking for existing entries before insertion. The database-level constraint provides a reliable, atomic mechanism to prevent duplicates.
Important fields:
-
emailUser email address.
-
funnel_typeFunnel type:
-
languagefor language tests; -
non_languagefor non-language tests.
-
-
user_id,test_idLink to test and user in MODX database.
-
entered_atDate and time when the user entered the funnel.
-
certificate_purchasedFlag:
-
0certificate not purchased; -
1certificate purchased.
-
-
certificate_purchased_atDate and time of certificate purchase. Populated during synchronization with MODX tables.
Before applying the unique constraint migration in production, operators should check for existing duplicate entries in the funnel_entries table. This can be done using the diagnostic script:
python -m scripts.find_funnel_duplicatesThe script queries the database for groups of entries that share the same (email, funnel_type, test_id) combination and displays them in a readable table format. The output includes:
- The duplicate email, funnel type, and test ID combination
- The count of duplicate entries for each combination
- The minimum and maximum entry IDs
- The earliest and latest entry timestamps
Important: If duplicates are found, they must be cleaned manually or via MySQL scripts before running the migration that adds the unique constraint. The migration will fail if duplicate entries exist when the constraint is applied.
The script is strictly read-only and does not modify the database in any way. It is safe to run on production databases for diagnostic purposes.
The brevo_sync_outbox table stores pending and processed jobs for Brevo synchronization. This table implements an outbox pattern, decoupling database writes from external API calls to ensure reliable processing of Brevo operations.
Each row in the outbox is linked to a funnel entry via the funnel_entry_id field, which references funnel_entries.id. The table tracks operation status, retry attempts, and error information to support reliable delivery of Brevo API calls.
Jobs in the outbox can have the following statuses:
-
pending: The job is waiting to be processed or scheduled for retry. Jobs withstatus='pending'andnext_attempt_at IS NULL OR next_attempt_at <= NOW()are eligible for processing. -
success: The job has been successfully processed. The job is no longer picked up by the worker. -
failed: The job has permanently failed after exceeding the maximum retry count or encountering a fatal error. The job is no longer picked up by the worker.
The outbox implements explicit retry scheduling using retry_count and next_attempt_at:
-
retry_count: Tracks the number of times a job has been attempted. This counter is incremented each time a job fails. -
next_attempt_at: A DATETIME field that specifies when the job should be retried. This field is used to schedule retries with exponential backoff:- When a job fails with a transient error and
retry_countis still below the maximum (default: 5), the job status remainspendingandnext_attempt_atis set toNOW() + INTERVAL (retry_count * 5) MINUTE. - When a job fails with a fatal error or exceeds the maximum retry count,
next_attempt_atis set toNULLand the job status is set tofailed.
- When a job fails with a transient error and
-
Maximum retries: By default, jobs are retried up to 5 times. After the maximum retry count is exceeded, the job is marked as
failedand is no longer processed by the worker.
The worker only processes jobs where status='pending' and (next_attempt_at IS NULL OR next_attempt_at <= NOW()), ensuring that scheduled retries are respected and jobs are not processed before their scheduled time.
To view funnel conversion, use the script:
python -m app.report_conversionsExample output:
Funnel conversion report
------------------------
language: entries=10, purchased=3, conversion=30.00%
non_language: entries=5, purchased=1, conversion=20.00%
Where:
entriesnumber of users who entered the funnel;purchasednumber of users who purchased a certificate after entering the funnel;conversionratiopurchased / entriesas a percentage.
You can specify a period:
python -m app.report_conversions --from-date 2024-01-01 --to-date 2025-01-01--from-dateinclusive;--to-dateexclusive.
If only --from-date is specified, entries from that date to the current moment are considered.
If parameters are not specified, all entries from funnel_entries are taken.
Example questions that can be answered:
- How many people entered the language funnel in the last month?
- What is the conversion rate from funnel to certificate purchase for language tests?
- How does language test conversion differ from non-language tests?
For more detailed analytics, you can use SQL queries to funnel_entries, combining conditions by email, user_id, test_id, and time.
Currently, the script does not modify email content, only sends contacts to Brevo. For extended analytics via UTM tags, you can use the following approach:
-
Add UTM tags to links in Brevo email templates, for example:
-
for language funnel:
?utm_source=testizer&utm_medium=email&utm_campaign=language_funnel -
for non-language:
?utm_source=testizer&utm_medium=email&utm_campaign=non_language_funnel
-
-
Analyze clicks on these UTMs in analytics systems (e.g., Google Analytics or similar).
-
If needed, link external analytics with
funnel_entriesdata by email and time periods.
With this approach, the database and script structure doesn't change, and extended analytics is configured through email templates and external reports.
If more detailed analytics are needed in the future, you can:
- add a
sourcefield tofunnel_entries, for exampleemail_language_v1,email_non_language_v1; - record funnel or campaign version there;
- build reports by combination of
funnel_type+source.
The current architecture is already ready for such extensions, as:
- funnel entry is recorded centrally in
funnel_entries; - certificate purchase is linked to the same entries;
- the
app.report_conversionsreport can be extended without changing the main business code.