Skip to content New SourceLoop MCP: chat with your attribution data in Claude, ChatGPT & Cursor
SourceLoop
Tracking pixel

Install SourceLoop in your app (SaaS, logged-in product)

Install SourceLoop on your app subdomain with the script tag or npm SDK, so signups are captured but logins, password resets, and in-app forms never count as conversions.

On this page
  1. Why the app is different
  2. Step 1: Choose script tag or npm SDK
  3. Step 2: Install in conversions mode
  4. Script tag
  5. npm SDK
  6. What the two modes actually do
  7. Step 3: Record the signup yourself
  8. Check that logins are not showing up
  9. If you want automatic capture in your app anyway
  10. Credential forms are already skipped
  11. Exclude your own forms explicitly
  12. SDK reference
  13. Browser
  14. Server, for OAuth signups and payments
  15. Your website id is not a secret
  16. Which install goes where
  17. Verify it is working
  18. Troubleshooting
  19. Checklist

If you run a SaaS product you have two different surfaces, and they need two different installs:

SurfaceExampleInstallWhat gets captured
Marketing sitewww.yoursite.comScript tag, full modePageviews, attribution, every lead form, automatically
Your appapp.yoursite.comScript tag or npm SDK, conversions modeOnly the signups and payments you record explicitly

This page covers the app. For the marketing site, see Install the tracking pixel instead, and if you have no logged-in product at all, that page is the only one you need.

Why the app is different

On a marketing site almost every form is a lead form, so capturing all of them is exactly right. Inside an app the opposite is true: most forms are product UI, and only one moment, the signup, is a conversion.

There are three specific problems, and conversions mode solves all three at once:

  1. Login forms look like lead forms. A sign-in submit and a trial signup are nearly identical in structure. The password is never collected either way, so what reaches SourceLoop is an email address, which is precisely what a lead looks like.
  2. Every internal form is a candidate. “Invite a teammate”, “change billing email”, and “contact support” are all forms with an email field in them.
  3. In-app navigation is not marketing signal. A user clicking through twenty screens of your product tells you nothing about attribution, but it does consume your pageview allowance.

Step 1: Choose script tag or npm SDK

Both send data to the same place, so pick whichever fits your stack. You can mix them: script tag on the marketing site, SDK in the app.

MethodBest forNeeds code?
Script tagAny app, including ones you cannot easily add a dependency toNo, paste one snippet
npm SDK (@sourceloop-analytics/sdk)Apps built with React, Next.js, Vue, or Node, especially when you need the server clientYes, a few lines

Step 2: Install in conversions mode

Add SourceLoop to your app’s root layout using the same websiteId as your marketing site, plus mode: 'conversions'.

Script tag

<script>
  window.SourceLoopConfig = {
    websiteId: "YOUR_WEBSITE_ID",
    mode: "conversions"
  };
</script>
<script async src="https://app.sourceloop.ai/tracking-v3.js"></script>

npm SDK

npm install @sourceloop-analytics/sdk

Call init() as early as possible, once per page load. In Next.js App Router, put it in a client component rendered from the root layout.

// app/sourceloop-init.tsx
'use client';
import { useEffect } from 'react';
import { init } from '@sourceloop-analytics/sdk';

export function SourceLoopInit() {
  useEffect(() => {
    init({ websiteId: 'YOUR_WEBSITE_ID', mode: 'conversions' });
  }, []);
  return null;
}
// app/layout.tsx
import { SourceLoopInit } from './sourceloop-init';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <SourceLoopInit />
        {children}
      </body>
    </html>
  );
}

What the two modes actually do

init({ websiteId: 'YOUR_WEBSITE_ID' });                       // 'full' — marketing site
init({ websiteId: 'YOUR_WEBSITE_ID', mode: 'conversions' });  // inside your logged-in app
full (marketing site)conversions (your app)
Pageviews and SPA navigationsCapturedOff
Automatic form captureEvery non-credential formOff
Scroll, click, and engagement eventsCapturedOff
Page speed metricsCapturedOff
Chat and embedded-widget captureCapturedOff
Visitor identity and attribution cookieKeptKept
identify(), track()AvailableAvailable

The last two rows are the important ones. Conversions mode is not “SourceLoop off”. The visitor’s identity and original marketing source are still carried into the app, so when you record a signup it still attributes back to the ad or search that started it. You are only switching off the automatic guessing.

Step 3: Record the signup yourself

Because nothing is captured automatically now, the signup is something you call explicitly. This is a feature: it fires on account creation and nowhere else, so a login can never be mistaken for one.

import { identify, track } from '@sourceloop-analytics/sdk';

// After the account is actually created — not on the login screen
identify({ email: user.email });
track('signup_completed', { plan: 'free_trial' });

For Google, GitHub, or any other social sign-in, the email only exists on your server, so this has to happen in your auth callback rather than in the browser. That case, along with Stripe and other subscription stitching, is covered step by step in Track SaaS signups, trials, and subscriptions.

