Skip to content

Commit d50bf6b

Browse files
authored
fix: replace @salesforce/telemetry with direct applicationinsights dependency (#262)
* fix: replace @salesforce/telemetry with direct applicationinsights dependency (#261) Eliminates the punycode deprecation warning on Node 21+ by removing the heavy @salesforce/telemetry → @salesforce/core → @jsforce/jsforce-node → node-fetch@2 → punycode dependency chain. Uses applicationinsights TelemetryClient directly (~100 lines) since we already bypassed most of the wrapper's logic. Net: -181 packages removed, +16 added. All env var controls preserved. * fix: track mrtProject in telemetry context Add mrtProject to addTelemetryContext() so MRT commands include the project name in telemetry events.
1 parent 1f24b81 commit d50bf6b

7 files changed

Lines changed: 250 additions & 1308 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@salesforce/b2c-tooling-sdk': patch
3+
---
4+
5+
Replace @salesforce/telemetry with direct applicationinsights dependency to eliminate the punycode deprecation warning on Node 21+

packages/b2c-tooling-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@
383383
"node": ">=22.16.0"
384384
},
385385
"dependencies": {
386-
"@salesforce/telemetry": "6.4.6",
386+
"applicationinsights": "2.9.8",
387387
"archiver": "7.0.1",
388388
"chokidar": "5.0.0",
389389
"cliui": "catalog:",

packages/b2c-tooling-sdk/src/cli/base-command.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
365365
attributes.shortCode = values.shortCode;
366366
}
367367

368+
if (values.mrtProject) {
369+
attributes.mrtProject = values.mrtProject;
370+
}
371+
368372
// Record which config sources contributed
369373
if (sources.length > 0) {
370374
attributes.configSources = sources.map((s) => s.name).join(', ');

packages/b2c-tooling-sdk/src/telemetry/telemetry.ts

Lines changed: 73 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import {randomBytes} from 'node:crypto';
88
import fs from 'node:fs';
99
import path from 'node:path';
10-
import {TelemetryReporter} from '@salesforce/telemetry';
10+
import appInsights from 'applicationinsights';
1111
import type {TelemetryAttributes, TelemetryEventProperties, TelemetryOptions} from './types.js';
1212
import {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
}

packages/b2c-tooling-sdk/test/cli/base-command.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,19 @@ describe('cli/base-command', () => {
577577
expect(attrs.clientId).to.equal('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
578578
});
579579

580+
it('adds mrtProject when available', async () => {
581+
stubParse(command);
582+
await command.init();
583+
setupTelemetry(command);
584+
setResolvedConfig(command, {mrtProject: 'my-storefront'});
585+
586+
command.testAddTelemetryContext();
587+
588+
expect(telemetryAddAttributesStub.calledOnce).to.be.true;
589+
const attrs = telemetryAddAttributesStub.firstCall.args[0];
590+
expect(attrs.mrtProject).to.equal('my-storefront');
591+
});
592+
580593
it('adds configSources from resolved sources', async () => {
581594
stubParse(command);
582595
await command.init();

0 commit comments

Comments
 (0)