How do I use DTM for a Single Page App?

The question of how to use DTM on Single Page Apps (SPAs) is a VERY hot item right now. By Single Page App, I’m referring to a full user flow contained on a single web page, so as to provide the user a more seamless experience. Often, these pages act like typical web pages, but they don’t always change URLs or load new resources. Many common web development technologies, such as Angular.js, Ember.js, and AJAX use SPA principles.

Unfortunately, there isn’t a single great answer for how to deploy DTM- it depends on many things. I’ll work through some of the options and the limitations to be aware of.

Suppressing Page View Beacons

Whatever method you take for tracking page views in a SPA, keep in mind most SPAs do have one true “page view” when the DOM first loads. If you are going strictly with the DCR or EBR route, you may need to suppress the initial page view beacon the Analytics tool will want to set by default. Otherwise, in the example below where the developers are firing a direct call rule on all page views, you’d get TWO beacons on the first page and 1 on all subsequent pages.


Data Layer Considerations

You’ll need to make sure that whatever the sources of your Data Elements are (CSS selector, javascript objects, cookies…) have the correct values BEFORE your rule is triggered. I have an example site showing one way you might do this for a data layer (though you’ll need to look in the source code), but ultimately it’s going to depend on your site.

Variable Persistence

One last consideration is that once your Analytics object exists (as in, the “s” in “s.pageName”), variables set on it will continue to exist unless specifically overwritten. In most cases, you’d overwrite s.pageName with a new value so it isn’t a problem, but something like s.eVar5 may be set on the first beacon in your SPA, and not desired on subsequent beacons. You can use s.clearVars() to “refresh” your “s” object, but you have to make sure it fires at the right time- for example, after the beacon on Page A, but before DTM starts mapping Data Elements to variables for the beacon on Page B. How you do this will depend on the overall deployment method you choose.

Deployment Methods

1) Direct Call Rules

Perhaps the most straight-forward approach is to have developers fire a Direct Call Rule, like _satellite.track(“page view”) on every thing YOU consider a page view, whether it’s a fresh new DOM or not.

Advantages: Disadvantages: 
  • You have ultimate control over when a page view in considered a page view.
  • If you need to clear out variables between beacons (for instance, you set s.eVar5 in the first beacon in the SPA, and don’t want it in the second beacon), Direct Call Rules don’t provide a great place to use something like s.clearsVars(). There are some potential work-arounds, but none are ideal.
  • Developers need to add more DTM-specific code (satellite.track) to your pages.
  • Direct Call Rules don’t allow for extra conditions (like “fire THIS logic on pageA, and THAT logic on pageB”) in the interface.
  • Direct Call Rules don’t “stack”- if multiple rules have conditions that are met, multiple rules will fire.

2) pushState or hashChange

Many SPA frameworks, like Angular, use a certain flag to let the browser know the user is viewing a new “page”.


DTM can listen for this flag in an Event Based Rule using a pushState or hashChange condition.

  • No additional code is needed- most SPA frameworks are already firing something DTM can listen to
  • It’s an Event Based Rule, which allows you to fire clearVars(), and set extra conditions
  • Because you are listening for an event set by the framework, you have less control over timing. Updating a data layer BEFORE the “pushState” event is detected would be critical.
  • Event Based Rules don’t “stack”- if multiple rules have conditions that are met, multiple rules will fire.

3) Custom Event EBR

Another option, which feels a bit like a blend of the first two options, is to use a Custom Event-based Event Based Rule (and no, that’s not a typo- it’s an EBR based on the JavaScript Concept of a Custom Event). It’s possible Developers are already using this Custom Event concept for their own code and purposes, and DTM can just listen for it… or you can have developers set one specific to our DTM needs by using something like my digitalData.userAction hack.

  • You have a little more control over timing
  • It’s an Event Based Rule, which allows you to fire clearVars(), and set extra conditions
  • May require more developer work- similar level of effort as DCRs
  • Event Based Rules don’t “stack”- if multiple rules have conditions that are met, multiple rules will fire.

 4) (NEW OPTION!) “dataelementchanged” Event Based Rule

