diff --git a/README.md b/README.md index 279da1d..0ad111a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ to provide several ### Run core reports 📙 - `run_report`: Runs a Google Analytics report using the Data API. +- `run_funnel_report`: Runs a Google Analytics funnel report using the Data API. - `get_custom_dimensions_and_metrics`: Retrieves the custom dimensions and metrics for a specific property. diff --git a/analytics_mcp/server.py b/analytics_mcp/server.py index 234f493..f97bd83 100755 --- a/analytics_mcp/server.py +++ b/analytics_mcp/server.py @@ -25,6 +25,7 @@ from analytics_mcp.tools.admin import info # noqa: F401 from analytics_mcp.tools.reporting import realtime # noqa: F401 from analytics_mcp.tools.reporting import core # noqa: F401 +from analytics_mcp.tools.reporting import funnel # noqa: F401 def run_server() -> None: diff --git a/analytics_mcp/tools/reporting/funnel.py b/analytics_mcp/tools/reporting/funnel.py new file mode 100644 index 0000000..78675b0 --- /dev/null +++ b/analytics_mcp/tools/reporting/funnel.py @@ -0,0 +1,177 @@ +from typing import Any, Dict, List + +from analytics_mcp.coordinator import mcp +from analytics_mcp.tools.reporting.metadata import ( + get_date_ranges_hints, + get_funnel_steps_hints, +) +from analytics_mcp.tools.utils import ( + construct_property_rn, + create_data_api_alpha_client, + proto_to_dict, +) +from google.analytics import data_v1alpha + + +def _run_funnel_report_description() -> str: + """Returns the description for the `run_funnel_report` tool.""" + return f""" + {run_funnel_report.__doc__} + + ## Hints for arguments + + Here are some hints that outline the expected format and requirements + for arguments. + + ### Hints for `funnel_breakdown` + + The `funnel_breakdown` parameter allows you to segment funnel results by a dimension: + ```json + {{ + "breakdown_dimension": "deviceCategory" + }} + ``` + Common breakdown dimensions include: + - `deviceCategory` - Desktop, Mobile, Tablet + - `country` - User's country + - `operatingSystem` - User's operating system + - `browser` - User's browser + + ### Hints for `funnel_next_action` + + The `funnel_next_action` parameter analyzes what users do after completing or dropping off from the funnel: + ```json + {{ + "next_action_dimension": "eventName", + "limit": 5 + }} + ``` + Common next action dimensions include: + - `eventName` - Next events users trigger + - `pagePath` - Next pages users visit + + ### Hints for `date_ranges`: + {get_date_ranges_hints()} + + ### Hints for `funnel_steps` + {get_funnel_steps_hints()} + + """ + + +async def run_funnel_report( + property_id: int | str, + funnel_steps: List[Dict[str, Any]], + date_ranges: List[Dict[str, str]] = None, + funnel_breakdown: Dict[str, str] = None, + funnel_next_action: Dict[str, str] = None, + segments: List[Dict[str, Any]] = None, + return_property_quota: bool = False, +) -> Dict[str, Any]: + """Run a Google Analytics Data API funnel report. + + See the funnel report guide at + https://developers.google.com/analytics/devguides/reporting/data/v1/funnels + for details and examples. + + Args: + property_id: The Google Analytics property ID. Accepted formats are: + - A number + - A string consisting of 'properties/' followed by a number + funnel_steps: A list of funnel steps. Each step should be a dictionary + containing: + - 'name': (str) Display name for the step + - 'filter_expression': (Dict) Complete filter expression for the step + OR for simple event-based steps: + - 'name': (str) Display name for the step + - 'event': (str) Event name to filter on + date_ranges: A list of date ranges + (https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/DateRange) + to include in the report. + funnel_breakdown: Optional breakdown dimension to segment the funnel. + This creates separate funnel results for each value of the dimension. + Example: {"breakdown_dimension": "deviceCategory"} + funnel_next_action: Optional next action analysis configuration. + This analyzes what users do after completing or dropping off from the funnel. + Example: {"next_action_dimension": "eventName", "limit": 5} + segments: Optional list of segments to apply to the funnel. + return_property_quota: Whether to return current property quota information. + + Returns: + Dict containing the funnel report response with funnel results including: + - funnel_table: Table showing progression through funnel steps + - funnel_visualization: Data for visualizing the funnel + - property_quota: (if requested) Current quota usage information + + Raises: + ValueError: If funnel_steps is empty or contains invalid configurations + Exception: If the API request fails + + + """ + + steps = [] + for i, step in enumerate(funnel_steps): + if not isinstance(step, dict): + raise ValueError(f"Step {i+1} must be a dictionary") + + step_name = step.get("name", f"Step {i+1}") + + if "filter_expression" in step: + filter_expr = data_v1alpha.FunnelFilterExpression( + step["filter_expression"] + ) + elif "event" in step: + filter_expr = data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name=step["event"] + ) + ) + else: + raise ValueError( + f"Step {i+1} must contain either 'filter_expression' or 'event' key" + ) + + funnel_step = data_v1alpha.FunnelStep( + name=step_name, filter_expression=filter_expr + ) + steps.append(funnel_step) + + request = data_v1alpha.RunFunnelReportRequest( + property=construct_property_rn(property_id), + funnel=data_v1alpha.Funnel(steps=steps), + date_ranges=[data_v1alpha.DateRange(dr) for dr in date_ranges], + return_property_quota=return_property_quota, + ) + + if funnel_breakdown and "breakdown_dimension" in funnel_breakdown: + request.funnel_breakdown = data_v1alpha.FunnelBreakdown( + breakdown_dimension=data_v1alpha.Dimension( + name=funnel_breakdown["breakdown_dimension"] + ) + ) + + if funnel_next_action and "next_action_dimension" in funnel_next_action: + next_action_config = data_v1alpha.FunnelNextAction( + next_action_dimension=data_v1alpha.Dimension( + name=funnel_next_action["next_action_dimension"] + ) + ) + if "limit" in funnel_next_action: + next_action_config.limit = funnel_next_action["limit"] + request.funnel_next_action = next_action_config + + if segments: + request.segments = [ + data_v1alpha.Segment(segment) for segment in segments + ] + + response = await create_data_api_alpha_client().run_funnel_report(request) + return proto_to_dict(response) + + +mcp.add_tool( + run_funnel_report, + title="Run a Google Analytics Data API funnel report using the Data API", + description=_run_funnel_report_description(), +) diff --git a/analytics_mcp/tools/reporting/metadata.py b/analytics_mcp/tools/reporting/metadata.py index 1fa7a63..1cf22c0 100644 --- a/analytics_mcp/tools/reporting/metadata.py +++ b/analytics_mcp/tools/reporting/metadata.py @@ -56,6 +56,173 @@ def get_date_ranges_hints(): """ +def get_funnel_steps_hints(): + """Returns hints and examples for funnel steps configuration.""" + from google.analytics import data_v1alpha + + step_first_open = data_v1alpha.FunnelStep( + name="First open/visit", + filter_expression=data_v1alpha.FunnelFilterExpression( + or_group=data_v1alpha.FunnelFilterExpressionList( + expressions=[ + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="first_open" + ) + ), + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="first_visit" + ) + ), + ] + ) + ), + ) + + step_organic_visitors = data_v1alpha.FunnelStep( + name="Organic visitors", + filter_expression=data_v1alpha.FunnelFilterExpression( + funnel_field_filter=data_v1alpha.FunnelFieldFilter( + field_name="firstUserMedium", + string_filter=data_v1alpha.StringFilter( + match_type=data_v1alpha.StringFilter.MatchType.CONTAINS, + case_sensitive=False, + value="organic", + ), + ) + ), + ) + + step_session_start = data_v1alpha.FunnelStep( + name="Session start", + filter_expression=data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="session_start" + ) + ), + ) + + step_page_view = data_v1alpha.FunnelStep( + name="Screen/Page view", + filter_expression=data_v1alpha.FunnelFilterExpression( + or_group=data_v1alpha.FunnelFilterExpressionList( + expressions=[ + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="screen_view" + ) + ), + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="page_view" + ) + ), + ] + ) + ), + ) + + step_purchase = data_v1alpha.FunnelStep( + name="Purchase", + filter_expression=data_v1alpha.FunnelFilterExpression( + or_group=data_v1alpha.FunnelFilterExpressionList( + expressions=[ + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="purchase" + ) + ), + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="in_app_purchase" + ) + ), + ] + ) + ), + ) + + step_add_to_cart_value = data_v1alpha.FunnelStep( + name="Add to cart (value > 50)", + filter_expression=data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="add_to_cart", + funnel_parameter_filter_expression=data_v1alpha.FunnelParameterFilterExpression( + funnel_parameter_filter=data_v1alpha.FunnelParameterFilter( + event_parameter_name="value", + numeric_filter=data_v1alpha.NumericFilter( + operation=data_v1alpha.NumericFilter.Operation.GREATER_THAN, + value=data_v1alpha.NumericValue(double_value=50.0), + ), + ) + ), + ) + ), + ) + + step_home_page_view = data_v1alpha.FunnelStep( + name="Home page view", + filter_expression=data_v1alpha.FunnelFilterExpression( + and_group=data_v1alpha.FunnelFilterExpressionList( + expressions=[ + data_v1alpha.FunnelFilterExpression( + funnel_event_filter=data_v1alpha.FunnelEventFilter( + event_name="page_view" + ) + ), + data_v1alpha.FunnelFilterExpression( + funnel_field_filter=data_v1alpha.FunnelFieldFilter( + field_name="pagePath", + string_filter=data_v1alpha.StringFilter( + match_type=data_v1alpha.StringFilter.MatchType.EXACT, + value="/", + ), + ) + ), + ] + ) + ), + ) + + return f"""Example funnel_steps configurations: + + 1. Simple event-based step (first open/visit): + {proto_to_json(step_first_open)} + + 2. Field filter for organic traffic: + {proto_to_json(step_organic_visitors)} + + 3. Simple event filter: + {proto_to_json(step_session_start)} + + 4. Multiple events with OR condition: + {proto_to_json(step_page_view)} + + 5. Purchase events (multiple event types): + {proto_to_json(step_purchase)} + + 6. Event with parameter filter (value > 50): + {proto_to_json(step_add_to_cart_value)} + + 7. Complex AND condition (page view + specific path): + {proto_to_json(step_home_page_view)} + + + ## Complete Funnel Example + + A typical e-commerce funnel with 5 steps: + [ + {proto_to_json(step_first_open)}, + {proto_to_json(step_organic_visitors)}, + {proto_to_json(step_session_start)}, + {proto_to_json(step_page_view)}, + {proto_to_json(step_purchase)} + ] + + """ + + # Common notes to consider when applying dimension and metric filters. _FILTER_NOTES = """ Notes: diff --git a/analytics_mcp/tools/utils.py b/analytics_mcp/tools/utils.py index 34eec86..ed4906a 100644 --- a/analytics_mcp/tools/utils.py +++ b/analytics_mcp/tools/utils.py @@ -16,7 +16,7 @@ from typing import Any, Dict -from google.analytics import admin_v1beta, data_v1beta +from google.analytics import admin_v1beta, data_v1beta, data_v1alpha from google.api_core.gapic_v1.client_info import ClientInfo from importlib import metadata import google.auth @@ -71,6 +71,18 @@ def create_data_api_client() -> data_v1beta.BetaAnalyticsDataAsyncClient: ) +def create_data_api_alpha_client() -> ( + data_v1alpha.AlphaAnalyticsDataAsyncClient +): + """Returns a properly configured Google Analytics Data API (Alpha) async client. + + Uses Application Default Credentials with read-only scope. + """ + return data_v1alpha.AlphaAnalyticsDataAsyncClient( + client_info=_CLIENT_INFO, credentials=_create_credentials() + ) + + def construct_property_rn(property_value: int | str) -> str: """Returns a property resource name in the format required by APIs.""" property_num = None