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
1919from analytics_mcp .coordinator import mcp
2020from analytics_mcp .tools .reporting .metadata import (
2626from 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
3435def _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+
82189async 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