Temporarily show hidden field values (for debugging conversion tracking scripts)

Whether your tracking logic uses standard UTMs, custom query params, or other dimensions, you’re gonna need hidden form fields.

Problem is... they’re hidden. And – take it from someone who’s built 3 separate fairly-full-fledged tracking libraries — debugging hidden fields can be really annoying.

Your JS should have an option for verbose console logging (ours does, though most others don’t!) and Dev Tools lets you inspect <input type="hidden"> elements. But both of those are cumbersome and completely impractical on mobile.[1].

Wouldn’t it be better if you could peek at hidden field names + values, while leaving them hidden and without changing any form logic? So you get a cute little widget at the bottom of the form:

And when you open the widget, you see the current hidden field details:

I even outdid myself and added a refresh button, in case hidden fields change as you interact with the page (and depending on script load order, not all fields will be populated immediately).

Get the code

Add the code below into your staging and prod environment as-is. Then (see next section) you can initialize the widget in different ways.

/**
 * Add hidden fields peek widget to Marketo forms
 * @author Sanford Whiteman
 * @version v2.0.0 2023-02-15
 * @copyright © 2023 Sanford Whiteman
 * @license Hippocratic 3.0: This license must appear with all reproductions of this software.
 *
 */
function initHiddenFieldsWidget(formEl,userOptions){
      
   const defaultOptions = {
      requireQueryParam: undefined, // if set to string, param must be present in query string
      lessInterestingHiddens: ["formid", "munchkinId"]
   };

   const options = Object.assign({}, defaultOptions, userOptions);

   if(typeof options.requireQueryParam == "string"){
      let currentQuery = new URL(document.location.href).searchParams;
      if( !currentQuery.has(options.requireQueryParam) ) { return; }
   }

   const widgetStyles = {
      "position" : "relative",
      "font-size" : "12px",
      "font-weight" : "normal",
      "line-height" : "16px",
      "margin-top": "12px",
      "padding": "12px",
      "border": "1px solid #666666",
      "border-radius": "6px",
      "background-color": "#f5f5f5",
      "font-family": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif",
      "color": "#111111"
   };

   const titleStyles = {
      "cursor" : "pointer"
   };

   const buttonStyles = {
      "position": "absolute",
      "font-size" : "24px",
      "line-height" : "24px",
      "top": "4px",
      "right" : "4px",
      "border": "none",
      "background-color": "transparent",
      "cursor": "pointer"
   };

   const fieldsetStyles = {
      "border" : "none"
   };

   const outputStyles = {
      "display" : "block",
      "margin": "5px 0"
   };

   function applyStyles(element,styles){
      Object.entries(styles).forEach(([propName,compoundValue]) => {
         let [propValue,valuePriority] = compoundValue.split(/\s*!important\s*$/i);
         element.style.setProperty(propName, propValue, valuePriority === "" ? "important" : undefined);
      });
   }

   const wrapper = document.createElement("div");
   wrapper.classList.add("hidden-fields-peek-wrapper");
   wrapper.attachShadow({ mode: "open" });

   const widget = document.createElement("details");  
   widget.classList.add("hidden-fields-peek-widget");      
   applyStyles(widget,widgetStyles);

   const title = document.createElement("summary");  
   title.textContent = "Hidden fields";  
   applyStyles(title,titleStyles);
   widget.append(title);

   const button = document.createElement("button");
   button.type = "button";
   button.textContent = "⟳";
   button.title = "refresh fields";
   applyStyles(button,buttonStyles);
   widget.append(button);

   const fieldset = document.createElement("fieldset");
   applyStyles(fieldset,fieldsetStyles);
   widget.append(fieldset);

   widget.addEventListener("toggle", refreshFieldset);      
   button.addEventListener("click", refreshFieldset.bind(widget));

   wrapper.shadowRoot.append(widget);
   formEl.append(wrapper);

   function refreshFieldset(){
      if(!this.open) return;

      fieldset.replaceChildren();

      let hiddens = formEl.querySelectorAll("input[type='hidden']");
      hiddens.forEach(function(hidden){
         if(options.lessInterestingHiddens.includes(hidden.name)) { return; };

         const output = document.createElement("output");
         output.title = hidden.name;
         output.textContent = `${hidden.name} = ${hidden.value}`;
         applyStyles(output,outputStyles);
         fieldset.append(output);
      });
   }
}

Initialize the widget

Here’s a simple init that adds the widget to every Marketo form:

MktoForms2.whenReady(function(mktoForm){
   let formEl = mktoForm.getFormElem()[0];
    
   initHiddenFieldsWidget(formEl);
}); 

You can also have the widget load only if a particular query param is present. That lets you have the widget “lying in wait” on your prod site, too:

MktoForms2.whenReady(function(mktoForm){
   let formEl = mktoForm.getFormElem()[0];
   
   initHiddenFieldsWidget(formEl, {
     requireQueryParam : "showHiddens"
   });
}); 

Now, https://example.com/page?utm_what=ever won’t have the widget but https://example.com/page?utm_what=ever&showHiddens will.

Non-Marketo forms, too

The widget was originally built for a Marketo Forms 2.0 project, but in fact works with any HTML form. Just pass in a <form> element.

This will attach to all generic forms in the original document (don’t use this for Marketo forms, which need MktoForms2.whenReady):

Array.from(document.forms).forEach(function(formEl){
   initHiddenFieldsWidget(formEl, {
     requireQueryParam : "showHiddens"
   });
});

Why not just switch the <input type> from hidden to text?

Because I didn’t want to disrupt the overall layout of the form. Switching the type attribute of a hidden field using JS shows the value — but it’ll also inherit the styles of regular text fields, including 2-column layouts, etc.

Notes

[1] Unless you attach a desktop debugger to the mobile browser. This sometimes proves necessary and is easy in a BrowserStack type environment. But when you’re spot-testing a physical device, that won’t help, especially not somebody else’s device thousands of miles away!