Just in the last few weeks, a new option emerged as part of the AEM Context Hub integration. This allows DTM to listen for changes to certain data elements- for instance, you could have a rule fire whenever the “pageName” has changed. My friends at 33 sticks have a great blog post about this already for more info.

  • You have a little more control over timing
  • It’s an Event Based Rule, which allows you to fire clearVars(), and set extra conditions
  • Requires careful consideration of when the data layer changes/loads
  • Event Based Rules don’t “stack”- if multiple rules have conditions that are met, multiple rules will fire.

18 thoughts on “How do I use DTM for a Single Page App?”

  1. For the issue of not being able to do Conditional logic in a DCR, what I’ve been doing to get around that is to have an intermediary DCR. I put the logic in the “base” DCR, which will fire other DCR’s based upon the result of the logic. Not ideal, but a workable solution.

    • Do you mean that you have a named event like “pageView” and then add your conditional logic inside of a script block, calling _satellite.track() when you want to run the specific rules? Feels like that defeats the purpose of having the DTM firing rule UI in the first place.

      • There are some smoother ways of doing it, but essentially, yes- one of the options is to do as you describe. In the end, you can either do something like _satellite.track(“page view”) on every page, then within the code of that “page view” Direct Call Rule in DTM, add some conditions based on the current page (for instance, add something extra on search results pages)… or have conditions on your page that decide whether to fire _satellite.track(“basic page view”) vs _satellite.track(“search results”), and have a cleaner DTM interface but more rules and code to manage on the front end. Neither option is awesome, to be honest, but I do see folks doing the former much more than the latter. I believe some of the options may improve when the next major iteration of DTM comes out (probably next Spring).

  2. Hi Jennifer,

    thanks for a great post. Really helpful. I had a question whether the clearVars function would also clear the evars/props that we want to have for the session or user level in Adobe Analytics. For example, we collect user id at one stage, send it to Adobe Analytics and then on the next page view we clearVars function to remove it. Would that mean that we cannot see user id in adobe analytics and use user id to see how many pages the user visited etc.? Thanks, Jonas

    • No, thankfully, clearVars doesn’t affect how the data is stored/persisted on the back end. If you are sending the data into an eVar and that eVar is set to not expire until, say, the end of the visit, then you could set it on page A, set clearVars, and Adobe will hold on to that value on page B. For eVars, clearVars generally doesn’t have much impact, because they’re set to persist anyways. It would just lower the “instances” metric because the eVar doesn’t get set over and over. But for props and events, clearVars will make it so your prop pageviews or your event metrics don’t get set again unless you explicitly re-set them.

  3. Hi,

    Unfortunately i don’t see clearVars being used in DTM as it is very hard to use it especially when you make use of direct calls and location change (event based rule).
    Has anyone been successful implementing that function? I had to work on my own function to clear all variables based on the rule.
    I would instead expect a more friendlier option from DTM, i.e. option in the UI to confirm that you would like to clear the vars.

    • I believe the UI will eventually have this option; in the meantime, if you’re in an EBR, you can add this as a custom condition for the rule:
      return true
      This will run before the analytics tool portion of the rule, and guarantee you have a fresh slate for your variables. This is one reason I prefer EBRs over Direct Call Rules.

  4. Does anyone ever tested mutationObserver ? It does not depend on the framework and could be used to detect change in the page you are interested in.

    • Hm, this is something I hadn’t heard of before, thanks for the tip. So this would be in place of using “dataElementChanged” (which relies on a data layer or CSS selector value, for instance), “element is present” (which doesn’t work that great, IMO), or pushstate/hash change?

  5. Hi Jenn,

    For a React based site we are using Direct call to trigger both event based & page level tracking(set to s.t() in DTM). Now we notice the variables set in the Direct call to track events are flowing into subsequent page load calls. Any suggestions to remove those additional variables?


    • Ah, the classic pesky clearVars problem. This is an issue in all SPA implementations- beacons are good about only picking up the variables you want (using s.linkTrackVars and s.linkTrackEvents) but s.t beacons will always pick up all the variables on the page. If you need to get rid of all the variables that were previously set, you can use the s.clearVars function. The problem when you are implementing your SPA through DTM is there isn’t a good place to run that s.clearVars function- you need it running after the previous beacon fires, but before you start setting variables for the current beacon.
      As I hint at in my post, this is one reason I don’t like using Direct Call Rules- there is simply no place to put that function that won’t mess with your current beacon but will consistently run at the right time. There are some workarounds- has some examples but I’ll admit I hate using those workarounds, they can cause performance issues. If you can switch to using Event Based Rules instead, then you can just run s.clearVars in the custom condition code block, which will always run before the analytics tool part of your rule.
      In theory, Launch will have clearVars as an option built in to the interface (or something similar) to solve this problem. In the meantime, you may also be able to try the s.registerPostTrackCallback function that should run after your s.t/ beacons. I’ll admit I haven’t done this myself, but @josh in #measure slack had posted this, which you could put in your s_code next to your doplugins:


      // do plugins
      s.doPlugins=function(s) {
      // stuff

  6. Hi Jenn

    I have a problem , wherein I set an EBR to track click count on a link with Custom code like s.eVar6=”mylink”. Problem is this eVar6 is also gets passed in subsequent rules where this eVar6 is not even specified.
    I am just wondering if s.clearVars() would do the trick for me, but just wondering where to place it.

    • Yes, clearVars would do the trick. You can fire it before the Analytics section of the EBR (so it doesn’t “clear” any of the variables you are TRYING to set) by putting it in the Custom Conditions code for that rule:
      You will need your “s” object to be globally scoped for this to work.
      Alternatively, you could use the registerPostTrackCallback function in your s_code to force variables to clear out AFTER every beacon-

  7. I am running into an issue with the s.clearVars() function. Once a data element has been defined it is retaining that value whenever we call the variable on another page.
    For example, we have a data element called ‘adsServed’ mapped to prop20, which gives the name of any third party ads we are showing on a given page. These ads CAN show up on any page, but don’t have to (example: we could have an ad show on the search results page, but not on the property page). We want to set prop20 if it exists so we define it globally.
    So if we show an ad on the search results page, we set a value ‘SRP_ad’ which works fine. Then we move to the property page, where we ask if there are any ads. Our data object doesn’t return a value (expected), but prop20 is still set as ‘SRP_ad’ on the property page. _satellite.getVar(‘adsServed’) still shows a value of SRP_ad even though the clearVars function has run as part of our direct call rule.
    Is there any way to clear out the values of _satellite.getVar so that it always pulls in the current value of the analytics object we have created?

    • Alas, this is a really annoying problem (and one I’ve experienced too).
      Data Elements with a “page view” persistence do, unfortunately, hang around longer than we want them for Single Page Apps. It doesn’t have to do with clearVars or even Analytics, it’s strictly a DTM/Data Element problem: once a data element is set on a given DOM, it can only be overwritten by a new value for that data element- for instance, if my DE “search term” pulls the value “red wug” from digitalData.searchTerm for one page, then on the following page digitalData.searchTerm doesn’t even exist for my DE to check, it will continue using the “red wug” value it got from the first page- it doesn’t think it should override a value (“red wug”) with nothing.
      I’m impressed you figured out that it was the data element- usually people get stuck at the fact that you set clearVars (which clears out “s” objects) but in this case, it’s the data element that’s persisting, not the “s” variable
      This issue actually caused a recent conversation with the Launch product team on #measure slack, which resulted in Data Elements in Launch having a a new persistence=”none” option (in addition to pageview, session and visitor.)
      Unfortunately, if you’re in DTM, your options are still limited. I haven’t found a great solution, short of moving into custom code (where you can check if the source of the data element still exists and has a value), or making sure the data layer (or whatever your source for that data element) still exists on subsequent pages, just with no value- for example, if page 2 sets digitalData.searchTerm=”” rather than the searchTerm object simply not existing.
      Or move to Launch.


Leave a Comment