Metric Sensei tracking JavaScript API

Version: v2 (older)
Admin API docs Admin pageSupport: support@mycromsystems.com

You're reading the docs for v2, an older version that is still live. The current version is v3: read its docs.

Overview

Every website you track with Metric Sensei has one tag. Depending on the services turned on for the website it runs Analytics (page views, engagement, events, revenue) and/or Attribution (visitor IDs, marketing touches, form filling). Your website talks to both through one global function, msa(...), much like Google's gtag(...).

This page lists everything a website admin or developer can call, what each call takes and what it gives back. Calls for a service that isn't on for the website are ignored, so the same code is safe on every page.

You want toCallService
Count something that happened (sign-up, purchase, video play)msa('event', name, params)Analytics
Add the same parameters to every eventmsa('set', params)Analytics
Count a page view the URL doesn't showmsa('pageview', {path})Analytics
Read the visit's infomsa('analytics')Analytics
Read A/B test variants and feature flagsmsa('variant', key), msa('flag', key)Analytics add-on
Read the visitor's IDs and marketing touchesmsa('ready', fn), msa('get')Attribution
Put the source, campaign and IDs into your formsms_source, ms_campaign ... fieldsAttribution
Link the visitor to a known personmsa('identify', emailOrUserId)Attribution
Pass your cookie banner's answermsa('consent', {analytics, ads})Both
Show a visitor their ID for a privacy request, or clear itmsa('privacy', fn), msa('privacy', 'forget')Attribution

Install the tag

Copy your website's snippet from the admin page (the client's Websites & Apps, then the website, then Install) and put it high in the <head> of every page. It looks like this, with your site ID in place of MS-XXXXXXXX:

<script>(function(w,d,i){w.msa=w.msa||function(){(w.msa.q=w.msa.q||[]).push(arguments)};
var s=d.createElement('script');s.async=1;s.src='https://cdn.metricsensei.com/s/'+i+'.js';
d.head.appendChild(s)})(window,document,'MS-XXXXXXXX');</script>

The WordPress plugin, the Shopify app and the HubSpot and Salesforce installs add the same snippet for you. With first-party mode the script is served from your own domain instead of cdn.metricsensei.com; nothing else changes.

The tag only runs on the hosts listed for the website (its domain and the subdomains you chose), and never in automated browsers unless the website's debug switch is on.

How msa(...) calls work

The snippet defines msa at once as a small queue, then loads the real tag in the background. Calls made before the tag has loaded are kept and run, in order, as soon as it loads. So you can call msa(...) anywhere, any time, without waiting.

  • Return values only exist once the tag has loaded. A call that returns something (msa('analytics'), msa('get'), msa('variant', key), msa('flag', key)) returns undefined while it is still queued. Use the callback forms when you need the answer early: msa('ready', fn) and msa('analytics', fn).
  • Unknown commands are ignored. A command for a service that isn't on waits in the queue and is never sent.
  • Calls never throw. A bad argument is left out, and with debug on a warning is written to the browser console.

Quick start: send an event

// A sign-up, with two parameters
msa('event', 'sign_up', { method: 'Google', plan: 'pro' });

// A purchase, GA4 style: value + currency count as revenue
msa('event', 'purchase', {
  transaction_id: 'T-10293',
  value: 59.90,
  currency: 'USD',
  coupon: 'FALL10',
  items: [
    { item_id: 'SKU-1', item_name: 'Mug', price: 19.95, quantity: 2 },
    { item_id: 'SKU-7', item_name: 'Tea', price: 20.00, quantity: 1 }
  ]
});

// From a button
document.querySelector('#demo').addEventListener('click', () => msa('event', 'demo_request'));

Events show up in the client's Analytics page under Goals & events (and in Realtime within a minute). To count an event as a conversion, add an event goal with the same name in the website's Analytics settings.

msa('event', name, params)

Sends one named event for the current page and visit.

ArgumentTypeNotes
namestringRequired. 1 to 60 characters: letters, digits, spaces, _ . : / & ( ) ' # + -. Case matters (Signup and signup are two events). Names starting with $ are reserved for Metric Sensei and are ignored. GA4's snake_case names (sign_up, generate_lead, purchase) work as they are.
paramsobjectOptional. See parameters and limits.

