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. When you save a tag configuration in the dashboard, two caches stand between you and the change: the license record caches at the edge for 5 minutes, and the tag JavaScript itself is served with max-age=300, so your browser holds the old copy for another 5. Worst case, it takes ~10 minutes for the config-carrying tag to reach you.
Wait 10 minutes from saving, then hard-refresh the page in GTM preview (Ctrl+Shift+R) or tick "Disable cache" in DevTools → Network. The mtk_attribution dataLayer event should return with all your mapped fields. (This exact scenario happened on launch day — waiting was the whole fix.)
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 servedfail-open— licensing backend unreachable; tag served anyway by designnot-found/inactive/expired— key problems; a console-warn stub is served instead of the enginedomain-not-allowed— the requesting site isn't in the license's domain listwrong-product— the key belongs to a different MTK productmissing-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+
Same ~10-minute propagation as config changes: the edge caches license records for 5 minutes and browsers cache the JS for 5 more. Suspension is fully effective once those roll over — 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 + Body-End script workaround.
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:
- In the Wix form editor, set each field's Field Title to exactly the MTK variable you want it to hold — e.g.
mtk_ft_sourceormtk_session_count. The script matches fields by that title, so it must match the field-map key character-for-character. - In Wix, open Settings → Custom Code and add the script below to Body - End, loaded on all pages. It reads the
mtk_attributiondataLayer event, finds each field by its title, hides its wrapper, and writes the value through React's value setter so the form state actually accepts it.
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.
<script>
(function () {
// Every field the tool can write. Trim to just the ones you actually
// added to the form — matched against each field's title, exactly.
var FIELDS = [
// First touch (frozen, write-once)
'mtk_ft_channel', 'mtk_ft_source', 'mtk_ft_campaign', 'mtk_ft_term',
'mtk_ft_content', 'mtk_ft_placement', 'mtk_ft_landing',
// Last touch (refreshed each session)
'mtk_lt_channel', 'mtk_lt_source', 'mtk_lt_campaign', 'mtk_lt_term',
'mtk_lt_content', 'mtk_lt_placement', 'mtk_lt_landing',
// Click IDs
'mtk_lt_gclid', 'mtk_lt_gbraid', 'mtk_lt_wbraid', 'mtk_lt_msclkid',
// Journey
'mtk_touch_path', 'mtk_touch_path_json', 'mtk_session_count', 'mtk_pageview_count',
// Timing / conversion
'mtk_time_to_conversion', 'mtk_conversion_timestamp', 'mtk_conversion_datetime',
// Meta CAPI params
'mtk_fbclid', 'mtk_fbc', 'mtk_fbp'
];
var hidden = {};
function labelOf(el) {
var l = el.id && document.querySelector('label[for="' + el.id + '"]');
return ((l && l.textContent) || el.getAttribute('aria-label') || '').trim();
}
// climb to the largest wrapper that contains only this one field, hide it
function hideWrapper(el) {
var node = el;
while (node.parentElement) {
var p = node.parentElement;
if (p.tagName === 'FORM' || p.querySelectorAll('input, textarea').length > 1) break;
node = p;
}
node.style.display = 'none';
}
// set value so React's form state accepts it (not just the DOM)
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;
document.querySelectorAll('form input, form textarea').forEach(function (el) {
var key = labelOf(el);
if (FIELDS.indexOf(key) === -1) return;
if (!hidden[el.id]) { hideWrapper(el); hidden[el.id] = true; }
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>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.