@@ -124,14 +124,15 @@ public int Execute()
124124
125125 Func < ProjectCollection , ProjectInstance > ? projectFactory = null ;
126126 RunProperties ? cachedRunProperties = null ;
127+ VirtualProjectBuildingCommand ? virtualCommand = null ;
127128 if ( ShouldBuild )
128129 {
129130 if ( string . Equals ( "true" , launchSettings ? . DotNetRunMessages , StringComparison . OrdinalIgnoreCase ) )
130131 {
131132 Reporter . Output . WriteLine ( CliCommandStrings . RunCommandBuilding ) ;
132133 }
133134
134- EnsureProjectIsBuilt ( out projectFactory , out cachedRunProperties ) ;
135+ EnsureProjectIsBuilt ( out projectFactory , out cachedRunProperties , out virtualCommand ) ;
135136 }
136137 else
137138 {
@@ -143,11 +144,11 @@ public int Execute()
143144 if ( EntryPointFileFullPath is not null )
144145 {
145146 Debug . Assert ( ! ReadCodeFromStdin ) ;
146- var command = CreateVirtualCommand ( ) ;
147- command . MarkArtifactsFolderUsed ( ) ;
147+ virtualCommand = CreateVirtualCommand ( ) ;
148+ virtualCommand . MarkArtifactsFolderUsed ( ) ;
148149
149- var cacheEntry = command . GetPreviousCacheEntry ( ) ;
150- projectFactory = CanUseRunPropertiesForCscBuiltProgram ( BuildLevel . None , cacheEntry ) ? null : command . CreateProjectInstance ;
150+ var cacheEntry = virtualCommand . GetPreviousCacheEntry ( ) ;
151+ projectFactory = CanUseRunPropertiesForCscBuiltProgram ( BuildLevel . None , cacheEntry ) ? null : virtualCommand . CreateProjectInstance ;
151152 cachedRunProperties = cacheEntry ? . Run ;
152153 }
153154 }
@@ -163,6 +164,9 @@ public int Execute()
163164 targetCommand . EnvironmentVariable ( name , value ) ;
164165 }
165166
167+ // Send telemetry about the run operation
168+ SendRunTelemetry ( launchSettings , virtualCommand ) ;
169+
166170 // Ignore Ctrl-C for the remainder of the command's execution
167171 Console . CancelKeyPress += ( sender , e ) => { e . Cancel = true ; } ;
168172
@@ -297,22 +301,23 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
297301 }
298302 }
299303
300- private void EnsureProjectIsBuilt ( out Func < ProjectCollection , ProjectInstance > ? projectFactory , out RunProperties ? cachedRunProperties )
304+ private void EnsureProjectIsBuilt ( out Func < ProjectCollection , ProjectInstance > ? projectFactory , out RunProperties ? cachedRunProperties , out VirtualProjectBuildingCommand ? virtualCommand )
301305 {
302306 int buildResult ;
303307 if ( EntryPointFileFullPath is not null )
304308 {
305- var command = CreateVirtualCommand ( ) ;
306- buildResult = command . Execute ( ) ;
307- projectFactory = CanUseRunPropertiesForCscBuiltProgram ( command . LastBuild . Level , command . LastBuild . Cache ? . PreviousEntry ) ? null : command . CreateProjectInstance ;
308- cachedRunProperties = command . LastBuild . Cache ? . CurrentEntry . Run ;
309+ virtualCommand = CreateVirtualCommand ( ) ;
310+ buildResult = virtualCommand . Execute ( ) ;
311+ projectFactory = CanUseRunPropertiesForCscBuiltProgram ( virtualCommand . LastBuild . Level , virtualCommand . LastBuild . Cache ? . PreviousEntry ) ? null : virtualCommand . CreateProjectInstance ;
312+ cachedRunProperties = virtualCommand . LastBuild . Cache ? . CurrentEntry . Run ;
309313 }
310314 else
311315 {
312316 Debug . Assert ( ProjectFileFullPath is not null ) ;
313317
314318 projectFactory = null ;
315319 cachedRunProperties = null ;
320+ virtualCommand = null ;
316321 buildResult = new RestoringCommand (
317322 MSBuildArgs . CloneWithExplicitArgs ( [ ProjectFileFullPath , .. MSBuildArgs . OtherMSBuildArgs ] ) ,
318323 NoRestore ,
@@ -753,4 +758,135 @@ public static ParseResult ModifyParseResultForShorthandProjectOption(ParseResult
753758 var newParseResult = Parser . Parse ( tokensToParse ) ;
754759 return newParseResult ;
755760 }
761+
762+ /// <summary>
763+ /// Sends telemetry about the run operation.
764+ /// </summary>
765+ private void SendRunTelemetry (
766+ ProjectLaunchSettingsModel ? launchSettings ,
767+ VirtualProjectBuildingCommand ? virtualCommand )
768+ {
769+ try
770+ {
771+ if ( virtualCommand != null )
772+ {
773+ SendFileBasedTelemetry ( launchSettings , virtualCommand ) ;
774+ }
775+ else
776+ {
777+ SendProjectBasedTelemetry ( launchSettings ) ;
778+ }
779+ }
780+ catch ( Exception ex )
781+ {
782+ // Silently ignore telemetry errors to not affect the run operation
783+ if ( CommandLoggingContext . IsVerbose )
784+ {
785+ Reporter . Verbose . WriteLine ( $ "Failed to send run telemetry: { ex } ") ;
786+ }
787+ }
788+ }
789+
790+ /// <summary>
791+ /// Builds and sends telemetry data for file-based app runs.
792+ /// </summary>
793+ private void SendFileBasedTelemetry (
794+ ProjectLaunchSettingsModel ? launchSettings ,
795+ VirtualProjectBuildingCommand virtualCommand )
796+ {
797+ Debug . Assert ( EntryPointFileFullPath != null ) ;
798+ var projectIdentifier = RunTelemetry . GetFileBasedIdentifier ( EntryPointFileFullPath , Sha256Hasher . Hash ) ;
799+
800+ var directives = virtualCommand . Directives ;
801+ var sdkCount = RunTelemetry . CountSdks ( directives ) ;
802+ var packageReferenceCount = RunTelemetry . CountPackageReferences ( directives ) ;
803+ var projectReferenceCount = RunTelemetry . CountProjectReferences ( directives ) ;
804+ var additionalPropertiesCount = RunTelemetry . CountAdditionalProperties ( directives ) ;
805+
806+ RunTelemetry . TrackRunEvent (
807+ isFileBased : true ,
808+ projectIdentifier : projectIdentifier ,
809+ launchProfile : LaunchProfile ,
810+ noLaunchProfile : NoLaunchProfile ,
811+ launchSettings : launchSettings ,
812+ sdkCount : sdkCount ,
813+ packageReferenceCount : packageReferenceCount ,
814+ projectReferenceCount : projectReferenceCount ,
815+ additionalPropertiesCount : additionalPropertiesCount ,
816+ usedMSBuild : virtualCommand . LastBuild . Level is BuildLevel . All ,
817+ usedRoslynCompiler : virtualCommand . LastBuild . Level is BuildLevel . Csc ) ;
818+ }
819+
820+ /// <summary>
821+ /// Builds and sends telemetry data for project-based app runs.
822+ /// </summary>
823+ private void SendProjectBasedTelemetry ( ProjectLaunchSettingsModel ? launchSettings )
824+ {
825+ Debug . Assert ( ProjectFileFullPath != null ) ;
826+ var projectIdentifier = RunTelemetry . GetProjectBasedIdentifier ( ProjectFileFullPath , GetRepositoryRoot ( ) , Sha256Hasher . Hash ) ;
827+
828+ // Get package and project reference counts for project-based apps
829+ int packageReferenceCount = 0 ;
830+ int projectReferenceCount = 0 ;
831+
832+ // Try to get project information for telemetry if we built the project
833+ if ( ShouldBuild )
834+ {
835+ try
836+ {
837+ var globalProperties = MSBuildArgs . GlobalProperties ? . ToDictionary ( ) ?? new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
838+ globalProperties [ Constants . EnableDefaultItems ] = "false" ;
839+ globalProperties [ Constants . MSBuildExtensionsPath ] = AppContext . BaseDirectory ;
840+
841+ using var collection = new ProjectCollection ( globalProperties : globalProperties ) ;
842+ var project = collection . LoadProject ( ProjectFileFullPath ) . CreateProjectInstance ( ) ;
843+
844+ packageReferenceCount = RunTelemetry . CountPackageReferences ( project ) ;
845+ projectReferenceCount = RunTelemetry . CountProjectReferences ( project ) ;
846+ }
847+ catch
848+ {
849+ // If project evaluation fails for telemetry, use defaults
850+ // We don't want telemetry collection to affect the run operation
851+ }
852+ }
853+
854+ RunTelemetry . TrackRunEvent (
855+ isFileBased : false ,
856+ projectIdentifier : projectIdentifier ,
857+ launchProfile : LaunchProfile ,
858+ noLaunchProfile : NoLaunchProfile ,
859+ launchSettings : launchSettings ,
860+ packageReferenceCount : packageReferenceCount ,
861+ projectReferenceCount : projectReferenceCount ) ;
862+ }
863+
864+ /// <summary>
865+ /// Attempts to find the repository root directory.
866+ /// </summary>
867+ /// <returns>Repository root path if found, null otherwise</returns>
868+ private string ? GetRepositoryRoot ( )
869+ {
870+ try
871+ {
872+ var currentDir = ProjectFileFullPath != null
873+ ? Path . GetDirectoryName ( ProjectFileFullPath )
874+ : Directory . GetCurrentDirectory ( ) ;
875+
876+ while ( currentDir != null )
877+ {
878+ if ( Directory . Exists ( Path . Combine ( currentDir , ".git" ) ) )
879+ {
880+ return currentDir ;
881+ }
882+ currentDir = Directory . GetParent ( currentDir ) ? . FullName ;
883+ }
884+ }
885+ catch
886+ {
887+ // Ignore errors when trying to find repo root
888+ }
889+
890+ return null ;
891+ }
756892}
0 commit comments