Returns true when the event was queued to send, false when it wasn't (bad name, a $ name, a transaction_id already sent from this browser, or a page excluded in the website's settings). While the tag is still loading the call is queued and returns undefined.

When it is sent. Events are batched with page views and sent within 5 seconds, after 10 events, or when the page is hidden or closed (using navigator.sendBeacon, which survives leaving the page). You don't need to wait before navigating.

event_callback: run code after the event is handed over

As in GA4, a function in event_callback is called once the event has been handed to the browser to send. It gets true or false (whether the event was sent) and is always called, also when the event was left out.

form.addEventListener('submit', (e) => {
  e.preventDefault();
  msa('event', 'generate_lead', { form: 'contact', event_callback: () => form.submit() });
});

If the tag can't load (an ad blocker, or a call before the snippet), the callback is never called. As with GA4, add your own fallback timer when the callback continues something the visitor is waiting for: setTimeout(go, 1000).

Parameters and limits

WhatLimitWhat happens over it
Parameters per event25 (same as GA4), including the ones from msa('set')The rest are left out
Parameter name40 charactersCut to 40
Text value200 characters (GA4: 100)Cut to 200
Value typestext, number, true/falseObjects, arrays (other than items), functions, NaN, empty text and null are left out
items10 itemsThe rest are left out
Event name60 charactersCut to 60; other characters make the event invalid
Events per page batch50Sent in the next batch

Text, numbers and true/false

  • Text values are counted per value: the events report shows, for each event, how often each parameter value occurred (plan = pro: 120). Each website keeps its 300 most common event/parameter/value combinations per day.
  • Numbers are added up: the report shows, per event and parameter, how many events had it, the total and the average (duration_s: 340 events, total 51,200, average 150.6). Send IDs as text ('10293', not 10293) so they are counted rather than added up.
  • true / false are kept as the text "true" and "false".

Names with a meaning

ParameterMeaning
value or revenueMoney from this event (a positive number below 10,000,000). Counted as revenue. revenue wins if both are sent.
currencyThree-letter ISO code (USD, EUR). Revenue only counts in the website's report currency (set in its Analytics settings, USD by default); other currencies are kept in the raw data but not added to the revenue report.
transaction_idSent once per browser: the same ID again within 24 hours is not sent (stops double counting when a thank-you page is reloaded). Not shown as a value breakdown.
itemsArray of products, see ecommerce.
event_callbackFunction called after sending, see above. Never sent.

Revenue and ecommerce

Any event can carry revenue. GA4's ecommerce events work unchanged:

msa('event', 'add_to_cart', { currency: 'USD', value: 19.95,
  items: [{ item_id: 'SKU-1', item_name: 'Mug', item_category: 'Kitchen', price: 19.95, quantity: 1 }] });

msa('event', 'purchase', { transaction_id: 'T-10293', value: 59.90, currency: 'USD', shipping: 4.99, tax: 4.10,
  items: [{ item_id: 'SKU-1', item_name: 'Mug', price: 19.95, quantity: 2 }] });
Item fieldTypeNotes
item_idtext or numberAn item needs an ID or a name, else it is left out
item_nametextShown in the items report (the ID when there is no name)
item_category, item_brand, item_varianttext100 characters each; kept in the raw data
pricenumberPer unit
quantitywhole number1 when left out; 1 to 10,000

The events report shows per event and item: events, quantity and item revenue (price × quantity). Event revenue (value) drives the Revenue report by channel, campaign, entry page and event, with revenue per visit and ROAS. Revenue is not worked out from items: send value.

On Shopify and WooCommerce the store's own orders also come in from the store connection; send browser purchase events only if you want them in Analytics too.

msa('set', params)

