Tracking script MauticJS (mtc.js)
Mautic provides a means for Plugins to inject custom JavaScript into mtc.js, the PHP generated script that manages Mautic’s tracking pixel and Dynamic Web Content.
You can embed mtc.js in third party websites to manage communication between those and Mautic.
Note
For basic guidance on how to implement mtc.js on your website, please visit the Mautic User Documentation.
mtc.js
<?php
namespace Mautic\PageBundle\EventListener;
use Mautic\CoreBundle\CoreEvents;
use Mautic\CoreBundle\Event\BuildJsEvent;
use Mautic\PageBundle\Event\TrackingEvent;
use Mautic\PageBundle\PageEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TrackingSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
CoreEvents::BUILD_MAUTIC_JS => ['onBuildJs', 0],
PageEvents::ON_CONTACT_TRACKED => ['onContactTracked', 0],
];
}
public function onBuildJs(BuildJsEvent $event)
{
$event->appendJs(
<<<JS
document.addEventListener('mauticPageEventDelivered', function(e) {
var detail = e.detail;
if (detail.response && detail.response.events && detail.response.events.tracked) {
console.log(detail.response.events.tracked);
}
});
JS
);
}
public function onContactTracked(TrackingEvent $event)
{
$contact = $event->getContact();
$response = $event->getResponse();
$response->set(
'tracked',
[
'email' => $contact->getEmail()
]
);
}
}
To inject custom JavaScript into mtc.js, use an Event Listener for the CoreEvents::BUILD_MAUTIC_JS event.
This event receives a Mautic\CoreBundle\Event\BuildJsEvent object where $event->appendJs($js, $sectionName); can be used to inject the script’s code.
Note
appendJs() still works but now delegates to appendJsForScope() with BuildJsScope::TRACKING, so Mautic treats code appended this way as TRACKING scope and includes it only in tracking-enabled builds - including /mtc.js and /mautic-tracking.js - but excludes it from /mautic-essential.js. See Script scopes and split scripts.
Warning
Note that the code that triggers the tracking call to Mautic has a priority of -255. Thus, any listener to this event should use a priority greater than -255.
Warning
Only use native JavaScript or <a href=”#mauticjs-api-functions”>MauticJS API functions</a> since jQuery and other libraries aren’t guaranteed to be available in third party websites.
Script scopes and split scripts
Mautic generates its JavaScript under a scope model defined by Mautic\CoreBundle\Event\BuildJsScope, an enum with three cases:
RUNTIME- the anonymous bootstrap runtime that the other scopes depend on. It performs no tracking.ESSENTIAL- pre-consent features that need no identity, such as preserving existing Dynamic Content fallback content without making a new request, and initializing Forms already embedded in that fallback content.TRACKING- the identity and tracking code, including the tracking pixel and the/mtc/eventcall.
When you subscribe to CoreEvents::BUILD_MAUTIC_JS, the BuildJsEvent exposes which scopes the current build accepts. Its constructor accepts an array $acceptedScopes that defaults to all three cases - [BuildJsScope::RUNTIME, BuildJsScope::ESSENTIAL, BuildJsScope::TRACKING] - so a single subscriber can contribute code to more than one generated script depending on the scope it targets.
Warning
A subscriber that only calls the legacy appendJs() now contributes TRACKING scoped code and is therefore excluded from /mautic-essential.js. If a Plugin’s code must run in the essential, pre-consent context, the subscriber must call appendJsForScope() with BuildJsScope::ESSENTIAL. It can also gate on acceptsScope() first to skip building a payload the build would discard.
Mind the argument positions when migrating: the legacy appendJs($js, $section) takes the section name as the 2nd argument, whereas appendJsForScope($js, BuildJsScope $scope, $section = '') inserts the scope as the 2nd argument and moves the section name to the 3rd. A mechanical find-and-replace that keeps the old argument order would pass the section string where the scope now goes.
appendJsForScope($js, BuildJsScope $scope, $section = '')
Appends code for a specific scope. If the current build doesn’t accept that scope, the call is a no-op and returns the event without appending anything. Use it when a Plugin needs to inject code into the essential, pre-consent build rather than the tracking layer.
<?php
namespace MauticPlugin\HelloWorldBundle\EventListener;
use Mautic\CoreBundle\CoreEvents;
use Mautic\CoreBundle\Event\BuildJsEvent;
use Mautic\CoreBundle\Event\BuildJsScope;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class EssentialSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
CoreEvents::BUILD_MAUTIC_JS => ['onBuildJs', 0],
];
}
public function onBuildJs(BuildJsEvent $event)
{
$event->appendJsForScope(
<<<JS
MauticJS.log('essential runtime loaded');
JS,
BuildJsScope::ESSENTIAL,
'essentialGreeting'
);
}
}
acceptsScope(BuildJsScope $scope): bool
Reports whether the current build accepts a given scope. Call it to return early from a subscriber when the build doesn’t include the scope you target, so you avoid building a payload that Mautic would discard.
<?php
namespace MauticPlugin\HelloWorldBundle\EventListener;
use Mautic\CoreBundle\CoreEvents;
use Mautic\CoreBundle\Event\BuildJsEvent;
use Mautic\CoreBundle\Event\BuildJsScope;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TrackingLoggerSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
CoreEvents::BUILD_MAUTIC_JS => ['onBuildJs', 0],
];
}
public function onBuildJs(BuildJsEvent $event)
{
if (!$event->acceptsScope(BuildJsScope::TRACKING)) {
return;
}
$event->appendJsForScope(
<<<JS
document.addEventListener('mauticPageEventDelivered', function (e) {
MauticJS.log(e.detail);
});
JS,
BuildJsScope::TRACKING,
'trackingLogger'
);
}
}
Generated scripts and endpoints
Mautic serves the scopes through separate endpoints so that a site can load only the code it needs before consent, then add the tracking layer later. Each endpoint path resolves against your Mautic instance’s base URL.
Endpoint |
Included scopes |
Purpose |
|---|---|---|
|
|
Anonymous runtime, Dynamic Content fallback handling, and initializing Forms already embedded in fallback content. No tracking. |
|
|
The identity and tracking layer. |
|
|
The legacy aggregate script. Unchanged. |
Client-side runtime globals
The split scripts expose a small set of client-side global variables and an event so that code can detect which layers have loaded:
MauticJS.runtimeReady- set totrueonce the runtime bootstrap has loaded. Tracking code guards on it before running.MauticJS.trackingEnabled-falsein the essential or runtime build andtrueonce the tracking layer loads.MauticJS.requestWithCredentials-falseby default in the essential or runtime build andtrueonce tracking loads.mauticEssentialReady- a convention event, not something the generated runtime emits on its own. The authoritative readiness flag isMauticJS.runtimeReady; the consent-managed essential loader snippet - the copy-paste snippet that loads/mautic-essential.js- dispatchesmauticEssentialReadyonceMauticJS.runtimeReady === trueby callingMauticJS.dispatchEvent('mauticEssentialReady'). That helper builds a nativeCustomEventand dispatches it ondocument, so browser-side code following the split-script loader pattern can rely on it as a readiness hook and listen withdocument.addEventListener('mauticEssentialReady', ...).
Because the essential script may have finished loading before your code runs - in which case the event has already fired and a late listener would never run - guard on MauticJS.runtimeReady first and run immediately when it’s already true, falling back to the listener only when the runtime isn’t ready yet. The shipped tracking add-on snippet follows this same dual path. This handles both cases safely:
if (window.MauticJS && MauticJS.runtimeReady === true) {
// essential runtime is already ready - run now
MauticJS.log('runtime ready');
} else {
document.addEventListener('mauticEssentialReady', function () {
// essential runtime is now ready - run here
MauticJS.log('runtime ready');
});
}
The mauticEssentialReady event comes only from the shipped consent-managed loader snippet, not from /mautic-essential.js itself. If you use a custom loader instead, run your code from your own script tag’s load or onload callback once MauticJS.runtimeReady === true, or dispatch an equivalent event yourself.
Hooking into the tracking process and returning custom responses
If you need to do something during the request to track the Contact through /mtc/event, or append to the payload returned to the tracking code which you can leverage by custom JavaScript injected through CoreEvents::BUILD_MAUTIC_JS, subscribe to the PageEvents::ON_CONTACT_TRACKED event.
The listener can inject a custom payload through the Mautic\PageBundle\Event\TrackingEvent::set method.
This will expose the payload to the tracking code’s mauticPageEventDelivered event in the detail.response.events object. See the PHP code example.
JavaScript Form processing hooks
if (typeof MauticFormCallback == 'undefined') {
var MauticFormCallback = {};
}
MauticFormCallback['replaceWithFormName'] = {
onValidateEnd: function (formValid) {
// before Form submit
},
onResponse: function (response) {
// after Form submit
}
};
If you wish to run additional code before or after submission of the Form, create a MauticFormCallback object.
In the example code, replace replaceWithFormName with the name of your Form.
onValidateEnd and onResponse are actions called by Form.customCallbackHandler.
onValidate()
Called before built-in Form validation. Implement this callback to override the built-in Form validation logic.
Your callback’s return value determines the processing of the Form:
Return
Trueto skip the built-in Form validation and continue with Form processing.Return
Falseto skip the built-in Form validation and prevent the Form submission.Return
nullto execute built-in Form validation and let its logic determine whether to continue with or prevent the Form submission.
Returning True or False skips the execution of onValidateStart.
MauticFormCallback['replaceWithFormName'] = {
onValidate: function () {
// executed before built-in Form validation
var formIsGood = True;
var dontUpdate = False;
if(dontUpdate){
return null;
}else if(formIsGood){
return True;
}else if(!formIsGood){
return False;
}
},
};
onValidateStart()
Called at the beginning of the default Form validation, this receives no values and a return value isn’t required and isn’t processed.
Warning
onValidateStart isn’t executed if you add the onValidate callback and it returns True or False.
MauticFormCallback['replaceWithFormName'] = {
onValidateStart: function () {
// executed before built-in Form validation
},
};
onValidateEnd(formValid)
Called after all Form validations are complete - either the default validations and/or the onValidate callback - and before submitting the Form.
Receives formValid to determine if the Form is valid.
If this callback returns False then this prevents submitting the Form.
MauticFormCallback['replaceWithFormName'] = {
onValidateEnd: function (formValid) {
// before Form submit
// return False; // prevents submitting the Form
},
};
onErrorMark(callbackData)
Called during error marking. It receives a callbackData object. Return True to skip the default error marking.
var callbackData = {
containerId: containerId,
valid: valid,
validationMessage: callbackValidationMessage
};
MauticFormCallback['replaceWithFormName'] = {
onErrorMark: function (callbackData) {
// called during error marking
},
};
onErrorClear(containerId)
Called to clear an existing error. Receives containerId with the id of the element containing the error. Return True to skip the default error clearing.
MauticFormCallback['replaceWithFormName'] = {
onErrorClear: function (containerId) {
// called to clear an existing error
},
};
onResponse(response)
Called prior to default Form submission response processing. Receives response containing the Form submission response.
Return True to skip the default Form submission response processing.
MauticFormCallback['replaceWithFormName'] = {
onResponse: function (response) {
// called to process the response to the Form submission
},
};
onResponseStart(response)
Called at the beginning of the default Form submission response processing. Receives response containing the Form submission response.
Return value isn’t required and isn’t processed.
Warning
onResponseStart may not get executed if the default response processing gets handled during the onResponse callback
MauticFormCallback['replaceWithFormName'] = {
onResponseStart: function (response) {
// called to process the response to the Form submission
},
};
onResponseEnd(response)
MauticFormCallback['replaceWithFormName'] = {
onResponseEnd: function (response) {
// called to process the response to the Form submission
},
};
Called at the end of the default Form submission response processing. Receives response containing the Form submission response.
Return value isn’t required and isn’t processed.
Warning
onResponseEnd may not get executed if the default response processing gets handled during the onResponse callback
onMessageSet(messageObject)
Called prior to default message insertion. Receives a messageObject containing the message and message type.
Return True to skip the default message insertion.
var messageObject = {
message: message,
type: type
};
MauticFormCallback['replaceWithFormName'] = {
onErrorMark: function (messageObject) {
// called prior to default message insertion
},
};
onShowNextPage()
Called prior to going to the next page in the Form. Useful to adjust the DOM prior to making the page visible.
MauticFormCallback['replaceWithFormName'] = {
onShowNextPage: function (pageNumber) {
// called prior to going to the next page
},
};
onShowPreviousPage()
Called prior to going back to a previous page in the Form. Useful to adjust the DOM prior to making the page visible.
MauticFormCallback['replaceWithFormName'] = {
onShowPreviousPage: function (pageNumber) {
// called prior to going back to previous page
},
};
MauticJS API functions
MauticJS.serialize(object)
This method transforms an object properties into a key=value string, concatenating them with an ampersand.
It’s used when submitting data via MauticJS.makeCORSRequest.
var obj = {firstname: "John", lastname: "Doe"};
var serialized = MauticJS.serialize(obj);
alert(serialized); // Shows "firstname=John&lastname=Doe"
MauticJS.documentReady(functionName|function)
This method validates if the document has finished rendering, then executes the given function. The function argument can be the name of a function or an anonymous function.
function test() {
alert('test');
}
MauticJS.documentReady(test);
MauticJS.iterateCollection(collection)(functionName|function)
This method iterates over the provided collection, which can be an array, object, HTMLCollection, etc. - it uses the provided function argument.
The function argument can be the name of a function or an anonymous function. The function receives the collection node and the iteration number as arguments.
var videos = document.getElementsByTagName('video');
// Add a custom data attribute to all videos
MauticJS.iterateCollection(videos)(function(node, i) {
node.dataset.customAttribute = 'test';
});
MauticJS.log(arguments)
This method is a lightweight wrapper around console.log. It exists because some browsers don’t provide this feature.
It takes any number of arguments, logs them, then passes those same arguments to the console.log method if it exists.
MauticJS.log('Something happened');
MauticJS.createCORSRequest(method, url)
This method creates an XMLHttpRequest, then checks to see if it supports the withCredentials property.
If not, the User is probably on Windows, so it then checks for the existence of XDomainRequest, then creates it if found.
Finally, it opens then returns the XHR. You can use that to send cross-domain requests that include the cookies for the domain.
It’s used internally within the MauticJS.makeCORSRequest method.
MauticJS.createCORSRequest('GET', 'https://mymautic.com/dwc/slot1');
MauticJS.makeCORSRequest(method, url, data, callbackSuccess, callbackError)
This method uses MauticJS.createCORSRequest to open a cross domain request to the specified URL, then sets the callbackSuccess and callbackError values accordingly.
You may omit either of the callbacks. If you do, the callbacks get replaced with a basic function that uses MauticJS.log(response) to log the response from the request.
The callback methods receive the server response and the XHR object as arguments.
If the response is a JSON string, it’s automatically parsed to a JSON object.
The data argument gets serialized using MauticJS.serialize(data), then sent with the request to the server.
All requests made this way have the X-Requested-With header set to XMLHttpRequest.
MauticJS.makeCORSRequest('GET', 'https://mymautic.com/dwc/slot1', [], function (response, xhr) {
if (response.success) {
document.getElementById('slot1').innerHTML = response.content;
}
});
MauticJS.parseTextToJSON(maybeJSON)
This method takes a text string and verifies whether it’s a valid JSON string. If so, it parses it into a JSON object and returns. If not, then it simply returns the argument passed to it.
var text = '{"firstname": "John", "lastname": "Doe"}';
var json = MauticJS.parseTextToJSON(text);
alert(json); // Will show [object Object]
var text = 'not valid json';
var json = MauticJS.parseTextToJSON(text);
alert(json); // Will show 'not valid json'
MauticJS.insertScript(scriptUrl)
This method inserts a script tag with the provided URL in the head of your document, before other scripts.
MauticJS.insertScript('http://google.com/ga.js');