Skip to content

Developer guide

app/addons/kv_ga4_ecommerce/
├── addon.xml scheme 3.0, settings (ROOT,STOREFRONT), order table, install/uninstall functions
├── init.php hook registration
├── func.php hook handlers, head config, Klaro integration, settings info blocks
├── controllers/frontend/kv_ga4_ecommerce.php POST "confirm": landing page confirms delivered purchases
├── controllers/backend/kv_ga4_ecommerce.php "process": runs the Measurement Protocol fallback (cron/console)
├── controllers/backend/kv_ga4_ecommerce_license.php "Check now" / cron license check
├── schemas/gdpr/klaro_config.post.php cookie consent services
├── schemas/permissions/admin.post.php controller permission (update_settings)
├── schemas/settings/variants.functions.post.php brand feature, currency, refund status variants
└── Tygh/Addons/KvGa4Ecommerce/ PSR-0 classes
├── Config.php validated settings, per storefront / per order
├── ItemBuilder.php product lines → GA4 items (batch loading)
├── Tracker.php session queue, cart/wishlist diff, page events, checkout steps
├── OrderTracker.php order rows, landing purchase, confirm tokens, cookies
├── MeasurementProtocol.php fallback runner, refunds, payload, HTTP
└── License/Client.php license check client (generated from the shared Kavio Labs template)
design/backend/templates/addons/kv_ga4_ecommerce/settings/{status,server}.tpl
design/backend/templates/addons/kv_ga4_ecommerce/license/info.tpl License tab (points to the card)
design/backend/templates/addons/kv_ga4_ecommerce/license/card.tpl License card (add-on page) / banner
design/backend/templates/addons/kv_ga4_ecommerce/license/status_bar.tpl status chip/banner for own admin pages
design/backend/templates/addons/kv_ga4_ecommerce/license/badge.tpl badge in Add-ons > Manage add-ons
design/backend/templates/addons/kv_ga4_ecommerce/hooks/addons/ tabs_content.pre.tpl (card), manage.post.tpl (badge)
design/backend/templates/addons/kv_ga4_ecommerce/hooks/index/styles.post.tpl loads license.less
design/backend/css/addons/kv_ga4_ecommerce/license.less card, chip and badge styles
var/themes_repository/responsive/templates/addons/kv_ga4_ecommerce/hooks/index/
├── head_scripts.post.tpl consent defaults, tag loader, kvGa4Config
├── body.pre.tpl GTM <noscript> iframe
└── scripts.post.tpl Klaro translations + func.js
js/addons/kv_ga4_ecommerce/func.js storefront runtime
var/langs/en/addons/kv_ga4_ecommerce.po

No core files are modified.

Type Hook Purpose
PHP before_dispatch Snapshot of the session cart and wishlist (once per PHP process)
PHP redirect Diff cart/wishlist → queue add_to_cart, remove_from_cart, add_to_wishlist before a redirect
PHP dispatch_before_display Diff again, page events, assign kv_ga4_config_json (page) or kv_ga4 (AJAX); throttled fallback run
PHP place_order Order row (client ID, session ID, consent); pending add_shipping_info / add_payment_info
PHP change_order_status refund through the Measurement Protocol
PHP login_user_post login
PHP update_profile sign_up (action add, storefront)
PHP before_dispatch (admin area and console only; static handler License\Client::onBeforeDispatch) Runs the due license check and shows license notices
Template (admin) addons:tabs_content (pre) License card at the top of the add-on page
Template (admin) addons:manage (post) License badge, moved into the add-on’s row (4.17–4.21 have no per-row hook)
Template (admin) index:styles (post) license.less (kv-license block)
Template index:head_scripts (post) Consent defaults, tag, config and page events
Template index:body (pre) GTM <noscript>
Template index:scripts (post) Runtime script
Schema gdpr/klaro_config Two Klaro services: kv_ga4_analytics, kv_ga4_ads

All hooks exist with the same signatures in CS-Cart 4.17.2 (Ultimate and Multi-Vendor), Multi-Vendor 4.18.3 and Multi-Vendor 4.21.2.

