@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.
- package/Buzl_GoogleAppsScript_Template.gs +948 -0
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/assets/js/buzl-tracking.js +390 -0
- package/bin/cli.js +155 -0
- package/package.json +54 -0
- package/src/cli/terminal.js +388 -0
- package/src/core/injector.js +321 -0
- package/src/core/rollback.js +281 -0
- package/src/core/scanner.js +506 -0
- package/src/core/tester.js +357 -0
- package/src/gui/public/app.js +942 -0
- package/src/gui/public/index.html +634 -0
- package/src/gui/public/style.css +1130 -0
- package/src/gui/server.js +232 -0
- package/src/index.js +47 -0
- package/src/templates/GoogleAppsScript.gs +948 -0
- package/src/templates/buzl-tracking.js +475 -0
- package/src/templates/gtm-meta-snippets.js +149 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automated Verification & Self-Testing Engine
|
|
3
|
+
* Validates HTML tags, form hooks, script existence, and endpoint health
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ping an HTTP/HTTPS URL
|
|
12
|
+
*/
|
|
13
|
+
function pingUrl(urlStr) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(urlStr);
|
|
17
|
+
const client = parsed.protocol === 'https:' ? https : http;
|
|
18
|
+
|
|
19
|
+
const req = client.get(urlStr, { timeout: 4000 }, (res) => {
|
|
20
|
+
// If Google Apps Script redirects to Google Accounts login, access is restricted
|
|
21
|
+
if (res.headers && res.headers.location && res.headers.location.includes('accounts.google.com')) {
|
|
22
|
+
resolve({ ok: false, statusCode: 401, error: 'Authentication required. In Google Apps Script, set "Who has access: Anyone".' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Google Apps Script redirects 302 to script.googleusercontent.com
|
|
26
|
+
if (res.statusCode >= 200 && res.statusCode < 400) {
|
|
27
|
+
resolve({ ok: true, statusCode: res.statusCode });
|
|
28
|
+
} else {
|
|
29
|
+
resolve({ ok: false, statusCode: res.statusCode });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
req.on('timeout', () => {
|
|
34
|
+
req.destroy();
|
|
35
|
+
resolve({ ok: false, error: 'Request timed out after 4000ms' });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
req.on('error', (err) => {
|
|
39
|
+
resolve({ ok: false, error: err.message });
|
|
40
|
+
});
|
|
41
|
+
} catch (e) {
|
|
42
|
+
resolve({ ok: false, error: 'Invalid URL format' });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run verification tests on modified files and configuration
|
|
49
|
+
*/
|
|
50
|
+
async function runVerification(rootDir, htmlFiles, config) {
|
|
51
|
+
const testResults = [];
|
|
52
|
+
|
|
53
|
+
function record(name, passed, detail) {
|
|
54
|
+
testResults.push({ name, passed, detail: detail || '' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 1. Check runtime script existence
|
|
58
|
+
const runtimePath = path.join(rootDir, 'assets', 'js', 'buzl-tracking.js');
|
|
59
|
+
const runtimeExists = fs.existsSync(runtimePath);
|
|
60
|
+
record('Runtime script deployed (assets/js/buzl-tracking.js)', runtimeExists);
|
|
61
|
+
|
|
62
|
+
// 2. Check each HTML file
|
|
63
|
+
for (const filePath of htmlFiles) {
|
|
64
|
+
const relName = path.relative(rootDir, filePath);
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
|
|
67
|
+
// Structural integrity
|
|
68
|
+
const hasHead = /<head[\s>]/i.test(content) && /<\/head>/i.test(content);
|
|
69
|
+
record(`[${relName}] Valid <head> block integrity`, hasHead);
|
|
70
|
+
|
|
71
|
+
const hasBody = /<body[\s>]/i.test(content) && /<\/body>/i.test(content);
|
|
72
|
+
record(`[${relName}] Valid <body> block integrity`, hasBody);
|
|
73
|
+
|
|
74
|
+
// GTM Verification
|
|
75
|
+
if (config.gtmId) {
|
|
76
|
+
const hasGtmHead = content.includes(config.gtmId);
|
|
77
|
+
const hasGtmBody = content.includes(`googletagmanager.com/ns.html?id=${config.gtmId}`);
|
|
78
|
+
record(`[${relName}] GTM Head snippet matches ${config.gtmId}`, hasGtmHead);
|
|
79
|
+
record(`[${relName}] GTM Body noscript fallback matches ${config.gtmId}`, hasGtmBody);
|
|
80
|
+
|
|
81
|
+
// Duplication check
|
|
82
|
+
const gtmCount = (content.match(new RegExp(config.gtmId, 'g')) || []).length;
|
|
83
|
+
record(`[${relName}] No duplicate GTM tags (head + noscript count <= 2)`, gtmCount <= 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Meta Pixel Verification
|
|
87
|
+
if (config.metaPixelId) {
|
|
88
|
+
const hasMetaHead = content.includes(`fbq("init", "${config.metaPixelId}")`) || content.includes(config.metaPixelId);
|
|
89
|
+
const hasMetaNoscript = content.includes(`facebook.com/tr?id=${config.metaPixelId}`);
|
|
90
|
+
record(`[${relName}] Meta Pixel Head code configured with ${config.metaPixelId}`, hasMetaHead);
|
|
91
|
+
record(`[${relName}] Meta Pixel Noscript fallback present`, hasMetaNoscript);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Runtime script tag link
|
|
95
|
+
const hasScriptLink = content.includes('buzl-tracking.js');
|
|
96
|
+
record(`[${relName}] Runtime tracker linked before </body>`, hasScriptLink);
|
|
97
|
+
|
|
98
|
+
// Form Hook Check
|
|
99
|
+
const hasForms = /<form[\s>]/i.test(content);
|
|
100
|
+
if (hasForms) {
|
|
101
|
+
const hasTrackAttr = /<form[^>]*data-buzl-track="true"/i.test(content);
|
|
102
|
+
record(`[${relName}] Forms tagged with data-buzl-track attribute`, hasTrackAttr);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Google Apps Script Webhook Ping
|
|
107
|
+
if (config.googleSheetUrl) {
|
|
108
|
+
const pingRes = await pingUrl(config.googleSheetUrl);
|
|
109
|
+
if (pingRes.ok) {
|
|
110
|
+
record(`Google Apps Script endpoint reachable (HTTP ${pingRes.statusCode})`, true);
|
|
111
|
+
} else {
|
|
112
|
+
let detail = pingRes.error || `HTTP ${pingRes.statusCode}`;
|
|
113
|
+
if (pingRes.statusCode === 404) {
|
|
114
|
+
const isPlaceholder = /AKfycbx_eY7V34n2Gz3h125JmO6r89Q2k|placeholder|example|AKfycbz_test/i.test(config.googleSheetUrl);
|
|
115
|
+
detail = isPlaceholder
|
|
116
|
+
? 'HTTP 404 (Placeholder script ID detected. Deploy Buzl_GoogleAppsScript_Template.gs in your Google Sheet via Deploy > New deployment > Web app > Anyone, and paste your live /exec URL)'
|
|
117
|
+
: 'HTTP 404 (Script not found on Google. In your Google Sheet, click Deploy > Manage deployments and verify a Web app is deployed with "Who has access: Anyone")';
|
|
118
|
+
} else if (pingRes.statusCode === 401 || pingRes.statusCode === 403) {
|
|
119
|
+
detail = `HTTP ${pingRes.statusCode} (Access denied. In your Google Sheet, ensure Web app deployment has "Who has access: Anyone")`;
|
|
120
|
+
}
|
|
121
|
+
record(`Google Apps Script endpoint reachability`, false, detail);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const passedCount = testResults.filter(t => t.passed).length;
|
|
126
|
+
const failedCount = testResults.filter(t => !t.passed).length;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
total: testResults.length,
|
|
130
|
+
passedCount,
|
|
131
|
+
failedCount,
|
|
132
|
+
allPassed: failedCount === 0,
|
|
133
|
+
tests: testResults
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Dispatch a live synthetic test lead to all configured channels
|
|
139
|
+
*/
|
|
140
|
+
async function testDispatch(arg1, arg2 = {}, arg3 = {}) {
|
|
141
|
+
let config = arg1;
|
|
142
|
+
let sampleLead = arg2;
|
|
143
|
+
let rootDir = '';
|
|
144
|
+
|
|
145
|
+
// Polymorphic support: if called as (rootDir, config, sampleLead)
|
|
146
|
+
if (typeof arg1 === 'string' && typeof arg2 === 'object') {
|
|
147
|
+
rootDir = arg1;
|
|
148
|
+
config = arg2;
|
|
149
|
+
sampleLead = arg3;
|
|
150
|
+
}
|
|
151
|
+
if (!config) config = {};
|
|
152
|
+
if (!sampleLead) sampleLead = {};
|
|
153
|
+
|
|
154
|
+
const leadId = sampleLead.leadId || ('test-lead-' + Date.now());
|
|
155
|
+
const name = sampleLead.name || 'Test Lead';
|
|
156
|
+
const phone = sampleLead.phone || config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '';
|
|
157
|
+
const location = sampleLead.location || config.siteLocation || '';
|
|
158
|
+
const service = sampleLead.service || 'General Inquiry';
|
|
159
|
+
|
|
160
|
+
const results = {
|
|
161
|
+
leadId,
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
channels: {}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// 1. Test Google Sheets Sync
|
|
167
|
+
const googleSheetUrl = config.googleSheetUrl || (config.sheets && config.sheets.url) || '';
|
|
168
|
+
if (googleSheetUrl) {
|
|
169
|
+
try {
|
|
170
|
+
const sheetPayload = {
|
|
171
|
+
timestamp: new Date().toISOString(),
|
|
172
|
+
leadId: leadId,
|
|
173
|
+
name: name,
|
|
174
|
+
location: location,
|
|
175
|
+
siteLocation: config.siteLocation || location,
|
|
176
|
+
phone: phone,
|
|
177
|
+
service: service,
|
|
178
|
+
email: sampleLead.email || config.notificationEmail || 'test-lead@example.com',
|
|
179
|
+
source: service,
|
|
180
|
+
utm: { source: 'live_test_button', medium: 'gui_test', campaign: 'buzl_verification' },
|
|
181
|
+
eventSourceUrl: sampleLead.eventSourceUrl || 'http://localhost:3333/test',
|
|
182
|
+
landingPageUrl: sampleLead.landingPageUrl || 'http://localhost:3333/',
|
|
183
|
+
pagePath: sampleLead.pagePath || '/test',
|
|
184
|
+
userAgent: 'Buzl-Test-Agent/1.0',
|
|
185
|
+
rawFields: Object.assign({ service: service, isTest: true }, sampleLead.rawFields || {})
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const res = await fetch(googleSheetUrl, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify(sheetPayload)
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const bodyText = await res.text();
|
|
195
|
+
let parsed = null;
|
|
196
|
+
try { parsed = JSON.parse(bodyText); } catch (e) {}
|
|
197
|
+
|
|
198
|
+
let statusMsg = (parsed && parsed.message) ? `${parsed.message} (Row ${parsed.row || 'N/A'})` : `HTTP ${res.status}`;
|
|
199
|
+
if (res.status === 404) {
|
|
200
|
+
const isPlaceholder = /AKfycbx_eY7V34n2Gz3h125JmO6r89Q2k|placeholder|example|AKfycbz_test/i.test(googleSheetUrl);
|
|
201
|
+
statusMsg = isPlaceholder
|
|
202
|
+
? 'Placeholder URL detected. Deploy Buzl_GoogleAppsScript_Template.gs and paste your real /exec URL.'
|
|
203
|
+
: 'HTTP 404: Script not found on Google. Verify deployment in Google Sheet with access: Anyone.';
|
|
204
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
205
|
+
statusMsg = `HTTP ${res.status}: Access Denied. Ensure Web app deployment access is set to 'Anyone'.`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
results.channels.googleSheets = {
|
|
209
|
+
tested: true,
|
|
210
|
+
ok: res.ok || (parsed && parsed.status === 'success'),
|
|
211
|
+
status: res.status,
|
|
212
|
+
message: statusMsg
|
|
213
|
+
};
|
|
214
|
+
} catch (err) {
|
|
215
|
+
results.channels.googleSheets = {
|
|
216
|
+
tested: true,
|
|
217
|
+
ok: false,
|
|
218
|
+
status: 0,
|
|
219
|
+
message: err.message
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
results.channels.googleSheets = { tested: false, message: 'Google Sheets not configured or disabled' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 2. Test Buzl CAPI
|
|
227
|
+
const buzlCapi = config.buzlCapi || {};
|
|
228
|
+
const capiEndpoint = buzlCapi.endpoint || config.buzlCapiEndpoint || '';
|
|
229
|
+
const authUser = buzlCapi.authUser || config.buzlCapiUser || '';
|
|
230
|
+
const authPass = buzlCapi.authPass || config.buzlCapiPass || '';
|
|
231
|
+
|
|
232
|
+
if (capiEndpoint && authUser) {
|
|
233
|
+
try {
|
|
234
|
+
const resolvedDomain = sampleLead.domain ||
|
|
235
|
+
config.domain ||
|
|
236
|
+
(config.siteUrl ? new URL(config.siteUrl).hostname : '') ||
|
|
237
|
+
(rootDir ? path.basename(rootDir).toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') : '') ||
|
|
238
|
+
'landing-page';
|
|
239
|
+
|
|
240
|
+
const capiPayload = {
|
|
241
|
+
leadId: leadId,
|
|
242
|
+
domain: resolvedDomain,
|
|
243
|
+
eventName: 'Lead',
|
|
244
|
+
eventTime: Math.floor(Date.now() / 1000),
|
|
245
|
+
actionSource: 'website',
|
|
246
|
+
eventSourceUrl: sampleLead.eventSourceUrl || 'http://localhost:3333/test',
|
|
247
|
+
landingPageUrl: sampleLead.landingPageUrl || 'http://localhost:3333/',
|
|
248
|
+
contact: { name: name, phone: phone, location: location },
|
|
249
|
+
source: service,
|
|
250
|
+
utm: { source: 'live_test_button', medium: 'gui_test' }
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const basicAuth = Buffer.from(`${authUser}:${authPass}`).toString('base64');
|
|
254
|
+
const res = await fetch(capiEndpoint, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
'Authorization': `Basic ${basicAuth}`
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify(capiPayload)
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const text = await res.text();
|
|
264
|
+
results.channels.buzlCapi = {
|
|
265
|
+
tested: true,
|
|
266
|
+
ok: res.ok,
|
|
267
|
+
status: res.status,
|
|
268
|
+
message: res.ok ? `Lead accepted by Buzl CAPI (HTTP ${res.status})` : `Failed: HTTP ${res.status} - ${text.slice(0, 100)}`
|
|
269
|
+
};
|
|
270
|
+
} catch (err) {
|
|
271
|
+
results.channels.buzlCapi = {
|
|
272
|
+
tested: true,
|
|
273
|
+
ok: false,
|
|
274
|
+
status: 0,
|
|
275
|
+
message: err.message
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
results.channels.buzlCapi = { tested: false, message: 'Buzl CAPI not configured or disabled' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 3. Test Zoho CRM
|
|
283
|
+
const zoho = config.zoho || {};
|
|
284
|
+
const zohoEndpoint = zoho.endpoint || config.zohoEndpoint || '';
|
|
285
|
+
const zohoXnqsjsdp = zoho.xnQsjsdp || config.zohoXnqsjsdp || '';
|
|
286
|
+
|
|
287
|
+
if (zohoEndpoint && zohoXnqsjsdp) {
|
|
288
|
+
try {
|
|
289
|
+
const params = new URLSearchParams();
|
|
290
|
+
params.append('xnQsjsdp', zohoXnqsjsdp);
|
|
291
|
+
if (zoho.xmIwtLD || config.zohoXmiwtld) params.append('xmIwtLD', zoho.xmIwtLD || config.zohoXmiwtld);
|
|
292
|
+
params.append('actionType', 'TGVhZHM=');
|
|
293
|
+
params.append('Last Name', name);
|
|
294
|
+
params.append('Phone', phone);
|
|
295
|
+
params.append('City', location);
|
|
296
|
+
params.append('Lead Source', service);
|
|
297
|
+
|
|
298
|
+
const res = await fetch(zohoEndpoint, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
body: params
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
results.channels.zoho = {
|
|
304
|
+
tested: true,
|
|
305
|
+
ok: res.ok,
|
|
306
|
+
status: res.status,
|
|
307
|
+
message: `Dispatched to Zoho (HTTP ${res.status})`
|
|
308
|
+
};
|
|
309
|
+
} catch (err) {
|
|
310
|
+
results.channels.zoho = {
|
|
311
|
+
tested: true,
|
|
312
|
+
ok: false,
|
|
313
|
+
status: 0,
|
|
314
|
+
message: err.message
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
results.channels.zoho = { tested: false, message: 'Zoho CRM not configured or disabled' };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Test a specific individual form submission
|
|
326
|
+
*/
|
|
327
|
+
async function testIndividualForm(rootDir, formId, formFields = {}, config = {}, extraOpts = {}) {
|
|
328
|
+
const leadId = 'test-' + (formId || 'form').replace(/[^a-zA-Z0-9_-]/g, '') + '-' + Date.now();
|
|
329
|
+
|
|
330
|
+
const name = formFields.name || formFields.fullName || 'Test Lead';
|
|
331
|
+
const phone = formFields.phone || formFields.mobile || config.whatsappNumber || (config.whatsapp && config.whatsapp.number) || '';
|
|
332
|
+
const location = formFields.location || formFields.city || config.siteLocation || '';
|
|
333
|
+
const service = formFields.service || formFields.subject || formFields.inquiry || formId || 'General Inquiry';
|
|
334
|
+
const pagePath = extraOpts.pagePath || formFields.pagePath || '/';
|
|
335
|
+
|
|
336
|
+
const sampleLead = {
|
|
337
|
+
leadId,
|
|
338
|
+
name,
|
|
339
|
+
phone,
|
|
340
|
+
location,
|
|
341
|
+
service,
|
|
342
|
+
source: formId || 'Individual Form Test',
|
|
343
|
+
eventSourceUrl: `http://localhost:3333/${pagePath.replace(/^\//, '')}`,
|
|
344
|
+
landingPageUrl: 'http://localhost:3333/',
|
|
345
|
+
pagePath: pagePath,
|
|
346
|
+
rawFields: Object.assign({}, formFields, { formId, pagePath, isTest: true })
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return await testDispatch(rootDir, config, sampleLead);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
runVerification,
|
|
354
|
+
pingUrl,
|
|
355
|
+
testDispatch,
|
|
356
|
+
testIndividualForm
|
|
357
|
+
};
|