Tracking and Analytics
In order to track usage of the Add-on in your storefront and get valuable insights into funnel metrics (e.g. customers adding a product to the basket after virtually trying it on), we recommend setting up tracking events.
The Virtual Try-On widget emits a full analytics funnel onto the Google Tag Manager (GTM) dataLayer. This page describes the events the storefront pushes and the GTM and Google Analytics 4 (GA4) configuration you need to report on them.
Storefront Events
All event shaping lives in a single composable: useVirtualTryOnEvents.
The useVirtualTryOnEvents Composable
track accepts either of the following:
- The native
vto:trackCustomEventdispatched by the widget bundle, so you can bind it directly with@vto:track="track". - A plain
{ name, params }object, so the host can originate events such asvto_impression.
track unwraps the payload and forwards it to useTracking().push as follows:
useTracking().push then automatically merges the storefront page and session context. It's also SSR-guarded and consent-gated. As a result, Virtual Try-On events inherit the same envelope and privacy guarantees as every other storefront event.
For local debugging, a development-only [VTO] console.log, guarded by import.meta.dev, mirrors each push. It's removed from production builds by tree shaking.
Impression Event
The wrapper emits vto_impression once per PDP view of a product variant. It fires through the v-element-visibility directive from @vueuse/components, so placements below the fold count only once they're scrolled into view. When the selected variant changes while the widget is still visible, a new impression is reported.
The impression carries the part of the common envelope that the host knows:
The impression doesn't carry an entry_label, because it measures only that the widget was shown. The distinction between try_it_on and my_look depends on the saved-look state, which the host can't see. The widget reports it on the vto_open event instead.
Event Catalog
The following event names form the VtoEventName union:
vto_impression(emitted by the host)vto_openvto_step_viewvto_consent_givenvto_photo_selectedvto_generation_startedvto_generation_completedvto_generation_failedvto_result_viewvto_result_enlargevto_look_savedvto_look_downloadedvto_retryvto_add_to_cartvto_close
The widget bundle emits all events except vto_impression through @vto:track. Each of these events carries event-specific params, for example, { entry_label, has_saved_look }, in addition to the full common envelope. The host emits vto_impression with the reduced envelope described in Impression Event.
Commerce Attribution with Dual add_to_cart
When a customer adds a product to the basket from within the widget, the host does the following:
Perform the basket mutation
It performs the real basket mutation through useBasketActions().addItem, which fires its own cart state event. If this step fails, the host skips the next step, so the funnel is never counted twice.
Mirror the GA4-shaped event
It mirrors the standard GA4-shaped add_to_cart event, built from the product and variant props, through trackAddToBasket(...). The event is tagged with vto_source: true and interaction_source: 'virtual_try_on'.
This keeps Virtual Try-On add-to-carts inside the normal basket funnel while keeping them attributable. The widget's own vto_add_to_cart funnel event flows separately through vto:track.
Engagement Flag
On the first vto_generation_completed event per session, the composable pushes vto_engaged: true as a bare dataLayer variable. Because the push has no event key, GTM never treats it as a funnel event.
The variable stays available in the dataLayer, so you can use it to segment your own purchase and refund events by Virtual Try-On exposure. The widget itself never sees orders.
The flag is pushed only once per session. The guard uses sessionStorage, which survives client-side navigation and reloads within the tab, plus an in-memory flag. If sessionStorage is unavailable, for example, in private browsing mode, the in-memory guard is used.
Required Tracking Module Changes
To keep the pushes type-safe, make the following backward-compatible changes to the storefront tracking module:
- In
modules/tracking/runtime/types/tracking.ts:- Add
'virtual_try_on'toTrackingInteractionSource. - Add a
`vto_${string}`member toTrackingEventName. This accepts allvto_*event names without listing each one.
- Add
- In
modules/tracking/runtime/composables/useBasketEvents.ts, add an optionalextraParams?: Record<string, unknown>argument totrackAddToBasketand spread it first into the push. This lets the mirroredadd_to_cartevent carry{ vto_source: true }. Because the argument is optional, existing call sites aren't affected.
The storefront only pushes the vto_* events and vto_engaged onto window.dataLayer. Nothing consumes them until your GTM container has matching triggers and tags. For more information, see Google Tag Manager Setup.
Google Tag Manager Setup
This section describes only the Virtual Try-On-specific configuration you add on top of a working GTM setup. It requires no storefront code changes.
Before you start, make sure the standard GTM and dataLayer integration of your storefront is in place: the container is installed, connected to GA4, and consent is wired. For more information, see modules/tracking/TRACKING.md in your storefront.
Create the Trigger
All funnel events, from vto_open to vto_close plus the host-emitted vto_impression, share the vto_ prefix. This means a single Custom Event trigger consumes the whole funnel.
To create the trigger, configure the following in GTM:
- Trigger type – "Custom Event"
- Event name –
^vto_, with Use regex matching enabled - This trigger fires on – "All Custom Events"
Per-event triggers also work. However, we recommend the single regex trigger, because you maintain it in one place and it picks up new vto_* events automatically.
Forward Events to GA4
To forward the events, fire a GA4 Event tag, or your own analytics tag, on the trigger:
- Event Name –
{{Event}}, the built-in GTM variable that holds thedataLayereventvalue. This forwards eachvto_*event name unchanged. - Event parameters – map the parameters you want to report on from Data Layer Variables. The
pageandsessionenvelope is already on every push, added byuseTracking, so page context is included without extra mapping.
Register Custom Dimensions
GA4 reports only on parameters that are registered as custom dimensions. For each Virtual Try-On field you want to segment or report on, create a Data Layer Variable and a matching GA4 custom dimension. The following table lists the recommended fields:
| Field | Source | Suggested GA4 Scope | Purpose |
|---|---|---|---|
vto_engaged | Bare dataLayer variable, without event | User-scoped (user property) | Segment your own purchase and refund events by Virtual Try-On exposure. This is the key attribution dimension. |
vto_source | Mirrored standard add_to_cart event, value true | Event-scoped | Distinguish add-to-carts that originate from Virtual Try-On within the normal basket funnel, without double counting. |
try_on_session_id | Common parameter, from vto_generation_started onward | Event-scoped | Join the client funnel to the server-side generation_log or the Generation Usage API. |
vto_interaction_id | Common parameter on all widget events, not on the host-emitted vto_impression | Event-scoped | Connect a single widget engagement across the pre-generation funnel. |
| Common parameters on every vto_* event, including vto_impression | Event-scoped | Standard segmentation by customer type (guest or registered), product, variant, and shop. |
widget_version | Common parameter on widget events only, because the host can't know it | Event-scoped | Attribute funnel behavior to a widget release. |
vto_engaged is the only field that must persist beyond the event that set it. Register it as a user-scoped custom dimension (user property), sourced from the vto_engaged Data Layer Variable. This makes it available on your later purchase and refund events, because user scope keeps the value on all later events from the same user, including later sessions.
Troubleshooting
GTM Doesn't Start (gtm_debug 403)
If the GTM runtime doesn't start, it looks the same as a tracking bug, because the storefront side keeps working: window.dataLayer fills with every storefront and vto_* event, nothing is logged, and no console error appears. Only the container is missing, so nothing consumes the events.
The most common cause is NUXT_PUBLIC_TRACKING_GTM_DEBUG=true. This setting sets runtimeConfig.public.tracking.gtm.debug, which @nuxt/scripts turns into >m_debug=x on the container URL. Outside a live Tag Assistant session, Google responds to that URL with 403:
The script then fires onerror, and window.google_tag_manager is never created.
To fix this, keep the flag set to false. You don't need it for debugging: when you connect through Preview, Tag Assistant adds its own gtm_auth and gtm_preview parameters.
To check whether GTM is running, run the following in the browser console on a page that should be tracked:
gtm.js pushes gtm.dom and gtm.load itself. If they're missing, the runtime never ran, no matter how complete the rest of the dataLayer looks.
A blocked request produces the same symptom, for example, when an ad blocker or privacy extension cancels requests to googletagmanager.com. To rule this out, test in an incognito window, where extensions are disabled by default.