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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user