77import { randomBytes } from 'node:crypto' ;
88import fs from 'node:fs' ;
99import path from 'node:path' ;
10- import { TelemetryReporter } from '@salesforce/telemetry ' ;
10+ import appInsights from 'applicationinsights ' ;
1111import type { TelemetryAttributes , TelemetryEventProperties , TelemetryOptions } from './types.js' ;
1212import { getLogger , type Logger } from '../logging/index.js' ;
1313
@@ -27,6 +27,31 @@ function sanitizeAttributes(attributes: TelemetryAttributes): TelemetryAttribute
2727 return out ;
2828}
2929
30+ /**
31+ * Split attributes into string properties and numeric measurements
32+ * for Application Insights, matching the behavior of @salesforce/telemetry.
33+ * String values have $HOME replaced with ~ for GDPR safety.
34+ * Boolean values are converted to strings.
35+ */
36+ function buildPropertiesAndMeasurements ( attributes : TelemetryAttributes ) : {
37+ properties : Record < string , string > ;
38+ measurements : Record < string , number > ;
39+ } {
40+ const properties : Record < string , string > = { } ;
41+ const measurements : Record < string , number > = { } ;
42+ const home = process . env . HOME ?? '' ;
43+ for ( const [ key , value ] of Object . entries ( attributes ) ) {
44+ if ( typeof value === 'string' ) {
45+ properties [ key ] = home ? value . replace ( home , '~' ) : value ;
46+ } else if ( typeof value === 'number' ) {
47+ measurements [ key ] = value ;
48+ } else if ( typeof value === 'boolean' ) {
49+ properties [ key ] = String ( value ) ;
50+ }
51+ }
52+ return { properties, measurements} ;
53+ }
54+
3055/**
3156 * Get the path to the persistent CLI ID file.
3257 * @param dataDir - oclif dataDir for persistent storage
@@ -69,16 +94,6 @@ const readOrCreateCliId = (dataDir?: string): string => {
6994 return newId ;
7095} ;
7196
72- /**
73- * Custom TelemetryReporter that always enables telemetry.
74- * Gating is handled at the instantiation site.
75- */
76- class ConfigurableTelemetryReporter extends TelemetryReporter {
77- override isSfdxTelemetryEnabled ( ) : boolean {
78- return true ;
79- }
80- }
81-
8297/**
8398 * Telemetry client for sending events to Application Insights.
8499 *
@@ -100,7 +115,7 @@ export class Telemetry {
100115 private attributes : TelemetryAttributes ;
101116 private cliId : string ;
102117 private project : string ;
103- private reporter : TelemetryReporter | undefined ;
118+ private client : appInsights . TelemetryClient | undefined ;
104119 private sessionId : string ;
105120 private started : boolean ;
106121 private version : string ;
@@ -130,7 +145,7 @@ export class Telemetry {
130145 this . appInsightsKey = options . appInsightsKey ;
131146 this . attributes = { ...( options . initialAttributes ?? { } ) } ;
132147 this . cliId = readOrCreateCliId ( options . dataDir ) ;
133- this . reporter = undefined ;
148+ this . client = undefined ;
134149 this . sessionId = generateRandomId ( ) ;
135150 this . started = false ;
136151 this . version = options . version ?? '0.0.0' ;
@@ -161,7 +176,8 @@ export class Telemetry {
161176 const name = eventName ?. trim ( ) || 'UNKNOWN' ;
162177 this . traceLog ?. debug ( { event : name , attributes} , 'telemetry sendEvent' ) ;
163178 const eventProperties = this . buildEventProperties ( attributes ) ;
164- this . reporter ?. sendTelemetryEvent ( name , eventProperties ) ;
179+ const { properties, measurements} = buildPropertiesAndMeasurements ( eventProperties ) ;
180+ this . client ?. trackEvent ( { name : `${ this . project } /${ name } ` , properties, measurements} ) ;
165181 } catch {
166182 // ignore send errors
167183 }
@@ -190,8 +206,9 @@ export class Telemetry {
190206 sendException ( error : Error , attributes : TelemetryAttributes = { } ) : void {
191207 try {
192208 this . traceLog ?. debug ( { error : error . name , message : error . message } , 'telemetry sendException' ) ;
193- const properties = this . buildEventProperties ( sanitizeAttributes ( attributes ) ) ;
194- this . reporter ?. sendTelemetryException ( error , properties ) ;
209+ const eventProperties = this . buildEventProperties ( sanitizeAttributes ( attributes ) ) ;
210+ const { properties, measurements} = buildPropertiesAndMeasurements ( eventProperties ) ;
211+ this . client ?. trackException ( { exception : error , properties, measurements} ) ;
195212 } catch {
196213 // ignore send errors
197214 }
@@ -214,53 +231,39 @@ export class Telemetry {
214231 ) ;
215232
216233 try {
217- await this . createReporter ( ) ;
234+ this . createClient ( ) ;
218235 } catch {
219- // Best-effort retry after ~1s: first runs can hit transient failures
220- // establishing the Application Insights connection (DNS/proxy/VPN warm-up,
221- // brief network blips, or backend cold start). One short delay usually fixes it.
222- // If the retry still fails, ignore it to avoid impacting the application.
223- try {
224- await this . createReporter ( ) ;
225- } catch {
226- // ignore
227- }
236+ // best-effort — telemetry failure never impacts the application
228237 }
229238 }
230239
231240 /**
232- * Flush pending telemetry events without stopping the reporter .
241+ * Flush pending telemetry events without stopping the client .
233242 * Use this for long-running processes that need to ensure events are sent periodically.
234- * Uses the native reporter.flush() as documented in https://github.com/forcedotcom/telemetry,
235- * and also flushes the App Insights client when present (SDK flush() only flushes O11y).
236243 */
237244 async flush ( ) : Promise < void > {
238- if ( ! this . started || ! this . reporter ) return ;
245+ if ( ! this . started || ! this . client ) return ;
239246
240- await this . reporter . flush ( ) ;
241-
242- // SDK flush() only flushes O11y; we use App Insights. Flush the underlying client.
243- try {
244- const client = this . reporter . getTelemetryClient ( ) ;
245- if ( client ?. flush ) {
246- await new Promise < void > ( ( resolve ) => {
247- client . flush ( { callback : ( ) => resolve ( ) } ) ;
248- } ) ;
249- }
250- } catch {
251- // getTelemetryClient() throws if App Insights not initialized
252- }
247+ await new Promise < void > ( ( resolve ) => {
248+ this . client ! . flush ( { callback : ( ) => resolve ( ) } ) ;
249+ } ) ;
253250 }
254251
255252 /**
256- * Stop the telemetry reporter and flush any pending events.
253+ * Stop the telemetry client and flush any pending events.
257254 * Includes a delay to allow async HTTP requests to complete.
258255 */
259256 async stop ( ) : Promise < void > {
260257 if ( ! this . started ) return ;
261258 this . traceLog ?. debug ( 'telemetry stop' ) ;
262259 this . started = false ;
263- this . reporter ?. stop ( ) ;
260+
261+ if ( this . client ) {
262+ await new Promise < void > ( ( resolve ) => {
263+ this . client ! . flush ( { callback : ( ) => resolve ( ) } ) ;
264+ } ) ;
265+ this . client = undefined ;
266+ }
264267
265268 // Allow pending HTTP requests to flush before process exits.
266269 // Application Insights SDK sends events asynchronously, so we need
@@ -288,12 +291,30 @@ export class Telemetry {
288291 } ;
289292 }
290293
291- private async createReporter ( ) : Promise < void > {
292- this . reporter = await ConfigurableTelemetryReporter . create ( {
293- project : this . project ,
294- key : this . appInsightsKey ?? '' ,
295- userId : this . cliId ,
296- } ) ;
297- this . reporter . start ( ) ;
294+ private createClient ( ) : void {
295+ const client = new appInsights . TelemetryClient ( this . appInsightsKey ) ;
296+
297+ // Disable all auto-collection — we only do manual event tracking
298+ client . config . enableAutoCollectConsole = false ;
299+ client . config . enableAutoCollectExceptions = false ;
300+ client . config . enableAutoCollectPerformance = false ;
301+ client . config . enableAutoCollectRequests = false ;
302+ client . config . enableAutoCollectDependencies = false ;
303+ client . config . enableAutoDependencyCorrelation = false ;
304+ client . config . enableAutoCollectHeartbeat = false ;
305+ client . config . enableAutoCollectPreAggregatedMetrics = false ;
306+ client . config . enableAutoCollectIncomingRequestAzureFunctions = false ;
307+ client . config . enableSendLiveMetrics = false ;
308+ client . config . enableUseDiskRetryCaching = false ;
309+ client . config . disableStatsbeat = true ;
310+ client . config . enableInternalDebugLogging = false ;
311+ client . config . enableInternalWarningLogging = false ;
312+
313+ // Set user ID for session tracking
314+ client . context . tags [ client . context . keys . userId ] = this . cliId ;
315+ // GDPR: hide machine-identifying cloud role instance
316+ client . context . tags [ client . context . keys . cloudRoleInstance ] = '' ;
317+
318+ this . client = client ;
298319 }
299320}
0 commit comments