Skip to content

Commit 4ca6257

Browse files
W-21146754 Fix telemetry event firing issues (#105)
* Bug fixes one and two * Server stopped fix * Fix Tool_called log * Expand dev.js files for env vars * Update dev.js and fix error logs * Revert env change * Re-add disable by default * Fix flush and align features with other repo * Fix logic to read env var from mcp.json * Fix failing tests * Fix dev.js logic * Fix som cli error, update relevant unit tests * Remove unneeded code and update README * Remove debug code * Remove debug code again * Simpllify README, run.js, and combine send event and flush * Revert file * Update jsdoc for clarity
1 parent c5f9337 commit 4ca6257

File tree

11 files changed

+342
-99
lines changed

11 files changed

+342
-99
lines changed

packages/b2c-cli/bin/dev.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,26 @@
55
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
66
*/
77

8-
// Disable telemetry in development mode to avoid polluting production data
9-
process.env.SFCC_DISABLE_TELEMETRY = process.env.SFCC_DISABLE_TELEMETRY || 'true';
10-
11-
// Load .env file if present (Node.js native support)
8+
// Load .env file if present (Node.js native support; same as main run.js)
9+
// For development, place .env in the package directory and run from there.
1210
try {
1311
process.loadEnvFile();
1412
} catch {
1513
// .env file not found or not readable, continue without it
1614
}
1715

16+
// Disable telemetry by default in development when both vars are unset.
17+
// Set SFCC_DISABLE_TELEMETRY=false (or SF_DISABLE_TELEMETRY=false) in .env to enable COMMAND_START/COMMAND_SUCCESS.
18+
const userWantsTelemetryEnabled =
19+
process.env.SF_DISABLE_TELEMETRY === 'false' || process.env.SFCC_DISABLE_TELEMETRY === 'false';
20+
if (userWantsTelemetryEnabled) {
21+
process.env.SF_DISABLE_TELEMETRY = 'false';
22+
process.env.SFCC_DISABLE_TELEMETRY = 'false';
23+
} else {
24+
process.env.SF_DISABLE_TELEMETRY = 'true';
25+
process.env.SFCC_DISABLE_TELEMETRY = 'true';
26+
}
27+
1828
import {execute} from '@oclif/core';
1929

2030
await execute({development: true, dir: import.meta.url});

packages/b2c-dx-mcp/README.md

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -304,23 +304,12 @@ The MCP server collects anonymous usage telemetry to help improve the developer
304304

305305
**Development mode**: Telemetry is automatically disabled when using `bin/dev.js`, so local development and testing won't pollute production data.
306306

307-
### Disabling Telemetry
307+
### Configuring telemetry
308308

309-
Set one of these environment variables to disable telemetry:
309+
Set options in the `env` object of your server entry in `.cursor/mcp.json` or `~/.cursor/mcp.json` (the client injects these when it starts the server):
310310

311-
```bash
312-
# Salesforce CLI standard (recommended)
313-
SF_DISABLE_TELEMETRY=true
314-
315-
# Or SFCC-specific
316-
SFCC_DISABLE_TELEMETRY=true
317-
```
318-
319-
You can also override the telemetry connection string for testing:
320-
321-
```bash
322-
SFCC_APP_INSIGHTS_KEY=your-connection-string
323-
```
311+
- **Disable**: `SF_DISABLE_TELEMETRY=true` or `SFCC_DISABLE_TELEMETRY=true`
312+
- **Custom endpoint**: `SFCC_APP_INSIGHTS_KEY=your-key`
324313

325314
### What We Collect
326315

packages/b2c-dx-mcp/bin/dev.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
66
*/
77

8-
// Disable telemetry in development mode to avoid polluting production data
9-
process.env.SFCC_DISABLE_TELEMETRY = process.env.SFCC_DISABLE_TELEMETRY || 'true';
10-
11-
// Load .env file if present (Node.js native support)
12-
try {
13-
process.loadEnvFile();
14-
} catch {
15-
// .env file not found or not readable, continue without it
8+
// Disable telemetry in development to avoid polluting production data.
9+
// Honor SF_DISABLE_TELEMETRY (sf CLI) and SFCC_DISABLE_TELEMETRY.
10+
// If user explicitly enables (either is 'false'), respect that and sync both to 'false'.
11+
// Otherwise, default to disabling in dev.
12+
const userWantsTelemetryEnabled =
13+
process.env.SF_DISABLE_TELEMETRY === 'false' || process.env.SFCC_DISABLE_TELEMETRY === 'false';
14+
if (userWantsTelemetryEnabled) {
15+
process.env.SF_DISABLE_TELEMETRY = 'false';
16+
process.env.SFCC_DISABLE_TELEMETRY = 'false';
17+
} else {
18+
process.env.SF_DISABLE_TELEMETRY = 'true';
19+
process.env.SFCC_DISABLE_TELEMETRY = 'true';
1620
}
1721

1822
import {execute} from '@oclif/core';

packages/b2c-dx-mcp/src/commands/mcp.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,32 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
219219
}),
220220
};
221221