Default parameters added to every later event on this page (GA4's gtag('set', ...)). The event's own parameters win when a name is in both. null removes a default. Defaults last until the page is left (in a single-page app, until a full reload).

msa('set', { logged_in: true, plan: 'pro' });
msa('event', 'export');                 // sent with logged_in and plan
msa('set', { plan: null });             // stop sending plan

Returns nothing. Defaults count towards the 25 parameters. They are also added to the events the tag sends by itself.

msa('pageview', options)

Page views are counted for you, including in single-page apps that change the address with history.pushState (and #/ routes when hash routes is on in the website's settings). Call this only for screens whose address doesn't change: steps of a checkout, tabs, modals that act as pages.

msa('pageview', { path: '/checkout/shipping' });
OptionNotes
pathMust start with /. Anything after ? or # is removed. Without it, the current address is used.

The same path twice in a row counts once. Later events belong to this path until the next page view.

msa('analytics', callback)

Returns the visit's Analytics info, and also calls callback with it when you pass one (use the callback if the tag may not have loaded yet).

msa('analytics', (info) => console.log(info.visitId, info.consented));
FieldTypeMeaning
siteIdstringThe website's ID (MS-...)
visitIdstringThis visit's random ID (a visit ends after 30 minutes without activity, or when a new campaign or outside site brings the visitor back)
pathstringThe page path events are counted on now
versionstringThe Analytics tag version
consentedbooleanWhether the visitor allows analytics (see consent). When false, visits are still counted, anonymously.
defaultsobjectThe parameters set with msa('set')

msa('variant', key) and msa('flag', key)

With the A/B tests & feature flags add-on. Experiments and flags are set up in the client's Analytics page (or suggested by Website Optimizer).

CallReturns
msa('variant', 'hero-test')The visitor's variant key (for example "b"), or "" when the experiment isn't running on this page. The first answer per page records that the visitor saw the experiment.
msa('flag', 'new-pricing')true or false for this visitor

Running variants also add a class to <html> right away, msa-exp-<experiment>-<variant>, so CSS can switch content without waiting for JavaScript. Variants are stable per visitor.

Events sent for you

You don't need code for these; each can be turned off in the website's Analytics settings. Don't send events with these names yourself.

EventParametersSetting
Outbound Link: Clickurl, domain, text, pageOutbound links
File Downloadurl, file, text, pageDownloads
Form: Submitform (id or name), path, action, button; never field valuesForms
Call Link: Click, Email Link: Click, SMS Link: Click, WhatsApp Link: Clicknumber or email (your own published details), text, pageContact clicks
404pathNot-found page title
Smart eventsNamed by you in the Analytics pageSmart events (rules run with no code)

Page views, time engaged, scroll depth and Core Web Vitals are measured too, and a sample of clicks for heatmaps.

msa('ready', callback) and msa('get')

With Attribution. msa('ready', fn) calls fn(state) once the visitor's IDs are known (right away if they already are). msa('get') returns the same state, or null before it's ready.

msa('ready', (s) => {
  console.log(s.browser_id, s.touches.last_marketing && s.touches.last_marketing.channel);
});
FieldMeaning
siteIdThe website's ID
browser_idThis browser's ID (32 hex characters)
device_id, person_id, household_idMatched IDs, when known (a person ID after identify or a form fill)
session_id, new_sessionAttribution's session and whether this page started it
ga_client_id, ga_session_idGA4's IDs, when GA4 runs on the page
touchesfirst_touch, last_touch, last_marketing and last_paid, each with source, medium, channel, campaign, content, term, click ID type and time

msa('identify', emailOrUserId)

Links this browser to a known person, for example after sign-in, so their visits on other devices join up. Only a SHA-256 hash of the trimmed, lower-cased value leaves the page. An email also helps Google Ads match conversions when ad consent is given. The visitor's person_id is then available from msa('get').

msa('identify', 'dana@example.com');

Fill forms with attribution data

With Attribution on, the tag writes the visitor's source, campaign and IDs into your forms, so every lead reaches your CRM or inbox with where it came from. No code is needed: add fields with the right names and the tag fills them as soon as the page is ready, again when the visitor's IDs arrive, and in forms your page adds later.

How a field is found

For each value below, the tag fills every input and textarea that has, with the default prefix ms_:

  • name="ms_source", or
  • id="ms_source", or
  • class="ms_source" on the field itself, or
  • class="ms_source" on an element around it (form builders that let you set a CSS class on a field's wrapper, such as WPForms, Gravity Forms, Elementor or HubSpot's embedded forms).
<form action="/contact" method="post">
  <input name="email" type="email">
  <input type="hidden" name="ms_source">
  <input type="hidden" name="ms_medium">
  <input type="hidden" name="ms_campaign">
  <input type="hidden" name="ms_first_touch_source">
  <input type="hidden" name="ms_browser_id">
  <button>Send</button>
</form>

<!-- A form builder that only lets you set a class on the field's wrapper -->
<div class="wpforms-field ms_campaign"><input type="hidden" name="wpforms[fields][7]"></div>

Use hidden fields where you can. In a visible field, a value the visitor typed is never replaced. The tag sets values so that React, Vue and similar libraries notice (it fires input and change). Form submits are never blocked or changed.

Field names

The unprefixed touch fields describe the visit that matters for the lead: by default the visitor's last marketing touch (their last visit that wasn't direct: from a campaign, ad, search engine, social site or referral), or their last visit when every visit was direct.

FieldValue
ms_sourceSource, e.g. google, facebook, newsletter (utm_source, or worked out from the click ID or referrer)
ms_mediumMedium, e.g. cpc, organic, email, referral
ms_channelChannel, e.g. Paid Search, Organic Search, Email, AI Assistants
ms_campaign, ms_content, ms_termutm_campaign, utm_content, utm_term
ms_utm_idutm_id
ms_source_platform, ms_creative_format, ms_marketing_tacticGA4's newer UTM tags
ms_click_id, ms_click_id_typeThe ad click ID and its type (gclid, fbclid, msclkid, ttclid ...)
ms_gclid, ms_fbclid, ms_msclkid ...The click ID under its own name, for CRMs that expect it (only the type that was clicked is filled)
ms_referrerThe referring site
ms_landing_pageThe page the visit started on
ms_<param>Ad and affiliate IDs we keep from the link (e.g. ms_irclickid), and the website's own campaign parameters (set in its tracking settings)
ms_browser_id, ms_session_idOur browser and session IDs (join the lead to the visitor's whole journey)
ms_device_id, ms_person_id, ms_household_idMatched IDs, when known
ms_ga_client_id, ms_ga_session_idGA4's IDs, when GA4 runs on the page

Fields for each touch

Every touch field above is also available for a specific touch: ms_<touch>_<field>.

TouchMeaningExample field
first_touchThe visitor's first visitms_first_touch_source
last_touchThe latest visit, whatever brought itms_last_touch_medium
first_marketingThe first visit from marketing (not direct)ms_first_marketing_campaign
last_marketingThe latest visit from marketing (not direct)ms_last_marketing_channel
last_paidThe latest paid or tagged campaign visitms_last_paid_gclid

Settings

In the website's Attribution settings in the admin page (client → Websites & Apps → the website), or through the Admin API's website settings:

SettingAPI fieldDefault
Fill forms on or offform_fillon
Which touch fills the unprefixed fieldsform_touch: default, first_touch, last_touch, first_marketing, last_marketing, last_paidlast non-direct touch
Field name prefix (e.g. lead_ makes lead_source)form_prefixms_
Extra CSS selector per field, for forms whose field names you can't changeform_selectors, e.g. {"source": "#hs_lead_source"}none
Count form submits (never their values)count_submitson

Showing values in the page

Only form fields are filled; other elements with these class names are left alone. To show a value in page text, read it yourself:

msa('ready', (s) => {
  const t = s.touches.last_marketing || s.touches.last_touch;
  document.querySelectorAll('.show-source').forEach((el) => { el.textContent = t ? t.source : ''; });
});

Form commands

CallUse
msa('fill')Fill forms again now (after your code changes a form; forms added to the page are filled anyway)
msa('managedForms', '.wpforms-form')These forms report success themselves (a CSS selector), so a submit only counts once the plugin confirms it
msa('submitted', form, { plugin: 'wpforms', form_id: 12 })The form plugin accepted this submission

The WordPress plugin, Shopify app and HubSpot and Salesforce installs set these up for their own forms.

dataLayer events

For Google Tag Manager, Attribution pushes {event: 'ms_attribution', ms_browser_id, ms_person_id, ms_session_id, ms_source, ms_medium, ms_channel, ms_campaign, ms_touches, ...} on each page (and again after identify, with ms_reason: 'identify'). The event and dataLayer names can be changed in the website's settings.

msa('debug')

msa('debug') turns Attribution's console log on for this page (msa('debug', false) off). The website's debug switch in the admin turns it on for every visitor and also makes Analytics warn in the console about parameters it left out. Turn it off again when you're done.

Forwarding to other trackers

Every named event the Analytics tag records (your msa('event') calls, the events sent for you and smart events) is also sent to the other trackers on your page, so you can use it there without extra code. The copy is named ms_ + the event name, made safe for GA4: lower case, letters, digits and _, 40 characters at most. Signup becomes ms_signup and Call Link: Click becomes ms_call_link_click. It's on by default. To turn it off, clear "Send our events to this site's other trackers" in the website's Analytics settings, or set forward_events: false in the website's settings.analytics with the Admin API.

TrackerHow the event is sentNeeds ads consent
Google Analytics 4 (gtag.js)gtag('event', 'ms_signup', params), with itemsNo
Google Tag ManagerdataLayer.push({event: 'ms_signup', ...params, ecommerce: {items}}), only when a GTM container is loaded; use a Custom Event triggerNo
Meta Pixelfbq('trackCustom', 'ms_signup', params)Yes
TikTok Pixelttq.track('ms_signup', params)Yes
Microsoft Advertising (UET)uetq.push('event', 'ms_signup', {event_category: 'metric_sensei', event_label, revenue_value, currency})Yes
Reddit Pixelrdt('track', 'Custom', {customEventName: 'ms_signup', value, currency})Yes
Klaviyoklaviyo.push(['track', 'ms_signup', params])Yes
PostHog, Mixpanel, Amplitude, Segment, Heap, UmamiTheir capture / track call with the parametersNo
Matomo_paq.push(['trackEvent', 'Metric Sensei', 'ms_signup', label, value])No
Plausibleplausible('ms_signup', {props: params})No
Fathom, Microsoft Clarity, HotjarThe event name onlyNo
  • Not sent: page views (those tools count their own), time engaged, scroll depth, speed and heatmap clicks, and events whose name already starts with ms_. A tracker also doesn't get our automatic events that it records by itself: GA4 and Tag Manager (enhanced measurement), PostHog, Heap, Amplitude, Clarity and Hotjar (autocapture) get no Outbound Link: Click, File Download or Form: Submit, and Matomo no outbound or download clicks. Your own events, smart events, contact link clicks and 404 go to every tracker. An event a tracker sends back to msa while it's being forwarded isn't sent on again, so a GTM tag that calls msa('event', ...) can't make a loop.
  • Consent: nothing is sent unless the visitor allows analytics (see consent). The ad platforms (Meta, TikTok, Microsoft Advertising, Reddit, Klaviyo) also need ads consent: ad_storage (and ad_user_data when set) not denied in Google consent mode, or ads: true in msa('consent', ...) with a banner. They never get events when the browser sends Global Privacy Control.
  • Parameters: the event's own (up to 25, values cut to 100 characters, names made GA4-safe). Anything that looks like an email address or phone number is left out, and so are the number and email of contact link clicks. The event itself still goes. Visitor and visit IDs are never sent.
  • Double counting: if you already send the same event to a tracker yourself, you'll see it twice there, once under your name and once as ms_…. Use one of them, or turn forwarding off. With both gtag.js and Google Tag Manager on the page, a GTM tag that sends every custom event to GA4 would also double it.
  • The forwarding code is a small extra file next to the tag, loaded the first time an event is forwarded, so a Content-Security-Policy that allows the tag allows it too. With the website's debug switch on, the console says where each event went.

msa('privacy', callback) and msa('privacy', 'forget')

For your privacy page. msa('privacy', fn) calls fn({ browser_id }) once the visitor's ID is known, so you can show it. A visitor who asks you to see or delete their data can give you that ID, which proves the request comes from their browser; log it under Settings › Privacy requests (or the Admin API's privacy-requests). msa('privacy', 'forget') clears our cookies and storage on this browser, including the cookie our server keeps on your own tracking domain. It doesn't delete what was already collected: use a privacy request for that.

msa('privacy', function (p) {
  document.getElementById('my-id').textContent = p.browser_id;
});
document.getElementById('forget-me').onclick = function () { msa('privacy', 'forget'); };

Personal data

Don't send names, email addresses, phone numbers or other personal data as event parameters. As a safety net, text that looks like an email address or a phone number (with a leading + or the (555) 555-5555 shape) is replaced with (redacted) on our servers before it is stored. The exceptions are the events our tag makes from your own page: the contact link clicks and your smart events, which carry the phone number, address or link text published on your site. Plain digit strings such as order IDs are kept. Use msa('identify') for linking people; it only sends a hash.

The tag never sends URL query strings (except campaign parameters such as utm_* and click IDs), form field values, full referrer URLs or IP addresses.

Events forwarded to other trackers never carry anything that looks like an email address or phone number, the contact clicks' own included.

Recipes

Moving from GA4's gtag

// gtag('event', 'sign_up', { method: 'Google' });
msa('event', 'sign_up', { method: 'Google' });

// gtag('set', { user_type: 'member' });
msa('set', { user_type: 'member' });

Both can run side by side; with GA4 linked, the Analytics page can show GA4's numbers next to ours or blended. GA4 also gets each of our events as ms_sign_up (forwarding), so once you've moved an event to msa you can drop its gtag call and use ms_sign_up in GA4.

Google Tag Manager

Put the snippet in a Custom HTML tag on Initialization - All Pages. For events, a Custom HTML tag with your trigger:

<script>msa('event', 'video_play', { title: {{Video Title}} });</script>

React, Vue and other single-page apps

// Page views are automatic with pushState routers. Events:
<button onClick={() => msa('event', 'upgrade_click', { from: 'banner' })}>Upgrade</button>

// TypeScript: declare the global once
declare global { interface Window { msa: (...args: unknown[]) => unknown } }

Server-side events

To send events from your server (a payment webhook, a CRM status change), use the Analytics events API with a server key created on the client's Analytics page:

curl -s -X POST https://analytics.api.metricsensei.com/v1/events \
  -H "Authorization: Bearer msk_..." -H "Content-Type: application/json" \
  -d '{"events": [{"name": "purchase", "visitor_id": "<browser_id from msa(\"get\")>",
       "props": {"transaction_id": "T-10293"}, "revenue": 59.9, "currency": "USD",
       "items": [{"item_id": "SKU-1", "price": 19.95, "quantity": 2}]}]}'

