Adobe’s Opt-In Service for Consent Management

This is cross-posted from the 33 Sticks blog.

Adobe’s Opt-In Service is a tool provided by Adobe to help decide which Adobe tools should or should not fire, based on the user’s consent preferences. It has some advantages I don’t see folks talk about much:

  1. It manages your Adobe tags (Analytics, ECID, Target) at a global level. If you’ve got it set up right, you don’t need to make any changes to your logic that sets variables and fires beacons (you don’t need a ton of conditions in your TMS)- any rule’s Analytics or Target actions will still run, but not set cookies or send info to Adobe.
  2. When your existing variable-and-beacon-setting logic runs while the user is opted out, Adobe queues that logic up, holding on to it just in case the user does opt in. Any Adobe logic can run, but no cookies are created and no beacons get fired until Adobe’s opt-in service says it’s ok.

This latter point is very important, because in an opt-in situation, frequently consent happens after the initial page landing – the page that has campaign and referrer information. If you miss tracking on that page altogether, you will lose that critical traffic source information. Some folks just re-trigger the page load rule, which (depending on how you do it) can mess up (or at bare minimum, add complexity) to your data layer or rule architecture. So I’m a big fan of this “queue” of unconsented data.

You can even experiment and see for yourself- this site has a globally-scoped s object, and an already-instantiated Visitor object. Open the developer console, keep an eye on your network tab, and put this in, to opt out of analytics tracking on my site:

adobe.optIn.deny('aa');//opt out analytics

Then run code that would usually fire a beacon:

s.pageName="messing up Jenn's Analytics data?" //you can tell me a joke here, if you want
s.t()

No beacon!

Now let’s pretend you’ve clicked a Cookie Banner and granted consent. Fire this in the console:

adobe.optIn.approve('aa');//opt in analytics

And watch the beacon from earlier appear! Isn’t that magical?

Side note: The variables sent in the beacon will reflect the state when time the beacon would have fired. I tested this- if I do this sequence:

adobe.optIn.denyAll()
s.prop15="firstValue"
s.t() //no beacon fires because consent was denied
s.prop15="secondValue"
adobe.optIn.approveAll() //now my beacon fires

…the beacon that fires will have prop15 set to “firstValue”- it reflects things at the time the beacon would have fired.

To re-iterate: if you have the ECID service set up correctly, it will still look like your Analytics/Target rules fire, but the cookies and beacons won’t happen until consent is granted.

How to Use within Adobe Launch*

Most of Adobe’s documentation assumes you’re using Adobe Launch*, and even then, leaves out some key bits. The Experience Cloud ID extension has what you need to get things set up, but is not the complete solution (you can skip this and go straight to the Update Preferences section below if you want the part that’s not particularly well documented). Let’s walk through it, end-to-end:

Opt-In Required

First, tell Launch when to require Opt-in.

If it’s just “yes” or “no”, then it’s simple enough. If you want to decide dynamically (based on region, for example), then you need to create a separate Data Element that returns true or false. For example:

if(_satellite.getVar("region")=="Georgia"){
     return false //Georgia doesn't care about consent. Yet.
}else{
     return true
}

To be honest, to me it’s always made more sense to just click the “yes” option, then further down, change up people’s default pre opt-in settings based on region.

Store Preferences

I’ll admit, I’ve never used these, but I think they’re self-explanatory enough: the ECID is offering to store the user’s consent settings for you. This seems convenient, but I’ve never seen a set up where the overall CMP solution doesn’t already store the consent preferences somewhere, usually in a way that is not Adobe-specific.

Previous Permissions/Pre Opt In Approvals:

The “Previous Permissions” and “Pre-Opt-In Approval” settings work very similarly, but “Pre-Opt-In Approval” is more of a global setting, and “Previous Permissions” is user-specific (and overwrites “Pre-Opt In Approval”). In other words: Pre-Opt-In Approval settings are used when we don’t have Previous Permissions for the user.

The key here is the format of the object, usually in a Custom Code Data Element, that Adobe is looking for:

{
   aam:true,
   aa:true,
   ecid:true,
   target:true
}

…where true/false for each category would be figured out dynamically based on the user. So your data element might have something like this:

var consent={}
if(_satellite.cookie.get("OptInInfoFromMyCMS").indexOf("0002")!=-1){
    consent.aa=true
    consent.target=true
    consent.ecid=true
}else{
    consent.aa=false
    consent.target=false
    consent.ecid=false
}
return consent

Important but often missed: updating preferences

All of the above is great for when the ECID extension first loads. But what about when there is a change in the user’s preferences? As far as I know, this can’t be done within the ECID extension interface, and it’s not particularly well documented… this is where you have to use some custom code which is kind of buried in the non-Launch optIn service documentation or directly in the Opt-In reference documentation. (I think we’re up to 4 different Adobe docs at this point?)

When your user’s preferences change, perhaps because they interacted with your banner, there are various objects on adobe.optIn that you can use:

adobe.optIn.approve(categories, shouldWaitForComplete)
adobe.optIn.deny(categories, shouldWaitForComplete)
adobe.optIn.approveAll():
adobe.optIn.denyAll():
adobe.optIn.complete(); //have adobe register your changes if you set shouldWaitForComplete to True

The “shouldWaitForComplete” bit is a true or false- if true, then Adobe won’t register your changes until you fire adobe.optIn.complete(). If false, or omitted, then Adobe will immediately recognize those changes.

So I might have a rule in Launch that fires when the user clicks “approve all” on my banner (or if I’m using oneTrust, within their OptanonWrapper function), with a custom code action that looks something like this:

adobe.optIn.approve('target',true);//opt in target 
adobe.optIn.approve('aa',true);//opt in analytics
adobe.optIn.approve('ecid',true);//opt in the visitor ID service
adobe.optIn.complete(); 

Which could also be accomplished like this:

adobe.optIn.approve(['target','aa','ecid']);

Or even just this (in my tests, this did not require adobe.optIn.complete()):

adobe.optIn.approveAll();

What if I’m Not Using Launch?

If you’re not using the Launch ECID extension, then the initial stuff- the stuff that WOULD be in the extension, as detailed above, is all handled when you instantiate the Visitor object. This is all covered Adobe’s Opt-In Service documentation, which I personally found a bit confusing, so I’ll go through it a bit here.

As far as I can tell, this bit from their documentation:

adobe.OptInCategories = {
    AAM: "aam",
    TARGET: "target",
    ANALYTICS: "aa",
    ECID: "ecid",

};

// FORMAT: Object<adobe.OptInCategories enum: boolean>
var preOptInApprovalsConfig = {};
preOptInApprovalsConfig[adobe.OptInCategories.ANALYTICS] = true;

// FORMAT: Object<adobe.OptInCategories enum: boolean>
// If you are storing the OptIn permissions on your side (in a cookie you manage or in a CMP),
// you have to provide those permissions through the previousPermissions config.
// previousPermissions will overwrite preOptInApprovals.
var previousPermissionsConfig = {};
previousPermissionsConfig[adobe.OptInCategories.AAM] = true;
previousPermissionsConfig[adobe.OptInCategories.ANALYTICS] = false;

Visitor.getInstance("YOUR_ORG_ID", {
    "doesOptInApply": true, // NOTE: This can be a function that evaluates to true or false.
    "preOptInApprovals": preOptInApprovalsConfig,
    "previousPermissions": previousPermissionsConfig,
    "isOptInStorageEnabled": true
});

Could be simplified down to this, if we skip the extra OptInCategory mapping and the preOptInApprovalsConfig and previousPermissionsConfig objects:

Visitor.getInstance("YOUR_ORG_ID", {
    "doesOptInApply": true, 
    "preOptInApprovals":{
         "aa":true,
     },
    "previousPermissions": {
         "aam":true,
         "aa":false,
     },
    "isOptInStorageEnabled": true
});

Literally, that’s it. That’s what that whole chunk of the code in the documentation is trying to get you to do.

Beyond that, updating preferences happens just like it would within Launch, as detailed above.

Side note- what’s up with adobe.OptInCategories?

I’m not sure why Adobe has that adobe.OptInCategories object in their docs. These two lines of code have the same effect, but one seems much more straightforward to me:

previousPermissionsConfig[adobe.OptInCategories.AAM] = true;
previousPermissionsConfig["aam"] = true;

The adobe.OptInCategories mapping is set by Adobe- despite it being in their code example, you don’t need to define it, it comes like this:

I’m guessing this was to make it so folks didn’t have to know that analytics is abbreviated as “aa”, but… if you find the extra layer doesn’t add much, you can bypass it entirely if you want.

Conclusion

Once I understood it, I came to really like how Adobe handles consent.

I am by no means considering myself an expert, or trying to make a comprehensive guide. But the documentation is lacking or spread out, so most of this took some trial and error to really understand, and I’m hoping my findings will prevent others from having to do that same trial and error. That said, I’d love to hear if folks have experienced something different, or have any best practices or gotchas they feel I left out.

*still not calling it “Adobe Experience Platform Data Collection Tags”

The Adobe Launch “Rule Sandwich”

I’ve mentioned before how much I love that Launch* can “stack” all sorts of rules- by which I mean multiple rules of varying scopes can combine together into a single analytics beacon.

This means (using an example from that old post) I can have a global rule that sets my universal variables, a rule for search results, a rule for filters, a rule for null search results, and a rule that fires the beacon, all resulting in a single beacon where all the variables might be coming from a different rule. I call this a “Rule Sandwich”:

That particular example may be a bit overkill, but this ability to divide up your variables by scope can be key to a scaleable implementation.

As a programmer, there are some coding best practices we should all try to follow:

  • Don’t Repeat Yourself (DRY): If you can find a single place to set a variable, do it. Don’t set site section in every page that has a site section- set it in a global place that can dynamically set the right value from the data layer.
  • Keep It Simple, Stupid (KISS): Daisy-chaining Direct Call Rules can quickly complicate an implementation and introduce multiple points of failure. Huge Switch statements in code can make it hard to find specific dimensions or events.
  • Principal of Least Astonishment: People who encounter your setup shouldn’t have any surprises. If the next person who signs in to your Launch property says “oh, wow” or needs a “eureka” moment, you may have over-engineered things.

Following those principals, here’s the “Sandwich” set up I most like to use:

The bottom slice: Global Variables

My first rule sets my universal variables- things like site section, campaign, login state, user ID, etc. Stuff where I can say “if it exists in the data layer, I want it in my beacon”. It is set up to fire on all triggers that might end up in an analytics beacon. If you’re using the Adobe Client Data Layer extension and all your rules are triggered by your ACDL data layer, then this can be pretty simple, thanks for “Listen to All Events”:

But I’ve also had setups like this:

I add “#1” to the trigger name and rule name to indicate that these are triggered with a rule order of 1:

Which means for any given trigger**, this will be the first rule to fire. In this rule, I set any variables that can be applied globally- usually things like page name, site section, page type, language, etc.

**A note about sequencing: the order for rules only applies when rules share a common trigger. If you have a rule that fires on DOM Ready, and a rule that fires on a “page view” event in your data layer when DOM Ready occurs, even if they happen at the exact same time, Launch won’t compare their order to see which comes first. The rules would either both have to be triggered on DOM Ready, or both triggered on the “page view” event.