222+
/** Signal that triggered shutdown (if any) - used to exit process after finally() */
223+
private shutdownSignal?: string;
224+
225+
/** Promise that resolves when stdin closes (MCP client disconnects) */
226+
private stdinClosePromise?: Promise<void>;
227+
228+
/**
229+
* Override finally() to wait for stdin close before stopping telemetry.
230+
* This ensures SERVER_STOPPED is sent before telemetry.stop() is called.
231+
*/
232+
protected async finally(err: Error | undefined): Promise<void> {
233+
// Wait for stdin to close and SERVER_STOPPED to be sent
234+
// This keeps the command "running" until the MCP client disconnects
235+
await this.stdinClosePromise;
236+
237+
// Now call super.finally() which sends COMMAND_SUCCESS and stops telemetry
238+
await super.finally(err);
239+
240+
// Exit process if shutdown was triggered by a signal (SIGINT/SIGTERM)
241+
// Catching these signals prevents Node's default exit behavior
242+
if (this.shutdownSignal === 'SIGINT' || this.shutdownSignal === 'SIGTERM') {
243+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
244+
process.exit(0);
245+
}
246+
}
247+
222248
/**
223249
* Loads configuration from flags, environment variables, and config files.
224250
*
@@ -261,7 +287,7 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
261287
* 4. Create Services via Services.fromResolvedConfig() using already-resolved config
262288
* 5. Register tools based on --toolsets and --tools flags
263289
* 6. Connect to stdio transport (JSON-RPC over stdin/stdout)
264-
* 7. Log startup message to stderr
290+
* 7. Log startup message via structured logger
265291
*
266292
* @throws Never throws - invalid toolsets are filtered, not rejected
267293
*
@@ -320,14 +346,28 @@ export default class McpServerCommand extends BaseCommand<typeof McpServerComman
320346
const transport = new StdioServerTransport();
321347
await server.connect(transport);
322348

323-
// Track server stop when stdin closes (MCP client disconnects)
324-
// Note: The 'close' event has no arguments - it's just a signal that the stream closed
325-
process.stdin.on('close', () => {
326-
this.telemetry?.sendEvent('SERVER_STOPPED');
327-
// Don't call stop() here - let finally() handle telemetry cleanup
349+
// Create promise that resolves when server stops (stdin close or signal)
350+
// This allows finally() to wait for SERVER_STOPPED before stopping telemetry
351+
this.stdinClosePromise = new Promise((resolve) => {
352+
const sendStopAndResolve = (signal: string): void => {
353+
this.shutdownSignal = signal;
354+
this.telemetry?.sendEvent('SERVER_STOPPED', {signal});
355+
// Flush telemetry before resolving to ensure SERVER_STOPPED is sent
356+
// before finally() proceeds to stop telemetry
357+
const flushPromise = this.telemetry?.flush() ?? Promise.resolve();
358+
flushPromise.then(() => resolve()).catch(() => resolve());
359+
};
360+
361+
// Handle stdin close (MCP client disconnects normally)
362+
process.stdin.on('close', () => sendStopAndResolve('stdin_close'));
363+
364+
// Handle Ctrl+C
365+
process.on('SIGINT', () => sendStopAndResolve('SIGINT'));
366+
367+
// Handle kill signal
368+
process.on('SIGTERM', () => sendStopAndResolve('SIGTERM'));
328369
});
329370

330-
// Log startup message using the structured logger
331371
this.logger.info({version: this.config.version}, 'MCP Server running on stdio');
332372
}
333373
}

packages/b2c-dx-mcp/src/server.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -83,21 +83,25 @@ export class B2CDxMcpServer extends McpServer {
8383
const result = await handler(args);
8484
const runTimeMs = Date.now() - startTime;
8585

86-
this.telemetry?.sendEvent('TOOL_CALLED', {
87-
toolName: name,
88-
runTimeMs,
89-
isError: result.isError ?? false,
90-
});
86+
await this.telemetry
87+
?.sendEventAndFlush('TOOL_CALLED', {
88+
toolName: name,
89+
runTimeMs,
90+
isError: result.isError ?? false,
91+
})
92+
.catch(() => {});
9193

9294
return result;
9395
} catch (error) {
9496
const runTimeMs = Date.now() - startTime;
9597

96-
this.telemetry?.sendEvent('TOOL_CALLED', {
97-
toolName: name,
98-
runTimeMs,
99-
isError: true,
100-
});
98+
await this.telemetry
99+
?.sendEventAndFlush('TOOL_CALLED', {
100+
toolName: name,
101+
runTimeMs,
102+
isError: true,
103+
})
104+
.catch(() => {});
101105

102106
throw error;
103107
}
@@ -113,19 +117,19 @@ export class B2CDxMcpServer extends McpServer {
113117
public override async connect(transport: Transport): Promise<void> {
114118
try {
115119
await super.connect(transport);
116-
if (this.isConnected()) {
117-
this.telemetry?.sendEvent('SERVER_STATUS', {status: 'started'});
118-
} else {
119-
this.telemetry?.sendEvent('SERVER_STATUS', {
120-
status: 'error',
121-
errorMessage: 'Server not connected after connect() call',
122-
});
123-
}
120+
const statusPromise = this.isConnected()
121+
? this.telemetry?.sendEventAndFlush('SERVER_STATUS', {status: 'started'})
122+
: this.telemetry?.sendEventAndFlush('SERVER_STATUS', {
123+
status: 'error',
124+
errorMessage: 'Server not connected after connect() call',
125+
});
126+
await (statusPromise ?? Promise.resolve()).catch(() => {});
124127
} catch (error) {
125-
this.telemetry?.sendEvent('SERVER_STATUS', {
128+
const errorPromise = this.telemetry?.sendEventAndFlush('SERVER_STATUS', {
126129
status: 'error',
127130
errorMessage: error instanceof Error ? error.message : String(error),
128131
});
132+
await (errorPromise ?? Promise.resolve()).catch(() => {});
129133
throw error;
130134
}
131135
}

packages/b2c-dx-mcp/test/commands/mcp.test.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -330,62 +330,78 @@ describe('McpServerCommand', () => {
330330
});
331331

332332
it('returns project default when no env override', () => {
333-
const originalDisable = process.env.SFCC_DISABLE_TELEMETRY;
333+
const originalSfDisable = process.env.SF_DISABLE_TELEMETRY;
334+
const originalSfccDisable = process.env.SFCC_DISABLE_TELEMETRY;
334335
const originalKey = process.env.SFCC_APP_INSIGHTS_KEY;
335336
try {
337+
delete process.env.SF_DISABLE_TELEMETRY;
336338
delete process.env.SFCC_DISABLE_TELEMETRY;
337339
delete process.env.SFCC_APP_INSIGHTS_KEY;
338340
expect(Telemetry.getConnectionString('default-key')).to.equal('default-key');
339341
} finally {
340-
if (originalDisable !== undefined) process.env.SFCC_DISABLE_TELEMETRY = originalDisable;
341-
if (originalKey !== undefined) process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
342+
if (originalSfDisable === undefined) delete process.env.SF_DISABLE_TELEMETRY;
343+
else process.env.SF_DISABLE_TELEMETRY = originalSfDisable;
344+
if (originalSfccDisable === undefined) delete process.env.SFCC_DISABLE_TELEMETRY;
345+
else process.env.SFCC_DISABLE_TELEMETRY = originalSfccDisable;
346+
if (originalKey === undefined) delete process.env.SFCC_APP_INSIGHTS_KEY;
347+
else process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
342348
}
343349
});
344350

345351
it('returns env override when SFCC_APP_INSIGHTS_KEY is set', () => {
346-
const originalDisable = process.env.SFCC_DISABLE_TELEMETRY;
352+
const originalSfDisable = process.env.SF_DISABLE_TELEMETRY;
353+
const originalSfccDisable = process.env.SFCC_DISABLE_TELEMETRY;
347354
const originalKey = process.env.SFCC_APP_INSIGHTS_KEY;
348355
try {
356+
delete process.env.SF_DISABLE_TELEMETRY;
349357
delete process.env.SFCC_DISABLE_TELEMETRY;
350358
process.env.SFCC_APP_INSIGHTS_KEY = 'env-override-key';
351359
expect(Telemetry.getConnectionString('default-key')).to.equal('env-override-key');
352360
} finally {
353-
if (originalDisable !== undefined) process.env.SFCC_DISABLE_TELEMETRY = originalDisable;
354-
if (originalKey === undefined) {
355-
delete process.env.SFCC_APP_INSIGHTS_KEY;
356-
} else {
357-
process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
358-
}
361+
if (originalSfDisable === undefined) delete process.env.SF_DISABLE_TELEMETRY;
362+
else process.env.SF_DISABLE_TELEMETRY = originalSfDisable;
363+
if (originalSfccDisable === undefined) delete process.env.SFCC_DISABLE_TELEMETRY;
364+
else process.env.SFCC_DISABLE_TELEMETRY = originalSfccDisable;
365+
if (originalKey === undefined) delete process.env.SFCC_APP_INSIGHTS_KEY;
366+
else process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
359367
}
360368
});
361369

362370
it('returns undefined when no default and no env override', () => {
363-
const originalDisable = process.env.SFCC_DISABLE_TELEMETRY;
371+
const originalSfDisable = process.env.SF_DISABLE_TELEMETRY;
372+
const originalSfccDisable = process.env.SFCC_DISABLE_TELEMETRY;
364373
const originalKey = process.env.SFCC_APP_INSIGHTS_KEY;
365374
try {
375+
delete process.env.SF_DISABLE_TELEMETRY;
366376
delete process.env.SFCC_DISABLE_TELEMETRY;
367377
delete process.env.SFCC_APP_INSIGHTS_KEY;
368378
expect(Telemetry.getConnectionString()).to.be.undefined;
369379
} finally {
370-
if (originalDisable !== undefined) process.env.SFCC_DISABLE_TELEMETRY = originalDisable;
371-
if (originalKey !== undefined) process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
380+
if (originalSfDisable === undefined) delete process.env.SF_DISABLE_TELEMETRY;
381+
else process.env.SF_DISABLE_TELEMETRY = originalSfDisable;
382+
if (originalSfccDisable === undefined) delete process.env.SFCC_DISABLE_TELEMETRY;
383+
else process.env.SFCC_DISABLE_TELEMETRY = originalSfccDisable;
384+
if (originalKey === undefined) delete process.env.SFCC_APP_INSIGHTS_KEY;
385+
else process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
372386
}
373387
});
374388

375389
it('returns env override even without project default', () => {
376-
const originalDisable = process.env.SFCC_DISABLE_TELEMETRY;
390+
const originalSfDisable = process.env.SF_DISABLE_TELEMETRY;
391+
const originalSfccDisable = process.env.SFCC_DISABLE_TELEMETRY;
377392
const originalKey = process.env.SFCC_APP_INSIGHTS_KEY;
378393
try {
394+
delete process.env.SF_DISABLE_TELEMETRY;
379395
delete process.env.SFCC_DISABLE_TELEMETRY;
380396
process.env.SFCC_APP_INSIGHTS_KEY = 'env-only-key';
381397
expect(Telemetry.getConnectionString()).to.equal('env-only-key');
382398
} finally {
383-
if (originalDisable !== undefined) process.env.SFCC_DISABLE_TELEMETRY = originalDisable;
384-
if (originalKey === undefined) {
385-
delete process.env.SFCC_APP_INSIGHTS_KEY;
386-
} else {
387-
process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
388-
}
399+
if (originalSfDisable === undefined) delete process.env.SF_DISABLE_TELEMETRY;
400+
else process.env.SF_DISABLE_TELEMETRY = originalSfDisable;
401+
if (originalSfccDisable === undefined) delete process.env.SFCC_DISABLE_TELEMETRY;
402+
else process.env.SFCC_DISABLE_TELEMETRY = originalSfccDisable;
403+
if (originalKey === undefined) delete process.env.SFCC_APP_INSIGHTS_KEY;
404+
else process.env.SFCC_APP_INSIGHTS_KEY = originalKey;
389405
}
390406
});
391407
});

packages/b2c-dx-mcp/test/server.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class MockTelemetry {
2222
this.attributes = {...this.attributes, ...attrs};
2323
}
2424

25+
async flush(): Promise<void> {
26+
// Mock flush - does nothing but allows tests to pass
27+
}
28+
2529
sendEvent(name: string, attributes: Record<string, unknown> = {}): void {
2630
this.events.push({name, attributes});
2731
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
206206
}): Promise<Telemetry | undefined> {
207207
// If telemetry was already initialized by initTelemetryFromConfig, stop it first
208208
if (this.telemetry) {
209-
this.telemetry.stop();
209+
await this.telemetry.stop();
210210
}
211211

212212
const connectionString = Telemetry.getConnectionString(options.appInsightsKey);
@@ -472,9 +472,20 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
472472
const exitCode = err.exitCode ?? 1;
473473
const duration = this.commandStartTime ? Date.now() - this.commandStartTime : undefined;
474474

475-
// Send exception to telemetry if initialized
476-
this.telemetry?.sendException(err, {command: this.id, exitCode, duration});
477-
this.telemetry?.stop();
475+
// Send exception and COMMAND_ERROR event so the error appears in custom events (same view as COMMAND_START)
476+
// Flush explicitly before stop to ensure events are sent before process exits
477+
if (this.telemetry) {
478+
this.telemetry.sendException(err, {command: this.id, exitCode, duration});
479+
this.telemetry.sendEvent('COMMAND_ERROR', {
480+
command: this.id,
481+
exitCode,
482+
duration,
483+
errorMessage: err.message,
484+
...(err.cause ? {errorCause: String(err.cause)} : {}),
485+
});
486+
await this.telemetry.flush();
487+
await this.telemetry.stop();
488+
}
478489

479490
// Log if logger is available (may not be if error during init)
480491
if (this.logger) {
@@ -507,7 +518,7 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
507518
if (!err && this.telemetry) {
508519
const duration = this.commandStartTime ? Date.now() - this.commandStartTime : undefined;
509520
this.telemetry.sendEvent('COMMAND_SUCCESS', {command: this.id, duration});
510-
this.telemetry.stop();
521+
await this.telemetry.stop();
511522
}
512523
await super.finally(err);
513524
}

0 commit comments

Comments
 (0)