@analitikisler/ai-tag-debugger 0.7.2 → 0.7.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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/browser.js +52 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.4 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
- Saving a site no longer fails on pages that keep a request open after they are left (a GA4 hit to `www.google.com/g/collect`, for example). The recording waited for it until the 30-second limit and was then skipped, so the site was not saved. The browser's pages are now closed before the recording is written.
|
|
6
|
+
- Fewer test hits from popups reach the real account while a recording is written.
|
|
7
|
+
|
|
8
|
+
## 0.7.3 (2026-10-13)
|
|
9
|
+
|
|
10
|
+
- `replay` no longer crashes with `NavigationAbortedError: net::ERR_BLOCKED_BY_CLIENT` when a saved page redirects to another site. The redirect is refused again, so the journey stays unfinished as it was saved, and the other sites are still replayed.
|
|
11
|
+
- Tracking hits sent while the browser closes are answered locally again on normal runs. In 0.7.2 the routes were removed before closing, so a hit sent at that moment could reach the real account. They are now removed only on `replay`, which never goes online.
|
|
12
|
+
|
|
3
13
|
## 0.7.2 (2026-10-13)
|
|
4
14
|
|
|
5
15
|
- **GA4 and Google Ads hits sent through Google tag gateway are captured.** Sites using the gateway send GA4 to a short path on their own domain (`/<prefix>/ag/g/c`, `/<prefix>/ga/g/c`) and Google Ads to `/<prefix>/as/p/c/<id>/`. These were neither recognized nor answered locally, so their events were reported as missing and the audit's visits reached the site's real accounts. A `/g/c` path counts as GA4 only with a GA4 measurement id (`tid=G-…`).
|
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.4",
|
|
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
|
@@ -322,6 +322,11 @@ export class AuditSession {
|
|
|
322
322
|
launch.proxy = { server: this.egress.url, bypass: "<-loopback>" };
|
|
323
323
|
launch.args = ["--force-webrtc-ip-handling-policy=disable_non_proxied_udp"];
|
|
324
324
|
}
|
|
325
|
+
if (this.opts.replayHar) {
|
|
326
|
+
// A replay never goes online: requests the routes below don't answer, including any still in
|
|
327
|
+
// flight once they are removed at the end, go to a proxy that isn't there.
|
|
328
|
+
launch.proxy = { server: "http://127.0.0.1:9", bypass: "<-loopback>" };
|
|
329
|
+
}
|
|
325
330
|
this.browser = await chromium.launch(launch);
|
|
326
331
|
const record = this.opts.recordHar ? { recordHar: { path: this.opts.recordHar, mode: "minimal" } } : {};
|
|
327
332
|
this.context = await this.browser.newContext({ ...device, locale: "en-US", ...record });
|
|
@@ -407,6 +412,12 @@ export class AuditSession {
|
|
|
407
412
|
(isDroppedMedia(route.request()) ? route.fulfill({ status: 200, body: "" }) : route.abort()).catch(() => {}),
|
|
408
413
|
);
|
|
409
414
|
await this.context.routeFromHAR(this.opts.replayHar, { notFound: "fallback" });
|
|
415
|
+
// A saved redirect to a page the browser may not open fails here, as it did when saved. Left to the
|
|
416
|
+
// recording, the redirect would be followed outside any handler, and its failure would end the run.
|
|
417
|
+
const refusedRedirects = await this.refusedRedirects(this.opts.replayHar);
|
|
418
|
+
if (refusedRedirects.size) {
|
|
419
|
+
await this.context.route((url) => refusedRedirects.has(url.href), (route) => route.abort("blockedbyclient").catch(() => {}));
|
|
420
|
+
}
|
|
410
421
|
}
|
|
411
422
|
if (this.opts.blockHits) {
|
|
412
423
|
await this.context.route((url) => isCollectionEndpoint(url.href), async (route) => {
|
|
@@ -495,6 +506,21 @@ export class AuditSession {
|
|
|
495
506
|
return true;
|
|
496
507
|
}
|
|
497
508
|
|
|
509
|
+
/** Addresses whose saved answer redirects somewhere the browser may not go. */
|
|
510
|
+
async refusedRedirects(file) {
|
|
511
|
+
const refused = new Set();
|
|
512
|
+
const har = JSON.parse(await readFile(file, "utf8"));
|
|
513
|
+
for (const e of har.log.entries) {
|
|
514
|
+
const status = e.response?.status;
|
|
515
|
+
if (status < 300 || status >= 400) continue;
|
|
516
|
+
const location = e.response.redirectURL || e.response.headers?.find((h) => /^location$/i.test(h.name))?.value;
|
|
517
|
+
if (!location) continue;
|
|
518
|
+
const target = new URL(location, e.request.url).href;
|
|
519
|
+
if (await this.checkUrl(target)) refused.add(e.request.url);
|
|
520
|
+
}
|
|
521
|
+
return refused;
|
|
522
|
+
}
|
|
523
|
+
|
|
498
524
|
/** Error message when the browser may not be on this URL, or null. */
|
|
499
525
|
checkUrl(url) {
|
|
500
526
|
if (url.startsWith("chrome-error:")) return null; // Chromium's own error page, e.g. after a blocked navigation
|
|
@@ -765,15 +791,32 @@ export class AuditSession {
|
|
|
765
791
|
async close() {
|
|
766
792
|
// Leaving the page first cancels what it is still loading (the recording would otherwise wait for it),
|
|
767
793
|
// while tracking requests sent on the way out are still answered locally.
|
|
768
|
-
if (this.opts.recordHar
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
new Promise((resolve) =>
|
|
775
|
-
|
|
776
|
-
|
|
794
|
+
if (this.opts.recordHar && this.context) {
|
|
795
|
+
await this.page?.goto("about:blank", { timeout: 5000 }).catch(() => {});
|
|
796
|
+
// The recording also waits for requests the browser holds for an open page, such as a tracking hit
|
|
797
|
+
// sent as the page was left, which can stay open for good. Once their page is closed it stops waiting.
|
|
798
|
+
// A moment first lets hits already sent be answered locally: once their page is closed,
|
|
799
|
+
// the browser would send them on its own.
|
|
800
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
801
|
+
let pagesTimer;
|
|
802
|
+
await Promise.race([
|
|
803
|
+
Promise.all(this.context.pages().map((p) => p.close().catch(() => {}))),
|
|
804
|
+
new Promise((resolve) => (pagesTimer = setTimeout(resolve, 5000))),
|
|
805
|
+
]);
|
|
806
|
+
clearTimeout(pagesTimer);
|
|
807
|
+
}
|
|
808
|
+
if (this.opts.replayHar) {
|
|
809
|
+
// The recording's own route callbacks would otherwise throw for requests still in flight once the
|
|
810
|
+
// browser is gone. Without routes, those go nowhere (see start). Elsewhere the routes stay, so a
|
|
811
|
+
// hit sent while the browser closes is still answered locally instead of reaching the real account.
|
|
812
|
+
// A page stuck in a script never answers, so this waits at most 5 seconds.
|
|
813
|
+
let unrouteTimer;
|
|
814
|
+
await Promise.race([
|
|
815
|
+
this.context?.unrouteAll({ behavior: "ignoreErrors" }).catch(() => {}),
|
|
816
|
+
new Promise((resolve) => (unrouteTimer = setTimeout(resolve, 5000))),
|
|
817
|
+
]);
|
|
818
|
+
clearTimeout(unrouteTimer);
|
|
819
|
+
}
|
|
777
820
|
if (this.opts.recordHar && this.context) {
|
|
778
821
|
// Writes the recording. If the browser already went away, or writing it takes too long, there is
|
|
779
822
|
// none to clean: closing the browser below then ends the run's wait.
|