Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
272 changes: 271 additions & 1 deletion analytics_mcp/tools/reporting/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -79,6 +80,103 @@ def _run_report_description() -> str:
"""


def _run_funnel_report_description() -> str:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this file into a funnel.py file, similar to how we separate out runReport into core.py, run_realtime_report into realtime.py, and get_custom_dimensions_and_metrics, etc. into metadata.py?

"""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]],
Expand Down Expand Up @@ -173,6 +271,172 @@ 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Run a Google Analytics Data API funnel report.
"""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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yh data analysis programing method

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"
}
]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the JSON examples into separate methods that you call here, similar to what we're doing with date range hints, etc. for runReport:

This offers a few advantages:

  1. It keeps this docstring reasonably short.
  2. In the methods that return JSON, you can use the client library to generate the JSON so you know that it's well-formed. This will also make maintenance easier if the API objects evolve and change.

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
Expand All @@ -182,3 +446,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(),
)
14 changes: 13 additions & 1 deletion analytics_mcp/tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down