@mahe_pkm/buzl-capi 0.1.2 → 0.1.4
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.
- package/Buzl_GoogleAppsScript_Template.gs +948 -948
- package/CHANGELOG.md +118 -83
- package/LICENSE +21 -21
- package/README.md +339 -253
- package/USER_HANDBOOK.html +808 -0
- package/USER_HANDBOOK.md +842 -0
- package/assets/js/buzl-tracking.js +390 -390
- package/bin/cli.js +175 -155
- package/package.json +3 -1
- package/src/cli/terminal.js +389 -388
- package/src/core/injector.js +321 -321
- package/src/core/rollback.js +281 -281
- package/src/core/scanner.js +506 -506
- package/src/core/tester.js +666 -357
- package/src/gui/public/app.js +915 -87
- package/src/gui/public/index.html +620 -230
- package/src/gui/public/style.css +1603 -509
- package/src/gui/server.js +242 -232
- package/src/index.js +47 -47
- package/src/templates/GoogleAppsScript.gs +948 -948
- package/src/templates/buzl-tracking.js +475 -475
- package/src/templates/gtm-meta-snippets.js +149 -149
package/src/core/injector.js
CHANGED
|
@@ -1,321 +1,321 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Safe HTML & Tracking Injector
|
|
3
|
-
* Injects GTM, Meta Pixel, Form Sync runtime, and configuration
|
|
4
|
-
*/
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const {
|
|
8
|
-
HEAD_START_MARKER,
|
|
9
|
-
HEAD_END_MARKER,
|
|
10
|
-
BODY_START_MARKER,
|
|
11
|
-
BODY_END_MARKER,
|
|
12
|
-
generateHeadSnippet,
|
|
13
|
-
generateBodySnippet
|
|
14
|
-
} = require('../templates/gtm-meta-snippets');
|
|
15
|
-
const { createBackup } = require('./rollback');
|
|
16
|
-
|
|
17
|
-
const RUNTIME_START_MARKER = '<!-- BUZL_TRACKING_RUNTIME_START -->';
|
|
18
|
-
const RUNTIME_END_MARKER = '<!-- BUZL_TRACKING_RUNTIME_END -->';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Generate runtime script tag & inline configuration
|
|
22
|
-
*/
|
|
23
|
-
function generateRuntimeSnippet(config, scriptRelPath) {
|
|
24
|
-
const zohoObj = config.zoho || {};
|
|
25
|
-
const zohoXn = (config.zohoXnqsjsdp || zohoObj.xnQsjsdp || '').trim();
|
|
26
|
-
const zohoEndpoint = zohoXn ? (config.zohoEndpoint || zohoObj.endpoint || 'https://crm.zoho.in/crm/WebToLeadForm') : '';
|
|
27
|
-
const zohoXm = zohoXn ? (config.zohoXmiwtld || zohoObj.xmIwtLD || '') : '';
|
|
28
|
-
const zohoFields = zohoXn ? (config.zohoFields || zohoObj.fields || { lastName: 'Last Name', phone: 'Mobile', location: 'City' }) : {};
|
|
29
|
-
|
|
30
|
-
const clientConfig = {
|
|
31
|
-
enableGTM: !!(config.gtmId && config.gtmId.trim()),
|
|
32
|
-
gtmEvent: config.gtmEvent || 'lead_form_submitted',
|
|
33
|
-
enableMeta: !!(config.metaPixelId && config.metaPixelId.trim()),
|
|
34
|
-
trackMetaLeadEvent: !!config.trackMetaLeadEvent,
|
|
35
|
-
googleSheetUrl: config.googleSheetUrl || '',
|
|
36
|
-
siteLocation: config.siteLocation || '',
|
|
37
|
-
zoho: {
|
|
38
|
-
endpoint: zohoEndpoint,
|
|
39
|
-
xnQsjsdp: zohoXn,
|
|
40
|
-
xmIwtLD: zohoXm,
|
|
41
|
-
fields: zohoFields
|
|
42
|
-
},
|
|
43
|
-
buzlCapi: {
|
|
44
|
-
endpoint: (config.buzlCapi && config.buzlCapi.endpoint) || config.buzlCapiEndpoint || '',
|
|
45
|
-
authUser: (config.buzlCapi && config.buzlCapi.authUser) || config.buzlCapiUser || '',
|
|
46
|
-
authPass: (config.buzlCapi && config.buzlCapi.authPass) || config.buzlCapiPass || ''
|
|
47
|
-
},
|
|
48
|
-
whatsapp: {
|
|
49
|
-
number: config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '',
|
|
50
|
-
template: config.whatsappTemplate || (config.whatsapp && config.whatsapp.template) || 'Hi, I submitted an inquiry from {name} in {location}.'
|
|
51
|
-
},
|
|
52
|
-
safetyTimeoutMs: config.safetyTimeoutMs || 800
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
return [
|
|
56
|
-
RUNTIME_START_MARKER,
|
|
57
|
-
' <script id="buzl-tracking-config">',
|
|
58
|
-
` window.__BUZL_CONFIG__ = ${JSON.stringify(clientConfig, null, 2)};`,
|
|
59
|
-
' </script>',
|
|
60
|
-
` <script src="${scriptRelPath}" defer></script>`,
|
|
61
|
-
RUNTIME_END_MARKER
|
|
62
|
-
].join('\n');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Injects tracking into a single HTML string
|
|
67
|
-
*/
|
|
68
|
-
function injectHtml(htmlContent, config, scriptRelPath = 'assets/js/buzl-tracking.js') {
|
|
69
|
-
let content = htmlContent;
|
|
70
|
-
|
|
71
|
-
const hasHeadTrackers = !!((config.gtmId && config.gtmId.trim()) || (config.metaPixelId && config.metaPixelId.trim()));
|
|
72
|
-
const headSnippet = hasHeadTrackers ? generateHeadSnippet(config) : '';
|
|
73
|
-
const bodySnippet = hasHeadTrackers ? generateBodySnippet(config) : '';
|
|
74
|
-
const runtimeSnippet = generateRuntimeSnippet(config, scriptRelPath);
|
|
75
|
-
|
|
76
|
-
// 1. Head snippet replacement or injection
|
|
77
|
-
const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
|
|
78
|
-
if (headMarkerRegex.test(content)) {
|
|
79
|
-
content = content.replace(headMarkerRegex, hasHeadTrackers ? `${headSnippet}\n` : '');
|
|
80
|
-
} else if (hasHeadTrackers) {
|
|
81
|
-
if (/<\/head>/i.test(content)) {
|
|
82
|
-
content = content.replace(/<\/head>/i, `${headSnippet}\n</head>`);
|
|
83
|
-
} else if (/<head[\s>]/i.test(content)) {
|
|
84
|
-
content = content.replace(/(<head[\s>][^>]*>)/i, `$1\n${headSnippet}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 2. Body noscript snippet replacement or injection
|
|
89
|
-
const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
|
|
90
|
-
if (bodyMarkerRegex.test(content)) {
|
|
91
|
-
content = content.replace(bodyMarkerRegex, hasHeadTrackers ? `${bodySnippet}\n` : '');
|
|
92
|
-
} else if (hasHeadTrackers) {
|
|
93
|
-
if (/<body[\s>]/i.test(content)) {
|
|
94
|
-
content = content.replace(/(<body[\s>][^>]*>)/i, `$1\n${bodySnippet}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 3. Runtime script replacement or injection
|
|
99
|
-
const runtimeMarkerRegex = new RegExp(`${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}`, 'g');
|
|
100
|
-
if (runtimeMarkerRegex.test(content)) {
|
|
101
|
-
content = content.replace(runtimeMarkerRegex, runtimeSnippet);
|
|
102
|
-
} else if (/<\/body>/i.test(content)) {
|
|
103
|
-
content = content.replace(/<\/body>/i, `${runtimeSnippet}\n</body>`);
|
|
104
|
-
} else {
|
|
105
|
-
content += `\n${runtimeSnippet}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 4. Form tag attribute injection (adds data-buzl-track="true" to forms if missing)
|
|
109
|
-
content = content.replace(/<form\b(?![^>]*\bdata-buzl-)([^>]*)>/gi, (match, p1) => {
|
|
110
|
-
return `<form data-buzl-track="true"${p1}>`;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return content;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Apply injection across the whole project
|
|
118
|
-
*/
|
|
119
|
-
function applyInjection(rootDir, htmlFiles, config) {
|
|
120
|
-
// If whatsapp number is not explicitly configured, fallback to auto-detected number from buttons
|
|
121
|
-
if (!config.whatsappNumber && !(config.whatsapp && config.whatsapp.number)) {
|
|
122
|
-
try {
|
|
123
|
-
const { scanProject } = require('./scanner');
|
|
124
|
-
const scan = scanProject(rootDir);
|
|
125
|
-
if (scan && scan.detectedWhatsapp) {
|
|
126
|
-
config = Object.assign({}, config, {
|
|
127
|
-
whatsappNumber: scan.detectedWhatsapp,
|
|
128
|
-
whatsapp: Object.assign({}, config.whatsapp || {}, { number: scan.detectedWhatsapp, autoDetected: true })
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
} catch (e) {}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 1. Ensure backup first
|
|
135
|
-
const backup = createBackup(htmlFiles, rootDir);
|
|
136
|
-
|
|
137
|
-
// 2. Deploy runtime SDK script into assets/js/
|
|
138
|
-
const targetJsDir = path.join(rootDir, 'assets', 'js');
|
|
139
|
-
fs.mkdirSync(targetJsDir, { recursive: true });
|
|
140
|
-
|
|
141
|
-
const sdkSourcePath = path.join(__dirname, '..', 'templates', 'buzl-tracking.js');
|
|
142
|
-
const sdkDestPath = path.join(targetJsDir, 'buzl-tracking.js');
|
|
143
|
-
fs.copyFileSync(sdkSourcePath, sdkDestPath);
|
|
144
|
-
|
|
145
|
-
// 3. Deploy GoogleAppsScript.gs to root or reference folder
|
|
146
|
-
const gsSource = path.join(__dirname, '..', 'templates', 'GoogleAppsScript.gs');
|
|
147
|
-
const gsDest = path.join(rootDir, 'Buzl_GoogleAppsScript_Template.gs');
|
|
148
|
-
if (!fs.existsSync(gsDest)) {
|
|
149
|
-
fs.copyFileSync(gsSource, gsDest);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// 4. Process each HTML file
|
|
153
|
-
const modifiedFiles = [];
|
|
154
|
-
for (const filePath of htmlFiles) {
|
|
155
|
-
const originalHtml = fs.readFileSync(filePath, 'utf8');
|
|
156
|
-
// Calculate relative path to assets/js/buzl-tracking.js
|
|
157
|
-
let scriptRelPath = 'assets/js/buzl-tracking.js';
|
|
158
|
-
if (config && config.pathMode === 'root-absolute') {
|
|
159
|
-
scriptRelPath = '/assets/js/buzl-tracking.js';
|
|
160
|
-
} else {
|
|
161
|
-
const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
|
|
162
|
-
scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const modifiedHtml = injectHtml(originalHtml, config, scriptRelPath);
|
|
166
|
-
fs.writeFileSync(filePath, modifiedHtml, 'utf8');
|
|
167
|
-
modifiedFiles.push({
|
|
168
|
-
path: filePath,
|
|
169
|
-
relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
success: true,
|
|
175
|
-
backupDir: backup.backupDir,
|
|
176
|
-
modifiedFiles,
|
|
177
|
-
runtimeScript: path.relative(rootDir, sdkDestPath),
|
|
178
|
-
googleAppsScriptFile: path.relative(rootDir, gsDest)
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Cleanly remove all Buzl tracking tags and restore pristine HTML files
|
|
184
|
-
*/
|
|
185
|
-
function removeTracking(rootDir, htmlFiles) {
|
|
186
|
-
if (!htmlFiles || !Array.isArray(htmlFiles)) {
|
|
187
|
-
const { findHtmlFiles } = require('./scanner');
|
|
188
|
-
htmlFiles = findHtmlFiles(rootDir);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 1. Create a named safety backup before removal
|
|
192
|
-
const backup = createBackup(htmlFiles, rootDir, 'Pre-Uninstall Clean Backup');
|
|
193
|
-
|
|
194
|
-
const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
|
|
195
|
-
const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
|
|
196
|
-
const runtimeMarkerRegex = new RegExp(`[\\t ]*${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}\\r?\\n?`, 'g');
|
|
197
|
-
|
|
198
|
-
const cleanedFiles = [];
|
|
199
|
-
|
|
200
|
-
for (const filePath of htmlFiles) {
|
|
201
|
-
let content = fs.readFileSync(filePath, 'utf8');
|
|
202
|
-
|
|
203
|
-
// Strip markers
|
|
204
|
-
content = content.replace(headMarkerRegex, '');
|
|
205
|
-
content = content.replace(bodyMarkerRegex, '');
|
|
206
|
-
content = content.replace(runtimeMarkerRegex, '');
|
|
207
|
-
|
|
208
|
-
// Strip data-buzl-track="true" attribute from forms
|
|
209
|
-
content = content.replace(/\s*data-buzl-track=["']true["']/gi, '');
|
|
210
|
-
|
|
211
|
-
// Clean up any double blank lines before </head> or </body>
|
|
212
|
-
content = content.replace(/(\r?\n\s*){2,}<\/head>/i, '\n</head>');
|
|
213
|
-
content = content.replace(/(\r?\n\s*){2,}<\/body>/i, '\n</body>');
|
|
214
|
-
|
|
215
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
216
|
-
cleanedFiles.push({
|
|
217
|
-
path: filePath,
|
|
218
|
-
relative: path.relative(rootDir, filePath)
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
success: true,
|
|
224
|
-
backupDir: backup.backupDir,
|
|
225
|
-
cleanedFiles,
|
|
226
|
-
message: `Successfully removed all tracking from ${cleanedFiles.length} file(s). Clean safety backup saved in ${path.basename(backup.backupDir)}.`
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Selectively remove a single tracking service or configuration without breaking other services
|
|
232
|
-
*/
|
|
233
|
-
function removeService(rootDir, serviceName) {
|
|
234
|
-
const normService = (serviceName || '').toLowerCase().trim();
|
|
235
|
-
const { scanProject } = require('./scanner');
|
|
236
|
-
const scan = scanProject(rootDir);
|
|
237
|
-
const htmlFiles = scan.files.map(f => f.filePath);
|
|
238
|
-
|
|
239
|
-
if (htmlFiles.length === 0) {
|
|
240
|
-
return { success: false, message: 'No HTML files found in project.' };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// If full uninstall requested
|
|
244
|
-
if (normService === 'all' || normService === 'everything') {
|
|
245
|
-
return removeTracking(rootDir, htmlFiles);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 1. Create safety snapshot backup before selective removal
|
|
249
|
-
const backup = createBackup(htmlFiles, rootDir, `Pre-Remove ${normService.toUpperCase()} Backup`);
|
|
250
|
-
|
|
251
|
-
// 2. Derive new configuration by blanking the selected service
|
|
252
|
-
const current = scan.existingConfig || {};
|
|
253
|
-
const newConfig = {
|
|
254
|
-
gtmId: normService === 'gtm' ? '' : (current.gtmId || (normService !== 'gtm' && scan.liveState.gtm.id) || ''),
|
|
255
|
-
enableDeferred: current.enableDeferred !== false,
|
|
256
|
-
metaPixelId: normService === 'meta' ? '' : (current.metaPixelId || (normService !== 'meta' && scan.liveState.meta.id) || ''),
|
|
257
|
-
trackMetaLeadEvent: normService === 'meta' ? false : !!current.trackMetaLeadEvent,
|
|
258
|
-
googleSheetUrl: normService === 'sheets' ? '' : (current.googleSheetUrl || ''),
|
|
259
|
-
siteLocation: current.siteLocation || scan.detectedLocation || '',
|
|
260
|
-
dynamicFields: current.dynamicFields || [],
|
|
261
|
-
buzlCapi: {
|
|
262
|
-
endpoint: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.endpoint) || ''),
|
|
263
|
-
authUser: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authUser) || ''),
|
|
264
|
-
authPass: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authPass) || '')
|
|
265
|
-
},
|
|
266
|
-
zoho: {
|
|
267
|
-
endpoint: normService === 'zoho' ? '' : ((current.zoho && current.zoho.endpoint) || ''),
|
|
268
|
-
xnQsjsdp: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xnQsjsdp) || ''),
|
|
269
|
-
xmIwtLD: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xmIwtLD) || ''),
|
|
270
|
-
fields: normService === 'zoho' ? {} : ((current.zoho && current.zoho.fields) || {})
|
|
271
|
-
},
|
|
272
|
-
whatsappNumber: normService === 'whatsapp' ? '' : (current.whatsappNumber || (current.whatsapp && current.whatsapp.number) || '')
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// 3. Inject updated HTML files
|
|
276
|
-
const targetJsDir = path.join(rootDir, 'assets', 'js');
|
|
277
|
-
const modifiedFiles = [];
|
|
278
|
-
|
|
279
|
-
for (const filePath of htmlFiles) {
|
|
280
|
-
const originalHtml = fs.readFileSync(filePath, 'utf8');
|
|
281
|
-
let scriptRelPath = 'assets/js/buzl-tracking.js';
|
|
282
|
-
if (current && current.pathMode === 'root-absolute') {
|
|
283
|
-
scriptRelPath = '/assets/js/buzl-tracking.js';
|
|
284
|
-
} else {
|
|
285
|
-
const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
|
|
286
|
-
scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const modifiedHtml = injectHtml(originalHtml, newConfig, scriptRelPath);
|
|
290
|
-
fs.writeFileSync(filePath, modifiedHtml, 'utf8');
|
|
291
|
-
modifiedFiles.push({
|
|
292
|
-
path: filePath,
|
|
293
|
-
relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const serviceLabels = {
|
|
298
|
-
gtm: 'Google Tag Manager (GTM)',
|
|
299
|
-
meta: 'Meta Pixel & Client CAPI',
|
|
300
|
-
capi: 'Buzl CAPI Server-Side Endpoint',
|
|
301
|
-
sheets: 'Google Sheets Direct Sync',
|
|
302
|
-
zoho: 'Zoho CRM Web-to-Lead',
|
|
303
|
-
whatsapp: 'WhatsApp Handoff'
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
return {
|
|
307
|
-
success: true,
|
|
308
|
-
service: normService,
|
|
309
|
-
serviceLabel: serviceLabels[normService] || normService,
|
|
310
|
-
backupDir: backup.backupDir,
|
|
311
|
-
modifiedFiles,
|
|
312
|
-
message: `Successfully removed ${serviceLabels[normService] || normService} from site. Clean safety backup saved in ${path.basename(backup.backupDir)}.`
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
module.exports = {
|
|
317
|
-
injectHtml,
|
|
318
|
-
applyInjection,
|
|
319
|
-
removeTracking,
|
|
320
|
-
removeService
|
|
321
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Safe HTML & Tracking Injector
|
|
3
|
+
* Injects GTM, Meta Pixel, Form Sync runtime, and configuration
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const {
|
|
8
|
+
HEAD_START_MARKER,
|
|
9
|
+
HEAD_END_MARKER,
|
|
10
|
+
BODY_START_MARKER,
|
|
11
|
+
BODY_END_MARKER,
|
|
12
|
+
generateHeadSnippet,
|
|
13
|
+
generateBodySnippet
|
|
14
|
+
} = require('../templates/gtm-meta-snippets');
|
|
15
|
+
const { createBackup } = require('./rollback');
|
|
16
|
+
|
|
17
|
+
const RUNTIME_START_MARKER = '<!-- BUZL_TRACKING_RUNTIME_START -->';
|
|
18
|
+
const RUNTIME_END_MARKER = '<!-- BUZL_TRACKING_RUNTIME_END -->';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate runtime script tag & inline configuration
|
|
22
|
+
*/
|
|
23
|
+
function generateRuntimeSnippet(config, scriptRelPath) {
|
|
24
|
+
const zohoObj = config.zoho || {};
|
|
25
|
+
const zohoXn = (config.zohoXnqsjsdp || zohoObj.xnQsjsdp || '').trim();
|
|
26
|
+
const zohoEndpoint = zohoXn ? (config.zohoEndpoint || zohoObj.endpoint || 'https://crm.zoho.in/crm/WebToLeadForm') : '';
|
|
27
|
+
const zohoXm = zohoXn ? (config.zohoXmiwtld || zohoObj.xmIwtLD || '') : '';
|
|
28
|
+
const zohoFields = zohoXn ? (config.zohoFields || zohoObj.fields || { lastName: 'Last Name', phone: 'Mobile', location: 'City' }) : {};
|
|
29
|
+
|
|
30
|
+
const clientConfig = {
|
|
31
|
+
enableGTM: !!(config.gtmId && config.gtmId.trim()),
|
|
32
|
+
gtmEvent: config.gtmEvent || 'lead_form_submitted',
|
|
33
|
+
enableMeta: !!(config.metaPixelId && config.metaPixelId.trim()),
|
|
34
|
+
trackMetaLeadEvent: !!config.trackMetaLeadEvent,
|
|
35
|
+
googleSheetUrl: config.googleSheetUrl || '',
|
|
36
|
+
siteLocation: config.siteLocation || '',
|
|
37
|
+
zoho: {
|
|
38
|
+
endpoint: zohoEndpoint,
|
|
39
|
+
xnQsjsdp: zohoXn,
|
|
40
|
+
xmIwtLD: zohoXm,
|
|
41
|
+
fields: zohoFields
|
|
42
|
+
},
|
|
43
|
+
buzlCapi: {
|
|
44
|
+
endpoint: (config.buzlCapi && config.buzlCapi.endpoint) || config.buzlCapiEndpoint || '',
|
|
45
|
+
authUser: (config.buzlCapi && config.buzlCapi.authUser) || config.buzlCapiUser || '',
|
|
46
|
+
authPass: (config.buzlCapi && config.buzlCapi.authPass) || config.buzlCapiPass || ''
|
|
47
|
+
},
|
|
48
|
+
whatsapp: {
|
|
49
|
+
number: config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '',
|
|
50
|
+
template: config.whatsappTemplate || (config.whatsapp && config.whatsapp.template) || 'Hi, I submitted an inquiry from {name} in {location}.'
|
|
51
|
+
},
|
|
52
|
+
safetyTimeoutMs: config.safetyTimeoutMs || 800
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
RUNTIME_START_MARKER,
|
|
57
|
+
' <script id="buzl-tracking-config">',
|
|
58
|
+
` window.__BUZL_CONFIG__ = ${JSON.stringify(clientConfig, null, 2)};`,
|
|
59
|
+
' </script>',
|
|
60
|
+
` <script src="${scriptRelPath}" defer></script>`,
|
|
61
|
+
RUNTIME_END_MARKER
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Injects tracking into a single HTML string
|
|
67
|
+
*/
|
|
68
|
+
function injectHtml(htmlContent, config, scriptRelPath = 'assets/js/buzl-tracking.js') {
|
|
69
|
+
let content = htmlContent;
|
|
70
|
+
|
|
71
|
+
const hasHeadTrackers = !!((config.gtmId && config.gtmId.trim()) || (config.metaPixelId && config.metaPixelId.trim()));
|
|
72
|
+
const headSnippet = hasHeadTrackers ? generateHeadSnippet(config) : '';
|
|
73
|
+
const bodySnippet = hasHeadTrackers ? generateBodySnippet(config) : '';
|
|
74
|
+
const runtimeSnippet = generateRuntimeSnippet(config, scriptRelPath);
|
|
75
|
+
|
|
76
|
+
// 1. Head snippet replacement or injection
|
|
77
|
+
const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
|
|
78
|
+
if (headMarkerRegex.test(content)) {
|
|
79
|
+
content = content.replace(headMarkerRegex, hasHeadTrackers ? `${headSnippet}\n` : '');
|
|
80
|
+
} else if (hasHeadTrackers) {
|
|
81
|
+
if (/<\/head>/i.test(content)) {
|
|
82
|
+
content = content.replace(/<\/head>/i, `${headSnippet}\n</head>`);
|
|
83
|
+
} else if (/<head[\s>]/i.test(content)) {
|
|
84
|
+
content = content.replace(/(<head[\s>][^>]*>)/i, `$1\n${headSnippet}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Body noscript snippet replacement or injection
|
|
89
|
+
const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
|
|
90
|
+
if (bodyMarkerRegex.test(content)) {
|
|
91
|
+
content = content.replace(bodyMarkerRegex, hasHeadTrackers ? `${bodySnippet}\n` : '');
|
|
92
|
+
} else if (hasHeadTrackers) {
|
|
93
|
+
if (/<body[\s>]/i.test(content)) {
|
|
94
|
+
content = content.replace(/(<body[\s>][^>]*>)/i, `$1\n${bodySnippet}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. Runtime script replacement or injection
|
|
99
|
+
const runtimeMarkerRegex = new RegExp(`${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}`, 'g');
|
|
100
|
+
if (runtimeMarkerRegex.test(content)) {
|
|
101
|
+
content = content.replace(runtimeMarkerRegex, runtimeSnippet);
|
|
102
|
+
} else if (/<\/body>/i.test(content)) {
|
|
103
|
+
content = content.replace(/<\/body>/i, `${runtimeSnippet}\n</body>`);
|
|
104
|
+
} else {
|
|
105
|
+
content += `\n${runtimeSnippet}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 4. Form tag attribute injection (adds data-buzl-track="true" to forms if missing)
|
|
109
|
+
content = content.replace(/<form\b(?![^>]*\bdata-buzl-)([^>]*)>/gi, (match, p1) => {
|
|
110
|
+
return `<form data-buzl-track="true"${p1}>`;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return content;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Apply injection across the whole project
|
|
118
|
+
*/
|
|
119
|
+
function applyInjection(rootDir, htmlFiles, config) {
|
|
120
|
+
// If whatsapp number is not explicitly configured, fallback to auto-detected number from buttons
|
|
121
|
+
if (!config.whatsappNumber && !(config.whatsapp && config.whatsapp.number)) {
|
|
122
|
+
try {
|
|
123
|
+
const { scanProject } = require('./scanner');
|
|
124
|
+
const scan = scanProject(rootDir);
|
|
125
|
+
if (scan && scan.detectedWhatsapp) {
|
|
126
|
+
config = Object.assign({}, config, {
|
|
127
|
+
whatsappNumber: scan.detectedWhatsapp,
|
|
128
|
+
whatsapp: Object.assign({}, config.whatsapp || {}, { number: scan.detectedWhatsapp, autoDetected: true })
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. Ensure backup first
|
|
135
|
+
const backup = createBackup(htmlFiles, rootDir);
|
|
136
|
+
|
|
137
|
+
// 2. Deploy runtime SDK script into assets/js/
|
|
138
|
+
const targetJsDir = path.join(rootDir, 'assets', 'js');
|
|
139
|
+
fs.mkdirSync(targetJsDir, { recursive: true });
|
|
140
|
+
|
|
141
|
+
const sdkSourcePath = path.join(__dirname, '..', 'templates', 'buzl-tracking.js');
|
|
142
|
+
const sdkDestPath = path.join(targetJsDir, 'buzl-tracking.js');
|
|
143
|
+
fs.copyFileSync(sdkSourcePath, sdkDestPath);
|
|
144
|
+
|
|
145
|
+
// 3. Deploy GoogleAppsScript.gs to root or reference folder
|
|
146
|
+
const gsSource = path.join(__dirname, '..', 'templates', 'GoogleAppsScript.gs');
|
|
147
|
+
const gsDest = path.join(rootDir, 'Buzl_GoogleAppsScript_Template.gs');
|
|
148
|
+
if (!fs.existsSync(gsDest)) {
|
|
149
|
+
fs.copyFileSync(gsSource, gsDest);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 4. Process each HTML file
|
|
153
|
+
const modifiedFiles = [];
|
|
154
|
+
for (const filePath of htmlFiles) {
|
|
155
|
+
const originalHtml = fs.readFileSync(filePath, 'utf8');
|
|
156
|
+
// Calculate relative path to assets/js/buzl-tracking.js
|
|
157
|
+
let scriptRelPath = 'assets/js/buzl-tracking.js';
|
|
158
|
+
if (config && config.pathMode === 'root-absolute') {
|
|
159
|
+
scriptRelPath = '/assets/js/buzl-tracking.js';
|
|
160
|
+
} else {
|
|
161
|
+
const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
|
|
162
|
+
scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const modifiedHtml = injectHtml(originalHtml, config, scriptRelPath);
|
|
166
|
+
fs.writeFileSync(filePath, modifiedHtml, 'utf8');
|
|
167
|
+
modifiedFiles.push({
|
|
168
|
+
path: filePath,
|
|
169
|
+
relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
backupDir: backup.backupDir,
|
|
176
|
+
modifiedFiles,
|
|
177
|
+
runtimeScript: path.relative(rootDir, sdkDestPath),
|
|
178
|
+
googleAppsScriptFile: path.relative(rootDir, gsDest)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Cleanly remove all Buzl tracking tags and restore pristine HTML files
|
|
184
|
+
*/
|
|
185
|
+
function removeTracking(rootDir, htmlFiles) {
|
|
186
|
+
if (!htmlFiles || !Array.isArray(htmlFiles)) {
|
|
187
|
+
const { findHtmlFiles } = require('./scanner');
|
|
188
|
+
htmlFiles = findHtmlFiles(rootDir);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 1. Create a named safety backup before removal
|
|
192
|
+
const backup = createBackup(htmlFiles, rootDir, 'Pre-Uninstall Clean Backup');
|
|
193
|
+
|
|
194
|
+
const headMarkerRegex = new RegExp(`[\\t ]*${HEAD_START_MARKER}[\\s\\S]*?${HEAD_END_MARKER}\\r?\\n?`, 'g');
|
|
195
|
+
const bodyMarkerRegex = new RegExp(`[\\t ]*${BODY_START_MARKER}[\\s\\S]*?${BODY_END_MARKER}\\r?\\n?`, 'g');
|
|
196
|
+
const runtimeMarkerRegex = new RegExp(`[\\t ]*${RUNTIME_START_MARKER}[\\s\\S]*?${RUNTIME_END_MARKER}\\r?\\n?`, 'g');
|
|
197
|
+
|
|
198
|
+
const cleanedFiles = [];
|
|
199
|
+
|
|
200
|
+
for (const filePath of htmlFiles) {
|
|
201
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
202
|
+
|
|
203
|
+
// Strip markers
|
|
204
|
+
content = content.replace(headMarkerRegex, '');
|
|
205
|
+
content = content.replace(bodyMarkerRegex, '');
|
|
206
|
+
content = content.replace(runtimeMarkerRegex, '');
|
|
207
|
+
|
|
208
|
+
// Strip data-buzl-track="true" attribute from forms
|
|
209
|
+
content = content.replace(/\s*data-buzl-track=["']true["']/gi, '');
|
|
210
|
+
|
|
211
|
+
// Clean up any double blank lines before </head> or </body>
|
|
212
|
+
content = content.replace(/(\r?\n\s*){2,}<\/head>/i, '\n</head>');
|
|
213
|
+
content = content.replace(/(\r?\n\s*){2,}<\/body>/i, '\n</body>');
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
216
|
+
cleanedFiles.push({
|
|
217
|
+
path: filePath,
|
|
218
|
+
relative: path.relative(rootDir, filePath)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
backupDir: backup.backupDir,
|
|
225
|
+
cleanedFiles,
|
|
226
|
+
message: `Successfully removed all tracking from ${cleanedFiles.length} file(s). Clean safety backup saved in ${path.basename(backup.backupDir)}.`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Selectively remove a single tracking service or configuration without breaking other services
|
|
232
|
+
*/
|
|
233
|
+
function removeService(rootDir, serviceName) {
|
|
234
|
+
const normService = (serviceName || '').toLowerCase().trim();
|
|
235
|
+
const { scanProject } = require('./scanner');
|
|
236
|
+
const scan = scanProject(rootDir);
|
|
237
|
+
const htmlFiles = scan.files.map(f => f.filePath);
|
|
238
|
+
|
|
239
|
+
if (htmlFiles.length === 0) {
|
|
240
|
+
return { success: false, message: 'No HTML files found in project.' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// If full uninstall requested
|
|
244
|
+
if (normService === 'all' || normService === 'everything') {
|
|
245
|
+
return removeTracking(rootDir, htmlFiles);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 1. Create safety snapshot backup before selective removal
|
|
249
|
+
const backup = createBackup(htmlFiles, rootDir, `Pre-Remove ${normService.toUpperCase()} Backup`);
|
|
250
|
+
|
|
251
|
+
// 2. Derive new configuration by blanking the selected service
|
|
252
|
+
const current = scan.existingConfig || {};
|
|
253
|
+
const newConfig = {
|
|
254
|
+
gtmId: normService === 'gtm' ? '' : (current.gtmId || (normService !== 'gtm' && scan.liveState.gtm.id) || ''),
|
|
255
|
+
enableDeferred: current.enableDeferred !== false,
|
|
256
|
+
metaPixelId: normService === 'meta' ? '' : (current.metaPixelId || (normService !== 'meta' && scan.liveState.meta.id) || ''),
|
|
257
|
+
trackMetaLeadEvent: normService === 'meta' ? false : !!current.trackMetaLeadEvent,
|
|
258
|
+
googleSheetUrl: normService === 'sheets' ? '' : (current.googleSheetUrl || ''),
|
|
259
|
+
siteLocation: current.siteLocation || scan.detectedLocation || '',
|
|
260
|
+
dynamicFields: current.dynamicFields || [],
|
|
261
|
+
buzlCapi: {
|
|
262
|
+
endpoint: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.endpoint) || ''),
|
|
263
|
+
authUser: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authUser) || ''),
|
|
264
|
+
authPass: normService === 'capi' ? '' : ((current.buzlCapi && current.buzlCapi.authPass) || '')
|
|
265
|
+
},
|
|
266
|
+
zoho: {
|
|
267
|
+
endpoint: normService === 'zoho' ? '' : ((current.zoho && current.zoho.endpoint) || ''),
|
|
268
|
+
xnQsjsdp: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xnQsjsdp) || ''),
|
|
269
|
+
xmIwtLD: normService === 'zoho' ? '' : ((current.zoho && current.zoho.xmIwtLD) || ''),
|
|
270
|
+
fields: normService === 'zoho' ? {} : ((current.zoho && current.zoho.fields) || {})
|
|
271
|
+
},
|
|
272
|
+
whatsappNumber: normService === 'whatsapp' ? '' : (current.whatsappNumber || (current.whatsapp && current.whatsapp.number) || '')
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// 3. Inject updated HTML files
|
|
276
|
+
const targetJsDir = path.join(rootDir, 'assets', 'js');
|
|
277
|
+
const modifiedFiles = [];
|
|
278
|
+
|
|
279
|
+
for (const filePath of htmlFiles) {
|
|
280
|
+
const originalHtml = fs.readFileSync(filePath, 'utf8');
|
|
281
|
+
let scriptRelPath = 'assets/js/buzl-tracking.js';
|
|
282
|
+
if (current && current.pathMode === 'root-absolute') {
|
|
283
|
+
scriptRelPath = '/assets/js/buzl-tracking.js';
|
|
284
|
+
} else {
|
|
285
|
+
const relToTarget = path.relative(path.dirname(filePath), targetJsDir).replace(/\\/g, '/');
|
|
286
|
+
scriptRelPath = relToTarget ? `${relToTarget}/buzl-tracking.js` : 'assets/js/buzl-tracking.js';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const modifiedHtml = injectHtml(originalHtml, newConfig, scriptRelPath);
|
|
290
|
+
fs.writeFileSync(filePath, modifiedHtml, 'utf8');
|
|
291
|
+
modifiedFiles.push({
|
|
292
|
+
path: filePath,
|
|
293
|
+
relative: path.relative(rootDir, filePath).replace(/\\/g, '/')
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const serviceLabels = {
|
|
298
|
+
gtm: 'Google Tag Manager (GTM)',
|
|
299
|
+
meta: 'Meta Pixel & Client CAPI',
|
|
300
|
+
capi: 'Buzl CAPI Server-Side Endpoint',
|
|
301
|
+
sheets: 'Google Sheets Direct Sync',
|
|
302
|
+
zoho: 'Zoho CRM Web-to-Lead',
|
|
303
|
+
whatsapp: 'WhatsApp Handoff'
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
service: normService,
|
|
309
|
+
serviceLabel: serviceLabels[normService] || normService,
|
|
310
|
+
backupDir: backup.backupDir,
|
|
311
|
+
modifiedFiles,
|
|
312
|
+
message: `Successfully removed ${serviceLabels[normService] || normService} from site. Clean safety backup saved in ${path.basename(backup.backupDir)}.`
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
injectHtml,
|
|
318
|
+
applyInjection,
|
|
319
|
+
removeTracking,
|
|
320
|
+
removeService
|
|
321
|
+
};
|