@mahe_pkm/buzl-capi 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,475 @@
1
+ /**
2
+ * =========================================================================
3
+ * BUZL UNIVERSAL TRACKING & FORM DISPATCHER RUNTIME
4
+ * Headless SDK for GTM, Meta Pixel/CAPI, Google Sheets, & Zoho CRM
5
+ * =========================================================================
6
+ */
7
+ (function (root, factory) {
8
+ if (typeof define === 'function' && define.amd) {
9
+ define([], factory);
10
+ } else if (typeof module === 'object' && module.exports) {
11
+ module.exports = factory();
12
+ } else {
13
+ root.BuzlTracker = factory();
14
+ }
15
+ }(typeof self !== 'undefined' ? self : this, function () {
16
+ 'use strict';
17
+
18
+ var VERSION = '1.0.0';
19
+
20
+ function win() { return typeof window !== 'undefined' ? window : {}; }
21
+ function doc() { return typeof document !== 'undefined' ? document : {}; }
22
+ function nav() { return typeof navigator !== 'undefined' ? navigator : {}; }
23
+
24
+ function getCookie(name) {
25
+ try {
26
+ var m = doc().cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/+^])/g, '\\$1') + '=([^;]*)'));
27
+ return m ? decodeURIComponent(m[1]) : '';
28
+ } catch (e) { return ''; }
29
+ }
30
+
31
+ function sessionGet(key) {
32
+ try { return (win().sessionStorage && win().sessionStorage.getItem(key)) || ''; }
33
+ catch (e) { return ''; }
34
+ }
35
+
36
+ function sessionSet(key, val) {
37
+ try { if (win().sessionStorage) win().sessionStorage.setItem(key, val); }
38
+ catch (e) {}
39
+ }
40
+
41
+ function getFbclid() {
42
+ try {
43
+ return new URLSearchParams(win().location.search).get('fbclid') || sessionGet('fbclid') || '';
44
+ } catch (e) { return ''; }
45
+ }
46
+
47
+ function getFbc() {
48
+ var fbc = getCookie('_fbc');
49
+ if (fbc) return fbc;
50
+ var fbclid = getFbclid();
51
+ return fbclid ? 'fb.1.' + Date.now() + '.' + fbclid : '';
52
+ }
53
+
54
+ function getFbp() {
55
+ return getCookie('_fbp') || '';
56
+ }
57
+
58
+ function getDomainLabel() {
59
+ try {
60
+ var host = (win().location.hostname || '').trim().toLowerCase();
61
+ host = host.replace(/^www\./, '');
62
+ host = host.replace(/[^a-z0-9_-]/g, '-').slice(0, 63);
63
+ return host || 'website';
64
+ } catch (e) {
65
+ return 'website';
66
+ }
67
+ }
68
+
69
+ function generateLeadId() {
70
+ var uuid = '';
71
+ try {
72
+ if (win().crypto && win().crypto.randomUUID) {
73
+ uuid = win().crypto.randomUUID();
74
+ } else if (win().crypto && win().crypto.getRandomValues) {
75
+ var b = win().crypto.getRandomValues(new Uint8Array(16));
76
+ b[6] = (b[6] & 0x0f) | 0x40;
77
+ b[8] = (b[8] & 0x3f) | 0x80;
78
+ var h = [];
79
+ for (var i = 0; i < 16; i++) h.push((b[i] + 0x100).toString(16).slice(1));
80
+ uuid = h[0] + h[1] + h[2] + h[3] + '-' + h[4] + h[5] + '-' + h[6] + h[7] + '-' + h[8] + h[9] + '-' + h[10] + h[11] + h[12] + h[13] + h[14] + h[15];
81
+ }
82
+ } catch (e) {}
83
+
84
+ if (!uuid) {
85
+ uuid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 10);
86
+ }
87
+ return getDomainLabel() + '-' + uuid;
88
+ }
89
+
90
+ function captureUtm() {
91
+ try {
92
+ if (!sessionGet('buzl_landing_page')) {
93
+ sessionSet('buzl_landing_page', win().location.href);
94
+ sessionSet('buzl_initial_referrer', doc().referrer || '');
95
+ }
96
+
97
+ var params = new URLSearchParams(win().location.search);
98
+ var keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid'];
99
+ keys.forEach(function (k) {
100
+ var v = params.get(k);
101
+ if (v) sessionSet(k, v);
102
+ });
103
+ } catch (e) {}
104
+ }
105
+
106
+ function getUtm() {
107
+ return {
108
+ source: sessionGet('utm_source'),
109
+ medium: sessionGet('utm_medium'),
110
+ campaign: sessionGet('utm_campaign'),
111
+ term: sessionGet('utm_term'),
112
+ content: sessionGet('utm_content'),
113
+ fbclid: sessionGet('fbclid'),
114
+ gclid: sessionGet('gclid')
115
+ };
116
+ }
117
+
118
+ function createTracker(userConfig) {
119
+ var cfg = userConfig || {};
120
+ var googleSheetUrl = cfg.googleSheetUrl || '';
121
+ var gtmEvent = cfg.gtmEvent || 'lead_form_submitted';
122
+ var safetyTimeoutMs = typeof cfg.safetyTimeoutMs === 'number' ? cfg.safetyTimeoutMs : 800;
123
+ var zoho = cfg.zoho || {};
124
+ var buzlCapi = cfg.buzlCapi || {};
125
+ var whatsapp = cfg.whatsapp || {};
126
+
127
+ captureUtm();
128
+
129
+ /* 1. Push to GTM dataLayer */
130
+ function pushGTM(leadId, payload) {
131
+ if (!cfg.enableGTM) return;
132
+ if (!win().dataLayer) win().dataLayer = [];
133
+ try {
134
+ win().dataLayer.push({
135
+ event: gtmEvent,
136
+ leadId: leadId,
137
+ contact: payload.contact,
138
+ utm: payload.utm,
139
+ pageUrl: win().location.href
140
+ });
141
+ } catch (e) {
142
+ console.warn('[BuzlTracker] GTM push error', e);
143
+ }
144
+ }
145
+
146
+ /* 2. Track with Meta Pixel */
147
+ function trackMeta(leadId, payload) {
148
+ if (!cfg.enableMeta || typeof win().fbq !== 'function') return;
149
+ try {
150
+ var customData = {
151
+ content_name: payload.source || 'Lead Form',
152
+ buzl_lead_id: leadId,
153
+ utm_source: payload.utm.source,
154
+ utm_medium: payload.utm.medium,
155
+ utm_campaign: payload.utm.campaign
156
+ };
157
+ win().fbq('trackCustom', 'formSubmitted', customData, { eventID: leadId });
158
+ if (cfg.trackMetaLeadEvent) {
159
+ win().fbq('track', 'Lead', customData, { eventID: leadId });
160
+ }
161
+ } catch (e) {
162
+ console.warn('[BuzlTracker] Meta track error', e);
163
+ }
164
+ }
165
+
166
+ /* 3. Sync to Google Sheets */
167
+ function syncToGoogleSheet(leadId, payload) {
168
+ if (!googleSheetUrl) return Promise.resolve();
169
+ return new Promise(function (resolve) {
170
+ var sheetPayload = {
171
+ timestamp: new Date().toISOString(),
172
+ leadId: leadId,
173
+ name: payload.contact.name || '',
174
+ phone: payload.contact.phone || '',
175
+ email: payload.contact.email || '',
176
+ location: payload.contact.location || cfg.siteLocation || '',
177
+ siteLocation: cfg.siteLocation || '',
178
+ source: payload.source || 'Website Form',
179
+ utm: payload.utm,
180
+ fbclid: getFbclid(),
181
+ fbc: getFbc(),
182
+ fbp: getFbp(),
183
+ eventSourceUrl: win().location.href,
184
+ landingPageUrl: sessionGet('buzl_landing_page') || win().location.href,
185
+ initialReferrer: sessionGet('buzl_initial_referrer') || '',
186
+ pageTitle: (doc().title || '').slice(0, 100),
187
+ pagePath: win().location.pathname || '',
188
+ userAgent: nav().userAgent || '',
189
+ rawFields: payload.rawFields || {}
190
+ };
191
+
192
+ // Attach dynamic fields directly for top-level access
193
+ if (payload.rawFields) {
194
+ for (var rk in payload.rawFields) {
195
+ if (sheetPayload[rk] === undefined) {
196
+ sheetPayload[rk] = payload.rawFields[rk];
197
+ }
198
+ }
199
+ }
200
+
201
+ try {
202
+ win().fetch(googleSheetUrl, {
203
+ method: 'POST',
204
+ mode: 'no-cors',
205
+ headers: { 'Content-Type': 'application/json' },
206
+ body: JSON.stringify(sheetPayload),
207
+ keepalive: true
208
+ }).then(function () { resolve({ status: 'sent' }); })
209
+ .catch(function () { resolve({ status: 'error' }); });
210
+ } catch (e) {
211
+ resolve({ status: 'exception' });
212
+ }
213
+ });
214
+ }
215
+
216
+ /* 4. Sync to Zoho CRM Web-to-Lead */
217
+ function syncToZoho(payload) {
218
+ if (!zoho.endpoint || !zoho.xnQsjsdp) return Promise.resolve();
219
+ return new Promise(function (resolve) {
220
+ try {
221
+ var fd = new FormData();
222
+ fd.append('xnQsjsdp', zoho.xnQsjsdp);
223
+ if (zoho.xmIwtLD) fd.append('xmIwtLD', zoho.xmIwtLD);
224
+ fd.append('actionType', zoho.actionType || 'TGVhZHM=');
225
+
226
+ var fMap = zoho.fields || {};
227
+ var nameField = fMap.lastName || 'Last Name';
228
+ var phoneField = fMap.phone || 'Phone';
229
+ var emailField = fMap.email || 'Email';
230
+ var locationField = fMap.location || 'City';
231
+ var sourceField = fMap.source || 'Lead Source';
232
+
233
+ if (payload.contact.name) fd.append(nameField, payload.contact.name);
234
+ if (payload.contact.phone) fd.append(phoneField, payload.contact.phone);
235
+ if (payload.contact.email) fd.append(emailField, payload.contact.email);
236
+ if (payload.contact.location) fd.append(locationField, payload.contact.location);
237
+ if (payload.source) fd.append(sourceField, payload.source);
238
+
239
+ if (nav().sendBeacon && nav().sendBeacon(zoho.endpoint, fd)) {
240
+ resolve({ status: 'beacon_sent' });
241
+ } else {
242
+ win().fetch(zoho.endpoint, { method: 'POST', body: fd, mode: 'no-cors', keepalive: true })
243
+ .then(function () { resolve({ status: 'sent' }); })
244
+ .catch(function () { resolve({ status: 'error' }); });
245
+ }
246
+ } catch (e) {
247
+ resolve({ status: 'exception' });
248
+ }
249
+ });
250
+ }
251
+
252
+ /* 5. Sync to Server-Side Buzl CAPI */
253
+ function syncToBuzlCAPI(leadId, payload) {
254
+ if (!buzlCapi.endpoint || !buzlCapi.authUser) return Promise.resolve();
255
+ return new Promise(function (resolve) {
256
+ try {
257
+ var capiData = {
258
+ leadId: leadId,
259
+ domain: getDomainLabel(),
260
+ eventName: 'Lead',
261
+ eventTime: Math.floor(Date.now() / 1000),
262
+ actionSource: 'website',
263
+ eventSourceUrl: win().location.href,
264
+ landingPageUrl: sessionGet('buzl_landing_page') || win().location.href,
265
+ initialReferrer: sessionGet('buzl_initial_referrer') || '',
266
+ contact: payload.contact,
267
+ source: payload.source,
268
+ fbc: getFbc(),
269
+ fbp: getFbp(),
270
+ fbclid: getFbclid(),
271
+ userAgent: nav().userAgent || '',
272
+ utm: payload.utm
273
+ };
274
+
275
+ win().fetch(buzlCapi.endpoint, {
276
+ method: 'POST',
277
+ headers: {
278
+ 'Content-Type': 'application/json',
279
+ 'Authorization': 'Basic ' + win().btoa(buzlCapi.authUser + ':' + (buzlCapi.authPass || ''))
280
+ },
281
+ body: JSON.stringify(capiData),
282
+ keepalive: true
283
+ }).then(function () { resolve({ status: 'capi_sent' }); })
284
+ .catch(function () { resolve({ status: 'capi_error' }); });
285
+ } catch (e) {
286
+ resolve({ status: 'capi_exception' });
287
+ }
288
+ });
289
+ }
290
+
291
+ /* Extract contact inputs from a Form element */
292
+ function extractFormFields(form) {
293
+ var data = {};
294
+ var elements = form.elements || [];
295
+ for (var i = 0; i < elements.length; i++) {
296
+ var el = elements[i];
297
+ if (!el.name || el.type === 'submit' || el.type === 'button') continue;
298
+ data[el.name] = el.value;
299
+ }
300
+
301
+ // Semantic inference
302
+ var name = data.name || data.fullName || data.bizName || data['first-name'] || data['Last Name'] || '';
303
+ var phone = data.phone || data.mobile || data.tel || data.bizPhone || data['Phone'] || '';
304
+ var email = data.email || data.mail || data['Email'] || '';
305
+ var location = data.location || data.city || data.bizLocation || data['City'] || cfg.siteLocation || '';
306
+
307
+ return {
308
+ contact: { name: name, phone: phone, email: email, location: location },
309
+ rawFields: data
310
+ };
311
+ }
312
+
313
+ /* Main Submit Handler */
314
+ function submitLead(opts) {
315
+ opts = opts || {};
316
+ var leadId = generateLeadId();
317
+ var utm = getUtm();
318
+ var contact = opts.contact || {};
319
+ var source = opts.source || 'Website Form';
320
+ var payload = {
321
+ leadId: leadId,
322
+ contact: contact,
323
+ source: source,
324
+ utm: utm,
325
+ rawFields: opts.rawFields || {}
326
+ };
327
+
328
+ // 1. GTM & Meta client triggers
329
+ pushGTM(leadId, payload);
330
+ trackMeta(leadId, payload);
331
+
332
+ // 2. Race async endpoints against safety timeout
333
+ var safetyTimer = new Promise(function (res) { setTimeout(res, safetyTimeoutMs); });
334
+ var networkPromises = Promise.all([
335
+ syncToGoogleSheet(leadId, payload),
336
+ syncToZoho(payload),
337
+ syncToBuzlCAPI(leadId, payload)
338
+ ]);
339
+
340
+ return Promise.race([networkPromises, safetyTimer]).then(function () {
341
+ // Redirection or follow-up
342
+ var targetWa = opts.whatsappNumber || resolveFormWhatsapp(null, null) || cleanPhone(whatsapp.number || '');
343
+ if (targetWa && opts.redirect !== false) {
344
+ var textTmpl = whatsapp.template || 'Hi, I submitted an inquiry from {name} in {location}.';
345
+ var msg = textTmpl
346
+ .replace(/{name}/g, contact.name || 'my business')
347
+ .replace(/{location}/g, contact.location || '')
348
+ .replace(/{phone}/g, contact.phone || '');
349
+ var waUrl = 'https://wa.me/' + targetWa + '?text=' + encodeURIComponent(msg);
350
+ win().location.href = waUrl;
351
+ }
352
+ return { leadId: leadId, success: true };
353
+ });
354
+ }
355
+
356
+ /* Helper: Sanitize phone digits */
357
+ function cleanPhone(raw) {
358
+ if (!raw) return '';
359
+ var digits = String(raw).replace(/\D/g, '');
360
+ if (digits.length === 10 && /^[6-9]/.test(digits)) return '91' + digits;
361
+ if (digits.length === 11 && digits.indexOf('0') === 0) return '91' + digits.slice(1);
362
+ return digits;
363
+ }
364
+
365
+ /* Helper: Extract WhatsApp number from DOM element */
366
+ function extractWaFromElement(el) {
367
+ if (!el) return '';
368
+ var attr = el.getAttribute('data-whatsapp') ||
369
+ el.getAttribute('data-wa') ||
370
+ el.getAttribute('data-phone') ||
371
+ el.getAttribute('data-buzl-wa') ||
372
+ el.getAttribute('data-number');
373
+ if (attr) return cleanPhone(attr);
374
+
375
+ var href = el.getAttribute('href') || '';
376
+ var waMeM = href.match(/wa\.me\/(\+?\d+)/i);
377
+ if (waMeM) return cleanPhone(waMeM[1]);
378
+
379
+ var apiM = href.match(/phone=(\+?\d+)/i);
380
+ if (apiM) return cleanPhone(apiM[1]);
381
+
382
+ var telM = href.match(/tel:(\+?\d+)/i);
383
+ if (telM) return cleanPhone(telM[1]);
384
+
385
+ return '';
386
+ }
387
+
388
+ /* Dynamic WhatsApp Number Resolution for a submitted form & its submit button */
389
+ function resolveFormWhatsapp(form, submitter) {
390
+ if (submitter) {
391
+ var fromBtn = extractWaFromElement(submitter);
392
+ if (fromBtn) return fromBtn;
393
+ }
394
+
395
+ if (form) {
396
+ var btnInside = form.querySelector('button[type="submit"], button:not([type]), input[type="submit"], .btn--wa, a.btn--wa');
397
+ if (btnInside) {
398
+ var fromBtnInside = extractWaFromElement(btnInside);
399
+ if (fromBtnInside) return fromBtnInside;
400
+ }
401
+
402
+ var fromForm = extractWaFromElement(form);
403
+ if (fromForm) return fromForm;
404
+
405
+ var hiddenInput = form.querySelector('input[type="hidden"][name="whatsapp"], input[type="hidden"][name="wa"], input[type="hidden"][name="phone_to"]');
406
+ if (hiddenInput && hiddenInput.value) {
407
+ var cleanHidden = cleanPhone(hiddenInput.value);
408
+ if (cleanHidden) return cleanHidden;
409
+ }
410
+ }
411
+
412
+ if (win().activeTrigger) {
413
+ var fromTrigger = extractWaFromElement(win().activeTrigger);
414
+ if (fromTrigger) return fromTrigger;
415
+ }
416
+
417
+ var pageWaLink = doc().querySelector('a[href*="wa.me/"], [data-whatsapp]');
418
+ if (pageWaLink) {
419
+ var fromPageLink = extractWaFromElement(pageWaLink);
420
+ if (fromPageLink) return fromPageLink;
421
+ }
422
+
423
+ return cleanPhone(whatsapp.number || '');
424
+ }
425
+
426
+ /* Auto-bind to forms */
427
+ function autoBindForms() {
428
+ var forms = doc().querySelectorAll('form[data-buzl-track], form:not([data-buzl-ignore])');
429
+ forms.forEach(function (f) {
430
+ if (f._buzlBound) return;
431
+ f._buzlBound = true;
432
+ f.addEventListener('submit', function (e) {
433
+ var submitter = e.submitter || f.querySelector('button[type="submit"], button:not([type]), input[type="submit"], .btn--wa');
434
+ var targetWaNumber = resolveFormWhatsapp(f, submitter);
435
+
436
+ var extracted = extractFormFields(f);
437
+ var formSource = f.getAttribute('data-buzl-source') || f.getAttribute('id') || 'Form Submission';
438
+ submitLead({
439
+ contact: extracted.contact,
440
+ rawFields: extracted.rawFields,
441
+ source: formSource,
442
+ whatsappNumber: targetWaNumber,
443
+ redirect: f.getAttribute('data-buzl-no-redirect') !== 'true'
444
+ });
445
+ });
446
+ });
447
+ }
448
+
449
+ if (doc().readyState === 'loading') {
450
+ doc().addEventListener('DOMContentLoaded', autoBindForms);
451
+ } else {
452
+ autoBindForms();
453
+ }
454
+
455
+ return {
456
+ version: VERSION,
457
+ submitLead: submitLead,
458
+ getUtm: getUtm,
459
+ getLeadId: generateLeadId,
460
+ autoBindForms: autoBindForms
461
+ };
462
+ }
463
+
464
+ // Global auto-init if config object is defined on window
465
+ var Tracker = {
466
+ version: VERSION,
467
+ init: createTracker
468
+ };
469
+
470
+ if (typeof win().__BUZL_CONFIG__ !== 'undefined') {
471
+ win().buzl = Tracker.init(win().__BUZL_CONFIG__);
472
+ }
473
+
474
+ return Tracker;
475
+ }));
@@ -0,0 +1,149 @@
1
+ /**
2
+ * GTM & Meta Pixel Snippet Generators
3
+ */
4
+
5
+ const HEAD_START_MARKER = '<!-- BUZL_TRACKING_HEAD_START -->';
6
+ const HEAD_END_MARKER = '<!-- BUZL_TRACKING_HEAD_END -->';
7
+ const BODY_START_MARKER = '<!-- BUZL_TRACKING_BODY_START -->';
8
+ const BODY_END_MARKER = '<!-- BUZL_TRACKING_BODY_END -->';
9
+
10
+ /**
11
+ * Generate GTM & Meta Pixel head snippet
12
+ */
13
+ function generateHeadSnippet(config) {
14
+ const gtmId = (config.gtmId || '').trim();
15
+ const metaPixelId = (config.metaPixelId || '').trim();
16
+ const enableDeferred = config.enableDeferred !== false;
17
+
18
+ const lines = [HEAD_START_MARKER];
19
+
20
+ // Preconnects for performance
21
+ if (gtmId || metaPixelId) {
22
+ lines.push(' <!-- Performance Preconnects for Tracking -->');
23
+ if (gtmId) {
24
+ lines.push(' <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>');
25
+ }
26
+ if (metaPixelId) {
27
+ lines.push(' <link rel="preconnect" href="https://connect.facebook.net" crossorigin>');
28
+ }
29
+ }
30
+
31
+ if (enableDeferred) {
32
+ lines.push(' <!-- Buzl Deferred Tracking: GTM & Meta Pixel initialized on first interaction or timeout -->');
33
+ lines.push(' <script>');
34
+ lines.push(' (function() {');
35
+ lines.push(' var initialized = false;');
36
+ lines.push(' function initTrackers() {');
37
+ lines.push(' if (initialized) return;');
38
+ lines.push(' initialized = true;');
39
+ lines.push(' window.removeEventListener("scroll", initTrackers);');
40
+ lines.push(' window.removeEventListener("mousemove", initTrackers);');
41
+ lines.push(' window.removeEventListener("touchstart", initTrackers);');
42
+ lines.push('');
43
+
44
+ if (gtmId) {
45
+ lines.push(' // Google Tag Manager');
46
+ lines.push(' (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({"gtm.start":');
47
+ lines.push(' new Date().getTime(),event:"gtm.js"});var f=d.getElementsByTagName(s)[0],');
48
+ lines.push(' j=d.createElement(s),dl=l!="dataLayer"?"&l="+l:"";j.async=true;j.src=');
49
+ lines.push(' "https://www.googletagmanager.com/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);');
50
+ lines.push(` })(window,document,"script","dataLayer","${gtmId}");`);
51
+ lines.push('');
52
+ }
53
+
54
+ if (metaPixelId) {
55
+ lines.push(' // Meta Pixel Code');
56
+ lines.push(' !function(f,b,e,v,n,t,s)');
57
+ lines.push(' {if(f.fbq)return;n=f.fbq=function(){n.callMethod?');
58
+ lines.push(' n.callMethod.apply(n,arguments):n.queue.push(arguments)};');
59
+ lines.push(' if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version="2.0";');
60
+ lines.push(' n.queue=[];t=b.createElement(e);t.async=!0;');
61
+ lines.push(' t.src=v;s=b.getElementsByTagName(e)[0];');
62
+ lines.push(' s.parentNode.insertBefore(t,s)}(window, document,"script",');
63
+ lines.push(' "https://connect.facebook.net/en_US/fbevents.js");');
64
+ lines.push(` fbq("init", "${metaPixelId}");`);
65
+ lines.push(' fbq("track", "PageView");');
66
+ lines.push('');
67
+ }
68
+
69
+ lines.push(' }');
70
+ lines.push('');
71
+ lines.push(' // Debug override: trigger instantly if GTM preview / debug active');
72
+ lines.push(' var url = window.location.href;');
73
+ lines.push(' if (url.indexOf("gtm_debug=") !== -1 || url.indexOf("gtm_preview=") !== -1 || url.indexOf("gtm_auth=") !== -1) {');
74
+ lines.push(' initTrackers();');
75
+ lines.push(' } else {');
76
+ lines.push(' window.addEventListener("load", function() { setTimeout(initTrackers, 2000); });');
77
+ lines.push(' window.addEventListener("scroll", initTrackers, { passive: true });');
78
+ lines.push(' window.addEventListener("mousemove", initTrackers, { passive: true });');
79
+ lines.push(' window.addEventListener("touchstart", initTrackers, { passive: true });');
80
+ lines.push(' }');
81
+ lines.push(' })();');
82
+ lines.push(' </script>');
83
+ } else {
84
+ // Standard synchronous loading
85
+ if (gtmId) {
86
+ lines.push(' <!-- Google Tag Manager -->');
87
+ lines.push(' <script>');
88
+ lines.push(' (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({"gtm.start":');
89
+ lines.push(' new Date().getTime(),event:"gtm.js"});var f=d.getElementsByTagName(s)[0],');
90
+ lines.push(' j=d.createElement(s),dl=l!="dataLayer"?"&l="+l:"";j.async=true;j.src=');
91
+ lines.push(' "https://www.googletagmanager.com/gtm.js?id="+i+dl;f.parentNode.insertBefore(j,f);');
92
+ lines.push(` })(window,document,"script","dataLayer","${gtmId}");`);
93
+ lines.push(' </script>');
94
+ }
95
+
96
+ if (metaPixelId) {
97
+ lines.push(' <!-- Meta Pixel Code -->');
98
+ lines.push(' <script>');
99
+ lines.push(' !function(f,b,e,v,n,t,s)');
100
+ lines.push(' {if(f.fbq)return;n=f.fbq=function(){n.callMethod?');
101
+ lines.push(' n.callMethod.apply(n,arguments):n.queue.push(arguments)};');
102
+ lines.push(' if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version="2.0";');
103
+ lines.push(' n.queue=[];t=b.createElement(e);t.async=!0;');
104
+ lines.push(' t.src=v;s=b.getElementsByTagName(e)[0];');
105
+ lines.push(' s.parentNode.insertBefore(t,s)}(window, document,"script",');
106
+ lines.push(' "https://connect.facebook.net/en_US/fbevents.js");');
107
+ lines.push(` fbq("init", "${metaPixelId}");`);
108
+ lines.push(' fbq("track", "PageView");');
109
+ lines.push(' </script>');
110
+ }
111
+ }
112
+
113
+ lines.push(HEAD_END_MARKER);
114
+ return lines.join('\n');
115
+ }
116
+
117
+ /**
118
+ * Generate noscript tags for opening <body>
119
+ */
120
+ function generateBodySnippet(config) {
121
+ const gtmId = (config.gtmId || '').trim();
122
+ const metaPixelId = (config.metaPixelId || '').trim();
123
+
124
+ const lines = [BODY_START_MARKER];
125
+
126
+ if (gtmId) {
127
+ lines.push(' <!-- Google Tag Manager (noscript) -->');
128
+ lines.push(` <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${gtmId}"`);
129
+ lines.push(' height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>');
130
+ }
131
+
132
+ if (metaPixelId) {
133
+ lines.push(' <!-- Meta Pixel (noscript) -->');
134
+ lines.push(' <noscript><img height="1" width="1" style="display:none"');
135
+ lines.push(` src="https://www.facebook.com/tr?id=${metaPixelId}&ev=PageView&noscript=1" /></noscript>`);
136
+ }
137
+
138
+ lines.push(BODY_END_MARKER);
139
+ return lines.join('\n');
140
+ }
141
+
142
+ module.exports = {
143
+ HEAD_START_MARKER,
144
+ HEAD_END_MARKER,
145
+ BODY_START_MARKER,
146
+ BODY_END_MARKER,
147
+ generateHeadSnippet,
148
+ generateBodySnippet
149
+ };