Tygh\Addons\KvGa4Ecommerce\License\Client verifies the license against the Kavio Labs license server. The client, the “Check now”/cron controller controllers/backend/kv_ga4_ecommerce_license.php and the License tab template design/backend/templates/addons/kv_ga4_ecommerce/license/info.tpl are generated from the shared Kavio Labs template; don’t edit them by hand.

  • Admin license UI (generated, REVISION 3; don’t edit by hand): design/backend/templates/addons/kv_ga4_ecommerce/license/card.tpl (License card at the top of the add-on page, every tab; hooks/addons/tabs_content.pre.tpl), license/badge.tpl (badge in Add-ons > Manage add-ons; hooks/addons/manage.post.tpl), license/status_bar.tpl (chip/banner for the add-on’s own admin pages), design/backend/css/addons/kv_ga4_ecommerce/license.less (loaded by the kv-license block of hooks/index/styles.post.tpl). The add-on has no admin pages of its own, so it uses no status bar. All of them render nothing for vendors and for administrators without the “Manage settings” privilege. The “what is switched off” sentence is the language variable kv_ga4_ecommerce.license_off_effect. License\Client assigns the view data to $kv_lic_kv_ga4_ecommerce in its before_dispatch handler (only for administrators with the Manage settings privilege); the templates only read this variable, so they render nothing (and no error) while an older version of the PHP code is loaded, for example in the request that uploads a new zip over the installed add-on. Templates never call add-on functions; PHP code can call fn_kv_ga4_ecommerce_license_ui($context).
  • fn_kv_ga4_ecommerce_is_licensed($storefront_id = 0) (func.php) is the gate. It reads the stored state only and never contacts the server. With a storefront ID in the admin area or cron it checks that storefront’s host (Ultimate with several domains).
  • Tracker::isTrackingActive() returns false: no kv_ga4_config_json (no tag, no GTM <noscript>), no events, no place_order tracking row, no login/sign-up events.
  • MeasurementProtocol::maybeRun() returns at once; processRow() returns waiting for orders of an unlicensed storefront (nothing marked, sent later when licensed and still within 72 hours); sendRefund() sends nothing.
  • Client::boot() (init.php) registers a before_dispatch handler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.
  • Request: POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.check with JSON {"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent through Tygh\Http (3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce.
  • State: storage key kv_license_kv_ga4_ecommerce (?:storage_data), or var/kv_storage/kv_ga4_ecommerce/license.json while the full page cache is active, protected by an HMAC with the store’s crypt key.
  • Cron: php admin.php --dispatch=kv_ga4_ecommerce_license.check.
  • Development stores only (http_host and https_host are development hosts): the constants KV_LICENSE_SERVER_URL, KV_LICENSE_TEST_HOST (act as if the development storefronts ran on that host) and KV_LICENSE_TEST_TIME_OFFSET (seconds). They are ignored on production hosts.
  1. Snapshot (before_dispatch): Tracker::takeSnapshot() stores cart_id → {product_id, quantity, price, options, …} for the cart and the wishlist.
  2. Diff (redirect and dispatch_before_display): Tracker::detectChanges() compares and re-baselines, so AJAX in-script redirects don’t report the same change twice. The diff is skipped when:
    • the user changed during the request (login or logout);
    • the request is auth.*, profiles.*, payment_notification.*, or checkout.place_order|complete|process_payment. Items flagged extra.exclude_from_calculate (promotion gifts) or extra.parent are ignored.
  3. Queue: events are stored in $_SESSION['kv_ga4_ecommerce']['queue'] (at most 30, dropped after one hour).
  4. Delivery:
    • full page: queued events plus page events go into window.kvGa4Config.events in the head;
    • AJAX: Tygh::$app['ajax']->assign('kv_ga4', ['events' => …, 'products' => …]), read on ce.ajaxdone.
  5. Sending (func.js):
    • gtag mode: gtag('event', name, params);
    • GTM mode: dataLayer.push({ecommerce: null}) followed by dataLayer.push({event, ecommerce});
    • purchases carry a confirm object, and the gtag event_callback / GTM eventCallback posts order_ids[] and tokens[] to kv_ga4_ecommerce.confirm.
{
"event": "purchase",
"params": { "transaction_id": "98", "value": 150, "currency": "USD", "tax": 0, "shipping": 28, "affiliation": "CS-Cart", "items": [ … ] },
"confirm": { "order_id": 98, "token": "435ddf4752af1a26586a4942" }
}

The token is HMAC-SHA256("kv_ga4_ecommerce:<order_id>", config.crypt_key), truncated to 24 characters.

mode, measurementId, containerId, loadTag, debug, userId, tagConfig, consent {mode, defaults[], adsDataRedaction, urlPassthrough, gdpr, cookie}, confirmUrl, events[], products{}.

products maps product URLs, and pid:<product_id> for quick view links, to list items. select_item uses this map.

kvGa4.send({ event: 'generate_lead', params: { value: 10, currency: 'USD' } }); // any custom event, same transport
kvGa4.consent({ analytics: true, ads: false }); // consent update from any CMP

cscart_kv_ga4_ecommerce_orders:

Column Meaning
order_id Order (child orders in Multi-Vendor; the parent is skipped by the fallback)
client_id, session_id From the _ga and _ga_<stream> cookies (GS1 and GS2 formats) at checkout
consent_analytics, consent_ads G granted, D denied, empty if unknown (from the kv_ga4_consent cookie)
placed_at, rendered_at, confirmed_at, mp_sent_at, refund_sent_at Timestamps
mp_attempts Failed fallback attempts; 255 = never send (consent denied, failed order, parent order, fallback disabled)
  • Endpoint: POST {host}/mp/collect?measurement_id=…&api_secret=…. The host is www.google-analytics.com or region1.google-analytics.com.
  • Body: client_id, optional user_id, timestamp_micros (order time, within 72 hours), consent {ad_user_data, ad_personalization}, and events[{name, params}]. The params include session_id and engagement_time_msec.
  • In debug mode, the same payload is first posted to /debug/mp/collect, and Google’s validationMessages are written to the log.
  • Fallback runner: MeasurementProtocol::maybeRun() runs at most every 5 minutes, on non-AJAX requests (3 orders on storefront requests, 25 in the admin panel). Cron: php admin.php --dispatch=kv_ga4_ecommerce.process.
  • More lists: Tracker::collectPageEvents() handles categories.view, products.search, product_features.view and companies.products. Other controllers that assign products can be added in an add-on with a higher priority by calling the same code from a dispatch_before_display handler.
  • Custom events: call kvGa4.send() in the browser. On the server, call \Tygh\Addons\KvGa4Ecommerce\Tracker::queue(['event' => 'x', 'params' => [...]]). Queued events must be enabled in Events, so only built-in event names pass isEventEnabled(). Use kvGa4.send() for custom ones.
  • Error handling: every hook handler catches \Throwable. With DEVELOPMENT on, errors go to the PHP error log with the [kv_ga4_ecommerce] prefix.

Tygh/Addons/KvGa4Ecommerce/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.