@analitikisler/ai-tag-debugger 0.7.4 → 0.7.6
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 +22 -4
- package/src/cli.js +1 -0
- package/src/replay.js +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.6 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
- `replay` says when a saved click was found by its text rather than its saved place (`~` lines), since that may not be the element that was saved.
|
|
6
|
+
- Only clicks fall back to their text. Typing into a field or choosing an option uses its saved place only.
|
|
7
|
+
- A click's text is not saved when it looks like an email address or phone number.
|
|
8
|
+
|
|
9
|
+
## 0.7.5 (2026-10-14)
|
|
10
|
+
|
|
11
|
+
- A saved click keeps the element's text as well as its path through the page. When a replay finds nothing visible at that path (a banner or overlay shifted the layout), it clicks the element with that text instead. Sites saved before 0.7.5 have no text to fall back on; save them again to use it.
|
|
12
|
+
|
|
3
13
|
## 0.7.4 (2026-10-14)
|
|
4
14
|
|
|
5
15
|
- 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.
|
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.6",
|
|
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,8 +201,13 @@ 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
|
+
const savableText = (text) => (typeof text === "string" && text && !/@|\d[\d\s().+-]{6,}\d/.test(text) ? text : undefined);
|
|
206
|
+
|
|
204
207
|
async function replayTarget(a, locator) {
|
|
205
|
-
|
|
208
|
+
// Only a click keeps its text: a field's text is what was typed in it, not what identifies it.
|
|
209
|
+
const keepText = a.action === "click";
|
|
210
|
+
if (a.selector) return { selector: a.selector, ...(keepText && savableText(a.text) ? { text: a.text } : {}) };
|
|
206
211
|
const found = await locator
|
|
207
212
|
.evaluate((el) => {
|
|
208
213
|
const text = (el.innerText || el.value || el.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
@@ -220,8 +225,10 @@ async function replayTarget(a, locator) {
|
|
|
220
225
|
return { selector: parts.join(" > "), text };
|
|
221
226
|
}, null, { timeout: 5000 })
|
|
222
227
|
.catch(() => null);
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
const text = found?.text || a.text;
|
|
229
|
+
// The text too, since a path through the page's layout breaks when a banner or overlay shifts it.
|
|
230
|
+
if (found?.selector) return { selector: found.selector, ...(keepText && savableText(text) ? { text } : {}) };
|
|
231
|
+
return text ? { text } : {};
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
// Images, fonts and video don't change what tracking sends, and are most of a recording's size.
|
|
@@ -739,7 +746,18 @@ export class AuditSession {
|
|
|
739
746
|
case "fill":
|
|
740
747
|
case "select": {
|
|
741
748
|
let target;
|
|
742
|
-
if (a.selector
|
|
749
|
+
if (a.action === "click" && a.selector && typeof a.text === "string" && a.text.trim()) {
|
|
750
|
+
// A saved click: its path first, then its text if the path finds nothing to click.
|
|
751
|
+
target = this.locatorFor(a);
|
|
752
|
+
const shown = await target.waitFor({ state: "visible", timeout: 5000 }).then(() => true, () => false);
|
|
753
|
+
if (!shown) {
|
|
754
|
+
const found = await this.locateByText(a.text.trim());
|
|
755
|
+
if (!found.error) {
|
|
756
|
+
target = found.locator;
|
|
757
|
+
step.foundBy = "text"; // the replay summary says so, in case it is not the element saved
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} else if (a.selector || Number.isInteger(a.ref)) target = this.locatorFor(a);
|
|
743
761
|
else if (typeof a.text === "string" && a.text.trim()) {
|
|
744
762
|
const found = await this.locateByText(a.text.trim());
|
|
745
763
|
if (found.error) return found.error;
|
package/src/cli.js
CHANGED
|
@@ -125,6 +125,7 @@ async function main() {
|
|
|
125
125
|
for (const k of r.extra) console.log(` + ${k}`);
|
|
126
126
|
for (const k of r.missing) console.log(` - ${k}`);
|
|
127
127
|
for (const note of r.incomplete) console.log(` ! ${note}`);
|
|
128
|
+
for (const note of r.foundByText) console.log(` ~ ${note}`);
|
|
128
129
|
}
|
|
129
130
|
if (changed && !values.update) process.exitCode = 1;
|
|
130
131
|
return;
|
package/src/replay.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function writeSavedSite(dir, { config, plan, runs, findings }) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Audits a saved site again, offline and without AI, and compares the findings with the expected ones.
|
|
67
67
|
* update: make this replay's findings the expected ones.
|
|
68
|
-
* @returns {Promise<{ site: string, missing: string[], extra: string[], reportPaths: string[], incomplete: string[] }>}
|
|
68
|
+
* @returns {Promise<{ site: string, missing: string[], extra: string[], reportPaths: string[], incomplete: string[], foundByText: string[] }>}
|
|
69
69
|
*/
|
|
70
70
|
export async function replaySavedSite(dir, { update = false, outDir, log = () => {} } = {}) {
|
|
71
71
|
const file = path.join(dir, SAVED_SITE_FILE);
|
|
@@ -79,6 +79,7 @@ export async function replaySavedSite(dir, { update = false, outDir, log = () =>
|
|
|
79
79
|
const findings = [];
|
|
80
80
|
const reportPaths = [];
|
|
81
81
|
const incomplete = [];
|
|
82
|
+
const foundByText = [];
|
|
82
83
|
for (const viewport of viewports) {
|
|
83
84
|
const journeys = saved.journeys.filter((j) => j.viewport === viewport).map(({ viewport: _v, ...j }) => j);
|
|
84
85
|
const out = path.resolve(outDir ?? path.join(dir, "last-replay"), viewports.length > 1 ? viewport : "");
|
|
@@ -98,11 +99,14 @@ export async function replaySavedSite(dir, { update = false, outDir, log = () =>
|
|
|
98
99
|
// Journeys that were already unfinished when saved are compared through their findings only.
|
|
99
100
|
const wasIncomplete = (r) => journeys.find((j) => j.id === r.journeyId)?.incomplete === r.note;
|
|
100
101
|
incomplete.push(...result.runs.filter((r) => !r.completed && !wasIncomplete(r)).map((r) => `${r.journeyId} (${r.viewport}): ${r.note}`));
|
|
102
|
+
for (const r of result.runs) {
|
|
103
|
+
for (const s of r.steps.filter((s) => s.foundBy === "text")) foundByText.push(`${r.journeyId} (${r.viewport}): "${s.label}" was clicked by its text, since nothing was at its saved place.`);
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
106
|
const actual = findingKeys(findings);
|
|
103
107
|
const { missing, extra } = compareKeys(saved.expected ?? [], actual);
|
|
104
108
|
if (update && (missing.length || extra.length)) {
|
|
105
109
|
await writeFile(file, JSON.stringify({ ...saved, expected: actual }, null, 2));
|
|
106
110
|
}
|
|
107
|
-
return { site: saved.config.site, missing, extra, reportPaths, incomplete };
|
|
111
|
+
return { site: saved.config.site, missing, extra, reportPaths, incomplete, foundByText };
|
|
108
112
|
}
|