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:
2026-03-31 17:48:12 +02:00
parent 68c84b3ece
commit 9905a28eb0
33 changed files with 1672 additions and 3207 deletions

View File

@@ -1,11 +1,8 @@
"""
OpenPanel Plugin - Product Analytics Management
OpenPanel Plugin - Product Analytics Management.
Complete OpenPanel Self-Hosted management through REST API.
Provides tools for event tracking, data export, funnels,
dashboards, user profiles, and analytics reports.
For Self-Hosted instances deployed on Coolify.
Self-hosted OpenPanel management through public REST APIs (42 tools).
Event tracking, data export, analytics, project & client management.
"""
from plugins.openpanel.client import OpenPanelClient

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,36 @@
"""
OpenPanel Handlers
OpenPanel Handlers — 42 tools across 7 handlers.
Phase H.1: Core (25 tools)
- events.py: Event tracking (9 tools - alias_user removed)
- export.py: Data export (10 tools)
- system.py: Health & stats (6 tools)
All tools use public REST APIs (no tRPC/session dependency).
Phase H.2: Analytics (24 tools)
- reports.py: Analytics reports (8 tools)
- funnels.py: Funnel analysis (8 tools)
- profiles.py: User profiles (8 tools)
Track API (/track) — write mode:
- events.py: Event tracking, groups (11 tools)
Phase H.3: Management (24 tools)
- projects.py: Project management (8 tools)
- dashboards.py: Dashboard management (10 tools)
- clients.py: API client management (6 tools)
Export API (/export) — read mode:
- export.py: Data export & analytics (10 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 (
clients,
dashboards,
events,
export,
funnels,
profiles,
projects,
reports,
@@ -36,9 +42,7 @@ __all__ = [
"export",
"system",
"reports",
"funnels",
"profiles",
"projects",
"dashboards",
"clients",
]

View File

@@ -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
from typing import Any
@@ -7,107 +11,82 @@ from plugins.openpanel.client import OpenPanelClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (6 tools)"""
"""Return tool specifications for ToolGenerator (5 tools)."""
return [
{
"name": "list_clients",
"method_name": "list_clients",
"description": "List all API clients for a project.",
"schema": {
"type": "object",
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
"required": ["project_id"],
},
"scope": "read",
"description": "List all API clients via Manage API. Requires 'root' mode client.",
"schema": {"type": "object", "properties": {}},
"scope": "admin",
},
{
"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": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"client_id": {"type": "string", "description": "API Client ID"},
"client_id": {"type": "string", "description": "Client ID"},
},
"required": ["project_id", "client_id"],
"required": ["client_id"],
},
"scope": "read",
"scope": "admin",
},
{
"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": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"name": {
"type": "string",
"description": "Client name (e.g., 'Web Tracker', 'Backend Export')",
},
"name": {"type": "string", "description": "Client name"},
"project_id": {"type": "string", "description": "Project to associate with"},
"mode": {
"type": "string",
"enum": ["write", "read", "root"],
"description": "Client mode: write (tracking), read (export), root (full access)",
},
"cors_domains": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Allowed domains for CORS (write mode)",
"description": "Client mode: write (tracking), read (export/analytics), root (full access)",
"default": "write",
},
},
"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",
},
{
"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": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"client_id": {"type": "string", "description": "Client ID to delete"},
},
"required": ["project_id", "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"],
"required": ["client_id"],
},
"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:
"""List all API clients"""
async def list_clients(client: OpenPanelClient) -> str:
"""List all API clients via GET /manage/clients."""
try:
result = await client.list_clients()
clients = (
result
if isinstance(result, list)
else result.get("data", []) if isinstance(result, dict) else []
)
return json.dumps(
{
"success": True,
"project_id": project_id,
"note": "Client listing requires dashboard tRPC API. Use OpenPanel dashboard to view clients.",
"message": "Client list request processed",
},
{"success": True, "count": len(clients), "clients": clients},
indent=2,
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)
async def get_client(client: OpenPanelClient, project_id: str, client_id: str) -> str:
"""Get API client details"""
async def get_client(client: OpenPanelClient, client_id: str) -> str:
"""Get client details via GET /manage/clients/:id."""
try:
return json.dumps(
{
"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,
)
result = await client.get_client(client_id)
return json.dumps({"success": True, "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 create_client(
client: OpenPanelClient,
project_id: 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,
client_id: str,
mode: str,
cors_domains: list[str] | None = None,
mode: str = "write",
) -> str:
"""Update client permissions"""
"""Create a new API client via POST /manage/clients."""
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(
{
"success": True,
"project_id": project_id,
"client_id": client_id,
"updates": updates,
"note": "Client mode update requires dashboard tRPC API. Use OpenPanel dashboard to modify.",
"message": f"Client mode update to {mode} configuration created",
"message": f"Client '{name}' created with {mode} mode",
"client": result,
"note": "Save the client_secret — it cannot be retrieved later.",
},
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(
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)

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
"""Events Handler - OpenPanel event tracking operations (10 tools)"""
"""Events Handler - OpenPanel event tracking operations (11 tools)"""
import json
from typing import Any
@@ -7,7 +7,7 @@ from plugins.openpanel.client import OpenPanelClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
"""Return tool specifications for ToolGenerator (11 tools)"""
return [
{
"name": "track_event",
@@ -222,7 +222,56 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"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",
"method_name": "track_revenue",
@@ -566,32 +615,55 @@ async def decrement_property(
)
async def alias_user(client: OpenPanelClient, profile_id: str, alias: str) -> str:
"""Create an alias to link two profile IDs"""
async def create_group(
client: OpenPanelClient,
group_id: str,
group_type: str,
name: str,
properties: dict[str, Any] | None = None,
) -> str:
"""Create or update a group"""
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(
{
"success": True,
"profile_id": profile_id,
"alias": alias,
"message": f"Alias '{alias}' linked to profile '{profile_id}'",
"group_id": group_id,
"group_type": group_type,
"name": name,
"message": f"Group '{name}' ({group_type}) created/updated",
"response": result,
},
indent=2,
ensure_ascii=False,
)
except Exception as e:
error_str = str(e)
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.)"
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
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(
{"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,
ensure_ascii=False,
)
except Exception as e:
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
async def track_revenue(

View File

@@ -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.
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
@@ -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]]:
"""Return tool specifications for ToolGenerator (10 tools)"""
"""Return tool specifications for ToolGenerator (10 tools)."""
return [
{
"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": {
"type": "object",
"properties": {
@@ -56,11 +62,22 @@ def get_tool_specifications() -> list[dict[str, Any]]:
"anyOf": [
{
"type": "array",
"items": {"type": "string", "enum": ["profile", "meta"]},
"items": {
"type": "string",
"enum": [
"profile",
"meta",
"properties",
"region",
"device",
"referrer",
"revenue",
],
},
},
{"type": "null"},
],
"description": "Additional data to include (profile, meta)",
"description": "Additional data to include (profile, meta, properties, region, device, referrer, revenue)",
},
},
"required": [],
@@ -70,7 +87,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -103,7 +120,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -150,7 +167,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
"breakdowns": {
@@ -188,7 +205,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -202,7 +219,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
},
@@ -213,7 +230,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -223,7 +240,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
},
@@ -234,7 +251,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -244,15 +261,9 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
"interval": {
"type": "string",
"enum": ["hour", "day", "week", "month"],
"description": "Time interval",
"default": "day",
},
},
"required": [],
},
@@ -261,7 +272,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -271,7 +282,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
"limit": {
@@ -287,7 +298,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -297,7 +308,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
"limit": {
@@ -313,7 +324,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -323,7 +334,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
"breakdown": {
@@ -345,7 +356,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -355,7 +366,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"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",
},
"breakdown": {
@@ -393,11 +404,11 @@ async def export_events(
page: int = 1,
includes: list[str] | None = None,
) -> str:
"""Export raw event data"""
"""Export raw event data via GET /export/events."""
try:
effective_project_id = _get_project_id(client, project_id)
pid = _get_project_id(client, project_id)
result = await client.export_events(
project_id=effective_project_id,
project_id=pid,
event=event,
profile_id=profile_id,
start=start,
@@ -406,18 +417,17 @@ async def export_events(
limit=limit,
includes=includes,
)
events_count = len(result.get("data", [])) if isinstance(result, dict) else 0
meta = result.get("meta", {}) if isinstance(result, dict) else {}
data = result.get("data", []) if isinstance(result, dict) else []
return json.dumps(
{
"success": True,
"project_id": effective_project_id,
"count": events_count,
"page": page,
"limit": limit,
"filters": {"event": event, "profile_id": profile_id, "start": start, "end": end},
"data": result,
"project_id": pid,
"count": len(data),
"total": meta.get("totalCount", len(data)),
"page": meta.get("current", page),
"pages": meta.get("pages", 1),
"data": data,
},
indent=2,
ensure_ascii=False,
@@ -434,20 +444,18 @@ async def export_events_csv(
end: str | None = None,
limit: int = 1000,
) -> str:
"""Export events as CSV-formatted data"""
"""Export events as CSV-formatted data."""
try:
effective_project_id = _get_project_id(client, project_id)
pid = _get_project_id(client, project_id)
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 []
if not events:
return json.dumps(
{
"success": True,
"project_id": effective_project_id,
"project_id": pid,
"count": 0,
"csv": "",
"message": "No events found",
@@ -455,28 +463,22 @@ async def export_events_csv(
indent=2,
ensure_ascii=False,
)
# Build CSV
headers = ["timestamp", "name", "profile_id"]
csv_lines = [",".join(headers)]
for event_data in events:
for ev in events:
row = [
str(event_data.get("timestamp", "")),
str(event_data.get("name", "")),
str(event_data.get("profileId", "")),
str(ev.get("createdAt", ev.get("timestamp", ""))),
str(ev.get("name", "")),
str(ev.get("profileId", "")),
]
csv_lines.append(",".join(row))
csv_content = "\n".join(csv_lines)
return json.dumps(
{
"success": True,
"project_id": effective_project_id,
"project_id": pid,
"count": len(events),
"format": "csv",
"csv": csv_content,
"csv": "\n".join(csv_lines),
},
indent=2,
ensure_ascii=False,
@@ -494,22 +496,22 @@ async def export_chart_data(
breakdowns: list[str] | None = None,
previous: bool = False,
) -> str:
"""Export aggregated chart data"""
"""Export aggregated chart data via GET /export/charts."""
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(
project_id=effective_project_id,
project_id=pid,
events=events,
interval=interval,
date_range=date_range,
breakdowns=breakdowns,
breakdowns=bd,
previous=previous,
)
return json.dumps(
{
"success": True,
"project_id": effective_project_id,
"project_id": pid,
"events": [e.get("name") for e in events],
"interval": interval,
"date_range": date_range,
@@ -529,29 +531,33 @@ async def get_event_count(
event: str | None = None,
date_range: str = "30d",
) -> str:
"""Get total event count"""
"""Get total event count via Export charts API."""
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"}]
result = await client.export_charts(
project_id=effective_project_id,
project_id=pid,
events=events_config,
interval="day",
date_range=date_range,
)
# Sum up the counts
# Extract total from chart response
total = 0
if isinstance(result, dict) and "data" in result:
for point in result.get("data", []):
total += point.get("count", 0)
if isinstance(result, dict):
series = result.get("series", [])
if series:
for s in series:
for point in s.get("data", []):
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(
{
"success": True,
"project_id": effective_project_id,
"event": event if event else "all events",
"project_id": pid,
"event": event or "all events",
"date_range": date_range,
"total_count": total,
},
@@ -565,47 +571,12 @@ async def get_event_count(
async def get_unique_users(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
) -> str:
"""Get unique user count using tRPC overview.stats"""
"""Get unique user count via Insights metrics API."""
try:
effective_project_id = _get_project_id(client, project_id)
# 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(
{
"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)
)
pid = _get_project_id(client, project_id)
result = await client.get_overview_stats(project_id=pid, date_range=date_range)
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,
},
{"success": True, "project_id": pid, "date_range": date_range, "stats": result},
indent=2,
ensure_ascii=False,
)
@@ -617,51 +588,13 @@ async def get_page_views(
client: OpenPanelClient,
project_id: str | None = None,
date_range: str = "30d",
interval: str = "day",
) -> str:
"""Get page view statistics using tRPC overview.stats"""
"""Get page view statistics via Insights metrics API."""
try:
effective_project_id = _get_project_id(client, project_id)
# 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(
{
"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)
)
pid = _get_project_id(client, project_id)
result = await client.get_overview_stats(project_id=pid, date_range=date_range)
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,
},
{"success": True, "project_id": pid, "date_range": date_range, "stats": result},
indent=2,
ensure_ascii=False,
)
@@ -672,44 +605,12 @@ async def get_page_views(
async def get_top_pages(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
) -> str:
"""Get top pages by view count using tRPC overview.topPages"""
"""Get top pages via Insights pages API."""
try:
effective_project_id = _get_project_id(client, project_id)
# 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(
{
"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]
pid = _get_project_id(client, project_id)
result = await client.get_top_pages(project_id=pid, date_range=date_range, limit=limit)
return json.dumps(
{
"success": True,
"project_id": effective_project_id,
"date_range": date_range,
"count": len(pages),
"top_pages": pages,
},
{"success": True, "project_id": pid, "date_range": date_range, "data": result},
indent=2,
ensure_ascii=False,
)
@@ -720,54 +621,12 @@ async def get_top_pages(
async def get_top_referrers(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d", limit: int = 10
) -> str:
"""Get top traffic sources using chart.chart with referrer breakdown"""
"""Get top traffic sources via Insights breakdown API."""
try:
effective_project_id = _get_project_id(client, project_id)
# 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(
{
"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),
}
)
pid = _get_project_id(client, project_id)
result = await client.get_top_sources(project_id=pid, date_range=date_range, limit=limit)
return json.dumps(
{
"success": True,
"project_id": effective_project_id,
"date_range": date_range,
"count": len(referrers),
"top_referrers": referrers,
},
{"success": True, "project_id": pid, "date_range": date_range, "data": result},
indent=2,
ensure_ascii=False,
)
@@ -782,55 +641,19 @@ async def get_geo_data(
breakdown: str = "country",
limit: int = 10,
) -> str:
"""Get geographic distribution using chart.chart with country/city/region breakdown"""
"""Get geographic distribution via Insights breakdown API."""
try:
effective_project_id = _get_project_id(client, project_id)
# Use chart.chart with geographic breakdown
pid = _get_project_id(client, project_id)
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(
{
"success": True,
"project_id": effective_project_id,
"project_id": pid,
"date_range": date_range,
"breakdown": breakdown,
"count": len(locations),
"locations": locations,
"data": result,
},
indent=2,
ensure_ascii=False,
@@ -846,64 +669,26 @@ async def get_device_data(
breakdown: str = "device",
limit: int = 10,
) -> str:
"""Get device/browser/OS breakdown using chart.chart with appropriate breakdown"""
"""Get device/browser/OS breakdown via Insights breakdown API."""
try:
effective_project_id = _get_project_id(client, project_id)
# Use appropriate chart.chart breakdown based on breakdown type
pid = _get_project_id(client, project_id)
if breakdown == "browser":
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":
result = await client.get_top_os(
project_id=effective_project_id, date_range=date_range, limit=limit
)
else: # device (default)
result = await client.get_top_os(project_id=pid, date_range=date_range, limit=limit)
else:
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(
{
"success": True,
"project_id": effective_project_id,
"project_id": pid,
"date_range": date_range,
"breakdown": breakdown,
"count": len(devices),
"devices": devices,
"data": result,
},
indent=2,
ensure_ascii=False,

View File

@@ -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)

View File

@@ -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
from typing import Any
@@ -7,81 +10,12 @@ from plugins.openpanel.client import OpenPanelClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
"""Return tool specifications for ToolGenerator (3 tools)."""
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",
"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": {
"type": "object",
"properties": {
@@ -95,7 +29,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
"type": "integer",
"description": "Number of events",
"default": 50,
"maximum": 200,
"maximum": 1000,
},
},
"required": ["project_id", "profile_id"],
@@ -105,7 +39,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -115,57 +49,17 @@ def get_tool_specifications() -> list[dict[str, Any]]:
"type": "integer",
"description": "Number of sessions",
"default": 20,
"maximum": 50,
"maximum": 100,
},
},
"required": ["project_id", "profile_id"],
},
"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",
"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": {
"type": "object",
"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(
client: OpenPanelClient,
project_id: str,
@@ -291,22 +91,22 @@ async def get_profile_events(
event: str | None = None,
limit: int = 50,
) -> str:
"""Get events for a profile"""
"""Get events for a profile via Export API."""
try:
result = await client.export_events(
project_id=project_id, profile_id=profile_id, event=event, limit=limit
)
events_count = len(result.get("data", [])) if isinstance(result, dict) else 0
data = result.get("data", []) if isinstance(result, dict) else []
meta = result.get("meta", {}) if isinstance(result, dict) else {}
return json.dumps(
{
"success": True,
"project_id": project_id,
"profile_id": profile_id,
"event_filter": event,
"count": events_count,
"data": result,
"count": len(data),
"total": meta.get("totalCount", len(data)),
"data": data,
},
indent=2,
ensure_ascii=False,
@@ -318,75 +118,19 @@ async def get_profile_events(
async def get_profile_sessions(
client: OpenPanelClient, project_id: str, profile_id: str, limit: int = 20
) -> str:
"""Get sessions for a profile"""
"""Get sessions for a profile via Export API."""
try:
# Get session events
result = await client.export_events(
project_id=project_id, profile_id=profile_id, event="session_start", limit=limit
)
sessions = result.get("data", []) if isinstance(result, dict) else []
data = result.get("data", []) if isinstance(result, dict) else []
return json.dumps(
{
"success": True,
"project_id": project_id,
"profile_id": profile_id,
"sessions_count": len(sessions),
"sessions": sessions,
},
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",
"sessions_count": len(data),
"sessions": data,
},
indent=2,
ensure_ascii=False,
@@ -398,30 +142,23 @@ async def merge_profiles(
async def export_profile_data(
client: OpenPanelClient, project_id: str, profile_id: str, format: str = "json"
) -> str:
"""Export all profile data (GDPR)"""
"""Export all profile data (GDPR compliance)."""
try:
# Export events for this profile
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 []
export_data = {
"profile_id": profile_id,
"project_id": project_id,
"events_count": len(events),
"events": events,
}
if format == "csv":
# Build CSV
csv_lines = ["timestamp,event_name,properties"]
for event in events:
for ev in events:
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(
{
"success": True,
@@ -429,7 +166,6 @@ async def export_profile_data(
"format": "csv",
"events_count": len(events),
"csv": "\n".join(csv_lines),
"message": "Profile data exported (GDPR compliance)",
},
indent=2,
ensure_ascii=False,
@@ -441,8 +177,7 @@ async def export_profile_data(
"profile_id": profile_id,
"format": "json",
"events_count": len(events),
"data": export_data,
"message": "Profile data exported (GDPR compliance)",
"data": {"profile_id": profile_id, "project_id": project_id, "events": events},
},
indent=2,
ensure_ascii=False,

View File

@@ -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
from typing import Any
@@ -7,22 +11,24 @@ from plugins.openpanel.client import OpenPanelClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
"""Return tool specifications for ToolGenerator (5 tools)."""
return [
{
"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": {}},
"scope": "read",
"scope": "admin",
},
{
"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": {
"type": "object",
"properties": {"project_id": {"type": "string", "description": "Project ID"}},
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
},
"required": ["project_id"],
},
"scope": "read",
@@ -30,19 +36,14 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Project name"},
"domain": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Primary domain for the project",
},
"timezone": {
"type": "string",
"description": "Project timezone",
"default": "UTC",
"description": "Project domain (e.g., 'example.com')",
},
},
"required": ["name"],
@@ -52,22 +53,18 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"name": "update_project",
"method_name": "update_project",
"description": "Update project settings.",
"description": "Update a project via Manage API. Requires 'root' mode client.",
"schema": {
"type": "object",
"properties": {
"project_id": {"type": "string", "description": "Project ID"},
"project_id": {"type": "string", "description": "Project ID to update"},
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New project name",
},
"domain": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New primary domain",
},
"timezone": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New timezone",
"description": "New domain",
},
},
"required": ["project_id"],
@@ -77,85 +74,35 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
"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"],
},
"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",
},
]
# =====================
# Project Functions (8)
# Project Functions (5)
# =====================
async def list_projects(client: OpenPanelClient) -> str:
"""List all projects"""
"""List all projects via GET /manage/projects."""
try:
result = await client.list_projects()
projects = (
result
if isinstance(result, list)
else result.get("data", []) if isinstance(result, dict) else []
)
return json.dumps(
{
"success": True,
"note": "Project listing requires dashboard tRPC API. Use OpenPanel dashboard to view projects.",
"message": "Project list request processed",
},
{"success": True, "count": len(projects), "projects": projects},
indent=2,
ensure_ascii=False,
)
@@ -164,36 +111,23 @@ async def list_projects(client: OpenPanelClient) -> str:
async def get_project(client: OpenPanelClient, project_id: str) -> str:
"""Get project details"""
"""Get project details via GET /manage/projects/:id."""
try:
return json.dumps(
{
"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,
)
result = await client.get_project(project_id)
return json.dumps({"success": True, "project": 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 create_project(
client: OpenPanelClient, name: str, domain: str | None = None, timezone: str = "UTC"
) -> str:
"""Create a new project"""
async def create_project(client: OpenPanelClient, name: str, domain: str | None = None) -> str:
"""Create a new project via POST /manage/projects."""
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(
{
"success": True,
"project": project_config,
"note": "Project creation requires dashboard tRPC API. Use OpenPanel dashboard to create projects.",
"message": f"Project '{name}' configuration created",
},
{"success": True, "message": f"Project '{name}' created", "project": result},
indent=2,
ensure_ascii=False,
)
@@ -206,55 +140,23 @@ async def update_project(
project_id: str,
name: str | None = None,
domain: str | None = None,
timezone: str | None = None,
) -> str:
"""Update project settings"""
"""Update a project via PATCH /manage/projects/:id."""
try:
updates = {}
data: dict[str, Any] = {}
if name:
updates["name"] = name
data["name"] = name
if domain:
updates["domain"] = domain
if timezone:
updates["timezone"] = timezone
return json.dumps(
{
"success": True,
"project_id": project_id,
"updates": updates,
"note": "Project updates require dashboard tRPC API. Use OpenPanel dashboard to modify projects.",
"message": "Project 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_project(client: OpenPanelClient, project_id: str, confirm: bool = False) -> str:
"""Delete a project"""
try:
if not confirm:
data["domain"] = domain
if not data:
return json.dumps(
{
"success": False,
"error": "Deletion not confirmed. Set confirm=true to proceed.",
"warning": "This will permanently delete all project data including events, users, and settings.",
},
{"success": False, "error": "No fields to update. Provide name or domain."},
indent=2,
ensure_ascii=False,
)
result = await client.update_project(project_id, data)
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",
},
{"success": True, "message": f"Project '{project_id}' updated", "project": result},
indent=2,
ensure_ascii=False,
)
@@ -262,81 +164,12 @@ async def delete_project(client: OpenPanelClient, project_id: str, confirm: bool
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"""
async def delete_project(client: OpenPanelClient, project_id: str) -> str:
"""Delete a project via DELETE /manage/projects/:id."""
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
result = await client.delete_project(project_id)
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",
},
{"success": True, "message": f"Project '{project_id}' deleted", "result": result},
indent=2,
ensure_ascii=False,
)

View File

@@ -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
@@ -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]]:
"""Return tool specifications for ToolGenerator (8 tools)"""
"""Return tool specifications for ToolGenerator (2 tools)."""
return [
{
"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": {
"type": "object",
"properties": {
@@ -26,116 +26,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"date_range": {
"type": "string",
"description": "Date range. Common: today, yesterday, 7d, 14d, 30d, 60d, 90d, 6m, 12m, monthToDate, yearToDate, all",
"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",
"description": "Date range (today, 7d, 30d, 6m, 12m, etc.)",
"default": "30d",
},
},
@@ -146,7 +37,7 @@ def get_tool_specifications() -> list[dict[str, Any]]:
{
"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": {
"type": "object",
"properties": {
@@ -159,250 +50,23 @@ def get_tool_specifications() -> list[dict[str, Any]]:
},
"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(
client: OpenPanelClient, project_id: str | None = None, date_range: str = "30d"
) -> str:
"""Get overview statistics using tRPC overview.stats endpoint"""
"""Get overview statistics via Insights metrics API."""
try:
effective_project_id = _get_project_id(client, project_id)
# 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(
{
"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),
}
pid = _get_project_id(client, project_id)
result = await client.get_overview_stats(project_id=pid, date_range=date_range)
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",
},
{"success": True, "project_id": pid, "date_range": date_range, "stats": result},
indent=2,
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:
"""Get real-time visitor statistics using chart.chart with 30min range"""
"""Get real-time active visitor count via Insights live API."""
try:
effective_project_id = _get_project_id(client, project_id)
# 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(
{
"success": False,
"project_id": effective_project_id,
"error": result.get("error"),
"note": "tRPC chart.chart endpoint may require authentication",
},
indent=2,
ensure_ascii=False,
)
pid = _get_project_id(client, project_id)
result = await client.get_live_visitors(project_id=pid)
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",
},
{"success": True, "project_id": pid, "realtime": result},
indent=2,
ensure_ascii=False,
)

View File

@@ -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:
"""Get usage statistics for a project"""
"""Get usage statistics for a project via Insights metrics API."""
try:
# Get event count
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)
result = await client.get_overview_stats(project_id=project_id, date_range=date_range)
return json.dumps(
{
"success": True,
"project_id": project_id,
"date_range": date_range,
"stats": {
"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}",
"stats": result,
},
indent=2,
ensure_ascii=False,

View File

@@ -1,10 +1,17 @@
"""
OpenPanel Plugin - Product Analytics Management
OpenPanel Plugin - Product Analytics Management.
Complete OpenPanel Self-Hosted management through REST API.
Provides tools for event tracking, data export, and analytics.
Self-hosted OpenPanel management through public REST APIs.
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
@@ -16,30 +23,17 @@ from plugins.openpanel.client import OpenPanelClient
class OpenPanelPlugin(BasePlugin):
"""
OpenPanel Analytics Plugin - Comprehensive product analytics.
OpenPanel Analytics Plugin — 42 tools across 7 handlers.
Provides complete OpenPanel management capabilities including:
- Event tracking (track, identify, increment, decrement)
- Data export (events, charts, CSV)
- Analytics reports (page views, referrers, geo, devices)
- System operations (health, stats)
All tools use public REST APIs (no tRPC/session dependency).
Phase H.1: Core (25 tools)
- Events Handler: 9 tools (alias_user removed - not supported on self-hosted)
- Export Handler: 10 tools
- System Handler: 6 tools
Phase H.2: Analytics (24 tools)
- Reports Handler: 8 tools
- 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
Events (11): track, identify, increment, decrement, group, assign_group, batch, revenue
Export (10): events, charts, CSV, counts, top pages/referrers/geo/devices
Reports (2): overview stats, realtime visitors
Profiles (3): profile events, sessions, GDPR export
Projects (5): list, get, create, update, delete
Clients (5): list, get, create, update, delete
System (6): health, instance info, usage stats, storage, connection test, rate limits
"""
@staticmethod
@@ -61,63 +55,42 @@ class OpenPanelPlugin(BasePlugin):
- url: OpenPanel instance URL
- client_id: Client ID for authentication
- client_secret: Client Secret for authentication
- project_id: OpenPanel project ID (for Export/Read APIs)
- organization_id: Organization/Workspace ID (for multi-tenant setups)
- project_id: OpenPanel project ID (for Export/Insights APIs)
- organization_id: Organization/Workspace ID (optional)
project_id: Optional MCP project ID (auto-generated if not provided)
"""
super().__init__(config, project_id=project_id)
# Get OpenPanel project_id and organization_id from config
openpanel_project_id = config.get("project_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(
base_url=config["url"],
client_id=config["client_id"],
client_secret=config["client_secret"],
project_id=openpanel_project_id,
organization_id=openpanel_organization_id,
session_cookie=session_cookie,
)
# Store for reference
self.openpanel_project_id = openpanel_project_id
self.openpanel_organization_id = openpanel_organization_id
self.has_session = bool(session_cookie)
@staticmethod
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
with site parameter routing.
Returns:
List of tool specification dictionaries (26 tools in Phase H.1)
"""
specs = []
# Phase H.1: Core (26 tools)
specs.extend(handlers.events.get_tool_specifications()) # 10 tools
specs.extend(handlers.events.get_tool_specifications()) # 11 tools
specs.extend(handlers.export.get_tool_specifications()) # 10 tools
specs.extend(handlers.system.get_tool_specifications()) # 6 tools
# Phase H.2: Analytics (24 tools)
specs.extend(handlers.reports.get_tool_specifications()) # 8 tools
specs.extend(handlers.funnels.get_tool_specifications()) # 8 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
specs.extend(handlers.reports.get_tool_specifications()) # 2 tools
specs.extend(handlers.profiles.get_tool_specifications()) # 3 tools
specs.extend(handlers.projects.get_tool_specifications()) # 5 tools
specs.extend(handlers.clients.get_tool_specifications()) # 5 tools
return specs
def __getattr__(self, name: str):
@@ -139,10 +112,8 @@ class OpenPanelPlugin(BasePlugin):
handlers.export,
handlers.system,
handlers.reports,
handlers.funnels,
handlers.profiles,
handlers.projects,
handlers.dashboards,
handlers.clients,
]