Sign in
Attribution

Attribution troubleshooting

Answers to the questions that actually come up. Not finding yours? Ask in the Discord.

I removed the config script from my GTM tag and now the mtk_ variables don't show in GTM preview+

Almost always a cache-timing issue, not a breakage — though far less of one than it used to be. Saving in the dashboard now refreshes the license record at the edge straight away, so the only thing left between you and the change is your own browser, which holds the tag JavaScript for its max-age=300.

So don't wait it out: hard-refresh the page in GTM preview (Ctrl+Shift+R) or tick "Disable cache" in DevTools → Network, and the mtk_attribution dataLayer event should come back with all your mapped fields immediately. Left alone, it corrects itself within 5 minutes.

The exception is if the dashboard warned that the edge cache could not be refreshed when you saved. Then the old config really is still being served, and the fix is to save again.

To confirm the edge is already serving your config, check that the first line of the tag response is the injected config block rather than the engine comment:

curl.exe -s "https://attr.boggsmtk.com/v1/tag.js?k=YOUR_KEY" | Select-Object -First 1
# should start with:  (function(){var c={"fields":...
How do I check whether a request passed the license check?+

Every tag response carries an X-MTK-License header with the real verdict — the body is always HTTP 200 so client sites never see script errors:

  • ok — licensed and served
  • fail-open — licensing backend unreachable; tag served anyway by design
  • not-found / inactive / expired — key problems; a console-warn stub is served instead of the engine
  • domain-not-allowed— the requesting site isn't in the license's domain list
  • wrong-product — the key belongs to a different MTK product
  • missing-key — no ?k= on the request
curl.exe -s -D - -o NUL "https://attr.boggsmtk.com/v1/tag.js?k=YOUR_KEY"
I suspended a license but the tag still loads+

Suspending refreshes the edge's copy of the license immediately, so the only lag left is the visitor's own browser holding the tag JS for its max-age=300 — up to 5 minutes, and only for people who already loaded the page. After that the tag becomes a harmless stub with X-MTK-License: inactive.

The tag loads, but nothing lands on my form fields+

