@adgine/geo-cli 1.2.8
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/README.md +93 -0
- package/bin/adgine-geo.js +224 -0
- package/package.json +38 -0
- package/skills/.env.example +11 -0
- package/skills/README.md +247 -0
- package/skills/SKILL.md +117 -0
- package/skills/VERSION +1 -0
- package/skills/adgine-geo-aiagent/SKILL.md +200 -0
- package/skills/adgine-geo-aiagent/scripts/_client.py +341 -0
- package/skills/adgine-geo-aiagent/scripts/bot_traffic.py +278 -0
- package/skills/adgine-geo-aiagent/scripts/human_traffic.py +357 -0
- package/skills/adgine-geo-aiagent/scripts/page_analytics.py +317 -0
- package/skills/adgine-geo-aiagent/scripts/page_detail.py +273 -0
- package/skills/adgine-geo-analytics/API.md +89 -0
- package/skills/adgine-geo-analytics/SKILL.md +191 -0
- package/skills/adgine-geo-analytics/scripts/_client.py +341 -0
- package/skills/adgine-geo-analytics/scripts/get_overview.py +129 -0
- package/skills/adgine-geo-billing/SKILL.md +118 -0
- package/skills/adgine-geo-billing/scripts/_client.py +341 -0
- package/skills/adgine-geo-billing/scripts/get_credits.py +51 -0
- package/skills/adgine-geo-billing/scripts/get_credits_pricing.py +52 -0
- package/skills/adgine-geo-billing/scripts/get_subscription.py +48 -0
- package/skills/adgine-geo-billing/scripts/list_plans.py +66 -0
- package/skills/adgine-geo-brand/SKILL.md +193 -0
- package/skills/adgine-geo-brand/WORKFLOW.md +66 -0
- package/skills/adgine-geo-brand/scripts/_client.py +341 -0
- package/skills/adgine-geo-brand/scripts/generate_brand.py +95 -0
- package/skills/adgine-geo-brand/scripts/get_brand.py +67 -0
- package/skills/adgine-geo-brand/scripts/list_jobs.py +153 -0
- package/skills/adgine-geo-brand/scripts/update_brand.py +61 -0
- package/skills/adgine-geo-citation/SKILL.md +220 -0
- package/skills/adgine-geo-citation/scripts/_client.py +341 -0
- package/skills/adgine-geo-citation/scripts/create_tests.py +62 -0
- package/skills/adgine-geo-citation/scripts/get_aggregate.py +108 -0
- package/skills/adgine-geo-citation/scripts/get_results.py +179 -0
- package/skills/adgine-geo-content/SKILL.md +224 -0
- package/skills/adgine-geo-content/WORKFLOW.md +100 -0
- package/skills/adgine-geo-content/scripts/_client.py +341 -0
- package/skills/adgine-geo-content/scripts/generate_article.py +108 -0
- package/skills/adgine-geo-content/scripts/generate_outline.py +113 -0
- package/skills/adgine-geo-content/scripts/generate_titles.py +51 -0
- package/skills/adgine-geo-content/scripts/list_content.py +65 -0
- package/skills/adgine-geo-content/scripts/manage_content.py +90 -0
- package/skills/adgine-geo-content/scripts/manage_jobs.py +184 -0
- package/skills/adgine-geo-dashboard/SKILL.md +181 -0
- package/skills/adgine-geo-dashboard/scripts/_client.py +341 -0
- package/skills/adgine-geo-dashboard/scripts/check_integrations.py +139 -0
- package/skills/adgine-geo-dashboard/scripts/get_overview.py +155 -0
- package/skills/adgine-geo-dashboard/scripts/get_visibility_overview.py +93 -0
- package/skills/adgine-geo-docs/SKILL.md +60 -0
- package/skills/adgine-geo-domains/SKILL.md +223 -0
- package/skills/adgine-geo-domains/scripts/_client.py +341 -0
- package/skills/adgine-geo-domains/scripts/get_domain.py +75 -0
- package/skills/adgine-geo-domains/scripts/list_domains.py +66 -0
- package/skills/adgine-geo-domains/scripts/search_domains.py +85 -0
- package/skills/adgine-geo-integrations/SKILL.md +188 -0
- package/skills/adgine-geo-integrations/scripts/_client.py +341 -0
- package/skills/adgine-geo-integrations/scripts/cloudflare_connect.py +155 -0
- package/skills/adgine-geo-integrations/scripts/cloudflare_worker.py +212 -0
- package/skills/adgine-geo-integrations/scripts/ga4_data.py +192 -0
- package/skills/adgine-geo-integrations/scripts/ga4_setup.py +120 -0
- package/skills/adgine-geo-opportunities/SKILL.md +162 -0
- package/skills/adgine-geo-opportunities/scripts/_client.py +341 -0
- package/skills/adgine-geo-opportunities/scripts/get_opportunity.py +90 -0
- package/skills/adgine-geo-opportunities/scripts/list_opportunities.py +92 -0
- package/skills/adgine-geo-performance/SKILL.md +177 -0
- package/skills/adgine-geo-performance/scripts/_client.py +341 -0
- package/skills/adgine-geo-performance/scripts/analyze_page.py +100 -0
- package/skills/adgine-geo-projects/SKILL.md +279 -0
- package/skills/adgine-geo-projects/scripts/_client.py +341 -0
- package/skills/adgine-geo-projects/scripts/check_auth.py +45 -0
- package/skills/adgine-geo-projects/scripts/list_projects.py +47 -0
- package/skills/adgine-geo-projects/scripts/manage_competitors.py +122 -0
- package/skills/adgine-geo-projects/scripts/manage_project.py +88 -0
- package/skills/adgine-geo-saas/SKILL.md +144 -0
- package/skills/adgine-geo-saas/scripts/_client.py +341 -0
- package/skills/adgine-geo-saas/scripts/check_domain.py +43 -0
- package/skills/adgine-geo-saas/scripts/create_website.py +56 -0
- package/skills/adgine-geo-saas/scripts/get_task.py +94 -0
- package/skills/adgine-geo-site-audit/README.md +281 -0
- package/skills/adgine-geo-site-audit/SKILL.md +449 -0
- package/skills/adgine-geo-site-audit/requirements.txt +5 -0
- package/skills/adgine-geo-site-audit/scripts/_client.py +341 -0
- package/skills/adgine-geo-site-audit/scripts/_geo_notice.py +55 -0
- package/skills/adgine-geo-site-audit/scripts/export_skill_package.py +197 -0
- package/skills/adgine-geo-site-audit/scripts/geo_collect.py +2443 -0
- package/skills/adgine-geo-site-audit/scripts/geo_score.py +490 -0
- package/skills/adgine-geo-site-audit/scripts/geo_timing.py +399 -0
- package/skills/adgine-geo-site-audit/scripts/geo_visibility.py +718 -0
- package/skills/adgine-geo-site-audit/scripts/render_report_pdf.py +1502 -0
- package/skills/adgine-geo-topics/SKILL.md +232 -0
- package/skills/adgine-geo-topics/scripts/_client.py +341 -0
- package/skills/adgine-geo-topics/scripts/generate_prompts.py +140 -0
- package/skills/adgine-geo-topics/scripts/manage_prompts.py +153 -0
- package/skills/adgine-geo-topics/scripts/manage_topics.py +145 -0
- package/skills/adgine-geo-visibility/SKILL.md +168 -0
- package/skills/adgine-geo-visibility/scripts/_client.py +341 -0
- package/skills/adgine-geo-visibility/scripts/get_execution.py +151 -0
- package/skills/adgine-geo-visibility/scripts/get_matrix.py +111 -0
- package/skills/adgine-geo-visibility/scripts/get_prompt_metrics.py +104 -0
- package/skills/adgine-geo-visibility/scripts/get_topic_metrics.py +201 -0
- package/skills/adgine-geo-visibility/scripts/get_visibility.py +166 -0
- package/skills/adgine-geo-wordpress/SKILL.md +151 -0
- package/skills/adgine-geo-wordpress/scripts/_client.py +341 -0
- package/skills/adgine-geo-wordpress/scripts/list_publishable.py +62 -0
- package/skills/adgine-geo-wordpress/scripts/manage_credentials.py +139 -0
- package/skills/adgine-geo-wordpress/scripts/manage_publishes.py +108 -0
- package/skills/adgine-geo-wordpress/scripts/publish.py +79 -0
- package/skills/scripts/check_version.py +290 -0
- package/skills/setup.py +248 -0
- package/src/config.js +126 -0
- package/src/doctor.js +137 -0
- package/src/install.js +90 -0
- package/src/manifest.js +99 -0
- package/src/runner.js +159 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Site-wide AI traffic analytics — overall KPI, page rankings, Sankey, raw logs.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
overview-kpi — site overall KPIs (5 cards + trends):
|
|
6
|
+
citation / index / training / agent / referral
|
|
7
|
+
pages [--page 1] [--limit 20] — top pages by AI references
|
|
8
|
+
pages-detail [--limit 20] — 5-metric table per page (citation/index/training/
|
|
9
|
+
agent/referral) with previous-period delta
|
|
10
|
+
pages-export [--format csv|json] — download pages-detail as CSV/JSON
|
|
11
|
+
platform-flow — Sankey: AI platform → page path
|
|
12
|
+
logs [--limit 50] — raw AI event logs (bot + human, paginated)
|
|
13
|
+
|
|
14
|
+
Common opts: --start --end [--platform]
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 scripts/page_analytics.py overview-kpi
|
|
18
|
+
python3 scripts/page_analytics.py pages-detail --start 2025-11-01
|
|
19
|
+
python3 scripts/page_analytics.py pages-export --format csv > pages.csv
|
|
20
|
+
"""
|
|
21
|
+
import sys
|
|
22
|
+
import os
|
|
23
|
+
import argparse
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
26
|
+
from _client import (
|
|
27
|
+
get_api_config, get_project_id,
|
|
28
|
+
api_get,
|
|
29
|
+
extract_data, print_json, truncate,
|
|
30
|
+
pad,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fmt_num(n):
|
|
35
|
+
if n is None:
|
|
36
|
+
return "--"
|
|
37
|
+
try:
|
|
38
|
+
f = float(n)
|
|
39
|
+
if f == int(f):
|
|
40
|
+
return f"{int(f):,}"
|
|
41
|
+
return f"{f:,.1f}"
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return str(n)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fmt_change(c):
|
|
47
|
+
if c is None:
|
|
48
|
+
return "--"
|
|
49
|
+
try:
|
|
50
|
+
f = float(c)
|
|
51
|
+
sign = "+" if f >= 0 else ""
|
|
52
|
+
return f"{sign}{f:.1f}%"
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
return str(c)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _date_params(args):
|
|
58
|
+
p = {}
|
|
59
|
+
if args.start:
|
|
60
|
+
p["start_date"] = args.start
|
|
61
|
+
if args.end:
|
|
62
|
+
p["end_date"] = args.end
|
|
63
|
+
if getattr(args, "platform", None):
|
|
64
|
+
p["platform"] = args.platform
|
|
65
|
+
return p or None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_overview_kpi(args, key, base, pid):
|
|
69
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/overview-kpi",
|
|
70
|
+
key, base, params=_date_params(args))
|
|
71
|
+
data = extract_data(result) or {}
|
|
72
|
+
if args.json:
|
|
73
|
+
print_json(data)
|
|
74
|
+
return
|
|
75
|
+
# This endpoint returns a flat dict:
|
|
76
|
+
# {ai_citations, ai_index, ai_training, ai_agent, human_referrals, ai_human_ratio, ...}
|
|
77
|
+
# Each value is a {current, prev, delta, delta_pct, ...} dict.
|
|
78
|
+
|
|
79
|
+
def _get_kpi(d, k):
|
|
80
|
+
v = d.get(k)
|
|
81
|
+
if isinstance(v, dict):
|
|
82
|
+
cur = v.get("current")
|
|
83
|
+
delta_pct = v.get("delta_pct")
|
|
84
|
+
if delta_pct is not None:
|
|
85
|
+
ch_str = f"+{delta_pct:.1f}%" if delta_pct >= 0 else f"{delta_pct:.1f}%"
|
|
86
|
+
else:
|
|
87
|
+
ch_str = "--"
|
|
88
|
+
else:
|
|
89
|
+
cur, ch_str = v, "--"
|
|
90
|
+
return cur, ch_str
|
|
91
|
+
|
|
92
|
+
print("Site AI Overview — 5 KPI cards")
|
|
93
|
+
print()
|
|
94
|
+
print("```")
|
|
95
|
+
print("┌────────────────────┬──────────────┬──────────────┐")
|
|
96
|
+
print("│ KPI │ Current │ Change │")
|
|
97
|
+
print("├────────────────────┼──────────────┼──────────────┤")
|
|
98
|
+
for k, label in [
|
|
99
|
+
("ai_citations", "AI Citation"),
|
|
100
|
+
("ai_index", "AI Index"),
|
|
101
|
+
("ai_training", "AI Training"),
|
|
102
|
+
("ai_agent", "AI Agent"),
|
|
103
|
+
("human_referrals","AI Referral"),
|
|
104
|
+
("ai_human_ratio", "AI Referral %"),
|
|
105
|
+
]:
|
|
106
|
+
cur, ch_str = _get_kpi(data, k)
|
|
107
|
+
print(f"│ {pad(label, 18)} │ {_fmt_num(cur):>12} │ {ch_str:>12} │")
|
|
108
|
+
print("└────────────────────┴──────────────┴──────────────┘")
|
|
109
|
+
print("```")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cmd_pages(args, key, base, pid):
|
|
113
|
+
params = _date_params(args) or {}
|
|
114
|
+
params["page"] = args.page
|
|
115
|
+
params["limit"] = args.limit
|
|
116
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages",
|
|
117
|
+
key, base, params=params)
|
|
118
|
+
data = extract_data(result)
|
|
119
|
+
# API returns {items, total, ...} not a bare list or {"pages": [...]}
|
|
120
|
+
if isinstance(data, list):
|
|
121
|
+
items = data
|
|
122
|
+
elif isinstance(data, dict):
|
|
123
|
+
items = data.get("items") or data.get("pages", [])
|
|
124
|
+
else:
|
|
125
|
+
items = []
|
|
126
|
+
total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
|
|
127
|
+
if args.json:
|
|
128
|
+
print_json(items)
|
|
129
|
+
return
|
|
130
|
+
if not items:
|
|
131
|
+
print("No page data.")
|
|
132
|
+
return
|
|
133
|
+
print(f"Top AI-referenced pages ({total} total, showing {len(items)})")
|
|
134
|
+
print()
|
|
135
|
+
print("```")
|
|
136
|
+
print("┌────────────────────────────────────────────┬──────────┬────────┬──────────┐")
|
|
137
|
+
print("│ Page │ Bot Hits │ Human │ Total AI │")
|
|
138
|
+
print("├────────────────────────────────────────────┼──────────┼────────┼──────────┤")
|
|
139
|
+
for p in items:
|
|
140
|
+
path = truncate(p.get("path") or p.get("url"), 42)
|
|
141
|
+
bot = _fmt_num(p.get("bot_requests") or p.get("ai_hits") or p.get("count"))
|
|
142
|
+
human = _fmt_num(p.get("human_referral_requests"))
|
|
143
|
+
total_ai = _fmt_num(p.get("total_ai_requests") or p.get("references"))
|
|
144
|
+
print(f"│ {pad(path, 42)} │ {bot:>8} │ {human:>6} │ {total_ai:>8} │")
|
|
145
|
+
print("└────────────────────────────────────────────┴──────────┴────────┴──────────┘")
|
|
146
|
+
print("```")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def cmd_pages_detail(args, key, base, pid):
|
|
150
|
+
params = _date_params(args) or {}
|
|
151
|
+
params["page"] = args.page
|
|
152
|
+
params["limit"] = args.limit
|
|
153
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages-detail",
|
|
154
|
+
key, base, params=params)
|
|
155
|
+
data = extract_data(result)
|
|
156
|
+
# API returns {items, total, ...} not a bare list or {"pages": [...]}
|
|
157
|
+
if isinstance(data, list):
|
|
158
|
+
items = data
|
|
159
|
+
elif isinstance(data, dict):
|
|
160
|
+
items = data.get("items") or data.get("pages", [])
|
|
161
|
+
else:
|
|
162
|
+
items = []
|
|
163
|
+
total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
|
|
164
|
+
if args.json:
|
|
165
|
+
print_json(items)
|
|
166
|
+
return
|
|
167
|
+
if not items:
|
|
168
|
+
print("No page detail data.")
|
|
169
|
+
return
|
|
170
|
+
print(f"Pages — 5-metric detail ({total} total, showing {len(items)})")
|
|
171
|
+
print()
|
|
172
|
+
print("```")
|
|
173
|
+
print("┌────────────────────────────────┬──────┬──────┬──────┬──────┬──────┐")
|
|
174
|
+
print("│ Page │ Cite │ Indx │ Trn │ Agnt │ Ref │")
|
|
175
|
+
print("├────────────────────────────────┼──────┼──────┼──────┼──────┼──────┤")
|
|
176
|
+
for p in items:
|
|
177
|
+
path = truncate(p.get("path") or p.get("url"), 30)
|
|
178
|
+
c = _fmt_num(p.get("ai_citation"))
|
|
179
|
+
i = _fmt_num(p.get("ai_search") or p.get("ai_index"))
|
|
180
|
+
t = _fmt_num(p.get("ai_training"))
|
|
181
|
+
a = _fmt_num(p.get("ai_agent"))
|
|
182
|
+
r = _fmt_num(p.get("ai_referral") or p.get("ai_referral_visits"))
|
|
183
|
+
print(f"│ {pad(path, 30)} │ {c:>4} │ {i:>4} │ {t:>4} │ {a:>4} │ {r:>4} │")
|
|
184
|
+
print("└────────────────────────────────┴──────┴──────┴──────┴──────┴──────┘")
|
|
185
|
+
print("```")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def cmd_pages_export(args, key, base, pid):
|
|
189
|
+
params = _date_params(args) or {}
|
|
190
|
+
params["format"] = args.format
|
|
191
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages-detail/export",
|
|
192
|
+
key, base, params=params)
|
|
193
|
+
# Export returns raw CSV or JSON — pass through.
|
|
194
|
+
if isinstance(result, (dict, list)):
|
|
195
|
+
if args.format == "json":
|
|
196
|
+
print_json(result)
|
|
197
|
+
else:
|
|
198
|
+
print(result)
|
|
199
|
+
else:
|
|
200
|
+
print(result)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_platform_flow(args, key, base, pid):
|
|
204
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages-platform-flow",
|
|
205
|
+
key, base, params=_date_params(args))
|
|
206
|
+
data = extract_data(result) or {}
|
|
207
|
+
if args.json:
|
|
208
|
+
print_json(data)
|
|
209
|
+
return
|
|
210
|
+
nodes = data.get("nodes", [])
|
|
211
|
+
links = data.get("links", [])
|
|
212
|
+
print(f"Sankey AI platform → page: {len(nodes)} nodes, {len(links)} links")
|
|
213
|
+
print()
|
|
214
|
+
sorted_links = sorted(links, key=lambda l: l.get("value", 0), reverse=True)[:15]
|
|
215
|
+
for ln in sorted_links:
|
|
216
|
+
s = ln.get("source")
|
|
217
|
+
t = ln.get("target")
|
|
218
|
+
v = _fmt_num(ln.get("value"))
|
|
219
|
+
print(f" {s} → {t} ({v})")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_logs(args, key, base, pid):
|
|
223
|
+
params = _date_params(args) or {}
|
|
224
|
+
params["page"] = args.page
|
|
225
|
+
params["limit"] = args.limit
|
|
226
|
+
if args.traffic_type:
|
|
227
|
+
params["traffic_type"] = args.traffic_type
|
|
228
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/logs",
|
|
229
|
+
key, base, params=params)
|
|
230
|
+
data = extract_data(result)
|
|
231
|
+
# API returns {items, total, page, limit, pages}
|
|
232
|
+
if isinstance(data, list):
|
|
233
|
+
items = data
|
|
234
|
+
total = len(data)
|
|
235
|
+
elif isinstance(data, dict):
|
|
236
|
+
items = data.get("items") or data.get("logs") or data.get("events", [])
|
|
237
|
+
total = data.get("total", len(items))
|
|
238
|
+
else:
|
|
239
|
+
items = []
|
|
240
|
+
total = 0
|
|
241
|
+
if args.json:
|
|
242
|
+
print_json(items)
|
|
243
|
+
return
|
|
244
|
+
if not items:
|
|
245
|
+
print("No log events.")
|
|
246
|
+
return
|
|
247
|
+
print(f"Raw AI event logs (total {total}, showing {len(items)})")
|
|
248
|
+
print()
|
|
249
|
+
print("```")
|
|
250
|
+
print("┌────────────────────┬────────────────────┬────────────┬──────────────────────────┐")
|
|
251
|
+
print("│ Time (UTC) │ Type │ Platform │ Path │")
|
|
252
|
+
print("├────────────────────┼────────────────────┼────────────┼──────────────────────────┤")
|
|
253
|
+
for e in items:
|
|
254
|
+
t = truncate(
|
|
255
|
+
(e.get("occurred_at") or e.get("timestamp") or e.get("created_at") or "")[:19].replace("T", " "),
|
|
256
|
+
18)
|
|
257
|
+
ttype = truncate(
|
|
258
|
+
e.get("traffic_type_label") or e.get("bot_name") or e.get("traffic_type") or "(human)",
|
|
259
|
+
18)
|
|
260
|
+
plat = truncate(e.get("platform_name") or e.get("platform") or "--", 10)
|
|
261
|
+
path = truncate(e.get("path") or e.get("url"), 24)
|
|
262
|
+
print(f"│ {pad(t, 18)} │ {pad(ttype, 18)} │ {pad(plat, 10)} │ {pad(path, 24)} │")
|
|
263
|
+
print("└────────────────────┴────────────────────┴────────────┴──────────────────────────┘")
|
|
264
|
+
print("```")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main():
|
|
268
|
+
parser = argparse.ArgumentParser(description="Site-wide AI traffic analytics")
|
|
269
|
+
parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
|
|
270
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
271
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
272
|
+
|
|
273
|
+
for name in ("overview-kpi", "platform-flow"):
|
|
274
|
+
p = sub.add_parser(name)
|
|
275
|
+
p.add_argument("--start")
|
|
276
|
+
p.add_argument("--end")
|
|
277
|
+
p.add_argument("--platform")
|
|
278
|
+
|
|
279
|
+
for name in ("pages", "pages-detail"):
|
|
280
|
+
p = sub.add_parser(name)
|
|
281
|
+
p.add_argument("--start")
|
|
282
|
+
p.add_argument("--end")
|
|
283
|
+
p.add_argument("--platform")
|
|
284
|
+
p.add_argument("--page", type=int, default=1)
|
|
285
|
+
p.add_argument("--limit", type=int, default=20)
|
|
286
|
+
|
|
287
|
+
p_ex = sub.add_parser("pages-export")
|
|
288
|
+
p_ex.add_argument("--start")
|
|
289
|
+
p_ex.add_argument("--end")
|
|
290
|
+
p_ex.add_argument("--platform")
|
|
291
|
+
p_ex.add_argument("--format", choices=["csv", "json"], default="csv")
|
|
292
|
+
|
|
293
|
+
p_lg = sub.add_parser("logs")
|
|
294
|
+
p_lg.add_argument("--start")
|
|
295
|
+
p_lg.add_argument("--end")
|
|
296
|
+
p_lg.add_argument("--platform")
|
|
297
|
+
p_lg.add_argument("--page", type=int, default=1)
|
|
298
|
+
p_lg.add_argument("--limit", type=int, default=50)
|
|
299
|
+
p_lg.add_argument("--traffic-type", choices=["bot", "human"])
|
|
300
|
+
|
|
301
|
+
args = parser.parse_args()
|
|
302
|
+
key, base = get_api_config()
|
|
303
|
+
pid = get_project_id(args.project_id)
|
|
304
|
+
|
|
305
|
+
handlers = {
|
|
306
|
+
"overview-kpi": cmd_overview_kpi,
|
|
307
|
+
"pages": cmd_pages,
|
|
308
|
+
"pages-detail": cmd_pages_detail,
|
|
309
|
+
"pages-export": cmd_pages_export,
|
|
310
|
+
"platform-flow": cmd_platform_flow,
|
|
311
|
+
"logs": cmd_logs,
|
|
312
|
+
}
|
|
313
|
+
handlers[args.command](args, key, base, pid)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
main()
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deep-dive analytics for a specific page path.
|
|
3
|
+
|
|
4
|
+
All subcommands require --path (URL path on the project's website).
|
|
5
|
+
|
|
6
|
+
Subcommands:
|
|
7
|
+
kpi --path <p> — 5 KPI cards for this page (citation/index/training/
|
|
8
|
+
agent/total bots) with delta + daily trend
|
|
9
|
+
logs --path <p> — recent AI access events on this exact path
|
|
10
|
+
[--limit 50] [--traffic-type bot|human]
|
|
11
|
+
platforms --path <p> — per-platform 4-AI + human-referral + share table
|
|
12
|
+
related --path <p> — sibling pages under the same parent path, with
|
|
13
|
+
5-metric comparison
|
|
14
|
+
health --path <p> — PageSpeed Insights report (cached). Returns
|
|
15
|
+
report=null if never analyzed.
|
|
16
|
+
health-refresh --path <p> — synchronously refresh PSI report (15–60s blocking)
|
|
17
|
+
|
|
18
|
+
Common opts: --start --end
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
python3 scripts/page_detail.py kpi --path /blog/my-article
|
|
22
|
+
python3 scripts/page_detail.py logs --path /blog/my-article --limit 20
|
|
23
|
+
python3 scripts/page_detail.py health --path /blog/my-article
|
|
24
|
+
"""
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
import argparse
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
30
|
+
from _client import (
|
|
31
|
+
get_api_config, get_project_id,
|
|
32
|
+
api_get, api_post,
|
|
33
|
+
extract_data, print_json, truncate,
|
|
34
|
+
pad,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fmt_num(n):
|
|
39
|
+
if n is None:
|
|
40
|
+
return "--"
|
|
41
|
+
try:
|
|
42
|
+
f = float(n)
|
|
43
|
+
if f == int(f):
|
|
44
|
+
return f"{int(f):,}"
|
|
45
|
+
return f"{f:,.1f}"
|
|
46
|
+
except (TypeError, ValueError):
|
|
47
|
+
return str(n)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fmt_change(c):
|
|
51
|
+
if c is None:
|
|
52
|
+
return "--"
|
|
53
|
+
try:
|
|
54
|
+
f = float(c)
|
|
55
|
+
sign = "+" if f >= 0 else ""
|
|
56
|
+
return f"{sign}{f:.1f}%"
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
return str(c)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _base_params(args):
|
|
62
|
+
p = {"path": args.path}
|
|
63
|
+
if args.start:
|
|
64
|
+
p["start_date"] = args.start
|
|
65
|
+
if args.end:
|
|
66
|
+
p["end_date"] = args.end
|
|
67
|
+
return p
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_kpi(args, key, base, pid):
|
|
71
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/kpi",
|
|
72
|
+
key, base, params=_base_params(args))
|
|
73
|
+
data = extract_data(result) or {}
|
|
74
|
+
if args.json:
|
|
75
|
+
print_json(data)
|
|
76
|
+
return
|
|
77
|
+
kpis = data.get("kpis") or data
|
|
78
|
+
print(f"Page KPIs — {args.path}")
|
|
79
|
+
print()
|
|
80
|
+
print("```")
|
|
81
|
+
print("┌────────────────────┬──────────────┬──────────────┐")
|
|
82
|
+
print("│ KPI │ Current │ Change │")
|
|
83
|
+
print("├────────────────────┼──────────────┼──────────────┤")
|
|
84
|
+
for k, label in [
|
|
85
|
+
("ai_citation", "AI Citation"),
|
|
86
|
+
("ai_index", "AI Index"),
|
|
87
|
+
("ai_training", "AI Training"),
|
|
88
|
+
("ai_agent", "AI Agent"),
|
|
89
|
+
("total_bots", "All bots"),
|
|
90
|
+
]:
|
|
91
|
+
v = kpis.get(k) if isinstance(kpis, dict) else None
|
|
92
|
+
if isinstance(v, dict):
|
|
93
|
+
cur, ch = v.get("current"), v.get("change")
|
|
94
|
+
else:
|
|
95
|
+
cur, ch = v, None
|
|
96
|
+
print(f"│ {pad(label, 18)} │ {_fmt_num(cur):>12} │ {_fmt_change(ch):>12} │")
|
|
97
|
+
print("└────────────────────┴──────────────┴──────────────┘")
|
|
98
|
+
print("```")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_logs(args, key, base, pid):
|
|
102
|
+
params = _base_params(args)
|
|
103
|
+
params["limit"] = args.limit
|
|
104
|
+
if args.traffic_type:
|
|
105
|
+
params["traffic_type"] = args.traffic_type
|
|
106
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/logs",
|
|
107
|
+
key, base, params=params)
|
|
108
|
+
data = extract_data(result)
|
|
109
|
+
items = data if isinstance(data, list) else (data or {}).get("logs") or (data or {}).get("events", [])
|
|
110
|
+
if args.json:
|
|
111
|
+
print_json(items)
|
|
112
|
+
return
|
|
113
|
+
if not items:
|
|
114
|
+
print(f"No log events for {args.path}.")
|
|
115
|
+
return
|
|
116
|
+
print(f"Logs for {args.path} ({len(items)})")
|
|
117
|
+
print()
|
|
118
|
+
print("```")
|
|
119
|
+
print("┌────────────────────┬────────────────────┬────────────┐")
|
|
120
|
+
print("│ Time │ Bot / UA │ Platform │")
|
|
121
|
+
print("├────────────────────┼────────────────────┼────────────┤")
|
|
122
|
+
for e in items:
|
|
123
|
+
t = truncate((e.get("timestamp") or e.get("created_at") or "")[:19], 18)
|
|
124
|
+
bot = truncate(e.get("bot_name") or e.get("user_agent") or "(human)", 18)
|
|
125
|
+
plat = truncate(e.get("platform") or "--", 10)
|
|
126
|
+
print(f"│ {pad(t, 18)} │ {pad(bot, 18)} │ {pad(plat, 10)} │")
|
|
127
|
+
print("└────────────────────┴────────────────────┴────────────┘")
|
|
128
|
+
print("```")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def cmd_platforms(args, key, base, pid):
|
|
132
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/platforms",
|
|
133
|
+
key, base, params=_base_params(args))
|
|
134
|
+
data = extract_data(result)
|
|
135
|
+
items = data if isinstance(data, list) else (data or {}).get("platforms", [])
|
|
136
|
+
if args.json:
|
|
137
|
+
print_json(items)
|
|
138
|
+
return
|
|
139
|
+
if not items:
|
|
140
|
+
print(f"No platform breakdown for {args.path}.")
|
|
141
|
+
return
|
|
142
|
+
print(f"Per-platform metrics for {args.path} ({len(items)})")
|
|
143
|
+
print()
|
|
144
|
+
print("```")
|
|
145
|
+
print("┌────────────────────┬──────┬──────┬──────┬──────┬────────┬────────┐")
|
|
146
|
+
print("│ Platform │ Cite │ Indx │ Trn │ Agnt │ Human │ % │")
|
|
147
|
+
print("├────────────────────┼──────┼──────┼──────┼──────┼────────┼────────┤")
|
|
148
|
+
for p in items:
|
|
149
|
+
name = truncate(p.get("name") or p.get("platform"), 18)
|
|
150
|
+
c = _fmt_num(p.get("ai_citation"))
|
|
151
|
+
i = _fmt_num(p.get("ai_index"))
|
|
152
|
+
t = _fmt_num(p.get("ai_training"))
|
|
153
|
+
a = _fmt_num(p.get("ai_agent"))
|
|
154
|
+
h = _fmt_num(p.get("human_referral") or p.get("human"))
|
|
155
|
+
pct = p.get("share") or p.get("percentage")
|
|
156
|
+
pct_s = (f"{float(pct):.1f}%" if pct is not None else "--")
|
|
157
|
+
print(f"│ {pad(name, 18)} │ {c:>4} │ {i:>4} │ {t:>4} │ {a:>4} │ {h:>6} │ {pct_s:>6} │")
|
|
158
|
+
print("└────────────────────┴──────┴──────┴──────┴──────┴────────┴────────┘")
|
|
159
|
+
print("```")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cmd_related(args, key, base, pid):
|
|
163
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/related",
|
|
164
|
+
key, base, params=_base_params(args))
|
|
165
|
+
data = extract_data(result)
|
|
166
|
+
items = data if isinstance(data, list) else (data or {}).get("pages", [])
|
|
167
|
+
if args.json:
|
|
168
|
+
print_json(items)
|
|
169
|
+
return
|
|
170
|
+
if not items:
|
|
171
|
+
print(f"No sibling pages under parent of {args.path}.")
|
|
172
|
+
return
|
|
173
|
+
print(f"Sibling pages of {args.path} ({len(items)})")
|
|
174
|
+
print()
|
|
175
|
+
print("```")
|
|
176
|
+
print("┌────────────────────────────────┬──────┬──────┬──────┬──────┬──────┐")
|
|
177
|
+
print("│ Page │ Cite │ Indx │ Trn │ Agnt │ Cit% │")
|
|
178
|
+
print("├────────────────────────────────┼──────┼──────┼──────┼──────┼──────┤")
|
|
179
|
+
for p in items:
|
|
180
|
+
path = truncate(p.get("path") or p.get("url"), 30)
|
|
181
|
+
c = _fmt_num(p.get("ai_citation"))
|
|
182
|
+
i = _fmt_num(p.get("ai_index"))
|
|
183
|
+
t = _fmt_num(p.get("ai_training"))
|
|
184
|
+
a = _fmt_num(p.get("ai_agent"))
|
|
185
|
+
cp = p.get("ai_citation_pct")
|
|
186
|
+
cp_s = (f"{float(cp):.1f}" if cp is not None else "--")
|
|
187
|
+
print(f"│ {pad(path, 30)} │ {c:>4} │ {i:>4} │ {t:>4} │ {a:>4} │ {cp_s:>4} │")
|
|
188
|
+
print("└────────────────────────────────┴──────┴──────┴──────┴──────┴──────┘")
|
|
189
|
+
print("```")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cmd_health(args, key, base, pid):
|
|
193
|
+
result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/health",
|
|
194
|
+
key, base, params={"path": args.path})
|
|
195
|
+
data = extract_data(result) or {}
|
|
196
|
+
if args.json:
|
|
197
|
+
print_json(data)
|
|
198
|
+
return
|
|
199
|
+
report = data.get("report") or data
|
|
200
|
+
if not report or report == {}:
|
|
201
|
+
print(f"No PageSpeed report cached for {args.path}.")
|
|
202
|
+
print("Run `python3 scripts/page_detail.py health-refresh --path ...` to generate one.")
|
|
203
|
+
return
|
|
204
|
+
print(f"PageSpeed Insights — {args.path}")
|
|
205
|
+
print()
|
|
206
|
+
perf = report.get("performance_score") or report.get("performance")
|
|
207
|
+
print(f" Performance score: {perf}")
|
|
208
|
+
cwv = report.get("core_web_vitals") or report.get("cwv") or {}
|
|
209
|
+
if cwv:
|
|
210
|
+
print(" Core Web Vitals:")
|
|
211
|
+
for k, v in cwv.items():
|
|
212
|
+
print(f" {k}: {v}")
|
|
213
|
+
fetched = report.get("fetched_at") or report.get("updated_at")
|
|
214
|
+
if fetched:
|
|
215
|
+
print(f" Fetched at: {fetched}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cmd_health_refresh(args, key, base, pid):
|
|
219
|
+
print(f"Refreshing PageSpeed report for {args.path} (15–60s blocking)...")
|
|
220
|
+
result = api_post(f"/api/projects/{pid}/ai-agent/pages/by-path/health/refresh",
|
|
221
|
+
key, base, body={"path": args.path})
|
|
222
|
+
data = extract_data(result) or {}
|
|
223
|
+
if args.json:
|
|
224
|
+
print_json(data)
|
|
225
|
+
return
|
|
226
|
+
print("Refresh completed.")
|
|
227
|
+
perf = data.get("performance_score") or data.get("performance")
|
|
228
|
+
if perf is not None:
|
|
229
|
+
print(f" Performance score: {perf}")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def main():
|
|
233
|
+
parser = argparse.ArgumentParser(description="Page-level deep-dive AI analytics")
|
|
234
|
+
parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
|
|
235
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
236
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
237
|
+
|
|
238
|
+
for name in ("kpi", "platforms", "related"):
|
|
239
|
+
p = sub.add_parser(name)
|
|
240
|
+
p.add_argument("--path", required=True)
|
|
241
|
+
p.add_argument("--start")
|
|
242
|
+
p.add_argument("--end")
|
|
243
|
+
|
|
244
|
+
p_lg = sub.add_parser("logs")
|
|
245
|
+
p_lg.add_argument("--path", required=True)
|
|
246
|
+
p_lg.add_argument("--start")
|
|
247
|
+
p_lg.add_argument("--end")
|
|
248
|
+
p_lg.add_argument("--limit", type=int, default=50)
|
|
249
|
+
p_lg.add_argument("--traffic-type", choices=["bot", "human"])
|
|
250
|
+
|
|
251
|
+
p_h = sub.add_parser("health")
|
|
252
|
+
p_h.add_argument("--path", required=True)
|
|
253
|
+
|
|
254
|
+
p_hr = sub.add_parser("health-refresh")
|
|
255
|
+
p_hr.add_argument("--path", required=True)
|
|
256
|
+
|
|
257
|
+
args = parser.parse_args()
|
|
258
|
+
key, base = get_api_config()
|
|
259
|
+
pid = get_project_id(args.project_id)
|
|
260
|
+
|
|
261
|
+
handlers = {
|
|
262
|
+
"kpi": cmd_kpi,
|
|
263
|
+
"logs": cmd_logs,
|
|
264
|
+
"platforms": cmd_platforms,
|
|
265
|
+
"related": cmd_related,
|
|
266
|
+
"health": cmd_health,
|
|
267
|
+
"health-refresh": cmd_health_refresh,
|
|
268
|
+
}
|
|
269
|
+
handlers[args.command](args, key, base, pid)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# GEO Analytics API Reference
|
|
2
|
+
|
|
3
|
+
## Endpoint
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
GET /api/public/projects/{project_id}/dashboard/overview
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Query parameters
|
|
10
|
+
|
|
11
|
+
| Param | Values | Default | Description |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| `period` | `7d` `14d` `30d` `90d` | `30d` | Analysis time window |
|
|
14
|
+
|
|
15
|
+
### Response envelope
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"code": 0,
|
|
20
|
+
"message": "success",
|
|
21
|
+
"data": {
|
|
22
|
+
"period": "30d",
|
|
23
|
+
"date_range": { "start": "2026-03-19", "end": "2026-04-18" },
|
|
24
|
+
"compare_range": { "start": "2026-02-17", "end": "2026-03-18" },
|
|
25
|
+
"integrations": { "gsc": true, "ga4": true, "cloudflare": false },
|
|
26
|
+
"search": { ... },
|
|
27
|
+
"traffic": { ... },
|
|
28
|
+
"ai_impact": { ... },
|
|
29
|
+
"infrastructure":{ ... }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Sections that are `null` → integration not connected.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Section: `search` (Google Search Console)
|
|
39
|
+
|
|
40
|
+
| Field | Type | Description |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `clicks` | int | Total organic clicks |
|
|
43
|
+
| `impressions` | int | Total impressions |
|
|
44
|
+
| `avg_ctr` | float | Average click-through rate |
|
|
45
|
+
| `avg_position` | float | Average ranking position |
|
|
46
|
+
| `clicks_trend` | array | Daily clicks `[{ date, clicks }]` |
|
|
47
|
+
| `top_queries` | array | Top 5 search queries `[{ query, clicks, impressions, ctr, position }]` |
|
|
48
|
+
| `prev_clicks` | int | Previous period clicks (for comparison) |
|
|
49
|
+
| `prev_impressions` | int | Previous period impressions |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Section: `traffic` (Google Analytics 4)
|
|
54
|
+
|
|
55
|
+
| Field | Type | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `sessions` | int | Total sessions |
|
|
58
|
+
| `active_users` | int | Active users |
|
|
59
|
+
| `sessions_trend` | array | Daily sessions |
|
|
60
|
+
| `top_sources` | array | Top traffic sources |
|
|
61
|
+
| `prev_sessions` | int | Previous period sessions |
|
|
62
|
+
| `prev_active_users` | int | Previous period active users |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Section: `ai_impact`
|
|
67
|
+
|
|
68
|
+
| Field | Type | Description |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `ai_referral_sessions` | int | Sessions from AI platforms (ChatGPT, Perplexity, etc.) |
|
|
71
|
+
| `ai_crawler_requests` | int | Requests from AI crawlers via Cloudflare |
|
|
72
|
+
| `ai_referral_trend` | array | Daily AI referral sessions |
|
|
73
|
+
| `ai_crawler_trend` | array | Daily AI crawler requests |
|
|
74
|
+
| `top_ai_sources` | array | Top AI referral sources |
|
|
75
|
+
| `top_ai_bots` | array | Top AI crawlers |
|
|
76
|
+
|
|
77
|
+
**Tracked AI sources**: chat.openai.com, chatgpt.com, perplexity.ai, claude.ai,
|
|
78
|
+
copilot.microsoft.com, gemini.google.com, you.com, phind.com, poe.com
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Section: `infrastructure` (Cloudflare)
|
|
83
|
+
|
|
84
|
+
| Field | Type | Description |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `total_requests` | int | Total HTTP requests |
|
|
87
|
+
| `bandwidth_bytes` | int | Bandwidth consumed |
|
|
88
|
+
| `threats_blocked` | int | Blocked threats |
|
|
89
|
+
| `requests_trend` | array | Daily request counts |
|