Check that logins are not showing up

Sign in to your own app with a test account, then open Contacts in the SourceLoop dashboard.

  • A signup should appear as a new conversion, attributed to the source that originally brought that visitor in.
  • A login should produce no new conversion at all. The existing contact may update, which is expected and correct.

If a login is creating conversions, jump to Troubleshooting.

If you want automatic capture in your app anyway

Some teams do want the app on full mode, usually because the signup form lives on the app subdomain and they would rather not write any code. That is supported, and there are two layers of protection.

Credential forms are already skipped

Even in full mode, SourceLoop recognises credential surfaces and does not capture them:

  • Sign-in and log-in forms, in any of the common spellings
  • Password reset, forgotten password, and “set a new password” forms
  • One-time codes, 2FA, authenticator, and magic-link forms
  • Pages living at credential routes such as /login, /forgot-password, or /auth/...

The rule deliberately leans one way: when a form is genuinely ambiguous, it is captured. Missing a real signup costs you attribution you can never recover, while an occasional stray login is easy to spot and clean up.

You can make the decision unambiguous from your side, and it is worth doing:

  • Put autocomplete="new-password" on the password field of your signup form. This is the strongest possible “this is account creation” signal and it overrides everything else, including a /login URL. It is also what password managers use to offer a generated password, so it is good practice regardless.
  • Put autocomplete="current-password" on the password field of your login form.
  • Give the forms honest labels. A submit button reading “Sign in” or a form with id="login-form" is recognised; a button reading “Continue” on a page at /account is not.

Exclude your own forms explicitly

For everything that is neither a credential form nor a lead, list it in the snippet. This is the tool for invite forms, settings forms, internal search, and support widgets.

<script>
  window.SourceLoopConfig = {
    websiteId: "YOUR_WEBSITE_ID",

    // Forms to ignore
    formExclusion: {
      form_ids: ["invite-teammate", "billing-email"],
      form_classes: ["settings-form"],
      form_patterns: ["^admin-", "-internal$"],
      field_patterns: ["^invite_", "coupon_code"]
    },

    // Whole pages to ignore
    excludedPagePatterns: ["/settings", "/admin", "/billing"]
  };
</script>
<script async src="https://app.sourceloop.ai/tracking-v3.js"></script>
OptionMatches againstMatching
form_idsThe form’s idExact
form_classesOne of the form’s classesExact
form_patternsThe form’s id or classRegex
field_patternsThe name of any field in the formRegex
excludedPagePatternsThe current URLRegex, skips the page entirely

excludedPagePatterns is the blunt one: a matching page sends nothing at all, not even a pageview. Use it for whole areas of your product that have no marketing meaning.

SDK reference

One package gives you both a browser client and a server client.

  • Browser code: import { ... } from '@sourceloop-analytics/sdk'
  • Server code: import { ... } from '@sourceloop-analytics/sdk/server'

The server client requires Node 18+ (it uses the built-in fetch).

Browser

import { identify, track, reset, checkoutMetadata } from '@sourceloop-analytics/sdk';

identify({ email: user.email });               // on login/signup (idempotent)
track('signup_completed', { plan: 'pro' });    // a conversion you want recorded
reset();                                        // on logout
const meta = checkoutMetadata();                // { sourceloop_anonymous_id } for client checkouts

Server, for OAuth signups and payments

The server client reads the visitor’s id from the request cookie and binds it. This is the reliable way to attribute things that happen on your backend (Google/GitHub login, Stripe checkout).

import { Sourceloop, getAnonymousId, checkoutMetadata } from '@sourceloop-analytics/sdk/server';

const sl = new Sourceloop({ websiteId: 'YOUR_WEBSITE_ID' });

const anonymousId = getAnonymousId(req);  // reads the _sl_aid cookie off the request
await sl.identify({ anonymousId, email: user.email });
await sl.track({ anonymousId, email: user.email, eventName: 'signup_completed' });

Full examples for NextAuth, Clerk, Supabase Auth, and Stripe are in Track SaaS signups, trials, and subscriptions.

Your website id is not a secret

Paste your websiteId straight into the code, both in the browser and on the server. It is already visible in your site’s tracking snippet, so there is nothing to hide and no environment variable to set up. (If you prefer env vars for managing multiple environments you still can, but it is optional.)

Which install goes where

WhereInstallWhat it captures
Marketing site (WordPress/Webflow/…)Script tag, full modepageviews, forms, attribution
App frontend (React/Next/Vue)Script tag or @sourceloop-analytics/sdk, mode: 'conversions'identify on login, explicit signup conversions, client checkout metadata
App backend (Node/Next API routes)@sourceloop-analytics/sdk/serverOAuth signups, server-side checkout stitching, server conversions

