Google Tag Manager & GA4 in the Calendar Widget

The widget does not load Google Tag Manager, gtag.js, or a GA4 Measurement ID. It only pushes events onto a data layer array on the host page (window.dataLayer by default). Your GTM container on that page is what sends those events to Google Analytics.

How it works

User clicks "View website" in the widget
        ↓
Widget pushes { event: "click_outbound", link_type: "website", ... }
        ↓
window.dataLayer  (or the name you pass in gtmDataLayerName)
        ↓
Your GTM container (Custom Event trigger)
        ↓
GA4 Event tag

Three consequences:

  1. If GTM is not on the host page, nothing is sent to GA. The objects still appear in the JavaScript array; they are not forwarded anywhere.
  2. Do not use GTM’s automatic Click / Link Click triggers (gtm.click, gtm.linkClick) for widget buttons. The UI runs in Shadow DOM, so GTM only sees the shadow host (often div#cw-portal-root) with an empty Click URL and indistinguishable Click Text.
  3. Use the custom events below (click_outbound, calendar_query, and so on). Those are the supported contract.

Embed setup (webmaster)

Your page must already include the GTM snippet. Then mount the widget as usual.

Default data layer (dataLayer)

If your GTM snippet uses the default name (most sites):

window.CalendarWidget.mount('#calendar-widget', {
  calendarId: 'your-calendar-id',
  usePopups: true,
});

Events are pushed to window.dataLayer. No extra option is required.

The same default applies to the submission form:

window.SubmitFormWidget.mount('#calendar-widget', {
  calendarId: 'your-calendar-id',
});

Renamed data layer (gtmDataLayerName)

Some sites install GTM with a custom data layer name (4th argument of the GTM snippet), for example gtmDataLayer:

<script>
  (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
  })(window,document,'script','gtmDataLayer','GTM-XXXXXXX');
</script>

In that case GTM never reads window.dataLayer. Point the widget at the same name:

window.CalendarWidget.mount('#calendar-widget', {
  calendarId: 'your-calendar-id',
  gtmDataLayerName: 'gtmDataLayer',
});
window.SubmitFormWidget.mount('#calendar-widget', {
  calendarId: 'your-calendar-id',
  gtmDataLayerName: 'gtmDataLayer',
});
Option Default Meaning
gtmDataLayerName 'dataLayer' window property the widget pushes to

Empty or omitted values fall back to 'dataLayer'. The widget creates the array if it does not exist yet. If that property already exists and has no .push() method, the widget skips the event and logs a one-time warning in the console.

Pass exactly the name your GTM snippet uses. Pushing to the wrong array is the most common reason Preview shows gtm.click but never click_outbound.

If a consent platform (for example Usercentrics) copies events from dataLayer to your renamed layer, check which event names it copies. Many bridges only forward consent_status* and drop widget events.

You can either:

  • Widen the bridge so it also copies click_outbound, calendar_query, calendar_view_click, map_event_click, and submit_event, or
  • Skip the bridge and set gtmDataLayerName to the array GTM actually reads.

Do not do both for the same events, or they will fire twice.


Verify in the browser (before GTM tags)

Open the page that embeds the widget, open DevTools → Console, interact with the calendar, then:

// default
dataLayer.filter((e) => e && typeof e.event === 'string' && [
  'click_outbound',
  'calendar_query',
  'calendar_view_click',
  'map_event_click',
  'submit_event',
].includes(e.event))

If you use a renamed layer:

gtmDataLayer.filter((e) => e && e.event === 'click_outbound')
Result What it means
Object appears in the array GTM reads Widget is fine. Finish GTM tags if Preview still has no GA hit.
Object appears only in dataLayer, not in your renamed layer Set gtmDataLayerName or extend the consent bridge.
No object after a click The click did not produce a widget event, or an old widget bundle is on the page.

Recommended smoke tests

Action Event to look for
Open an event and click View website click_outbound with link_type: "website"
Click Get directions click_outbound with link_type: "directions"
Switch list / map / calendar calendar_view_click
Search or change filters calendar_query

In GTM Preview, look at the full event list on the left. Do not filter to “Click”. The widget event name is click_outbound, not gtm.click.


Event catalog

All events are plain objects pushed with event plus parameters. Parameter names are stable; treat them as the public analytics API.

click_outbound

Outbound / CTA clicks on the event profile and related links.

Parameter Type Description
event string Always 'click_outbound'
link_type string What was clicked. See table below.
link_url string Destination URL (mailto:, tel:, calendar URL, or 'download' for .ics)
link_domain string Hostname, 'phone', 'email', 'ics', or similar
page_location string window.location.href of the host page at click time

link_type values

User action link_type When it fires
View website (event URL) website Immediately on click
Get tickets (one ticket URL) url Immediately on click
Get tickets (several ticket URLs) url When a ticket link is chosen in the menu, not when the menu opens
Add to calendar → Google / Outlook / .ics calendar When a provider is chosen, not when the button opens the menu
Get directions directions Immediately on click
Venue or organizer phone phone Immediately on click
Venue or organizer website url Immediately on click
Organizer email email Immediately on click

