Troubleshooting

Single-Page Applications (SPAs)

SourceTag works with single-page applications built on React, Vue, Next.js, Nuxt, SvelteKit, Angular, and similar frameworks. The script runs once on page load and stores attribution data in a cookie. From there, you can access the data via the JavaScript API.

How SourceTag behaves in an SPA

When the initial page loads, SourceTag:

  1. Reads the URL parameters and referrer
  2. Categorises the visitor into a channel
  3. Stores the attribution data in the _sourcetag cookie
  4. Looks for any <form> elements on the page and populates hidden fields
  5. Sets up a MutationObserver to watch for forms added later

In a traditional multi-page site, every page navigation triggers a full page load and the script runs again. In an SPA, client-side navigation (using the router) does not trigger a full page load. The script only runs on the initial hard load.

This means:

  • Attribution data is captured on the first page load. Whatever UTMs, click IDs, or referrer data are present on the entry URL are stored in the cookie. This is correct behaviour, since attribution data comes from the entry point.
  • The MutationObserver keeps watching. If your SPA dynamically adds a <form> element to the DOM (e.g. rendering a contact form component), the observer will detect it and populate the hidden fields.
  • Client-side navigations are not tracked as new visits. This is intentional. A single session in an SPA is one visit, regardless of how many “pages” the user navigates to.

Using the JavaScript API

For SPAs, the recommended approach is to use the window.__sourcetag JavaScript API to read attribution data and include it in your form submissions programmatically.

React example

function ContactForm() {
  const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);

    // Read attribution data from SourceTag
    const tt = window.__sourcetag;
    if (tt) {
      const fc = tt.getFC();
      const lc = tt.getLC();

      formData.append('st_fc_channel', fc ? fc.channel : '');
      formData.append('st_fc_detail_1', fc ? fc.d1 : '');
      formData.append('st_fc_detail_2', fc ? fc.d2 : '');
      formData.append('st_lc_channel', lc ? lc.channel : '');
      formData.append('st_lc_detail_1', lc ? lc.d1 : '');
      formData.append('st_lc_detail_2', lc ? lc.d2 : '');
      formData.append('st_visits', tt.getVisits());
      formData.append('st_days_to_convert', tt.getDaysToConvert());
    }

    await fetch('/api/contact', {
      method: 'POST',
      body: formData,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="name" placeholder="Your name" required />
      <input type="email" name="email" placeholder="Your email" required />
      <textarea name="message" placeholder="Your message" />
      <button type="submit">Send</button>
    </form>
  );
}

Vue example

export default {
  methods: {
    async handleSubmit() {
      const formData = {
        name: this.name,
        email: this.email,
        message: this.message,
      };

      // Add attribution data
      const tt = window.__sourcetag;
      if (tt) {
        const fc = tt.getFC();
        const lc = tt.getLC();
        formData.st_fc_channel = fc ? fc.channel : '';
        formData.st_fc_detail_1 = fc ? fc.d1 : '';
        formData.st_lc_channel = lc ? lc.channel : '';
        formData.st_lc_detail_1 = lc ? lc.d1 : '';
        formData.st_visits = tt.getVisits();
      }

      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });
    },
  },
};

Waiting for the script to load

The SourceTag script loads asynchronously, so window.__sourcetag might be undefined if your code runs before the script finishes loading. Use a helper function to handle this:

function withSourceTag(callback) {
  if (window.__sourcetag) {
    callback(window.__sourcetag);
    return;
  }

  var attempts = 0;
  var interval = setInterval(function () {
    attempts++;
    if (window.__sourcetag) {
      clearInterval(interval);
      callback(window.__sourcetag);
    } else if (attempts > 50) {
      clearInterval(interval); // Give up after 5 seconds
    }
  }, 100);
}

// Usage
withSourceTag(function (tt) {
  console.log('Channel:', tt.getFC().channel);
});

Installing the script in an SPA

Add the script tag to the <head> of your main HTML file:

  • Next.js: In pages/_document.js or app/layout.js, add the script to the <Head> component
  • Nuxt: In nuxt.config.js (or nuxt.config.ts), add the script to the head configuration
  • Create React App: In public/index.html, add the script tag in the <head>
  • Vite (React/Vue/Svelte): In index.html, add the script tag in the <head>
  • Angular: In src/index.html, add the script tag in the <head>
<script src="https://cdn.sourcetag.io/scripts/YOUR_SITE_ID/st.js" async></script>

Do I still need hidden form fields?

If your SPA renders standard <form> elements, SourceTag’s MutationObserver will detect them and add hidden fields automatically. This works the same as on a traditional site.

If your SPA handles form submissions entirely in JavaScript (using fetch or axios without a <form> element), hidden fields aren’t relevant. Use the JavaScript API approach shown above instead.

Server-side rendering (SSR) and static site generation (SSG)

Frameworks like Next.js, Nuxt, and SvelteKit can render pages on the server before sending them to the browser. The SourceTag script only runs in the browser (it needs access to document.cookie, window.location, and document.referrer), so SSR doesn’t affect its behaviour.

The script tag in your HTML is sent to the browser, the browser loads and runs the script, and everything works as normal.

Further reading