Up to 100 events per call, each up to 7 days old, with the same parameter rules as the browser. Pass Attribution's browser_id as visitor_id so browser and server events join up.

Where events show up

  • Analytics page: Goals & events (counts, visits, conversion rate, text values, number totals, items), Revenue, Funnels, Realtime.
  • Admin API: GET https://api.metricsensei.com/clients/{client}/analytics/sites/{site_id}/events returns rows, properties, numbers and items. See the Admin API docs.
  • API/MCP Gateway: the analytics_* tools, for AI assistants and your own code.

Reports update every 15 minutes; Realtime within a minute.

Command reference

CommandArgumentsReturnsService
eventname, params?true / falseAnalytics
setparams (null removes)nothingAnalytics
pageview{path}?nothingAnalytics
analyticscallback?info objectAnalytics
variantexperiment keyvariant key or ""Analytics add-on
flagflag keytrue / falseAnalytics add-on
consent{analytics, ads}nothingBoth
readycallback(state)nothingAttribution
getstate or nullAttribution
identifyemail or user IDnothingAttribution
fillnothingAttribution
managedFormsCSS selectornothingAttribution
submittedform, metanothingAttribution
debugfalse?nothingAttribution

Commands that return something return undefined while queued before the tag loads.

Troubleshooting

  • Nothing arrives: check the page's host is one of the website's hosts, Analytics is on for the website, and no ad blocker stops cdn.metricsensei.com (first-party mode avoids that). Your browser's Network tab shows a request to /a.
  • An event is missing: check the name (allowed characters, no leading $), and that msa('event', ...) returned true. Turn on debug to see left-out parameters.
  • A purchase counted once although the page was reloaded: that's transaction_id doing its job.
  • Numbers show as totals, not values: send IDs and codes as text.
  • A value shows as (redacted): it looked like an email address or phone number.