You do not need a separate SourceLoop account or website id per surface. What changes between surfaces is the mode, not the id.

Verify it is working

  1. Open your app, then open the browser DevTools, Application, Cookies. You should see a _sl_aid cookie set on your root domain (e.g. .yoursite.com).
  2. Trigger a test signup and confirm the conversion appears in your dashboard.
  3. Trigger a test login and confirm no new conversion appears.

Troubleshooting

Logins are creating conversions. Switch that surface to mode: 'conversions'. If you need to stay on full mode, add autocomplete="current-password" to the login form’s password field and give the form a recognisable id such as login-form.

Signups stopped being captured after I labelled my login form. Your signup form is probably being caught by a shared attribute or a shared route. Add autocomplete="new-password" to the signup password field, which overrides every other signal, and confirm the submit button does not read “Sign in”.

In-app forms like “invite a teammate” are showing as leads. Either move to conversions mode, or add the form to formExclusion. Matching a field name via field_patterns is usually the most durable, since ids and classes change when components are restyled.

My pageview count is much higher than my traffic. Full mode is installed inside your app and in-app navigation is being counted. Conversions mode fixes this.

No conversions at all after switching to conversions mode. Expected until you add the calls. Conversions mode captures nothing on its own, so identify() and track() from Step 3 have to be wired up.

No _sl_aid cookie. The snippet is not on the page, or websiteId is wrong. View source and confirm the snippet is present with the correct id.

@sourceloop-analytics/sdk import errors on the server. Make sure you are on Node 18+ and importing server helpers from @sourceloop-analytics/sdk/server, not the package root.

Marketing visits and app signups appear as two different people. Almost always a different websiteId between the two surfaces. If your app is on a separate root domain instead of a subdomain, see Cross-domain and subdomain tracking.

Checklist

  1. Marketing site on the standard snippet, full mode.
  2. App on the same websiteId, mode: 'conversions'.
  3. identify() on login and signup, track('signup_completed') at account creation only.
  4. Social sign-ins and subscription revenue wired up per Track SaaS signups, trials, and subscriptions.
  5. Test login produces no conversion, test signup does.

Frequently asked questions

  1. Do I need this page, or is the tracking pixel enough?

    If you only have a marketing site, the tracking pixel page is enough and you can ignore this one. You need this page if you also have a logged-in product where people sign up and log in, because the default install would treat your login and in-app forms as conversions and count in-app navigation against your pageview allowance.

  2. Will SourceLoop count my login form as a conversion?

    No. The tracker recognises credential surfaces (sign-in, password reset, OTP, magic link) and skips them on every install. It errs toward capturing when a form is genuinely ambiguous, because missing a real signup is worse than letting one login through. If a login form on your app still slips through, conversions mode turns off automatic form capture entirely.

  3. What is the difference between full mode and conversions mode?

    Full mode is the marketing-site install. It captures pageviews, engagement, and every non-credential form automatically. Conversions mode is the app install. It captures nothing automatically, only the identify and track calls you write yourself, so in-app navigation and internal forms never become pageviews or conversions.

  4. Do I need a separate SourceLoop account or website id for my app?

    No. One website id covers your marketing site and your app, including subdomains like www.yoursite.com and app.yoursite.com. Use the same websiteId in the script tag and in the SDK so the journey stays as one visitor. What changes between the two surfaces is the mode, not the id. You find your websiteId in the dashboard under Settings, Tracking Code.

  5. Is my website id a secret?

    No. Paste it straight into your code, in the browser and on the server. It is already visible in your site's tracking snippet, so there is nothing to hide and no environment variable to set up. If you prefer env vars to manage multiple environments you still can, but it is optional.

  6. Can I use the script tag and the npm SDK together?

    Yes. They send data to the same place, so a common setup is the script tag on your marketing site and the SDK in your app. Just use the same websiteId everywhere.

  7. Will in-app browsing use up my pageview allowance?

    Not in conversions mode. Pageviews, SPA navigations, engagement events, and web vitals are all switched off, so a user clicking around inside your product sends nothing to SourceLoop until you explicitly record a conversion.

  8. What Node version does the server client need?

    Node 18 or newer. The server client uses the built-in fetch, which is available from Node 18. Import server helpers from "@sourceloop-analytics/sdk/server", not the package root.

Track every conversion to its true source

Capture and send full attribution data from every signup, lead, booking, and sale to your CRM and ad platforms, so you know exactly what's driving revenue.

Without SourceLoop

Untagged

Kayden Floyd

kayden@abc.com

  • SourceUnknown
  • MediumUnknown
  • CampaignUnknown
  • Landing pageUnknown
Journey
No touchpoints captured

With SourceLoop

Auto-tagged

Kayden Floyd

kayden@abc.com · Acme Co.

  • Channel Paid Social
  • CampaignFree_demo
  • Landing page/pricing
Journey
Synced to HubSpot Google Ads Meta