Example after View website:

{
  event: 'click_outbound',
  link_type: 'website',
  link_url: 'https://example.com/event',
  link_domain: 'example.com',
  page_location: 'https://www.example.com/events/?_cwpath=event&_cwslug=…'
}

calendar_view_click

Fired when the visitor switches calendar view (grid, rows, map, calendar, carousel).

Parameter Type Description
event string 'calendar_view_click'
view string 'grid' | 'rows' | 'map' | 'calendar' | 'carousel'
page_location string Host page URL

Legacy embed configs may still use list; the widget treats that as grid.

calendar_query

Fired when search or filters are applied and results are fetched.

Parameter Type Description
event string 'calendar_query'
search_term string Search box text, or ''
category_filter string Selected category labels joined with |
region_filter string Selected region names joined with |
date_start string YYYY-MM-DD or ''
date_end string YYYY-MM-DD or ''
filters_active_count number How many filter groups are active
results_count number Number of events returned

map_event_click

Fired when the visitor clicks an event pin on the map view.

Parameter Type Description
event string 'map_event_click'
event_id number / string Seeker event id
event_slug string Event slug
event_name string Event title
page_location string Host page URL

submit_event

Fired after a successful guest event submission (SubmitFormWidget or the in-calendar submit flow).

Parameter Type Description
event string 'submit_event'
page_location string Host page URL

GTM setup (analyst)

Do this in your GTM container — the one that already loads on the host page.

1. Confirm the event in Preview

  1. Open GTM Preview (Tag Assistant) on the page with the widget.
  2. Open an event and click View website (or Get directions).
  3. In the left-hand event list, select click_outbound.
  4. In Data Layer, confirm link_type, link_url, and link_domain.

If you only see gtm.click / gtm.linkClick, you are looking at GTM’s built-in click listener. That is expected and not usable for widget CTAs. Scroll for click_outbound. If it is missing, go back to Verify in the browser and Renamed data layer.

2. Data Layer Variables

Variables → New → Data Layer Variable (one each):

Variable name (suggested) Data Layer Variable Name
DLV - link_type link_type
DLV - link_url link_url
DLV - link_domain link_domain
DLV - view view
DLV - search_term search_term
DLV - event_name event_name
DLV - event_id event_id
DLV - event_slug event_slug

Version 1 is correct for these keys. You do not need to create a variable for event itself.

Until a future widget update includes event id on every outbound click, you can also create a URL variable: Component type Query, Query key _cwslug, to read the event slug from page_location when the widget is in popup / query-param mode.

3. Triggers (Custom Event)

Triggers → New → Custom Event

Trigger name (suggested) Event name Fire on
CE - click_outbound click_outbound All Custom Events
CE - calendar_query calendar_query All Custom Events
CE - calendar_view_click calendar_view_click All Custom Events
CE - map_event_click map_event_click All Custom Events
CE - submit_event submit_event All Custom Events

To send Get directions separately from View website, duplicate the click_outbound trigger and add a condition: DLV - link_type equals directions (or website, calendar, phone, url, email).

Do not use:

  • Click - All Elements
  • Click - Just Links
  • Form Submission
  • CSS selectors / Click Classes on widget buttons

Those inspect the light DOM and cannot see inside Shadow DOM.

4. GA4 Event tags

Tags → New → Google Analytics: GA4 Event

Use the same GA4 Configuration tag you already use for the rest of the site. Match that tag’s consent settings (and any CMP exception) so widget hits are not blocked while other GA4 hits are allowed, or vice versa.

Suggested GA4 event name Trigger Event parameters
calendar_outbound_click CE - click_outbound link_type, link_url, link_domain
calendar_query CE - calendar_query search_term, category_filter, region_filter, date_start, date_end, results_count
calendar_view_click CE - calendar_view_click view
calendar_map_event_click CE - map_event_click event_id, event_slug, event_name
calendar_submit_event CE - submit_event (none required)

You may reuse the data-layer event names as GA4 event names (click_outbound, …). Pick one convention and keep it.

Example parameter mapping for the outbound tag:

Parameter name Value
link_type {{DLV - link_type}}
link_url {{DLV - link_url}}
link_domain {{DLV - link_domain}}

5. GA4 custom definitions

In GA4 Admin → Custom definitions, create event-scoped custom dimensions before you rely on them in reports. Hits sent before the definition exists will not backfill that dimension.

At minimum:

Dimension name Scope Event parameter
Link type Event link_type
Link URL Event link_url
Calendar view Event view

DebugView shows events immediately. Standard reports typically lag 24–48 hours.

6. Publish

Publish the GTM container. Then confirm in GA4 DebugView (or GTM Preview → the GA4 tag fired).