Skip to content

Commit 6e90ca7

Browse files
author
Arunabh Banerjee
committed
feat: added funnel report generation
1 parent 69ae860 commit 6e90ca7

2 files changed

Lines changed: 340 additions & 4 deletions

File tree

analytics_mcp/tools/reporting/core.py

Lines changed: 327 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
"""Tools for running core reports using the Data API."""
1616

17-
from typing import Any, Dict, List
17+
from typing import Any, Dict, List, Optional
1818

1919
from analytics_mcp.coordinator import mcp
2020
from analytics_mcp.tools.reporting.metadata import (
@@ -26,9 +26,10 @@
2626
from analytics_mcp.tools.utils import (
2727
construct_property_rn,
2828
create_data_api_client,
29+
create_data_api_alpha_client,
2930
proto_to_dict,
3031
)
31-
from google.analytics import data_v1beta
32+
from google.analytics import data_v1beta, data_v1alpha
3233

3334

3435
def _run_report_description() -> str:
@@ -79,6 +80,112 @@ def _run_report_description() -> str:
7980
"""
8081

8182

83+
def _run_funnel_report_description() -> str:
84+
"""Returns the description for the `run_funnel_report` tool."""
85+
return f"""
86+
{run_funnel_report.__doc__}
87+
88+
## Hints for arguments
89+
90+
Here are some hints that outline the expected format and requirements
91+
for arguments.
92+
93+
### Hints for `funnel_steps`
94+
95+
The `funnel_steps` list must contain at least 2 steps. Each step can be configured in two ways:
96+
97+
1. **Simple Event-Based Steps**: For basic event filtering
98+
```json
99+
{{
100+
"name": "Step Name",
101+
"event": "event_name"
102+
}}
103+
```
104+
105+
2. **Advanced Filter Expression Steps**: For complex filtering with multiple conditions
106+
```json
107+
{{
108+
"name": "Step Name",
109+
"filter_expression": {{
110+
"funnel_field_filter": {{
111+
"field_name": "eventName",
112+
"string_filter": {{
113+
"match_type": "EXACT",
114+
"value": "page_view"
115+
}}
116+
}}
117+
}}
118+
}}
119+
```
120+
121+
For page path filtering, use:
122+
```json
123+
{{
124+
"name": "Home Page View",
125+
"filter_expression": {{
126+
"and_group": {{
127+
"expressions": [
128+
{{
129+
"funnel_field_filter": {{
130+
"field_name": "eventName",
131+
"string_filter": {{"match_type": "EXACT", "value": "page_view"}}
132+
}}
133+
}},
134+
{{
135+
"funnel_field_filter": {{
136+
"field_name": "pagePath",
137+
"string_filter": {{"match_type": "EXACT", "value": "/"}}
138+
}}
139+
}}
140+
]
141+
}}
142+
}}
143+
}}
144+
```
145+
146+
### Hints for `date_ranges`:
147+
{get_date_ranges_hints()}
148+
149+
### Hints for `funnel_breakdown`
150+
151+
The `funnel_breakdown` parameter allows you to segment funnel results by a dimension:
152+
```json
153+
{{
154+
"breakdown_dimension": "deviceCategory"
155+
}}
156+
```
157+
158+
Common breakdown dimensions include:
159+
- `deviceCategory` - Desktop, Mobile, Tablet
160+
- `country` - User's country
161+
- `operatingSystem` - User's operating system
162+
- `browser` - User's browser
163+
164+
### Hints for `funnel_next_action`
165+
166+
The `funnel_next_action` parameter analyzes what users do after completing or dropping off from the funnel:
167+
```json
168+
{{
169+
"next_action_dimension": "eventName",
170+
"limit": 5
171+
}}
172+
```
173+
174+
Common next action dimensions include:
175+
- `eventName` - Next events users trigger
176+
- `pagePath` - Next pages users visit
177+
178+
### Important Notes
179+
180+
- The runFunnelReport method is currently in **alpha** status and may change
181+
- Funnel reports require at least 2 steps to be meaningful
182+
- Use `return_property_quota: true` to monitor your API usage
183+
- For complex filtering, prefer `filter_expression` over simple `event` filters
184+
- Date ranges support relative dates like "7daysAgo", "today", "yesterday"
185+
186+
"""
187+
188+
82189
async def run_report(
83190
property_id: int | str,
84191
date_ranges: List[Dict[str, str]],
@@ -172,6 +279,218 @@ async def run_report(
172279

173280
return proto_to_dict(response)
174281

282+
async def run_funnel_report(
283+
property_id: int | str,
284+
funnel_steps: List[Dict[str, Any]],
285+
date_ranges: List[Dict[str, str]] = None,
286+
funnel_breakdown: Dict[str, str] = None,
287+
funnel_next_action: Dict[str, str] = None,
288+
segments: List[Dict[str, Any]] = None,
289+
return_property_quota: bool = False,
290+
) -> Dict[str, Any]:
291+
"""Run a Google Analytics Data API funnel report using the v1alpha API.
292+
293+
The runFunnelReport method is currently in alpha and allows you to create
294+
funnel reports showing how users progress through a sequence of steps.
295+
296+
Args:
297+
property_id: The Google Analytics property ID. Accepted formats are:
298+
- A number
299+
- A string consisting of 'properties/' followed by a number
300+
funnel_steps: A list of funnel steps. Each step should be a dictionary
301+
containing:
302+
- 'name': (str) Display name for the step
303+
- 'filter_expression': (Dict) Complete filter expression for the step
304+
OR for simple event-based steps:
305+
- 'name': (str) Display name for the step
306+
- 'event': (str) Event name to filter on
307+
Example:
308+
[
309+
{
310+
"name": "Page View",
311+
"filter_expression": {
312+
"funnel_field_filter": {
313+
"field_name": "eventName",
314+
"string_filter": {
315+
"match_type": "EXACT",
316+
"value": "page_view"
317+
}
318+
}
319+
}
320+
},
321+
{
322+
"name": "Sign Up",
323+
"event": "sign_up"
324+
}
325+
]
326+
date_ranges: A list of date ranges. If not provided, defaults to last 30 days.
327+
Each date range should have 'start_date' and 'end_date' keys.
328+
Example: [{"start_date": "2024-01-01", "end_date": "2024-01-31"}]
329+
funnel_breakdown: Optional breakdown dimension to segment the funnel.
330+
This creates separate funnel results for each value of the dimension.
331+
Example: {"breakdown_dimension": "deviceCategory"}
332+
funnel_next_action: Optional next action analysis configuration.
333+
This analyzes what users do after completing or dropping off from the funnel.
334+
Example: {"next_action_dimension": "eventName", "limit": 5}
335+
segments: Optional list of segments to apply to the funnel.
336+
return_property_quota: Whether to return current property quota information.
337+
338+
Returns:
339+
Dict containing the funnel report response with funnel results including:
340+
- funnel_table: Table showing progression through funnel steps
341+
- funnel_visualization: Data for visualizing the funnel
342+
- property_quota: (if requested) Current quota usage information
343+
344+
Raises:
345+
ValueError: If funnel_steps is empty or contains invalid configurations
346+
Exception: If the API request fails
347+
348+
Example:
349+
# Simple event-based funnel
350+
result = await run_funnel_report(
351+
property_id="123456789",
352+
funnel_steps=[
353+
{"name": "Landing Page", "event": "page_view"},
354+
{"name": "Add to Cart", "event": "add_to_cart"},
355+
{"name": "Purchase", "event": "purchase"}
356+
]
357+
)
358+
359+
# Advanced funnel with page path filtering
360+
result = await run_funnel_report(
361+
property_id="123456789",
362+
funnel_steps=[
363+
{
364+
"name": "Home Page View",
365+
"filter_expression": {
366+
"and_group": {
367+
"expressions": [
368+
{
369+
"funnel_field_filter": {
370+
"field_name": "eventName",
371+
"string_filter": {"match_type": "EXACT", "value": "page_view"}
372+
}
373+
},
374+
{
375+
"funnel_field_filter": {
376+
"field_name": "pagePath",
377+
"string_filter": {"match_type": "EXACT", "value": "/"}
378+
}
379+
}
380+
]
381+
}
382+
}
383+
},
384+
{"name": "Purchase", "event": "purchase"}
385+
],
386+
funnel_breakdown={"breakdown_dimension": "deviceCategory"},
387+
date_ranges=[{"start_date": "7daysAgo", "end_date": "today"}]
388+
)
389+
"""
390+
# Validate inputs
391+
if not funnel_steps:
392+
raise ValueError("funnel_steps cannot be empty")
393+
394+
if len(funnel_steps) < 2:
395+
raise ValueError("funnel_steps must contain at least 2 steps")
396+
397+
# Set default date range if not provided
398+
if not date_ranges:
399+
date_ranges = [{"start_date": "30daysAgo", "end_date": "today"}]
400+
401+
# Validate and create funnel steps
402+
steps = []
403+
for i, step in enumerate(funnel_steps):
404+
if not isinstance(step, dict):
405+
raise ValueError(f"Step {i+1} must be a dictionary")
406+
407+
step_name = step.get('name', f'Step {i+1}')
408+
409+
# Build filter expression
410+
if 'filter_expression' in step:
411+
# Use provided filter expression
412+
filter_expr = data_v1alpha.FunnelFilterExpression(step['filter_expression'])
413+
elif 'event' in step:
414+
# Simple event-based filter
415+
filter_expr = data_v1alpha.FunnelFilterExpression(
416+
funnel_event_filter=data_v1alpha.FunnelEventFilter(
417+
event_name=step['event']
418+
)
419+
)
420+
else:
421+
raise ValueError(
422+
f"Step {i+1} must contain either 'filter_expression' or 'event' key"
423+
)
424+
425+
funnel_step = data_v1alpha.FunnelStep(
426+
name=step_name,
427+
filter_expression=filter_expr
428+
)
429+
steps.append(funnel_step)
430+
431+
# Create the funnel configuration
432+
funnel_config = data_v1alpha.Funnel(steps=steps)
433+
434+
# Create date ranges
435+
date_range_objects = []
436+
for dr in date_ranges:
437+
if not isinstance(dr, dict) or 'start_date' not in dr or 'end_date' not in dr:
438+
raise ValueError(
439+
"Each date range must be a dictionary with 'start_date' and 'end_date' keys"
440+
)
441+
date_range_objects.append(
442+
data_v1alpha.DateRange(start_date=dr['start_date'], end_date=dr['end_date'])
443+
)
444+
445+
# Create the request
446+
request = data_v1alpha.RunFunnelReportRequest(
447+
property=construct_property_rn(property_id),
448+
funnel=funnel_config,
449+
date_ranges=date_range_objects,
450+
return_property_quota=return_property_quota
451+
)
452+
453+
# Add breakdown if specified (this goes on the request, not the funnel)
454+
if funnel_breakdown and 'breakdown_dimension' in funnel_breakdown:
455+
request.funnel_breakdown = data_v1alpha.FunnelBreakdown(
456+
breakdown_dimension=data_v1alpha.Dimension(
457+
name=funnel_breakdown['breakdown_dimension']
458+
)
459+
)
460+
461+
# Add next action if specified (this also goes on the request, not the funnel)
462+
if funnel_next_action and 'next_action_dimension' in funnel_next_action:
463+
next_action_config = data_v1alpha.FunnelNextAction(
464+
next_action_dimension=data_v1alpha.Dimension(
465+
name=funnel_next_action['next_action_dimension']
466+
)
467+
)
468+
if 'limit' in funnel_next_action:
469+
next_action_config.limit = funnel_next_action['limit']
470+
request.funnel_next_action = next_action_config
471+
472+
# Add segments if provided
473+
if segments:
474+
request.segments = [data_v1alpha.Segment(segment) for segment in segments]
475+
476+
# Execute the request with enhanced error handling
477+
try:
478+
client = create_data_api_alpha_client()
479+
response = await client.run_funnel_report(request=request)
480+
return proto_to_dict(response)
481+
except Exception as e:
482+
error_msg = str(e)
483+
if "INVALID_ARGUMENT" in error_msg:
484+
raise ValueError(f"Invalid funnel configuration: {error_msg}")
485+
elif "PERMISSION_DENIED" in error_msg:
486+
raise PermissionError(f"Permission denied accessing property: {error_msg}")
487+
elif "NOT_FOUND" in error_msg:
488+
raise ValueError(f"Property not found: {error_msg}")
489+
elif "QUOTA_EXCEEDED" in error_msg:
490+
raise RuntimeError(f"API quota exceeded: {error_msg}")
491+
else:
492+
raise Exception(f"Failed to run funnel report: {error_msg}")
493+
175494

176495
# The `run_report` tool requires a more complex description that's generated at
177496
# runtime. Uses the `add_tool` method instead of an annnotation since `add_tool`
@@ -182,3 +501,9 @@ async def run_report(
182501
title="Run a Google Analytics Data API report using the Data API",
183502
description=_run_report_description(),
184503
)
504+
505+
mcp.add_tool(
506+
run_funnel_report,
507+
title="Run a Google Analytics Data API funnel report using the Data API",
508+
description=_run_funnel_report_description(),
509+
)

0 commit comments

Comments
 (0)