Skill Tracer
Skills › Marketing Skills › conversion-tracking › hubspot-forms.js

hubspot-forms.js

160 lines · skills/conversion-tracking/assets/snippets/hubspot-forms.js at 88c79f6

1/*! HubSpot Forms conversion detection (MIT)
2 * Detects: form submission. Pushes dataLayer event: hubspot_form_submitted
3 * From the conversion-tracking skill in Marketing Skills */
4(function () {
5 if (window.__ctDetectHubspotForms) return;
6 window.__ctDetectHubspotForms = true;
7 window.dataLayer = window.dataLayer || [];
8
9 // Updated-editor conversions are keyed by HubSpot's documented conversion
10 // ID. Legacy messages use event-object identity because their documented
11 // callback payload has no conversion ID.
12 var seenConversions = {};
13 var seenLegacyEvents = typeof WeakSet === 'function' ? new WeakSet() : [];
15 function fireConversion(formId, conversionId, acceptedContext) {
16 var id = formId ? String(formId) : '';
17 if (typeof conversionId !== 'string' || !conversionId) return false;
18 var key = '$' + conversionId;
19 if (seenConversions[key]) return false;
20 seenConversions[key] = true;
21 var handled = false;
22 var adapter = window.__ctHubspotMethod2Handler;
23 if (typeof adapter === 'function') {
24 try {
25 handled = adapter({
26 formId: id,
27 instanceId: acceptedContext && acceptedContext.instanceId || '',
28 conversionId: conversionId,
29 form: acceptedContext && acceptedContext.form || null
30 }) === true;
31 } catch (e) {
32 handled = false;
33 }
34 }
35 // The push is contained, and a conversion whose push failed is
36 // forgotten, so a repeat of HubSpot's event can still count it.
37 if (!handled) {
38 try {
39 window.dataLayer = window.dataLayer || [];
40 window.dataLayer.push({ event: 'hubspot_form_submitted', form_id: id });
41 } catch (e) {
42 delete seenConversions[key];
43 return false;
44 }
45 }
46 return true;
47 }
49 function claimLegacyEvent(event) {
50 if (!event || typeof event !== 'object') return false;
51 if (seenLegacyEvents && typeof seenLegacyEvents.has === 'function') {
52 if (seenLegacyEvents.has(event)) return false;
53 seenLegacyEvents.add(event);
54 return true;
55 }
56 if (seenLegacyEvents.indexOf(event) !== -1) return false;
57 seenLegacyEvents.push(event);
58 return true;
59 }
61 function releaseLegacyEvent(event) {
62 try {
63 if (typeof seenLegacyEvents.delete === 'function') { seenLegacyEvents.delete(event); return; }
64 var i = seenLegacyEvents.indexOf(event);
65 if (i !== -1) seenLegacyEvents.splice(i, 1);
66 } catch (e) {}
67 }
69 // ------------------------------------------------------------
70 // Path 1: current forms editor (live-verified)
71 // ------------------------------------------------------------
72 // HubSpot dispatches this on the .hs-form-frame container and lets it bubble
73 // (or on document itself when it cannot find the container), with
74 // detail = { formId, instanceId }. It only ever follows a submission
75 // HubSpot's server accepted. Listening on document in the capture phase
76 // catches both dispatch points and cannot be silenced by page code that
77 // stops the event's propagation further down.
78 function hubspotFormsApi() {
79 return window.HubSpotFormsV4 || window.HubspotFormsV4 || null;
80 }
82 function handleSubmissionSuccess(event) {
83 try {
84 var detail = event && event.detail;
85 if (!detail || typeof detail.formId !== 'string' || !detail.formId) return;
86 if (typeof detail.instanceId !== 'string' || !detail.instanceId) return;
87 // When HubSpot's forms API is on the page, the event must name a form
88 // instance HubSpot actually rendered here. A stray or malformed event
89 // with a made-up instance id is ignored.
90 var api = hubspotFormsApi();
91 if (!api || typeof api.getFormFromEvent !== 'function') return;
92 var form = api.getFormFromEvent(event);
93 if (!form || typeof form.getFormId !== 'function' ||
94 typeof form.getInstanceId !== 'function' ||
95 typeof form.getConversionId !== 'function') return;
96 if (form.getFormId() !== detail.formId ||
97 form.getInstanceId() !== detail.instanceId) return;
98 // HubSpot documents getConversionId as valid after successful
99 // submission. It is the stable key for event replay and repeated submits.
100 fireConversion(detail.formId, form.getConversionId(), { form: form, instanceId: detail.instanceId });
101 } catch (e) {
102 // Never throw back into HubSpot's own event dispatch.
103 }
104 }
106 // ------------------------------------------------------------
107 // Path 2: legacy forms editor (ported from the production module)
108 // ------------------------------------------------------------
109 // Legacy forms announce each lifecycle step with a postMessage of
110 // { type: 'hsFormCallback', eventName, id, data }. Only 'onFormSubmitted'
111 // means the submission was accepted; 'onFormSubmit' also fires for
112 // submissions that then fail validation, so it is ignored.
113 //
114 // The legacy callback payload documents its form ID but not a safe mapping
115 // from a cross-origin source WindowProxy to that ID. Accept only the inline
116 // same-window callback; cross-origin iframe messages fail closed until a
117 // vendor-supported source/form binding can be verified.
118 function isTrustedOrigin(event) {
119 return !!(event && event.source === window &&
120 event.origin === window.location.origin);
121 }
123 function handleMessage(event) {
124 try {
125 // Reject an unbound source before even reading its structured-clone data.
126 if (!isTrustedOrigin(event)) return;
127 var data = event && event.data;
128 if (!data || typeof data !== 'object') return;
129 if (data.type !== 'hsFormCallback') return;
130 if (data.eventName !== 'onFormSubmitted') return;
131 var formId = data.id;
132 if (typeof formId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(formId)) return;
133 if (!claimLegacyEvent(event)) return;
134 // The documented legacy callback runs after persistence, but has no
135 // stable conversion ID. The local guard only recognizes replay of this
136 // exact MessageEvent object; a distinct event object may be a replay or a
137 // new conversion, so cross-event deduplication is not claimed.
138 var handled = false;
139 var adapter = window.__ctHubspotMethod2Handler;
140 if (typeof adapter === 'function') {
141 try {
142 handled = adapter({ formId: formId, legacyEvent: event }) === true;
143 } catch (e) { handled = false; }
144 }
145 if (!handled) {
146 try {
147 window.dataLayer = window.dataLayer || [];
148 window.dataLayer.push({ event: 'hubspot_form_submitted', form_id: formId });
149 } catch (e) {
150 releaseLegacyEvent(event);
151 }
152 }
153 } catch (e) {
154 // Never throw back into the page's own message handling.
155 }
156 }
158 document.addEventListener('hs-form-event:on-submission:success', handleSubmissionSuccess, true);
159 window.addEventListener('message', handleMessage, false);
160})();