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:
- 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.
- 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 (oftendiv#cw-portal-root) with an empty Click URL and indistinguishable Click Text. - 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.
Consent / CMP bridges
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, andsubmit_event, or - Skip the bridge and set
gtmDataLayerNameto 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
- Open GTM Preview (Tag Assistant) on the page with the widget.
- Open an event and click View website (or Get directions).
- In the left-hand event list, select
click_outbound. - In Data Layer, confirm
link_type,link_url, andlink_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).