@analitikisler/ai-tag-debugger 0.7.2 → 0.7.3

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3 (2026-10-13)
4
+
5
+ - `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.
6
+ - 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.
7
+
3
8
  ## 0.7.2 (2026-10-13)
4
9
 
5
10
  - **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.2",
3
+ "version": "0.7.3",
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
@@ -766,14 +792,18 @@ export class AuditSession {
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
794
  if (this.opts.recordHar) await this.page?.goto("about:blank", { timeout: 5000 }).catch(() => {});
769
- // Requests still in flight then get no route callback that would throw once the browser is gone.
770
- // A page stuck in a script never answers, so this waits at most 5 seconds.
771
- let unrouteTimer;
772
- await Promise.race([
773
- this.context?.unrouteAll({ behavior: "ignoreErrors" }).catch(() => {}),
774
- new Promise((resolve) => (unrouteTimer = setTimeout(resolve, 5000))),
775
- ]);
776
- clearTimeout(unrouteTimer);
795
+ if (this.opts.replayHar) {
796
+ // The recording's own route callbacks would otherwise throw for requests still in flight once the
797
+ // browser is gone. Without routes, those go nowhere (see start). Elsewhere the routes stay, so a
798
+ // hit sent while the browser closes is still answered locally instead of reaching the real account.
799
+ // A page stuck in a script never answers, so this waits at most 5 seconds.
800
+ let unrouteTimer;
801
+ await Promise.race([
802
+ this.context?.unrouteAll({ behavior: "ignoreErrors" }).catch(() => {}),
803
+ new Promise((resolve) => (unrouteTimer = setTimeout(resolve, 5000))),
804
+ ]);
805
+ clearTimeout(unrouteTimer);
806
+ }
777
807
  if (this.opts.recordHar && this.context) {
778
808
  // Writes the recording. If the browser already went away, or writing it takes too long, there is
779
809
  // none to clean: closing the browser below then ends the run's wait.