@analitikisler/ai-tag-debugger 0.21.0 → 0.22.1
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 +17 -0
- package/README.md +32 -3
- package/README.tr.md +31 -2
- package/package.json +4 -1
- package/src/agent.js +5 -3
- package/src/browser.js +111 -15
- package/src/checks.js +44 -12
- package/src/containers.js +164 -0
- package/src/discover.js +5 -2
- package/src/frames.js +90 -0
- package/src/html/scope.js +15 -1
- package/src/i18n.js +98 -0
- package/src/mcp.js +85 -10
- package/src/report.js +17 -0
package/src/mcp.js
CHANGED
|
@@ -33,6 +33,8 @@ import { decodeGcd, DEFAULT_PROVIDERS, googleConsent, isCollectionEndpoint, pars
|
|
|
33
33
|
import { hitParams } from "./hit-params.js";
|
|
34
34
|
import { CAMPAIGN_PARAMS, campaignParamsIn, withCampaignParams } from "./campaign.js";
|
|
35
35
|
import { cmpFacts } from "./cmp.js";
|
|
36
|
+
import { containerRows, loadSources } from "./containers.js";
|
|
37
|
+
import { isWidgetHit, siteIds } from "./frames.js";
|
|
36
38
|
import { consentMatrixMarkdown, consentTable, runConsentScenarios } from "./consent.js";
|
|
37
39
|
import { expectEvents, filterFindings, JOURNEY_PRESETS, onlyEvents, presetFor, withPresetEvents } from "./journeys.js";
|
|
38
40
|
|
|
@@ -90,25 +92,36 @@ const consentText = (c) => Object.entries(c).map(([k, v]) => `${k}=${v}`).join("
|
|
|
90
92
|
/** The Google protocol fields of a hit, short: values for the ids and consent strings, bare names for the 1-flags. */
|
|
91
93
|
const protocolText = (p) => Object.entries(p).map(([k, v]) => (["_fv", "_ss", "_nsi"].includes(k) && v === "1" ? k : `${k}=${v}`)).join(" ");
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
/** Where an iframe's hit came from, and whether the plan checks will count it (see src/frames.js). */
|
|
96
|
+
function frameNote(h, ids) {
|
|
97
|
+
if (!h.frame) return "";
|
|
98
|
+
if (h.frame.party === "site") return ` (from an iframe on the site: ${h.frame.origin})`;
|
|
99
|
+
return isWidgetHit(h, ids)
|
|
100
|
+
? ` (from an embedded widget at ${h.frame.origin}, to an account the site's tags don't use: left out of the plan checks, kept for consent and personal data)`
|
|
101
|
+
: ` (from a third-party iframe at ${h.frame.origin}, to the site's own account)`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function describeHit(h, steps, index, ids = new Set()) {
|
|
94
105
|
const entries = Object.entries(h.params ?? {}).filter(([k]) => !["v", "tid", "gtm", "_p", "cid", "sid", "_s", "sct", "seg", "dl", "dr", "dt", "ul", "sr", "_et", "en"].includes(k));
|
|
95
106
|
const params = [
|
|
96
107
|
...entries.filter(([k]) => !WHOLE_FIELDS.has(k)).slice(0, 12).map(([k, v]) => cut(`${k}=${show(v)}`, VALUE_LIMIT)),
|
|
97
108
|
...entries.filter(([k]) => WHOLE_FIELDS.has(k)).map(([k, v]) => `${k}=${show(v)}`),
|
|
98
109
|
].join(", ");
|
|
99
110
|
const late = h.late ? ` (arrived ${(h.late.ms / 1000).toFixed(1)} s late, after the step that loaded its page: "${steps[h.step]?.label ?? `step ${h.step}`}")` : "";
|
|
100
|
-
return `- [h${index}] ${h.platform} ${h.name}${h.consent ? ` [consent ${consentText(h.consent)}]` : ""}${h.protocol ? ` {${protocolText(h.protocol)}}` : ""}${params ? `: ${params}` : ""}${late}`;
|
|
111
|
+
return `- [h${index}] ${h.platform} ${h.name}${h.consent ? ` [consent ${consentText(h.consent)}]` : ""}${h.protocol ? ` {${protocolText(h.protocol)}}` : ""}${params ? `: ${params}` : ""}${late}${frameNote(h, ids)}`;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/** A consent command in full, as key=value; any other push as JSON, cut, with its page address fields whole. */
|
|
104
115
|
function describePush(p) {
|
|
116
|
+
// Pushes to a dataLayer of another name (a container's l=) say which.
|
|
117
|
+
const layer = `${p.frame ? `[iframe ${p.frame.origin}] ` : ""}${p.layer ? `[${p.layer}] ` : ""}`;
|
|
105
118
|
if (isConsentCommand(p, "default") || isConsentCommand(p, "update")) {
|
|
106
119
|
const signals = p.value[2] && typeof p.value[2] === "object" ? Object.entries(p.value[2]).map(([k, v]) => `${k}=${show(v)}`).join(" ") : show(p.value[2]);
|
|
107
|
-
return `- consent ${p.value[1]}: ${signals}`;
|
|
120
|
+
return `- ${layer}consent ${p.value[1]}: ${signals}`;
|
|
108
121
|
}
|
|
109
122
|
const json = JSON.stringify(p.value) ?? String(p.value);
|
|
110
123
|
const whole = json.length > PUSH_LIMIT ? wholeFieldsIn(p.value) : [];
|
|
111
|
-
return `- ${cut(json, PUSH_LIMIT)}${whole.length ? ` (${whole.map(([k, v]) => `${k}=${v}`).join(", ")})` : ""}`;
|
|
124
|
+
return `- ${layer}${cut(json, PUSH_LIMIT)}${whole.length ? ` (${whole.map(([k, v]) => `${k}=${v}`).join(", ")})` : ""}`;
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
/** A call to a tracking endpoint that carries no event the tool reads, such as a Consent Mode ping, with its consent. */
|
|
@@ -150,9 +163,17 @@ function cookieLines(session, step) {
|
|
|
150
163
|
return lines;
|
|
151
164
|
}
|
|
152
165
|
|
|
166
|
+
/** One container load in words: its ID, what inserted it, its dataLayer and where it came from. */
|
|
167
|
+
function describeLoad(l) {
|
|
168
|
+
const c = messages("en").ui.containers;
|
|
169
|
+
const extra = [l.layer !== "dataLayer" && `dataLayer ${l.layer}`, l.served === "site" && `served from ${l.host}`, l.environment && c.environment(l.environment), l.frame && c.iframe(l.frame.origin, l.frame.party)].filter(Boolean);
|
|
170
|
+
return ` ${l.id} (${c.script[l.script]}): ${c.kinds[l.kind](l.by)}${extra.length ? `; ${extra.join(", ")}` : ""}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
153
173
|
export function stepCapture(audit, step) {
|
|
154
174
|
const { session } = audit;
|
|
155
175
|
const hits = session.hits.filter((h) => h.step === step);
|
|
176
|
+
const ids = siteIds(session);
|
|
156
177
|
const pushes = session.dataLayerLog.filter((e) => e.step === step);
|
|
157
178
|
// Hits that arrived during this step for a page an earlier step loaded are listed there, and noted here.
|
|
158
179
|
const lateHere = session.hits.filter((h) => h.late?.during === step);
|
|
@@ -163,11 +184,13 @@ export function stepCapture(audit, step) {
|
|
|
163
184
|
...(problem?.kind === "crash" ? [`WARNING: the page crashed in the browser (the ${problem.vendor} error screen)${problem.refusedScripts ? `; ${problem.refusedScripts} of its own scripts came back as HTML pages, which bot protection or a mismatched deployment does` : ""}. Its tags may never have run.`] : []),
|
|
164
185
|
`Step ${step}: "${session.steps[step]?.label ?? ""}". Read a hit's full parameters with get_hit (audit_id, step ${step}, its [h] number).`,
|
|
165
186
|
`Tracking hits sent by this step: ${hits.length}`,
|
|
166
|
-
...hits.map((h, i) => describeHit(h, session.steps, i)),
|
|
187
|
+
...hits.map((h, i) => describeHit(h, session.steps, i, ids)),
|
|
167
188
|
];
|
|
168
189
|
if (lateHere.length) lines.push(`Arrived during this step but belong to the step that loaded their page: ${lateHere.map((h) => `${h.platform} ${h.name} (step "${session.steps[h.step]?.label ?? h.step}")`).join(", ")}`);
|
|
169
190
|
if (pings.length) lines.push("Other calls to analytics and ad endpoints, not read as events (Consent Mode pings, resends, remarketing):", ...pings.map(describeCall));
|
|
170
191
|
if (pushes.length) lines.push("dataLayer pushes:", ...pushes.map(describePush));
|
|
192
|
+
const loads = loadSources({ containerLoads: (session.containerLoads ?? []).filter((l) => l.step === step), dataLayerLog: session.dataLayerLog });
|
|
193
|
+
if (loads.length) lines.push("Google containers loaded:", ...loads.map(describeLoad));
|
|
171
194
|
lines.push(...cookieLines(session, step));
|
|
172
195
|
const tag = session.steps[step]?.tagConsent;
|
|
173
196
|
if (tag) {
|
|
@@ -214,6 +237,32 @@ export function hitDetail(audit, step, index) {
|
|
|
214
237
|
* hits it sent with their consent, its consent commands and other tracking calls, the Google tag's consent when it
|
|
215
238
|
* changed, and the cookies and ids that changed.
|
|
216
239
|
*/
|
|
240
|
+
/** A container load or row as compact JSON: the ID, its source, and the dataLayer, host and environment when they are not the usual ones. */
|
|
241
|
+
function containerJson(l) {
|
|
242
|
+
return {
|
|
243
|
+
id: l.id, source: l.kind, ...(l.by ? { by: l.by } : {}),
|
|
244
|
+
...(l.layer !== "dataLayer" ? { data_layer: l.layer } : {}),
|
|
245
|
+
...(l.served === "site" ? { served_from: l.host } : {}),
|
|
246
|
+
...(l.environment ? { gtm_environment: l.environment } : {}),
|
|
247
|
+
...(l.frame ? { iframe: { origin: l.frame.origin, party: l.frame.party } } : {}),
|
|
248
|
+
...(l.pages ? { pages: l.pages } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The Google containers of the runs and how each reached the page, as lines for a tool result; [] when none loaded. */
|
|
253
|
+
export function containerLines(runs) {
|
|
254
|
+
const rows = containerRows(runs);
|
|
255
|
+
if (!rows.length) return [];
|
|
256
|
+
const c = messages("en").ui.containers;
|
|
257
|
+
return [
|
|
258
|
+
"Google containers (how each reached the page):",
|
|
259
|
+
...rows.map((r) => {
|
|
260
|
+
const extra = [r.layer !== "dataLayer" && `dataLayer ${r.layer}`, r.served === "site" && `served from ${r.host}`, r.environment && c.environment(r.environment), r.frame && c.iframe(r.frame.origin, r.frame.party), `${r.pages.length} ${r.pages.length === 1 ? "page" : "pages"}`].filter(Boolean);
|
|
261
|
+
return ` ${r.id} (${c.script[r.script]}): ${c.kinds[r.kind](r.by)}; ${extra.join(", ")}`;
|
|
262
|
+
}),
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
217
266
|
export function timelineOf(run) {
|
|
218
267
|
let lastIds = null;
|
|
219
268
|
return (run.steps ?? []).map((st) => {
|
|
@@ -224,12 +273,17 @@ export function timelineOf(run) {
|
|
|
224
273
|
const names = (kind) => [...new Set(changes.filter((c) => c.change === kind).map((c) => c.name))];
|
|
225
274
|
const entry = { step: st.index, action: st.label, ...(st.pagePath ? { page: st.pagePath } : {}) };
|
|
226
275
|
if (st.error) entry.error = st.error;
|
|
227
|
-
if (hits.length) entry.hits = hits.map((h) => ({ platform: h.platform, event: h.name, ...(h.consent ? { consent: h.consent } : {}), ...(h.late ? { late_ms: h.late.ms } : {}) }));
|
|
276
|
+
if (hits.length) entry.hits = hits.map((h) => ({ platform: h.platform, event: h.name, ...(h.consent ? { consent: h.consent } : {}), ...(h.late ? { late_ms: h.late.ms } : {}), ...(h.frame ? { iframe: h.frame.origin } : {}) }));
|
|
277
|
+
// An embedded widget's own hits, kept apart from the site's (see src/frames.js).
|
|
278
|
+
const widget = (run.widgetHits ?? []).filter((h) => h.step === st.index);
|
|
279
|
+
if (widget.length) entry.widget_hits = widget.map((h) => ({ widget: h.frame.origin, platform: h.platform, event: h.name, ...(h.id ? { id: h.id } : {}), ...(h.consent ? { consent: h.consent } : {}) }));
|
|
228
280
|
if (others.length) entry.other_tracking_calls = others.length;
|
|
229
281
|
if (commands.length) entry.consent_commands = commands.map((e) => ({ [e.value[1]]: e.value[2] }));
|
|
230
282
|
if (st.tagConsent) entry.tag_consent = Object.fromEntries(Object.entries(st.tagConsent.entries).map(([k, e]) => [k, e.update ?? e.default ?? null]));
|
|
231
283
|
const cookies = Object.fromEntries(["added", "changed", "removed"].map((k) => [k, names(k)]).filter(([, list]) => list.length));
|
|
232
284
|
if (Object.keys(cookies).length) entry.cookies = cookies;
|
|
285
|
+
const loads = loadSources({ containerLoads: (run.containerLoads ?? []).filter((l) => l.step === st.index), dataLayerLog: run.dataLayerLog });
|
|
286
|
+
if (loads.length) entry.containers = loads.map(containerJson);
|
|
233
287
|
if (st.identity && JSON.stringify(st.identity) !== JSON.stringify(lastIds)) entry.ids = st.identity;
|
|
234
288
|
if (st.identity) lastIds = st.identity;
|
|
235
289
|
return entry;
|
|
@@ -466,15 +520,33 @@ export function buildServer(opts) {
|
|
|
466
520
|
},
|
|
467
521
|
);
|
|
468
522
|
|
|
523
|
+
server.registerTool(
|
|
524
|
+
"get_containers",
|
|
525
|
+
{
|
|
526
|
+
title: "List the Google containers loaded so far",
|
|
527
|
+
description:
|
|
528
|
+
"Returns every GTM container (gtm.js) and Google tag (gtag/js) this audit's pages loaded so far, and how each reached the page: a script tag in the HTML, the inline snippet, another script (named by its address, such as a consent platform), a Custom HTML tag or a Google tag in another GTM container, a container loading another itself (a Zone or a tag template), a gtag('config') command, an event's send_to with no config, or a destination of the Google tag. Also the dataLayer each reads, whether it was served from the site's own domain, the GTM environment when one is named, and the step it loaded at. Duplicates, GTM environments and split dataLayers are flagged by finish_audit.",
|
|
529
|
+
inputSchema: { audit_id: z.string() },
|
|
530
|
+
annotations: { readOnlyHint: true },
|
|
531
|
+
},
|
|
532
|
+
async ({ audit_id }) => {
|
|
533
|
+
const { session } = getAudit(audit_id);
|
|
534
|
+
const loads = loadSources({ containerLoads: session.containerLoads ?? [], dataLayerLog: session.dataLayerLog });
|
|
535
|
+
if (!loads.length) return text("No GTM container or Google tag script has loaded on this audit's pages yet.");
|
|
536
|
+
const lines = loads.map((l) => `Step ${l.step} ("${session.steps[l.step]?.label ?? ""}"), ${l.page ?? "?"}:\n${describeLoad(l)}`);
|
|
537
|
+
return text(`${loads.length} container ${loads.length === 1 ? "load" : "loads"}:\n${lines.join("\n")}\n\nAs JSON: ${JSON.stringify(loads.map((l) => ({ step: l.step, page: l.page, ...containerJson(l) })))}`);
|
|
538
|
+
},
|
|
539
|
+
);
|
|
540
|
+
|
|
469
541
|
server.registerTool(
|
|
470
542
|
"get_report",
|
|
471
543
|
{
|
|
472
544
|
title: "Read a report this server wrote",
|
|
473
545
|
description:
|
|
474
|
-
"Returns a report finish_audit or run_audit wrote, so it can be read without access to the computer running this server. part markdown (default): report.md, the verdict, findings, consent and journeys in about 120 lines. summary: summary.json, the counts, journeys and findings as JSON. timeline: one JSON entry per step of each journey (hits with their consent, consent commands, the Google tag's consent, cookies and ids).",
|
|
546
|
+
"Returns a report finish_audit or run_audit wrote, so it can be read without access to the computer running this server. part markdown (default): report.md, the verdict, findings, consent and journeys in about 120 lines. summary: summary.json, the counts, journeys and findings as JSON. timeline: one JSON entry per step of each journey (hits with their consent, consent commands, the Google tag's consent, cookies and ids, the Google containers loaded). containers: every GTM container and Google tag the journeys loaded as JSON, with how each reached the page (source html, inline, script, custom_html, google_tag, nested, config, send_to or destination, and by: the container or script that loaded it), its dataLayer, host, GTM environment and pages.",
|
|
475
547
|
inputSchema: {
|
|
476
548
|
report: z.string().describe("The report name finish_audit or run_audit gave."),
|
|
477
|
-
part: z.enum(["markdown", "summary", "timeline"]).optional().describe("Default markdown."),
|
|
549
|
+
part: z.enum(["markdown", "summary", "timeline", "containers"]).optional().describe("Default markdown."),
|
|
478
550
|
},
|
|
479
551
|
annotations: { readOnlyHint: true },
|
|
480
552
|
},
|
|
@@ -485,6 +557,7 @@ export function buildServer(opts) {
|
|
|
485
557
|
if (part === "markdown") return text(await readFile(path.join(dir, "report.md"), "utf8"));
|
|
486
558
|
if (part === "summary") return text(await readFile(path.join(dir, "summary.json"), "utf8"));
|
|
487
559
|
const capture = JSON.parse(await readFile(path.join(dir, "capture.json"), "utf8"));
|
|
560
|
+
if (part === "containers") return text(JSON.stringify(containerRows(capture.runs ?? []).map(containerJson)));
|
|
488
561
|
return text(JSON.stringify((capture.runs ?? []).map((r) => ({ journey: r.journeyId, viewport: r.viewport, steps: timelineOf(r) }))));
|
|
489
562
|
} catch (err) {
|
|
490
563
|
return fail(`Could not read the report: ${err.message}`);
|
|
@@ -594,13 +667,14 @@ export function buildServer(opts) {
|
|
|
594
667
|
`${run.hits.length} tracking hits, ${run.dataLayerLog.length} dataLayer pushes and ${run.network.length} analytics or ad network calls captured over ${run.steps.length} steps.`,
|
|
595
668
|
skippedLine(run),
|
|
596
669
|
...((platform) => (platform ? [`Consent platform: ${platform.vendor}. ${platform.facts.join(" ")}`] : []))(cmpFacts(messages("en"), [run])),
|
|
670
|
+
...containerLines([run]),
|
|
597
671
|
opts.allowFiles
|
|
598
672
|
? `Full report with the event timeline: ${reportPath}`
|
|
599
673
|
: opts.reportsUrl
|
|
600
674
|
? `Full report with the event timeline: ${opts.reportsUrl}/${path.basename(outDir)}/report.html`
|
|
601
675
|
: "",
|
|
602
676
|
"",
|
|
603
|
-
`To read the report here: get_report with report "${path.basename(outDir)}" (markdown, summary or
|
|
677
|
+
`To read the report here: get_report with report "${path.basename(outDir)}" (markdown, summary, timeline or containers).`,
|
|
604
678
|
"Explain each problem to the user with its likely cause and fix.",
|
|
605
679
|
];
|
|
606
680
|
return {
|
|
@@ -746,8 +820,9 @@ export function buildServer(opts) {
|
|
|
746
820
|
...(result.baseline ? [`${result.baseline.accepted} accepted from the baseline${result.baseline.partial ? `, ${result.baseline.partial} accepted in some places only (still counted)` : ""}, ${result.baseline.stale.length} stale baseline entries`] : []),
|
|
747
821
|
...result.runs.map((r) => `${r.journeyId} (${r.viewport}): ${r.completed ? "completed" : "not completed"}, ${r.note}`),
|
|
748
822
|
...grouped.map((f) => `- ${m.severity[f.severity]}${f.accepted ? ` (${m.ui.accepted})` : ""}: ${f.detail}`),
|
|
823
|
+
...containerLines(result.runs),
|
|
749
824
|
`Report: ${result.reportPath}`,
|
|
750
|
-
`To read the report here: get_report with report "${path.basename(result.outDir)}" (markdown, summary or
|
|
825
|
+
`To read the report here: get_report with report "${path.basename(result.outDir)}" (markdown, summary, timeline or containers).`,
|
|
751
826
|
].join("\n"),
|
|
752
827
|
);
|
|
753
828
|
},
|
package/src/report.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createProvider } from "./providers.js";
|
|
|
5
5
|
import { canonicalEvent, groupFindings, summarize } from "./checks.js";
|
|
6
6
|
import { messages } from "./i18n.js";
|
|
7
7
|
import { buildInventory } from "./inventory.js";
|
|
8
|
+
import { containerRows } from "./containers.js";
|
|
8
9
|
import { computeMetrics } from "./metrics.js";
|
|
9
10
|
import { historySummaryLines, sparkline } from "./history.js";
|
|
10
11
|
import { cardIds } from "./html/lib.js";
|
|
@@ -396,6 +397,21 @@ function mdInventory(m, runs) {
|
|
|
396
397
|
return lines;
|
|
397
398
|
}
|
|
398
399
|
|
|
400
|
+
/** The Google containers and how each reached the page, one row per container and source. Omitted when none loaded. */
|
|
401
|
+
function mdContainers(m, runs) {
|
|
402
|
+
const rows = containerRows(runs);
|
|
403
|
+
if (!rows.length) return [];
|
|
404
|
+
const c = m.ui.containers;
|
|
405
|
+
const lines = [`## ${m.ui.containersTitle}`, "", `| ${m.ui.containerCol} | ${m.ui.sourceCol} | ${m.ui.layerCol} | ${m.ui.servedCol} | ${m.ui.pagesCol} |`, "|---|---|---|---|---|"];
|
|
406
|
+
for (const r of rows.slice(0, MD_INVENTORY_ROWS)) {
|
|
407
|
+
const served = r.served === "google" ? c.served.google : `${c.served.site} (${r.host})`;
|
|
408
|
+
lines.push(`| ${mdText(r.id)} (${c.script[r.script]}) | ${mdText(c.kinds[r.kind](r.by))}${r.frame ? `, ${mdText(c.iframe(r.frame.origin, r.frame.party))}` : ""}${r.environment ? `, ${mdText(c.environment(r.environment))}` : ""} | ${mdText(r.layer)} | ${mdText(served)} | ${r.pages.length} |`);
|
|
409
|
+
}
|
|
410
|
+
if (rows.length > MD_INVENTORY_ROWS) lines.push("", m.ui.md.moreIn(rows.length - MD_INVENTORY_ROWS, "report.html"));
|
|
411
|
+
lines.push("");
|
|
412
|
+
return lines;
|
|
413
|
+
}
|
|
414
|
+
|
|
399
415
|
/** The trend in up to five lines: the text sparkline with the direction, then the open-longest and flapping lines. */
|
|
400
416
|
function mdTrend(m, trend, lang) {
|
|
401
417
|
if (!trend || trend.runs < 2) return [];
|
|
@@ -447,6 +463,7 @@ export function renderMarkdown({ title, site = null, generatedAt, findings, expl
|
|
|
447
463
|
lines.push(...mdJourneys(m, runs, rows));
|
|
448
464
|
lines.push(...mdNotChecked(m, runs, rows, plan, consentScenarios));
|
|
449
465
|
lines.push(...mdInventory(m, runs));
|
|
466
|
+
lines.push(...mdContainers(m, runs));
|
|
450
467
|
// A report with its own name or logo carries no Analitik İşler credit; the ai tag debugger one stays.
|
|
451
468
|
if (brand?.name || brand?.logo) lines.push("---", "", `_${brand.name ? `${brand.name} · ` : ""}${m.ui.generatedBy}_`, "");
|
|
452
469
|
else if (branding) lines.push("---", "", `_${m.ui.generatedBy} · [analitikisler.com](https://analitikisler.com)_`, "");
|