Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@salesforce/core": "^8.25.1",
"@salesforce/kit": "^3.2.4",
"@salesforce/sf-plugins-core": "^12",
"@salesforce/telemetry": "^6.6.4",
"@salesforce/telemetry": "^6.7.0",
"@salesforce/ts-types": "^2.0.12",
"debug": "^4.4.3"
},
Expand Down
27 changes: 14 additions & 13 deletions src/commandExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ export class CommandExecution extends AsyncCreatable {
private readonly argv: string[];
private config: Partial<Config>;

private orgId?: string | null;
private devhubId?: string | null;
private agentPseudoTypeUsed?: boolean | null;
private orgApiVersion?: string | null;
private devhubApiVersion?: string | null;
private orgId?: string;
private devhubId?: string;
private agentPseudoTypeUsed?: boolean;
private orgApiVersion?: string;
private devhubApiVersion?: string;
private argKeys: string[] = [];
private enableO11y?: boolean;
private o11yUploadEndpoint?: string;
private productFeatureId?: string;
private targetOrgUsername?: string;

public constructor(options: CommandExecutionOptions) {
super(options);
Expand Down Expand Up @@ -115,6 +116,7 @@ export class CommandExecution extends AsyncCreatable {

// Salesforce Information
orgId: this.orgId,
targetOrgUsername: this.targetOrgUsername,
devhubId: this.devhubId,
orgApiVersion: this.orgApiVersion,
devhubApiVersion: this.devhubApiVersion,
Expand Down Expand Up @@ -176,15 +178,14 @@ export class CommandExecution extends AsyncCreatable {
} catch (error) {
debug('Error parsing flags');
}
const targetOrg = flags['target-org'] ? (flags['target-org'] as unknown as Org) : undefined;
const targetDevHub = flags['target-dev-hub'] ? (flags['target-dev-hub'] as unknown as Org) : undefined;

this.orgId = flags['target-org'] ? (flags['target-org'] as unknown as Org).getOrgId() : null;
this.devhubId = flags['target-dev-hub'] ? (flags['target-dev-hub'] as unknown as Org).getOrgId() : null;
this.orgApiVersion = flags['target-org']
? (flags['target-org'] as unknown as Org).getConnection().getApiVersion()
: null;
this.devhubApiVersion = flags['target-dev-hub']
? (flags['target-dev-hub'] as unknown as Org).getConnection().getApiVersion()
: null;
this.orgId = targetOrg ? targetOrg.getOrgId() : undefined;
this.targetOrgUsername = targetOrg ? targetOrg.getUsername() : undefined;
this.devhubId = targetDevHub ? targetDevHub.getOrgId() : undefined;
this.orgApiVersion = targetOrg ? targetOrg.getConnection().getApiVersion() : undefined;
this.devhubApiVersion = targetDevHub ? targetDevHub.getConnection().getApiVersion() : undefined;
this.determineSpecifiedFlags(argv, flags, flagDefinitions);

// Read o11y configuration from the plugin's package.json (plugin that owns the command)
Expand Down
79 changes: 51 additions & 28 deletions src/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
*/

import { SfError } from '@salesforce/core/sfError';
import type { Attributes, PdpEvent } from '@salesforce/telemetry';
import type { Attributes, PdpEvent, TelemetryOptions } from '@salesforce/telemetry';
import { asBoolean, asString, Dictionary } from '@salesforce/ts-types';
import { Connection, Org } from '@salesforce/core';
import Telemetry from './telemetry.js';
import { debug } from './debugger.js';
import { TelemetryGlobal } from './telemetryGlobal.js';
Expand All @@ -27,6 +28,7 @@ const PROJECT = 'salesforce-cli';
const APP_INSIGHTS_KEY =
'InstrumentationKey=2ca64abb-6123-4c7b-bd9e-4fe73e71fe9c;IngestionEndpoint=https://eastus-1.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=ecd8fa7a-0e0d-4109-94db-4d7878ada862';

export type PdpEventWithUsername = PdpEvent & { targetOrgUsername?: string };
export class Uploader {
private o11yUploadEndpoint: string = '';

Expand All @@ -46,8 +48,8 @@ export class Uploader {
*/
private async sendToTelemetry(): Promise<void> {
const { TelemetryReporter } = await import('@salesforce/telemetry');
let appInsightsReporter: InstanceType<typeof TelemetryReporter>;
let o11yReporter: InstanceType<typeof TelemetryReporter> | undefined;
let appInsightsReporter: InstanceType<typeof TelemetryReporter> | undefined;
const o11yReporterMap: Map<string, InstanceType<typeof TelemetryReporter>> = new Map();

try {
appInsightsReporter = await TelemetryReporter.create({
Expand All @@ -60,18 +62,15 @@ export class Uploader {
});
} catch (err) {
const error = SfError.wrap(err);
debug(`Error creating reporter: ${error.message}`);
// We can't do much without a reporter, so clear the telemetry file and move on.
await this.telemetry.clear();
return;
debug(`Error creating app insightsreporter: ${error.message}`);
}

try {
const events = await this.telemetry.read();
const { appInsightsEvents, appInsightsErrors, o11yEvents } = this.parseEvents(events);

// Send AppInsights events
if (appInsightsEvents.length > 0) {
if (appInsightsReporter && appInsightsEvents.length > 0) {
appInsightsEvents.forEach((event) => {
const eventName = asString(event.eventName) ?? 'UNKNOWN';
delete event.eventName;
Expand All @@ -80,7 +79,7 @@ export class Uploader {
}

// Send AppInsights errors
if (appInsightsErrors.length > 0) {
if (appInsightsReporter && appInsightsErrors.length > 0) {
appInsightsErrors.forEach((event) => {
const error = new Error();
// We know this is an object because it is logged as such
Expand All @@ -98,31 +97,52 @@ export class Uploader {

// Send PDP events via O11y
if (o11yEvents.length > 0) {
try {
o11yReporter = await TelemetryReporter.create({
project: PROJECT,
key: 'not-used',
userId: this.telemetry.getCLIId(),
waitForConnection: true,
enableO11y: true,
enableAppInsights: false,
o11yUploadEndpoint: this.o11yUploadEndpoint,
});
} catch (err) {
const error = SfError.wrap(err);
debug(`Error creating o11y reporter: ${error.message}`);
for (const event of o11yEvents) {
let o11yReporter: InstanceType<typeof TelemetryReporter> | undefined;
const o11yReporterKey = event.targetOrgUsername ?? 'default';
if (!o11yReporterMap.has(o11yReporterKey)) {
try {
const telemetryReporterOptions: TelemetryOptions = {
project: PROJECT,
key: 'not-used',
userId: this.telemetry.getCLIId(),
waitForConnection: true,
enableO11y: true,
enableAppInsights: false,
o11yUploadEndpoint: this.o11yUploadEndpoint,
// We shouldn't batch events from the plugin since it uses a 30 second
// timer and will keep the uploader process alive.
o11yBatching: { enableAutoBatching: false },
};
if (event.targetOrgUsername) {
telemetryReporterOptions.getConnectionFn = async (): Promise<Connection> =>
(await Org.create({ aliasOrUsername: event.targetOrgUsername! })).getConnection();
}
// eslint-disable-next-line no-await-in-loop
o11yReporter = await TelemetryReporter.create(telemetryReporterOptions);
o11yReporterMap.set(o11yReporterKey, o11yReporter);
} catch (err) {
const error = SfError.wrap(err);
debug(`Error creating o11y reporter for PDP event: ${error.message}`);
}
} else {
o11yReporter = o11yReporterMap.get(o11yReporterKey);
}
delete event.targetOrgUsername;
o11yReporter?.sendPdpEvent(event);
}
o11yEvents.forEach((event) => o11yReporter?.sendPdpEvent(event));
}
} catch (err) {
const error = SfError.wrap(err);
debug(`Error reading or sending telemetry events: ${error.message}`);
} finally {
try {
// We are done sending events
appInsightsReporter.stop();
if (o11yReporter) {
o11yReporter.stop();
appInsightsReporter?.stop();
if (o11yReporterMap.size > 0) {
for (const reporter of o11yReporterMap.values()) {
reporter.stop();
}
}
} catch (err) {
const error = SfError.wrap(err);
Expand All @@ -137,22 +157,24 @@ export class Uploader {
private parseEvents(events: Attributes[]): {
appInsightsEvents: Attributes[];
appInsightsErrors: Attributes[];
o11yEvents: PdpEvent[];
o11yEvents: PdpEventWithUsername[];
} {
const appInsightsEvents: Attributes[] = [];
const appInsightsErrors: Attributes[] = [];
const o11yEvents: PdpEvent[] = [];
const o11yEvents: PdpEventWithUsername[] = [];
for (const event of events) {
event.telemetryVersion = this.version;
const eventType = asString(event.type) ?? Telemetry.EVENT;
const eventName = asString(event.eventName) ?? 'UNKNOWN';
const enableO11y = asBoolean(event.enableO11y) ?? false;
const productFeatureId = asString(event.productFeatureId) ?? 'aJCEE0000000mHP4AY';
const targetOrgUsername = asString(event.targetOrgUsername) ?? undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you need them to eventually be string|undefined,

I'd use string? and undefined un the commandExecution stuff (instead of null) just to save some steps

this.o11yUploadEndpoint = asString(event.o11yUploadEndpoint) ?? '';
delete event.type;
delete event.enableO11y;
delete event.o11yUploadEndpoint;
delete event.productFeatureId;
delete event.targetOrgUsername;

if (eventType === Telemetry.EVENT) {
appInsightsEvents.push(event);
Expand All @@ -161,6 +183,7 @@ export class Uploader {
const commandName = `${asString(event.command) ?? 'unknownCommand'}`;
o11yEvents.push({
eventName: 'salesforceCli.executed',
targetOrgUsername,
productFeatureId: productFeatureId as `aJC${string}`,
componentId: `${pluginName}.${commandName}`,
contextName: 'orgId::devhubId', // Delimited string of keys
Expand Down
51 changes: 51 additions & 0 deletions test/uploader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,26 @@ describe('uploader', () => {
expect(pdpEvent.contextValue).to.equal('org1::hub1');
});

it('creates O11y reporter for PDP events with o11yBatching disabled', async () => {
readStub.resolves([
{
eventName: 'COMMAND_EXECUTION',
type: Telemetry.EVENT,
enableO11y: true,
o11yUploadEndpoint: 'https://o11y.example.com',
productFeatureId: 'aJCEE0000000mHP4AY',
plugin: 'myPlugin',
command: 'myCommand',
},
]);

await Uploader.upload('test', 'test', '1.0.0');

expect(createStub.calledTwice).to.equal(true);
const o11yReporterOptions = createStub.secondCall.args[0];
expect(o11yReporterOptions.o11yBatching).to.deep.equal({ enableAutoBatching: false });
});

it('does not create O11y reporter or call sendPdpEvent when no COMMAND_EXECUTION has enableO11y', async () => {
readStub.resolves([
{
Expand Down Expand Up @@ -180,4 +200,35 @@ describe('uploader', () => {
expect(sendPdpEventStub.called).to.equal(false);
expect(clearStub.called).to.equal(true);
});

it('when AppInsights reporter creation fails, clears telemetry file in outer finally block', async () => {
readStub.resolves([]);
createStub.onFirstCall().rejects(new Error('AppInsights create failed'));

await Uploader.upload('test', 'test', '1.0.0');

expect(clearStub.called).to.equal(true);
});

it('when COMMAND_EXECUTION has targetOrgUsername and enableO11y, creates O11y reporter with getConnectionFn in options', async () => {
readStub.resolves([
{
eventName: 'COMMAND_EXECUTION',
type: Telemetry.EVENT,
enableO11y: true,
o11yUploadEndpoint: 'https://o11y.example.com',
productFeatureId: 'aJCEE0000000mHP4AY',
plugin: 'myPlugin',
command: 'myCommand',
orgId: 'org1',
devhubId: 'hub1',
targetOrgUsername: 'user@example.com',
},
]);

await Uploader.upload('test', 'test', '1.0.0');

expect(createStub.calledTwice).to.equal(true);
expect(createStub.secondCall.args[0].getConnectionFn).to.be.a('function');
});
});
Loading
Loading