The Analytics extension does a great job of keeping everything in sequence. In other words, if I have a rule with an order of 1 that sets analytics variables I can be confident that a rule with the same trigger with an order of 50 will apply its analytics variables only after the first rule has finished. (However, Custom Code blocks will not wait for earlier-ordered custom code blocks from other rules to finish. If I have a rule with an order of 1 that runs some script in a custom code action, I can’t know for sure that script will have finished before code in a separate rule with an order of 50 happens.)

You might ask “Why don’t you use the Adobe Analytics Extension’s global variables?

The problem with these is they only evaluate once, when the extension first kicks in on page load (go upvote my idea if you, too, want it to behave differently). So if you clear variables, they disappear and won’t come back until the next page load. And if you try to pass new values- maybe the language preference changed, or maybe you have a SPA and need to pass a new page name- the extension won’t pick those up.

Many folks use the doPlugins function for this purpose, because it fires on every beacon. There’s a few reasons I avoid this when I can:

  1. I try to keep my variables out of code as much as possible- the more transparency in the interface, the better.
  2. I try to keep doPlugins light. With default settings, it runs on every click, whether that click results in a beacon or not.
  3. doPlugins is the LAST thing to fire before a beacon is sent to Adobe, meaning any customization I’ve done in other rules might be overwritten.

Which brings me back to my #1 rule that fires on all of my common triggers. I set my global variables, and that’s it- no sending a beacon (yet).

The middle of the sandwich- the “ingredients”

Next come all my sandwich ingredients: the rules that set variables based on more specific scenarios. Any logic that needs to only fire under certain conditions go here. It’s where I set events, set any user-action-specific dimension, and hard code any values.

These rules might be triggered by a page view under certain conditions (like “pageType of article”), or based on a specific event or element interaction.

So I might have a rule that fires on page view if the page type is equal to “article”:

(In theory, I could set something like content.title in my global variables rule, because it will only set if the data layer currently has a value, but I’d rather keep all my blog stuff together and not make my global rule evaluate data elements unless they’re needed.)

I may also use these rules to customize/fix anything that had been set in my global variables. In an ideal world, data layers would be perfect and we’d never have to “fix” anything in Launch. But this isn’t that ideal world. My global variables might set the page type as “search results”, then later I realize our other search results page has a page type of “search”. Obviously, I should go to the devs and ask them to make it consistent. But in the meantime, I can use an “ingredient” rule to overwrite what was set in my global variables rule.

Not everything merits its own rule. If the scope only calls for a single extra event to be set, I could probably just do that in my global rule with a conditional JavaScript statement. But something like Purchase Confirmation, which has its own product string, purchaseID, etc… that makes sense as an “ingredients” rule. I try to find a balance between not having too many rules, and not having any single rule be super complicated.

The top slice: Send Beacon and Clear Vars

For any situation where many rules might all contribute to the same beacon- for example, page views- you can have the top piece of the sandwich as a rule with an order of 100, which fires the beacon and clears the variables.

If you have global page view variables- stuff you want on all page views but not necessarily all beacons- you could also put them here.

Note, splitting the “set variables” (our ingredients) and the “send beacon” (our top slice) like this makes the most sense in a situation where you’ll have many potential rules all needing a beacon. Page Views is the best use case. Having a single separate “Send Beacon” rule makes it so if you have a few dozen page-specific Page Load rules, you don’t have to add the beacon and the clear vars action to all of them. It’s a time-saver, and a way to guarantee whatever the combination of ingredient rules, a single beacon will fire.

But in cases where there’s just one rule contributing to a beacon- something like a certain button click, for instance- then there isn’t much advantage to having a single “set variables” rule and a separate single “send beacon/clear vars” rule. You can combine your “ingredients” and your “top slice” into a single rule, like this:

Clear Variables makes sure that variables (and particularly events) from earlier beacons won’t accidentally get attached to something they don’t belong to. Different folks handle “Clear Variables” differently. Some people set it at the beginning of rules (or sequences of rules). I find it easiest to always set it any time I fire a beacon, so I don’t have to worry about where in the sequence of things I’m clearing my variables.

Put It All Together (Examples)

Following this approach, my rules list might look like this:

