Developer guide
File map
Section titled “File map”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}.tpldesign/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) / bannerdesign/backend/templates/addons/kv_ga4_ecommerce/license/status_bar.tpl status chip/banner for own admin pagesdesign/backend/templates/addons/kv_ga4_ecommerce/license/badge.tpl badge in Add-ons > Manage add-onsdesign/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.lessdesign/backend/css/addons/kv_ga4_ecommerce/license.less card, chip and badge stylesvar/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.jsjs/addons/kv_ga4_ecommerce/func.js storefront runtimevar/langs/en/addons/kv_ga4_ecommerce.poNo core files are modified.
Hooks used
Section titled “Hooks used”| 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.
License check
Section titled “License check”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 ofhooks/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 variablekv_ga4_ecommerce.license_off_effect.License\Clientassigns the view data to$kv_lic_kv_ga4_ecommercein itsbefore_dispatchhandler (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 callfn_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()returnsfalse: nokv_ga4_config_json(no tag, no GTM<noscript>), no events, noplace_ordertracking row, no login/sign-up events.MeasurementProtocol::maybeRun()returns at once;processRow()returnswaitingfor orders of an unlicensed storefront (nothing marked, sent later when licensed and still within 72 hours);sendRefund()sends nothing.Client::boot()(init.php) registers abefore_dispatchhandler 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.checkwith JSON{"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent throughTygh\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), orvar/kv_storage/kv_ga4_ecommerce/license.jsonwhile 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_hostandhttps_hostare development hosts): the constantsKV_LICENSE_SERVER_URL,KV_LICENSE_TEST_HOST(act as if the development storefronts ran on that host) andKV_LICENSE_TEST_TIME_OFFSET(seconds). They are ignored on production hosts.
Event pipeline
Section titled “Event pipeline”- Snapshot (
before_dispatch):Tracker::takeSnapshot()storescart_id → {product_id, quantity, price, options, …}for the cart and the wishlist. - Diff (
redirectanddispatch_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.*, orcheckout.place_order|complete|process_payment. Items flaggedextra.exclude_from_calculate(promotion gifts) orextra.parentare ignored.
- Queue: events are stored in
$_SESSION['kv_ga4_ecommerce']['queue'](at most 30, dropped after one hour). - Delivery:
- full page: queued events plus page events go into
window.kvGa4Config.eventsin the head; - AJAX:
Tygh::$app['ajax']->assign('kv_ga4', ['events' => …, 'products' => …]), read once.ajaxdone.
- full page: queued events plus page events go into
- Sending (
func.js):- gtag mode:
gtag('event', name, params); - GTM mode:
dataLayer.push({ecommerce: null})followed bydataLayer.push({event, ecommerce}); - purchases carry a
confirmobject, and the gtagevent_callback/ GTMeventCallbackpostsorder_ids[]andtokens[]tokv_ga4_ecommerce.confirm.
- gtag mode:
Event object
Section titled “Event object”{ "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.
window.kvGa4Config
Section titled “window.kvGa4Config”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.
JavaScript API
Section titled “JavaScript API”kvGa4.send({ event: 'generate_lead', params: { value: 10, currency: 'USD' } }); // any custom event, same transportkvGa4.consent({ analytics: true, ads: false }); // consent update from any CMPDatabase
Section titled “Database”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) |
Measurement Protocol
Section titled “Measurement Protocol”- Endpoint:
POST {host}/mp/collect?measurement_id=…&api_secret=…. The host iswww.google-analytics.comorregion1.google-analytics.com. - Body:
client_id, optionaluser_id,timestamp_micros(order time, within 72 hours),consent {ad_user_data, ad_personalization}, andevents[{name, params}]. The params includesession_idandengagement_time_msec. - In debug mode, the same payload is first posted to
/debug/mp/collect, and Google’svalidationMessagesare 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.
Extending
Section titled “Extending”- More lists:
Tracker::collectPageEvents()handlescategories.view,products.search,product_features.viewandcompanies.products. Other controllers that assignproductscan be added in an add-on with a higher priority by calling the same code from adispatch_before_displayhandler. - 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 passisEventEnabled(). UsekvGa4.send()for custom ones. - Error handling: every hook handler catches
\Throwable. WithDEVELOPMENTon, errors go to the PHP error log with the[kv_ga4_ecommerce]prefix.
Third-party code
Section titled “Third-party code”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.