Three things to check, in order:

  • Field map— the license's tag configuration must map field names to data sources. No field map means the tag tracks but writes nothing.
  • Exact field names— the map's left side must match the hidden field's Nameon the form exactly (that's also the key pushed to the dataLayer). Fields that don't exist on a form are skipped silently.
  • Missing hidden inputs — either create the hidden fields on the form, or enable Auto-create hidden form fields in the tag configuration to have the tag inject them.

On Wixthis works differently — form fields are React-controlled and you can't set a hidden field's Name, so both approaches above fall short. See How do I install MTK Attribution on a Wix site? below for the field-title + companion-script workaround. And if a Wix install that used to work suddenly stops, check that the MTK fields are still set to Showin the form editor — a field toggled to Hidden is removed from the page entirely, so the script can't fill it.

How do I install MTK Attribution on a Wix site?+

Wix form fields are React-controlled, and Wix doesn't let you set a hidden field's Name— so the usual approach (map to a hidden field's Name, or Auto-create hidden form fields) doesn't stick: React reverts any value written straight to the DOM. Two steps get it working:

  1. In the Wix form editor, set each field's Field Title to exactly the MTK variable you want it to hold — e.g. mtk_first_source or mtk_session_count. The script matches fields by that title, so it must match the field-map key character-for-character. Keep the fields not required and set to Show— the script does the hiding, and a field toggled to Hidden in the form editor isn't rendered at all, so it can't be filled.
  2. In Wix, open Settings → Custom Code and add the script below to Head, loaded on all pages. It hides each titled field before the browser's first paint(a stylesheet plus a MutationObserver, so React hydration can't un-hide them and nothing flashes visible), then reads the mtk_attributiondataLayer event and writes each value through React's value setter so the form state actually accepts it. Head placement is what prevents the flash — in Body-End the script still works, but the fields show for a moment first.

The FIELDS array at the top of the script is pre-filled with every variable the tool can write. Leave it as-is, or trim it to just the field titles you actually added to the form — either way, each entry has to match a field title character-for-character.

<!-- MTK Attribution Wix Companion Script -->
  <script>
  (function () {
    // Values to keep OUT. The first two are long enough to crowd a URL past
    // its ceiling on their own, and everything inside them already rides
    // along in the individual fields. Add any field you don't want on the
    // platform — each one you drop frees room for the rest.
    var EXCLUDE = [
      'mtk_journey_json',
      'mtk_attribution_summary'
    ];

    // Only when the platform's key has to differ from the field name.
    var RENAME = {
      // 'mtk_first_channel': 'first_channel'
    };

    // The tag also publishes each value under its retired spelling so older
    // client forms keep filling. That is for forms, not for us — carrying both
    // would double the payload for nothing.
    var LEGACY_PREFIXES = ['mtk_ft_', 'mtk_lt_'];
    var LEGACY_NAMES = [
      'mtk_time_since_paid',
      'mtk_conversion_timestamp',
      'mtk_summary',
      'mtk_touch_path',
      'mtk_touch_path_json'
    ];

    // Titles the generic rule can't produce (click IDs stay lowercase).
    var TITLE_EXCEPTIONS = {"mtk_gclid":"gclid","mtk_gbraid":"gbraid","mtk_wbraid":"wbraid","mtk_msclkid":"msclkid","mtk_page_url":"Page URL","mtk_os":"OS","mtk_fbclid":"fbclid","mtk_fbc":"fbc","mtk_fbp":"fbp"};

    function isLegacy(key) {
      for (var i = 0; i < LEGACY_PREFIXES.length; i++) {
        if (key.indexOf(LEGACY_PREFIXES[i]) === 0) return true;
      }
      return LEGACY_NAMES.indexOf(key) > -1;
    }

    // 'mtk_first_channel' -> 'First Channel', matching the Field Name the
    // dashboard shows for that row.
    function titleOf(key) {
      if (TITLE_EXCEPTIONS[key]) return TITLE_EXCEPTIONS[key];
      var words = key.replace(/^mtk_/, '').split('_');
      var out = [];
      for (var i = 0; i < words.length; i++) {
        if (!words[i]) continue;
        out.push(
          words[i] === 'json'
            ? 'JSON'
            : words[i].charAt(0).toUpperCase() + words[i].slice(1)
        );
      }
      return out.join(' ');
    }

    // Order values are added in, which on a length-capped platform is also
    // drop priority: what a lead is worth least without goes last. Anything
    // not listed (a custom field, a new default) sorts after these, so a drop
    // is predictable rather than whatever order the event happened to be in.
    var PRIORITY = [
      'mtk_last_channel', 'mtk_last_source', 'mtk_last_medium', 'mtk_last_campaign',
      'mtk_gclid', 'mtk_gbraid', 'mtk_wbraid', 'mtk_msclkid',
      'mtk_first_channel', 'mtk_first_source', 'mtk_first_medium', 'mtk_first_campaign',
      'mtk_page_path', 'mtk_page_url',
      'mtk_conversion_unix', 'mtk_conversion_datetime', 'mtk_custom_conversion_time',
      'mtk_fbclid', 'mtk_fbc', 'mtk_fbp',
      'mtk_last_landing_page', 'mtk_last_landing_page_group', 'mtk_first_landing_page',
      'mtk_session_count', 'mtk_pageview_count', 'mtk_paid_touch_count',
      'mtk_last_paid_touch', 'mtk_time_since_last_paid_touch', 'mtk_time_to_conversion',
      'mtk_journey_string',
      'mtk_browser', 'mtk_os', 'mtk_device'
    ];

    // Every mtk_* value the event carries, minus the excluded and the retired.
    function keysFrom(data) {
      var out = [];
      for (var key in data) {
        if (!data.hasOwnProperty(key)) continue;
        if (key.indexOf('mtk_') !== 0) continue;
        if (isLegacy(key)) continue;
        if (EXCLUDE.indexOf(key) > -1) continue;
        out.push(key);
      }
      out.sort(function (a, b) {
        var ia = PRIORITY.indexOf(a), ib = PRIORITY.indexOf(b);
        if (ia === -1 && ib === -1) return a < b ? -1 : a > b ? 1 : 0;
        if (ia === -1) return 1;
        if (ib === -1) return -1;
        return ia - ib;
      });
      return out;
    }

    // A field answers to its HTML name or its Field Name. Every spelling of
    // the standard fields is seeded here — readable title, raw name, and the
    // retired name the engine still publishes — so the hiding CSS applies
    // before first paint; waiting for the tag's values would let a titled
    // field flash visible. The map is then extended with whatever the event
    // actually carries, so a custom field works without editing this script.
    var LABEL_TO_KEY = {};
    var SEEDED_TITLES = ["First Ad Placement","mtk_first_ad_placement","mtk_ft_placement","mtk_ft_ad_placement","Last Ad Placement","mtk_last_ad_placement","mtk_lt_placement","mtk_lt_ad_placement","First Campaign","mtk_first_campaign","mtk_ft_campaign","Last Campaign","mtk_last_campaign","mtk_lt_campaign","First Channel","mtk_first_channel","mtk_ft_channel","Last Channel","mtk_last_channel","mtk_lt_channel","First Content","mtk_first_content","mtk_ft_content","Last Content","mtk_last_content","mtk_lt_content","First Landing Page","mtk_first_landing_page","mtk_ft_landing","mtk_ft_landing_page","Last Landing Page","mtk_last_landing_page","mtk_lt_landing","mtk_lt_landing_page","First Landing Page Group","mtk_first_landing_page_group","mtk_ft_landing_page_group","Last Landing Page Group","mtk_last_landing_page_group","mtk_lt_landing_page_group","First Medium","mtk_first_medium","mtk_ft_medium","Last Medium","mtk_last_medium","mtk_lt_medium","First Query","mtk_first_query","mtk_ft_landing_query","mtk_ft_query","Last Query","mtk_last_query","mtk_lt_landing_query","mtk_lt_query","First Referrer","mtk_first_referrer","mtk_ft_referrer","Last Referrer","mtk_last_referrer","mtk_lt_referrer","First Source","mtk_first_source","mtk_ft_source","Last Source","mtk_last_source","mtk_lt_source","First Term","mtk_first_term","mtk_ft_term","Last Term","mtk_last_term","mtk_lt_term","gclid","mtk_gclid","mtk_lt_gclid","gbraid","mtk_gbraid","mtk_lt_gbraid","wbraid","mtk_wbraid","mtk_lt_wbraid","msclkid","mtk_msclkid","mtk_lt_msclkid","Journey String","mtk_journey_string","mtk_touch_path","Journey JSON","mtk_journey_json","mtk_touch_path_json","Session Count","mtk_session_count","Pageview Count","mtk_pageview_count","Paid Touch Count","mtk_paid_touch_count","Last Paid Touch","mtk_last_paid_touch","mtk_lt_paid_touch","Time Since Last Paid Touch","mtk_time_since_last_paid_touch","mtk_time_since_paid","Page Path","mtk_page_path","Page URL","mtk_page_url","Time To Conversion","mtk_time_to_conversion","Conversion Unix","mtk_conversion_unix","mtk_conversion_timestamp","Conversion Datetime","mtk_conversion_datetime","Custom Conversion Time","mtk_custom_conversion_time","Tracking Health","mtk_tracking_health","Browser","mtk_browser","OS","mtk_os","Device","mtk_device","fbclid","mtk_fbclid","fbc","mtk_fbc","fbp","mtk_fbp","Attribution Summary","mtk_attribution_summary","mtk_summary"];
    for (var si = 0; si < SEEDED_TITLES.length; si++) {
      LABEL_TO_KEY[SEEDED_TITLES[si]] = SEEDED_TITLES[si];
    }

    function learn(data) {
      // Matching a form field is not the same job as choosing a payload.
      // keysFrom() drops every retired spelling on purpose — carrying both
      // would double a length-capped URL — but the tag publishes those
      // spellings precisely so older forms keep filling, and a field titled
      // 'mtk_ft_source' is exactly that form. Match on the raw event keys, so
      // a legacy-titled field is both filled and hidden.
      for (var k in data) {
        if (!data.hasOwnProperty(k)) continue;
        if (k.indexOf('mtk_') !== 0) continue;
        LABEL_TO_KEY[k] = k;
      }
      // keysFrom() still decides which fields get a readable title, so the
      // titles stay the current ones rather than 'Ft Source'.
      var keys = keysFrom(data);
      for (var i = 0; i < keys.length; i++) {
        LABEL_TO_KEY[keys[i]] = keys[i];
        LABEL_TO_KEY[RENAME[keys[i]] || titleOf(keys[i])] = keys[i];
      }
    }

    var style = document.createElement('style');
    style.textContent =
      'input[data-mtk-hide], textarea[data-mtk-hide], [data-mtk-hidden] { display: none !important; }';
    (document.head || document.documentElement).appendChild(style);

    function labelOf(el) {
      var l = el.id && document.querySelector('label[for="' + el.id + '"]');
      return ((l && l.textContent) || el.getAttribute('aria-label') || '').trim();
    }

    function wrapperOf(el) {
      var node = el;
      while (node.parentElement) {
        var p = node.parentElement;
        if (p.tagName === 'FORM' || p.querySelectorAll('input, textarea').length > 1) break;
        node = p;
      }
      return node;
    }

    function ensureHidden(el) {
      if (!el.hasAttribute('data-mtk-hide')) el.setAttribute('data-mtk-hide', '1');
      var w = wrapperOf(el);
      if (w !== el && !w.hasAttribute('data-mtk-hidden')) w.setAttribute('data-mtk-hidden', '1');
    }

    function sweep(root) {
      if (!root || !root.querySelectorAll) return;
      var els = root.querySelectorAll('input, textarea');
      for (var i = 0; i < els.length; i++) {
        if (LABEL_TO_KEY[labelOf(els[i])]) ensureHidden(els[i]);
      }
    }

    new MutationObserver(function (muts) {
      for (var i = 0; i < muts.length; i++) {
        var m = muts[i];
        if (m.type === 'childList') {
          for (var j = 0; j < m.addedNodes.length; j++) {
            if (m.addedNodes[j].nodeType === 1) sweep(m.addedNodes[j]);
          }
        } else if (m.target && m.target.nodeType === 1) {
          sweep(m.target.parentElement || m.target);
        }
      }
    }).observe(document.documentElement, {
      childList: true, subtree: true,
      attributes: true, attributeFilter: ['style', 'class', 'aria-label']
    });
    sweep(document);

    function setReactValue(el, value) {
      var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement : HTMLInputElement;
      Object.getOwnPropertyDescriptor(proto.prototype, 'value').set.call(el, value);
      if (el._valueTracker) el._valueTracker.setValue(value === '' ? '\u0000' : '');
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
    }

    (function tick() {
      var data = (window.dataLayer || []).find(function (e) { return e.event === 'mtk_attribution'; });
      var settled = !!data;
      if (data) learn(data);            // pick up fields beyond the standard set

      document.querySelectorAll('form input, form textarea').forEach(function (el) {
        var key = LABEL_TO_KEY[labelOf(el)];
        if (!key) return;
        ensureHidden(el);
        if (data && data[key] != null) {
          if (el.value !== String(data[key])) setReactValue(el, String(data[key]));
        } else {
          settled = false;
        }
      });

      setTimeout(tick, settled ? 2000 : 150);
    })();
  })();
  </script>
<!-- End MTK Attribution Wix Companion Script -->
Does my clients' tracking break if MTK has an outage?+

No — the delivery Worker is fail-open by design. If the licensing backend is ever unreachable, the tag is served anyway (with X-MTK-License: fail-open so we can see it happened). A licensing blip never takes down tracking on a live site.

I still have the old config block pasted in GTM — will it fight the dashboard?+

Once a configuration is saved in the dashboard, it overridesany on-page config block, key for key — the dashboard is the source of truth. Page-only keys the dashboard can't store (like customResolvers functions) survive. You can leave the old block in place safely, but trimming the GTM tag down to the one-line loader keeps things tidy.