Skip to content

Conversation

@giovaniortolani
Copy link
Contributor

@giovaniortolani giovaniortolani commented Nov 11, 2025

This PR totally solves the issues #28 and https://community.stape.io/t/data-tag-client-property-pollution-across-events/3425/15, and is an alternative and better option to the PR #33.


Solution that covers all problematic scenarios described in #28 and https://community.stape.io/t/data-tag-client-property-pollution-across-events/3425/15.

Description of the problem and why it happens:

The ideal scenario would be the tag accessing the data model immediately when it runs, not from inside the injectScript success handler. However, access to the data model (window.dataTagGetData.dataModel) is only available after loading an external script because it's not possible to directly access some things in the global scope through GTM templates.

However, GTM wraps the injectScript success/failure handlers in setTimeout(handler, 0), deferring their execution to the next event loop cycle. This delays data model access, making it too late to capture the correct state from the triggering data layer push.

Note: the Send all from DataLayer option collects the GTM dataModel, not the actual dataLayer object of when the tag fired (which can be different due to async operations).
Learn more: [1] and [2].

Solutions:

  • A flag in window prevents injectScript() from running again after the script has loaded, avoiding repeated setTimeout executions. This flag is actually a flag for each script URL, to account for scenarios where multiple GTMs are executing the Data Tag (with or without the same script URL). [1]
  • The dataTagGetData global function from data-tag.js now accepts 3 new arguments: Event ID, a flag Use Own Data Model and a flag Use only current data layer event object. The Use Own Data Model flag, when enabled, recreates the dataModel manually by replicating what GTM does under the hood, and it doesn't use the native dataModel value from GTM (as suggested here). And the Use only current data layer event object, if enabled, only uses the data layer event that triggered the tag.
    [1] [2] [3].
  • The Event ID is saved in the tag’s global context before the injectScript handler runs, ensuring it's available to sendPostRequest(), and is passed to the dataTagGetData global function from data-tag.js to be used to locate the actual data layer event that triggered the tag, and then build the dataModel manually. This follows the same idea proposed here, but adjusts timing and placement. [1] [2]

event: item[1],
eventModel: eventParams
};
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we can also process consent, just in case:

} else if (
  command === 'consent' &&
  isPlainObject(item[2])
) {
  processedItem = {
    consent: cloneObject(item[2])
  };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kucherenko-ae! The consent command was intentionally omitted because the native code creating the dataModel does not put it into the dataModel. The purpose of this custom code is to replicate the functionality of the native code.

@giovaniortolani giovaniortolani force-pushed the race-conditions-issue-data-model-replication branch from 57a5e01 to a186c9f Compare January 21, 2026 18:21
@giovaniortolani giovaniortolani force-pushed the race-conditions-issue-data-model-replication branch from a186c9f to 271c9eb Compare January 21, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants