@@ -773,3 +773,103 @@ def tml(
773773 syncer .dump (models .MetadataTML .__tablename__ , data = rows )
774774
775775 return 0
776+
777+ @app .command ()
778+ @depends_on (thoughtspot = ThoughtSpot ())
779+ def ts_ai_stats (
780+ ctx : typer .Context ,
781+ syncer : Syncer = typer .Option (
782+ ...,
783+ click_type = custom_types .Syncer (models = [models .AIStats ]),
784+ help = "protocol and path for options to pass to the syncer" ,
785+ rich_help_panel = "Syncer Options" ,
786+ ),
787+ from_date : custom_types .Date = typer .Option (..., help = "inclusive lower bound of rows to select from TS: BI Server" ),
788+ to_date : custom_types .Date = typer .Option (..., help = "inclusive upper bound of rows to select from TS: BI Server" ),
789+ org_override : str = typer .Option (None , "--org" , help = "The Org to switch to before performing actions." ),
790+ compact : bool = typer .Option (True , "--compact / --full" , help = "If compact, add [User Action] != {null} 'invalid'" ),
791+ ) -> _types .ExitCode :
792+ """
793+ Extract query performance metrics for each query made against an external database
794+
795+ To extract one day of data, set [b cyan]--from-date[/] and [b cyan]--to-date[/] to the same value.
796+ \b
797+ Fields extracted from TS: AI and BI Stats
798+ - Answer Session ID - Average Query Latency (External) - Average System Latency (Overall) - Impressions
799+ - Connection - Connection ID - DB Auth Type - Is System
800+ - DB Type - Error Message - External Database Query ID - Is Billable
801+ - Model - Model ID - Object - Object ID
802+ - Object Subtype - Object Type - Org - Org ID
803+ - Query Count - Query End Time - Query Errors - Query Start Time
804+ - Query Status - SQL Query - ThoughtSpot Query ID - ThoughtSpot Start Time
805+ - Total Credits - Total Nums Rows Fetched - Trace ID - User
806+ - User Action - User Action Count - User Count - User Display Name
807+ - User ID - Visualization ID
808+ """
809+ assert isinstance (from_date , dt .date ), f"Could not coerce from_date '{ from_date } ' to a date."
810+ assert isinstance (to_date , dt .date ), f"Could not coerce to_date '{ to_date } ' to a date."
811+ ts = ctx .obj .thoughtspot
812+
813+ CLUSTER_UUID = ts .session_context .thoughtspot .cluster_id
814+
815+ TZ_UTC = zoneinfo .ZoneInfo ("UTC" )
816+ TS_AI_TIMEZONE = TZ_UTC if ts .session_context .thoughtspot .is_cloud else ts .session_context .thoughtspot .timezone
817+ print (f"TS_AI_TIMEZONE -> { TS_AI_TIMEZONE } " )
818+
819+ if syncer .protocol == "falcon" :
820+ log .error ("Falcon Syncer is not supported for TS: AI Server reflection." )
821+ models .AIStats .__table__ .drop (syncer .engine )
822+ return 1
823+
824+ if (to_date - from_date ) > dt .timedelta (days = 31 ): # type: ignore[operator]
825+ log .warning ("Due to how the Search API functions, it's recommended to request no more than 1 month at a time." )
826+
827+ # DEV NOTE: @boonhapus
828+ # As of 9.10.0.cl , TS: BI Server only resides in the Primary Org(0), so switch to it
829+ if ts .session_context .thoughtspot .is_orgs_enabled :
830+ ts .switch_org (org_id = 0 )
831+
832+ if org_override is not None :
833+ c = workflows .metadata .fetch_one (identifier = org_override , metadata_type = "ORG" , attr_path = "id" , http = ts .api )
834+ _ = utils .run_sync (c )
835+ org_override = _
836+
837+ SEARCH_DATA_DATE_FMT = "%m/%d/%Y"
838+ SEARCH_TOKENS = (
839+ "[Query Start Time] [Query Start Time].detailed [Query End Time] [Query End Time].detailed [Org]"
840+ "[Query Status] [Connection] [User] [Nums Rows Fetched] [ThoughtSpot Query ID] [Is Billable] [ThoughtSpot Start Time]"
841+ "[ThoughtSpot Start Time].detailed [User Action] [Is System] [Visualization ID] [External Database Query ID] [Query Latency (External)] "
842+ "[Object] [User ID] [Org ID] [Credits] [Impressions] [Query Count] [Query Errors] [System Latency (Overall)] [User Action Count]"
843+ "[User Action Count] [User Count] [Answer Session ID] [Connection ID] [DB Auth Type] [DB Type] [Error Message] [Model]"
844+ "[Model ID] [Object ID] [Object Subtype] [Object Type] [SQL Query] [User Display Name] [Trace ID]"
845+ "[ThoughtSpot Start Time].detailed [ThoughtSpot Start Time] != 'today'"
846+ # FOR DATA QUALITY PURPOSES
847+ # CONDITIONALS BASED ON CLI OPTIONS OR ENVIRONMENT
848+ + ("" if not compact else " [user action] != [user action].invalid [user action].{null}" )
849+ + ("" if from_date is None else f" [ThoughtSpot Start Time] >= '{ from_date .strftime (SEARCH_DATA_DATE_FMT )} '" )
850+ + ("" if to_date is None else f" [ThoughtSpot Start Time] <= '{ to_date .strftime (SEARCH_DATA_DATE_FMT )} '" )
851+ + ("" if not ts .session_context .thoughtspot .is_orgs_enabled else " [org id]" )
852+ + ("" if org_override is None else f" [org id] = { org_override } " )
853+ )
854+
855+ TOOL_TASKS = [
856+ px .WorkTask (id = "SEARCH" , description = "Fetching data from ThoughtSpot" ),
857+ px .WorkTask (id = "CLEAN" , description = "Transforming API results" ),
858+ px .WorkTask (id = "DUMP_DATA" , description = f"Sending data to { syncer .name } " ),
859+ ]
860+
861+ # DEV NOTE: @saurabhsingh1608. 09/15/2025
862+ # Currently worksheet name is "TS: AI and BI Stats (Beta)" change it in future as need arise
863+
864+ with px .WorkTracker ("Fetching TS: AI and BI Stats" , tasks = TOOL_TASKS ) as tracker :
865+ with tracker ["SEARCH" ]:
866+ c = workflows .search (worksheet = "TS: AI and BI Stats (Beta)" , query = SEARCH_TOKENS , timezone = TS_AI_TIMEZONE , http = ts .api )
867+ _ = utils .run_sync (c )
868+
869+ with tracker ["CLEAN" ]:
870+ d = api_transformer .ts_ai_stats (data = _ , cluster = CLUSTER_UUID )
871+
872+ with tracker ["DUMP_DATA" ]:
873+ syncer .dump ("ts_ai_stats" , data = d )
874+
875+ return 0
0 commit comments