← Back to Documentation

What Was Done

GTM Workspace: TZI - Temp

Important: All changes described below are made in a GTM workspace called TZI - Temp. This ensures our changes don't conflict with your current work.

Workspace Link: TZI - Temp Workspace

You can view the workspace and test the changes yourself if you'd like.

Status: This is still going through QA testing and will be pushed, most likely around 3pm EST on Friday, May 8th. If you prefer not to push on weekends then let us know and we will wait until Monday.

The implementation adds minimal complexity to your existing GTM setup while solving the conversion inflation problem.

Summary of Changes

New Variables Added

New GTM Variables

1. Fire Once Per Session

This is the core logic variable that implements the once-per-session deduplication.

What it does:

The code:

function() {
  var type       = '';
  var identifier = '';
  var gtmEvent   = {{Event}};

  // VIDEO
  if (gtmEvent === 'gtm.video') {
    type = 'video';

    var videoUrl   = {{Video URL}} || '';
    var cleanVideo = videoUrl
      .replace(/^https?:\/\/(www\.)?/i, '');

    identifier = cleanVideo + '_' + {{Video Percent}};

  // SCROLL
  } else if (gtmEvent === 'gtm.scrollDepth') {
    type       = 'scroll';
    identifier = {{Scroll Depth Threshold}};

  // LINK CLICK
  } else if (gtmEvent === 'gtm.linkClick') {
    type = 'link_click';

    var clickUrl    = {{Click URL}} || '';
    var rootDomain  = window.location.hostname;
    var isInternal  = clickUrl.indexOf(rootDomain) !== -1;
    var newWinEntry = window.dataLayer
      .slice()
      .reverse()
      .find(function(obj) {
        return obj['gtm.willOpenInNewWindow'] !== undefined;
      });
    var newWindow = newWinEntry
      ? newWinEntry['gtm.willOpenInNewWindow']
      : false;

    identifier = (newWindow && !isInternal) ? clickUrl : {{Click Text}};

  } else {
    return false;
  }

  // GUARD — nothing to key on
  if (identifier === undefined || identifier === null || identifier === '') {
    return false;
  }

  var normalized = identifier
    .toString()
    .toLowerCase()
    .trim()
    .replace(/\s+/g, '_')
    .replace(/[^a-z0-9_\-\/]/g, '');

  var key = 'session_event_' + type + '_' + normalized;

  if (sessionStorage.getItem(key)) {
    return false;
  }

  sessionStorage.setItem(key, 'true');
  return true;
}
    

How it works:

When an event fires, this variable:

  1. Determines the event type (video, scroll, or link click) by checking the GTM event
  2. For video events: Cleans the video URL and combines it with the percentage watched
  3. For scroll events: Uses the scroll depth threshold directly
  4. For link click events: Intelligently determines the identifier:
    • Checks if the link is internal or external
    • Checks if the link opens in a new window (using gtm.willOpenInNewWindow from dataLayer)
    • If external AND opens in new window: uses the URL
    • Otherwise: uses the click text
  5. Normalizes the identifier to create a consistent key
  6. Checks if this key already exists in session storage
  7. If it exists, returns false (block the event)
  8. If it doesn't exist, sets the session key and returns true (allow the event)

Example session keys:

session_event_link_click_give_it_a_try
session_event_video_youtubecom/watchvengw7tlk6r8_90
session_event_video_youtubecom/watchvengw7tlk6r8_100
session_event_scroll_50
session_event_link_click_https//linkedincom/
  

Note: Special characters are removed from session keys for safety and consistency.

2. DLV - willOpenInNewWindow

This is a data layer variable that determines if a button/link will open in a new window.

What it does:

Note: This is an out-of-the-box GTM variable type. No custom JavaScript is needed - GTM provides this functionality natively.

Trigger Updates

The following 12 triggers were updated with one additional condition:

The change: Added one condition to each trigger:

Fire Once Per Session = true
  

This ensures that each event only fires once per session, preventing conversion inflation while preserving analytics visibility.

Trigger Update Examples

URL-Based Trigger Example

URL Based Trigger Configuration

Text-Based Trigger Example

Text Based Trigger Configuration

Scroll Depth Trigger Example

Scroll Depth Trigger Configuration

YouTube Video Trigger Example

YouTube Video Trigger Configuration

Why This Approach?

This solution is elegant because:

🚀 Scalable: You can add {{Fire Once Per Session}} to any future GA4 or other vendor pixels and it will work for any click/video/scroll trigger in GTM

Video Events Note

GA4 video events will fire once per video per segment.

This should be fine as all the values sent to Google Ads will still be unique if you're sending video title and progress to Google Ads. Each video engagement will be tracked separately based on the video URL and percentage watched.