(I tend to not put “#50” in the name of rules- it’s the defaultiest option; it can be assumed if no other order is specified. I also don’t specify “clear vars” in rule names anywhere- to me, it’s implied with any beacon being sent. See my post about rule naming conventions.)

On my search results page (which fires on a “page view” ACDL event where page type=”search results”), these rules would fire, in this order:

  • All Events | ACDL Any Event | Analytics: Set global vars #1
  • Search Result Views | ACDL Page View | Analytics: Set vars (#50)
  • All Page Views | ACDL Page View | Analytics: Send s.t, clear vars #100

On a Search Filter click, the following would fire:

  • All Events | ACDL Any Event | Analytics: Set global vars #1
  • Search Filters | ACDL Search Filter | Analytics: Set vars, send s.tl beacon (#50)

Conclusion

There are, of course, many “correct” ways to architect a TMS; this is just the one that has worked best for me across many organizations, particularly if they have a reliable event-driven data layer. I will say, it can get complicated if you’re using a wide variety of rule triggers, like page bottom, clicks on this but NOT clicks on that, form submission, element enters viewport, etc… the” “Top Slice”/Global Vars Rule’s list of triggers can get very unwieldy. And sadly, if you have a very Direct-Call-Rule-based implementation, since there is no out-of-the-box “fire on all direct calls”, you may find resorting to doPlugins is the simplest, most scaleable route. Your mileage may vary.

I’d love to hear what has worked for others!

No AI was used in this post. Images are good old-fashioned paid stock images.

*I’m still not going to call it “Adobe Experience Platform Data Collection Tags”.

Silly Launch Builder Chime

I spend way too much time on a daily basis waiting for Launch libraries to build- on particularly bloated libraries, I’ve had it take up to two minutes. And, with my ADHD, I get too easily distracted during that wait time. It’s a major productivity killer.

So I figured out I can throw this code in as a Live Expression in my Chrome Console:

if (document.location.href.indexOf("data-collection/tags/") != -1 || document.location.href.indexOf("reactor-lens/tags/")!= -1) {window.launchChimer = window.launchChimer || false;if (document.querySelectorAll(".library .spectrum-CircleLoader.spectrum-CircleLoader--small").length > 0 && !window.launchChimer){window.launchChimer = true} else if (document.querySelectorAll(".library .spectrum-CircleLoader.spectrum-CircleLoader--small").length == 0 && window.launchChimer == true) {var snd = new Audio("http://soundbible.com/grab.php?id=1599&type=wav");snd.play();window.launchChimer = false}}

Like this:

And it chimes when the loading icon stops spinning.

I got the sound file from soundbible– you could replace it with whatever you want. Originally I had it pull from a sound that already exists on any mac (as found on stack overflow) but it made the script ugly and long.

If it isn’t working for you, double check which frame your Live Expression is trying to be on. For me, it automatically went to the “Main Content” frame, which is where it works best, but it sounds like for some folks, you may need to manually select it:

There are better ways to do this. I know this. If I had more hours in the day, I could even make a decent Chrome extension out of it. But this works for now, and I’ve already held on to this for months without blogging it, waiting for time to make it cooler, and it would appear that time is never going to come, so… here’s my imperfect solution.

Consider this a beta. I fully expect something about it will not work in some situation. Please let me know how it goes for you. Or if anyone wants to take it and improve upon it, I’d appreciate it!

How I got OneTrust to Work with Adobe Launch

Disclaimer: I’m not a lawyer. Please don’t take any of this post as legal advice. I claim no responsibility for how the information in this post is used. Each organization needs to work with their lawyers to ensure their setup is compliant. Please test thoroughly and often to make sure your OneTrust set up is working as expected.

You’ll note I’m not calling this “The Best Way to set up OneTrust in Launch”, because I don’t know if this IS the best way; it’s just the only way I could get it all to work in my particular situation. If folks have found other potentially better ways I’d love to exchange knowledge! I will be updating this post if and/or when I learn new things.

To be honest, this is one of the scarier posts I’ve written (and not just for legal reasons)- I fully expect someone to say “this is a really weird way of doing it” (heaven knows I’ve said to myself “there must be a better way”). And I do know there are probably a few places where I could simplify/optimize. But I’ve talked to a lot of different people trying to set up OneTrust, and no one has ever been able to give me a different/better way yet, and I’m getting asked weekly how I’ve managed to get it to work, so here goes!

While this post is very specific to Adobe Launch*, a lot of the script and logic could apply to any TMS/Adobe Analytics setup. Some of it could even apply to other Consent Management Platforms (CMPs).

This post is long, and while the topic is complicated, much of what I’ve included is just for reference, so please don’t feel overwhelmed. We’ve got this.

Also, don’t be evil. I primarily work with first-party analytics (ie, Adobe Analytics), and I have no ethical compunctions about doing so. The only impact we have on the user is we improve website experiences (and I don’t mean that in a “we show better ads” way… I mean our focus is the actual site user experience). That said, I don’t want to “trick” the user into allowing it, and I want them to be informed about it. But consent management (and tag management) also applies to stuff that can be very ethically (not to mention legally) questionable. Prioritize your user’s privacy. Keep them informed. Remember most regulations don’t care specifically about cookies, they care about what you are doing with user’s data (which often includes but is not limited to cookies). There is no “getting around” requests for privacy. (That goes for server-side tracking, too, by the way.)

My Goals With This Setup (Opt-In and Opt-Out)

I’ve worked with OneTrust*** and Launch a fair amount, but for one project in particular, I had nearly complete ownership of it- this is the project I’m basing this post off.

Their set up had to work for a Launch property that has users throughout the US, as well as in a few EU countries, meaning we need to account for situations where we can track only if the user has opted in (GDPR), AND situations where we can track until the user has opted out (most non-GDPR regulations, like California’s CCPA**).

A diagram showing groups of people before and after being presented with two types of prompts: one which opts the user in to tracking (GDPR-style), and another that allows them to opt out. Colors indicate which groups get to be tracked.

We’ve set up two OneTrust templates and two Geolocation rules. Fortunately, OneTrust uses geolocation to tell which banner they need to show the user, and whether they should be considered opted-in or opted-out by default. Unfortunately, on the very first page view, if OneTrust hasn’t had a full chance to run its script and define those categories, you can have timing issues, so we’ll have to work around that in this solution.

Some organizations choose to just treat EVERYONE as opt-in-only, including in the US where that is not legally necessary. This definitely is the legally safest choice but given that most orgs get opt-in rates of 30-50%, you may end up losing 50-70% of your data. I’ve seen one site in the UK that only had 11% of users opt in, though if you optimize your banner and really think through how you present information to your users, you should be able to get it much much higher. Either way, our goal here was to opt-in as many people as we legally/ethically could.

OneTrust Setup

I’m not going to get too into too much detail here about the actual configuration in OneTrust; OneTrust’s documentation is pretty thorough (though guilty of the atrocity of requiring you to login to access it***) and this post is more than long enough already. This whole post assumes you have an entirely correct and compliant set up in OneTrust including that you’ve correctly created your “Categorizations”, you’ve created banners (though I do have tips for working with their CSS- ping me on twitter or slack if it’s giving you trouble), and you’ve made sure your Geolocation rules are set up correctly (make sure you check the “Do Not Track” and “Global Privacy Control” options!)

A screenshot from OneTrust's geolocation rules showing where you can check boxes to honor Do Not Track or Global Privacy Control.

Don’t use Autoblock if you can help it

As you publish a script in OneTrust, it gives you an option to toggle on Automatic Blocking of Cookies, also known as Autoblock:

A screenshot of the toggle in OneTrust's publishing flow which allows you to enable Automatic Blocking of Cookies

Autoblock basically allows itself to become a middle man between your user and anything your site is trying to load. Everything gets routed through it and it gets to choose which requests to block or allow, based on what the user has consented to. This may be tempting… in theory, you could leave all of your tags and cookies in OneTrust’s hands and not have to worry about setting up conditions in your TMS. (I’ve heard a few people mention how much this feels like when TMSes were touted as “one simple line of code that does all the work for you!”)

But please consider it only as a last resort! Yes, turning off Autoblock and instead using conditions to decide which tags to fire looks like a lot more work, but it’s more flexible, and gives you more control and visibility. (And, to be honest, getting Autoblock to work the way you want may end up being more work than you expect, anyways.) I had a whole section of this post devoted to Autoblock, but it was enough it merited a post of its own. So please, before deciding to use Autoblock, get enough information to make an informed decision.

Why not use OneTrust to block Launch altogether?

This is another kind of lazy (or overzealous) solution I’ve seen folks try: just don’t load your TMS library at all until the user has consented to some tracking. Certainly, it is a fairly safe approach (assuming you don’t have anything on your site, outside of your TMS, that needs consent). It really limits your flexibility, though. Especially if you have different categories for consent, you may want to fire analytics (performance category) but not Doubleclick (marketing/targeting category). Or you may have script in your TMS that doesn’t require consent… for instance, I know of one site that uses Launch to deploy some ADA compliance updates (hardly a best practice, but it happens). Or your TMS may have code in it that helps keep you compliant by managing cookies, etc.

In an opt-out scenario, such an approach seems a bit silly- by the time they opt out, your TMS has already loaded and may continue tracking link clicks, etc.

In an opt-in/GDPR scenario, there is a key reason I don’t like this approach: the ECID Opt-In Service, when set up properly, is very good at holding on to tracking until there is consent.

So if my page loads, and we don’t have consent, but my Page Load rule normally would have set variables and fire a beacon… those actions get queued up, so to speak, for once the user DOES consent. Once they click the “allow all” button, my Page Load rule doesn’t re-run, but the things it would have done initially, like sending page view data to Adobe, finally get to fire. That way, you don’t have to either fire a new rule after acceptance, or wait until the next page view, to get information about that first critical page view (which often includes things like Campaign tracking codes or session referrer information).

Why not use the OneTrust Launch Extension?

I hate to say it, but the OneTrust extension doesn’t offer many advantages***. There is still enough you have to set up and tweak outside of it that in the end, you may as well just skip using it. Not using it keeps everything transparent, too, which I like.

Global “True” Page Top Rule

The setup we used needed a rule that runs before anything else possibly even starts. This is particularly important if you are firing your OneTrust library from within Launch, but you may want it either way, to set up compliance logic before anything else fires.

We didn’t even use “Page Top” as the trigger; rather we used a little trick that ensures it really is the first thing to run: we create an Custom Code Event that just contains one line of code:

trigger();

And we set the order on that to -1 (or 0 or 1… just make sure it’s a lower number than anything else):

A screenshot from Adobe Launch showing Event Configuration based on the custom code of "trigger();"

That may all seem overkill (and it may very well be) but it gives us some peace of mind, without negative side effects.

Inserting the OneTrust library (from within Launch, if you must)

Ideally you would have your devs insert the OneTrust library directly on the page, as high on the page as possible. This will give you the best chance to not have timing issues. To be clear: this is my recommendation. If you’re going that route, skip to the “Set up consent data elements” section.

But we live in the real world, where sometimes we have to work with what’s available. In this particular client case, a hard-coded reference to OneTrust wasn’t a practical option at the time, so we needed to add our OneTrust libraries into the Page Top rule we just created.

We put a single Custom Code Action in this rule, which calls on our OneTrust library. You can get these code snippets from within OneTrust- the important part is the data-domain-script. Note, you can get both a test script and a production script snippet from OneTrust, similar to how Launch has a dev environment to test your changes before you go to prod. Just make sure you’re using the right one for the stage you’re at.

A screenshot from a Custom code action in Adobe Launch, showing HTML references to the oneTrust testing and production libraries.

We’ll also use this rule and code block to fire our Adobe/ECID consent, but we’ll come back to that later.

Set up consent data elements

I tend to have one master “Onetrust Consent Groups” data element, then data elements for each category:

A screenshot from Launch showing 4 data elements: "onetrust consent groups", "onetrust consent: marketing", "onetrust consent: performance" and "onetrust consent: targeting"

For the master consent groups data element, we have a few things we need to consider. OneTrust provides two places to see what groups your user is in:

1. The most supported-by-OneTrust approach is to use a Javascript object, window.OnetrustActiveGroups, which returns a value like this:
‘,C0001,C0003,C0007,C0004,C0002,’
These codes correspond to the Categories you set up in OneTrust. You can view YOUR Categories by going to Categorizations>Categories.

2. An “OptanonConsent” cookie, which I’m pretty sure they don’t particularly want us using, but it can be handy because once it is created, it’s accessible as a page loads, before that window.OnetrustActiveGroups object is. It returns a value like this:

isGpcEnabled=0&datestamp=Tue+Mar+14+2023+12:10:28+GMT-0400+(Eastern+Daylight+Time)&version=202212.1.0&isIABGlobal=false&hosts=&landingPath=NotLandingPage&groups=C0001:1,C0003:1,C0007:1,C0004:1,C0002:1&AwaitingReconsent=false&geolocation=US;GAConsoleNetwork request blockingSearchIssuestop

…which is a bit messy, but potentially useable. I don’t currently use it in my solution but I figured I’d note it because I have seen others use it.

Important: Timing Issues for opt-in land (GDPR)

(To be clear, much of this section is only needed for folks who have timing issues because their OneTrust library is loaded by Launch or isn’t loading before Launch starts trying to do stuff.)

The problem is, if your OneTrust library is not called as high on the page as possible, there is a chance that OnetrustActiveGroups JavaScript object won’t exist when you need it; on each page load, it has to wait for the OneTrust library to create it. And the cookie only exists after the first time the OneTrust library loads… meaning if it’s the user’s first page view, you won’t know whether they count as opted-in or opted-out until after the OneTrust library loads! That’s one reason you should put the OneTrust library as high on your page as possible. But if you can’t, or you just want a failsafe, you can define some default consent groups. This would be simple if you were in a everyone-is-auto-opted-in scenario, or a everyone-is-auto-opted-out scenario. Where it gets fun is when you need to account for both.

Fortunately, my client already has a reliable method to tell me the user’s country (and even continent!) I’ll be honest, I’m not sure what I would have done otherwise, because we have to be able to detect the user’s jurisdiction to decide if they should be auto-opted-in or auto-opted-out. I have some theories of other ways we could do this but I’m glad I already had a reliable way to do it. Here is what I did for my “onetrust consent groups” data element:

try {
  var consentGroups=""
    if(window.OnetrustActiveGroups){
        consentGroups=window.OnetrustActiveGroups //if this exists and is accurate, use it. We often have a timing issue though. So, our back up options:
    }else if( _satellite.cookie.get("OptConsentGroups")){
        consentGroups=_satellite.cookie.get("OptConsentGroups") //if we've previously gotten the consent groups, we don't have to worry about the timing with onetrust, we just grab it from a cookie that we create
    }else if(_satellite.getVar("continentCode")=="eu" || navigator.globalPrivacyControl==1 || navigator.doNotTrack==1){//if no previous permissions, but in the EU or have GPC or DNT enabled, opt OUT of all not-strictly-necessary
        consentGroups=',C0001,'
    }else{ //everyone NOT in the EU who doesn't have GPC or DNT gets auto-opted-in to all categories
        consentGroups=',C0003,C0001,C0007,C0004,C0002,'
    }

    //store groups in a cookie 
    _satellite.cookie.set("OptConsentGroups", consentGroups, {expires: 360, samesite:"lax", secure:true, domain: "example.com"})

    return consentGroups;

} catch (e) {
	_satellite.logger.info("onetrust error:" + e);
}

Basically, if OneTrust has loaded, great, let’s use their JS object. If not, then based on the user’s location or privacy settings, we auto-opt them in or out of all categories (aside from “strictly necessary”). Then we create our OWN cookie, OptConsentGroups, so it’ll be easily accessible early on in the next page load.

NOTE: YOUR CATEGORIES WILL VARY. C0002 is for performance cookies for this particular client, based on how they have their categories set up within OneTrust. You will need to figure out how to identify the categories for your set up.

This data element gets referenced in a few places, meaning it is constantly getting updated, in case preferences change.

Set up data elements for each category

Once that data element exists, I create data elements for each category. My “onetrust consent: marketing” data element looks like this:

var consentGroups=_satellite.getVar("onetrust consent groups")||"" 

if(consentGroups.indexOf("C0007")!=-1){
  return "true"
}else{
  return "false"
}

I’ll use these to create conditions for any of my rules that contain non-Adobe tracking. Adobe rules don’t need conditions, they’re handled by the Experience Cloud Opt-in service.

Set up the ECID Opt-in Service

This is how I configured the Experience Cloud ID Service extension, essentially shifting all the work to my “onetrust consent: adobe” data element:

A screenshot of the ECID extension configuration from within Launch. Enable Opt In is toggled to "yes", and the previous permissions field says %onetrust consent: adobe%"

That “onetrust consent: adobe” data element looks like this:

var consentGroups=_satellite.getVar("onetrust consent groups")
var consent=false //false by default

//set per adobe category (we're treating all three- analytics, ecid and target- as performance. Your situation may merit a different split)
if(consentGroups.indexOf("C0002")!=-1){ //C0002= performance cookies
	var consent=true
}

//_satellite.logger.info("onetrust consent groups:" + consentGroups) //handy for debugging
return { aa: consent, target: consent, ecid: consent };

It checks if the user has consented to “performance” tracking, and if so, returns an object like this:

{aa: true, target: true, ecid: true}

This is the format that that “previous permission”/pre-opt-in approvals piece of the ECID is looking for. I’d love to say I learned that in the Adobe Documentation, but unless I’m missing something obvious, you have to backwards-engineer their code example to figure it out. Fortunately, this blog post by Abhinav Puri documents it all nicely.

(But wait, shouldn’t Adobe Target be categorized as “Targeting”? You should definitely ask your lawyers, but by my reasoning, the Targeting category is for targeted advertising and retargeting. Adobe Target may be named Target, but it is primarily a user experience/performance tool. It doesn’t chase you around the internet with personally-relevant advertising.)

Setting up the extension with this data element will cover Adobe permissions on page load, before the user interacts with the banner.

(In theory, this ECID extension part is the main thing the OneTrust extension would help with. But I like keeping all of the logic very transparent. And I’m a control freak that doesn’t trust vendor code I haven’t thoroughly tested.)

Updating Permissions (Banner Interaction)

To update permissions when the user has changed their preferences (including their initial reaction to the banner), I added code to my Page Top rule, within the OptanonWrapper() function, which is functionality provided by OneTrust that fires when OneTrust library first loads, as well as whenever there is a change to the user’s permissions:

<script type="text/javascript">
function OptanonWrapper() { //this function runs any time there are changes to one trust
  var consentSettings=_satellite.getVar("onetrust consent: adobe")   
  if(consentSettings.ecid===true){ //experience cloud ID service
      adobe.optIn.approve('ecid',true);
  }else{adobe.optIn.deny('ecid',true);}
  
  if(consentSettings.aa===true){ //analytics
      adobe.optIn.approve('aa',true);
  }else{adobe.optIn.deny('aa',true);}
  
  if(consentSettings.target===true){ //target, obv
      adobe.optIn.approve('target',true);
  }else{adobe.optIn.deny('target',true);}
  
  adobe.optIn.complete();
}
</script>

(In theory I could simplify this a bit since all three categories pretty much always have the same consent, but I wanted this to be flexible for potential changes in the future.) So now that rule looks like this:

A screenshot from Adobe Launch showing the above code block placed within the same rule/code that referenced our OneTrust library.

(If you’re not deploying your OneTrust Library through Launch, then lines 1-7 don’t apply.)

Set up Rule Conditions

Fortunately, the ECID Opt-in Service will handle everything Adobe-related: you do NOT need to take further action, such as adding Rule Conditions to all rules with Analytics actions, in order for Analytics to not fire when it shouldn’t. The rules will still fire but they will not send anything to Analytics or Target (yet).

Marketing tags, however, need to be explicitly told to pay attention to the user’s consent preferences. This is, unfortunately, still a very manual process. First, you may need to make your rules a little more category-based. In general it’s easiest if for any particular rule event/condition combo, if you have your performance-category actions together in one rule, and your marketing/targeting actions in a separate rule. We were already generally following that, but there were a few rules that had both analytics and marketing tags- for those, I copied the rule and deleted analytics out of one of them and deleted the marketing tags out of the other one, resulting in one analytics-only rule and one marketing-tag-only rule. The analytics-only rule is fine as-is (thanks, ECID!) but the marketing tag rule needs an extra condition.

If your entire rule falls into one consent category, then it’s as simple as adding a condition where “%onetrust consent: marketing%” equals “true”:

A screenshot from Adobe Launch which shows a rule with the condition of "%onetrust consent: marketing% equals true"

If, however, you need to use conditions within your code to decide what fires and what doesn’t (perhaps your rules don’t neatly align with your categories), that might look like this:

if(_satellite.getVar("onetrust consent: marketing")=="true"){
  //insert code for marketing tag here
}

If someone out there has some extra time on their hands and wanted to write a Launch API function that would automatically add such a condition to all rules, that sure would be swell. In the meantime, I tend to take this approach: open my list of rules, and starting at the top, left-click on each rule while holding down cmd (or ctrl for a PC). This will open each rule in a new tab, while leaving your Rule List tab alone so you can keep track of what you’ve done and not done.

A screenshot showing a ridiculous number of Adobe Launch tabs open in Chrome.

Then work you way through the tabs, adding your condition, saving, then closing that tab. I’ll often open 10-15 at a time, being careful to note where in the rule list I stop. When you’re done, you can use something like Tagtician to export your whole library and make sure you didn’t miss anything.

Side Note: Don’t forget to honor non-banner-based opt-outs!

Part of the California Consumer Privacy Act (CCPA) and others like it, is that sites must have a “Do Not Sell My Personal Info” page where the user can opt out, or view/delete/modify what data you have on them. Lawyers have generally been pretty good about making sure orgs get these pages in place- you probably already have one. Often, these pages are set up to sync up with the org’s CRM… if r.swanson@pawnee.gov fills out a form to opt out of tracking, then the CRM may delete all the info they have for dear R.Swanson, and/or someone may use the Adobe Privacy API or the Adobe Privacy JS to delete/edit/modify/view the Adobe data on that user. That’s all as it should be. The problem is, this process often exists completely separately from the Consent Management Platform. You may have deleted the person for your backend systems, but you’re still setting cookies and collecting data on the site!

Make sure that your Tag Manager knows when these kind of opt-outs occurs and honors them. OneTrust does have a way of tapping into those forms, though I can’t find any public documentation other than this general good advice about CCPA. But within their knowledge base, look up Do Not Sell for AdTech Vendors”- it has tips and script you can use to help OneTrust know what happens on those forms.

Side note: Cookie Deletion

Note, right now, when you tell the ECID Opt-in Service to opt a user out of tracking, it does not remove any existing cookies. So in an opt-out situation, where everyone gets cookies to begin with, you may have users that have declined tracking and won’t be getting analytics beacons, but still have Adobe-created-cookies in their browser. Legally, this is a bit of a gray area. Even if it is technically legal (I’ll leave that to the lawyers to confirm), unfortunately the general public cares a lot about appearances. I’ve already seen one instance where Adobe cookies that were set but not even used (the site didn’t fire analytics beacons) caused a lawsuit in Spain, where they were accused of being “one of the most invasive types of cookie for user privacy”. This is complete nonsense, but the authorities and all the lawyers involved may not understand that without a bunch of effort. Even if the lawsuit fails, it has consumed a lot of that company’s resources.

At bare minimum, you should consider adding verbage to your banner or privacy policy that instructs users how to delete cookies on their own. The regulations strongly encourage this.

If you want to delete the AMCV and AMCVS cookies on behalf of the user, it can be a little tricky, since most of the time there is a domain mismatch between the www.example.com site you are on and the “.example.com” domain the AMCV cookies are set at, so you can’t just use _satellite.cookie.remove or somesuch. But you can do something like this to delete them:

_satellite.cookie.set("AMCV_21C9EXAMPLE123450101@AdobeOrg", "delete", {domain:'.example.com', expires: -1})

Note, many plugins also create cookies- you might see s_ppv or s_vnum or any number of other “s_” cookies that are the default names of cookies created by Adobe plugins. Generally, these don’t have “personal information” and in theory shouldn’t have to worry too much about (though I am not a lawyer so please don’t base your decisions off of me), you may want to delete them or at least be aware of them.

Depending on your ECID settings, you may also be setting a demdex cookie. Since this is a third-party cookie (and can effect how users are identified on other sites) it’s even more legally dodgy, and unfortunately even more difficult to delete. You can either stop that cookie from being set to begin with ECID’s disableThirdPartyCookies setting, or be very careful in how you inform your users about it and their ability to opt-out and/or delete it.

Validation

Here is a non-comprehensive list of things to check to confirm you have things set up correctly: obviously, you want to validate that your Analytics, Target, ECID, marketing tags, targeting tags, etc all only fire with user consent. Check both for cookies (in the “Applications” tab if you’re in chrome) and beacons/scripts (in the “Network” tab). Double check that your setup honors the Global Privacy Control signal (I’ve found this easiest to do in Firefox). Check that the user experience isn’t hindered. Be careful to check things that may be using third party technology, like video players- both that they work, and that they comply with consent policies. If you have iframes anywhere on your site, make sure the content within them is also being controlled by your CMP. Make sure you test for each of your Geolocation rules in OneTrust.

Ideally, before you roll out major OneTrust changes, you’d do a full regression test (the type of QA typically done before a major site release, where you make sure none of your pre-existing stuff is impacted). If you are resorting to using Autoblock for anything, a full regression test is a necessity.

Things I’ve found useful for testing:

OneTrust does provide a preview mode, but the most important thing I got out of that is that you can use query parameters (even in production) to change how OneTrust behaves:

?otreset=true&otpreview=false&otgeo=gb

For example, the above combo resets my preferences so I’ll see the banner again (you can also do this by deleting the OptanonAlertBoxClosed cookie.) The handiest part is the otgeo param- I can have Onetrust treat me like I’m in Great Britain (or any other country code I put there) to test that it works as expected in different jurisdictions.

Another useful thing: you can use this in the console to see what Adobe thinks its current permissions are:

adobe.optIn.permissions

I console.logged the heck out of that when figuring out the timing of things.

Tracking Opt-In/Opt-Out Rates

This is a tricky subject: how do you track that someone said they don’t want to be tracked? It may be worth a chat with your lawyers about. It’s important to remember, most regulations are more about having personal information, such as an ID in a cookie (even anonymous IDs), sent to third parties. Cookies are generally needed for keeping track of a session/visitor from page view to page view (or domain to domain, in some cases). If you are only tracking one user action without context, and there is nothing that ties that action to a particular user in any way, then the user has far less to be concerned about when it comes to their privacy. It’s sensitive and definitely worth discussing with your lawyers, but most regulations do allow for the most basic of tracking if there are no IDs used, the data isn’t shared or sold, it’s stored and processed correctly, the user is informed, etc. The data is useless for journey analysis and attribution, but can actually be used to make sure you are respecting user’s privacy properly. But TALK TO LAWYERS before deciding on any such tracking.

OneTrust does have the option to get high-level reporting on consent rates. I would link to the documentation on it but I’ve never gotten a link to Onetrust documentation to work. Instead, I have to click the ? icon and read any documentation within that panel:

A screenshot showing how to access OneTrust's knowledge base

I’ll admit I have not enabled or used their reporting. I’m curious how they get the info they do in cases where the user has consent to zero tracking. I’m assuming that it is free of personal information such as a cookie ID, etc.

We implemented another solution, where for the first 2 weeks after launching OneTrust, we fired hard-coded, identifier-less/cookie-less Adobe Analytics beacons to a Report Suite set up for that purpose, just to get a rough estimate of how it was working. I hope to have a separate blog post about that in the near future.

Adobe Analytics Settings for Privacy

Adobe does offer some admin settings that also help with privacy. You may want to consider these in addition to what you have set up in your TMS:

  • Your General Report Suite Settings include a “Replace the last octet of IP addresses with 0” option and an “IP Obfuscation” option. The first happens before any processing on a hit, meaning the IP never gets stored or processed, and everywhere in Adobe, that user’s true IP is not accessible (including Data Warehouse and Data feeds). Accordingly, it has an impact on your reporting, especially geolocation rules. The IP Obfuscation Option, on the other hand, happens after some processing is done, so it has less impact. You can use it to merely obfuscate, or to remove the IP address altogether.
  • I only just learned of this back-end setting that respects browser privacy settings (presumably, the older Do Not Track option or the newer Global Privacy Control setting). In theory, if your OneTrust/TMS implementation is configured right, nothing with those flags will ever even make it to Adobe, but there’s no harm in a little redundancy, right?

Conclusion

As I said at the beginning, I would love to learn more and improve my approach where possible. The best solution may end up being a crowd-sourced one. Please let me know what has worked or not worked for you!

* Sorry, Adobe. No one is going to convince me to call it “Data Collection Tags”.

** CCPA: I know there are many laws out there beyond California’s Consumer Privacy Act; however, since CCPA tends to be one the most expansive laws ( for now), I tend to use the phrase “CCPA” as an umbrella stand-in term for US laws. It’s much more complicated than that, but I don’t want to open that can of worms every time I need to reference a opt-out-based regulation.

*** If it wasn’t clear, this post was not done in partnership with OneTrust, and I have no formal relationship with them. I’d be happy to talk to them about anything in this post, though. And for what it is worth, of the 5 CMPs I’ve worked with, OneTrust was the least awful, in my experience.

Enhanced logging for Direct Call Rules and Custom Events for Launch/DTM

33 Sticks Logo- Orange Circle with 3|3 in it

(This is cross-posted from the 33 Sticks blog)

UPDATE: The wonderful devs behind Adobe Launch have seen this and may be willing to build it in natively to the product. Please go upvote the idea in the Launch Forums!

As discussed previously on this blog, Direct Call Rules have gained some new abilities so you can send additional info with the _satellite.track method, but unfortunately, this can be difficult to troubleshoot. When you enabled _satellite.setDebug (which should really probably just be called “logging” since it isn’t exactly debugging) in DTM or Launch, your console will show you logs about which rules fire. For instance, if I run this JavaScript from our earlier blog post:

_satellite.track("add to cart",{name:"wug",price:"12.99",color:"red"})

I see this in my console:

Or, if I fire a DCR that doesn’t exist, it will tell me there is no match:

Unfortunately, this doesn’t tell me much about the parameters that were passed (especially if I haven’t yet set up a rule in Launch), and relies on having _satellite debugging turned on.

Improved Logging for Direct Call Rules

If you want to see what extra parameters are passed, try running this in your console before the DCR fires:

var satelliteTrackPlaceholder=_satellite.track //hold on to the original .track function
_satellite.track=function(name,details){ //rewrite it so you can make it extra special
   if(details){
      console.log("DCR NAME: '"+name+"' fired with the following additional params: ", details)
   }else{
      console.log("DCR NAME: '"+name+"' fired without any additional params")
   }
   //console.log("Data layer at this time:" + JSON.stringify(digitalData))
   satelliteTrackPlaceholder(name,details) //fire the original .track functionality
}

Now, if I fire my “add to cart” DCR, I can see that additional info, and Launch is still able to run the Direct Call Rule:

You may notice this commented-out line:

//console.log("Data layer at this time:" + JSON.stringify(digitalData))

This is for if you want to see the contents of your data layer at the time the DCR fires- you can uncomment it if that’d also be helpful to you. I find “stringifying” a JavaScript object in console logs is a good way of seeing the content of that object at that point in time- otherwise, sometimes what you see in the console reflects changes to that object over time.

Improved Logging for “Custom Event”-Based Rules

If you’re using “Custom Event” rules in DTM or Launch, you may have had some of the same debugging/logging frustrations. Logs from _satellite.setDebug will tell you a rule fired, but not what extra details were attached, and it really only tells you anything if you already have a rule set up in Launch.

For example, let’s say I have a link on my site for adding to cart:

Add To Cart!

My developers have attached a custom event to this link:

var addToCartButton = document.getElementById("cartAddButton"); 
addToCartButton.addEventListener("click", fireMyEvent, false); 
function fireMyEvent(e) { 
   e.preventDefault(); 
   var myCustomEvent = new CustomEvent("cartAdd", { detail: { name:"wug", price:"12.99", color:"red" }, bubbles: true, cancelable: true }); 
   e.currentTarget.dispatchEvent(myCustomEvent)
}

And I’ve set up a rule in Launch to listen to it:

With my rule and _satellite.setDebug in place, I see this in my console when I click that link:

But if this debugging doesn’t show up (for instance, if my rule doesn’t work for some reason), or if I don’t know what details the developers put on the custom event for me to work with, then I can put this script into my console:

var elem=document.getElementById("cartAddButton")
elem.addEventListener('cartAdd', function (e) { 
  console.log("'CUSTOM EVENT 'cartAdd' fired with these details:",e.detail)
}, false);

Note, you do need to know what element the custom event fires on (an element with the ID of “cartAddButton”), and the name of the event (“cartAdd” in this case)- you can’t be as generic as you can with the Direct Call Rules.

With that in place, it will show me this in my console:

Note, any rule set up in Launch for that custom event will still fire, but now I can also see those additional details, so I could now know I can reference the product color in my rule by referencing “event.detail.color” in my Launch rule:

Other tips

Either of these snippets will, of course, only last until the DOM changes (like if you navigate to a new page or refresh the page). You might consider adding them as code within Launch, particularly if you need them to fire on things that happen early in the page load, before you have a chance to put code into the console, but I’d say that should only be a temporary measure- I would not deploy that to a production library.

What other tricks do you use to troubleshoot Direct Call Rules and Custom Events?

Adobe’s performanceTiming plugin, with some improvements and an explanation

33 Sticks Logo- Orange Circle with 3|3 in it

(This is cross-posted form the 33 Sticks blog)

As Page Performance (rightfully) gets more and more attention, I’ve been hearing more and more questions about the Performance Timing plugin from Adobe consulting. Adobe does have public documentation for this plugin, but I think it deserves a little more explanation, as well as some discussions of gotchas, and potential enhancements.

How It Works

Adobe’s Page Performance plugin is actually just piggybacking on built-in functionality: your browser already determined at what time your content starting loading and at what time is stopped loading. You can see this in a JavaScript Console by looking at performance.timing:

This shows a timestamp (in milliseconds since Jan 1, 1970, which the internet considers the beginning of time) for when the current page hit certain loading milestones.

Adobe’s plugin does look at that performance timing data, compares a bunch of the different milestone timestamps versus each other, then does some math to put it into nice, easy-to-read seconds. For instance, my total load time would be the number of seconds between navigationStart and loadEventEnd:

1556048746779 (loadEventEnd) – 1556048745659 (navigationStart) = 1120 milliseconds, or 1.12 seconds.

Additionally, if I choose to, I can have the plugin grab information from the built-into-the-browser performance.getEntries(), put it into session storage (not a cookie because it can be a long list), and put it into the variable of your choice (usually a listVar or list prop) on the next page. These entries show you for EACH FILE on the page, how long they took to load.

Unfortunately, if I’m sending my analytics page view beacon while the page is still loading, then the browser can’t tell me when “domComplete” happened…. because it hasn’t happened yet! So the plugin writes all these values to a cookie, then on your NEXT beacon, reads them back and puts them into numeric events that you define when you set the plugin up. This means you won’t get a value on the first page of the visit, and the values for the last page of the visit won’t ever be sent in. It also means you don’t want to break these metrics down by page, but rather by PREVIOUS page- so often this plugin is rolled out alongside the getPreviousValue plugin. This also means that the plugin is not going to return data for single-page visits or for the last page of visits (it may collect the data but doesn’t have a second beacon to send the data in on). for this reason, your Performance Timing Instances metric may look significantly different from your Page Views metric.

What It Captures

Out of the box, the plugin captures all of the following into events:

  • Redirect Timing (seconds from navigationStart to fetchStart- should be zero if there was no redirect)
  • App Cache Timing (seconds from fetchStart to domainLookupStart)
  • DNS Timing (seconds from domainLookupStart to domainLookupEnd)
  • TCP Timing (seconds from connectStart to connectEnd)
  • Request Timing (seconds from connectEnd to responseStart)
  • Response Timing (seconds from responseStart to responseEnd )
  • Processing Timing (seconds from domLoading to loadEventStart)
  • onLoad Timing (seconds from loadEventStart to loadEventEnd)
  • Total Page Load Time (seconds from navigationStart to loadEventEnd )
  • Instances (for calculated metric- otherwise you only really get the aggregated seconds, which is fairly meaningless if your traffic fluctuates)

Which gets you reporting that looks like this:

…Which, to be honest, isn’t that useful, because it shows the aggregated number of seconds. The fact that our product page took 1.3 million seconds in redirect timing in this reporting period means nothing without some context. That’s why that last metric, “instances”, exists: you can turn any of the first 9 metrics into a calculated metric that shows you the AVERAGE number of seconds in each phase of the page load:

This gives me a much more useful report, so I can start seeing which pages take the longest to load:

As you can see, the calculated metric can use either the “Time” format or the “Decimal” format, depending on your preference.

Performance Entries

As mentioned previously, the plugin can also capture your performance entries (that is, a list of ALL of the resources a page loaded, like images and JS files) and put them into a listVar or prop of your choice. This returns a list, delimited by “!”, where each value has a format that includes:

The name of the resource (ignoring query params)!at what second in the page load this resource started loading!how long it took for that resource to finish loading!resource type (img, script, etc).

For example, on my blog, I might see it return something like this:

https://digitaldatatactics.com/beaconParser/index.html|0.0|0.9|navigation!https://www.digitaldatatactics.com/utility/spiffy.css|0.2|0.1|link!https://digitaldatatactics.com/beaconParser/decoder.css|0.2|0.1|link!https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js|0.2|0.1|script!https://digitaldatatactics.com/beaconParser/js/varInfo.js|0.2|0.1|script!https://digitaldatatactics.com/beaconParser/js/HBvarInfo.js|0.2|0.1|script!https://digitaldatatactics.com/beaconParser/js/wsse.js|0.2|0.1|script!https://digitaldatatactics.com/beaconParser/js/apiConfig.js|0.2|0.2|script!https://digitaldatatactics.com/beaconParser/js/apiRetrieve.js|0.2|0.1|script!https://digitaldatatactics.com/beaconParser/js/decode.js|0.2|0.1|script!https://digitaldatatactics.com/beaconParser/js/print.js|0.2|0.1|script!https://digitaldatatactics.com/images/NarrowBanner.png|0.2|0.3|img!https://digitaldatatactics.com/beaconParser/images/trash.png|0.2|0.5|img!https://assets.adobedtm.com/launch-EN3911ddbdce3b4c4697e2d3c903e9cfc5.min.js|0.4|0.1|script!https://digitaldatatactics.com/beaconParser/images/background-trans.png|0.4|0.3|css!first-paint|0.5|0.0|undefined!first-contentful-paint|0.5|0.0|undefined!https://assets.adobedtm.com/extensions/EP4c3fcccffd524251ae198bf677f3b6e9/AppMeasurement.min.js|0.5|0.0|script!https://jenniferkunz.d1.sc.omtrdc.net/b/ss/jenniferkunztestdev/1/JS-2.12.0-L9SG/s44127282881639|0.7|0.2|img

From this, I can see every file that is used on my page and how long it took to load (and yes, it is telling me that the last resource to load was my analytics beacon, which started .7 seconds into my page loading, and took .2 seconds to complete). This is a LOT of information, and at bare minimum, it can make my analytics beacons very long (you can pretty much accept that most of your beacons are going to become POST requests rather than GET requests), but it can be useful to see if certain files are consistently slowing down your page load times.

An Enhancement: Time to Interaction

Unfortunately, the plugin most commonly used by folks omits one performance timing metric that many folks believe is the most critical: Time to DomInteractive. As this helpful site states:

  • Page Load Time is the time in which it takes to download the entire content of a web page and to stabilize.
  • Time to Interactive is the amount of time in which it takes for the content on your page to become functional and ready for the user to interact with once the content has stabilized.

In other words, Page Load Time might include the time it takes for a lot of background activity to go on, which may not necessarily stop the user from interacting with the site. If your page performance goal is for the best user experience, then Time To Interaction should be a key metric in measuring that. So, how do we track that? It already exists in that performance.timing object, so I tweaked the existing plugin code to include it. I can then create a calculated metric off of that (Time to Interactive/Page Performance Instances) and you can see it tells a very different story for this site than Total Page Load Time did:

9.49 seconds DOES sound like a pretty awful experience, but all three of these top pages had a much lower (and much more consistent) number of seconds before the user could start interacting with the page.

Basic Implementation

There are three parts to setting up the code for this plugin: before doPlugins (configuration), during doPlugins (execution), and after doPlugins (definition).

Configuration

First, before doPlugins, you need to configure your usage by setting s.pte and s.ptc:

s.pte = 'event1,event2,event3,event4,event5,event6,event7,event8,event9,event10,event11'
s.ptc = false; //this should always be set to false for when your library first loads

In my above example, here is what each event will set:

  • event1= Redirect Timing (seconds from navigationStart to fetchStart- should be zero if there was no redirect)- set as Numeric Event
  • event2= App Cache Timing (seconds from fetchStart to domainLookupStart)- set as Numeric Event
  • event3= DNS Timing (seconds from domainLookupStart to domainLookupEnd)- set as Numeric Event
  • event4= TCP Timing (seconds from connectStart to connectEnd)- set as Numeric Event
  • event5= Request Timing (seconds from connectEnd to responseStart)- set as Numeric Event
  • event6= Response Timing (seconds from responseStart to responseEnd )- set as Numeric Event
  • event7= Processing Timing (seconds from domLoading to loadEventStart)- set as Numeric Event
  • event8= onLoad Timing (seconds from loadEventStart to loadEventEnd)- set as Numeric Event
  • event9= Total Page Load Time (seconds from navigationStart to loadEventEnd )- set as Numeric Event
  • event10= Total Time to Interaction (seconds from connectStart to timeToInteraction)- set as Numeric Event. NOTE- THIS IS ONLY ON MY VERSION OF THE PLUGIN, OTHERWISE SKIP TO INSTANCES
  • event11= Instances – set as Counter Event

I’d also need to make sure those events are enabled in my Report Suite with the correct settings (everything should be a Numeric Event, with the exception of instances, which should be a Counter Event).

Execution

Within doPlugins, I need to just run the s.performanceTiming function. If I don’t want to capture performance entries (which is reasonable- not everyone has the listVars to spare, and it can return a VERY long value that can be difficult to get value out of), then I fire the function without any arguments:

s.performanceTiming()

If I DO want those performance entries, then I add the name of that variable as an argument:

s.performanceTiming("list3")

Also, you’re going to want to be capturing Previous Page Name into a prop or eVar if you aren’t already:

s.prop1=s.getPreviousValue(s.pageName,'gpv_pn');

(If you are already capturing Previous Page Name into a variable, you don’t need to capture it separately just for this plugin- you just need to be capturing it once somewhere).

Definition

Finally, where I have all of my plugin code, I need to add the plugin definitions. You can get Adobe’s version from their documentation, or if you want it with Time To Interactive, you can use my version:

/* Plugin: Performance Timing Tracking - 0.11 BETA - with JKunz's changes for Time To Interaction. 
Can you guess which line I changed ;)?*/
s.performanceTiming=new Function("v",""
+"var s=this;if(v)s.ptv=v;if(typeof performance!='undefined'){if(perf"
+"ormance.timing.loadEventEnd==0){s.pi=setInterval(function(){s.perfo"
+"rmanceWrite()},250);}if(!s.ptc||s.linkType=='e'){s.performanceRead("
+");}else{s.rfe();s[s.ptv]='';}}");
s.performanceWrite=new Function("",""
+"var s=this;if(performance.timing.loadEventEnd>0)clearInterval(s.pi)"
+";try{if(s.c_r('s_ptc')==''&&performance.timing.loadEventEnd>0){try{"
+"var pt=performance.timing;var pta='';pta=s.performanceCheck(pt.fetc"
+"hStart,pt.navigationStart);pta+='^^'+s.performanceCheck(pt.domainLo"
+"okupStart,pt.fetchStart);pta+='^^'+s.performanceCheck(pt.domainLook"
+"upEnd,pt.domainLookupStart);pta+='^^'+s.performanceCheck(pt.connect"
+"End,pt.connectStart);pta+='^^'+s.performanceCheck(pt.responseStart,"
+"pt.connectEnd);pta+='^^'+s.performanceCheck(pt.responseEnd,pt.respo"
+"nseStart);pta+='^^'+s.performanceCheck(pt.loadEventStart,pt.domLoad"
+"ing);pta+='^^'+s.performanceCheck(pt.loadEventEnd,pt.loadEventStart"
+");pta+='^^'+s.performanceCheck(pt.loadEventEnd,pt.navigationStart);pta+='^^'+s.performanceCheck(pt.domInteractive, pt.connectStart);"
+"s.c_w('s_ptc',pta);if(sessionStorage&&navigator.cookieEnabled&&s.pt"
+"v!='undefined'){var pe=performance.getEntries();var tempPe='';for(v"
+"ar i=0;i<pe.length;i++){tempPe+='!';tempPe+=pe[i].name.indexOf('?')"
+">-1?pe[i].name.split('?')[0]:pe[i].name;tempPe+='|'+(Math.round(pe["
+"i].startTime)/1000).toFixed(1)+'|'+(Math.round(pe[i].duration)/1000"
+").toFixed(1)+'|'+pe[i].initiatorType;}sessionStorage.setItem('s_pec"
+"',tempPe);}}catch(err){return;}}}catch(err){return;}");
s.performanceCheck=new Function("a","b",""
+"if(a>=0&&b>=0){if((a-b)<60000&&((a-b)>=0)){return((a-b)/1000).toFix"
+"ed(2);}else{return 600;}}");
s.performanceRead=new Function("",""
+"var s=this;if(performance.timing.loadEventEnd>0)clearInterval(s.pi)"
+";var cv=s.c_r('s_ptc');if(s.pte){var ela=s.pte.split(',');}if(cv!='"
+"'){var cva=s.split(cv,'^^');if(cva[1]!=''){for(var x=0;x<(ela.lengt"
+"h-1);x++){s.events=s.apl(s.events,ela[x]+'='+cva[x],',',2);}}s.even"
+"ts=s.apl(s.events,ela[ela.length-1],',',2);}s.linkTrackEvents=s.apl"
+"(s.linkTrackEvents,s.pte,',',2);s.c_w('s_ptc','',0);if(sessionStora"
+"ge&&navigator.cookieEnabled&&s.ptv!='undefined'){s[s.ptv]=sessionSt"
+"orage.getItem('s_pec');sessionStorage.setItem('s_pec','',0);}else{s"
+"[s.ptv]='sessionStorage Unavailable';}s.ptc=true;");
/* Remove from Events 0.1 - Performance Specific, 
removes all performance events from s.events once being tracked. */
s.rfe=new Function("",""
+"var s=this;var ea=s.split(s.events,',');var pta=s.split(s.pte,',');"
+"try{for(x in pta){s.events=s.rfl(s.events,pta[x]);s.contextData['ev"
+"ents']=s.events;}}catch(e){return;}");
/* Plugin Utility - RFL (remove from list) 1.0*/
s.rfl=new Function("l","v","d1","d2","ku",""
+"var s=this,R=new Array(),C='',d1=!d1?',':d1,d2=!d2?',':d2,ku=!ku?0:"
+"1;if(!l)return'';L=l.split(d1);for(i=0;i<L.length;i++){if(L[i].inde"
+"xOf(':')>-1){C=L[i].split(':');C[1]=C[0]+':'+C[1];L[i]=C[0];}if(L[i"
+"].indexOf('=')>-1){C=L[i].split('=');C[1]=C[0]+'='+C[1];L[i]=C[0];}"
+"if(L[i]!=v&&C)R.push(C[1]);else if(L[i]!=v)R.push(L[i]);else if(L[i"
+"]==v&&ku){ku=0;if(C)R.push(C[1]);else R.push(L[i]);}C='';}return s."
+"join(R,{delim:d2})");

You’ll also need to have s.apl and s.split.

You can see a full example of what your plugins code might look like, as well as a deobfuscated picking-apart of the plugin, on our gitHub.

Performance Entries Classifications

I recommend if you ARE capturing Performance Entries in a listVar, setting up 5 classifications on that listVar:

  • Resource/File
  • Starting Point
  • Duration
  • Duration- Bucketed (if desired)
  • Resource Type

Then set up a Classification Rule, using this regex string as the basis:

^(.+)\|(.+)\|(.+)\|(.+)

In our git repo, I have a full list of the classification rules and regex I used, including how to bucket the durations so you get less granular values like “0.5-1.0 seconds”, which can give you a report like this:

Implications for Single Page Apps

Unfortunately, this plugin will NOT be able to tell you how long a “virtual page” on a single page app (SPA) takes to load, because it relies on the performance.timing info, which is tied to a when an initial DOM loads. This isn’t to say you can’t deploy it on a Single Page App- you may still get some good data, but the data will be tied to when the overall app loads. Take this user journey for example, where the user navigates through Page C of a SPA, then refreshes the page:

As you can see, we’d only get performanceTiming entries twice- once on Page A and once on the refreshed Page C. Even without the “virtual pages”, it may still be worth tracking- especially since a SPA may have a lot of upfront loading on the initial DOM. But it’s not going to tell the full story about how much time the user is spending waiting for content to load.

You can still try to measure performance for state changes/”virtual pages” on a SPA, but you’ll need to work with your developers to figure out a good point to start measuring (is it when the user clicks on the link that takes them to the next page? Or when the URL change happens?) and at what point to stop measuring (is there a certain method or API call that brings in content? Do you having a “loading” icon you can piggy back on to listen to the end?). Make sure if you start going this route (which could be pretty resource intensive), you ask yourselves what you can DO with the data: if you find out that it takes an average 2.5 seconds to get from virtual page B to virtual page C, what would your next step be? Would developers be able to speed up that transition if the data showed them the current speed was problematic?

Use the Data

Finally, it’s important to make sure after you’ve implemented the plugin, you set aside some time to gather insights and make recommendations. I find that this plugin is one that is often used to just “check a box”- it’s nice to know you have it implemented in case anyone ever wants it, but once it is implemented, if often goes ignored. It is good to have in place sooner rather than later, because often, questions about page performance only come up after a change to the site, and you’ll want a solid baseline already in place. For instance, if you’re migrating from DTM to Launch, you might want to roll this plugin out in DTM well in advance of your migration so after the migration, you can see the effect the migration had on page performance. Consider setting a calendar event 2 weeks after any major site change to remind you to go and look at how it affected the user experience.

DTM-to-Launch Migration Series #3: The Migration Process

33 Sticks Logo- Orange Circle with 3|3 in it

(This is cross-posted from the 33 Sticks Blog)

Thus far in this series, we’ve discussed your options for a DTM-to-Launch Migration, and some potential areas you can improve upon your solution as part of a migration. As you can see from my previous posts, there are a lot of possible considerations for a DTM-to-Launch migration. So what might the actual process look like to get your company on Launch instead of DTM?

Figure Out How You’ll Roll Out

Does it make sense for your org to roll Launch out all at once to all of your properties? Or would you prefer to bite off one chunk at a time? (For instance, one client is currently updating their internal search single page app, so they’re going to roll out Launch there first, as a sort of guinea pig.) Keep in mind that even if you are only rolling out Launch to 3 pages first, ANY roll out is going to have to tackle some global logic- it may be that those first three pages are the hardest because you’ll need to tackle how to handle not just the requirements for those three pages, but also global items like authentication status or global marketing tags.
If you do want to roll out all at once, you can keep using the same DTM embed code you always have so your developers don’t need to make changes to the pages, but that’s an all-or-nothing option (once you switch to Launch, Launch will “own” that embed code unless you choose to re-publish from DTM), and it only works in prod (dev/staging environments will still need the new embed codes).

Also, if you’re considering having DTM and Launch run alongside each other on the same page…. don’t even consider this an option. It won’t work. Both tools use the _satellite object and one WILL overwrite the other and/or get very confused by the presence of the other.

Validation

Keep in mind the effort to validate things- even if you are doing a “simple lift-and-shift”, you will still need to validate that Launch is doing all the things that DTM had been doing. Depending on how well-documented your current implementation is, and/or what QA efforts are currently in place, this may mean figuring out what it is that DTM is currently doing so you know whether Launch is matching it or not. This is a golden opportunity to set up some QA processes, if you haven’t already.
If you don’t have a solid process already in place, you won’t be able to test every possible beacon for every possible user, but you should can set up a testing procedure in place for critical beacons on your most typical flows. Note, none of this is specific to DTM or Launch, but is a best practice regardless and will help with the DTM-to-Launch migration.

  • Establish key user flows and document each beacon in the flow’s expected variables
  • Use a tool like Observepoint or Hub’scan to automate testing
  • For your KPIs, in Adobe Analytics set up anomaly detection and/or alerts based on reasonable thresholds (alert me if revenue dips below $___ or visits climbs above ___)

This is all much easier if you used the migration as a chance to document your solution.

Audit What You’ve Got and What You Want

Unfortunately, Adobe does not provide a great way to document all of your current rules and data elements in DTM. Fortunately, there is a tool to help: Tagtician has a free chrome extension that can create a spreadsheet with a list of all your data elements, rules (including third party tags and what is deployed in the Adobe Analytics/Google Analytics section of each rule.) I cannot overstate how incredibly helpful this has been for every DTM migration project I’ve been on.
Depending on how ambitious our migration plans are (on a scale of “lift-and-shift” to “burn it down and start fresh”), I’ve used this as a basis for a new solution design, so we know on each user action what variables are expected, where those variables are set, and where they pull their information from:

Then I take that to figure out how to deploy it through Launch (which may or may not look anything like how it was deployed in DTM): for instance, if pageName is always going to get it’s value from the same data element, I can set that in a global rule that fires on all page loads. Whereas my search variables can get their own rule, which will combine with the global rule on the search results page to create one analytics beacon with all the variables I need. Now that you can control load order and when analytics beacons fire in Launch, you may be able to really compartmentalize logic based on scope and get rid of redundancy in your implementation.

Decide On Your Publishing Flow

Launch has a new publishing flow- it’s no longer just staging vs production. You now have development (as many environments as you need), staging, and production; no changes automatically get built into a library unless you set it up to; you can use libraries to group together changes and move a group through the flow. If you only have one person in Launch at a time, and that one person tends to do most approvals and publishes, then the flow can definitely seem like “too much.” But for a lot of bigger organizations, this new flow is a game changer.
Part of moving to Launch is figuring out how this flow should apply to your organization. For example, one client came up with something similar to this:

At the start of each sprint, they create a library with that sprint name, and link it to the main dev environment. Each member of their analytics team has their own permanent library in dev, linked to alternative dev environments (which aren’t referenced by any pages and are only really interacted with through the switcher plugin- basically a sandbox for them to build in, using the switcher plugin to see the effect of their efforts in dev). As changes are completed and pass their first round of validation, they get moved into the Sprint’s library, which at the end of the sprint moves into Staging, where it is validated by the developer/UX QA team before being approved and published. (This is just an example- there is no single “right way” to use this flow, it was designed to be flexible for a reason.)
Be aware, once a library has “claimed” an environment (which is linked to an embed code), no other library can claim that environment, so if you want multiple libraries you will need multiple dev environments.
Also, you can no longer use code in a developer console to switch between environments- currently, the only way I know to switch between environments is to use the Search Discovery switcher chrome extension or to use something like Charles Proxy Map Remote.

The Migration Project Plan

A DTM-to-Launch migration can become quite the involved project. For the simplest of migrations, it still may be 4-6 weeks to migrate within the tools, do any necessary validation, and publish your changes. It may only need to be one or two main analytics/TMS folks and/or a QA person working on it.
Or, it may be a 9 month project that involves devs, QA/UAT, data architects, analysts… don’t underestimate the resource cost of the migration (though at the same time, don’t undervalue the long-term resource savings if you take the time to get it right as part of the migration and (re)build a scalable, maintainable, well-documented solution.)
For instance, below is an example of how a Launch migration could go. This example does not include any changes to the data layer, but does include a substantial attempt to re-deploy analytics rather than merely shift the existing implementation with all the same rules and data elements.

Next Steps and Resources

As you can see, even a simple lift-and-switch to Launch can be a bit involved, and folks can feel daunted by all the considerations, options, and things to be aware of. I’ve tried to be as thorough and comprehensive as possible in this series, and I hope I hit the right level of detail to give practical guidance on how to tackle a DTM-to-Launch migration. There is a great community out there for folks who need DTM/Launch support- check out the following resources:

Hopefully this series helped, but feel free to reach out if you have questions or if you’d like to engage with us to make sure you get off on the right foot as you move to Launch.

DTM-to-Launch Migration Series #2: A Golden Opportunity

33 Sticks Logo- Orange Circle with 3|3 in it

(Cross-posted from the 33 Sticks blog)

Aside from all of the things that Launch handles better than DTM did (which I discussed a bit in my previous post in the series), a move to Launch provides an opportunity to clean up and optimize your implementation (to the point that even if you weren’t moving to Launch, you could still do this clean up within DTM). You can save yourself from headaches and regret down the line if you take the time now to define some standards, adopt some best practices, or apply some “lessons learned” from your DTM implementation.

Redo Your Property Structure

Many companies set up their DTM properties based on a certain understanding of how properties should be used, and realized a bit too late that a different set up might work better.
previous post of mine on this topic is still applicable in Launch: your properties should not be based on Report Suites or domains, but rather, on the three following questions:

  • How similar are the implementations between your sites (do they use the same data layer, for instance? Would the rules be triggered by the same conditions?)
  • How similar are the publication timelines (if you publish a change for Site A, would it also apply to Site B at that time?)
  • Will the DTM/Launch implementation be maintained/updated by the same folks? (Properties are a good way to control user access.)

Keep in mind Launch has an API for configuration, so if you have 15 properties and want to make a change to all of them at once, you now can (though that API is not yet super documented/supported, so it’s a bit of a wild west so far). In general, I’ve seen folks using Launch as an opportunity to move to fewer properties.

Define Standards and Best Practices

Now is a great time to take lessons learned from DTM and define the standards your company will follow within Launch. Some things are arbitrary- it doesn’t really matter if I name the rule “Product Details Page View” or “page: product details”, but if we are consistent from the start, it can save us a lot of head ache and cleanup down the road.

Tags With the Same Condition(s)/Scope Should Share the Same Rule

To keep your library light, and your implementation scalable and maintainable, I highly recommend basing your rules on their scope/condition, rather than the tags they contain. For instance, a single rule named “Checkout: Order Confirmation” is better than 10 different rules that fire on Order Confirmation- “Doubleclick Order Confirmation” and “Google Conversion Tags”, etc.
I’ve written previously about why this matters– it can have a surprising affect on page performance (not to mention it cane make your TMS impossible to navigate/maintain), and that still applies in Launch.

Delete redundant and unused stuff

Run an audit of your DTM property. Do you have redundant or unused Data Elements? Empty (or permanently commented-out) rules or Third Party Tags? Inactive rules or data elements that aren’t likely to ever be used again? Often folks are afraid to delete things within DTM, but this is a great chance to delete anything that isn’t still useful.

Institute a Naming Schema

This is your chance to have a nice, clean naming standard in your TMS. Consider all the following things you can name in Launch:

  • Data Elements: I try to keep to the same [category]:[details], though since Launch doesn’t show the DE type from the DE list like DTM does, I also like to include the type: “search: term: QP” (QP for Query Parameter) or “checkout: order total:DL” (DL for Data Layer). I also prefer keeping everything for Data Elements lowercase so I don’t have to worry/remember how I capitalized things.
  • Rules: In DTM I liked to do something like “[category]:[scope/condition]” (eg “Search: Results”, “Catalog: Product Details”, “Checkout: Cart View”.) In Launch, because DCRs, EBRs and PLRs now share the same interface, I like to take it a step further and include the rule type at the front: “Page: Search: Results” or “Click: Search: Filter”. If you have a lot of rules potentially firing into the same beacon, then I’d also include info about the order (eg, “Page: Global: All Pages #100” and “Page: Home #25” so you know that the #100 one would fire AFTER the #25 one on the home page.) I’ve also found it helpful to call out the rules which actually fire my analytics BEACON as opposed to rules that run higher in the order and only set variables (eg: “Page: Global: All Pages (s.t) #100”). Then within Rules, there are more naming considerations than there had been in DTM:
    • Events: Should be descriptive, and it may be worth including the load order (so “Page Top- #100” or “Direct Call: Add to Cart #50” might do the trick.)
    • Conditions/Exceptions: Conditions and Exceptions particularly should have some sort of custom naming (instead of a condition “Core – Value Comparison”, I might name it “pageName DE=’search results’”).
    • Actions: I’ve been leaving some with the default (eg, “Adobe Analytics – Set Variables”, though depending on how complicated my implementation is, I might want to change that to “Analytics- Content Identification variables”). Any Core/Code actions should have a descriptive name (“Yahoo pixel- expires 12/19/19” or similar.)

Fix Up Your Data Layer

This is perhaps a very ambitious task for most migrations, but if you’re already taking the effort to audit your DTM implementation, now might be a good time to also look at your data layer- do you have data layer objects that aren’t being used in DTM at all currently? (Be aware, of course, that data layers don’t always exist solely for a TMS’s sake- make sure no one else is using it either). Before you go creating a bunch of data elements, is there something you wish your data layer had that it currently doesn’t? Or do you wish it were structured differently? Now might be a good chance to optimize it! Especially if you are rolling Launch out to one part of your site at a time, you may be able to work with devs to break up a Data Layer rollout into reasonable chunks. You may be surprised by how many devs are on board with fixing up the data layer, particularly if your current on is messy/confusing.

Move Third Party Tags to Asynchronous JS

This is one of the biggest areas for improvement I’ve seen amongst my current and past clients- they’ve potentially been using DTM for years and haven’t always taken advantage of DTM’s ability to improve page performance by moving third-party tags to asynchronous javascript.All tag managements systems have inherent weight- you are adding a JS library to your site. If you don’t mitigate this weight by using the TMS to optimize your tags, your TMS may be having a net-negative affect on your site- a substantial one, in many cases. I’ve written previously about the approach I would recommend for third-party tags, but to emphasize the importance of this: I have seen the overall page load time improve by 15-30% by simply moving tags within DTM to async. Unless the vendor’s code affects the user experience (chat, survey or optimization tools, for instance), there is no reason for most tags to be anything other than non-sequential JS.

In Launch, you can take it a step further, and use extensions to further optimize your tags. For instance, if you use Facebook or Doubleclick, there are extensions in place that you can use to move those tags entirely out of custom code blocks. Or, if you are deploying a simple pixel tag and the vendor does not have an extension, you can use 33 Sticks’ Pixel Loader extension to easily change it from an html  tag to asynchronous javascript.

Document Everything!

Moving to Launch also provides the ability to get solid, current documentation on your solution. Aside from auditing your solution (I’ll take about that in a moment) so you know which rules are setting what or what is expected in the Data Layer on certain pages, I also recommend using this fresh start as a change to document and enforce your standards and best practices for TMS deployment. For instance, I’ve helped clients create a confluence document that anyone at thier company who might be within Launch can access, detailing:

  • Naming Strategy (see notes above)
  • Third Party Tag deployment standards (which tags are “approved” by your org for use- as in, “do not use one TMS to deploy another TMS like GTM, not unless you hate your site loading quickly”); deploying tags as asynchronous JS- see note above…)
    • I also recommend as part of the auditing/documentation process getting a list of all your third party tags, documenting who at your org “owns” that tag, and setting “expiration/renewal” dates (“Jan Smith owns this floodlight tag, deployed 8-5-18; on 9-5-18 we will contact her to see if the tag is still valid or can be deleted”).
  • Best Practices (don’t check “apply handler directly to element” without good reason, try to limit the number of Data Elements used in “Data Element Change” rule triggers, etc.)
  • Publication Flow (how is your org using libraries and environments? Who approves and who publishes? Will publishing happen with a specific cadence, like every other Wednesday? What is your QA/validation process? Do you want to implement an “all changes must be reviewed by someone other than the person who made the change” rule?)

I know this level of documentation can be daunting and seem like overkill, but your future staff/employees will thank you for it, even if it’s informal and/or a work-in-progress.

Change Your Deployment Method (Adobe-Managed vs Self-Hosted)

DTM had a few deployment options:

  • An Adobe/Akamai-hosted library (ie, your embed code starts with “//assets.adobedtm.com”)
  • An FTP self-hosted library (DTM would push changes through FTP to a location on your own servers)
  • A downloaded self-hosting option (you would manually download after changes and put onto your servers).

Now may be an opportunity to change this- if you’ve been doing the manual download option because of security concerns, now that the publishing flow in Launch is more flexible/powerful, might you be able to simplify by moving to another option?

Technically, all three of these options also exist in Launch, though the approach is slightly different. I’ve documented in a separate post how you can achieve each of the three methods in Launch- especially the download method, which may not be intuitive for users who had used the download option in DTM.

Update Your visitorID/appMeasurement Libraries

A TMS upgrade is also a good chance to update to the most recent stable Adobe libraries (for instance, as of this moment, the most current Analytics library is 2.10). Unless you are doing something very custom/weird in your libraries (or are stuck in the dark ages on H code), updating should be a relatively easy process, and offers benefits like improved page performance.

It may also make sense to examine your doPlugins function (if you are still using it): do you have functionality you can move out of doPlugins (eg, do you still really need getQueryParam when you can just use the DTM/Launch interface?) (Also, word on the street is that some folks at Adobe may be releasing an extension to handle many of the common plugins, so that may provide some extra room for enhancement.)

Update cross-Adobe Tool integrations

If you’re not yet on the VisitorID service, you really should be. Then once you are on that, now would be a good time to update your implementation for integrating analytics with other Adobe tools:

  • If you use Target, are you on at.js (and is it current)? Do you have Analytics 4 Target (A4T) set up?
  • If you use Audience Manager, have you transitioned to a server-side integration? Are you currently deploying your DIL at the bottom of your Analytics code in DTM, and might you be able to transition that to use the AAM extension?

What’s Next

By now, you should have a sense of what type of migration path you’re going to take, and what aspects of your solution you may want to change or improve upon. The next post in the series will walk you through the actual process and provide a rough framework for a project plan.

DTM-to-Launch Migration Series #1: Options and Considerations

33 Sticks Logo- Orange Circle with 3|3 in it

(cross-posted from the 33 Sticks Blog)

Adobe’s Launch is really building momentum (they just announced the plan to sunset DTM– editing abilities end December 31st, 2019 July 1st, 2020; read-only access dies June 2020 December 31st, 2020 (dates updated to reflect Adobe’s change)), and in the past few months, it feels like almost every day, I get asked “what does a launch migration look like?”

And I’m afraid I have a very unhelpful answer: it totally depends.

We’ve had visibility into about a dozen migrations now, and each one has been a completely unique case. But I figured I can at least defend my answer of “it depends” by clarifying what it depends on, what the options are, and what considerations should you make.

WHAT YOU NEED TO KNOW

Preparing to Migrate
 Adobe DTM to Launch Migration Options
 Things to be aware when moving from DTM to Launch

Using the Migration as an Opportunity
 Redo your property structure
 Define standards within Launch
 Clean up redundant/unused items
 Best practices for Rule scope/conditions
 Institute a Naming Schema
 Fix up your data layer
 Optimize your third party tags
 Document everything
 Change your deployment method
 How to use the download option
 Update your visitorID/appMeasurement libraries
 Update cross-Adobe Tool integrations

The Migration Process
 How to roll out Launch
 Validation
 Audit what you have (and figure out what you want)
 Decide on a publishing flow that works for your org
 Create a Migration Project Plan
 Other Resources and Next Steps

Disclaimer: Info in this series is accurate as of, October 29, 2018. We will try to update it as it makes sense to do so, but things can change quickly in the world of TMSes and iterative product releases.

You’ve Got Options

As far as we see it, if you’re considering a move from Adobe DTM to Launch, you have a few options:

  1. Use the DTM-to-Launch Migration tool (SEE: Adobe’s documentation), essentially just doing a lift-and-shift of your current DTM implementation.
  2. Use the DTM-to-Launch migration tool, but do a fair amount of clean up before/after.
  3. Use a tool like Tagtician to audit what you currently have, decide what you want to carry over, and set it up “fresh” in Launch (have Launch accomplish the same thing as DTM, but perhaps accomplish it in different ways).
  4. Use this as a chance to rebuild your solution from the ground up.

Most folks we’ve talked to or worked with are looking at somewhere in that 2-3 range. In most cases, we’d strongly discourage going with option #1, that straight-up lift-and-shift. I PROMISE there is some room for review and improvement in your DTM implementation.

First, not everything in DTM will work in Launch. Our friends at Search Discovery have a great tool for detecting places within DTM that you may be using code that will no longer work (goodbye, _satellite.getQueryParam). (NOTE: this detects places in your DTM library you are using those “forbidden” functions- if you are using something like _satellite.getQueryParam in your own javascript outside of DTM, it will not detect it.)

Technically, aside from the things that that tool will flag, everything that worked in DTM should work in Launch (actually, there are a few major differences you should be aware of). BUT, many of the workarounds you may have resorted to in DTM are no longer needed, so you can definitely optimize things. There are some broader differences between DTM and Launch that open the door for some changes to your implementation that could be really valuable.

Consider the following questions:

Are you currently using DTM for Single Page Apps? (if so, you’ve almost certainly had to use some workarounds that are no longer needed)

Do you have any repeated global logic (all of your DCRs or EBRs might be setting “eVar5=%auth status%” because you didn’t have a way to get that eVar included on all beacons otherwise)

Do you use Direct Call Rules heavily?

Do you have s.clearVars running in odd places?

Are a large portion of your Analytics variables being set in custom code blocks instead of in the interface?

Do you fire any Direct Call Rules from within your DTM implementation (eg, DCRs calling other DCRs to get around timing/scope issues?)

Are you currently firing Adobe Analytics beacons from outside of the Analytics Tool (eg, are you using a third party tag box to fire s.t or s.tl because of timing issues?)

If you answered yes to any of the above questions (and perhaps even if not), then you absolutely should be considering moving to Launch ASAP, for all the reasons discussed on these other blog posts:

Differences between DTM and Launch to be Aware of

33 Sticks Logo- Orange Circle with 3|3

(cross-posted from the 33 Sticks blog)

There’s a lot of talk about how Adobe Launch is backwards-compatible- that, aside from a few _satellite methods that may not still work (that were probably not supported to begin with), anything you had in DTM should still work in Launch. But, well, not EVERYTHING in DTM is still going to work in Launch, and some things in Launch may catch you off guard. Here are some things you should be aware of:

Far fewer things happen automatically. For instance, Adobe Analytics no longer automatically fires a beacon on page load (which I view as a wonderful thing, but you still need to be aware of it). You need to set it up (and things like loading Target or firing Mboxes) in a rule.

 The following _satellite methods (among others, but these are the most common) are no longer supported (or, in some cases, may never have been supported but now simply won’t work).

  • _satellite.getQueryParam/_satellite.getQueryParamCaseInsensitive
  • _satellite.notify (this still technically works, but you should migrate to _satellite.logger)
  • _satellite.URI
  • _satellite.cleanText
  • _satellite.setCookie (which is now _satellite.cookie.set) and _satellite.readCookie (which is now _satellite.cookie.get)

 There is some interface functionality in DTM that is not yet in Launch:

  • There is no “notes” functionality currently (though I hear that is coming soon)
  • It’s not easy to do a revision comparison (diff compare) currently (though again, I hear that is in the works).

 Launch still has console debugging, but it no longer alerts you to what “SATELLITE DETECTED” (which I used a lot to troubleshooting bubbling issues)- it merely tells you what rules are firing, etc.

 Some tools like Tagtician or Disruptive Advertising’s DTM Debugger are not yet fully Launch-compatible. (Tagtician supports Launch but is working on improving how it handles it; I don’t know if the DTM Debugger has any plans to become Launch-compatible).

 The Adobe Analytics extension does not support multiple Adobe instances, nor can you have multiple Adobe Analytics extensions installed. (Multi-suite tagging is still ok).

 The Google Analytics extension does not support multiple GA instances.

 Some things have been renamed in a way that may throw you off- for instance, you can still easily have a Rule condition be based on a Data Element value- it’s just named “Value Comparison” now.

 While Launch gives you much more control over the order things happen in, be aware that while actions within a rule will START in the specified sequence, they may not COMPLETE in sequence: Action 1 will start, then Action 2 will start whether Action 1 is finished or not. This is particularly significant if the actions are just code (for instance, I had my first action try to pull information from an API, and my second action then use that info to fire a pixel… but the pixel kept firing before the API had done its thing). I hear that users may eventually get more control over this, but for now this is how it is.

 Adapters can be confusing (fortunately Jimalytics clears it up nicely on his blog). These days, Adobe automatically creates a “Managed by Adobe” adapter, and that single adapter should work for multiple environments.

None of these are necessarily a reason to not upgrade- especially since Adobe now has a plan for sunsetting DTM. But hopefully you won’t be caught unaware by any of these items. Has anything else surprised you about Launch? Let us know!