Sign in
Attribution docs
Install guide

Formik

Formik owns the form state, so the cleanest install reads the attribution values in JavaScript and sends them with the lead — no hidden inputs to fight over. If you'd rather the values live in Formik's valuesalongside the visitor's answers, hidden <Field>s named to the field map work too: the tag writes through React's native setter, so Formik accepts the fill. Both are below.

1

Load the tag in the app shell

The loader goes in the HTML document's <head>, not inside a component — it has to run on the very first page the visitor lands on, before React renders anything. Where that lives depends on the toolchain:

  • Vite / Create React App — paste it into index.html inside <head>.
  • Next.js (App Router) — add a next/script with strategy="beforeInteractive" to the root layout. Next injects it into <head> of the server HTML on every route. On the Pages Router the same element goes in pages/_document.js.
<script src="https://attr.boggsmtk.com/v1/tag.js?k=YOUR_LICENSE_KEY"></script>
// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        {/* beforeInteractive is always injected into <head>, on every route */}
        <Script
          src="https://attr.boggsmtk.com/v1/tag.js?k=YOUR_LICENSE_KEY"
          strategy="beforeInteractive"
        />
      </body>
    </html>
  );
}

Swap YOUR_LICENSE_KEYfor the client's key from the dashboard. On the first full page load the tag reads the landing URL (UTMs, click IDs, referrer), resolves the journey, pushes the mtk_attribution dataLayer event and stores everything in localStorage. Client-side route changes don't reload the page, and don't need to: the stored journey carries across them, a form that mounts later is filled the moment it appears, and window.mtkFormSubmit()reads the live store whenever it's called.

Put it in the <head> directly, not in Google Tag Manager. When GTM is blocked — an ad blocker, a privacy browser, a consent banner the visitor never accepts — nothing inside the container fires, and a tag that lives in GTM never loads. That lead arrives with empty attribution fields. Loaded straight from the <head>, the tag runs whether or not GTM does. A GTM Custom HTML tag on all pages still works if the <head> is genuinely off-limits, but treat it as the fallback, and expect to lose the leads whose browser blocks the container.

2

Get the values onto the lead — pick the method that fits

A · Merge in onSubmit (recommended) — Formik calls onSubmitonly after validation passes, and hands you the values object you're about to send. Read the attribution field set there with window.mtkFormSubmit() and spread it into the payload. Nothing touches the DOM, nothing depends on React accepting a write, and a blocked tag simply means the lead arrives without attribution instead of not arriving.

import { Formik, Form, Field } from "formik";

// Read the full attribution field set at the moment of submit. The tag exposes
// window.mtkFormSubmit on every page; it resolves live, so the conversion
// timestamps reflect the actual submit, not page load. Returns {} when the tag
// never loaded (ad blocker, script error) so the lead still goes through.
function mtkAttribution(formName) {
  if (typeof window === "undefined" || typeof window.mtkFormSubmit !== "function") {
    return {};
  }
  // Drop the dataLayer "event" key — everything else is an mtk_* field
  const { event, ...fields } = window.mtkFormSubmit({ form_name: formName });
  return fields;
}

export function ContactForm() {
  return (
    <Formik
      initialValues={{ name: "", email: "" }}
      onSubmit={async (values, { setSubmitting, resetForm }) => {
        // Attribution rides along on the same record as the lead
        const lead = { ...values, ...mtkAttribution("contact") };
        const res = await fetch("/api/lead", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(lead),
        });
        setSubmitting(false);
        if (res.ok) resetForm();
      }}
    >
      <Form>
        <Field name="name" />
        <Field name="email" type="email" />
        <button type="submit">Send</button>
      </Form>
    </Formik>
  );
}

window is undefined during a server render, which is why the helper checks for it: only touch the tag inside onSubmit, an event handler, or a useEffect. The keys on the returned object are the HTML Names from the field map (mtk_first_source, mtk_last_channel, …), so the record you store reads the same as any other MTK install.

mtkFormSubmit() also pushes the mtk_form_submit dataLayer event — a conversion signal for GTM → GA4 / Meta CAPI / Google — at the moment you call it, which here is post-validation but before the API answers. If the site already fires its own confirmed-success conversion event, or you need the signal gated on the response, read the values off the mtk_attribution dataLayer event for the payload instead (the mtkFields() helper in the Custom HTML guide) and call mtkFormSubmit() once, after res.ok. Leave Emit form-submit event (emitOnSubmit) off in the tag config either way, so a single conversion is never counted twice.

B · Hidden <Field>s by name— if the submit handler isn't yours to edit, or you want the values visible in Formik's values for validation or debugging, render one hidden <Field> per value and name it the HTML Namefrom the field map. The tag fills it on mount and again at submit (in the capture phase, before Formik's handler runs), writing through React's native value setter and firing input / changeso Formik's handleChange records the value.

import { Formik, Form, Field } from "formik";

// The HTML Names you want on the lead — copy them from the field map.
const MTK_FIELDS = [
  "mtk_first_channel",
  "mtk_first_source",
  "mtk_first_campaign",
  "mtk_last_channel",
  "mtk_last_source",
  "mtk_last_campaign",
  "mtk_gclid",
  "mtk_conversion_datetime",
];

// Every key must exist in initialValues, or Formik ignores the input entirely.
const mtkInitial = Object.fromEntries(MTK_FIELDS.map((key) => [key, ""]));

export function ContactForm() {
  return (
    <Formik
      initialValues={{ name: "", email: "", ...mtkInitial }}
      onSubmit={async (values) => {
        // values.mtk_first_source etc. are already populated by the tag
        await fetch("/api/lead", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(values),
        });
      }}
    >
      <Form>
        <Field name="name" />
        <Field name="email" type="email" />
        {/* The tag writes each value through React's native setter and fires
            input/change, so Formik's handleChange records it in `values`. */}
        {MTK_FIELDS.map((key) => (
          <Field key={key} type="hidden" name={key} />
        ))}
        <button type="submit">Send</button>
      </Form>
    </Formik>
  );
}

Two things to keep straight: every MTK key has to be in initialValues(Formik drops inputs it wasn't told about), and the keys must match the field map character for character — a misspelled name fills nothing and reports no error. If the tag never loads, a name-matched field submits empty, which is the graceful failure you want. The [mtk:…] token as an initial value works too, but a blocked tag would then submit the raw token text, so prefer names here.

3

Deploy and verify

Deploy, then land on the site with a test URL like ?utm_source=googleads&utm_medium=cpc, navigate to the form the way a visitor would, and submit. In DevTools → Network, open the request your onSubmit sends and confirm the mtk_* keys are in the body with the values you expect; then confirm they arrive wherever leads are stored. Running window.mtkFormSubmit() in the console shows exactly what the tag would return right now. In local development, note that every hot reload is a full page load and counts as a pageview, so test the journey fields against a deployed build.

Where the names come from:Dashboard → the client's license → Tag config → Field map — the HTML Name column (copy button on each cell) is both the key on the object mtkFormSubmit() returns and the name a hidden <Field> needs. Formik inside a cross-origin iframecan't see the parent page's journey — if the frame's domain is yours, install the tag there too (see Forms in a cross-domain iframe). Full field reference at Developer docs; stuck? Troubleshooting.