1
+ // Licensed to the .NET Foundation under one or more agreements.
2
+ // The .NET Foundation licenses this file to you under the MIT license.
3
+
4
+ using System . Collections . Immutable ;
5
+ using System . Security . Cryptography ;
6
+ using System . Text ;
7
+ using Microsoft . Build . Evaluation ;
8
+ using Microsoft . Build . Execution ;
9
+ using Microsoft . DotNet . Cli . Commands . Run . LaunchSettings ;
10
+ using Microsoft . DotNet . Cli . Utils ;
11
+
12
+ namespace Microsoft . DotNet . Cli . Commands . Run ;
13
+
14
+ /// <summary>
15
+ /// Provides telemetry functionality for dotnet run command.
16
+ /// </summary>
17
+ internal static class RunTelemetry
18
+ {
19
+ private const string RunEventName = "run" ;
20
+
21
+ /// <summary>
22
+ /// Sends telemetry for a dotnet run operation.
23
+ /// </summary>
24
+ /// <param name="isFileBased">True if this is a file-based app run, false for project-based</param>
25
+ /// <param name="launchProfile">The launch profile name if specified</param>
26
+ /// <param name="noLaunchProfile">True if --no-launch-profile was specified</param>
27
+ /// <param name="launchSettings">The applied launch settings model if any</param>
28
+ /// <param name="projectIdentifier">Obfuscated unique project identifier</param>
29
+ /// <param name="sdkCount">Number of SDKs used</param>
30
+ /// <param name="packageReferenceCount">Number of PackageReferences</param>
31
+ /// <param name="projectReferenceCount">Number of ProjectReferences</param>
32
+ /// <param name="additionalPropertiesCount">Number of additional properties (file-based only)</param>
33
+ /// <param name="usedMSBuild">Whether MSBuild was used (file-based only)</param>
34
+ /// <param name="usedRoslynCompiler">Whether Roslyn compiler was used directly (file-based only)</param>
35
+ public static void TrackRunEvent (
36
+ bool isFileBased ,
37
+ string ? launchProfile ,
38
+ bool noLaunchProfile ,
39
+ ProjectLaunchSettingsModel ? launchSettings ,
40
+ string projectIdentifier ,
41
+ int sdkCount ,
42
+ int packageReferenceCount ,
43
+ int projectReferenceCount ,
44
+ int additionalPropertiesCount = 0 ,
45
+ bool ? usedMSBuild = null ,
46
+ bool ? usedRoslynCompiler = null )
47
+ {
48
+ var properties = new Dictionary < string , string ? >
49
+ {
50
+ [ "app_type" ] = isFileBased ? "file_based" : "project_based" ,
51
+ [ "project_id" ] = projectIdentifier ,
52
+ [ "sdk_count" ] = sdkCount . ToString ( ) ,
53
+ [ "package_reference_count" ] = packageReferenceCount . ToString ( ) ,
54
+ [ "project_reference_count" ] = projectReferenceCount . ToString ( ) ,
55
+ } ;
56
+
57
+ // Launch profile telemetry
58
+ if ( noLaunchProfile )
59
+ {
60
+ properties [ "launch_profile_requested" ] = "none" ;
61
+ }
62
+ else if ( ! string . IsNullOrEmpty ( launchProfile ) )
63
+ {
64
+ properties [ "launch_profile_requested" ] = "explicit" ;
65
+ properties [ "launch_profile_is_default" ] = IsDefaultProfile ( launchProfile ) ? "true" : "false" ;
66
+ }
67
+ else if ( launchSettings != null )
68
+ {
69
+ properties [ "launch_profile_requested" ] = "default_used" ;
70
+ properties [ "launch_profile_is_default" ] = IsDefaultProfile ( launchSettings . LaunchProfileName ) ? "true" : "false" ;
71
+ }
72
+ else
73
+ {
74
+ properties [ "launch_profile_requested" ] = "false" ;
75
+ }
76
+
77
+ // File-based app specific telemetry
78
+ if ( isFileBased )
79
+ {
80
+ properties [ "additional_properties_count" ] = additionalPropertiesCount . ToString ( ) ;
81
+ if ( usedMSBuild . HasValue )
82
+ {
83
+ properties [ "used_msbuild" ] = usedMSBuild . Value ? "true" : "false" ;
84
+ }
85
+ if ( usedRoslynCompiler . HasValue )
86
+ {
87
+ properties [ "used_roslyn_compiler" ] = usedRoslynCompiler . Value ? "true" : "false" ;
88
+ }
89
+ }
90
+
91
+ TelemetryEventEntry . TrackEvent ( RunEventName , properties , measurements : null ) ;
92
+ }
93
+
94
+ /// <summary>
95
+ /// Generates an obfuscated unique identifier for a project-based app.
96
+ /// </summary>
97
+ /// <param name="projectFilePath">Full path to the project file</param>
98
+ /// <param name="repoRoot">Repository root path if available</param>
99
+ /// <returns>Hashed project identifier</returns>
100
+ public static string GetProjectBasedIdentifier ( string projectFilePath , string ? repoRoot = null )
101
+ {
102
+ // Use relative path from repo root if available, otherwise use full path
103
+ string pathToHash = repoRoot != null && projectFilePath . StartsWith ( repoRoot , StringComparison . OrdinalIgnoreCase )
104
+ ? Path . GetRelativePath ( repoRoot , projectFilePath )
105
+ : projectFilePath ;
106
+
107
+ return ComputeHash ( pathToHash ) ;
108
+ }
109
+
110
+ /// <summary>
111
+ /// Generates an obfuscated unique identifier for a file-based app.
112
+ /// This leverages the same caching infrastructure identifier already used.
113
+ /// </summary>
114
+ /// <param name="entryPointFilePath">Full path to the entry point file</param>
115
+ /// <returns>Hashed file identifier</returns>
116
+ public static string GetFileBasedIdentifier ( string entryPointFilePath )
117
+ {
118
+ // Use the same hash computation as the caching infrastructure
119
+ return ComputeHash ( entryPointFilePath ) ;
120
+ }
121
+
122
+ /// <summary>
123
+ /// Counts the number of SDKs used in a project.
124
+ /// </summary>
125
+ /// <param name="project">Project instance for project-based apps</param>
126
+ /// <param name="directives">Directives for file-based apps</param>
127
+ /// <returns>Number of SDKs</returns>
128
+ public static int CountSdks ( ProjectInstance ? project = null , ImmutableArray < CSharpDirective > directives = default )
129
+ {
130
+ if ( ! directives . IsDefaultOrEmpty )
131
+ {
132
+ // File-based: count SDK directives
133
+ var sdkDirectives = directives . OfType < CSharpDirective . Sdk > ( ) . Count ( ) ;
134
+ // If no explicit SDK directives, there's still the default Microsoft.NET.Sdk
135
+ return sdkDirectives > 0 ? sdkDirectives : 1 ;
136
+ }
137
+
138
+ if ( project != null )
139
+ {
140
+ // Project-based: look for Sdk attributes and Import nodes
141
+ var sdkCount = 0 ;
142
+
143
+ // Count main project SDK
144
+ var projectSdk = project . GetPropertyValue ( "MSBuildProjectSdk" ) ;
145
+ if ( ! string . IsNullOrEmpty ( projectSdk ) )
146
+ {
147
+ sdkCount ++ ;
148
+ }
149
+
150
+ // Count additional SDK imports
151
+ var imports = project . GetItems ( "_SdkImport" ) ;
152
+ sdkCount += imports . Count ;
153
+
154
+ return Math . Max ( 1 , sdkCount ) ; // At least 1 for Microsoft.NET.Sdk
155
+ }
156
+
157
+ return 1 ; // Default assumption for project-based apps
158
+ }
159
+
160
+ /// <summary>
161
+ /// Counts the number of PackageReferences in a project.
162
+ /// </summary>
163
+ /// <param name="project">Project instance for project-based apps</param>
164
+ /// <param name="directives">Directives for file-based apps</param>
165
+ /// <returns>Number of package references</returns>
166
+ public static int CountPackageReferences ( ProjectInstance ? project = null , ImmutableArray < CSharpDirective > directives = default )
167
+ {
168
+ if ( ! directives . IsDefaultOrEmpty )
169
+ {
170
+ // File-based: count package directives
171
+ return directives . OfType < CSharpDirective . Package > ( ) . Count ( ) ;
172
+ }
173
+
174
+ if ( project != null )
175
+ {
176
+ // Project-based: count PackageReference items
177
+ return project . GetItems ( "PackageReference" ) . Count ;
178
+ }
179
+
180
+ return 0 ;
181
+ }
182
+
183
+ /// <summary>
184
+ /// Counts the number of direct ProjectReferences in a project.
185
+ /// </summary>
186
+ /// <param name="project">Project instance for project-based apps</param>
187
+ /// <param name="directives">Directives for file-based apps</param>
188
+ /// <returns>Number of project references</returns>
189
+ public static int CountProjectReferences ( ProjectInstance ? project = null , ImmutableArray < CSharpDirective > directives = default )
190
+ {
191
+ if ( ! directives . IsDefaultOrEmpty )
192
+ {
193
+ // File-based: count project directives
194
+ return directives . OfType < CSharpDirective . Project > ( ) . Count ( ) ;
195
+ }
196
+
197
+ if ( project != null )
198
+ {
199
+ // Project-based: count ProjectReference items
200
+ return project . GetItems ( "ProjectReference" ) . Count ;
201
+ }
202
+
203
+ return 0 ;
204
+ }
205
+
206
+ /// <summary>
207
+ /// Counts the number of additional properties for file-based apps.
208
+ /// </summary>
209
+ /// <param name="directives">Directives for file-based apps</param>
210
+ /// <returns>Number of additional properties</returns>
211
+ public static int CountAdditionalProperties ( ImmutableArray < CSharpDirective > directives )
212
+ {
213
+ if ( directives . IsDefaultOrEmpty )
214
+ {
215
+ return 0 ;
216
+ }
217
+
218
+ return directives . OfType < CSharpDirective . Property > ( ) . Count ( ) ;
219
+ }
220
+
221
+ /// <summary>
222
+ /// Determines if a launch profile name is one of the default ones.
223
+ /// </summary>
224
+ /// <param name="profileName">The profile name to check</param>
225
+ /// <returns>True if it's a default profile name</returns>
226
+ private static bool IsDefaultProfile ( string ? profileName )
227
+ {
228
+ if ( string . IsNullOrEmpty ( profileName ) )
229
+ {
230
+ return false ;
231
+ }
232
+
233
+ // Common default profile names
234
+ return profileName . Equals ( "Default" , StringComparison . OrdinalIgnoreCase ) ||
235
+ profileName . Equals ( "Development" , StringComparison . OrdinalIgnoreCase ) ||
236
+ profileName . Equals ( "Production" , StringComparison . OrdinalIgnoreCase ) ||
237
+ profileName . Equals ( "Staging" , StringComparison . OrdinalIgnoreCase ) ;
238
+ }
239
+
240
+ /// <summary>
241
+ /// Computes a hash for the given input string.
242
+ /// </summary>
243
+ /// <param name="input">String to hash</param>
244
+ /// <returns>Hex-encoded hash</returns>
245
+ private static string ComputeHash ( string input )
246
+ {
247
+ var inputBytes = Encoding . UTF8 . GetBytes ( input . ToLowerInvariant ( ) ) ;
248
+ var hashBytes = SHA256 . HashData ( inputBytes ) ;
249
+ return Convert . ToHexString ( hashBytes ) . ToLowerInvariant ( ) ;
250
+ }
251
+ }
0 commit comments