From 75e9d7907915b571ec625bd96cd73083c9cefcec Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Tue, 26 Aug 2025 15:28:27 +0530 Subject: [PATCH 1/7] feat: added funnel report generation --- analytics_mcp/tools/reporting/core.py | 329 +++++++++++++++++++++++++- analytics_mcp/tools/utils.py | 15 +- 2 files changed, 340 insertions(+), 4 deletions(-) diff --git a/analytics_mcp/tools/reporting/core.py b/analytics_mcp/tools/reporting/core.py index 881fb4f..0c92a9b 100644 --- a/analytics_mcp/tools/reporting/core.py +++ b/analytics_mcp/tools/reporting/core.py @@ -14,7 +14,7 @@ """Tools for running core reports using the Data API.""" -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from analytics_mcp.coordinator import mcp from analytics_mcp.tools.reporting.metadata import ( @@ -26,9 +26,10 @@ from analytics_mcp.tools.utils import ( construct_property_rn, create_data_api_client, + create_data_api_alpha_client, proto_to_dict, ) -from google.analytics import data_v1beta +from google.analytics import data_v1beta, data_v1alpha def _run_report_description() -> str: @@ -79,6 +80,112 @@ def _run_report_description() -> str: """ +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_steps` + + The `funnel_steps` list must contain at least 2 steps. Each step can be configured in two ways: + + 1. **Simple Event-Based Steps**: For basic event filtering + ```json + {{ + "name": "Step Name", + "event": "event_name" + }} + ``` + + 2. **Advanced Filter Expression Steps**: For complex filtering with multiple conditions + ```json + {{ + "name": "Step Name", + "filter_expression": {{ + "funnel_field_filter": {{ + "field_name": "eventName", + "string_filter": {{ + "match_type": "EXACT", + "value": "page_view" + }} + }} + }} + }} + ``` + + For page path filtering, use: + ```json + {{ + "name": "Home Page View", + "filter_expression": {{ + "and_group": {{ + "expressions": [ + {{ + "funnel_field_filter": {{ + "field_name": "eventName", + "string_filter": {{"match_type": "EXACT", "value": "page_view"}} + }} + }}, + {{ + "funnel_field_filter": {{ + "field_name": "pagePath", + "string_filter": {{"match_type": "EXACT", "value": "/"}} + }} + }} + ] + }} + }} + }} + ``` + + ### Hints for `date_ranges`: + {get_date_ranges_hints()} + + ### 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 + + ### Important Notes + + - The runFunnelReport method is currently in **alpha** status and may change + - Funnel reports require at least 2 steps to be meaningful + - Use `return_property_quota: true` to monitor your API usage + - For complex filtering, prefer `filter_expression` over simple `event` filters + - Date ranges support relative dates like "7daysAgo", "today", "yesterday" + + """ + + async def run_report( property_id: int | str, date_ranges: List[Dict[str, str]], @@ -172,6 +279,218 @@ async def run_report( return proto_to_dict(response) +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 using the v1alpha API. + + The runFunnelReport method is currently in alpha and allows you to create + funnel reports showing how users progress through a sequence of steps. + + 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 + Example: + [ + { + "name": "Page View", + "filter_expression": { + "funnel_field_filter": { + "field_name": "eventName", + "string_filter": { + "match_type": "EXACT", + "value": "page_view" + } + } + } + }, + { + "name": "Sign Up", + "event": "sign_up" + } + ] + date_ranges: A list of date ranges. If not provided, defaults to last 30 days. + Each date range should have 'start_date' and 'end_date' keys. + Example: [{"start_date": "2024-01-01", "end_date": "2024-01-31"}] + 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 + + Example: + # Simple event-based funnel + result = await run_funnel_report( + property_id="123456789", + funnel_steps=[ + {"name": "Landing Page", "event": "page_view"}, + {"name": "Add to Cart", "event": "add_to_cart"}, + {"name": "Purchase", "event": "purchase"} + ] + ) + + # Advanced funnel with page path filtering + result = await run_funnel_report( + property_id="123456789", + funnel_steps=[ + { + "name": "Home Page View", + "filter_expression": { + "and_group": { + "expressions": [ + { + "funnel_field_filter": { + "field_name": "eventName", + "string_filter": {"match_type": "EXACT", "value": "page_view"} + } + }, + { + "funnel_field_filter": { + "field_name": "pagePath", + "string_filter": {"match_type": "EXACT", "value": "/"} + } + } + ] + } + } + }, + {"name": "Purchase", "event": "purchase"} + ], + funnel_breakdown={"breakdown_dimension": "deviceCategory"}, + date_ranges=[{"start_date": "7daysAgo", "end_date": "today"}] + ) + """ + # Validate inputs + if not funnel_steps: + raise ValueError("funnel_steps cannot be empty") + + if len(funnel_steps) < 2: + raise ValueError("funnel_steps must contain at least 2 steps") + + # Set default date range if not provided + if not date_ranges: + date_ranges = [{"start_date": "30daysAgo", "end_date": "today"}] + + # Validate and create funnel steps + 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}') + + # Build filter expression + if 'filter_expression' in step: + # Use provided filter expression + filter_expr = data_v1alpha.FunnelFilterExpression(step['filter_expression']) + elif 'event' in step: + # Simple event-based filter + 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) + + # Create the funnel configuration + funnel_config = data_v1alpha.Funnel(steps=steps) + + # Create date ranges + date_range_objects = [] + for dr in date_ranges: + if not isinstance(dr, dict) or 'start_date' not in dr or 'end_date' not in dr: + raise ValueError( + "Each date range must be a dictionary with 'start_date' and 'end_date' keys" + ) + date_range_objects.append( + data_v1alpha.DateRange(start_date=dr['start_date'], end_date=dr['end_date']) + ) + + # Create the request + request = data_v1alpha.RunFunnelReportRequest( + property=construct_property_rn(property_id), + funnel=funnel_config, + date_ranges=date_range_objects, + return_property_quota=return_property_quota + ) + + # Add breakdown if specified (this goes on the request, not the funnel) + 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'] + ) + ) + + # Add next action if specified (this also goes on the request, not the funnel) + 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 + + # Add segments if provided + if segments: + request.segments = [data_v1alpha.Segment(segment) for segment in segments] + + # Execute the request with enhanced error handling + try: + client = create_data_api_alpha_client() + response = await client.run_funnel_report(request=request) + return proto_to_dict(response) + except Exception as e: + error_msg = str(e) + if "INVALID_ARGUMENT" in error_msg: + raise ValueError(f"Invalid funnel configuration: {error_msg}") + elif "PERMISSION_DENIED" in error_msg: + raise PermissionError(f"Permission denied accessing property: {error_msg}") + elif "NOT_FOUND" in error_msg: + raise ValueError(f"Property not found: {error_msg}") + elif "QUOTA_EXCEEDED" in error_msg: + raise RuntimeError(f"API quota exceeded: {error_msg}") + else: + raise Exception(f"Failed to run funnel report: {error_msg}") + # The `run_report` tool requires a more complex description that's generated at # runtime. Uses the `add_tool` method instead of an annnotation since `add_tool` @@ -182,3 +501,9 @@ async def run_report( title="Run a Google Analytics Data API report using the Data API", description=_run_report_description(), ) + +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/utils.py b/analytics_mcp/tools/utils.py index 34eec86..e721c70 100644 --- a/analytics_mcp/tools/utils.py +++ b/analytics_mcp/tools/utils.py @@ -16,10 +16,12 @@ 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 +from google.oauth2.credentials import Credentials as OAuth2Credentials +from google.auth.credentials import Credentials import proto @@ -44,7 +46,6 @@ def _get_package_version_with_fallback(): "https://www.googleapis.com/auth/analytics.readonly" ) - def _create_credentials() -> google.auth.credentials.Credentials: """Returns Application Default Credentials with read-only scope.""" (credentials, _) = google.auth.default(scopes=[_READ_ONLY_ANALYTICS_SCOPE]) @@ -71,6 +72,16 @@ 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 From d785907542c7f68eb8ddca20f06205b5b06c8e50 Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Thu, 28 Aug 2025 14:20:52 +0530 Subject: [PATCH 2/7] chore: improved prompt --- analytics_mcp/tools/reporting/core.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/analytics_mcp/tools/reporting/core.py b/analytics_mcp/tools/reporting/core.py index 0c92a9b..4c895b4 100644 --- a/analytics_mcp/tools/reporting/core.py +++ b/analytics_mcp/tools/reporting/core.py @@ -174,15 +174,6 @@ def _run_funnel_report_description() -> str: Common next action dimensions include: - `eventName` - Next events users trigger - `pagePath` - Next pages users visit - - ### Important Notes - - - The runFunnelReport method is currently in **alpha** status and may change - - Funnel reports require at least 2 steps to be meaningful - - Use `return_property_quota: true` to monitor your API usage - - For complex filtering, prefer `filter_expression` over simple `event` filters - - Date ranges support relative dates like "7daysAgo", "today", "yesterday" - """ @@ -288,11 +279,8 @@ async def run_funnel_report( segments: List[Dict[str, Any]] = None, return_property_quota: bool = False, ) -> Dict[str, Any]: - """Run a Google Analytics Data API funnel report using the v1alpha API. - - The runFunnelReport method is currently in alpha and allows you to create - funnel reports showing how users progress through a sequence of steps. - + """Run a Google Analytics Data API funnel report. + Args: property_id: The Google Analytics property ID. Accepted formats are: - A number @@ -323,9 +311,9 @@ async def run_funnel_report( "event": "sign_up" } ] - date_ranges: A list of date ranges. If not provided, defaults to last 30 days. - Each date range should have 'start_date' and 'end_date' keys. - Example: [{"start_date": "2024-01-01", "end_date": "2024-01-31"}] + 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"} From 86fb75664de49a99dfbf487c8bbb375892673105 Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Tue, 2 Sep 2025 12:19:14 +0530 Subject: [PATCH 3/7] chore: Linting --- analytics_mcp/tools/reporting/core.py | 115 ++++++++------------------ analytics_mcp/tools/utils.py | 9 +- 2 files changed, 42 insertions(+), 82 deletions(-) diff --git a/analytics_mcp/tools/reporting/core.py b/analytics_mcp/tools/reporting/core.py index 4c895b4..0c2a619 100644 --- a/analytics_mcp/tools/reporting/core.py +++ b/analytics_mcp/tools/reporting/core.py @@ -270,6 +270,7 @@ async def run_report( return proto_to_dict(response) + async def run_funnel_report( property_id: int | str, funnel_steps: List[Dict[str, Any]], @@ -290,7 +291,7 @@ async def run_funnel_report( - '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 + - 'name': (str) Display name for the step - 'event': (str) Event name to filter on Example: [ @@ -322,17 +323,17 @@ async def run_funnel_report( 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 - + Example: # Simple event-based funnel result = await run_funnel_report( @@ -343,7 +344,7 @@ async def run_funnel_report( {"name": "Purchase", "event": "purchase"} ] ) - + # Advanced funnel with page path filtering result = await run_funnel_report( property_id="123456789", @@ -375,109 +376,65 @@ async def run_funnel_report( date_ranges=[{"start_date": "7daysAgo", "end_date": "today"}] ) """ - # Validate inputs - if not funnel_steps: - raise ValueError("funnel_steps cannot be empty") - - if len(funnel_steps) < 2: - raise ValueError("funnel_steps must contain at least 2 steps") - - # Set default date range if not provided - if not date_ranges: - date_ranges = [{"start_date": "30daysAgo", "end_date": "today"}] - - # Validate and create funnel steps + 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}') - - # Build filter expression - if 'filter_expression' in step: - # Use provided filter expression - filter_expr = data_v1alpha.FunnelFilterExpression(step['filter_expression']) - elif 'event' in step: - # Simple event-based filter + + 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'] + 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 + name=step_name, filter_expression=filter_expr ) steps.append(funnel_step) - - # Create the funnel configuration - funnel_config = data_v1alpha.Funnel(steps=steps) - - # Create date ranges - date_range_objects = [] - for dr in date_ranges: - if not isinstance(dr, dict) or 'start_date' not in dr or 'end_date' not in dr: - raise ValueError( - "Each date range must be a dictionary with 'start_date' and 'end_date' keys" - ) - date_range_objects.append( - data_v1alpha.DateRange(start_date=dr['start_date'], end_date=dr['end_date']) - ) - # Create the request request = data_v1alpha.RunFunnelReportRequest( property=construct_property_rn(property_id), - funnel=funnel_config, - date_ranges=date_range_objects, - return_property_quota=return_property_quota + funnel=data_v1alpha.Funnel(steps=steps), + date_ranges=[data_v1alpha.DateRange(dr) for dr in date_ranges], + return_property_quota=return_property_quota, ) - - # Add breakdown if specified (this goes on the request, not the funnel) - if funnel_breakdown and 'breakdown_dimension' in funnel_breakdown: + + 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'] + name=funnel_breakdown["breakdown_dimension"] ) ) - - # Add next action if specified (this also goes on the request, not the funnel) - if funnel_next_action and 'next_action_dimension' in funnel_next_action: + + 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'] + name=funnel_next_action["next_action_dimension"] ) ) - if 'limit' in funnel_next_action: - next_action_config.limit = funnel_next_action['limit'] + if "limit" in funnel_next_action: + next_action_config.limit = funnel_next_action["limit"] request.funnel_next_action = next_action_config - - # Add segments if provided + if segments: - request.segments = [data_v1alpha.Segment(segment) for segment in segments] - - # Execute the request with enhanced error handling - try: - client = create_data_api_alpha_client() - response = await client.run_funnel_report(request=request) - return proto_to_dict(response) - except Exception as e: - error_msg = str(e) - if "INVALID_ARGUMENT" in error_msg: - raise ValueError(f"Invalid funnel configuration: {error_msg}") - elif "PERMISSION_DENIED" in error_msg: - raise PermissionError(f"Permission denied accessing property: {error_msg}") - elif "NOT_FOUND" in error_msg: - raise ValueError(f"Property not found: {error_msg}") - elif "QUOTA_EXCEEDED" in error_msg: - raise RuntimeError(f"API quota exceeded: {error_msg}") - else: - raise Exception(f"Failed to run funnel report: {error_msg}") + 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) # The `run_report` tool requires a more complex description that's generated at diff --git a/analytics_mcp/tools/utils.py b/analytics_mcp/tools/utils.py index e721c70..945e65b 100644 --- a/analytics_mcp/tools/utils.py +++ b/analytics_mcp/tools/utils.py @@ -46,6 +46,7 @@ def _get_package_version_with_fallback(): "https://www.googleapis.com/auth/analytics.readonly" ) + def _create_credentials() -> google.auth.credentials.Credentials: """Returns Application Default Credentials with read-only scope.""" (credentials, _) = google.auth.default(scopes=[_READ_ONLY_ANALYTICS_SCOPE]) @@ -72,14 +73,16 @@ def create_data_api_client() -> data_v1beta.BetaAnalyticsDataAsyncClient: ) -def create_data_api_alpha_client() -> data_v1alpha.AlphaAnalyticsDataAsyncClient: +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: From 00070d7239d630ad8fc1df2b83bf34590f79104f Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Tue, 2 Sep 2025 12:27:31 +0530 Subject: [PATCH 4/7] docs: update README to add run_funnel_report tool --- README.md | 1 + 1 file changed, 1 insertion(+) 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. From 0db31925d951552d3022806f6eb7cf1615568603 Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Tue, 2 Sep 2025 12:51:18 +0530 Subject: [PATCH 5/7] chore: cleanup --- analytics_mcp/tools/reporting/core.py | 2 +- analytics_mcp/tools/utils.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/analytics_mcp/tools/reporting/core.py b/analytics_mcp/tools/reporting/core.py index 0c2a619..2db2faa 100644 --- a/analytics_mcp/tools/reporting/core.py +++ b/analytics_mcp/tools/reporting/core.py @@ -14,7 +14,7 @@ """Tools for running core reports using the Data API.""" -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from analytics_mcp.coordinator import mcp from analytics_mcp.tools.reporting.metadata import ( diff --git a/analytics_mcp/tools/utils.py b/analytics_mcp/tools/utils.py index 945e65b..ed4906a 100644 --- a/analytics_mcp/tools/utils.py +++ b/analytics_mcp/tools/utils.py @@ -20,8 +20,6 @@ from google.api_core.gapic_v1.client_info import ClientInfo from importlib import metadata import google.auth -from google.oauth2.credentials import Credentials as OAuth2Credentials -from google.auth.credentials import Credentials import proto From d9080f661df06cc5b62b33f02919ec624a765fb8 Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Sun, 5 Oct 2025 12:33:04 +0530 Subject: [PATCH 6/7] chore: create funnels.py, move funnel hints into separate method --- analytics_mcp/server.py | 1 + analytics_mcp/tools/reporting/core.py | 272 +--------------------- analytics_mcp/tools/reporting/funnel.py | 177 ++++++++++++++ analytics_mcp/tools/reporting/metadata.py | 167 +++++++++++++ 4 files changed, 346 insertions(+), 271 deletions(-) create mode 100644 analytics_mcp/tools/reporting/funnel.py 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/core.py b/analytics_mcp/tools/reporting/core.py index 2db2faa..881fb4f 100644 --- a/analytics_mcp/tools/reporting/core.py +++ b/analytics_mcp/tools/reporting/core.py @@ -26,10 +26,9 @@ from analytics_mcp.tools.utils import ( construct_property_rn, create_data_api_client, - create_data_api_alpha_client, proto_to_dict, ) -from google.analytics import data_v1beta, data_v1alpha +from google.analytics import data_v1beta def _run_report_description() -> str: @@ -80,103 +79,6 @@ def _run_report_description() -> str: """ -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_steps` - - The `funnel_steps` list must contain at least 2 steps. Each step can be configured in two ways: - - 1. **Simple Event-Based Steps**: For basic event filtering - ```json - {{ - "name": "Step Name", - "event": "event_name" - }} - ``` - - 2. **Advanced Filter Expression Steps**: For complex filtering with multiple conditions - ```json - {{ - "name": "Step Name", - "filter_expression": {{ - "funnel_field_filter": {{ - "field_name": "eventName", - "string_filter": {{ - "match_type": "EXACT", - "value": "page_view" - }} - }} - }} - }} - ``` - - For page path filtering, use: - ```json - {{ - "name": "Home Page View", - "filter_expression": {{ - "and_group": {{ - "expressions": [ - {{ - "funnel_field_filter": {{ - "field_name": "eventName", - "string_filter": {{"match_type": "EXACT", "value": "page_view"}} - }} - }}, - {{ - "funnel_field_filter": {{ - "field_name": "pagePath", - "string_filter": {{"match_type": "EXACT", "value": "/"}} - }} - }} - ] - }} - }} - }} - ``` - - ### Hints for `date_ranges`: - {get_date_ranges_hints()} - - ### 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 - """ - - async def run_report( property_id: int | str, date_ranges: List[Dict[str, str]], @@ -271,172 +173,6 @@ async def run_report( return proto_to_dict(response) -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. - - 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 - Example: - [ - { - "name": "Page View", - "filter_expression": { - "funnel_field_filter": { - "field_name": "eventName", - "string_filter": { - "match_type": "EXACT", - "value": "page_view" - } - } - } - }, - { - "name": "Sign Up", - "event": "sign_up" - } - ] - 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 - - Example: - # Simple event-based funnel - result = await run_funnel_report( - property_id="123456789", - funnel_steps=[ - {"name": "Landing Page", "event": "page_view"}, - {"name": "Add to Cart", "event": "add_to_cart"}, - {"name": "Purchase", "event": "purchase"} - ] - ) - - # Advanced funnel with page path filtering - result = await run_funnel_report( - property_id="123456789", - funnel_steps=[ - { - "name": "Home Page View", - "filter_expression": { - "and_group": { - "expressions": [ - { - "funnel_field_filter": { - "field_name": "eventName", - "string_filter": {"match_type": "EXACT", "value": "page_view"} - } - }, - { - "funnel_field_filter": { - "field_name": "pagePath", - "string_filter": {"match_type": "EXACT", "value": "/"} - } - } - ] - } - } - }, - {"name": "Purchase", "event": "purchase"} - ], - funnel_breakdown={"breakdown_dimension": "deviceCategory"}, - date_ranges=[{"start_date": "7daysAgo", "end_date": "today"}] - ) - """ - - 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) - - # The `run_report` tool requires a more complex description that's generated at # runtime. Uses the `add_tool` method instead of an annnotation since `add_tool` # provides the flexibility needed to generate the description while also @@ -446,9 +182,3 @@ async def run_funnel_report( title="Run a Google Analytics Data API report using the Data API", description=_run_report_description(), ) - -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/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..c371b0f 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( + 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: From 5c6df72eb0cf9f877fe334dc3608ea0ddcf154a4 Mon Sep 17 00:00:00 2001 From: cagnusmarlsen <0arunabh30@gmail.com> Date: Tue, 7 Oct 2025 09:11:12 +0530 Subject: [PATCH 7/7] fix: event_parameter_name instead of parameter_name --- analytics_mcp/tools/reporting/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics_mcp/tools/reporting/metadata.py b/analytics_mcp/tools/reporting/metadata.py index c371b0f..1cf22c0 100644 --- a/analytics_mcp/tools/reporting/metadata.py +++ b/analytics_mcp/tools/reporting/metadata.py @@ -150,7 +150,7 @@ def get_funnel_steps_hints(): event_name="add_to_cart", funnel_parameter_filter_expression=data_v1alpha.FunnelParameterFilterExpression( funnel_parameter_filter=data_v1alpha.FunnelParameterFilter( - parameter_name="value", + event_parameter_name="value", numeric_filter=data_v1alpha.NumericFilter( operation=data_v1alpha.NumericFilter.Operation.GREATER_THAN, value=data_v1alpha.NumericValue(double_value=50.0),