Add purchase event deduplication with event_id and localStorage - #45
Conversation
The success page pushes trytagging_purchase along two independent paths: the server-rendered layout event from checkout_onepage_success.xml and the session event that reaches the page through the gtm-checkout customerData section. Both carry the same values, but the layout path builds ecommerce with items first (Tag\Order\Order is a MergeTag that TagParser unsets and merges back), so the JSON.stringify hash that guards against duplicates differed and the second push went through. On Luma that means two purchase hits per order; Hyva only reads the cart and customer sections, so it saw one. - Hash events on a key-sorted serialization, so two events holding the same values dedup regardless of the order the keys were assembled in. - Dedup purchases on their transaction as well, kept in localStorage, since the in-memory guard is reset on every page load and does not survive a reload of the success page or a session event arriving on the next page view. Falls back to the in-memory guard when storage is unavailable. - Add a deterministic event_id (purchase.<increment_id>) to both browser events and to the order_created webhook. Meta deduplicates browser and Conversions API events on event_id, not on transaction_id, so both sides need the same value. EventIdGenerator is the single place that defines the format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Et1MecPwQ7nDwHxAxUsEQ2
MikeVerlinde
left a comment
There was a problem hiding this comment.
Koen mentioned the fix is for GA4. But GA4 deduplicates on transaction_id. Meta deduplicates on order_id.
If the fix is for GA4, the already present transaction_id should suffice. If for Meta, this fix is correct.
|
It is both, but they are two separate parts of this PR — and the two claims are worth taking apart. The duplicate pushes are the actual defect, and they are platform-agnostic. On Luma the success page pushes That is what the key-sorted hash and the per-transaction guard fix, and they fix it before the dataLayer — so it lands for GA4, Meta, Ads and our own logs at once. Relying on any platform's dedup to absorb a double push that our own module emits would leave the defect in place. On GA4 and On Meta and References:
No code change follows from this, so I have not pushed anything. Two things do still sit outside this repo before the Meta half pays off: Generated by Claude Code |
Summary
This PR implements purchase event deduplication across page reloads by introducing a deterministic
event_idfield and persistent transaction tracking via localStorage. This ensures purchase events are not double-counted when the success page is reloaded, and aligns browser-side and server-side events for proper Meta deduplication.Key Changes
EventIdGeneratorutility (Util/EventIdGenerator.php): Generates deterministic event IDs in the formatpurchase.<increment_id>for orders, ensuring consistency between browser and Conversions API eventsEventIdtag (DataLayer/Tag/Order/EventId.php): Provides the event ID from the checkout session's last orderpush.jsandscript-pusher.phtml:stableStringify(): Serializes objects with sorted keys for consistent hashing regardless of key assembly ordergetPushedTransactions(): Retrieves previously pushed transaction IDs from localStoragerememberTransaction(): Persists transaction IDs to localStorage (keeps last 50)getTransactionId(): Extracts transaction ID from purchase eventsPurchase.phpandPurchaseWebhookEvent.phpnow include theevent_idfield in their outputUSAGE.mdexplaining the event_id mapping and localStorage behaviorImplementation Details
purchase.<increment_id>) is shared between browser dataLayer events and theorder_createdwebhook for Meta's event deduplicationhttps://claude.ai/code/session_01Et1MecPwQ7nDwHxAxUsEQ2