@analitikisler/ai-tag-debugger 0.8.0 → 0.8.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/CHANGELOG.md +16 -0
- package/README.md +10 -1
- package/README.tr.md +10 -1
- package/package.json +5 -3
- package/src/agent.js +15 -0
- package/src/audit.js +9 -2
- package/src/checks.js +8 -2
- package/src/cli.js +11 -0
- package/src/mcp.js +10 -1
- package/src/providers.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
- A journey that fails with an error, such as the AI provider's API refusing a request or the browser crashing, no longer ends the whole audit. That journey is marked unfinished with the error as its note, what it captured until then is kept, the other journeys still run, and the report is written. This applies to the CLI and to `run_audit` over MCP.
|
|
6
|
+
- A Consent Mode default pushed after the first Google hit is now reported as late (`consent_default_late`) when both happen on the same step, such as the start page. The check used to compare only step numbers, so it missed this. Only Google hits count, since Consent Mode only governs Google tags.
|
|
7
|
+
- An unknown viewport, given with `--viewport` or in the config's `viewports`, now stops the audit at once with a message listing the allowed values (desktop, mobile). Before, it failed only when the browser started for that journey, after the model was set up and earlier journeys had run.
|
|
8
|
+
- `ai-tag-debugger --version` (or `aitd -v`) prints the installed version.
|
|
9
|
+
- New short command `aitd`, an alias for `ai-tag-debugger` installed alongside it.
|
|
10
|
+
- For contributors: `npm run setup` checks the Node version, installs the dependencies and Chromium, and says whether an AI provider key is set. A dev container (`.devcontainer/devcontainer.json`) runs it, so VS Code and Codespaces open the repository ready to run.
|
|
11
|
+
|
|
12
|
+
## 0.8.1 (2026-10-15)
|
|
13
|
+
|
|
14
|
+
- A run with no AI provider key set now stops at once with a message naming the keys to set and the `--scripted --no-ai` option, instead of starting the browser and failing at the first step with the SDK's "Could not resolve authentication method".
|
|
15
|
+
- Over MCP, `run_audit` with `scripted: false` and no key returns an error that tells the chat app what to do next: call it again with `scripted: true`, or browse with `start_audit`. It doesn't mention CLI flags.
|
|
16
|
+
- The README points to `ai-tag-debugger --help` for the full list of commands and options.
|
|
17
|
+
- `npm run demo` replays the demo shop with no AI model (`--no-ai`), so it works without an API key.
|
|
18
|
+
|
|
3
19
|
## 0.8.0 (2026-10-14)
|
|
4
20
|
|
|
5
21
|
- The browser installs itself the first time an audit starts, so connecting the MCP server no longer needs a separate `playwright install` step. Progress goes to stderr.
|
package/README.md
CHANGED
|
@@ -110,8 +110,12 @@ export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, or GEMINI_API_K
|
|
|
110
110
|
ai-tag-debugger run --site https://staging.yourshop.com --journey add_to_cart --headed
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
The global install also adds `aitd`, a short alias: `aitd run --site … --journey add_to_cart` does the same.
|
|
114
|
+
|
|
113
115
|
Or run it without installing: `npx -y @analitikisler/ai-tag-debugger run --site … --journey add_to_cart`.
|
|
114
116
|
|
|
117
|
+
Run `ai-tag-debugger --help` to see every command and option, such as `--providers`, `--scripted` and `--no-ai`.
|
|
118
|
+
|
|
115
119
|
### Try the demo shop (from source)
|
|
116
120
|
|
|
117
121
|
The repository includes a demo shop with three deliberate tracking bugs:
|
|
@@ -250,6 +254,8 @@ ai-tag-debugger mcp [--http] [--port <n>] [--host <addr>] [--token <secret>] [--
|
|
|
250
254
|
[--allow-private-network]
|
|
251
255
|
```
|
|
252
256
|
|
|
257
|
+
`aitd` is a short alias for `ai-tag-debugger` and takes the same commands and options.
|
|
258
|
+
|
|
253
259
|
The exit code is `1` when findings reach `--fail-on` (default `broken`), so it can gate a deploy. See [`examples/github-actions/ai-tag-debugger.yml`](examples/github-actions/ai-tag-debugger.yml) for running it after every deployment.
|
|
254
260
|
|
|
255
261
|
Output:
|
|
@@ -356,9 +362,12 @@ A hosted server, so you don't need a tunnel, is part of the [Analitik İşler](h
|
|
|
356
362
|
## Development
|
|
357
363
|
|
|
358
364
|
```bash
|
|
359
|
-
npm
|
|
365
|
+
npm run setup # checks Node, installs the dependencies and Chromium, says whether an AI provider key is set
|
|
366
|
+
npm test # parsers, checks, each AI provider and the MCP server, end to end against the demo shop
|
|
360
367
|
```
|
|
361
368
|
|
|
369
|
+
The repository also has a dev container (`.devcontainer/devcontainer.json`). Open it in VS Code with the Dev Containers extension, or in GitHub Codespaces, and it runs `npm run setup` for you, Chromium's system libraries included. To use an AI model there, add your key as a Codespaces secret or export it in the container's terminal.
|
|
370
|
+
|
|
362
371
|
The end-to-end tests use fake model clients, so they run without an API key.
|
|
363
372
|
|
|
364
373
|
## Hosted version
|
package/README.tr.md
CHANGED
|
@@ -110,8 +110,12 @@ export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, or GEMINI_API_K
|
|
|
110
110
|
ai-tag-debugger run --site https://staging.yourshop.com --journey add_to_cart --headed
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
Global kurulum kısa bir takma ad olan `aitd` komutunu da ekler: `aitd run --site … --journey add_to_cart` aynı işi yapar.
|
|
114
|
+
|
|
113
115
|
Ya da kurmadan çalıştırın: `npx -y @analitikisler/ai-tag-debugger run --site … --journey add_to_cart`.
|
|
114
116
|
|
|
117
|
+
Tüm komutları ve seçenekleri (`--providers`, `--scripted`, `--no-ai` gibi) görmek için `ai-tag-debugger --help` çalıştırın.
|
|
118
|
+
|
|
115
119
|
### Demo mağazayı deneyin (kaynak koddan)
|
|
116
120
|
|
|
117
121
|
Depoda, bilerek bırakılmış üç ölçümleme hatası olan bir demo mağaza var:
|
|
@@ -250,6 +254,8 @@ ai-tag-debugger mcp [--http] [--port <n>] [--host <addr>] [--token <secret>] [--
|
|
|
250
254
|
[--allow-private-network]
|
|
251
255
|
```
|
|
252
256
|
|
|
257
|
+
`aitd`, `ai-tag-debugger` için kısa bir takma addır ve aynı komut ve seçenekleri alır.
|
|
258
|
+
|
|
253
259
|
Bulgular `--fail-on` seviyesine (varsayılan `broken`) ulaştığında çıkış kodu `1` olur, böylece bir deploy'u durdurabilir. Her deploy sonrası çalıştırmak için [`examples/github-actions/ai-tag-debugger.yml`](examples/github-actions/ai-tag-debugger.yml) örneğine bakın.
|
|
254
260
|
|
|
255
261
|
Çıktılar:
|
|
@@ -356,9 +362,12 @@ Tünele gerek kalmayan, hazır bir adresle barındırılan sunucu, [Analitik İ
|
|
|
356
362
|
## Geliştirme
|
|
357
363
|
|
|
358
364
|
```bash
|
|
359
|
-
npm
|
|
365
|
+
npm run setup # checks Node, installs the dependencies and Chromium, says whether an AI provider key is set
|
|
366
|
+
npm test # parsers, checks, each AI provider and the MCP server, end to end against the demo shop
|
|
360
367
|
```
|
|
361
368
|
|
|
369
|
+
Depoda bir dev container (`.devcontainer/devcontainer.json`) da var. Depoyu VS Code'da Dev Containers eklentisiyle ya da GitHub Codespaces'te açtığınızda `npm run setup` sizin için çalışır ve Chromium'un sistem kütüphanelerini de kurar. Orada bir yapay zekâ modeli kullanmak için anahtarınızı Codespaces secret olarak ekleyin ya da container'ın terminalinde export edin.
|
|
370
|
+
|
|
362
371
|
Uçtan uca testler sahte model istemcileri kullanır, bu yüzden API anahtarı olmadan çalışır.
|
|
363
372
|
|
|
364
373
|
## Barındırılan sürüm
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analitikisler/ai-tag-debugger",
|
|
3
3
|
"mcpName": "io.github.analitikisler/ai-tag-debugger",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.2",
|
|
5
5
|
"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.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"ai-tag-debugger": "src/cli.js"
|
|
8
|
+
"ai-tag-debugger": "src/cli.js",
|
|
9
|
+
"aitd": "src/cli.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"src",
|
|
@@ -14,9 +15,10 @@
|
|
|
14
15
|
"CHANGELOG.md"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
18
|
+
"setup": "node scripts/setup.js",
|
|
17
19
|
"test": "node --test test/*.test.js",
|
|
18
20
|
"demo:serve": "node examples/serve.js",
|
|
19
|
-
"demo": "node src/cli.js run --config examples/demo.config.json --scripted --out reports/demo"
|
|
21
|
+
"demo": "node src/cli.js run --config examples/demo.config.json --scripted --no-ai --out reports/demo"
|
|
20
22
|
},
|
|
21
23
|
"engines": {
|
|
22
24
|
"node": ">=20"
|
package/src/agent.js
CHANGED
|
@@ -203,12 +203,27 @@ export async function runJourney(config, journey, viewport, opts = {}) {
|
|
|
203
203
|
throw err;
|
|
204
204
|
});
|
|
205
205
|
return { ...collect(session, journey, viewport), ...outcome, cookies: await session.cookies() };
|
|
206
|
+
} catch (err) {
|
|
207
|
+
// The model's API, the browser or the site can fail in many ways. That ends this journey, not the run,
|
|
208
|
+
// and what was captured until then is kept.
|
|
209
|
+
return { ...collect(session, journey, viewport), completed: false, note: journeyError(err) };
|
|
206
210
|
} finally {
|
|
207
211
|
if (opts.saveSite) opts.log?.(" Writing the recording…");
|
|
208
212
|
await session.close();
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
/** The note of a journey that ended with an error. */
|
|
217
|
+
export const journeyError = (err) => `The journey stopped with an error: ${err?.message ?? err}`;
|
|
218
|
+
|
|
219
|
+
/** A journey that failed before anything was captured, so the report can still list it. */
|
|
220
|
+
export function failedRun(journey, viewport, err, providers) {
|
|
221
|
+
return {
|
|
222
|
+
journeyId: journey.id, viewport, completed: false, note: journeyError(err), hits: [], steps: [], consentStep: null, rejectStep: null,
|
|
223
|
+
cookieLog: [], dataLayerLog: [], network: [], providers, skipped: {}, bodyBudgetStep: null, startedAt: Date.now(), pageErrors: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
212
227
|
export function collect(session, journey, viewport) {
|
|
213
228
|
return {
|
|
214
229
|
journeyId: journey.id,
|
package/src/audit.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Shared by the CLI and the MCP server's run_audit tool.
|
|
3
3
|
import { readFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { runJourney } from "./agent.js";
|
|
5
|
+
import { failedRun, runJourney } from "./agent.js";
|
|
6
|
+
import { VIEWPORTS } from "./browser.js";
|
|
6
7
|
import { checkRun, groupFindings, summarize } from "./checks.js";
|
|
7
8
|
import { explainFindings, renderCsv, renderHtml, renderMarkdown } from "./report.js";
|
|
8
9
|
import { LANGUAGES, messages } from "./i18n.js";
|
|
@@ -70,6 +71,10 @@ export async function runAudit(opts) {
|
|
|
70
71
|
const { journeys, plan } = planRun({ config, plan: opts.plan, picks: opts.journeys, goals: opts.goals, events });
|
|
71
72
|
config = { ...config, providers: resolveProviders(opts.providers ?? config.providers, plan) };
|
|
72
73
|
const viewports = opts.viewports?.length ? opts.viewports : config.viewports ?? ["desktop"];
|
|
74
|
+
const allowed = Object.keys(VIEWPORTS).join(", ");
|
|
75
|
+
if (!Array.isArray(viewports)) throw new Error(`"viewports" must be a list, e.g. ["desktop", "mobile"]. Allowed values: ${allowed}.`);
|
|
76
|
+
const unknown = viewports.filter((v) => !Object.hasOwn(VIEWPORTS, v));
|
|
77
|
+
if (unknown.length) throw new Error(`Unknown viewport ${unknown.map((v) => `"${v}"`).join(", ")}. Use: ${allowed}.`);
|
|
73
78
|
if (!journeys.length) throw new Error("No journeys to run. Pick one with --journey (e.g. purchase) or describe one with --goal.");
|
|
74
79
|
if (opts.scripted) {
|
|
75
80
|
const unscripted = journeys.filter((j) => !j.steps?.length && !j.incomplete).map((j) => j.id);
|
|
@@ -84,7 +89,9 @@ export async function runAudit(opts) {
|
|
|
84
89
|
for (const journey of journeys) {
|
|
85
90
|
for (const viewport of viewports) {
|
|
86
91
|
log(`\n▶ ${journey.id} (${viewport})`);
|
|
87
|
-
|
|
92
|
+
// runJourney keeps its own errors in the run. This catches the rest, such as a browser that won't close,
|
|
93
|
+
// so one journey can't cost the report of the others.
|
|
94
|
+
const run = await runJourney(config, journey, viewport, { scripted: opts.scripted, provider, maxSteps: opts.maxSteps, headed: opts.headed, lang, log, saveSite: opts.saveSite, replaySite: opts.replaySite }).catch((err) => failedRun(journey, viewport, err, config.providers));
|
|
88
95
|
log(` ${run.completed ? "✓" : "✗"} ${run.note} · ${run.hits.length} tracking hits captured`);
|
|
89
96
|
runs.push(run);
|
|
90
97
|
}
|
package/src/checks.js
CHANGED
|
@@ -5,6 +5,11 @@ import { messages } from "./i18n.js";
|
|
|
5
5
|
import { isOwnAddress } from "./redact.js";
|
|
6
6
|
|
|
7
7
|
const NON_GOOGLE_AD_PLATFORMS = new Set(["meta", "tiktok"]);
|
|
8
|
+
const GOOGLE_PLATFORMS = new Set(["ga4", "google_ads"]);
|
|
9
|
+
|
|
10
|
+
/** Whether a happened before b: an earlier step, or the same step and an earlier time when both have one. */
|
|
11
|
+
const sentBefore = (a, b) => a.step < b.step || (a.step === b.step && a.t != null && b.t != null && a.t < b.t);
|
|
12
|
+
|
|
8
13
|
const EMAIL = /[A-Z0-9._%+-]+(@|%40)[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
9
14
|
// Consent Mode v2 added two signals to ad_storage and analytics_storage. Google requires them for EEA/UK traffic.
|
|
10
15
|
const V2_SIGNALS = ["ad_user_data", "ad_personalization"];
|
|
@@ -222,12 +227,13 @@ export function checkRun(plan, run, { lang = "en" } = {}) {
|
|
|
222
227
|
|
|
223
228
|
// Consent.
|
|
224
229
|
const granted = consentGrantedStep(run);
|
|
225
|
-
|
|
230
|
+
// Consent Mode only governs Google tags. Within one step, the push and hit times decide which came first.
|
|
231
|
+
const firstGoogleHit = run.hits.filter((h) => GOOGLE_PLATFORMS.has(h.platform)).reduce((a, h) => (!a || sentBefore(h, a) ? h : a), null);
|
|
226
232
|
if (plan.consent_mode) {
|
|
227
233
|
const defaultEntry = run.dataLayerLog.find((e) => isConsentCommand(e, "default"));
|
|
228
234
|
if (!defaultEntry) {
|
|
229
235
|
findings.push({ ...base, severity: "risk", check: "consent_default_missing", detail: d.consentDefaultMissing() });
|
|
230
|
-
} else if (
|
|
236
|
+
} else if (firstGoogleHit && sentBefore(firstGoogleHit, defaultEntry)) {
|
|
231
237
|
findings.push({ ...base, severity: "risk", check: "consent_default_late", detail: d.consentDefaultLate() });
|
|
232
238
|
}
|
|
233
239
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { loadConfig, runAudit } from "./audit.js";
|
|
@@ -8,6 +9,8 @@ import { JOURNEY_PRESETS } from "./journeys.js";
|
|
|
8
9
|
import { PROVIDERS as TRACKING_PROVIDERS, DEFAULT_PROVIDERS } from "./parsers.js";
|
|
9
10
|
import { replaySavedSite } from "./replay.js";
|
|
10
11
|
|
|
12
|
+
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
13
|
+
|
|
11
14
|
const USAGE = `ai-tag-debugger: an open-source AI agent that QA-tests your analytics and ad tracking.
|
|
12
15
|
|
|
13
16
|
Usage:
|
|
@@ -61,6 +64,9 @@ MCP options:
|
|
|
61
64
|
--public-url <url> The HTTPS address your tunnel or host gives this server, so audits can link to their reports.
|
|
62
65
|
|
|
63
66
|
-h, --help Show this help.
|
|
67
|
+
-v, --version Print the version.
|
|
68
|
+
|
|
69
|
+
Short alias: aitd (aitd run ... is the same as ai-tag-debugger run ...).
|
|
64
70
|
|
|
65
71
|
API keys: set one of ${Object.values(PROVIDERS).map((p) => p.env).join(", ")}.
|
|
66
72
|
Any OpenAI-compatible API: --provider openai-compatible --base-url <url> --model <id>, with the key in OPENAI_COMPATIBLE_API_KEY.`;
|
|
@@ -99,9 +105,14 @@ async function main() {
|
|
|
99
105
|
"save-site": { type: "string" },
|
|
100
106
|
update: { type: "boolean", default: false },
|
|
101
107
|
help: { type: "boolean", short: "h", default: false },
|
|
108
|
+
version: { type: "boolean", short: "v", default: false },
|
|
102
109
|
},
|
|
103
110
|
});
|
|
104
111
|
const command = positionals[0];
|
|
112
|
+
if (values.version) {
|
|
113
|
+
console.log(VERSION);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
105
116
|
|
|
106
117
|
if (command === "mcp" && !values.help) {
|
|
107
118
|
const { startMcp } = await import("./mcp.js");
|
package/src/mcp.js
CHANGED
|
@@ -293,7 +293,16 @@ export function buildServer(opts) {
|
|
|
293
293
|
async ({ config: configFile, scripted, language, branding, providers }) => {
|
|
294
294
|
const loaded = await loadConfig(configFile);
|
|
295
295
|
const replay = scripted !== false;
|
|
296
|
-
|
|
296
|
+
let result;
|
|
297
|
+
try {
|
|
298
|
+
result = await runAudit({ ...loaded, scripted: replay, ai: !replay, lang: language, branding, providers, outDir: path.join(opts.reportsDir, new Date().toISOString().replace(/[:.]/g, "-")) });
|
|
299
|
+
} catch (err) {
|
|
300
|
+
// The CLI wording (--scripted --no-ai) means nothing to a chat app, so say it in this tool's terms.
|
|
301
|
+
if (err.code !== "NO_API_KEY") throw err;
|
|
302
|
+
return fail(
|
|
303
|
+
"No AI provider key (ANTHROPIC_API_KEY, OPENAI_API_KEY or GEMINI_API_KEY) is set on the computer running this server, so scripted false can't browse with a model. Call run_audit again with scripted true (or leave it out) to replay the journeys' fixed steps, or browse the site yourself with start_audit, browser_action and finish_audit. Neither needs a key.",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
297
306
|
const m = messages(result.lang);
|
|
298
307
|
const grouped = groupFindings(result.findings).filter((f) => f.severity !== "ok");
|
|
299
308
|
return text(
|
package/src/providers.js
CHANGED
|
@@ -28,6 +28,12 @@ export function resolveProviderName(name, env = process.env) {
|
|
|
28
28
|
return "anthropic";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const NO_API_KEY = `No AI provider key found. Set one of ANTHROPIC_API_KEY, OPENAI_API_KEY or GEMINI_API_KEY (or OPENAI_COMPATIBLE_BASE_URL for an OpenAI-compatible API), or pick one with --provider. To run without any AI model, replay the fixed steps with --scripted --no-ai.`;
|
|
32
|
+
|
|
33
|
+
function hasApiKey(env) {
|
|
34
|
+
return Boolean(env.OPENAI_COMPATIBLE_BASE_URL || env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || env.OPENAI_API_KEY || env.GEMINI_API_KEY || env.GOOGLE_API_KEY);
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
/**
|
|
32
38
|
* @param {{ name?: string, model?: string, baseUrl?: string, client?: any }} opts
|
|
33
39
|
* baseUrl: for openai-compatible, e.g. https://api.deepseek.com or http://localhost:11434/v1.
|
|
@@ -36,6 +42,10 @@ export function resolveProviderName(name, env = process.env) {
|
|
|
36
42
|
export async function createProvider(opts = {}, env = process.env) {
|
|
37
43
|
// A bare client (tests, embedding) is an Anthropic client unless named otherwise.
|
|
38
44
|
const name = opts.client && !opts.name ? "anthropic" : opts.baseUrl && !opts.name ? "openai-compatible" : resolveProviderName(opts.name, env);
|
|
45
|
+
// With no provider named and no key set, the SDK would only fail at the first step with an
|
|
46
|
+
// unclear auth error, after the browser has started. A named provider skips this check, since
|
|
47
|
+
// its SDK may find credentials elsewhere (an Anthropic profile, Google Cloud credentials).
|
|
48
|
+
if (!opts.client && !opts.name && !opts.baseUrl && !hasApiKey(env)) throw Object.assign(new Error(NO_API_KEY), { code: "NO_API_KEY" });
|
|
39
49
|
if (name === "openai-compatible") {
|
|
40
50
|
const baseURL = opts.baseUrl ?? env.OPENAI_COMPATIBLE_BASE_URL;
|
|
41
51
|
const model = opts.model ?? env.OPENAI_COMPATIBLE_MODEL;
|