Setting up abandoned cart tracking in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Configuring Abandoned Cart Tracking in 1C-Bitrix

An abandoned cart is when a product has been added but the order was never completed. Statistically, 65–75% of carts are abandoned. Recovering even 10% of them means increasing revenue without increasing traffic. The first step is to set up tracking: understand how many carts are being abandoned, who these users are, and what they left behind.

Where Cart Data Is Stored in Bitrix

The cart in 1C-Bitrix is linked to a virtual user (fuser) — an anonymous session identifier.

  • b_sale_fuser — a record for each visitor (anonymous and authenticated)
  • b_sale_basket — cart items linked to FUSER_ID
  • b_sale_basket.DATE_INSERT, b_sale_basket.DATE_UPDATE — date of addition and last modification

On login, the session's FUSER_ID is associated with the real user: b_sale_fuser.USER_ID. Before login, USER_ID = null.

Defining an "Abandoned" Cart

A cart is considered abandoned if:

  1. b_sale_basket contains items
  2. No completed order exists for that FUSER_ID within the last N hours
  3. The cart's last update (DATE_UPDATE) was more than M hours ago
// Select abandoned carts: items added more than 1 hour ago, no order placed
$cutoffTime = new \Bitrix\Main\Type\DateTime();
$cutoffTime->add('-1 hour');

$abandonedFusers = \Bitrix\Sale\BasketTable::getList([
    'filter' => [
        '<DATE_UPDATE' => $cutoffTime,
        '=ORDER_ID'    => false, // not linked to an order
    ],
    'group'  => ['FUSER_ID'],
    'select' => ['FUSER_ID'],
])->fetchAll();

After obtaining the FUSER_ID, check whether it has a USER_ID (authenticated user):

foreach ($abandonedFusers as $row) {
    $fuser = \Bitrix\Sale\FuserTable::getList([
        'filter' => ['=ID' => $row['FUSER_ID']],
        'select' => ['USER_ID', 'DATE_UPDATE'],
    ])->fetch();

    if (!$fuser || !$fuser['USER_ID']) continue; // anonymous — no contact for communication

    // Get the user's email
    $user = \Bitrix\Main\UserTable::getById($fuser['USER_ID'])->fetch();
    $email = $user['EMAIL'] ?? '';

    if (!$email) continue;

    // Add to the notification queue
    AbandonedCartQueue::push($fuser['USER_ID'], $row['FUSER_ID']);
}

Tracking via Analytics (GA4 / Yandex.Metrica)

In parallel with server-side tracking, configure client-side events:

// When a product is added to the cart
BX.addCustomEvent('onBasketItemAdded', function(data) {
    gtag('event', 'add_to_cart', {
        currency: 'RUB',
        value:    data.price * data.quantity,
        items: [{
            item_id:   data.productId,
            item_name: data.productName,
            price:     data.price,
            quantity:  data.quantity,
        }],
    });
    ym(METRIKA_ID, 'reachGoal', 'add_to_cart');
});

// When a product is removed from the cart
BX.addCustomEvent('onBasketItemDeleted', function(data) {
    gtag('event', 'remove_from_cart', { /* ... */ });
});

In GA4: Reports → Monetisation → Checkout journey — shows the funnel from add-to-cart to purchase with drop-off percentages at each step.

Agent for Regular Abandoned Cart Detection

Run the agent every 30–60 minutes:

// In /local/php_interface/init.php
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'main', 'OnBeforeLocalRedirect', // or register via agents
    function() {}
);

// Agent (Settings → Agents)
function checkAbandonedCarts(): string
{
    AbandonedCartService::processNew();
    return __FUNCTION__ . '();'; // return for recurring execution
}

AbandonedCartService::processNew() — your class that selects newly abandoned carts (not yet marked as processed) and adds them to the notification queue.

Storing Processing Status

Create a table to store abandoned cart status:

class AbandonedCartTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'local_abandoned_cart'; }

    public static function getMap(): array
    {
        return [
            new \Bitrix\Main\ORM\Fields\IntegerField('ID',      ['primary' => true, 'autocomplete' => true]),
            new \Bitrix\Main\ORM\Fields\IntegerField('USER_ID'),
            new \Bitrix\Main\ORM\Fields\IntegerField('FUSER_ID'),
            new \Bitrix\Main\ORM\Fields\StringField('STATUS'),   // new, email_sent, sms_sent, recovered
            new \Bitrix\Main\ORM\Fields\DatetimeField('DETECTED_AT'),
            new \Bitrix\Main\ORM\Fields\DatetimeField('EMAIL_SENT_AT'),
            new \Bitrix\Main\ORM\Fields\DatetimeField('SMS_SENT_AT'),
            new \Bitrix\Main\ORM\Fields\FloatField('CART_VALUE'),
        ];
    }
}

When a user places an order, update the status to recovered via the OnSaleOrderSaved event.

Reporting: How Many Are Recovered

After a few weeks of operation you have data: how many carts were detected, how many communications were sent, and how many conversions occurred after the communication. Build a simple report:

SELECT
    DATE(detected_at) AS date,
    COUNT(*) AS detected,
    SUM(CASE WHEN status = 'recovered' THEN 1 ELSE 0 END) AS recovered,
    ROUND(SUM(CASE WHEN status = 'recovered' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) AS recovery_rate
FROM local_abandoned_cart
GROUP BY DATE(detected_at)
ORDER BY date DESC;

Timelines: basic server-side tracking and agent — 1–2 days. Client-side GA4/Metrica events — 4 hours. Status table and reporting — an additional 1 day.