diff --git a/README.md b/README.md index 8c37b0b..1e43395 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. @@ -67,8 +68,8 @@ Setup involves the following steps: [Follow the instructions](https://support.google.com/googleapi/answer/6158841) to enable the following APIs in your Google Cloud project: -* [Google Analytics Admin API](https://console.cloud.google.com/apis/library/analyticsadmin.googleapis.com) -* [Google Analytics Data API](https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com) +- [Google Analytics Admin API](https://console.cloud.google.com/apis/library/analyticsadmin.googleapis.com) +- [Google Analytics Data API](https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com) ### Configure credentials 🔑 @@ -137,10 +138,7 @@ Credentials saved to file: [PATH_TO_CREDENTIALS_JSON] "mcpServers": { "analytics-mcp": { "command": "pipx", - "args": [ - "run", - "analytics-mcp" - ], + "args": ["run", "analytics-mcp"], "env": { "GOOGLE_APPLICATION_CREDENTIALS": "PATH_TO_CREDENTIALS_JSON", "GOOGLE_PROJECT_ID": "YOUR_PROJECT_ID" diff --git a/analytics_mcp/coordinator.py b/analytics_mcp/coordinator.py index 7d5274b..458f8fb 100644 --- a/analytics_mcp/coordinator.py +++ b/analytics_mcp/coordinator.py @@ -45,6 +45,10 @@ from analytics_mcp.tools.reporting.metadata import ( get_custom_dimensions_and_metrics, ) +from analytics_mcp.tools.reporting.funnel import ( + run_funnel_report, + _run_funnel_report_description, +) run_report_with_description = FunctionTool(run_report) run_report_with_description.description = _run_report_description() @@ -52,6 +56,10 @@ run_realtime_report_with_description.description = ( _run_realtime_report_description() ) +run_funnel_report_with_description = FunctionTool(run_funnel_report) +run_funnel_report_with_description.description = ( + _run_funnel_report_description() +) # Instantiate the ADK tools tools = [ @@ -62,6 +70,7 @@ FunctionTool(get_custom_dimensions_and_metrics), run_report_with_description, run_realtime_report_with_description, + run_funnel_report_with_description, ] tool_map = {t.name: t for t in tools} diff --git a/analytics_mcp/tools/reporting/funnel.py b/analytics_mcp/tools/reporting/funnel.py new file mode 100644 index 0000000..e33fbae --- /dev/null +++ b/analytics_mcp/tools/reporting/funnel.py @@ -0,0 +1,195 @@ +# Copyright 2025 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for running funnel reports using the Data API (Alpha).""" + +from typing import Any, Dict, List + +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 `segments` + + The `segments` parameter allows you to segment funnel results by user criteria. + Each segment is a dictionary passed directly to `data_v1alpha.Segment()`. + See https://developers.google.com/analytics/devguides/reporting/data/v1/funnels#segments + for details and examples. + + ### 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 + """ + if not funnel_steps: + raise ValueError("funnel_steps must contain at least one step") + + 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 or [])], + 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) diff --git a/analytics_mcp/tools/reporting/metadata.py b/analytics_mcp/tools/reporting/metadata.py index 22f5e13..2568953 100644 --- a/analytics_mcp/tools/reporting/metadata.py +++ b/analytics_mcp/tools/reporting/metadata.py @@ -22,7 +22,7 @@ proto_to_dict, proto_to_json, ) -from google.analytics import data_v1beta +from google.analytics import data_v1alpha, data_v1beta def get_date_ranges_hints(): @@ -55,6 +55,171 @@ def get_date_ranges_hints(): """ +def get_funnel_steps_hints(): + """Returns hints and examples for funnel steps configuration.""" + 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="pageLocation", + string_filter=data_v1alpha.StringFilter( + match_type=data_v1alpha.StringFilter.MatchType.CONTAINS, + 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 0f35040..fe124eb 100644 --- a/analytics_mcp/tools/utils.py +++ b/analytics_mcp/tools/utils.py @@ -16,7 +16,12 @@ from typing import Any, Dict -from google.analytics import admin_v1beta, data_v1beta, admin_v1alpha +from google.analytics import ( + admin_v1beta, + data_v1beta, + admin_v1alpha, + data_v1alpha, +) from google.api_core.gapic_v1.client_info import ClientInfo from importlib import metadata import google.auth @@ -82,6 +87,18 @@ def create_admin_alpha_api_client() -> ( ) +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