Skill Tracer
Skills › Marketing Skills › conversion-tracking › acuity-scheduling.js

acuity-scheduling.js

73 lines · skills/conversion-tracking/assets/snippets/acuity-scheduling.js at 88c79f6

1/*! Acuity Scheduling conversion detection (MIT)
2 * Detects: meeting booked. Pushes dataLayer event: acuity_appointment_scheduled
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 'use strict';
6 if (window.__ctDetectAcuity) return;
7 window.__ctDetectAcuity = true;
8 window.dataLayer = window.dataLayer || [];
9
10 // Acuity's booking form gives the page no booking event of its own. This
11 // listener pairs with a short snippet the site owner pastes into Acuity's
12 // custom conversion tracking field (the field snippet that comes with this
13 // listener). Acuity runs that snippet after each booking inside its
14 // conversion tracking frame, on
15 // https://conversion-tracking-sandbox.acuityinnovation.com, two frames below
16 // this page, and it posts the booking's ID to this page only (it names the
17 // page's own address as the target). The frame is a grandchild, so it can't
18 // be matched to an iframe on the page; it must come from Acuity's origin and
19 // sit inside this page's own frame tree.
20 var ORIGIN_PATTERN = /^https:\/\/([a-z0-9-]+\.)+acuityinnovation\.com$/;
21 var ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
22 var KIND_PATTERN = /^[A-Za-z_-]{1,32}$/;
23 var PRICE_PATTERN = /^[0-9]{1,9}(\.[0-9]{1,4})?$/;
24 var seenIds = [];
26 function isPlainObject(value) {
27 return !!value && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]';
28 }
30 // A frame inside this page has this page as its top window. A popup or a
31 // window this page opened does not.
32 function insideThisPage(source) {
33 try { return source.top === window; } catch (e) { return false; }
34 }
36 function idSeen(id) {
37 for (var i = 0; i < seenIds.length; i++) if (seenIds[i] === id) return true;
38 return false;
39 }
41 function cleanText(value, max) {
42 if (typeof value !== 'string') return '';
43 value = value.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/^\s+|\s+$/g, '');
44 return value.length > max ? value.slice(0, max) : value;
45 }
47 function onMessage(message) {
48 if (!message || !message.source || message.source === window) return;
49 if (typeof message.origin !== 'string' || !ORIGIN_PATTERN.test(message.origin)) return;
50 if (!insideThisPage(message.source)) return;
51 var data = message.data;
52 if (!isPlainObject(data) || data.source !== 'gtm-acuity-booking' || data.version !== 1) return;
53 if (typeof data.appointment_id !== 'string' || !ID_PATTERN.test(data.appointment_id)) return;
54 if (idSeen(data.appointment_id)) return;
55 var push = { event: 'acuity_appointment_scheduled', appointment_id: data.appointment_id };
56 if (typeof data.kind === 'string' && KIND_PATTERN.test(data.kind)) push.acuity_type = data.kind;
57 var type = cleanText(data.appointment_type, 200);
58 if (type) push.appointment_type = type;
59 if (typeof data.price === 'string' && PRICE_PATTERN.test(data.price)) push.price = data.price;
60 // The ID is remembered only once the push went through, so a dataLayer
61 // that throws (a broken wrapper) never escapes into the page, and a
62 // repeat of the same message can still be counted.
63 try {
64 window.dataLayer = window.dataLayer || [];
65 window.dataLayer.push(push);
66 } catch (e) {
67 return;
68 }
69 seenIds.push(data.appointment_id);
70 }
72 window.addEventListener('message', onMessage, false);
73})();