feat: v3.4.0 — OpenPanel plugin public release (42 tools)
OpenPanel product analytics plugin fully reviewed, tested, and published. Works with both self-hosted and cloud (openpanel.dev) instances. - 42 tools: event tracking, data export, analytics, project/client management - All tools use public REST APIs (Track, Export, Insights, Manage) - Client modes: write (tracking), read (analytics), root (full access) - Service page with description, setup notes, WordPress plugin download - Dynamic URL hints in Add Site form - 62 unit tests - ENABLED_PLUGINS default now includes openpanel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
34
CHANGELOG.md
34
CHANGELOG.md
@@ -5,6 +5,38 @@ All notable changes to MCP Hub will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.4.0] — 2026-03-31
|
||||||
|
|
||||||
|
### OpenPanel Plugin — Public Release (Track F.10)
|
||||||
|
|
||||||
|
OpenPanel product analytics plugin fully reviewed, tested, and published for public use. Works with both self-hosted and cloud (openpanel.dev) instances.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
- **OpenPanel Plugin** (42 tools): Complete rewrite using public REST APIs — no tRPC/session dependency
|
||||||
|
- **Track API** (11 tools): track_event, page_view, screen_view, identify_user, set_user_properties, increment/decrement_property, create_group, assign_group, track_revenue, track_batch
|
||||||
|
- **Export API** (10 tools): export_events, export_events_csv, export_chart_data, get_event_count, get_unique_users, get_page_views, get_top_pages, get_top_referrers, get_geo_data, get_device_data
|
||||||
|
- **Insights API** (2 tools): get_overview_report, get_realtime_stats
|
||||||
|
- **Profile API** (3 tools): get_profile_events, get_profile_sessions, export_profile_data
|
||||||
|
- **Manage API** (10 tools): list/get/create/update/delete projects and clients
|
||||||
|
- **System** (6 tools): health_check, get_instance_info, get_usage_stats, get_storage_stats, test_connection, get_rate_limit_status
|
||||||
|
- **Service Page descriptions**: Per-plugin description and notes section on service detail pages
|
||||||
|
- **Dynamic URL hints**: Add Site form shows per-plugin URL guidance (e.g., OpenPanel API URL vs dashboard URL)
|
||||||
|
- **WordPress plugin download**: OpenPanel WordPress plugin bundled at `/static/plugins/openpanel-self-hosted.zip`
|
||||||
|
- **62 unit tests** for OpenPanel plugin (`tests/test_openpanel_plugin.py`)
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- `ENABLED_PLUGINS` default: `wordpress,woocommerce,supabase,openpanel` (was: `wordpress,woocommerce,supabase`)
|
||||||
|
- OpenPanel health check uses `GET /healthcheck` (was: non-existent `/api/v1/oauth/token`)
|
||||||
|
- OpenPanel credential fields: added Project ID and Organization ID (optional)
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
- OpenPanel `alias_user` tool (explicitly unsupported by OpenPanel API)
|
||||||
|
- OpenPanel dashboard handler (18 tools) — no public API available, was placeholder stubs
|
||||||
|
- OpenPanel funnel handler (8 tools) — no public API available, was placeholder stubs
|
||||||
|
- tRPC client dependency — all tools now use public REST APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [3.3.0] — 2026-03-31
|
## [3.3.0] — 2026-03-31
|
||||||
|
|
||||||
### Platform Hardening & Admin Unification (Track F.1–F.8)
|
### Platform Hardening & Admin Unification (Track F.1–F.8)
|
||||||
@@ -22,7 +54,7 @@ Major quality release: plugin visibility control, UI/UX polish, unified admin pa
|
|||||||
- **Pre-configured OAuth** (F.2): Default OAuth redirect URIs for Claude.ai; green tip about optional OAuth
|
- **Pre-configured OAuth** (F.2): Default OAuth redirect URIs for Claude.ai; green tip about optional OAuth
|
||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
- **Connect Page** (F.2): WordPress/SEO amber info box now shown only for WordPress/WooCommerce sites (was shown for all plugin types)
|
- **Connect Page** (F.2): WordPress/SEO amber info box shown only for WordPress/WooCommerce sites (was shown for all plugin types)
|
||||||
- **Sidebar Version** (F.2): Fixed "v" displaying when version string is empty
|
- **Sidebar Version** (F.2): Fixed "v" displaying when version string is empty
|
||||||
- **Auth Page Language** (F.2): Auth page defaults to English regardless of Accept-Language header
|
- **Auth Page Language** (F.2): Auth page defaults to English regardless of Accept-Language header
|
||||||
- **Donation Link** (F.2): Moved from home page to sidebar for consistent visibility
|
- **Donation Link** (F.2): Moved from home page to sidebar for consistent visibility
|
||||||
|
|||||||
12
CLAUDE.md
12
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 9 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with 596 tools total. The tool count stays constant regardless of how many sites are configured.
|
**MCP Hub** — a Python MCP (Model Context Protocol) server that manages multiple self-hosted services through a unified plugin architecture. Supports 9 plugin types (WordPress, WooCommerce, WordPress Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with 565 tools total. The tool count stays constant regardless of how many sites are configured.
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
@@ -193,15 +193,15 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
|||||||
|
|
||||||
## Live Instances
|
## Live Instances
|
||||||
|
|
||||||
- **Platform**: `mcp.palebluedot.live` — Live MCP Hub with OAuth login
|
- **Platform**: `mcp.example.com` — Live MCP Hub with OAuth login
|
||||||
- **Blog**: `blog.palebluedot.live` — WordPress test site + project blog
|
- **Blog**: `blog.example.com` — WordPress test site + project blog
|
||||||
- **Deployment**: Coolify / Docker Compose, port 8000
|
- **Deployment**: Coolify / Docker Compose, port 8000
|
||||||
|
|
||||||
## Current Development (Track F)
|
## Current Development (Track F)
|
||||||
|
|
||||||
v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claude/plans/structured-popping-adleman.md` for full plan.
|
v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claude/plans/structured-popping-adleman.md` for full plan.
|
||||||
|
|
||||||
**Active plugins for public users**: WordPress, WooCommerce, Supabase (configurable via `ENABLED_PLUGINS` env var)
|
**Active plugins for public users**: WordPress, WooCommerce, Supabase, OpenPanel (configurable via `ENABLED_PLUGINS` env var)
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claud
|
|||||||
- `server_multi.py` is the alternative multi-endpoint entry point; `server.py` is the primary
|
- `server_multi.py` is the alternative multi-endpoint entry point; `server.py` is the primary
|
||||||
- `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python
|
- `wordpress-plugin/` contains companion WP plugins (openpanel, airano-mcp-seo-bridge) — these are PHP, not Python
|
||||||
- `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
|
- `env.example` has "FUTURE" labels for Supabase/Gitea but both are fully implemented
|
||||||
- Only 3 plugins are tested for public use: WordPress, WooCommerce, Supabase. Others are admin-only or disabled.
|
- 4 plugins are tested for public use: WordPress, WooCommerce, Supabase, OpenPanel. Others are admin-only or disabled.
|
||||||
- OAuth Clients (Client ID/Secret) are for MCP endpoint auth, NOT the same as GitHub/Google dashboard login
|
- OAuth Clients (Client ID/Secret) are for MCP endpoint auth, NOT the same as GitHub/Google dashboard login
|
||||||
- User sites stored in SQLite (`core/database.py`), admin sites still from env vars
|
- User sites stored in SQLite (`core/database.py`), admin sites still from env vars
|
||||||
- Dashboard templates live in `core/templates/` (included in pip package as `package_data`)
|
- Dashboard templates live in `core/templates/` (included in pip package as `package_data`)
|
||||||
@@ -225,4 +225,4 @@ v4 development cycle with 15 phases. See `docs/ROADMAP.md` (Track F) and `.claud
|
|||||||
- Required env vars: `MASTER_API_KEY`, `OAUTH_JWT_SECRET_KEY`, `OAUTH_BASE_URL`
|
- Required env vars: `MASTER_API_KEY`, `OAUTH_JWT_SECRET_KEY`, `OAUTH_BASE_URL`
|
||||||
- OAuth env vars: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `PUBLIC_URL`
|
- OAuth env vars: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `PUBLIC_URL`
|
||||||
- User limits: `MAX_SITES_PER_USER=10`, `USER_RATE_LIMIT_PER_MIN=30`, `USER_RATE_LIMIT_PER_HR=500`
|
- User limits: `MAX_SITES_PER_USER=10`, `USER_RATE_LIMIT_PER_MIN=30`, `USER_RATE_LIMIT_PER_HR=500`
|
||||||
- Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase` (default)
|
- Plugin visibility: `ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel` (default)
|
||||||
|
|||||||
@@ -112,14 +112,14 @@ You should see the login page. Log in with your `MASTER_API_KEY` or via **GitHub
|
|||||||
|
|
||||||
### Try It Now (No Setup Required)
|
### Try It Now (No Setup Required)
|
||||||
|
|
||||||
**Don't want to self-host?** Try the demo instance at **[mcp.palebluedot.live](https://mcp.palebluedot.live)**:
|
**Don't want to self-host?** Use the hosted instance at **[mcp.example.com](https://mcp.example.com)**:
|
||||||
|
|
||||||
1. Log in with **GitHub** or **Google**
|
1. Log in with **GitHub** or **Google**
|
||||||
2. Add your sites via the dashboard (My Sites → Add Service)
|
2. Add your sites via the dashboard (My Sites → Add Service)
|
||||||
3. Go to **Connect** page — generate config for your AI client
|
3. Go to **Connect** page — generate config for your AI client
|
||||||
4. Copy-paste the config into Claude Desktop, VS Code, or Claude Code
|
4. Copy-paste the config into Claude Desktop, VS Code, or Claude Code
|
||||||
|
|
||||||
Your personal MCP endpoint: `https://mcp.palebluedot.live/u/{your-user-id}/{alias}/mcp`
|
Your personal MCP endpoint: `https://mcp.example.com/u/{your-user-id}/{alias}/mcp`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import bcrypt
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -850,12 +850,9 @@ async def get_all_projects(
|
|||||||
is_admin = False
|
is_admin = False
|
||||||
current_user_id = None
|
current_user_id = None
|
||||||
if user_session:
|
if user_session:
|
||||||
if (
|
if hasattr(user_session, "user_type") and user_session.user_type == "master":
|
||||||
hasattr(user_session, "user_type")
|
is_admin = True
|
||||||
and user_session.user_type == "master"
|
elif isinstance(user_session, dict) and user_session.get("role") == "admin":
|
||||||
or isinstance(user_session, dict)
|
|
||||||
and user_session.get("role") == "admin"
|
|
||||||
):
|
|
||||||
is_admin = True
|
is_admin = True
|
||||||
elif isinstance(user_session, dict) and "user_id" in user_session:
|
elif isinstance(user_session, dict) and "user_id" in user_session:
|
||||||
current_user_id = user_session["user_id"]
|
current_user_id = user_session["user_id"]
|
||||||
@@ -3292,6 +3289,46 @@ async def get_service_page_data(plugin_type: str) -> dict | None:
|
|||||||
|
|
||||||
from core.plugin_visibility import is_plugin_public
|
from core.plugin_visibility import is_plugin_public
|
||||||
|
|
||||||
|
# Per-plugin descriptions and notes
|
||||||
|
service_descriptions = {
|
||||||
|
"openpanel": {
|
||||||
|
"en": (
|
||||||
|
"OpenPanel product analytics management. "
|
||||||
|
"Supports event tracking, data export, analytics reports, "
|
||||||
|
"and project/client management via public REST APIs. "
|
||||||
|
"Works with both self-hosted and cloud (openpanel.dev) instances."
|
||||||
|
),
|
||||||
|
"fa": (
|
||||||
|
"مدیریت آنالیتیکس محصول OpenPanel. "
|
||||||
|
"پشتیبانی از ردیابی رویداد، خروجی داده، گزارشهای آنالیتیکس، "
|
||||||
|
"و مدیریت پروژه/کلاینت از طریق APIهای عمومی REST. "
|
||||||
|
"هم نسخه خودمیزبان و هم نسخه cloud (openpanel.dev) پشتیبانی میشود."
|
||||||
|
),
|
||||||
|
"notes_en": [
|
||||||
|
"<strong>Site URL must be the API URL</strong>, not the dashboard URL. "
|
||||||
|
"Cloud: <code>https://api.openpanel.dev</code> — "
|
||||||
|
"Self-hosted: your API service URL (e.g., <code>https://analytics.example.com</code>).",
|
||||||
|
"Client must have <strong>root</strong> mode for full access (tracking + export + manage). "
|
||||||
|
"Use <strong>read</strong> mode for analytics only, or <strong>write</strong> mode for tracking only.",
|
||||||
|
"If you have WordPress, install the <a href='/static/plugins/openpanel-self-hosted.zip' "
|
||||||
|
"class='text-primary-400 hover:underline'>OpenPanel WordPress plugin</a> "
|
||||||
|
"to automatically send analytics data to your OpenPanel instance.",
|
||||||
|
],
|
||||||
|
"notes_fa": [
|
||||||
|
"<strong>آدرس سایت باید آدرس API باشد</strong>، نه داشبورد. "
|
||||||
|
"نسخه Cloud: <code>https://api.openpanel.dev</code> — "
|
||||||
|
"خودمیزبان: آدرس سرویس API شما (مثلاً <code>https://analytics.example.com</code>).",
|
||||||
|
"برای دسترسی کامل، کلاینت باید حالت <strong>root</strong> داشته باشد. "
|
||||||
|
"از حالت <strong>read</strong> برای آنالیتیکس و <strong>write</strong> برای ردیابی استفاده کنید.",
|
||||||
|
"اگر وردپرس دارید، افزونه <a href='/static/plugins/openpanel-self-hosted.zip' "
|
||||||
|
"class='text-primary-400 hover:underline'>OpenPanel WordPress</a> "
|
||||||
|
"را نصب کنید تا دادههای آنالیتیکس بهصورت خودکار به OpenPanel ارسال شود.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
desc_data = service_descriptions.get(plugin_type, {})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"plugin_type": plugin_type,
|
"plugin_type": plugin_type,
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
@@ -3299,6 +3336,7 @@ async def get_service_page_data(plugin_type: str) -> dict | None:
|
|||||||
"tools_count": len(tools),
|
"tools_count": len(tools),
|
||||||
"credential_fields": credential_fields,
|
"credential_fields": credential_fields,
|
||||||
"is_public": is_plugin_public(plugin_type),
|
"is_public": is_plugin_public(plugin_type),
|
||||||
|
"description": desc_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -249,16 +249,15 @@ ENDPOINT_CONFIGS = {
|
|||||||
},
|
},
|
||||||
max_tools=80,
|
max_tools=80,
|
||||||
),
|
),
|
||||||
# OpenPanel endpoint - Product Analytics (73 tools) - Phase H
|
# OpenPanel endpoint - Product Analytics (42 tools) - Phase H
|
||||||
EndpointType.OPENPANEL: EndpointConfig(
|
EndpointType.OPENPANEL: EndpointConfig(
|
||||||
path="/openpanel",
|
path="/openpanel",
|
||||||
name="OpenPanel Analytics",
|
name="OpenPanel Analytics",
|
||||||
description="OpenPanel product analytics management (events, export, funnels, dashboards)",
|
description="OpenPanel self-hosted product analytics (events, export, insights, project/client management)",
|
||||||
endpoint_type=EndpointType.OPENPANEL,
|
endpoint_type=EndpointType.OPENPANEL,
|
||||||
plugin_types=["openpanel"],
|
plugin_types=["openpanel"],
|
||||||
require_master_key=False,
|
require_master_key=False,
|
||||||
allowed_scopes={"read", "write", "admin"},
|
allowed_scopes={"read", "write", "admin"},
|
||||||
# Blacklist system and admin tools
|
|
||||||
tool_blacklist={
|
tool_blacklist={
|
||||||
"manage_api_keys_create",
|
"manage_api_keys_create",
|
||||||
"manage_api_keys_delete",
|
"manage_api_keys_delete",
|
||||||
@@ -266,7 +265,7 @@ ENDPOINT_CONFIGS = {
|
|||||||
"oauth_register_client",
|
"oauth_register_client",
|
||||||
"oauth_revoke_client",
|
"oauth_revoke_client",
|
||||||
},
|
},
|
||||||
max_tools=80,
|
max_tools=50,
|
||||||
),
|
),
|
||||||
# Appwrite endpoint - Backend-as-a-Service (100 tools) - Phase I
|
# Appwrite endpoint - Backend-as-a-Service (100 tools) - Phase I
|
||||||
EndpointType.APPWRITE: EndpointConfig(
|
EndpointType.APPWRITE: EndpointConfig(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Usage:
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
# Default plugins available to public (OAuth) users
|
# Default plugins available to public (OAuth) users
|
||||||
DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase"}
|
DEFAULT_PUBLIC_PLUGINS = {"wordpress", "woocommerce", "supabase", "openpanel"}
|
||||||
|
|
||||||
|
|
||||||
def _parse_plugins(val: str) -> set[str]:
|
def _parse_plugins(val: str) -> set[str]:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Usage:
|
Usage:
|
||||||
from core.settings import get_setting
|
from core.settings import get_setting
|
||||||
|
|
||||||
enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase")
|
enabled = await get_setting("ENABLED_PLUGINS", "wordpress,woocommerce,supabase,openpanel")
|
||||||
max_sites = int(await get_setting("MAX_SITES_PER_USER", "10"))
|
max_sites = int(await get_setting("MAX_SITES_PER_USER", "10"))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ _cached_plugins: set[str] | None = None
|
|||||||
|
|
||||||
# Default values for all managed settings
|
# Default values for all managed settings
|
||||||
SETTING_DEFAULTS: dict[str, str] = {
|
SETTING_DEFAULTS: dict[str, str] = {
|
||||||
"ENABLED_PLUGINS": "wordpress,woocommerce,supabase",
|
"ENABLED_PLUGINS": "wordpress,woocommerce,supabase,openpanel",
|
||||||
"MAX_SITES_PER_USER": "10",
|
"MAX_SITES_PER_USER": "10",
|
||||||
"USER_RATE_LIMIT_PER_MIN": "30",
|
"USER_RATE_LIMIT_PER_MIN": "30",
|
||||||
"USER_RATE_LIMIT_PER_HR": "500",
|
"USER_RATE_LIMIT_PER_HR": "500",
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ PLUGIN_CREDENTIAL_FIELDS: dict[str, list[dict[str, Any]]] = {
|
|||||||
"label": "Client ID",
|
"label": "Client ID",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"required": True,
|
"required": True,
|
||||||
"hint": "OpenPanel admin panel → API section",
|
"hint": "OpenPanel Dashboard → Settings → Clients → Create client with 'root' mode for full access",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "client_secret",
|
"name": "client_secret",
|
||||||
@@ -166,6 +166,21 @@ PLUGIN_CREDENTIAL_FIELDS: dict[str, list[dict[str, Any]]] = {
|
|||||||
"required": True,
|
"required": True,
|
||||||
"hint": "Generated with your Client ID",
|
"hint": "Generated with your Client ID",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "project_id",
|
||||||
|
"label": "Project ID",
|
||||||
|
"type": "text",
|
||||||
|
"required": False,
|
||||||
|
"hint": "From dashboard URL: dashboard.openpanel.dev/{org}/{project-id}/ — sets default for Export & Insights tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "organization_id",
|
||||||
|
"label": "Organization ID",
|
||||||
|
"type": "text",
|
||||||
|
"required": False,
|
||||||
|
"hint": "From dashboard URL: dashboard.openpanel.dev/{org}/{project-id}/",
|
||||||
|
"advanced": True,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"appwrite": [
|
"appwrite": [
|
||||||
{
|
{
|
||||||
@@ -215,7 +230,7 @@ _HEALTH_ENDPOINTS: dict[str, dict[str, Any]] = {
|
|||||||
"gitea": {"path": "/api/v1/user", "method": "GET"},
|
"gitea": {"path": "/api/v1/user", "method": "GET"},
|
||||||
"n8n": {"path": "/healthz", "method": "GET"},
|
"n8n": {"path": "/healthz", "method": "GET"},
|
||||||
"supabase": {"path": "/rest/v1/", "method": "GET"},
|
"supabase": {"path": "/rest/v1/", "method": "GET"},
|
||||||
"openpanel": {"path": "/api/v1/oauth/token", "method": "POST"},
|
"openpanel": {"path": "/healthcheck", "method": "GET"},
|
||||||
"appwrite": {"path": "/v1/health", "method": "GET"},
|
"appwrite": {"path": "/v1/health", "method": "GET"},
|
||||||
"directus": {"path": "/server/health", "method": "GET"},
|
"directus": {"path": "/server/health", "method": "GET"},
|
||||||
}
|
}
|
||||||
@@ -342,23 +357,13 @@ async def validate_site_connection(
|
|||||||
elif plugin_type == "directus":
|
elif plugin_type == "directus":
|
||||||
headers["Authorization"] = f"Bearer {credentials.get('token', '')}"
|
headers["Authorization"] = f"Bearer {credentials.get('token', '')}"
|
||||||
elif plugin_type == "openpanel":
|
elif plugin_type == "openpanel":
|
||||||
# OpenPanel uses token exchange — just check that the URL is reachable
|
headers["openpanel-client-id"] = credentials.get("client_id", "")
|
||||||
pass
|
headers["openpanel-client-secret"] = credentials.get("client_secret", "")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
timeout = aiohttp.ClientTimeout(total=15)
|
timeout = aiohttp.ClientTimeout(total=15)
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
if method == "POST" and plugin_type == "openpanel":
|
if method == "POST":
|
||||||
async with session.post(
|
|
||||||
check_url,
|
|
||||||
json={
|
|
||||||
"clientId": credentials.get("client_id", ""),
|
|
||||||
"clientSecret": credentials.get("client_secret", ""),
|
|
||||||
},
|
|
||||||
) as resp:
|
|
||||||
status_code = resp.status
|
|
||||||
resp_text = await resp.text()
|
|
||||||
elif method == "POST":
|
|
||||||
async with session.post(check_url, headers=headers) as resp:
|
async with session.post(check_url, headers=headers) as resp:
|
||||||
status_code = resp.status
|
status_code = resp.status
|
||||||
resp_text = await resp.text()
|
resp_text = await resp.text()
|
||||||
|
|||||||
@@ -31,6 +31,31 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description & Notes -->
|
||||||
|
{% if service.description %}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
{% if lang == 'fa' and service.description.fa %}
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed" dir="rtl">{{ service.description.fa }}</p>
|
||||||
|
{% elif service.description.en %}
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">{{ service.description.en }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set notes_key = 'notes_fa' if lang == 'fa' else 'notes_en' %}
|
||||||
|
{% if service.description[notes_key] %}
|
||||||
|
<ul class="mt-4 space-y-2{% if lang == 'fa' %} text-right{% endif %}" {% if lang == 'fa' %}dir="rtl"{% endif %}>
|
||||||
|
{% for note in service.description[notes_key] %}
|
||||||
|
<li class="flex items-start gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<svg class="w-4 h-4 text-primary-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ note|safe }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Overview Cards -->
|
<!-- Overview Cards -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<!-- Tools Count -->
|
<!-- Tools Count -->
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ t.site_url }}</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ t.site_url }}</label>
|
||||||
<input type="url" id="url" name="url" required placeholder="https://example.com"
|
<input type="url" id="url" name="url" required placeholder="https://example.com"
|
||||||
class="w-full bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2.5 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
class="w-full bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2.5 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||||
|
<p id="url-hint" class="text-xs text-gray-500 dark:text-gray-400 mt-1 hidden"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alias -->
|
<!-- Alias -->
|
||||||
@@ -73,9 +74,23 @@
|
|||||||
<script>
|
<script>
|
||||||
const pluginFields = {{ plugin_fields_json| safe }};
|
const pluginFields = {{ plugin_fields_json| safe }};
|
||||||
|
|
||||||
|
const urlHints = {
|
||||||
|
'openpanel': 'OpenPanel API URL — Cloud: https://api.openpanel.dev | Self-hosted: your API service URL (e.g., https://analytics.example.com)',
|
||||||
|
'supabase': 'Your Supabase project URL (e.g., https://xxxxx.supabase.co)',
|
||||||
|
};
|
||||||
|
|
||||||
function updateFields() {
|
function updateFields() {
|
||||||
const ptype = document.getElementById('plugin_type').value;
|
const ptype = document.getElementById('plugin_type').value;
|
||||||
const container = document.getElementById('credential-fields');
|
const container = document.getElementById('credential-fields');
|
||||||
|
const urlHintEl = document.getElementById('url-hint');
|
||||||
|
|
||||||
|
// Update URL hint
|
||||||
|
if (urlHints[ptype]) {
|
||||||
|
urlHintEl.textContent = urlHints[ptype];
|
||||||
|
urlHintEl.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
urlHintEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
if (!ptype || !pluginFields[ptype]) {
|
if (!ptype || !pluginFields[ptype]) {
|
||||||
container.innerHTML = '<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t.credentials }}</label><p class="text-sm text-gray-500 dark:text-gray-400">{{ t.select_plugin }}</p>';
|
container.innerHTML = '<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t.credentials }}</label><p class="text-sm text-gray-500 dark:text-gray-400">{{ t.select_plugin }}</p>';
|
||||||
|
|||||||
BIN
core/templates/static/plugins/openpanel-self-hosted.zip
Normal file
BIN
core/templates/static/plugins/openpanel-self-hosted.zip
Normal file
Binary file not shown.
@@ -46,7 +46,7 @@ For each WordPress site you want to manage:
|
|||||||
|
|
||||||
The fastest way to try MCP Hub — no installation needed:
|
The fastest way to try MCP Hub — no installation needed:
|
||||||
|
|
||||||
1. Visit **[mcp.palebluedot.live](https://mcp.palebluedot.live)**
|
1. Visit **[mcp.example.com](https://mcp.example.com)**
|
||||||
2. Log in with **GitHub** or **Google**
|
2. Log in with **GitHub** or **Google**
|
||||||
3. Add your sites via the dashboard (**My Sites → Add Service**)
|
3. Add your sites via the dashboard (**My Sites → Add Service**)
|
||||||
4. Go to **Connect** page to generate your AI client config
|
4. Go to **Connect** page to generate your AI client config
|
||||||
@@ -54,7 +54,7 @@ The fastest way to try MCP Hub — no installation needed:
|
|||||||
|
|
||||||
Your personal MCP endpoint:
|
Your personal MCP endpoint:
|
||||||
```
|
```
|
||||||
https://mcp.palebluedot.live/u/{your-user-id}/{alias}/mcp
|
https://mcp.example.com/u/{your-user-id}/{alias}/mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
**WordPress users:** Install the [Airano MCP SEO Bridge](https://wordpress.org/plugins/airano-mcp-seo-bridge/) plugin for SEO tools, and create an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) (Users → Profile) for authentication.
|
**WordPress users:** Install the [Airano MCP SEO Bridge](https://wordpress.org/plugins/airano-mcp-seo-bridge/) plugin for SEO tools, and create an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) (Users → Profile) for authentication.
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ DASHBOARD_SESSION_SECRET=
|
|||||||
# ============================================
|
# ============================================
|
||||||
# Comma-separated list of plugins visible to public (OAuth) users.
|
# Comma-separated list of plugins visible to public (OAuth) users.
|
||||||
# Admin users always see all plugins regardless of this setting.
|
# Admin users always see all plugins regardless of this setting.
|
||||||
# Default (if not set): wordpress,woocommerce,supabase
|
# Default (if not set): wordpress,woocommerce,supabase,openpanel
|
||||||
# ENABLED_PLUGINS=wordpress,woocommerce,supabase
|
# ENABLED_PLUGINS=wordpress,woocommerce,supabase,openpanel
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# ADMIN SYSTEM (Track F.4)
|
# ADMIN SYSTEM (Track F.4)
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
OpenPanel Plugin - Product Analytics Management
|
OpenPanel Plugin - Product Analytics Management.
|
||||||
|
|
||||||
Complete OpenPanel Self-Hosted management through REST API.
|
Self-hosted OpenPanel management through public REST APIs (42 tools).
|
||||||
Provides tools for event tracking, data export, funnels,
|
Event tracking, data export, analytics, project & client management.
|
||||||
dashboards, user profiles, and analytics reports.
|
|
||||||
|
|
||||||
For Self-Hosted instances deployed on Coolify.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plugins.openpanel.client import OpenPanelClient
|
from plugins.openpanel.client import OpenPanelClient
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,36 @@
|
|||||||
"""
|
"""
|
||||||
OpenPanel Handlers
|
OpenPanel Handlers — 42 tools across 7 handlers.
|
||||||
|
|
||||||
Phase H.1: Core (25 tools)
|
All tools use public REST APIs (no tRPC/session dependency).
|
||||||
- events.py: Event tracking (9 tools - alias_user removed)
|
|
||||||
- export.py: Data export (10 tools)
|
|
||||||
- system.py: Health & stats (6 tools)
|
|
||||||
|
|
||||||
Phase H.2: Analytics (24 tools)
|
Track API (/track) — write mode:
|
||||||
- reports.py: Analytics reports (8 tools)
|
- events.py: Event tracking, groups (11 tools)
|
||||||
- funnels.py: Funnel analysis (8 tools)
|
|
||||||
- profiles.py: User profiles (8 tools)
|
|
||||||
|
|
||||||
Phase H.3: Management (24 tools)
|
Export API (/export) — read mode:
|
||||||
- projects.py: Project management (8 tools)
|
- export.py: Data export & analytics (10 tools)
|
||||||
- dashboards.py: Dashboard management (10 tools)
|
|
||||||
- clients.py: API client management (6 tools)
|
|
||||||
|
|
||||||
Total: 73 tools
|
Insights API (/insights) — read mode:
|
||||||
|
- reports.py: Overview & realtime stats (2 tools)
|
||||||
|
|
||||||
|
Profile API — read mode:
|
||||||
|
- profiles.py: Profile events & data export (3 tools)
|
||||||
|
|
||||||
|
Manage API (/manage) — root mode:
|
||||||
|
- projects.py: Project CRUD (5 tools)
|
||||||
|
- clients.py: Client CRUD (5 tools)
|
||||||
|
|
||||||
|
System:
|
||||||
|
- system.py: Health & instance info (6 tools)
|
||||||
|
|
||||||
|
Removed (no public API):
|
||||||
|
- dashboards.py (tRPC-only)
|
||||||
|
- funnels.py (tRPC-only)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plugins.openpanel.handlers import (
|
from plugins.openpanel.handlers import (
|
||||||
clients,
|
clients,
|
||||||
dashboards,
|
|
||||||
events,
|
events,
|
||||||
export,
|
export,
|
||||||
funnels,
|
|
||||||
profiles,
|
profiles,
|
||||||
projects,
|
projects,
|
||||||
reports,
|
reports,
|
||||||
@@ -36,9 +42,7 @@ __all__ = [
|
|||||||
"export",
|
"export",
|
||||||
"system",
|
"system",
|
||||||
"reports",
|
"reports",
|
||||||
"funnels",
|
|
||||||
"profiles",
|
"profiles",
|
||||||
"projects",
|
"projects",
|
||||||
"dashboards",
|
|
||||||
"clients",
|
"clients",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"""Clients Handler - OpenPanel API client/key management (6 tools)"""
|
"""Clients Handler - OpenPanel API client management (5 tools).
|
||||||
|
|
||||||
|
Uses Manage API (GET/POST/PATCH/DELETE /manage/clients).
|
||||||
|
Requires 'root' mode client.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -7,107 +11,82 @@ from plugins.openpanel.client import OpenPanelClient
|
|||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""Return tool specifications for ToolGenerator (6 tools)"""
|
"""Return tool specifications for ToolGenerator (5 tools)."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": "list_clients",
|
"name": "list_clients",
|
||||||
"method_name": "list_clients",
|
"method_name": "list_clients",
|
||||||
"description": "List all API clients for a project.",
|
"description": "List all API clients via Manage API. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {"type": "object", "properties": {}},
|
||||||
"type": "object",
|
"scope": "admin",
|
||||||
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
|
|
||||||
"required": ["project_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "get_client",
|
"name": "get_client",
|
||||||
"method_name": "get_client",
|
"method_name": "get_client",
|
||||||
"description": "Get API client details.",
|
"description": "Get API client details via Manage API. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
"client_id": {"type": "string", "description": "Client ID"},
|
||||||
"client_id": {"type": "string", "description": "API Client ID"},
|
|
||||||
},
|
},
|
||||||
"required": ["project_id", "client_id"],
|
"required": ["client_id"],
|
||||||
},
|
},
|
||||||
"scope": "read",
|
"scope": "admin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "create_client",
|
"name": "create_client",
|
||||||
"method_name": "create_client",
|
"method_name": "create_client",
|
||||||
"description": "Create a new API client for tracking or export.",
|
"description": "Create a new API client via Manage API. Modes: 'write' (tracking only), 'read' (export/analytics), 'root' (full access). Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
"name": {"type": "string", "description": "Client name"},
|
||||||
"name": {
|
"project_id": {"type": "string", "description": "Project to associate with"},
|
||||||
"type": "string",
|
|
||||||
"description": "Client name (e.g., 'Web Tracker', 'Backend Export')",
|
|
||||||
},
|
|
||||||
"mode": {
|
"mode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["write", "read", "root"],
|
"enum": ["write", "read", "root"],
|
||||||
"description": "Client mode: write (tracking), read (export), root (full access)",
|
"description": "Client mode: write (tracking), read (export/analytics), root (full access)",
|
||||||
},
|
"default": "write",
|
||||||
"cors_domains": {
|
|
||||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
||||||
"description": "Allowed domains for CORS (write mode)",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["project_id", "name", "mode"],
|
"required": ["name", "project_id"],
|
||||||
|
},
|
||||||
|
"scope": "admin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_client",
|
||||||
|
"method_name": "update_client",
|
||||||
|
"description": "Update an API client via Manage API. Requires 'root' mode client.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"client_id": {"type": "string", "description": "Client ID to update"},
|
||||||
|
"name": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "New client name",
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string", "enum": ["write", "read", "root"]},
|
||||||
|
{"type": "null"},
|
||||||
|
],
|
||||||
|
"description": "New client mode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["client_id"],
|
||||||
},
|
},
|
||||||
"scope": "admin",
|
"scope": "admin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "delete_client",
|
"name": "delete_client",
|
||||||
"method_name": "delete_client",
|
"method_name": "delete_client",
|
||||||
"description": "Delete an API client.",
|
"description": "Delete an API client via Manage API. WARNING: Any integrations using this client will stop working. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"client_id": {"type": "string", "description": "Client ID to delete"},
|
"client_id": {"type": "string", "description": "Client ID to delete"},
|
||||||
},
|
},
|
||||||
"required": ["project_id", "client_id"],
|
"required": ["client_id"],
|
||||||
},
|
|
||||||
"scope": "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "regenerate_client_secret",
|
|
||||||
"method_name": "regenerate_client_secret",
|
|
||||||
"description": "Regenerate the secret for an API client.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"client_id": {"type": "string", "description": "Client ID"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "client_id"],
|
|
||||||
},
|
|
||||||
"scope": "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "update_client_mode",
|
|
||||||
"method_name": "update_client_mode",
|
|
||||||
"description": "Update API client permissions/mode.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"client_id": {"type": "string", "description": "Client ID"},
|
|
||||||
"mode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["write", "read", "root"],
|
|
||||||
"description": "New mode",
|
|
||||||
},
|
|
||||||
"cors_domains": {
|
|
||||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
||||||
"description": "New CORS domains",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "client_id", "mode"],
|
|
||||||
},
|
},
|
||||||
"scope": "admin",
|
"scope": "admin",
|
||||||
},
|
},
|
||||||
@@ -115,20 +94,21 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# Client Functions (6)
|
# Client Functions (5)
|
||||||
# =====================
|
# =====================
|
||||||
|
|
||||||
|
|
||||||
async def list_clients(client: OpenPanelClient, project_id: str) -> str:
|
async def list_clients(client: OpenPanelClient) -> str:
|
||||||
"""List all API clients"""
|
"""List all API clients via GET /manage/clients."""
|
||||||
try:
|
try:
|
||||||
|
result = await client.list_clients()
|
||||||
|
clients = (
|
||||||
|
result
|
||||||
|
if isinstance(result, list)
|
||||||
|
else result.get("data", []) if isinstance(result, dict) else []
|
||||||
|
)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "count": len(clients), "clients": clients},
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"note": "Client listing requires dashboard tRPC API. Use OpenPanel dashboard to view clients.",
|
|
||||||
"message": "Client list request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -136,117 +116,76 @@ async def list_clients(client: OpenPanelClient, project_id: str) -> str:
|
|||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
async def get_client(client: OpenPanelClient, project_id: str, client_id: str) -> str:
|
async def get_client(client: OpenPanelClient, client_id: str) -> str:
|
||||||
"""Get API client details"""
|
"""Get client details via GET /manage/clients/:id."""
|
||||||
try:
|
try:
|
||||||
return json.dumps(
|
result = await client.get_client(client_id)
|
||||||
{
|
return json.dumps({"success": True, "client": result}, indent=2, ensure_ascii=False)
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"client_id": client_id,
|
|
||||||
"note": "Client details require dashboard tRPC API. Use OpenPanel dashboard for full view.",
|
|
||||||
"message": "Client details request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
async def create_client(
|
async def create_client(
|
||||||
client: OpenPanelClient,
|
client: OpenPanelClient,
|
||||||
project_id: str,
|
|
||||||
name: str,
|
name: str,
|
||||||
mode: str,
|
|
||||||
cors_domains: list[str] | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Create a new API client"""
|
|
||||||
try:
|
|
||||||
client_config = {"name": name, "mode": mode, "cors_domains": cors_domains}
|
|
||||||
|
|
||||||
mode_descriptions = {
|
|
||||||
"write": "Can send events (tracking)",
|
|
||||||
"read": "Can read/export data",
|
|
||||||
"root": "Full access (tracking + export + management)",
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"client": client_config,
|
|
||||||
"mode_description": mode_descriptions.get(mode, "Unknown mode"),
|
|
||||||
"note": "Client creation requires dashboard tRPC API. Use OpenPanel dashboard to create clients.",
|
|
||||||
"message": f"Client '{name}' configuration created with {mode} mode",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_client(client: OpenPanelClient, project_id: str, client_id: str) -> str:
|
|
||||||
"""Delete an API client"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"client_id": client_id,
|
|
||||||
"note": "Client deletion requires dashboard tRPC API. Use OpenPanel dashboard to delete clients.",
|
|
||||||
"warning": "Deleting a client will invalidate all requests using its credentials.",
|
|
||||||
"message": "Client deletion request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def regenerate_client_secret(client: OpenPanelClient, project_id: str, client_id: str) -> str:
|
|
||||||
"""Regenerate client secret"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"client_id": client_id,
|
|
||||||
"note": "Secret regeneration requires dashboard tRPC API. Use OpenPanel dashboard to regenerate.",
|
|
||||||
"warning": "Regenerating secret will invalidate the current secret immediately.",
|
|
||||||
"message": "Secret regeneration request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_client_mode(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
project_id: str,
|
||||||
client_id: str,
|
mode: str = "write",
|
||||||
mode: str,
|
|
||||||
cors_domains: list[str] | None = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Update client permissions"""
|
"""Create a new API client via POST /manage/clients."""
|
||||||
try:
|
try:
|
||||||
updates = {"mode": mode, "cors_domains": cors_domains}
|
data: dict[str, Any] = {"name": name, "projectId": project_id, "mode": mode}
|
||||||
|
result = await client.create_client(data)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": project_id,
|
"message": f"Client '{name}' created with {mode} mode",
|
||||||
"client_id": client_id,
|
"client": result,
|
||||||
"updates": updates,
|
"note": "Save the client_secret — it cannot be retrieved later.",
|
||||||
"note": "Client mode update requires dashboard tRPC API. Use OpenPanel dashboard to modify.",
|
|
||||||
"message": f"Client mode update to {mode} configuration created",
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_client(
|
||||||
|
client: OpenPanelClient,
|
||||||
|
client_id: str,
|
||||||
|
name: str | None = None,
|
||||||
|
mode: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Update a client via PATCH /manage/clients/:id."""
|
||||||
|
try:
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
if name:
|
||||||
|
data["name"] = name
|
||||||
|
if mode:
|
||||||
|
data["mode"] = mode
|
||||||
|
if not data:
|
||||||
|
return json.dumps(
|
||||||
|
{"success": False, "error": "No fields to update. Provide name or mode."},
|
||||||
|
indent=2,
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
result = await client.update_client(client_id, data)
|
||||||
|
return json.dumps(
|
||||||
|
{"success": True, "message": f"Client '{client_id}' updated", "client": result},
|
||||||
|
indent=2,
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_client(client: OpenPanelClient, client_id: str) -> str:
|
||||||
|
"""Delete a client via DELETE /manage/clients/:id."""
|
||||||
|
try:
|
||||||
|
result = await client.delete_client(client_id)
|
||||||
|
return json.dumps(
|
||||||
|
{"success": True, "message": f"Client '{client_id}' deleted", "result": result},
|
||||||
|
indent=2,
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|||||||
@@ -1,467 +0,0 @@
|
|||||||
"""Dashboards Handler - OpenPanel dashboard management (10 tools)"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from plugins.openpanel.client import OpenPanelClient
|
|
||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
|
||||||
"""Return tool specifications for ToolGenerator (10 tools)"""
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"name": "list_dashboards",
|
|
||||||
"method_name": "list_dashboards",
|
|
||||||
"description": "List all dashboards for a project.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
|
|
||||||
"required": ["project_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_dashboard",
|
|
||||||
"method_name": "get_dashboard",
|
|
||||||
"description": "Get dashboard details including all charts.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "create_dashboard",
|
|
||||||
"method_name": "create_dashboard",
|
|
||||||
"description": "Create a new custom dashboard.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"name": {"type": "string", "description": "Dashboard name"},
|
|
||||||
"description": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Dashboard description",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "name"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "update_dashboard",
|
|
||||||
"method_name": "update_dashboard",
|
|
||||||
"description": "Update dashboard properties.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID"},
|
|
||||||
"name": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "New name",
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "New description",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "delete_dashboard",
|
|
||||||
"method_name": "delete_dashboard",
|
|
||||||
"description": "Delete a dashboard.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID to delete"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "add_chart",
|
|
||||||
"method_name": "add_chart",
|
|
||||||
"description": "Add a new chart to a dashboard.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID"},
|
|
||||||
"chart_type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"line",
|
|
||||||
"bar",
|
|
||||||
"area",
|
|
||||||
"pie",
|
|
||||||
"map",
|
|
||||||
"histogram",
|
|
||||||
"funnel",
|
|
||||||
"retention",
|
|
||||||
"metric",
|
|
||||||
],
|
|
||||||
"description": "Type of chart",
|
|
||||||
},
|
|
||||||
"title": {"type": "string", "description": "Chart title"},
|
|
||||||
"events": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"},
|
|
||||||
"segment": {"type": "string"},
|
|
||||||
},
|
|
||||||
"required": ["name"],
|
|
||||||
},
|
|
||||||
"description": "Events to include in chart",
|
|
||||||
},
|
|
||||||
"breakdowns": {
|
|
||||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
|
||||||
"description": "Breakdown dimensions",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id", "chart_type", "title", "events"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "update_chart",
|
|
||||||
"method_name": "update_chart",
|
|
||||||
"description": "Update an existing chart configuration.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID"},
|
|
||||||
"chart_id": {"type": "string", "description": "Chart ID to update"},
|
|
||||||
"title": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "New title",
|
|
||||||
},
|
|
||||||
"chart_type": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "New chart type",
|
|
||||||
},
|
|
||||||
"events": {
|
|
||||||
"anyOf": [{"type": "array"}, {"type": "null"}],
|
|
||||||
"description": "New events configuration",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id", "chart_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "delete_chart",
|
|
||||||
"method_name": "delete_chart",
|
|
||||||
"description": "Remove a chart from a dashboard.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID"},
|
|
||||||
"chart_id": {"type": "string", "description": "Chart ID to delete"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id", "chart_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "duplicate_dashboard",
|
|
||||||
"method_name": "duplicate_dashboard",
|
|
||||||
"description": "Create a copy of an existing dashboard.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID to duplicate"},
|
|
||||||
"new_name": {"type": "string", "description": "Name for the new dashboard"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id", "new_name"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "share_dashboard",
|
|
||||||
"method_name": "share_dashboard",
|
|
||||||
"description": "Generate a shareable link for a dashboard.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"dashboard_id": {"type": "string", "description": "Dashboard ID"},
|
|
||||||
"public": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Make dashboard publicly accessible",
|
|
||||||
"default": False,
|
|
||||||
},
|
|
||||||
"expires_in_days": {
|
|
||||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
||||||
"description": "Link expiration in days (null for no expiration)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "dashboard_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
|
||||||
# Dashboard Functions (10)
|
|
||||||
# =====================
|
|
||||||
|
|
||||||
|
|
||||||
async def list_dashboards(client: OpenPanelClient, project_id: str) -> str:
|
|
||||||
"""List all dashboards"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"note": "Dashboard listing requires dashboard tRPC API. Use OpenPanel dashboard to view dashboards.",
|
|
||||||
"message": "Dashboard list request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_dashboard(client: OpenPanelClient, project_id: str, dashboard_id: str) -> str:
|
|
||||||
"""Get dashboard details"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"note": "Dashboard details require dashboard tRPC API. Use OpenPanel dashboard for full view.",
|
|
||||||
"message": "Dashboard request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_dashboard(
|
|
||||||
client: OpenPanelClient, project_id: str, name: str, description: str | None = None
|
|
||||||
) -> str:
|
|
||||||
"""Create a new dashboard"""
|
|
||||||
try:
|
|
||||||
dashboard_config = {"name": name, "description": description}
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard": dashboard_config,
|
|
||||||
"note": "Dashboard creation requires dashboard tRPC API. Use OpenPanel dashboard to create.",
|
|
||||||
"message": f"Dashboard '{name}' configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_dashboard(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
dashboard_id: str,
|
|
||||||
name: str | None = None,
|
|
||||||
description: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Update dashboard properties"""
|
|
||||||
try:
|
|
||||||
updates = {}
|
|
||||||
if name:
|
|
||||||
updates["name"] = name
|
|
||||||
if description:
|
|
||||||
updates["description"] = description
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"updates": updates,
|
|
||||||
"note": "Dashboard updates require dashboard tRPC API. Use OpenPanel dashboard to modify.",
|
|
||||||
"message": "Dashboard update configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_dashboard(client: OpenPanelClient, project_id: str, dashboard_id: str) -> str:
|
|
||||||
"""Delete a dashboard"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"note": "Dashboard deletion requires dashboard tRPC API. Use OpenPanel dashboard to delete.",
|
|
||||||
"message": "Dashboard deletion request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def add_chart(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
dashboard_id: str,
|
|
||||||
chart_type: str,
|
|
||||||
title: str,
|
|
||||||
events: list[dict[str, Any]],
|
|
||||||
breakdowns: list[str] | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Add a chart to dashboard"""
|
|
||||||
try:
|
|
||||||
chart_config = {
|
|
||||||
"chart_type": chart_type,
|
|
||||||
"title": title,
|
|
||||||
"events": events,
|
|
||||||
"breakdowns": breakdowns,
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"chart": chart_config,
|
|
||||||
"note": "Chart creation requires dashboard tRPC API. Use OpenPanel dashboard to add charts.",
|
|
||||||
"message": f"Chart '{title}' configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_chart(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
dashboard_id: str,
|
|
||||||
chart_id: str,
|
|
||||||
title: str | None = None,
|
|
||||||
chart_type: str | None = None,
|
|
||||||
events: list[dict[str, Any]] | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Update chart configuration"""
|
|
||||||
try:
|
|
||||||
updates = {}
|
|
||||||
if title:
|
|
||||||
updates["title"] = title
|
|
||||||
if chart_type:
|
|
||||||
updates["chart_type"] = chart_type
|
|
||||||
if events:
|
|
||||||
updates["events"] = events
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"chart_id": chart_id,
|
|
||||||
"updates": updates,
|
|
||||||
"note": "Chart updates require dashboard tRPC API. Use OpenPanel dashboard to modify charts.",
|
|
||||||
"message": "Chart update configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_chart(
|
|
||||||
client: OpenPanelClient, project_id: str, dashboard_id: str, chart_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Remove chart from dashboard"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"chart_id": chart_id,
|
|
||||||
"note": "Chart deletion requires dashboard tRPC API. Use OpenPanel dashboard to remove charts.",
|
|
||||||
"message": "Chart deletion request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def duplicate_dashboard(
|
|
||||||
client: OpenPanelClient, project_id: str, dashboard_id: str, new_name: str
|
|
||||||
) -> str:
|
|
||||||
"""Duplicate a dashboard"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"new_name": new_name,
|
|
||||||
"note": "Dashboard duplication requires dashboard tRPC API. Use OpenPanel dashboard to clone.",
|
|
||||||
"message": f"Dashboard duplication request for '{new_name}'",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def share_dashboard(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
dashboard_id: str,
|
|
||||||
public: bool = False,
|
|
||||||
expires_in_days: int | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Generate shareable link"""
|
|
||||||
try:
|
|
||||||
share_config = {"public": public, "expires_in_days": expires_in_days}
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"dashboard_id": dashboard_id,
|
|
||||||
"share_config": share_config,
|
|
||||||
"note": "Dashboard sharing requires dashboard tRPC API. Use OpenPanel dashboard to share.",
|
|
||||||
"message": "Dashboard share configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Events Handler - OpenPanel event tracking operations (10 tools)"""
|
"""Events Handler - OpenPanel event tracking operations (11 tools)"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -7,7 +7,7 @@ from plugins.openpanel.client import OpenPanelClient
|
|||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""Return tool specifications for ToolGenerator (10 tools)"""
|
"""Return tool specifications for ToolGenerator (11 tools)"""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": "track_event",
|
"name": "track_event",
|
||||||
@@ -222,7 +222,56 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"scope": "write",
|
"scope": "write",
|
||||||
},
|
},
|
||||||
# NOTE: alias_user removed - not supported on most self-hosted OpenPanel instances
|
{
|
||||||
|
"name": "create_group",
|
||||||
|
"method_name": "create_group",
|
||||||
|
"description": "Create or update a group (e.g., company, workspace, team). Groups can be associated with events and profiles.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"group_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique group identifier",
|
||||||
|
},
|
||||||
|
"group_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Group category (e.g., 'company', 'workspace', 'team')",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display name for the group",
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||||
|
"description": 'Custom group properties (e.g., {"plan": "enterprise", "size": 50})',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["group_id", "group_type", "name"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "assign_group",
|
||||||
|
"method_name": "assign_group",
|
||||||
|
"description": "Assign a user to one or more groups. If no profile_id is provided, falls back to device ID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"group_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "List of group IDs to assign the user to",
|
||||||
|
"minItems": 1,
|
||||||
|
},
|
||||||
|
"profile_id": {
|
||||||
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
|
"description": "User profile ID (falls back to device ID if not provided)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["group_ids"],
|
||||||
|
},
|
||||||
|
"scope": "write",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "track_revenue",
|
"name": "track_revenue",
|
||||||
"method_name": "track_revenue",
|
"method_name": "track_revenue",
|
||||||
@@ -566,32 +615,55 @@ async def decrement_property(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def alias_user(client: OpenPanelClient, profile_id: str, alias: str) -> str:
|
async def create_group(
|
||||||
"""Create an alias to link two profile IDs"""
|
client: OpenPanelClient,
|
||||||
|
group_id: str,
|
||||||
|
group_type: str,
|
||||||
|
name: str,
|
||||||
|
properties: dict[str, Any] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Create or update a group"""
|
||||||
try:
|
try:
|
||||||
result = await client.alias_user(profile_id=profile_id, alias=alias)
|
result = await client.track_group(
|
||||||
|
group_id=group_id, group_type=group_type, name=name, properties=properties
|
||||||
|
)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"profile_id": profile_id,
|
"group_id": group_id,
|
||||||
"alias": alias,
|
"group_type": group_type,
|
||||||
"message": f"Alias '{alias}' linked to profile '{profile_id}'",
|
"name": name,
|
||||||
|
"message": f"Group '{name}' ({group_type}) created/updated",
|
||||||
"response": result,
|
"response": result,
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_str = str(e)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
hint = ""
|
|
||||||
if "not supported" in error_str.lower() or "400" in error_str:
|
|
||||||
hint = " (Note: Alias feature may not be available in all OpenPanel configurations. This is a server-side limitation.)"
|
async def assign_group(
|
||||||
|
client: OpenPanelClient,
|
||||||
|
group_ids: list[str],
|
||||||
|
profile_id: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Assign a user to groups"""
|
||||||
|
try:
|
||||||
|
result = await client.assign_group(group_ids=group_ids, profile_id=profile_id)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{"success": False, "error": error_str + hint, "profile_id": profile_id, "alias": alias},
|
{
|
||||||
|
"success": True,
|
||||||
|
"group_ids": group_ids,
|
||||||
|
"profile_id": profile_id,
|
||||||
|
"message": f"User assigned to {len(group_ids)} group(s)",
|
||||||
|
"response": result,
|
||||||
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
async def track_revenue(
|
async def track_revenue(
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
"""Export Handler - OpenPanel data export operations (10 tools)
|
"""Export Handler - OpenPanel data export and analytics operations (10 tools).
|
||||||
|
|
||||||
|
Uses REST APIs:
|
||||||
|
- Export API (GET /export/events, /export/charts) for raw data export
|
||||||
|
- Insights API (GET /insights/:projectId/*) for analytics queries
|
||||||
|
|
||||||
Note: project_id is optional if configured in environment variables.
|
Note: project_id is optional if configured in environment variables.
|
||||||
When not provided, the default project_id from OPENPANEL_SITE1_PROJECT_ID is used.
|
When not provided, the default project_id from OPENPANEL_SITE1_PROJECT_ID is used.
|
||||||
|
|
||||||
|
Requires 'read' or 'root' mode client for Export API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -12,12 +18,12 @@ from plugins.openpanel.handlers.utils import get_project_id as _get_project_id
|
|||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""Return tool specifications for ToolGenerator (10 tools)"""
|
"""Return tool specifications for ToolGenerator (10 tools)."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": "export_events",
|
"name": "export_events",
|
||||||
"method_name": "export_events",
|
"method_name": "export_events",
|
||||||
"description": "Export raw event data with filters and pagination. Returns individual event records. Note: project_id is optional if configured in environment.",
|
"description": "Export raw event data with filters and pagination. Returns individual event records. Requires 'read' or 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -56,11 +62,22 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string", "enum": ["profile", "meta"]},
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"profile",
|
||||||
|
"meta",
|
||||||
|
"properties",
|
||||||
|
"region",
|
||||||
|
"device",
|
||||||
|
"referrer",
|
||||||
|
"revenue",
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{"type": "null"},
|
{"type": "null"},
|
||||||
],
|
],
|
||||||
"description": "Additional data to include (profile, meta)",
|
"description": "Additional data to include (profile, meta, properties, region, device, referrer, revenue)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": [],
|
"required": [],
|
||||||
@@ -70,7 +87,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "export_events_csv",
|
"name": "export_events_csv",
|
||||||
"method_name": "export_events_csv",
|
"method_name": "export_events_csv",
|
||||||
"description": "Export events as CSV-formatted data for spreadsheet analysis. Note: project_id is optional if configured in environment.",
|
"description": "Export events as CSV-formatted data for spreadsheet analysis. Requires 'read' or 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -103,7 +120,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "export_chart_data",
|
"name": "export_chart_data",
|
||||||
"method_name": "export_chart_data",
|
"method_name": "export_chart_data",
|
||||||
"description": "Export aggregated chart data with time series and breakdowns. Note: project_id is optional if configured in environment.",
|
"description": "Export aggregated chart data with time series and breakdowns. Requires 'read' or 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -150,7 +167,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common values: 30min, lastHour, today, yesterday, 1d, 3d, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, lastMonth, yearToDate, lastYear, all. Can also use custom ranges like '2024-01-01 to 2024-12-31'",
|
"description": "Date range (30min, today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
"breakdowns": {
|
"breakdowns": {
|
||||||
@@ -188,7 +205,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_event_count",
|
"name": "get_event_count",
|
||||||
"method_name": "get_event_count",
|
"method_name": "get_event_count",
|
||||||
"description": "Get total event count with optional filters. Note: project_id is optional if configured in environment.",
|
"description": "Get total event count with optional filters via Export API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -202,7 +219,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -213,7 +230,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_unique_users",
|
"name": "get_unique_users",
|
||||||
"method_name": "get_unique_users",
|
"method_name": "get_unique_users",
|
||||||
"description": "Get unique user/visitor count for a time period. Note: project_id is optional if configured in environment.",
|
"description": "Get unique user/visitor count for a time period via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -223,7 +240,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -234,7 +251,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_page_views",
|
"name": "get_page_views",
|
||||||
"method_name": "get_page_views",
|
"method_name": "get_page_views",
|
||||||
"description": "Get page view statistics over time. Note: project_id is optional if configured in environment.",
|
"description": "Get page view statistics via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -244,15 +261,9 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
"interval": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["hour", "day", "week", "month"],
|
|
||||||
"description": "Time interval",
|
|
||||||
"default": "day",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"required": [],
|
"required": [],
|
||||||
},
|
},
|
||||||
@@ -261,7 +272,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_top_pages",
|
"name": "get_top_pages",
|
||||||
"method_name": "get_top_pages",
|
"method_name": "get_top_pages",
|
||||||
"description": "Get top pages by view count. Note: project_id is optional if configured in environment.",
|
"description": "Get top pages by view count via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -271,7 +282,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
@@ -287,7 +298,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_top_referrers",
|
"name": "get_top_referrers",
|
||||||
"method_name": "get_top_referrers",
|
"method_name": "get_top_referrers",
|
||||||
"description": "Get top traffic sources/referrers. Note: project_id is optional if configured in environment.",
|
"description": "Get top traffic sources/referrers via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -297,7 +308,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
@@ -313,7 +324,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_geo_data",
|
"name": "get_geo_data",
|
||||||
"method_name": "get_geo_data",
|
"method_name": "get_geo_data",
|
||||||
"description": "Get geographic distribution of users/visitors. Note: project_id is optional if configured in environment.",
|
"description": "Get geographic distribution of visitors via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -323,7 +334,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
"breakdown": {
|
"breakdown": {
|
||||||
@@ -345,7 +356,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_device_data",
|
"name": "get_device_data",
|
||||||
"method_name": "get_device_data",
|
"method_name": "get_device_data",
|
||||||
"description": "Get device/browser/OS breakdown of users. Note: project_id is optional if configured in environment.",
|
"description": "Get device/browser/OS breakdown of visitors via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -355,7 +366,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
"breakdown": {
|
"breakdown": {
|
||||||
@@ -393,11 +404,11 @@ async def export_events(
|
|||||||
page: int = 1,
|
page: int = 1,
|
||||||
includes: list[str] | None = None,
|
includes: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Export raw event data"""
|
"""Export raw event data via GET /export/events."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
result = await client.export_events(
|
result = await client.export_events(
|
||||||
project_id=effective_project_id,
|
project_id=pid,
|
||||||
event=event,
|
event=event,
|
||||||
profile_id=profile_id,
|
profile_id=profile_id,
|
||||||
start=start,
|
start=start,
|
||||||
@@ -406,18 +417,17 @@ async def export_events(
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
includes=includes,
|
includes=includes,
|
||||||
)
|
)
|
||||||
|
meta = result.get("meta", {}) if isinstance(result, dict) else {}
|
||||||
events_count = len(result.get("data", [])) if isinstance(result, dict) else 0
|
data = result.get("data", []) if isinstance(result, dict) else []
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"count": events_count,
|
"count": len(data),
|
||||||
"page": page,
|
"total": meta.get("totalCount", len(data)),
|
||||||
"limit": limit,
|
"page": meta.get("current", page),
|
||||||
"filters": {"event": event, "profile_id": profile_id, "start": start, "end": end},
|
"pages": meta.get("pages", 1),
|
||||||
"data": result,
|
"data": data,
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -434,20 +444,18 @@ async def export_events_csv(
|
|||||||
end: str | None = None,
|
end: str | None = None,
|
||||||
limit: int = 1000,
|
limit: int = 1000,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Export events as CSV-formatted data"""
|
"""Export events as CSV-formatted data."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
result = await client.export_events(
|
result = await client.export_events(
|
||||||
project_id=effective_project_id, event=event, start=start, end=end, limit=limit
|
project_id=pid, event=event, start=start, end=end, limit=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
events = result.get("data", []) if isinstance(result, dict) else []
|
events = result.get("data", []) if isinstance(result, dict) else []
|
||||||
|
|
||||||
if not events:
|
if not events:
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"count": 0,
|
"count": 0,
|
||||||
"csv": "",
|
"csv": "",
|
||||||
"message": "No events found",
|
"message": "No events found",
|
||||||
@@ -455,28 +463,22 @@ async def export_events_csv(
|
|||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build CSV
|
|
||||||
headers = ["timestamp", "name", "profile_id"]
|
headers = ["timestamp", "name", "profile_id"]
|
||||||
csv_lines = [",".join(headers)]
|
csv_lines = [",".join(headers)]
|
||||||
|
for ev in events:
|
||||||
for event_data in events:
|
|
||||||
row = [
|
row = [
|
||||||
str(event_data.get("timestamp", "")),
|
str(ev.get("createdAt", ev.get("timestamp", ""))),
|
||||||
str(event_data.get("name", "")),
|
str(ev.get("name", "")),
|
||||||
str(event_data.get("profileId", "")),
|
str(ev.get("profileId", "")),
|
||||||
]
|
]
|
||||||
csv_lines.append(",".join(row))
|
csv_lines.append(",".join(row))
|
||||||
|
|
||||||
csv_content = "\n".join(csv_lines)
|
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"count": len(events),
|
"count": len(events),
|
||||||
"format": "csv",
|
"format": "csv",
|
||||||
"csv": csv_content,
|
"csv": "\n".join(csv_lines),
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -494,22 +496,22 @@ async def export_chart_data(
|
|||||||
breakdowns: list[str] | None = None,
|
breakdowns: list[str] | None = None,
|
||||||
previous: bool = False,
|
previous: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Export aggregated chart data"""
|
"""Export aggregated chart data via GET /export/charts."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
bd = [{"name": b} for b in breakdowns] if breakdowns else None
|
||||||
result = await client.export_charts(
|
result = await client.export_charts(
|
||||||
project_id=effective_project_id,
|
project_id=pid,
|
||||||
events=events,
|
events=events,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
date_range=date_range,
|
date_range=date_range,
|
||||||
breakdowns=breakdowns,
|
breakdowns=bd,
|
||||||
previous=previous,
|
previous=previous,
|
||||||
)
|
)
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"events": [e.get("name") for e in events],
|
"events": [e.get("name") for e in events],
|
||||||
"interval": interval,
|
"interval": interval,
|
||||||
"date_range": date_range,
|
"date_range": date_range,
|
||||||
@@ -529,29 +531,33 @@ async def get_event_count(
|
|||||||
event: str | None = None,
|
event: str | None = None,
|
||||||
date_range: str = "30d",
|
date_range: str = "30d",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get total event count"""
|
"""Get total event count via Export charts API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
events_config = [{"name": event if event else "*", "segment": "event"}]
|
events_config = [{"name": event if event else "*", "segment": "event"}]
|
||||||
|
|
||||||
result = await client.export_charts(
|
result = await client.export_charts(
|
||||||
project_id=effective_project_id,
|
project_id=pid,
|
||||||
events=events_config,
|
events=events_config,
|
||||||
interval="day",
|
interval="day",
|
||||||
date_range=date_range,
|
date_range=date_range,
|
||||||
)
|
)
|
||||||
|
# Extract total from chart response
|
||||||
# Sum up the counts
|
|
||||||
total = 0
|
total = 0
|
||||||
if isinstance(result, dict) and "data" in result:
|
if isinstance(result, dict):
|
||||||
for point in result.get("data", []):
|
series = result.get("series", [])
|
||||||
|
if series:
|
||||||
|
for s in series:
|
||||||
|
for point in s.get("data", []):
|
||||||
total += point.get("count", 0)
|
total += point.get("count", 0)
|
||||||
|
# Alternative: check metrics
|
||||||
|
metrics = result.get("metrics", {})
|
||||||
|
if metrics.get("current", {}).get("value"):
|
||||||
|
total = metrics["current"]["value"]
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"event": event if event else "all events",
|
"event": event or "all events",
|
||||||
"date_range": date_range,
|
"date_range": date_range,
|
||||||
"total_count": total,
|
"total_count": total,
|
||||||
},
|
},
|
||||||
@@ -565,47 +571,12 @@ async def get_event_count(
|
|||||||
async def get_unique_users(
|
async def get_unique_users(
|
||||||
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
|
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get unique user count using tRPC overview.stats"""
|
"""Get unique user count via Insights metrics API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
result = await client.get_overview_stats(project_id=pid, date_range=date_range)
|
||||||
# Use overview.stats to get visitor count
|
|
||||||
result = await client.get_overview_stats(
|
|
||||||
project_id=effective_project_id, date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "project_id": pid, "date_range": date_range, "stats": result},
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC overview.stats endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract unique visitors from overview stats
|
|
||||||
unique_users = 0
|
|
||||||
if isinstance(result, dict):
|
|
||||||
# Try different possible field names for visitors
|
|
||||||
unique_users = (
|
|
||||||
result.get("visitors", 0)
|
|
||||||
or result.get("uniqueVisitors", 0)
|
|
||||||
or result.get("current", {}).get("visitors", 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"unique_users": unique_users,
|
|
||||||
"raw_stats": result if isinstance(result, dict) else None,
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -617,51 +588,13 @@ async def get_page_views(
|
|||||||
client: OpenPanelClient,
|
client: OpenPanelClient,
|
||||||
project_id: str | None = None,
|
project_id: str | None = None,
|
||||||
date_range: str = "30d",
|
date_range: str = "30d",
|
||||||
interval: str = "day",
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get page view statistics using tRPC overview.stats"""
|
"""Get page view statistics via Insights metrics API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
result = await client.get_overview_stats(project_id=pid, date_range=date_range)
|
||||||
# Use overview.stats to get pageview count
|
|
||||||
result = await client.get_overview_stats(
|
|
||||||
project_id=effective_project_id, date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "project_id": pid, "date_range": date_range, "stats": result},
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"interval": interval,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC overview.stats endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract pageview count from overview stats
|
|
||||||
total_page_views = 0
|
|
||||||
if isinstance(result, dict):
|
|
||||||
# Try different possible field names for pageviews
|
|
||||||
total_page_views = (
|
|
||||||
result.get("pageviews", 0)
|
|
||||||
or result.get("pageViews", 0)
|
|
||||||
or result.get("current", {}).get("pageviews", 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"interval": interval,
|
|
||||||
"total_page_views": total_page_views,
|
|
||||||
"raw_stats": result if isinstance(result, dict) else None,
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -672,44 +605,12 @@ async def get_page_views(
|
|||||||
async def get_top_pages(
|
async def get_top_pages(
|
||||||
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
|
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get top pages by view count using tRPC overview.topPages"""
|
"""Get top pages via Insights pages API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
result = await client.get_top_pages(project_id=pid, date_range=date_range, limit=limit)
|
||||||
# Use the new tRPC overview.topPages endpoint
|
|
||||||
result = await client.get_top_pages(
|
|
||||||
project_id=effective_project_id, date_range=date_range, mode="page"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "project_id": pid, "date_range": date_range, "data": result},
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC overview.topPages endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Transform tRPC response to our format
|
|
||||||
pages = []
|
|
||||||
if isinstance(result, list):
|
|
||||||
pages = result[:limit]
|
|
||||||
elif isinstance(result, dict) and "data" in result:
|
|
||||||
pages = result.get("data", [])[:limit]
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"count": len(pages),
|
|
||||||
"top_pages": pages,
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -720,54 +621,12 @@ async def get_top_pages(
|
|||||||
async def get_top_referrers(
|
async def get_top_referrers(
|
||||||
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
|
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get top traffic sources using chart.chart with referrer breakdown"""
|
"""Get top traffic sources via Insights breakdown API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
result = await client.get_top_sources(project_id=pid, date_range=date_range, limit=limit)
|
||||||
# Use chart.chart with referrer breakdown
|
|
||||||
result = await client.get_top_sources(
|
|
||||||
project_id=effective_project_id, date_range=date_range, limit=limit
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "project_id": pid, "date_range": date_range, "data": result},
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC chart.chart endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract referrers from chart.chart response
|
|
||||||
# Response format: {series: [{data: [...], breakdowns: {referrer: [...]}}]}
|
|
||||||
referrers = []
|
|
||||||
if isinstance(result, dict):
|
|
||||||
series = result.get("series", [])
|
|
||||||
if series:
|
|
||||||
for serie in series:
|
|
||||||
breakdown_data = serie.get("breakdowns", {}).get("referrer", [])
|
|
||||||
for item in breakdown_data[:limit]:
|
|
||||||
referrers.append(
|
|
||||||
{
|
|
||||||
"referrer": item.get("label", item.get("name", "")),
|
|
||||||
"count": item.get("count", 0),
|
|
||||||
"percentage": item.get("percentage", 0),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"count": len(referrers),
|
|
||||||
"top_referrers": referrers,
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -782,55 +641,19 @@ async def get_geo_data(
|
|||||||
breakdown: str = "country",
|
breakdown: str = "country",
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get geographic distribution using chart.chart with country/city/region breakdown"""
|
"""Get geographic distribution via Insights breakdown API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
|
||||||
# Use chart.chart with geographic breakdown
|
|
||||||
result = await client.get_top_locations(
|
result = await client.get_top_locations(
|
||||||
project_id=effective_project_id, date_range=date_range, breakdown=breakdown, limit=limit
|
project_id=pid, date_range=date_range, breakdown=breakdown, limit=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"breakdown": breakdown,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC chart.chart endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract locations from chart.chart response
|
|
||||||
# Response format: {series: [{data: [...], breakdowns: {country: [...]}}]}
|
|
||||||
locations = []
|
|
||||||
if isinstance(result, dict):
|
|
||||||
series = result.get("series", [])
|
|
||||||
if series:
|
|
||||||
for serie in series:
|
|
||||||
breakdown_data = serie.get("breakdowns", {}).get(breakdown, [])
|
|
||||||
for item in breakdown_data[:limit]:
|
|
||||||
locations.append(
|
|
||||||
{
|
|
||||||
"location": item.get("label", item.get("name", "")),
|
|
||||||
"count": item.get("count", 0),
|
|
||||||
"percentage": item.get("percentage", 0),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"date_range": date_range,
|
"date_range": date_range,
|
||||||
"breakdown": breakdown,
|
"breakdown": breakdown,
|
||||||
"count": len(locations),
|
"data": result,
|
||||||
"locations": locations,
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -846,64 +669,26 @@ async def get_device_data(
|
|||||||
breakdown: str = "device",
|
breakdown: str = "device",
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get device/browser/OS breakdown using chart.chart with appropriate breakdown"""
|
"""Get device/browser/OS breakdown via Insights breakdown API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
|
||||||
# Use appropriate chart.chart breakdown based on breakdown type
|
|
||||||
if breakdown == "browser":
|
if breakdown == "browser":
|
||||||
result = await client.get_top_browsers(
|
result = await client.get_top_browsers(
|
||||||
project_id=effective_project_id, date_range=date_range, limit=limit
|
project_id=pid, date_range=date_range, limit=limit
|
||||||
)
|
)
|
||||||
elif breakdown == "os":
|
elif breakdown == "os":
|
||||||
result = await client.get_top_os(
|
result = await client.get_top_os(project_id=pid, date_range=date_range, limit=limit)
|
||||||
project_id=effective_project_id, date_range=date_range, limit=limit
|
else:
|
||||||
)
|
|
||||||
else: # device (default)
|
|
||||||
result = await client.get_top_devices(
|
result = await client.get_top_devices(
|
||||||
project_id=effective_project_id, date_range=date_range, limit=limit
|
project_id=pid, date_range=date_range, limit=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"breakdown": breakdown,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC chart.chart endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract device data from chart.chart response
|
|
||||||
# Response format: {series: [{data: [...], breakdowns: {device: [...]}}]}
|
|
||||||
devices = []
|
|
||||||
if isinstance(result, dict):
|
|
||||||
series = result.get("series", [])
|
|
||||||
if series:
|
|
||||||
for serie in series:
|
|
||||||
breakdown_data = serie.get("breakdowns", {}).get(breakdown, [])
|
|
||||||
for item in breakdown_data[:limit]:
|
|
||||||
devices.append(
|
|
||||||
{
|
|
||||||
"name": item.get("label", item.get("name", "")),
|
|
||||||
"count": item.get("count", 0),
|
|
||||||
"percentage": item.get("percentage", 0),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": effective_project_id,
|
"project_id": pid,
|
||||||
"date_range": date_range,
|
"date_range": date_range,
|
||||||
"breakdown": breakdown,
|
"breakdown": breakdown,
|
||||||
"count": len(devices),
|
"data": result,
|
||||||
"devices": devices,
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
|
|||||||
@@ -1,388 +0,0 @@
|
|||||||
"""Funnels Handler - OpenPanel funnel analytics (8 tools)"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from plugins.openpanel.client import OpenPanelClient
|
|
||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
|
||||||
"""Return tool specifications for ToolGenerator (8 tools)"""
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"name": "list_funnels",
|
|
||||||
"method_name": "list_funnels",
|
|
||||||
"description": "List all funnels defined for a project.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
|
|
||||||
"required": ["project_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_funnel",
|
|
||||||
"method_name": "get_funnel",
|
|
||||||
"description": "Get funnel details and current conversion data.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"funnel_id": {"type": "string", "description": "Funnel ID"},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "funnel_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "create_funnel",
|
|
||||||
"method_name": "create_funnel",
|
|
||||||
"description": "Create a new funnel to track user journey through steps.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"name": {"type": "string", "description": "Funnel name"},
|
|
||||||
"steps": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string", "description": "Step name"},
|
|
||||||
"event": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Event name for this step",
|
|
||||||
},
|
|
||||||
"filters": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Optional filters for this step",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["name", "event"],
|
|
||||||
},
|
|
||||||
"description": "Funnel steps in sequence",
|
|
||||||
"minItems": 2,
|
|
||||||
"maxItems": 10,
|
|
||||||
},
|
|
||||||
"window_days": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Conversion window in days",
|
|
||||||
"default": 14,
|
|
||||||
"maximum": 90,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "name", "steps"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "update_funnel",
|
|
||||||
"method_name": "update_funnel",
|
|
||||||
"description": "Update an existing funnel's configuration.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"funnel_id": {"type": "string", "description": "Funnel ID"},
|
|
||||||
"name": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "New funnel name",
|
|
||||||
},
|
|
||||||
"steps": {
|
|
||||||
"anyOf": [{"type": "array"}, {"type": "null"}],
|
|
||||||
"description": "New funnel steps",
|
|
||||||
},
|
|
||||||
"window_days": {
|
|
||||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
||||||
"description": "New conversion window",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "funnel_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "delete_funnel",
|
|
||||||
"method_name": "delete_funnel",
|
|
||||||
"description": "Delete a funnel.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"funnel_id": {"type": "string", "description": "Funnel ID to delete"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "funnel_id"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_funnel_conversion",
|
|
||||||
"method_name": "get_funnel_conversion",
|
|
||||||
"description": "Get detailed conversion rates for each funnel step.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"funnel_id": {"type": "string", "description": "Funnel ID"},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "funnel_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_funnel_breakdown",
|
|
||||||
"method_name": "get_funnel_breakdown",
|
|
||||||
"description": "Get funnel breakdown by segment (country, device, etc.).",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"funnel_id": {"type": "string", "description": "Funnel ID"},
|
|
||||||
"breakdown_by": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["country", "device", "browser", "os", "referrer"],
|
|
||||||
"description": "Dimension to break down by",
|
|
||||||
},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "funnel_id", "breakdown_by"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "compare_funnels",
|
|
||||||
"method_name": "compare_funnels",
|
|
||||||
"description": "Compare conversion rates between multiple funnels.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"funnel_ids": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"description": "Funnel IDs to compare",
|
|
||||||
"minItems": 2,
|
|
||||||
"maxItems": 5,
|
|
||||||
},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "funnel_ids"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
|
||||||
# Funnel Functions (8)
|
|
||||||
# =====================
|
|
||||||
|
|
||||||
|
|
||||||
async def list_funnels(client: OpenPanelClient, project_id: str) -> str:
|
|
||||||
"""List all funnels for a project"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"note": "Funnel listing requires dashboard tRPC API. Use OpenPanel dashboard to view funnels.",
|
|
||||||
"message": "Funnel list request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_funnel(
|
|
||||||
client: OpenPanelClient, project_id: str, funnel_id: str, date_range: str = "30d"
|
|
||||||
) -> str:
|
|
||||||
"""Get funnel details with conversion data"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel_id": funnel_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"note": "Funnel details require dashboard tRPC API. Use OpenPanel dashboard to view funnel data.",
|
|
||||||
"message": "Funnel data request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_funnel(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
name: str,
|
|
||||||
steps: list[dict[str, Any]],
|
|
||||||
window_days: int = 14,
|
|
||||||
) -> str:
|
|
||||||
"""Create a new funnel"""
|
|
||||||
try:
|
|
||||||
# Validate steps
|
|
||||||
if len(steps) < 2:
|
|
||||||
return json.dumps(
|
|
||||||
{"success": False, "error": "Funnel must have at least 2 steps"},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
funnel_config = {"name": name, "steps": steps, "window_days": window_days}
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel": funnel_config,
|
|
||||||
"note": "Funnel creation requires dashboard tRPC API. Use OpenPanel dashboard to create funnels.",
|
|
||||||
"message": f"Funnel '{name}' configuration created with {len(steps)} steps",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_funnel(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
funnel_id: str,
|
|
||||||
name: str | None = None,
|
|
||||||
steps: list[dict[str, Any]] | None = None,
|
|
||||||
window_days: int | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Update an existing funnel"""
|
|
||||||
try:
|
|
||||||
updates = {}
|
|
||||||
if name:
|
|
||||||
updates["name"] = name
|
|
||||||
if steps:
|
|
||||||
updates["steps"] = steps
|
|
||||||
if window_days:
|
|
||||||
updates["window_days"] = window_days
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel_id": funnel_id,
|
|
||||||
"updates": updates,
|
|
||||||
"note": "Funnel updates require dashboard tRPC API. Use OpenPanel dashboard to modify funnels.",
|
|
||||||
"message": "Funnel update configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_funnel(client: OpenPanelClient, project_id: str, funnel_id: str) -> str:
|
|
||||||
"""Delete a funnel"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel_id": funnel_id,
|
|
||||||
"note": "Funnel deletion requires dashboard tRPC API. Use OpenPanel dashboard to delete funnels.",
|
|
||||||
"message": "Funnel deletion request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_funnel_conversion(
|
|
||||||
client: OpenPanelClient, project_id: str, funnel_id: str, date_range: str = "30d"
|
|
||||||
) -> str:
|
|
||||||
"""Get funnel conversion rates"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel_id": funnel_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"note": "Conversion data requires dashboard tRPC API. Use OpenPanel dashboard for detailed conversion analysis.",
|
|
||||||
"message": "Funnel conversion request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_funnel_breakdown(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
funnel_id: str,
|
|
||||||
breakdown_by: str,
|
|
||||||
date_range: str = "30d",
|
|
||||||
) -> str:
|
|
||||||
"""Get funnel breakdown by segment"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel_id": funnel_id,
|
|
||||||
"breakdown_by": breakdown_by,
|
|
||||||
"date_range": date_range,
|
|
||||||
"note": "Funnel breakdown requires dashboard tRPC API. Use OpenPanel dashboard for segmented analysis.",
|
|
||||||
"message": f"Funnel breakdown by {breakdown_by} request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def compare_funnels(
|
|
||||||
client: OpenPanelClient, project_id: str, funnel_ids: list[str], date_range: str = "30d"
|
|
||||||
) -> str:
|
|
||||||
"""Compare multiple funnels"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"funnel_ids": funnel_ids,
|
|
||||||
"date_range": date_range,
|
|
||||||
"note": "Funnel comparison requires dashboard tRPC API. Use OpenPanel dashboard for comparison views.",
|
|
||||||
"message": f"Comparison request for {len(funnel_ids)} funnels processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
"""Profiles Handler - OpenPanel user profile management (8 tools)"""
|
"""Profiles Handler - OpenPanel user profile operations (3 tools).
|
||||||
|
|
||||||
|
Uses Export API to retrieve profile events and data.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -7,81 +10,12 @@ from plugins.openpanel.client import OpenPanelClient
|
|||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""Return tool specifications for ToolGenerator (8 tools)"""
|
"""Return tool specifications for ToolGenerator (3 tools)."""
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
"name": "list_profiles",
|
|
||||||
"method_name": "list_profiles",
|
|
||||||
"description": "List user profiles with optional filtering and pagination.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"limit": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Number of profiles to return",
|
|
||||||
"default": 50,
|
|
||||||
"maximum": 100,
|
|
||||||
},
|
|
||||||
"offset": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Offset for pagination",
|
|
||||||
"default": 0,
|
|
||||||
},
|
|
||||||
"sort_by": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["last_seen", "first_seen", "event_count"],
|
|
||||||
"description": "Sort order",
|
|
||||||
"default": "last_seen",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_profile",
|
|
||||||
"method_name": "get_profile",
|
|
||||||
"description": "Get detailed information about a specific user profile.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"profile_id": {"type": "string", "description": "User profile ID"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "profile_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search_profiles",
|
|
||||||
"method_name": "search_profiles",
|
|
||||||
"description": "Search user profiles by property value.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"property": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Property to search (e.g., 'email', 'name', 'plan')",
|
|
||||||
},
|
|
||||||
"value": {"type": "string", "description": "Value to search for"},
|
|
||||||
"operator": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["is", "contains", "startsWith", "endsWith"],
|
|
||||||
"description": "Search operator",
|
|
||||||
"default": "contains",
|
|
||||||
},
|
|
||||||
"limit": {"type": "integer", "description": "Maximum results", "default": 50},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "property", "value"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "get_profile_events",
|
"name": "get_profile_events",
|
||||||
"method_name": "get_profile_events",
|
"method_name": "get_profile_events",
|
||||||
"description": "Get events for a specific user profile.",
|
"description": "Get events for a specific user profile via Export API. Requires 'read' or 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -95,7 +29,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of events",
|
"description": "Number of events",
|
||||||
"default": 50,
|
"default": 50,
|
||||||
"maximum": 200,
|
"maximum": 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["project_id", "profile_id"],
|
"required": ["project_id", "profile_id"],
|
||||||
@@ -105,7 +39,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_profile_sessions",
|
"name": "get_profile_sessions",
|
||||||
"method_name": "get_profile_sessions",
|
"method_name": "get_profile_sessions",
|
||||||
"description": "Get sessions for a specific user profile.",
|
"description": "Get sessions for a specific user profile via Export API. Requires 'read' or 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -115,57 +49,17 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of sessions",
|
"description": "Number of sessions",
|
||||||
"default": 20,
|
"default": 20,
|
||||||
"maximum": 50,
|
"maximum": 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["project_id", "profile_id"],
|
"required": ["project_id", "profile_id"],
|
||||||
},
|
},
|
||||||
"scope": "read",
|
"scope": "read",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "delete_profile",
|
|
||||||
"method_name": "delete_profile",
|
|
||||||
"description": "Delete a user profile and all associated data (GDPR compliance).",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"profile_id": {"type": "string", "description": "User profile ID to delete"},
|
|
||||||
"confirm": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Confirm deletion (required)",
|
|
||||||
"default": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "profile_id", "confirm"],
|
|
||||||
},
|
|
||||||
"scope": "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "merge_profiles",
|
|
||||||
"method_name": "merge_profiles",
|
|
||||||
"description": "Merge two user profiles into one (for duplicate resolution).",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"primary_profile_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Primary profile ID to keep",
|
|
||||||
},
|
|
||||||
"secondary_profile_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Secondary profile ID to merge and delete",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "primary_profile_id", "secondary_profile_id"],
|
|
||||||
},
|
|
||||||
"scope": "admin",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "export_profile_data",
|
"name": "export_profile_data",
|
||||||
"method_name": "export_profile_data",
|
"method_name": "export_profile_data",
|
||||||
"description": "Export all data for a user profile (GDPR data portability).",
|
"description": "Export all data for a user profile (GDPR data portability). Returns events and profile data. Requires 'read' or 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -186,104 +80,10 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# Profile Functions (8)
|
# Profile Functions (3)
|
||||||
# =====================
|
# =====================
|
||||||
|
|
||||||
|
|
||||||
async def list_profiles(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
limit: int = 50,
|
|
||||||
offset: int = 0,
|
|
||||||
sort_by: str = "last_seen",
|
|
||||||
) -> str:
|
|
||||||
"""List user profiles"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"pagination": {"limit": limit, "offset": offset, "sort_by": sort_by},
|
|
||||||
"note": "Profile listing requires dashboard tRPC API. Use OpenPanel dashboard to view profiles.",
|
|
||||||
"message": "Profile list request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_profile(client: OpenPanelClient, project_id: str, profile_id: str) -> str:
|
|
||||||
"""Get profile details"""
|
|
||||||
try:
|
|
||||||
# Try to get events for this profile via export API
|
|
||||||
try:
|
|
||||||
result = await client.export_events(
|
|
||||||
project_id=project_id, profile_id=profile_id, limit=1, includes=["profile"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(result, dict) and result.get("data"):
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"profile_id": profile_id,
|
|
||||||
"has_events": True,
|
|
||||||
"note": "Full profile details require dashboard tRPC API.",
|
|
||||||
"message": "Profile found with associated events",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass # Profile event fetch is optional; fall through to generic response
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"profile_id": profile_id,
|
|
||||||
"note": "Profile details require dashboard tRPC API. Use OpenPanel dashboard for full profile view.",
|
|
||||||
"message": "Profile request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def search_profiles(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str,
|
|
||||||
property: str,
|
|
||||||
value: str,
|
|
||||||
operator: str = "contains",
|
|
||||||
limit: int = 50,
|
|
||||||
) -> str:
|
|
||||||
"""Search profiles by property"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"search": {
|
|
||||||
"property": property,
|
|
||||||
"value": value,
|
|
||||||
"operator": operator,
|
|
||||||
"limit": limit,
|
|
||||||
},
|
|
||||||
"note": "Profile search requires dashboard tRPC API. Use OpenPanel dashboard for profile search.",
|
|
||||||
"message": "Profile search request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_profile_events(
|
async def get_profile_events(
|
||||||
client: OpenPanelClient,
|
client: OpenPanelClient,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
@@ -291,22 +91,22 @@ async def get_profile_events(
|
|||||||
event: str | None = None,
|
event: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get events for a profile"""
|
"""Get events for a profile via Export API."""
|
||||||
try:
|
try:
|
||||||
result = await client.export_events(
|
result = await client.export_events(
|
||||||
project_id=project_id, profile_id=profile_id, event=event, limit=limit
|
project_id=project_id, profile_id=profile_id, event=event, limit=limit
|
||||||
)
|
)
|
||||||
|
data = result.get("data", []) if isinstance(result, dict) else []
|
||||||
events_count = len(result.get("data", [])) if isinstance(result, dict) else 0
|
meta = result.get("meta", {}) if isinstance(result, dict) else {}
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"profile_id": profile_id,
|
"profile_id": profile_id,
|
||||||
"event_filter": event,
|
"event_filter": event,
|
||||||
"count": events_count,
|
"count": len(data),
|
||||||
"data": result,
|
"total": meta.get("totalCount", len(data)),
|
||||||
|
"data": data,
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -318,75 +118,19 @@ async def get_profile_events(
|
|||||||
async def get_profile_sessions(
|
async def get_profile_sessions(
|
||||||
client: OpenPanelClient, project_id: str, profile_id: str, limit: int = 20
|
client: OpenPanelClient, project_id: str, profile_id: str, limit: int = 20
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get sessions for a profile"""
|
"""Get sessions for a profile via Export API."""
|
||||||
try:
|
try:
|
||||||
# Get session events
|
|
||||||
result = await client.export_events(
|
result = await client.export_events(
|
||||||
project_id=project_id, profile_id=profile_id, event="session_start", limit=limit
|
project_id=project_id, profile_id=profile_id, event="session_start", limit=limit
|
||||||
)
|
)
|
||||||
|
data = result.get("data", []) if isinstance(result, dict) else []
|
||||||
sessions = result.get("data", []) if isinstance(result, dict) else []
|
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"profile_id": profile_id,
|
"profile_id": profile_id,
|
||||||
"sessions_count": len(sessions),
|
"sessions_count": len(data),
|
||||||
"sessions": sessions,
|
"sessions": data,
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_profile(
|
|
||||||
client: OpenPanelClient, project_id: str, profile_id: str, confirm: bool = False
|
|
||||||
) -> str:
|
|
||||||
"""Delete a user profile (GDPR)"""
|
|
||||||
try:
|
|
||||||
if not confirm:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"error": "Deletion not confirmed. Set confirm=true to proceed.",
|
|
||||||
"warning": "This will permanently delete all user data.",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"profile_id": profile_id,
|
|
||||||
"confirmed": confirm,
|
|
||||||
"note": "Profile deletion requires dashboard tRPC API. Use OpenPanel dashboard for GDPR deletion.",
|
|
||||||
"message": "Profile deletion request processed (GDPR)",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def merge_profiles(
|
|
||||||
client: OpenPanelClient, project_id: str, primary_profile_id: str, secondary_profile_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Merge two profiles"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"primary_profile_id": primary_profile_id,
|
|
||||||
"secondary_profile_id": secondary_profile_id,
|
|
||||||
"note": "Profile merging requires dashboard tRPC API. Use OpenPanel dashboard for profile management.",
|
|
||||||
"message": "Profile merge request processed",
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -398,30 +142,23 @@ async def merge_profiles(
|
|||||||
async def export_profile_data(
|
async def export_profile_data(
|
||||||
client: OpenPanelClient, project_id: str, profile_id: str, format: str = "json"
|
client: OpenPanelClient, project_id: str, profile_id: str, format: str = "json"
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Export all profile data (GDPR)"""
|
"""Export all profile data (GDPR compliance)."""
|
||||||
try:
|
try:
|
||||||
# Export events for this profile
|
|
||||||
result = await client.export_events(
|
result = await client.export_events(
|
||||||
project_id=project_id, profile_id=profile_id, limit=1000, includes=["profile", "meta"]
|
project_id=project_id,
|
||||||
|
profile_id=profile_id,
|
||||||
|
limit=1000,
|
||||||
|
includes=["profile", "meta", "properties"],
|
||||||
)
|
)
|
||||||
|
|
||||||
events = result.get("data", []) if isinstance(result, dict) else []
|
events = result.get("data", []) if isinstance(result, dict) else []
|
||||||
|
|
||||||
export_data = {
|
|
||||||
"profile_id": profile_id,
|
|
||||||
"project_id": project_id,
|
|
||||||
"events_count": len(events),
|
|
||||||
"events": events,
|
|
||||||
}
|
|
||||||
|
|
||||||
if format == "csv":
|
if format == "csv":
|
||||||
# Build CSV
|
|
||||||
csv_lines = ["timestamp,event_name,properties"]
|
csv_lines = ["timestamp,event_name,properties"]
|
||||||
for event in events:
|
for ev in events:
|
||||||
csv_lines.append(
|
csv_lines.append(
|
||||||
f"{event.get('timestamp', '')},{event.get('name', '')},{json.dumps(event.get('properties', {}))}"
|
f"{ev.get('createdAt', ev.get('timestamp', ''))},{ev.get('name', '')},"
|
||||||
|
f"{json.dumps(ev.get('properties', {}))}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -429,7 +166,6 @@ async def export_profile_data(
|
|||||||
"format": "csv",
|
"format": "csv",
|
||||||
"events_count": len(events),
|
"events_count": len(events),
|
||||||
"csv": "\n".join(csv_lines),
|
"csv": "\n".join(csv_lines),
|
||||||
"message": "Profile data exported (GDPR compliance)",
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
@@ -441,8 +177,7 @@ async def export_profile_data(
|
|||||||
"profile_id": profile_id,
|
"profile_id": profile_id,
|
||||||
"format": "json",
|
"format": "json",
|
||||||
"events_count": len(events),
|
"events_count": len(events),
|
||||||
"data": export_data,
|
"data": {"profile_id": profile_id, "project_id": project_id, "events": events},
|
||||||
"message": "Profile data exported (GDPR compliance)",
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"""Projects Handler - OpenPanel project management (8 tools)"""
|
"""Projects Handler - OpenPanel project management (5 tools).
|
||||||
|
|
||||||
|
Uses Manage API (GET/POST/PATCH/DELETE /manage/projects).
|
||||||
|
Requires 'root' mode client for write operations.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -7,22 +11,24 @@ from plugins.openpanel.client import OpenPanelClient
|
|||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""Return tool specifications for ToolGenerator (8 tools)"""
|
"""Return tool specifications for ToolGenerator (5 tools)."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": "list_projects",
|
"name": "list_projects",
|
||||||
"method_name": "list_projects",
|
"method_name": "list_projects",
|
||||||
"description": "List all OpenPanel projects.",
|
"description": "List all projects via Manage API. Requires 'root' mode client.",
|
||||||
"schema": {"type": "object", "properties": {}},
|
"schema": {"type": "object", "properties": {}},
|
||||||
"scope": "read",
|
"scope": "admin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "get_project",
|
"name": "get_project",
|
||||||
"method_name": "get_project",
|
"method_name": "get_project",
|
||||||
"description": "Get project details including settings and statistics.",
|
"description": "Get project details via Manage API. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
|
"properties": {
|
||||||
|
"project_id": {"type": "string", "description": "Project ID"},
|
||||||
|
},
|
||||||
"required": ["project_id"],
|
"required": ["project_id"],
|
||||||
},
|
},
|
||||||
"scope": "read",
|
"scope": "read",
|
||||||
@@ -30,19 +36,14 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "create_project",
|
"name": "create_project",
|
||||||
"method_name": "create_project",
|
"method_name": "create_project",
|
||||||
"description": "Create a new OpenPanel project.",
|
"description": "Create a new project via Manage API. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string", "description": "Project name"},
|
"name": {"type": "string", "description": "Project name"},
|
||||||
"domain": {
|
"domain": {
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
"description": "Primary domain for the project",
|
"description": "Project domain (e.g., 'example.com')",
|
||||||
},
|
|
||||||
"timezone": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Project timezone",
|
|
||||||
"default": "UTC",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
@@ -52,22 +53,18 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "update_project",
|
"name": "update_project",
|
||||||
"method_name": "update_project",
|
"method_name": "update_project",
|
||||||
"description": "Update project settings.",
|
"description": "Update a project via Manage API. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
"project_id": {"type": "string", "description": "Project ID to update"},
|
||||||
"name": {
|
"name": {
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
"description": "New project name",
|
"description": "New project name",
|
||||||
},
|
},
|
||||||
"domain": {
|
"domain": {
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||||
"description": "New primary domain",
|
"description": "New domain",
|
||||||
},
|
|
||||||
"timezone": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "New timezone",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["project_id"],
|
"required": ["project_id"],
|
||||||
@@ -77,85 +74,35 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "delete_project",
|
"name": "delete_project",
|
||||||
"method_name": "delete_project",
|
"method_name": "delete_project",
|
||||||
"description": "Delete a project and all its data.",
|
"description": "Delete a project via Manage API. WARNING: Permanently deletes all project data. Requires 'root' mode client.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"project_id": {"type": "string", "description": "Project ID to delete"},
|
"project_id": {"type": "string", "description": "Project ID to delete"},
|
||||||
"confirm": {"type": "boolean", "description": "Confirm deletion (required)"},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "confirm"],
|
|
||||||
},
|
|
||||||
"scope": "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_project_stats",
|
|
||||||
"method_name": "get_project_stats",
|
|
||||||
"description": "Get project statistics (events, users, storage).",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, yearToDate, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"required": ["project_id"],
|
"required": ["project_id"],
|
||||||
},
|
},
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_project_settings",
|
|
||||||
"method_name": "get_project_settings",
|
|
||||||
"description": "Get project configuration settings.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
|
|
||||||
"required": ["project_id"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "update_project_settings",
|
|
||||||
"method_name": "update_project_settings",
|
|
||||||
"description": "Update project configuration.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {"type": "string", "description": "Project ID"},
|
|
||||||
"settings": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Settings to update",
|
|
||||||
"properties": {
|
|
||||||
"cors_domains": {"type": "array", "items": {"type": "string"}},
|
|
||||||
"ip_anonymization": {"type": "boolean"},
|
|
||||||
"data_retention_days": {"type": "integer"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["project_id", "settings"],
|
|
||||||
},
|
|
||||||
"scope": "admin",
|
"scope": "admin",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# Project Functions (8)
|
# Project Functions (5)
|
||||||
# =====================
|
# =====================
|
||||||
|
|
||||||
|
|
||||||
async def list_projects(client: OpenPanelClient) -> str:
|
async def list_projects(client: OpenPanelClient) -> str:
|
||||||
"""List all projects"""
|
"""List all projects via GET /manage/projects."""
|
||||||
try:
|
try:
|
||||||
|
result = await client.list_projects()
|
||||||
|
projects = (
|
||||||
|
result
|
||||||
|
if isinstance(result, list)
|
||||||
|
else result.get("data", []) if isinstance(result, dict) else []
|
||||||
|
)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "count": len(projects), "projects": projects},
|
||||||
"success": True,
|
|
||||||
"note": "Project listing requires dashboard tRPC API. Use OpenPanel dashboard to view projects.",
|
|
||||||
"message": "Project list request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -164,36 +111,23 @@ async def list_projects(client: OpenPanelClient) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def get_project(client: OpenPanelClient, project_id: str) -> str:
|
async def get_project(client: OpenPanelClient, project_id: str) -> str:
|
||||||
"""Get project details"""
|
"""Get project details via GET /manage/projects/:id."""
|
||||||
try:
|
try:
|
||||||
return json.dumps(
|
result = await client.get_project(project_id)
|
||||||
{
|
return json.dumps({"success": True, "project": result}, indent=2, ensure_ascii=False)
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"note": "Project details require dashboard tRPC API. Use OpenPanel dashboard for full project view.",
|
|
||||||
"message": "Project details request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
async def create_project(
|
async def create_project(client: OpenPanelClient, name: str, domain: str | None = None) -> str:
|
||||||
client: OpenPanelClient, name: str, domain: str | None = None, timezone: str = "UTC"
|
"""Create a new project via POST /manage/projects."""
|
||||||
) -> str:
|
|
||||||
"""Create a new project"""
|
|
||||||
try:
|
try:
|
||||||
project_config = {"name": name, "domain": domain, "timezone": timezone}
|
data: dict[str, Any] = {"name": name}
|
||||||
|
if domain:
|
||||||
|
data["domain"] = domain
|
||||||
|
result = await client.create_project(data)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "message": f"Project '{name}' created", "project": result},
|
||||||
"success": True,
|
|
||||||
"project": project_config,
|
|
||||||
"note": "Project creation requires dashboard tRPC API. Use OpenPanel dashboard to create projects.",
|
|
||||||
"message": f"Project '{name}' configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -206,26 +140,23 @@ async def update_project(
|
|||||||
project_id: str,
|
project_id: str,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
domain: str | None = None,
|
domain: str | None = None,
|
||||||
timezone: str | None = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Update project settings"""
|
"""Update a project via PATCH /manage/projects/:id."""
|
||||||
try:
|
try:
|
||||||
updates = {}
|
data: dict[str, Any] = {}
|
||||||
if name:
|
if name:
|
||||||
updates["name"] = name
|
data["name"] = name
|
||||||
if domain:
|
if domain:
|
||||||
updates["domain"] = domain
|
data["domain"] = domain
|
||||||
if timezone:
|
if not data:
|
||||||
updates["timezone"] = timezone
|
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": False, "error": "No fields to update. Provide name or domain."},
|
||||||
"success": True,
|
indent=2,
|
||||||
"project_id": project_id,
|
ensure_ascii=False,
|
||||||
"updates": updates,
|
)
|
||||||
"note": "Project updates require dashboard tRPC API. Use OpenPanel dashboard to modify projects.",
|
result = await client.update_project(project_id, data)
|
||||||
"message": "Project update configuration created",
|
return json.dumps(
|
||||||
},
|
{"success": True, "message": f"Project '{project_id}' updated", "project": result},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -233,110 +164,12 @@ async def update_project(
|
|||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
async def delete_project(client: OpenPanelClient, project_id: str, confirm: bool = False) -> str:
|
async def delete_project(client: OpenPanelClient, project_id: str) -> str:
|
||||||
"""Delete a project"""
|
"""Delete a project via DELETE /manage/projects/:id."""
|
||||||
try:
|
try:
|
||||||
if not confirm:
|
result = await client.delete_project(project_id)
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "message": f"Project '{project_id}' deleted", "result": result},
|
||||||
"success": False,
|
|
||||||
"error": "Deletion not confirmed. Set confirm=true to proceed.",
|
|
||||||
"warning": "This will permanently delete all project data including events, users, and settings.",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"confirmed": confirm,
|
|
||||||
"note": "Project deletion requires dashboard tRPC API. Use OpenPanel dashboard to delete projects.",
|
|
||||||
"message": "Project deletion request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_project_stats(
|
|
||||||
client: OpenPanelClient, project_id: str, date_range: str = "30d"
|
|
||||||
) -> str:
|
|
||||||
"""Get project statistics"""
|
|
||||||
try:
|
|
||||||
# Get basic stats via export API
|
|
||||||
metrics = {}
|
|
||||||
|
|
||||||
# Total events
|
|
||||||
events_config = [{"name": "*", "segment": "event"}]
|
|
||||||
events_result = await client.export_charts(
|
|
||||||
project_id=project_id, events=events_config, interval="day", date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
total_events = 0
|
|
||||||
if isinstance(events_result, dict) and "data" in events_result:
|
|
||||||
for point in events_result.get("data", []):
|
|
||||||
total_events += point.get("count", 0)
|
|
||||||
metrics["total_events"] = total_events
|
|
||||||
|
|
||||||
# Unique users
|
|
||||||
users_config = [{"name": "*", "segment": "user"}]
|
|
||||||
users_result = await client.export_charts(
|
|
||||||
project_id=project_id, events=users_config, interval="day", date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(users_result, dict) and "data" in users_result:
|
|
||||||
data = users_result.get("data", [])
|
|
||||||
metrics["unique_users"] = data[-1].get("count", 0) if data else 0
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"stats": metrics,
|
|
||||||
"message": "Project stats retrieved",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_project_settings(client: OpenPanelClient, project_id: str) -> str:
|
|
||||||
"""Get project settings"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"note": "Project settings require dashboard tRPC API. Use OpenPanel dashboard for settings.",
|
|
||||||
"message": "Project settings request processed",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_project_settings(
|
|
||||||
client: OpenPanelClient, project_id: str, settings: dict[str, Any]
|
|
||||||
) -> str:
|
|
||||||
"""Update project settings"""
|
|
||||||
try:
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": project_id,
|
|
||||||
"settings": settings,
|
|
||||||
"note": "Settings update requires dashboard tRPC API. Use OpenPanel dashboard to configure.",
|
|
||||||
"message": "Project settings update configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Reports Handler - OpenPanel analytics reports (8 tools)
|
"""Reports Handler - OpenPanel analytics reports (2 tools).
|
||||||
|
|
||||||
Note: project_id is optional if configured in environment variables.
|
Uses Insights API for overview stats and live visitors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -11,12 +11,12 @@ from plugins.openpanel.handlers.utils import get_project_id as _get_project_id
|
|||||||
|
|
||||||
|
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""Return tool specifications for ToolGenerator (8 tools)"""
|
"""Return tool specifications for ToolGenerator (2 tools)."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": "get_overview_report",
|
"name": "get_overview_report",
|
||||||
"method_name": "get_overview_report",
|
"method_name": "get_overview_report",
|
||||||
"description": "Get overview statistics including total events, users, sessions, and page views. Note: project_id is optional if configured in environment.",
|
"description": "Get overview statistics including visitors, page views, sessions, bounce rate, and duration via Insights API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -26,116 +26,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"date_range": {
|
"date_range": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
|
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_retention_report",
|
|
||||||
"method_name": "get_retention_report",
|
|
||||||
"description": "Get user retention analysis showing how many users return over time. Note: project_id is optional if configured in environment.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Project ID (optional if configured in env)",
|
|
||||||
},
|
|
||||||
"start_event": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Event that marks user acquisition (e.g., 'signup_completed')",
|
|
||||||
"default": "session_start",
|
|
||||||
},
|
|
||||||
"return_event": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Event that marks user return (e.g., 'app_opened')",
|
|
||||||
"default": "session_start",
|
|
||||||
},
|
|
||||||
"period": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["day", "week", "month"],
|
|
||||||
"description": "Retention period granularity",
|
|
||||||
"default": "week",
|
|
||||||
},
|
|
||||||
"cohorts": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Number of cohorts to analyze",
|
|
||||||
"default": 8,
|
|
||||||
"maximum": 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_cohort_report",
|
|
||||||
"method_name": "get_cohort_report",
|
|
||||||
"description": "Get cohort analysis grouping users by acquisition date and tracking their behavior. Note: project_id is optional if configured in environment.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Project ID (optional if configured in env)",
|
|
||||||
},
|
|
||||||
"cohort_type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["first_seen", "signup", "custom"],
|
|
||||||
"description": "How to define cohorts",
|
|
||||||
"default": "first_seen",
|
|
||||||
},
|
|
||||||
"cohort_event": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Custom event for cohort definition (when cohort_type is 'custom')",
|
|
||||||
},
|
|
||||||
"measure_event": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Event to measure for each cohort",
|
|
||||||
"default": "session_start",
|
|
||||||
},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: 7d, 14d, 30d, 60d, 90d, 6m, 12m, yearToDate, all",
|
|
||||||
"default": "6m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_paths_report",
|
|
||||||
"method_name": "get_paths_report",
|
|
||||||
"description": "Get user flow/paths analysis showing common navigation patterns. Note: project_id is optional if configured in environment.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Project ID (optional if configured in env)",
|
|
||||||
},
|
|
||||||
"start_event": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Starting event for path analysis",
|
|
||||||
},
|
|
||||||
"end_event": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Ending event for path analysis",
|
|
||||||
},
|
|
||||||
"max_steps": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum path steps to analyze",
|
|
||||||
"default": 5,
|
|
||||||
"maximum": 10,
|
|
||||||
},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
"default": "30d",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -146,7 +37,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": "get_realtime_stats",
|
"name": "get_realtime_stats",
|
||||||
"method_name": "get_realtime_stats",
|
"method_name": "get_realtime_stats",
|
||||||
"description": "Get real-time visitor statistics for the last 30 minutes. Note: project_id is optional if configured in environment.",
|
"description": "Get real-time active visitor count via Insights live API.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -159,250 +50,23 @@ def get_tool_specifications() -> list[dict[str, Any]]:
|
|||||||
},
|
},
|
||||||
"scope": "read",
|
"scope": "read",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "get_ab_test_results",
|
|
||||||
"method_name": "get_ab_test_results",
|
|
||||||
"description": "Get A/B test results comparing variants by conversion rate. Note: project_id is optional if configured in environment.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Project ID (optional if configured in env)",
|
|
||||||
},
|
|
||||||
"test_name": {"type": "string", "description": "Name of the A/B test"},
|
|
||||||
"conversion_event": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Event that marks conversion",
|
|
||||||
},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["test_name", "conversion_event"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "create_scheduled_report",
|
|
||||||
"method_name": "create_scheduled_report",
|
|
||||||
"description": "Create a scheduled report to be sent via email. Note: project_id is optional if configured in environment.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Project ID (optional if configured in env)",
|
|
||||||
},
|
|
||||||
"name": {"type": "string", "description": "Report name"},
|
|
||||||
"report_type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["overview", "events", "funnels", "retention"],
|
|
||||||
"description": "Type of report",
|
|
||||||
},
|
|
||||||
"schedule": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["daily", "weekly", "monthly"],
|
|
||||||
"description": "Report schedule",
|
|
||||||
},
|
|
||||||
"recipients": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "string", "format": "email"},
|
|
||||||
"description": "Email recipients",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["name", "report_type", "schedule", "recipients"],
|
|
||||||
},
|
|
||||||
"scope": "write",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "export_report_pdf",
|
|
||||||
"method_name": "export_report_pdf",
|
|
||||||
"description": "Export a report as PDF format. Note: project_id is optional if configured in environment.",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"project_id": {
|
|
||||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
||||||
"description": "Project ID (optional if configured in env)",
|
|
||||||
},
|
|
||||||
"report_type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["overview", "events", "funnels", "retention", "paths"],
|
|
||||||
"description": "Type of report to export",
|
|
||||||
},
|
|
||||||
"date_range": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, all",
|
|
||||||
"default": "30d",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["report_type"],
|
|
||||||
},
|
|
||||||
"scope": "read",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# Report Functions (8)
|
# Report Functions (2)
|
||||||
# =====================
|
# =====================
|
||||||
|
|
||||||
|
|
||||||
async def get_overview_report(
|
async def get_overview_report(
|
||||||
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
|
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get overview statistics using tRPC overview.stats endpoint"""
|
"""Get overview statistics via Insights metrics API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
result = await client.get_overview_stats(project_id=pid, date_range=date_range)
|
||||||
# Use the new tRPC overview.stats endpoint
|
|
||||||
result = await client.get_overview_stats(
|
|
||||||
project_id=effective_project_id, date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if "error" in result:
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "project_id": pid, "date_range": date_range, "stats": result},
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC overview.stats endpoint may require authentication or the project may not exist",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Transform tRPC response to our format
|
|
||||||
# tRPC overview.stats returns: {current: {...}, previous: {...}}
|
|
||||||
current = result.get("current", result)
|
|
||||||
|
|
||||||
metrics = {
|
|
||||||
"unique_visitors": current.get("uniqueVisitors", 0),
|
|
||||||
"page_views": current.get("screenViews", 0),
|
|
||||||
"sessions": current.get("sessions", 0),
|
|
||||||
"bounce_rate": current.get("bounceRate", 0),
|
|
||||||
"session_duration": current.get("sessionDuration", 0),
|
|
||||||
"revenue": current.get("revenue", 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add previous period comparison if available
|
|
||||||
previous = result.get("previous", {})
|
|
||||||
if previous:
|
|
||||||
metrics["previous_period"] = {
|
|
||||||
"unique_visitors": previous.get("uniqueVisitors", 0),
|
|
||||||
"page_views": previous.get("screenViews", 0),
|
|
||||||
"sessions": previous.get("sessions", 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"date_range": date_range,
|
|
||||||
"overview": metrics,
|
|
||||||
"raw": result,
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_retention_report(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str | None = None,
|
|
||||||
start_event: str = "session_start",
|
|
||||||
return_event: str = "session_start",
|
|
||||||
period: str = "week",
|
|
||||||
cohorts: int = 8,
|
|
||||||
) -> str:
|
|
||||||
"""Get user retention analysis"""
|
|
||||||
try:
|
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
|
||||||
# Note: Full retention analysis requires tRPC API access
|
|
||||||
# This provides a simplified version using export API
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"retention_config": {
|
|
||||||
"start_event": start_event,
|
|
||||||
"return_event": return_event,
|
|
||||||
"period": period,
|
|
||||||
"cohorts": cohorts,
|
|
||||||
},
|
|
||||||
"note": "Full retention analysis requires dashboard tRPC API. Use OpenPanel dashboard for detailed retention charts.",
|
|
||||||
"message": "Retention report configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_cohort_report(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str | None = None,
|
|
||||||
cohort_type: str = "first_seen",
|
|
||||||
cohort_event: str | None = None,
|
|
||||||
measure_event: str = "session_start",
|
|
||||||
date_range: str = "6m",
|
|
||||||
) -> str:
|
|
||||||
"""Get cohort analysis"""
|
|
||||||
try:
|
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"cohort_config": {
|
|
||||||
"cohort_type": cohort_type,
|
|
||||||
"cohort_event": cohort_event,
|
|
||||||
"measure_event": measure_event,
|
|
||||||
"date_range": date_range,
|
|
||||||
},
|
|
||||||
"note": "Full cohort analysis requires dashboard tRPC API. Use OpenPanel dashboard for detailed cohort charts.",
|
|
||||||
"message": "Cohort report configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_paths_report(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
project_id: str | None = None,
|
|
||||||
start_event: str | None = None,
|
|
||||||
end_event: str | None = None,
|
|
||||||
max_steps: int = 5,
|
|
||||||
date_range: str = "30d",
|
|
||||||
) -> str:
|
|
||||||
"""Get user flow/paths analysis"""
|
|
||||||
try:
|
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"paths_config": {
|
|
||||||
"start_event": start_event,
|
|
||||||
"end_event": end_event,
|
|
||||||
"max_steps": max_steps,
|
|
||||||
"date_range": date_range,
|
|
||||||
},
|
|
||||||
"note": "Full path analysis requires dashboard tRPC API. Use OpenPanel dashboard for user flow visualization.",
|
|
||||||
"message": "Paths report configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
@@ -411,125 +75,12 @@ async def get_paths_report(
|
|||||||
|
|
||||||
|
|
||||||
async def get_realtime_stats(client: OpenPanelClient, project_id: str | None = None) -> str:
|
async def get_realtime_stats(client: OpenPanelClient, project_id: str | None = None) -> str:
|
||||||
"""Get real-time visitor statistics using chart.chart with 30min range"""
|
"""Get real-time active visitor count via Insights live API."""
|
||||||
try:
|
try:
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
pid = _get_project_id(client, project_id)
|
||||||
|
result = await client.get_live_visitors(project_id=pid)
|
||||||
# Use chart.chart with 30min range for realtime data
|
|
||||||
result = await client.get_live_visitors(project_id=effective_project_id)
|
|
||||||
|
|
||||||
active_users = result.get("count", 0) if isinstance(result, dict) else 0
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if isinstance(result, dict) and "error" in result:
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{"success": True, "project_id": pid, "realtime": result},
|
||||||
"success": False,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"error": result.get("error"),
|
|
||||||
"note": "tRPC chart.chart endpoint may require authentication",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"realtime": {
|
|
||||||
"active_visitors_30min": active_users,
|
|
||||||
"timestamp": "now",
|
|
||||||
"period": "30min",
|
|
||||||
},
|
|
||||||
"message": "Real-time stats retrieved via chart.chart with 30min range",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_ab_test_results(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
test_name: str,
|
|
||||||
conversion_event: str,
|
|
||||||
project_id: str | None = None,
|
|
||||||
date_range: str = "30d",
|
|
||||||
) -> str:
|
|
||||||
"""Get A/B test results"""
|
|
||||||
try:
|
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
|
||||||
# A/B test results typically tracked via properties
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"test_name": test_name,
|
|
||||||
"ab_test_config": {"conversion_event": conversion_event, "date_range": date_range},
|
|
||||||
"note": "A/B test analysis requires tracking variants via event properties. Use OpenPanel dashboard for detailed variant comparison.",
|
|
||||||
"message": "A/B test configuration retrieved",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_scheduled_report(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
name: str,
|
|
||||||
report_type: str,
|
|
||||||
schedule: str,
|
|
||||||
recipients: list[str],
|
|
||||||
project_id: str | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Create a scheduled report"""
|
|
||||||
try:
|
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"scheduled_report": {
|
|
||||||
"name": name,
|
|
||||||
"report_type": report_type,
|
|
||||||
"schedule": schedule,
|
|
||||||
"recipients": recipients,
|
|
||||||
},
|
|
||||||
"note": "Scheduled reports require dashboard tRPC API. Configure via OpenPanel dashboard.",
|
|
||||||
"message": f"Scheduled report '{name}' configuration created",
|
|
||||||
},
|
|
||||||
indent=2,
|
|
||||||
ensure_ascii=False,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def export_report_pdf(
|
|
||||||
client: OpenPanelClient,
|
|
||||||
report_type: str,
|
|
||||||
project_id: str | None = None,
|
|
||||||
date_range: str = "30d",
|
|
||||||
) -> str:
|
|
||||||
"""Export report as PDF"""
|
|
||||||
try:
|
|
||||||
effective_project_id = _get_project_id(client, project_id)
|
|
||||||
return json.dumps(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"project_id": effective_project_id,
|
|
||||||
"export_config": {
|
|
||||||
"report_type": report_type,
|
|
||||||
"format": "pdf",
|
|
||||||
"date_range": date_range,
|
|
||||||
},
|
|
||||||
"note": "PDF export requires dashboard functionality. Use OpenPanel dashboard to export reports.",
|
|
||||||
"message": f"PDF export configuration for {report_type} report",
|
|
||||||
},
|
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -132,45 +132,15 @@ async def get_instance_info(client: OpenPanelClient) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def get_usage_stats(client: OpenPanelClient, project_id: str, date_range: str = "30d") -> str:
|
async def get_usage_stats(client: OpenPanelClient, project_id: str, date_range: str = "30d") -> str:
|
||||||
"""Get usage statistics for a project"""
|
"""Get usage statistics for a project via Insights metrics API."""
|
||||||
try:
|
try:
|
||||||
# Get event count
|
result = await client.get_overview_stats(project_id=project_id, date_range=date_range)
|
||||||
events_config = [{"name": "*", "segment": "event"}]
|
|
||||||
events_result = await client.export_charts(
|
|
||||||
project_id=project_id, events=events_config, interval="day", date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get unique users
|
|
||||||
users_config = [{"name": "*", "segment": "user"}]
|
|
||||||
users_result = await client.export_charts(
|
|
||||||
project_id=project_id, events=users_config, interval="day", date_range=date_range
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate totals
|
|
||||||
total_events = 0
|
|
||||||
if isinstance(events_result, dict) and "data" in events_result:
|
|
||||||
for point in events_result.get("data", []):
|
|
||||||
total_events += point.get("count", 0)
|
|
||||||
|
|
||||||
total_users = 0
|
|
||||||
if isinstance(users_result, dict) and "data" in users_result:
|
|
||||||
data = users_result.get("data", [])
|
|
||||||
if data:
|
|
||||||
total_users = data[-1].get("count", 0)
|
|
||||||
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"date_range": date_range,
|
"date_range": date_range,
|
||||||
"stats": {
|
"stats": result,
|
||||||
"total_events": total_events,
|
|
||||||
"unique_users": total_users,
|
|
||||||
"events_per_user": (
|
|
||||||
round(total_events / total_users, 2) if total_users > 0 else 0
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"message": f"Usage stats for {date_range}",
|
|
||||||
},
|
},
|
||||||
indent=2,
|
indent=2,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
OpenPanel Plugin - Product Analytics Management
|
OpenPanel Plugin - Product Analytics Management.
|
||||||
|
|
||||||
Complete OpenPanel Self-Hosted management through REST API.
|
Self-hosted OpenPanel management through public REST APIs.
|
||||||
Provides tools for event tracking, data export, and analytics.
|
Provides event tracking, data export, analytics, and project/client management.
|
||||||
|
|
||||||
For Self-Hosted instances deployed on Coolify.
|
APIs used:
|
||||||
|
- Track API (/track) — Event ingestion (write mode)
|
||||||
|
- Export API (/export) — Raw data export (read mode)
|
||||||
|
- Insights API (/insights) — Analytics queries (read mode)
|
||||||
|
- Manage API (/manage) — Project & client CRUD (root mode)
|
||||||
|
- Health API (/healthcheck) — Instance health
|
||||||
|
|
||||||
|
For Self-Hosted instances deployed on Coolify or Docker.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -16,30 +23,17 @@ from plugins.openpanel.client import OpenPanelClient
|
|||||||
|
|
||||||
class OpenPanelPlugin(BasePlugin):
|
class OpenPanelPlugin(BasePlugin):
|
||||||
"""
|
"""
|
||||||
OpenPanel Analytics Plugin - Comprehensive product analytics.
|
OpenPanel Analytics Plugin — 42 tools across 7 handlers.
|
||||||
|
|
||||||
Provides complete OpenPanel management capabilities including:
|
All tools use public REST APIs (no tRPC/session dependency).
|
||||||
- Event tracking (track, identify, increment, decrement)
|
|
||||||
- Data export (events, charts, CSV)
|
|
||||||
- Analytics reports (page views, referrers, geo, devices)
|
|
||||||
- System operations (health, stats)
|
|
||||||
|
|
||||||
Phase H.1: Core (25 tools)
|
Events (11): track, identify, increment, decrement, group, assign_group, batch, revenue
|
||||||
- Events Handler: 9 tools (alias_user removed - not supported on self-hosted)
|
Export (10): events, charts, CSV, counts, top pages/referrers/geo/devices
|
||||||
- Export Handler: 10 tools
|
Reports (2): overview stats, realtime visitors
|
||||||
- System Handler: 6 tools
|
Profiles (3): profile events, sessions, GDPR export
|
||||||
|
Projects (5): list, get, create, update, delete
|
||||||
Phase H.2: Analytics (24 tools)
|
Clients (5): list, get, create, update, delete
|
||||||
- Reports Handler: 8 tools
|
System (6): health, instance info, usage stats, storage, connection test, rate limits
|
||||||
- Funnels Handler: 8 tools
|
|
||||||
- Profiles Handler: 8 tools
|
|
||||||
|
|
||||||
Phase H.3: Management (24 tools)
|
|
||||||
- Projects Handler: 8 tools
|
|
||||||
- Dashboards Handler: 10 tools
|
|
||||||
- Clients Handler: 6 tools
|
|
||||||
|
|
||||||
Total: 73 tools
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -61,63 +55,42 @@ class OpenPanelPlugin(BasePlugin):
|
|||||||
- url: OpenPanel instance URL
|
- url: OpenPanel instance URL
|
||||||
- client_id: Client ID for authentication
|
- client_id: Client ID for authentication
|
||||||
- client_secret: Client Secret for authentication
|
- client_secret: Client Secret for authentication
|
||||||
- project_id: OpenPanel project ID (for Export/Read APIs)
|
- project_id: OpenPanel project ID (for Export/Insights APIs)
|
||||||
- organization_id: Organization/Workspace ID (for multi-tenant setups)
|
- organization_id: Organization/Workspace ID (optional)
|
||||||
project_id: Optional MCP project ID (auto-generated if not provided)
|
project_id: Optional MCP project ID (auto-generated if not provided)
|
||||||
"""
|
"""
|
||||||
super().__init__(config, project_id=project_id)
|
super().__init__(config, project_id=project_id)
|
||||||
|
|
||||||
# Get OpenPanel project_id and organization_id from config
|
|
||||||
openpanel_project_id = config.get("project_id")
|
openpanel_project_id = config.get("project_id")
|
||||||
openpanel_organization_id = config.get("organization_id")
|
openpanel_organization_id = config.get("organization_id")
|
||||||
|
|
||||||
# Get session cookie for tRPC API access (optional)
|
|
||||||
# This is needed for analytics queries as tRPC uses session-based auth
|
|
||||||
session_cookie = config.get("session_cookie")
|
|
||||||
|
|
||||||
# Create OpenPanel API client
|
|
||||||
self.client = OpenPanelClient(
|
self.client = OpenPanelClient(
|
||||||
base_url=config["url"],
|
base_url=config["url"],
|
||||||
client_id=config["client_id"],
|
client_id=config["client_id"],
|
||||||
client_secret=config["client_secret"],
|
client_secret=config["client_secret"],
|
||||||
project_id=openpanel_project_id,
|
project_id=openpanel_project_id,
|
||||||
organization_id=openpanel_organization_id,
|
organization_id=openpanel_organization_id,
|
||||||
session_cookie=session_cookie,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store for reference
|
|
||||||
self.openpanel_project_id = openpanel_project_id
|
self.openpanel_project_id = openpanel_project_id
|
||||||
self.openpanel_organization_id = openpanel_organization_id
|
self.openpanel_organization_id = openpanel_organization_id
|
||||||
self.has_session = bool(session_cookie)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Return all tool specifications for ToolGenerator.
|
Return all tool specifications for ToolGenerator (42 tools).
|
||||||
|
|
||||||
This method is called by ToolGenerator to create unified tools
|
This method is called by ToolGenerator to create unified tools
|
||||||
with site parameter routing.
|
with site parameter routing.
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of tool specification dictionaries (26 tools in Phase H.1)
|
|
||||||
"""
|
"""
|
||||||
specs = []
|
specs = []
|
||||||
|
specs.extend(handlers.events.get_tool_specifications()) # 11 tools
|
||||||
# Phase H.1: Core (26 tools)
|
|
||||||
specs.extend(handlers.events.get_tool_specifications()) # 10 tools
|
|
||||||
specs.extend(handlers.export.get_tool_specifications()) # 10 tools
|
specs.extend(handlers.export.get_tool_specifications()) # 10 tools
|
||||||
specs.extend(handlers.system.get_tool_specifications()) # 6 tools
|
specs.extend(handlers.system.get_tool_specifications()) # 6 tools
|
||||||
|
specs.extend(handlers.reports.get_tool_specifications()) # 2 tools
|
||||||
# Phase H.2: Analytics (24 tools)
|
specs.extend(handlers.profiles.get_tool_specifications()) # 3 tools
|
||||||
specs.extend(handlers.reports.get_tool_specifications()) # 8 tools
|
specs.extend(handlers.projects.get_tool_specifications()) # 5 tools
|
||||||
specs.extend(handlers.funnels.get_tool_specifications()) # 8 tools
|
specs.extend(handlers.clients.get_tool_specifications()) # 5 tools
|
||||||
specs.extend(handlers.profiles.get_tool_specifications()) # 8 tools
|
|
||||||
|
|
||||||
# Phase H.3: Management (24 tools)
|
|
||||||
specs.extend(handlers.projects.get_tool_specifications()) # 8 tools
|
|
||||||
specs.extend(handlers.dashboards.get_tool_specifications()) # 10 tools
|
|
||||||
specs.extend(handlers.clients.get_tool_specifications()) # 6 tools
|
|
||||||
|
|
||||||
return specs
|
return specs
|
||||||
|
|
||||||
def __getattr__(self, name: str):
|
def __getattr__(self, name: str):
|
||||||
@@ -139,10 +112,8 @@ class OpenPanelPlugin(BasePlugin):
|
|||||||
handlers.export,
|
handlers.export,
|
||||||
handlers.system,
|
handlers.system,
|
||||||
handlers.reports,
|
handlers.reports,
|
||||||
handlers.funnels,
|
|
||||||
handlers.profiles,
|
handlers.profiles,
|
||||||
handlers.projects,
|
handlers.projects,
|
||||||
handlers.dashboards,
|
|
||||||
handlers.clients,
|
handlers.clients,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -483,6 +483,8 @@ async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: st
|
|||||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def search_users(
|
async def search_users(
|
||||||
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
|
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|||||||
@@ -77,12 +77,8 @@ class WPCLIManager:
|
|||||||
try:
|
try:
|
||||||
# First, test if we have Docker socket access
|
# First, test if we have Docker socket access
|
||||||
test_process = await asyncio.create_subprocess_exec(
|
test_process = await asyncio.create_subprocess_exec(
|
||||||
"docker",
|
"docker", "version", "--format", "{{.Server.Version}}",
|
||||||
"version",
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
"--format",
|
|
||||||
"{{.Server.Version}}",
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
test_stdout, test_stderr = await asyncio.wait_for(
|
test_stdout, test_stderr = await asyncio.wait_for(
|
||||||
@@ -103,15 +99,10 @@ class WPCLIManager:
|
|||||||
|
|
||||||
# Now check for our specific container using exact name match
|
# Now check for our specific container using exact name match
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
"docker",
|
"docker", "ps", "--all",
|
||||||
"ps",
|
"--filter", f"name=^{self.container_name}$",
|
||||||
"--all",
|
"--format", "{{.Names}}|{{.Status}}",
|
||||||
"--filter",
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
f"name=^{self.container_name}$",
|
|
||||||
"--format",
|
|
||||||
"{{.Names}}|{{.Status}}",
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
||||||
@@ -126,13 +117,8 @@ class WPCLIManager:
|
|||||||
if not output:
|
if not output:
|
||||||
# Container not found - get list of available containers for helpful error
|
# Container not found - get list of available containers for helpful error
|
||||||
list_process = await asyncio.create_subprocess_exec(
|
list_process = await asyncio.create_subprocess_exec(
|
||||||
"docker",
|
"docker", "ps", "--all", "--format", "{{.Names}}",
|
||||||
"ps",
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
"--all",
|
|
||||||
"--format",
|
|
||||||
"{{.Names}}",
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
list_stdout, _ = await list_process.communicate()
|
list_stdout, _ = await list_process.communicate()
|
||||||
available = list_stdout.decode().strip().split("\n") if list_stdout else []
|
available = list_stdout.decode().strip().split("\n") if list_stdout else []
|
||||||
@@ -184,14 +170,9 @@ class WPCLIManager:
|
|||||||
try:
|
try:
|
||||||
# Try to run wp --version
|
# Try to run wp --version
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
"docker",
|
"docker", "exec", self.container_name,
|
||||||
"exec",
|
"wp", "--version", "--allow-root",
|
||||||
self.container_name,
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
"wp",
|
|
||||||
"--version",
|
|
||||||
"--allow-root",
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
||||||
@@ -310,9 +291,7 @@ class WPCLIManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 4. Build docker exec command (split command into args to prevent shell injection)
|
# 4. Build docker exec command (split command into args to prevent shell injection)
|
||||||
cmd_parts = (
|
cmd_parts = ["docker", "exec", self.container_name, "wp"] + command.split() + ["--allow-root"]
|
||||||
["docker", "exec", self.container_name, "wp"] + command.split() + ["--allow-root"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.info(f"Executing: {' '.join(cmd_parts)}")
|
self.logger.info(f"Executing: {' '.join(cmd_parts)}")
|
||||||
|
|
||||||
@@ -603,29 +582,17 @@ class WPCLIManager:
|
|||||||
try:
|
try:
|
||||||
# Try GNU stat first, fall back to BSD stat
|
# Try GNU stat first, fall back to BSD stat
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
"docker",
|
"docker", "exec", self.container_name,
|
||||||
"exec",
|
"stat", "-c", "%s", export_path,
|
||||||
self.container_name,
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
"stat",
|
|
||||||
"-c",
|
|
||||||
"%s",
|
|
||||||
export_path,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
# BSD stat fallback
|
# BSD stat fallback
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
"docker",
|
"docker", "exec", self.container_name,
|
||||||
"exec",
|
"stat", "-f", "%z", export_path,
|
||||||
self.container_name,
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
"stat",
|
|
||||||
"-f",
|
|
||||||
"%z",
|
|
||||||
export_path,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
stdout, _ = await asyncio.wait_for(process.communicate(), timeout=5.0)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcphub-server"
|
name = "mcphub-server"
|
||||||
version = "3.3.0"
|
version = "3.4.0"
|
||||||
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
description = "AI-native management hub for WordPress, WooCommerce, and self-hosted services via Model Context Protocol (MCP)"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
{name = "MCP Hub", email = "contact@mcphub.dev"}
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ from core.dashboard.routes import (
|
|||||||
api_get_config,
|
api_get_config,
|
||||||
api_list_keys,
|
api_list_keys,
|
||||||
api_list_sites,
|
api_list_sites,
|
||||||
# K.5: Settings routes
|
|
||||||
api_save_setting,
|
|
||||||
api_test_site,
|
api_test_site,
|
||||||
api_update_site,
|
api_update_site,
|
||||||
# E.2: OAuth Social Login routes
|
# E.2: OAuth Social Login routes
|
||||||
@@ -109,6 +107,8 @@ from core.dashboard.routes import (
|
|||||||
# F.3: Service pages
|
# F.3: Service pages
|
||||||
dashboard_service_page,
|
dashboard_service_page,
|
||||||
dashboard_services_list,
|
dashboard_services_list,
|
||||||
|
# K.5: Settings routes
|
||||||
|
api_save_setting,
|
||||||
dashboard_settings_page,
|
dashboard_settings_page,
|
||||||
# E.3: Site Management pages
|
# E.3: Site Management pages
|
||||||
dashboard_sites_add,
|
dashboard_sites_add,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Tests for Admin System Unification (F.4)."""
|
"""Tests for Admin System Unification (F.4)."""
|
||||||
|
|
||||||
|
|
||||||
from core.dashboard.auth import (
|
from core.dashboard.auth import (
|
||||||
DashboardSession,
|
DashboardSession,
|
||||||
get_session_display_info,
|
get_session_display_info,
|
||||||
|
|||||||
767
tests/test_openpanel_plugin.py
Normal file
767
tests/test_openpanel_plugin.py
Normal file
@@ -0,0 +1,767 @@
|
|||||||
|
"""Tests for OpenPanel Plugin (plugins/openpanel/).
|
||||||
|
|
||||||
|
Unit tests covering client initialization, tool specifications,
|
||||||
|
handler delegation, API request building, and health checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plugins.openpanel.client import OpenPanelClient
|
||||||
|
from plugins.openpanel.plugin import OpenPanelPlugin
|
||||||
|
|
||||||
|
# --- OpenPanelClient Tests ---
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientInit:
|
||||||
|
"""Test OpenPanelClient initialization."""
|
||||||
|
|
||||||
|
def test_valid_initialization(self):
|
||||||
|
"""Should initialize with valid credentials."""
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://analytics.example.com",
|
||||||
|
client_id="test-client-id",
|
||||||
|
client_secret="test-client-secret",
|
||||||
|
)
|
||||||
|
assert client.base_url == "https://analytics.example.com"
|
||||||
|
assert client.client_id == "test-client-id"
|
||||||
|
assert client.client_secret == "test-client-secret"
|
||||||
|
assert client.default_project_id is None
|
||||||
|
|
||||||
|
def test_trailing_slash_stripped(self):
|
||||||
|
"""Should strip trailing slash from base URL."""
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://analytics.example.com/",
|
||||||
|
client_id="cid",
|
||||||
|
client_secret="csec",
|
||||||
|
)
|
||||||
|
assert client.base_url == "https://analytics.example.com"
|
||||||
|
|
||||||
|
def test_project_id_stored(self):
|
||||||
|
"""Should store optional project_id."""
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://analytics.example.com",
|
||||||
|
client_id="cid",
|
||||||
|
client_secret="csec",
|
||||||
|
project_id="proj123",
|
||||||
|
)
|
||||||
|
assert client.default_project_id == "proj123"
|
||||||
|
|
||||||
|
def test_organization_id_stored(self):
|
||||||
|
"""Should store optional organization_id."""
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://analytics.example.com",
|
||||||
|
client_id="cid",
|
||||||
|
client_secret="csec",
|
||||||
|
organization_id="org456",
|
||||||
|
)
|
||||||
|
assert client.default_organization_id == "org456"
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientHeaders:
|
||||||
|
"""Test request header generation."""
|
||||||
|
|
||||||
|
def test_auth_headers(self):
|
||||||
|
"""Should include client_id and client_secret in headers."""
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://a.com", client_id="my-id", client_secret="my-secret"
|
||||||
|
)
|
||||||
|
headers = client._get_headers()
|
||||||
|
assert headers["openpanel-client-id"] == "my-id"
|
||||||
|
assert headers["openpanel-client-secret"] == "my-secret"
|
||||||
|
assert headers["Content-Type"] == "application/json"
|
||||||
|
|
||||||
|
def test_client_ip_header(self):
|
||||||
|
"""Should include x-client-ip when provided."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
headers = client._get_headers(client_ip="1.2.3.4")
|
||||||
|
assert headers["x-client-ip"] == "1.2.3.4"
|
||||||
|
|
||||||
|
def test_user_agent_header(self):
|
||||||
|
"""Should include user-agent when provided."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
headers = client._get_headers(user_agent="Mozilla/5.0")
|
||||||
|
assert headers["user-agent"] == "Mozilla/5.0"
|
||||||
|
|
||||||
|
def test_additional_headers(self):
|
||||||
|
"""Should merge additional headers."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
headers = client._get_headers(additional_headers={"X-Custom": "val"})
|
||||||
|
assert headers["X-Custom"] == "val"
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientTrack:
|
||||||
|
"""Test Track API methods."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_track_event_builds_correct_payload(self):
|
||||||
|
"""Should build correct track payload."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.track_event(name="purchase", properties={"amount": 99}, profile_id="user1")
|
||||||
|
|
||||||
|
client.request.assert_called_once()
|
||||||
|
call_args = client.request.call_args
|
||||||
|
json_data = call_args.kwargs.get("json_data") or call_args[1].get("json_data")
|
||||||
|
assert json_data["type"] == "track"
|
||||||
|
assert json_data["payload"]["name"] == "purchase"
|
||||||
|
assert json_data["payload"]["properties"]["amount"] == 99
|
||||||
|
assert json_data["payload"]["profileId"] == "user1"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_identify_user_builds_correct_payload(self):
|
||||||
|
"""Should build correct identify payload."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.identify_user(profile_id="user1", first_name="John", email="john@test.com")
|
||||||
|
|
||||||
|
json_data = client.request.call_args.kwargs.get("json_data")
|
||||||
|
assert json_data["type"] == "identify"
|
||||||
|
assert json_data["payload"]["profileId"] == "user1"
|
||||||
|
assert json_data["payload"]["firstName"] == "John"
|
||||||
|
assert json_data["payload"]["email"] == "john@test.com"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_increment_property(self):
|
||||||
|
"""Should build correct increment payload."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.increment_property(profile_id="user1", property_name="visits", value=5)
|
||||||
|
|
||||||
|
json_data = client.request.call_args.kwargs.get("json_data")
|
||||||
|
assert json_data["type"] == "increment"
|
||||||
|
assert json_data["payload"]["property"] == "visits"
|
||||||
|
assert json_data["payload"]["value"] == 5
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_decrement_property(self):
|
||||||
|
"""Should build correct decrement payload."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.decrement_property(profile_id="user1", property_name="credits", value=3)
|
||||||
|
|
||||||
|
json_data = client.request.call_args.kwargs.get("json_data")
|
||||||
|
assert json_data["type"] == "decrement"
|
||||||
|
assert json_data["payload"]["value"] == 3
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_track_group(self):
|
||||||
|
"""Should build correct group payload."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.track_group(
|
||||||
|
group_id="g1", group_type="company", name="Acme", properties={"plan": "pro"}
|
||||||
|
)
|
||||||
|
|
||||||
|
json_data = client.request.call_args.kwargs.get("json_data")
|
||||||
|
assert json_data["type"] == "group"
|
||||||
|
assert json_data["payload"]["id"] == "g1"
|
||||||
|
assert json_data["payload"]["type"] == "company"
|
||||||
|
assert json_data["payload"]["name"] == "Acme"
|
||||||
|
assert json_data["payload"]["properties"]["plan"] == "pro"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_assign_group(self):
|
||||||
|
"""Should build correct assign_group payload."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.assign_group(group_ids=["g1", "g2"], profile_id="user1")
|
||||||
|
|
||||||
|
json_data = client.request.call_args.kwargs.get("json_data")
|
||||||
|
assert json_data["type"] == "assign_group"
|
||||||
|
assert json_data["payload"]["groupIds"] == ["g1", "g2"]
|
||||||
|
assert json_data["payload"]["profileId"] == "user1"
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientExport:
|
||||||
|
"""Test Export API methods."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_events_params(self):
|
||||||
|
"""Should build correct export_events query params."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"meta": {}, "data": []})
|
||||||
|
|
||||||
|
await client.export_events(
|
||||||
|
project_id="proj1", event="purchase", start="2024-01-01", limit=100
|
||||||
|
)
|
||||||
|
|
||||||
|
call_args = client.request.call_args
|
||||||
|
assert call_args[0][0] == "GET"
|
||||||
|
assert call_args[0][1] == "/export/events"
|
||||||
|
params = call_args.kwargs.get("params")
|
||||||
|
assert params["projectId"] == "proj1"
|
||||||
|
assert params["event"] == "purchase"
|
||||||
|
assert params["start"] == "2024-01-01"
|
||||||
|
assert params["limit"] == 100
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_events_array_event_filter(self):
|
||||||
|
"""Should join array event filters."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"meta": {}, "data": []})
|
||||||
|
|
||||||
|
await client.export_events(project_id="proj1", event=["click", "view"])
|
||||||
|
|
||||||
|
params = client.request.call_args.kwargs.get("params")
|
||||||
|
assert params["event"] == "click,view"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_charts_params(self):
|
||||||
|
"""Should build correct export_charts query params."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={})
|
||||||
|
|
||||||
|
await client.export_charts(
|
||||||
|
project_id="proj1",
|
||||||
|
events=[{"name": "page_view", "segment": "user"}],
|
||||||
|
interval="day",
|
||||||
|
date_range="7d",
|
||||||
|
)
|
||||||
|
|
||||||
|
call_args = client.request.call_args
|
||||||
|
assert call_args[0][0] == "GET"
|
||||||
|
assert call_args[0][1] == "/export/charts"
|
||||||
|
params = call_args.kwargs.get("params")
|
||||||
|
assert params["projectId"] == "proj1"
|
||||||
|
assert params["interval"] == "day"
|
||||||
|
assert params["range"] == "7d"
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientInsights:
|
||||||
|
"""Test Insights API methods."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_overview_stats(self):
|
||||||
|
"""Should call correct insights endpoint."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"visitors": 100})
|
||||||
|
|
||||||
|
result = await client.get_overview_stats(project_id="proj1", date_range="7d")
|
||||||
|
|
||||||
|
call_args = client.request.call_args
|
||||||
|
assert call_args[0][0] == "GET"
|
||||||
|
assert call_args[0][1] == "/insights/proj1/metrics"
|
||||||
|
assert result["visitors"] == 100
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_live_visitors(self):
|
||||||
|
"""Should call live endpoint."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"count": 5})
|
||||||
|
|
||||||
|
result = await client.get_live_visitors(project_id="proj1")
|
||||||
|
|
||||||
|
assert client.request.call_args[0][1] == "/insights/proj1/live"
|
||||||
|
assert result["count"] == 5
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_top_pages(self):
|
||||||
|
"""Should call pages endpoint with params."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
await client.get_top_pages(project_id="proj1", date_range="30d", limit=20)
|
||||||
|
|
||||||
|
assert client.request.call_args[0][1] == "/insights/proj1/pages"
|
||||||
|
params = client.request.call_args.kwargs.get("params")
|
||||||
|
assert params["limit"] == 20
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_insights_breakdown(self):
|
||||||
|
"""Should call breakdown endpoint with correct column."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
await client.get_insights_breakdown(project_id="proj1", column="country")
|
||||||
|
|
||||||
|
assert client.request.call_args[0][1] == "/insights/proj1/country"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_top_sources_uses_referrer_name(self):
|
||||||
|
"""Should use referrer_name breakdown."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.get_insights_breakdown = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
await client.get_top_sources(project_id="proj1")
|
||||||
|
|
||||||
|
client.get_insights_breakdown.assert_called_once()
|
||||||
|
assert client.get_insights_breakdown.call_args[0][1] == "referrer_name"
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientManage:
|
||||||
|
"""Test Manage API methods."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_projects(self):
|
||||||
|
"""Should call GET /manage/projects."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value=[{"id": "p1", "name": "Test"}])
|
||||||
|
|
||||||
|
result = await client.list_projects()
|
||||||
|
|
||||||
|
assert client.request.call_args[0] == ("GET", "/manage/projects")
|
||||||
|
assert result[0]["id"] == "p1"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project(self):
|
||||||
|
"""Should call POST /manage/projects."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"id": "new", "name": "New"})
|
||||||
|
|
||||||
|
await client.create_project({"name": "New", "domain": "new.com"})
|
||||||
|
|
||||||
|
assert client.request.call_args[0] == ("POST", "/manage/projects")
|
||||||
|
assert client.request.call_args.kwargs["json_data"]["name"] == "New"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_project(self):
|
||||||
|
"""Should call DELETE /manage/projects/:id."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
await client.delete_project("proj1")
|
||||||
|
|
||||||
|
assert client.request.call_args[0] == ("DELETE", "/manage/projects/proj1")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_clients(self):
|
||||||
|
"""Should call GET /manage/clients."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
await client.list_clients()
|
||||||
|
|
||||||
|
assert client.request.call_args[0] == ("GET", "/manage/clients")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_client(self):
|
||||||
|
"""Should call POST /manage/clients."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"id": "c1"})
|
||||||
|
|
||||||
|
await client.create_client({"name": "test", "projectId": "p1", "mode": "read"})
|
||||||
|
|
||||||
|
assert client.request.call_args[0] == ("POST", "/manage/clients")
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientHealth:
|
||||||
|
"""Test health check methods."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_check_healthy(self):
|
||||||
|
"""Should report healthy when both endpoints respond."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.request = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
result = await client.health_check()
|
||||||
|
|
||||||
|
assert result["healthy"] is True
|
||||||
|
assert "api" in result["services"]
|
||||||
|
assert "track_api" in result["services"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_instance_info(self):
|
||||||
|
"""Should return instance details."""
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://analytics.example.com",
|
||||||
|
client_id="long-client-id-12345",
|
||||||
|
client_secret="csec",
|
||||||
|
project_id="proj1",
|
||||||
|
)
|
||||||
|
|
||||||
|
info = await client.get_instance_info()
|
||||||
|
|
||||||
|
assert info["url"] == "https://analytics.example.com"
|
||||||
|
assert info["client_id"] == "long-cli..."
|
||||||
|
assert info["default_project_id"] == "proj1"
|
||||||
|
assert info["type"] == "openpanel"
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelClientErrors:
|
||||||
|
"""Test error handling."""
|
||||||
|
|
||||||
|
def test_extract_error_message_dict(self):
|
||||||
|
"""Should extract message from dict."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
msg = client._extract_error_message({"message": "Not found"}, 404)
|
||||||
|
assert "Not found" in msg
|
||||||
|
assert "Hint" in msg
|
||||||
|
|
||||||
|
def test_extract_error_auth_hint(self):
|
||||||
|
"""Should add auth hint for 401/403."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
msg = client._extract_error_message({"error": "Unauthorized"}, 401)
|
||||||
|
assert "Verify client_id" in msg
|
||||||
|
|
||||||
|
def test_extract_error_rate_limit_hint(self):
|
||||||
|
"""Should add rate limit hint for 429."""
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
msg = client._extract_error_message({"message": "Too many"}, 429)
|
||||||
|
assert "Rate limited" in msg
|
||||||
|
|
||||||
|
|
||||||
|
# --- OpenPanelPlugin Tests ---
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelPluginInit:
|
||||||
|
"""Test OpenPanelPlugin initialization."""
|
||||||
|
|
||||||
|
def test_valid_initialization(self):
|
||||||
|
"""Should initialize with valid config."""
|
||||||
|
plugin = OpenPanelPlugin(
|
||||||
|
config={
|
||||||
|
"url": "https://analytics.example.com",
|
||||||
|
"client_id": "cid",
|
||||||
|
"client_secret": "csec",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert isinstance(plugin.client, OpenPanelClient)
|
||||||
|
assert plugin.client.base_url == "https://analytics.example.com"
|
||||||
|
|
||||||
|
def test_project_id_passed_to_client(self):
|
||||||
|
"""Should pass project_id to client."""
|
||||||
|
plugin = OpenPanelPlugin(
|
||||||
|
config={
|
||||||
|
"url": "https://a.com",
|
||||||
|
"client_id": "cid",
|
||||||
|
"client_secret": "csec",
|
||||||
|
"project_id": "proj123",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert plugin.client.default_project_id == "proj123"
|
||||||
|
assert plugin.openpanel_project_id == "proj123"
|
||||||
|
|
||||||
|
def test_plugin_name(self):
|
||||||
|
"""Should return correct plugin name."""
|
||||||
|
assert OpenPanelPlugin.get_plugin_name() == "openpanel"
|
||||||
|
|
||||||
|
def test_required_config_keys(self):
|
||||||
|
"""Should require url, client_id, client_secret."""
|
||||||
|
keys = OpenPanelPlugin.get_required_config_keys()
|
||||||
|
assert "url" in keys
|
||||||
|
assert "client_id" in keys
|
||||||
|
assert "client_secret" in keys
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelPluginToolSpecs:
|
||||||
|
"""Test tool specifications."""
|
||||||
|
|
||||||
|
def test_tool_count(self):
|
||||||
|
"""Should return 42 tools."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
assert len(specs) == 42
|
||||||
|
|
||||||
|
def test_all_specs_have_required_fields(self):
|
||||||
|
"""All specs should have name, method_name, description, schema."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
for spec in specs:
|
||||||
|
assert "name" in spec, "Missing name in spec"
|
||||||
|
assert "method_name" in spec, f"Missing method_name in {spec.get('name')}"
|
||||||
|
assert "description" in spec, f"Missing description in {spec.get('name')}"
|
||||||
|
assert "schema" in spec, f"Missing schema in {spec.get('name')}"
|
||||||
|
|
||||||
|
def test_unique_tool_names(self):
|
||||||
|
"""All tool names should be unique."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = [s["name"] for s in specs]
|
||||||
|
assert len(names) == len(
|
||||||
|
set(names)
|
||||||
|
), f"Duplicate names: {[n for n in names if names.count(n) > 1]}"
|
||||||
|
|
||||||
|
def test_event_tools_present(self):
|
||||||
|
"""Should include core event tracking tools."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = {s["name"] for s in specs}
|
||||||
|
assert "track_event" in names
|
||||||
|
assert "identify_user" in names
|
||||||
|
assert "track_page_view" in names
|
||||||
|
assert "track_revenue" in names
|
||||||
|
assert "track_batch" in names
|
||||||
|
assert "create_group" in names
|
||||||
|
assert "assign_group" in names
|
||||||
|
|
||||||
|
def test_export_tools_present(self):
|
||||||
|
"""Should include export tools."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = {s["name"] for s in specs}
|
||||||
|
assert "export_events" in names
|
||||||
|
assert "export_chart_data" in names
|
||||||
|
assert "get_top_pages" in names
|
||||||
|
assert "get_top_referrers" in names
|
||||||
|
assert "get_geo_data" in names
|
||||||
|
|
||||||
|
def test_manage_tools_present(self):
|
||||||
|
"""Should include project and client management tools."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = {s["name"] for s in specs}
|
||||||
|
assert "list_projects" in names
|
||||||
|
assert "create_project" in names
|
||||||
|
assert "delete_project" in names
|
||||||
|
assert "list_clients" in names
|
||||||
|
assert "create_client" in names
|
||||||
|
|
||||||
|
def test_no_alias_tool(self):
|
||||||
|
"""Should NOT include alias_user (not supported by OpenPanel)."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = {s["name"] for s in specs}
|
||||||
|
assert "alias_user" not in names
|
||||||
|
|
||||||
|
def test_no_dashboard_tools(self):
|
||||||
|
"""Should NOT include dashboard tools (no public API)."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = {s["name"] for s in specs}
|
||||||
|
assert "list_dashboards" not in names
|
||||||
|
assert "create_dashboard" not in names
|
||||||
|
assert "add_chart" not in names
|
||||||
|
|
||||||
|
def test_no_funnel_tools(self):
|
||||||
|
"""Should NOT include funnel tools (no public API)."""
|
||||||
|
specs = OpenPanelPlugin.get_tool_specifications()
|
||||||
|
names = {s["name"] for s in specs}
|
||||||
|
assert "list_funnels" not in names
|
||||||
|
assert "create_funnel" not in names
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenPanelPluginDelegation:
|
||||||
|
"""Test handler method delegation."""
|
||||||
|
|
||||||
|
def test_track_event_delegated(self):
|
||||||
|
"""Should delegate track_event to events handler."""
|
||||||
|
plugin = OpenPanelPlugin(
|
||||||
|
config={"url": "https://a.com", "client_id": "cid", "client_secret": "csec"}
|
||||||
|
)
|
||||||
|
# __getattr__ should find track_event in events handler
|
||||||
|
assert callable(plugin.track_event)
|
||||||
|
|
||||||
|
def test_export_events_delegated(self):
|
||||||
|
"""Should delegate export_events to export handler."""
|
||||||
|
plugin = OpenPanelPlugin(
|
||||||
|
config={"url": "https://a.com", "client_id": "cid", "client_secret": "csec"}
|
||||||
|
)
|
||||||
|
assert callable(plugin.export_events)
|
||||||
|
|
||||||
|
def test_list_projects_delegated(self):
|
||||||
|
"""Should delegate list_projects to projects handler."""
|
||||||
|
plugin = OpenPanelPlugin(
|
||||||
|
config={"url": "https://a.com", "client_id": "cid", "client_secret": "csec"}
|
||||||
|
)
|
||||||
|
assert callable(plugin.list_projects)
|
||||||
|
|
||||||
|
def test_nonexistent_method_raises(self):
|
||||||
|
"""Should raise AttributeError for unknown methods."""
|
||||||
|
plugin = OpenPanelPlugin(
|
||||||
|
config={"url": "https://a.com", "client_id": "cid", "client_secret": "csec"}
|
||||||
|
)
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
plugin.nonexistent_method()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handler Integration Tests ---
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventHandlers:
|
||||||
|
"""Test event handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_track_event_handler(self):
|
||||||
|
"""Should return success JSON on track."""
|
||||||
|
from plugins.openpanel.handlers.events import track_event
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.track_event = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
result = await track_event(client, name="test_event")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["event"] == "test_event"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_track_event_error(self):
|
||||||
|
"""Should return error JSON on failure."""
|
||||||
|
from plugins.openpanel.handlers.events import track_event
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.track_event = AsyncMock(side_effect=Exception("Connection refused"))
|
||||||
|
|
||||||
|
result = await track_event(client, name="test_event")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is False
|
||||||
|
assert "Connection refused" in data["error"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_group_handler(self):
|
||||||
|
"""Should return success JSON on group creation."""
|
||||||
|
from plugins.openpanel.handlers.events import create_group
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.track_group = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
result = await create_group(client, group_id="g1", group_type="company", name="Acme")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["group_id"] == "g1"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_track_batch_handler(self):
|
||||||
|
"""Should track multiple events."""
|
||||||
|
from plugins.openpanel.handlers.events import track_batch
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.track_event = AsyncMock(return_value={"success": True})
|
||||||
|
|
||||||
|
events = [{"name": "event1"}, {"name": "event2"}, {"name": "event3"}]
|
||||||
|
result = await track_batch(client, events=events)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["tracked"] == 3
|
||||||
|
assert data["total"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
class TestExportHandlers:
|
||||||
|
"""Test export handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_events_handler(self):
|
||||||
|
"""Should return formatted export data."""
|
||||||
|
from plugins.openpanel.handlers.export import export_events
|
||||||
|
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://a.com", client_id="cid", client_secret="csec", project_id="proj1"
|
||||||
|
)
|
||||||
|
client.export_events = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"meta": {"totalCount": 2, "current": 1, "pages": 1},
|
||||||
|
"data": [{"name": "ev1"}, {"name": "ev2"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await export_events(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert data["total"] == 2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_top_pages_handler(self):
|
||||||
|
"""Should return top pages data."""
|
||||||
|
from plugins.openpanel.handlers.export import get_top_pages
|
||||||
|
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://a.com", client_id="cid", client_secret="csec", project_id="proj1"
|
||||||
|
)
|
||||||
|
client.get_top_pages = AsyncMock(return_value=[{"path": "/home", "count": 100}])
|
||||||
|
|
||||||
|
result = await get_top_pages(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectHandlers:
|
||||||
|
"""Test project management handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_projects_handler(self):
|
||||||
|
"""Should return projects list."""
|
||||||
|
from plugins.openpanel.handlers.projects import list_projects
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.list_projects = AsyncMock(return_value=[{"id": "p1", "name": "Test"}])
|
||||||
|
|
||||||
|
result = await list_projects(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["count"] == 1
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_project_handler(self):
|
||||||
|
"""Should create and return project."""
|
||||||
|
from plugins.openpanel.handlers.projects import create_project
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.create_project = AsyncMock(return_value={"id": "new", "name": "New"})
|
||||||
|
|
||||||
|
result = await create_project(client, name="New", domain="new.com")
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert "New" in data["message"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemHandlers:
|
||||||
|
"""Test system handler functions."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_check_handler(self):
|
||||||
|
"""Should return health status."""
|
||||||
|
from plugins.openpanel.handlers.system import health_check
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.health_check = AsyncMock(
|
||||||
|
return_value={"healthy": True, "services": {"api": "ok", "track_api": "ok"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await health_check(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["healthy"] is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_test_connection_handler(self):
|
||||||
|
"""Should return connection status."""
|
||||||
|
from plugins.openpanel.handlers.system import test_connection
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
client.health_check = AsyncMock(return_value={"healthy": True})
|
||||||
|
|
||||||
|
result = await test_connection(client)
|
||||||
|
data = json.loads(result)
|
||||||
|
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["connection"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
class TestUtilsModule:
|
||||||
|
"""Test utility functions."""
|
||||||
|
|
||||||
|
def test_get_project_id_explicit(self):
|
||||||
|
"""Should use explicit project_id."""
|
||||||
|
from plugins.openpanel.handlers.utils import get_project_id
|
||||||
|
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://a.com", client_id="cid", client_secret="csec", project_id="default"
|
||||||
|
)
|
||||||
|
assert get_project_id(client, "explicit") == "explicit"
|
||||||
|
|
||||||
|
def test_get_project_id_default(self):
|
||||||
|
"""Should fall back to client default."""
|
||||||
|
from plugins.openpanel.handlers.utils import get_project_id
|
||||||
|
|
||||||
|
client = OpenPanelClient(
|
||||||
|
base_url="https://a.com", client_id="cid", client_secret="csec", project_id="default"
|
||||||
|
)
|
||||||
|
assert get_project_id(client, None) == "default"
|
||||||
|
|
||||||
|
def test_get_project_id_missing_raises(self):
|
||||||
|
"""Should raise ValueError when no project_id available."""
|
||||||
|
from plugins.openpanel.handlers.utils import get_project_id
|
||||||
|
|
||||||
|
client = OpenPanelClient(base_url="https://a.com", client_id="cid", client_secret="csec")
|
||||||
|
with pytest.raises(ValueError, match="project_id"):
|
||||||
|
get_project_id(client, None)
|
||||||
Reference in New Issue
Block a user