azettl commited on
Commit
ab0d64c
Β·
0 Parent(s):

Initial commit

Browse files
Files changed (2) hide show
  1. app.py +494 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import pandas as pd
5
+ from datetime import datetime, timedelta
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ from typing import Dict, List, Any, Optional
9
+ import os
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables from .env file
13
+ load_dotenv()
14
+
15
+ # Configuration
16
+ PLAUSIBLE_URL = os.getenv("PLAUSIBLE_URL", "https://plausible.io/api/v2/query")
17
+ PLAUSIBLE_KEY = os.getenv("PLAUSIBLE_KEY")
18
+
19
+ class PlausibleAPI:
20
+ def __init__(self, api_key: str):
21
+ self.api_key = api_key
22
+ self.headers = {
23
+ 'Authorization': f'Bearer {api_key}',
24
+ 'Content-Type': 'application/json'
25
+ }
26
+
27
+ def query(self, payload: Dict[str, Any]) -> Dict[str, Any]:
28
+ """Make a query to the Plausible API"""
29
+
30
+ if not self.api_key:
31
+ return {"error": "PLAUSIBLE_KEY environment variable is not set"}
32
+
33
+ try:
34
+ response = requests.post(PLAUSIBLE_URL, headers=self.headers, json=payload)
35
+ response.raise_for_status()
36
+ return response.json()
37
+ except requests.exceptions.RequestException as e:
38
+ return {"error": f"API request failed: {str(e)}"}
39
+ except json.JSONDecodeError as e:
40
+ return {"error": f"Failed to parse JSON response: {str(e)}"}
41
+
42
+ # Initialize API client
43
+ api_client = PlausibleAPI(PLAUSIBLE_KEY)
44
+
45
+ def basic_stats_query(site_id: str, date_range: str, metrics: List[str]) -> tuple:
46
+ """Get basic site statistics"""
47
+ if not site_id:
48
+ return "Please enter a site ID", None, None
49
+
50
+ payload = {
51
+ "site_id": site_id,
52
+ "metrics": metrics,
53
+ "date_range": date_range
54
+ }
55
+
56
+ result = api_client.query(payload)
57
+
58
+ if "error" in result:
59
+ return result["error"], None, None
60
+
61
+ # Format results
62
+ if result.get("results"):
63
+ metrics_data = result["results"][0]["metrics"]
64
+ stats_dict = dict(zip(metrics, metrics_data))
65
+
66
+ # Create a simple bar chart
67
+ fig = px.bar(
68
+ x=list(stats_dict.keys()),
69
+ y=list(stats_dict.values()),
70
+ title=f"Stats for {site_id} ({date_range})"
71
+ )
72
+ fig.update_layout(xaxis_title="Metrics", yaxis_title="Values")
73
+
74
+ return json.dumps(result, indent=2), stats_dict, fig
75
+
76
+ return json.dumps(result, indent=2), None, None
77
+
78
+ def timeseries_query(site_id: str, date_range: str, metrics: List[str], time_dimension: str) -> tuple:
79
+ """Get timeseries data"""
80
+ if not site_id:
81
+ return "Please enter a site ID", None
82
+
83
+ payload = {
84
+ "site_id": site_id,
85
+ "metrics": metrics,
86
+ "date_range": date_range,
87
+ "dimensions": [time_dimension]
88
+ }
89
+
90
+ result = api_client.query(payload)
91
+
92
+ if "error" in result:
93
+ return result["error"], None
94
+
95
+ # Create timeseries chart
96
+ if result.get("results"):
97
+ df_data = []
98
+ for row in result["results"]:
99
+ row_dict = {"time": row["dimensions"][0]}
100
+ for i, metric in enumerate(metrics):
101
+ row_dict[metric] = row["metrics"][i]
102
+ df_data.append(row_dict)
103
+
104
+ df = pd.DataFrame(df_data)
105
+ df['time'] = pd.to_datetime(df['time'])
106
+
107
+ fig = go.Figure()
108
+ for metric in metrics:
109
+ fig.add_trace(go.Scatter(
110
+ x=df['time'],
111
+ y=df[metric],
112
+ mode='lines+markers',
113
+ name=metric
114
+ ))
115
+
116
+ fig.update_layout(
117
+ title=f"Timeseries for {site_id}",
118
+ xaxis_title="Time",
119
+ yaxis_title="Values"
120
+ )
121
+
122
+ return json.dumps(result, indent=2), fig
123
+
124
+ return json.dumps(result, indent=2), None
125
+
126
+ def geographic_analysis(site_id: str, date_range: str, metrics: List[str]) -> tuple:
127
+ """Analyze traffic by country and city"""
128
+ if not site_id:
129
+ return "Please enter a site ID", None, None
130
+
131
+ payload = {
132
+ "site_id": site_id,
133
+ "metrics": metrics,
134
+ "date_range": date_range,
135
+ "dimensions": ["visit:country_name", "visit:city_name"],
136
+ "filters": [["is_not", "visit:country_name", [""]]],
137
+ "order_by": [[metrics[0], "desc"]]
138
+ }
139
+
140
+ result = api_client.query(payload)
141
+
142
+ if "error" in result:
143
+ return result["error"], None, None
144
+
145
+ # Create geographic visualization
146
+ if result.get("results"):
147
+ df_data = []
148
+ for row in result["results"]:
149
+ row_dict = {
150
+ "country": row["dimensions"][0],
151
+ "city": row["dimensions"][1]
152
+ }
153
+ for i, metric in enumerate(metrics):
154
+ row_dict[metric] = row["metrics"][i]
155
+ df_data.append(row_dict)
156
+
157
+ df = pd.DataFrame(df_data)
158
+
159
+ # Create a bar chart of top countries
160
+ country_stats = df.groupby('country')[metrics[0]].sum().sort_values(ascending=False).head(10)
161
+
162
+ fig = px.bar(
163
+ x=country_stats.index,
164
+ y=country_stats.values,
165
+ title=f"Top Countries by {metrics[0]} for {site_id}",
166
+ labels={'x': 'Country', 'y': metrics[0]}
167
+ )
168
+ fig.update_xaxes(tickangle=45)
169
+
170
+ return json.dumps(result, indent=2), fig, df.head(20).to_dict('records')
171
+
172
+ return json.dumps(result, indent=2), None, None
173
+
174
+ def utm_analysis(site_id: str, date_range: str) -> tuple:
175
+ """Analyze UTM parameters"""
176
+ if not site_id:
177
+ return "Please enter a site ID", None, None
178
+
179
+ payload = {
180
+ "site_id": site_id,
181
+ "metrics": ["visitors", "events", "pageviews"],
182
+ "date_range": date_range,
183
+ "dimensions": ["visit:utm_medium", "visit:utm_source"],
184
+ "filters": [["is_not", "visit:utm_medium", [""]]]
185
+ }
186
+
187
+ result = api_client.query(payload)
188
+
189
+ if "error" in result:
190
+ return result["error"], None, None
191
+
192
+ if result.get("results"):
193
+ df_data = []
194
+ for row in result["results"]:
195
+ df_data.append({
196
+ "utm_medium": row["dimensions"][0] or "Direct",
197
+ "utm_source": row["dimensions"][1] or "Direct",
198
+ "visitors": row["metrics"][0],
199
+ "events": row["metrics"][1],
200
+ "pageviews": row["metrics"][2]
201
+ })
202
+
203
+ df = pd.DataFrame(df_data)
204
+
205
+ # Create sunburst chart
206
+ fig = px.sunburst(
207
+ df,
208
+ path=['utm_medium', 'utm_source'],
209
+ values='visitors',
210
+ title=f"UTM Analysis for {site_id}"
211
+ )
212
+
213
+ return json.dumps(result, indent=2), fig, df.to_dict('records')
214
+
215
+ return json.dumps(result, indent=2), None, None
216
+
217
+ def custom_query(site_id: str, query_json: str) -> str:
218
+ """Execute a custom JSON query"""
219
+ if not site_id:
220
+ return "Please enter a site ID"
221
+
222
+ try:
223
+ payload = json.loads(query_json)
224
+ payload["site_id"] = site_id # Override site_id
225
+
226
+ result = api_client.query(payload)
227
+ return json.dumps(result, indent=2)
228
+
229
+ except json.JSONDecodeError as e:
230
+ return f"Invalid JSON: {str(e)}"
231
+ except Exception as e:
232
+ return f"Error: {str(e)}"
233
+
234
+ # Gradio Interface
235
+ with gr.Blocks(title="Plausible Analytics Dashboard", theme=gr.themes.Soft()) as demo:
236
+ gr.Markdown("# πŸ“Š Plausible Analytics Dashboard")
237
+ gr.Markdown("MCP Server to analyze your website statistics using the Plausible Stats API.\n\nSo far this app is 100% vibe coded with the help of Claude Sonnet 4.\n\nTry it out with the site id 'azettl.net' or 'fridgeleftoversai.com'.")
238
+
239
+ with gr.Tab("Basic Stats"):
240
+ gr.Markdown("### Get basic website statistics")
241
+
242
+ with gr.Row():
243
+ site_input = gr.Textbox(
244
+ label="Site ID",
245
+ placeholder="example.com",
246
+ info="Your domain as added to Plausible"
247
+ )
248
+ date_range = gr.Dropdown(
249
+ choices=["day", "7d", "28d", "30d", "month", "6mo", "12mo", "year", "all"],
250
+ value="7d",
251
+ label="Date Range"
252
+ )
253
+
254
+ metrics_input = gr.CheckboxGroup(
255
+ choices=["visitors", "visits", "pageviews", "views_per_visit", "bounce_rate", "visit_duration", "events"],
256
+ value=["visitors", "pageviews", "bounce_rate"],
257
+ label="Metrics to Analyze"
258
+ )
259
+
260
+ basic_btn = gr.Button("Get Basic Stats", variant="primary")
261
+
262
+ with gr.Row():
263
+ basic_json = gr.Code(label="API Response", language="json")
264
+ basic_stats = gr.JSON(label="Stats Summary")
265
+
266
+ basic_chart = gr.Plot(label="Statistics Chart")
267
+
268
+ basic_btn.click(
269
+ basic_stats_query,
270
+ inputs=[site_input, date_range, metrics_input],
271
+ outputs=[basic_json, basic_stats, basic_chart]
272
+ )
273
+
274
+ with gr.Tab("Timeseries"):
275
+ gr.Markdown("### View trends over time")
276
+
277
+ with gr.Row():
278
+ ts_site = gr.Textbox(label="Site ID", placeholder="example.com")
279
+ ts_date_range = gr.Dropdown(
280
+ choices=["day", "7d", "28d", "30d", "month"],
281
+ value="7d",
282
+ label="Date Range"
283
+ )
284
+
285
+ with gr.Row():
286
+ ts_metrics = gr.CheckboxGroup(
287
+ choices=["visitors", "visits", "pageviews", "events"],
288
+ value=["visitors", "pageviews"],
289
+ label="Metrics"
290
+ )
291
+ ts_time_dim = gr.Dropdown(
292
+ choices=["time:hour", "time:day", "time:week", "time:month"],
293
+ value="time:day",
294
+ label="Time Dimension"
295
+ )
296
+
297
+ ts_btn = gr.Button("Generate Timeseries", variant="primary")
298
+
299
+ with gr.Row():
300
+ ts_json = gr.Code(label="API Response", language="json")
301
+ ts_chart = gr.Plot(label="Timeseries Chart")
302
+
303
+ ts_btn.click(
304
+ timeseries_query,
305
+ inputs=[ts_site, ts_date_range, ts_metrics, ts_time_dim],
306
+ outputs=[ts_json, ts_chart]
307
+ )
308
+
309
+ with gr.Tab("Geographic Analysis"):
310
+ gr.Markdown("### Analyze traffic by location")
311
+
312
+ with gr.Row():
313
+ geo_site = gr.Textbox(label="Site ID", placeholder="example.com")
314
+ geo_date_range = gr.Dropdown(
315
+ choices=["day", "7d", "28d", "30d", "month"],
316
+ value="7d",
317
+ label="Date Range"
318
+ )
319
+
320
+ geo_metrics = gr.CheckboxGroup(
321
+ choices=["visitors", "visits", "pageviews", "bounce_rate"],
322
+ value=["visitors", "pageviews"],
323
+ label="Metrics"
324
+ )
325
+
326
+ geo_btn = gr.Button("Analyze Geography", variant="primary")
327
+
328
+ with gr.Row():
329
+ geo_json = gr.Code(label="API Response", language="json")
330
+ geo_chart = gr.Plot(label="Geographic Chart")
331
+
332
+ geo_table = gr.DataFrame(label="Top Locations")
333
+
334
+ geo_btn.click(
335
+ geographic_analysis,
336
+ inputs=[geo_site, geo_date_range, geo_metrics],
337
+ outputs=[geo_json, geo_chart, geo_table]
338
+ )
339
+
340
+ with gr.Tab("UTM Analysis"):
341
+ gr.Markdown("### Analyze marketing campaigns and traffic sources")
342
+
343
+ with gr.Row():
344
+ utm_site = gr.Textbox(label="Site ID", placeholder="example.com")
345
+ utm_date_range = gr.Dropdown(
346
+ choices=["day", "7d", "28d", "30d", "month"],
347
+ value="7d",
348
+ label="Date Range"
349
+ )
350
+
351
+ utm_btn = gr.Button("Analyze UTM Parameters", variant="primary")
352
+
353
+ with gr.Row():
354
+ utm_json = gr.Code(label="API Response", language="json")
355
+ utm_chart = gr.Plot(label="UTM Sunburst Chart")
356
+
357
+ utm_table = gr.DataFrame(label="UTM Data")
358
+
359
+ utm_btn.click(
360
+ utm_analysis,
361
+ inputs=[utm_site, utm_date_range],
362
+ outputs=[utm_json, utm_chart, utm_table]
363
+ )
364
+
365
+ with gr.Tab("Custom Query"):
366
+ gr.Markdown("### Execute custom JSON queries")
367
+ gr.Markdown("Use this tab to run advanced queries with custom filters and dimensions.")
368
+
369
+ custom_site = gr.Textbox(label="Site ID", placeholder="example.com")
370
+
371
+ custom_query_input = gr.Code(
372
+ label="JSON Query",
373
+ language="json",
374
+ value="""{
375
+ "metrics": ["visitors", "pageviews"],
376
+ "date_range": "7d",
377
+ "dimensions": ["visit:source"],
378
+ "order_by": [["visitors", "desc"]],
379
+ "pagination": {"limit": 10}
380
+ }""",
381
+ lines=15
382
+ )
383
+
384
+ custom_btn = gr.Button("Execute Query", variant="primary")
385
+ custom_result = gr.Code(label="Query Result", language="json", lines=20)
386
+
387
+ custom_btn.click(
388
+ custom_query,
389
+ inputs=[custom_site, custom_query_input],
390
+ outputs=[custom_result]
391
+ )
392
+
393
+ with gr.Tab("Setup & Documentation"):
394
+ gr.Markdown("""
395
+ ## πŸ”§ Setup Instructions
396
+
397
+ ### For Personal Use (Recommended)
398
+ This MCP server is designed for **personal use only**. Each user should run their own instance.
399
+
400
+ **Setup Steps:**
401
+ 1. **Get your Plausible API key:**
402
+ - Log into your Plausible account
403
+ - Go to Account Settings β†’ API Keys
404
+ - Create a new key, select "Stats API" as type
405
+
406
+ 2. **Set environment variable:**
407
+ ```bash
408
+ # Windows
409
+ set PLAUSIBLE_API_KEY=your-key-here
410
+
411
+ # Mac/Linux
412
+ export PLAUSIBLE_API_KEY=your-key-here
413
+
414
+ # Or create .env file:
415
+ echo "PLAUSIBLE_API_KEY=your-key-here" > .env
416
+ ```
417
+
418
+ 2. **Install dependencies:**
419
+ ```bash
420
+ pip install -r requirements.txt
421
+ ```
422
+
423
+ 3. **Run the server:**
424
+ ```bash
425
+ python app.py
426
+ ```
427
+
428
+ 4. **Add to Claude Desktop config:**
429
+ ```json
430
+ {
431
+ "mcpServers": {
432
+ "plausible": {
433
+ "command": "npx",
434
+ "args": ["mcp-remote", "http://localhost:7860/gradio_api/mcp/sse"]
435
+ }
436
+ }
437
+ }
438
+ ```
439
+
440
+ ### ⚠️ Security Notice
441
+ - **DO NOT** share your API key with others
442
+ - **DO NOT** run this as a public server with your API key (Like I do here to show you how it works πŸ™ˆ)
443
+ - Each user should run their own instance with their own API key
444
+
445
+ ---
446
+
447
+ ## πŸ“– API Reference
448
+
449
+ **Available Metrics:**
450
+ - `visitors`: Unique visitors
451
+ - `visits`: Number of sessions
452
+ - `pageviews`: Total page views
453
+ - `views_per_visit`: Average pages per session
454
+ - `bounce_rate`: Bounce rate percentage
455
+ - `visit_duration`: Average visit duration
456
+ - `events`: Total events
457
+
458
+ **Date Ranges:**
459
+ - `day`: Current day
460
+ - `7d`: Last 7 days
461
+ - `28d`: Last 28 days
462
+ - `30d`: Last 30 days
463
+ - `month`: Current month
464
+ - `6mo`: Last 6 months
465
+ - `12mo`: Last 12 months
466
+ - `year`: Current year
467
+ - `all`: All time
468
+
469
+ **Common Dimensions:**
470
+ - `visit:country_name`: Country
471
+ - `visit:source`: Traffic source
472
+ - `visit:device`: Device type
473
+ - `visit:browser`: Browser
474
+ - `event:page`: Page path
475
+ - `time:day`: Daily grouping
476
+ - `time:hour`: Hourly grouping
477
+
478
+ **Example Filters:**
479
+ ```json
480
+ [["is", "visit:country_name", ["United States", "Canada"]]]
481
+ [["contains", "event:page", ["/blog"]]]
482
+ [["is_not", "visit:device", ["Mobile"]]]
483
+ ```
484
+ """)
485
+
486
+ # Launch configuration
487
+ if __name__ == "__main__":
488
+ demo.launch(
489
+ server_name="0.0.0.0",
490
+ server_port=7860,
491
+ share=False,
492
+ debug=False,
493
+ mcp_server=True
494
+ )
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio[mcp]
2
+ requests
3
+ pandas
4
+ plotly
5
+ python-dotenv