@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 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 test # parsers, checks, each AI provider and the MCP server, end to end against the demo shop
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 test # parsers, checks, each AI provider and the MCP server, end to end against the demo shop
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.0",
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
- 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 });
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
- const firstHitStep = run.hits.length ? Math.min(...run.hits.map((h) => h.step)) : null;
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 (firstHitStep !== null && defaultEntry.step > firstHitStep) {
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
- const result = await runAudit({ ...loaded, scripted: replay, ai: !replay, lang: language, branding, providers, outDir: path.join(opts.reportsDir, new Date().toISOString().replace(/[:.]/g, "-")) });
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;