@analitikisler/ai-tag-debugger 0.7.7 → 0.7.8
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/CHANGELOG.md +5 -0
- package/package.json +1 -1
- package/src/browser.js +12 -11
- package/src/checks.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.8 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
- Unnamed TikTok pings are left out of the duplicate check. TikTok sends them on its own timing, so their count changed between a save and its replay without the site changing.
|
|
6
|
+
- A replay skips a malformed address in a recording instead of stopping, and reads the recording once.
|
|
7
|
+
|
|
3
8
|
## 0.7.7 (2026-10-14)
|
|
4
9
|
|
|
5
10
|
- A replay answers a request from the recording when the address only differs by a timestamp (`_=1791977697130`, `ts`, `timestamp`, `cb`, `nocache`). Magento sites reload the basket this way after "add to basket", so the replay never got the answer that makes the page send `add_to_cart`. Exact matches still come from the recording as before.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analitikisler/ai-tag-debugger",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Open-source AI agent that QA-tests your website's analytics, ad and consent tracking (GA4, GTM, Google Ads, Meta, TikTok). Bring your own model (Claude, OpenAI, Gemini) or use it from your chat app over MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/browser.js
CHANGED
|
@@ -201,12 +201,12 @@ function trackingBody(request) {
|
|
|
201
201
|
* How a scripted run can find the element an action used again: the selector it was given,
|
|
202
202
|
* else a CSS path to the element, else its visible text. Refs only mean something in one run.
|
|
203
203
|
*/
|
|
204
|
-
// Text that looks like an email address or phone number is not saved with a click.
|
|
205
204
|
/** Query parameters that only carry the time or a cache-buster, so they differ between a recording and its replay. */
|
|
206
205
|
const STAMP_PARAMS = /^(_|_t|ts|timestamp|cb|cachebust|nocache)$/i;
|
|
207
206
|
/** The address without its timestamp parameters. */
|
|
208
207
|
function unstamped(href) {
|
|
209
|
-
|
|
208
|
+
let url;
|
|
209
|
+
try { url = new URL(href); } catch { return href; }
|
|
210
210
|
for (const name of [...url.searchParams.keys()]) if (STAMP_PARAMS.test(name)) url.searchParams.delete(name);
|
|
211
211
|
return url.href;
|
|
212
212
|
}
|
|
@@ -214,10 +214,9 @@ function unstamped(href) {
|
|
|
214
214
|
* The GET answers in a recording whose address carries a timestamp, by address without it.
|
|
215
215
|
* Entries the recording has no body for (still loading when the page was left) are not included.
|
|
216
216
|
*/
|
|
217
|
-
|
|
217
|
+
function restampedAnswers(har) {
|
|
218
218
|
const answers = new Map();
|
|
219
219
|
answers.exact = new Set();
|
|
220
|
-
const har = JSON.parse(await readFile(file, "utf8"));
|
|
221
220
|
for (const e of har.log.entries) {
|
|
222
221
|
const { request, response } = e;
|
|
223
222
|
answers.exact.add(request.url);
|
|
@@ -237,6 +236,7 @@ async function restampedAnswers(file) {
|
|
|
237
236
|
}
|
|
238
237
|
return answers;
|
|
239
238
|
}
|
|
239
|
+
// Text that looks like an email address or phone number is not saved with a click.
|
|
240
240
|
const savableText = (text) => (typeof text === "string" && text && !/@|\d[\d\s().+-]{6,}\d/.test(text) ? text : undefined);
|
|
241
241
|
|
|
242
242
|
async function replayTarget(a, locator) {
|
|
@@ -454,11 +454,10 @@ export class AuditSession {
|
|
|
454
454
|
(isDroppedMedia(route.request()) ? route.fulfill({ status: 200, body: "" }) : route.abort()).catch(() => {}),
|
|
455
455
|
);
|
|
456
456
|
await this.context.routeFromHAR(this.opts.replayHar, { notFound: "fallback" });
|
|
457
|
-
|
|
458
|
-
// recording, the redirect would be followed outside any handler, and its failure would end the run.
|
|
457
|
+
const har = JSON.parse(await readFile(this.opts.replayHar, "utf8"));
|
|
459
458
|
// A page asks again for what it asked when saved, with a new timestamp on the address. Registered
|
|
460
459
|
// after the recording, so it runs first and leaves every exact match to the recording.
|
|
461
|
-
const restamped =
|
|
460
|
+
const restamped = restampedAnswers(har);
|
|
462
461
|
if (restamped.size) {
|
|
463
462
|
await this.context.route(
|
|
464
463
|
(url) => restamped.has(unstamped(url.href)),
|
|
@@ -471,7 +470,9 @@ export class AuditSession {
|
|
|
471
470
|
},
|
|
472
471
|
);
|
|
473
472
|
}
|
|
474
|
-
|
|
473
|
+
// A saved redirect to a page the browser may not open fails here, as it did when saved. Left to the
|
|
474
|
+
// recording, the redirect would be followed outside any handler, and its failure would end the run.
|
|
475
|
+
const refusedRedirects = await this.refusedRedirects(har);
|
|
475
476
|
if (refusedRedirects.size) {
|
|
476
477
|
await this.context.route((url) => refusedRedirects.has(url.href), (route) => route.abort("blockedbyclient").catch(() => {}));
|
|
477
478
|
}
|
|
@@ -564,15 +565,15 @@ export class AuditSession {
|
|
|
564
565
|
}
|
|
565
566
|
|
|
566
567
|
/** Addresses whose saved answer redirects somewhere the browser may not go. */
|
|
567
|
-
async refusedRedirects(
|
|
568
|
+
async refusedRedirects(har) {
|
|
568
569
|
const refused = new Set();
|
|
569
|
-
const har = JSON.parse(await readFile(file, "utf8"));
|
|
570
570
|
for (const e of har.log.entries) {
|
|
571
571
|
const status = e.response?.status;
|
|
572
572
|
if (status < 300 || status >= 400) continue;
|
|
573
573
|
const location = e.response.redirectURL || e.response.headers?.find((h) => /^location$/i.test(h.name))?.value;
|
|
574
574
|
if (!location) continue;
|
|
575
|
-
|
|
575
|
+
let target;
|
|
576
|
+
try { target = new URL(location, e.request.url).href; } catch { continue; }
|
|
576
577
|
if (await this.checkUrl(target)) refused.add(e.request.url);
|
|
577
578
|
}
|
|
578
579
|
return refused;
|
package/src/checks.js
CHANGED
|
@@ -133,6 +133,8 @@ export function checkRun(plan, run, { lang = "en" } = {}) {
|
|
|
133
133
|
for (const [, hits] of perStep) {
|
|
134
134
|
const ev = planByKey.get(key(hits[0].platform, hits[0].name));
|
|
135
135
|
if (hits.length < 2 || ev?.allow_multiple) continue;
|
|
136
|
+
// TikTok sends unnamed pings (no event name) on its own schedule, so their count varies between runs.
|
|
137
|
+
if (hits[0].platform === "tiktok" && hits[0].name === "(unnamed)") continue;
|
|
136
138
|
findings.push({
|
|
137
139
|
...base,
|
|
138
140
|
severity: "warning",
|