diff --git a/LICENSE.txt b/LICENSE.txt index ca35d0df..1aeebc57 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ Apache License Version 2.0 -Copyright (c) 2025 Salesforce, Inc. +Copyright (c) 2026 Salesforce, Inc. All rights reserved. Apache License diff --git a/package.json b/package.json index 06eea44c..b4345706 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { "@oclif/core": "^4.8.0", - "@salesforce/core": "^8.24.0", - "@salesforce/kit": "^3.2.3", + "@salesforce/core": "^8.25.1", + "@salesforce/kit": "^3.2.4", "@salesforce/sf-plugins-core": "^12", - "@salesforce/telemetry": "^6.4.3", - "@salesforce/ts-types": "^2.0.11", + "@salesforce/telemetry": "^6.6.0", + "@salesforce/ts-types": "^2.0.12", "debug": "^4.4.3" }, "devDependencies": { diff --git a/src/commandExecution.ts b/src/commandExecution.ts index a7c34404..11931c36 100644 --- a/src/commandExecution.ts +++ b/src/commandExecution.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,10 @@ * limitations under the License. */ +import fs from 'node:fs/promises'; +import path from 'node:path'; import { Config, Command, Flags, Parser } from '@oclif/core'; -import { Org } from '@salesforce/core'; +import { Org, SfError } from '@salesforce/core'; import { AsyncCreatable } from '@salesforce/kit'; import { isNumber, JsonMap, Optional } from '@salesforce/ts-types'; import { parseVarArgs } from '@salesforce/sf-plugins-core'; @@ -49,6 +51,9 @@ export class CommandExecution extends AsyncCreatable { private orgApiVersion?: string | null; private devhubApiVersion?: string | null; private argKeys: string[] = []; + private enableO11y?: boolean; + private o11yUploadEndpoint?: string; + private productFeatureId?: string; public constructor(options: CommandExecutionOptions) { super(options); @@ -77,6 +82,9 @@ export class CommandExecution extends AsyncCreatable { nodeEnv: process.env.NODE_ENV, nodeVersion: process.version, processUptime: process.uptime() * 1000, + enableO11y: this.enableO11y, + o11yUploadEndpoint: this.o11yUploadEndpoint, + productFeatureId: this.productFeatureId, // CLI information version: this.config.version, @@ -178,6 +186,39 @@ export class CommandExecution extends AsyncCreatable { ? (flags['target-dev-hub'] as unknown as Org).getConnection().getApiVersion() : null; this.determineSpecifiedFlags(argv, flags, flagDefinitions); + + // Read o11y configuration from the plugin's package.json (plugin that owns the command) + const pluginRoot = this.command.plugin?.root; + if (pluginRoot) { + await this.setO11yConfig(pluginRoot); + } + } + + // Get and set the O11y configuration from the plugin's package.json + private async setO11yConfig(pluginRoot: string): Promise { + try { + const pjsonPath = path.join(pluginRoot, 'package.json'); + const pjsonContents = await fs.readFile(pjsonPath, 'utf-8'); + const pjson = JSON.parse(pjsonContents) as Record; + const rawEnableO11y = pjson.enableO11y; + this.enableO11y = + typeof rawEnableO11y === 'boolean' ? rawEnableO11y : String(rawEnableO11y ?? '').toLowerCase() === 'true'; + const endpoint = pjson.o11yUploadEndpoint; + this.o11yUploadEndpoint = typeof endpoint === 'string' && endpoint.length > 0 ? endpoint : undefined; + const productFeatureId = pjson.productFeatureId; + this.productFeatureId = + typeof productFeatureId === 'string' && productFeatureId.length > 0 ? productFeatureId : undefined; + if (this.productFeatureId) { + if (!this.productFeatureId.startsWith('aJC')) { + debug('WARNING: productFeatureId must start with "aJC" prefix. Found: ${this.productFeatureId}.'); + } else { + debug('O11y PDP Config - productFeatureId overridden in package.json to: ${this.productFeatureId}'); + } + } + } catch (err) { + const error = SfError.wrap(err); + debug('Could not read plugin package.json for o11y config', error.message); + } } private determineSpecifiedFlags(argv: string[], flags: Parser.FlagInput, flagDefinitions: Parser.FlagInput): void { diff --git a/src/commands/telemetry.ts b/src/commands/telemetry.ts index 4566a029..c74109af 100644 --- a/src/commands/telemetry.ts +++ b/src/commands/telemetry.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/debugger.ts b/src/debugger.ts index 022fab74..e028e1fb 100644 --- a/src/debugger.ts +++ b/src/debugger.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/gatherEnvs.ts b/src/gatherEnvs.ts index 3f6a4bbf..57f86ccb 100644 --- a/src/gatherEnvs.ts +++ b/src/gatherEnvs.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/guessCI.ts b/src/guessCI.ts index 16ca55f6..12a12c69 100644 --- a/src/guessCI.ts +++ b/src/guessCI.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/hooks/telemetryPrerun.ts b/src/hooks/telemetryPrerun.ts index c2bf5ad9..f5a2b6eb 100644 --- a/src/hooks/telemetryPrerun.ts +++ b/src/hooks/telemetryPrerun.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/index.ts b/src/index.ts index 82fe8d00..d4a222f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/telemetry.ts b/src/telemetry.ts index 81d79ba8..eb964fe7 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/telemetryGlobal.ts b/src/telemetryGlobal.ts index 9a4d02f1..81eed5cd 100644 --- a/src/telemetryGlobal.ts +++ b/src/telemetryGlobal.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/uploader.ts b/src/uploader.ts index fdf5d805..771d3bcb 100644 --- a/src/uploader.ts +++ b/src/uploader.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ */ import { SfError } from '@salesforce/core/sfError'; -import { asString, Dictionary } from '@salesforce/ts-types'; +import type { Attributes, PdpEvent } from '@salesforce/telemetry'; +import { asBoolean, asString, Dictionary } from '@salesforce/ts-types'; import Telemetry from './telemetry.js'; import { debug } from './debugger.js'; - import { TelemetryGlobal } from './telemetryGlobal.js'; declare const global: TelemetryGlobal; @@ -28,6 +28,8 @@ 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 class Uploader { + private o11yUploadEndpoint: string = ''; + private constructor(private telemetry: Telemetry, private version: string) {} /** @@ -44,16 +46,20 @@ export class Uploader { */ private async sendToTelemetry(): Promise { const { TelemetryReporter } = await import('@salesforce/telemetry'); - let reporter: InstanceType; + let appInsightsReporter: InstanceType; + let o11yReporter: InstanceType | undefined; + try { - reporter = await TelemetryReporter.create({ + appInsightsReporter = await TelemetryReporter.create({ project: PROJECT, key: APP_INSIGHTS_KEY, userId: this.telemetry.getCLIId(), waitForConnection: true, + enableO11y: false, + enableAppInsights: true, }); } catch (err) { - const error = err as SfError; + 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(); @@ -62,37 +68,64 @@ export class Uploader { try { const events = await this.telemetry.read(); - for (const event of events) { - event.telemetryVersion = this.version; - const eventType = asString(event.type) ?? Telemetry.EVENT; - const eventName = asString(event.eventName) ?? 'UNKNOWN'; - delete event.type; - delete event.eventName; + const { appInsightsEvents, appInsightsErrors, o11yEvents } = this.parseEvents(events); - if (eventType === Telemetry.EVENT) { - reporter.sendTelemetryEvent(eventName, event); - } else if (eventType === Telemetry.EXCEPTION) { + // Send AppInsights events + if (appInsightsEvents.length > 0) { + appInsightsEvents.forEach((event) => { + const eventName = asString(event.eventName) ?? 'UNKNOWN'; + delete event.eventName; + appInsightsReporter.sendTelemetryEvent(eventName, event); + }); + } + + // Send AppInsights errors + if (appInsightsErrors.length > 0) { + appInsightsErrors.forEach((event) => { const error = new Error(); // We know this is an object because it is logged as such const errorObject = event.error as unknown as Dictionary; delete event.error; + delete event.eventName; Object.assign(error, errorObject); error.name = asString(errorObject.name) ?? 'Unknown'; error.message = asString(errorObject.message) ?? 'Unknown'; error.stack = asString(errorObject.stack) ?? 'Unknown'; - reporter.sendTelemetryException(error, event); + appInsightsReporter.sendTelemetryException(error, event); + }); + } + + // 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}`); } + o11yEvents.forEach((event) => o11yReporter?.sendPdpEvent(event)); } } catch (err) { - const error = err as SfError; + const error = SfError.wrap(err); debug(`Error reading or sending telemetry events: ${error.message}`); } finally { try { // We are done sending events - reporter.stop(); + appInsightsReporter.stop(); + if (o11yReporter) { + o11yReporter.stop(); + } } catch (err) { - const error = err as SfError; + const error = SfError.wrap(err); debug(`Error stopping telemetry reporter: ${error.message}`); } finally { // We always want to clear the file. @@ -100,4 +133,44 @@ export class Uploader { } } } + + private parseEvents(events: Attributes[]): { + appInsightsEvents: Attributes[]; + appInsightsErrors: Attributes[]; + o11yEvents: PdpEvent[]; + } { + const appInsightsEvents: Attributes[] = []; + const appInsightsErrors: Attributes[] = []; + const o11yEvents: PdpEvent[] = []; + 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'; + this.o11yUploadEndpoint = asString(event.o11yUploadEndpoint) ?? ''; + delete event.type; + delete event.enableO11y; + delete event.o11yUploadEndpoint; + delete event.productFeatureId; + + if (eventType === Telemetry.EVENT) { + appInsightsEvents.push(event); + if (enableO11y && this.o11yUploadEndpoint.length > 0 && eventName === 'COMMAND_EXECUTION') { + const pluginName = `${asString(event.plugin) ?? 'unknownPlugin'}`; + const commandName = `${asString(event.command) ?? 'unknownCommand'}`; + o11yEvents.push({ + eventName: 'salesforceCli.executed', + productFeatureId: productFeatureId as `aJC${string}`, + componentId: `${pluginName}.${commandName}`, + contextName: 'orgId::devhubId', // Delimited string of keys + contextValue: `${event.orgId ?? ''}::${event.devhubId ?? ''}`, // Delimited string of values + }); + } + } else if (eventType === Telemetry.EXCEPTION) { + appInsightsErrors.push(event); + } + } + return { appInsightsEvents, appInsightsErrors, o11yEvents }; + } } diff --git a/test/commandExecution.test.ts b/test/commandExecution.test.ts index 4a38ee92..3d457682 100644 --- a/test/commandExecution.test.ts +++ b/test/commandExecution.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,9 @@ * limitations under the License. */ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import type { Command } from '@oclif/core'; import { Interfaces, Performance } from '@oclif/core'; import { stubInterface, stubMethod } from '@salesforce/ts-sinon'; import { expect } from 'chai'; @@ -335,4 +338,97 @@ describe('toJson', () => { expect(actual.argKeys).to.equal('baz foo'); }); }); + + describe('O11y / PDP config', () => { + const pluginRoot = path.join('/tmp', 'plugin-telemetry-test-o11y'); + + it('toJson includes O11y fields when plugin has root and package.json has O11y config', async () => { + process.env.CI = 'true'; + const readFileStub = sandbox.stub(fs, 'readFile').resolves( + JSON.stringify({ + enableO11y: true, + o11yUploadEndpoint: 'https://example.com', + productFeatureId: 'aJCEE0000000mHP4AY', + }) + ); + const config = stubInterface(sandbox, {}); + const commandWithPlugin = { + ...MyCommand, + plugin: { name: 'testPlugin', version: '1.0.0', root: pluginRoot }, + } as unknown as Partial; + const execution = await CommandExecution.create({ + argv: [], + command: commandWithPlugin, + config, + }); + const actual = execution.toJson(); + + expect(readFileStub.calledOnce).to.be.true; + expect(readFileStub.firstCall.args[0]).to.equal(path.join(pluginRoot, 'package.json')); + expect(actual.enableO11y).to.equal(true); + expect(actual.o11yUploadEndpoint).to.equal('https://example.com'); + expect(actual.productFeatureId).to.equal('aJCEE0000000mHP4AY'); + }); + + it('toJson includes productFeatureId override from package.json', async () => { + process.env.CI = 'true'; + const customProductFeatureId = 'aJCcustomPlugin123'; + sandbox.stub(fs, 'readFile').resolves( + JSON.stringify({ + enableO11y: true, + o11yUploadEndpoint: 'https://custom.example.com', + productFeatureId: customProductFeatureId, + }) + ); + const config = stubInterface(sandbox, {}); + const commandWithPlugin = { + ...MyCommand, + plugin: { name: 'testPlugin', version: '1.0.0', root: pluginRoot }, + } as unknown as Partial; + const execution = await CommandExecution.create({ + argv: [], + command: commandWithPlugin, + config, + }); + const actual = execution.toJson(); + + expect(actual.productFeatureId).to.equal(customProductFeatureId); + expect(actual.o11yUploadEndpoint).to.equal('https://custom.example.com'); + }); + + it('toJson omits or has undefined O11y fields when plugin has no root', async () => { + process.env.CI = 'true'; + const config = stubInterface(sandbox, {}); + const execution = await CommandExecution.create({ + argv: [], + command: MyCommand, + config, + }); + const actual = execution.toJson(); + + expect(actual.enableO11y).to.equal(undefined); + expect(actual.o11yUploadEndpoint).to.equal(undefined); + expect(actual.productFeatureId).to.equal(undefined); + }); + + it('toJson does not throw and O11y fields remain undefined when package.json read fails', async () => { + process.env.CI = 'true'; + sandbox.stub(fs, 'readFile').rejects(new Error('ENOENT')); + const config = stubInterface(sandbox, {}); + const commandWithPlugin = { + ...MyCommand, + plugin: { name: 'testPlugin', version: '1.0.0', root: pluginRoot }, + } as unknown as Partial; + const execution = await CommandExecution.create({ + argv: [], + command: commandWithPlugin, + config, + }); + const actual = execution.toJson(); + + expect(actual.enableO11y).to.equal(undefined); + expect(actual.o11yUploadEndpoint).to.equal(undefined); + expect(actual.productFeatureId).to.equal(undefined); + }); + }); }); diff --git a/test/commands/telemetry.nut.ts b/test/commands/telemetry.nut.ts index 45894b6b..d4d6719e 100644 --- a/test/commands/telemetry.nut.ts +++ b/test/commands/telemetry.nut.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/gatherEnvs.test.ts b/test/gatherEnvs.test.ts index d284c30c..15b31c6b 100644 --- a/test/gatherEnvs.test.ts +++ b/test/gatherEnvs.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/helpers/getTelemetryFiles.ts b/test/helpers/getTelemetryFiles.ts index 1cdbb6cf..fcc8d470 100644 --- a/test/helpers/getTelemetryFiles.ts +++ b/test/helpers/getTelemetryFiles.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/helpers/myArgCommand.ts b/test/helpers/myArgCommand.ts index 9a0287ab..1c9a6040 100644 --- a/test/helpers/myArgCommand.ts +++ b/test/helpers/myArgCommand.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/helpers/myCommand.ts b/test/helpers/myCommand.ts index 384db8c4..c902b201 100644 --- a/test/helpers/myCommand.ts +++ b/test/helpers/myCommand.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/hooks/telemetryPrerun-disabled.nut.ts b/test/hooks/telemetryPrerun-disabled.nut.ts index e4a9486e..f6c5c91f 100644 --- a/test/hooks/telemetryPrerun-disabled.nut.ts +++ b/test/hooks/telemetryPrerun-disabled.nut.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/hooks/telemetryPrerun.nut.ts b/test/hooks/telemetryPrerun.nut.ts index 901e4ef3..5455b43f 100644 --- a/test/hooks/telemetryPrerun.nut.ts +++ b/test/hooks/telemetryPrerun.nut.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/hooks/telemetryPrerun.test.ts b/test/hooks/telemetryPrerun.test.ts index 0768116a..f672dc73 100644 --- a/test/hooks/telemetryPrerun.test.ts +++ b/test/hooks/telemetryPrerun.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/telemetry.test.ts b/test/telemetry.test.ts index 1ceaf166..4d84f6e8 100644 --- a/test/telemetry.test.ts +++ b/test/telemetry.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/uploader.test.ts b/test/uploader.test.ts index edc07c7c..d6190164 100644 --- a/test/uploader.test.ts +++ b/test/uploader.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025, Salesforce, Inc. + * Copyright 2026, Salesforce, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ describe('uploader', () => { let createStub: sinon.SinonStub; let sendTelemetryEventStub: sinon.SinonStub; let sendTelemetryExceptionStub: sinon.SinonStub; + let sendPdpEventStub: sinon.SinonStub; let stopStub: sinon.SinonStub; let readStub: sinon.SinonStub; let clearStub: sinon.SinonStub; @@ -34,17 +35,25 @@ describe('uploader', () => { beforeEach(() => { sandbox = sinon.createSandbox(); sendTelemetryEventStub = sandbox.stub(); - sendTelemetryExceptionStub = sandbox.stub(); // stubMethod(sandbox, TelemetryReporter.prototype, 'sendTelemetryException'); - stopStub = sandbox.stub(); // stubMethod(sandbox, TelemetryReporter.prototype, 'stop'); + sendTelemetryExceptionStub = sandbox.stub(); + sendPdpEventStub = sandbox.stub(); + stopStub = sandbox.stub(); readStub = sandbox.stub(); clearStub = sandbox.stub(); getCliIdStub = sandbox.stub().returns('testId'); - createStub = stubMethod(sandbox, TelemetryReporter.default, 'create').callsFake(async () => ({ - sendTelemetryEvent: sendTelemetryEventStub, - sendTelemetryException: sendTelemetryExceptionStub, - stop: stopStub, - })); + createStub = stubMethod(sandbox, TelemetryReporter.default, 'create').callsFake( + async (options: { enableO11y?: boolean }) => { + if (options?.enableO11y === true) { + return { sendPdpEvent: sendPdpEventStub, stop: stopStub }; + } + return { + sendTelemetryEvent: sendTelemetryEventStub, + sendTelemetryException: sendTelemetryExceptionStub, + stop: stopStub, + }; + } + ); stubMethod(sandbox, Telemetry, 'create').callsFake(async () => ({ getCLIId: getCliIdStub, read: readStub, @@ -106,4 +115,69 @@ describe('uploader', () => { await Uploader.upload('test', 'test', '1.0.0'); expect(clearStub.called).to.equal(true); }); + + it('sends PDP events when COMMAND_EXECUTION has enableO11y and o11yUploadEndpoint', 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', + }, + ]); + + await Uploader.upload('test', 'test', '1.0.0'); + + expect(createStub.calledTwice).to.equal(true); + expect(sendPdpEventStub.calledOnce).to.equal(true); + const pdpEvent = sendPdpEventStub.firstCall.args[0]; + expect(pdpEvent.eventName).to.equal('salesforceCli.executed'); + expect(pdpEvent.productFeatureId).to.equal('aJCEE0000000mHP4AY'); + expect(pdpEvent.componentId).to.equal('myPlugin.myCommand'); + expect(pdpEvent.contextName).to.equal('orgId::devhubId'); + expect(pdpEvent.contextValue).to.equal('org1::hub1'); + }); + + it('does not create O11y reporter or call sendPdpEvent when no COMMAND_EXECUTION has enableO11y', async () => { + readStub.resolves([ + { + eventName: 'COMMAND_EXECUTION', + type: Telemetry.EVENT, + enableO11y: false, + o11yUploadEndpoint: 'https://o11y.example.com', + plugin: 'myPlugin', + command: 'myCommand', + }, + ]); + + await Uploader.upload('test', 'test', '1.0.0'); + + expect(createStub.calledOnce).to.equal(true); + expect(sendPdpEventStub.called).to.equal(false); + }); + + it('when O11y reporter create fails, does not throw and does not call sendPdpEvent', async () => { + readStub.resolves([ + { + eventName: 'COMMAND_EXECUTION', + type: Telemetry.EVENT, + enableO11y: true, + o11yUploadEndpoint: 'https://o11y.example.com', + productFeatureId: 'aJCEE0000000mHP4AY', + plugin: 'myPlugin', + command: 'myCommand', + }, + ]); + createStub.onSecondCall().rejects(new Error('O11y create failed')); + + await Uploader.upload('test', 'test', '1.0.0'); + + expect(sendPdpEventStub.called).to.equal(false); + expect(clearStub.called).to.equal(true); + }); }); diff --git a/yarn.lock b/yarn.lock index 914bd3a2..1b9e73c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1412,6 +1412,22 @@ node-fetch "^2.6.1" xml2js "^0.6.2" +"@jsforce/jsforce-node@^3.10.13": + version "3.10.13" + resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.10.13.tgz#d1e832178e2e74646c75b952e629ac5b8ceff7d0" + integrity sha512-Ft42/lp3WaVxijcX88Rb3yIxujk/u3LwL3913OTcB4WCpwjB9xTqP6jkVTKt2riXg+ZlNiS62SMpQeC3U1Efkw== + dependencies: + "@sindresorhus/is" "^4" + base64url "^3.0.1" + csv-parse "^5.5.2" + csv-stringify "^6.6.0" + faye "^1.4.0" + form-data "^4.0.4" + https-proxy-agent "^5.0.0" + multistream "^3.1.0" + node-fetch "^2.6.1" + xml2js "^0.6.2" + "@jsonjoy.com/base64@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" @@ -1704,7 +1720,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.18.7", "@salesforce/core@^8.23.3", "@salesforce/core@^8.24.0", "@salesforce/core@^8.5.1", "@salesforce/core@^8.8.0": +"@salesforce/core@^8.18.7", "@salesforce/core@^8.23.3", "@salesforce/core@^8.5.1", "@salesforce/core@^8.8.0": version "8.24.0" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.24.0.tgz#13426f9f3b5ed0ec126b8009e5eda68e03db0401" integrity sha512-8Ra5RT95bRkmHmaaFgABwkXbnHNSNS7l9gbJzJgO6VQpaEeytGPPyymnAE7TcTM2xp/QwlXn+PgX4biX7Lb7JA== @@ -1729,6 +1745,31 @@ ts-retry-promise "^0.8.1" zod "^4.1.12" +"@salesforce/core@^8.25.1": + version "8.25.1" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.25.1.tgz#7646025598bb59b6f95b3656baf8eb0b63a43052" + integrity sha512-Jon0a9uZpp+mNa5PiY+y8dTjaPcsMaxXEkswdzWotrdrZ4g84MmPKSEv+Q/LtXw3uc9i4RmqBJBUXSIvZhgrjg== + dependencies: + "@jsforce/jsforce-node" "^3.10.13" + "@salesforce/kit" "^3.2.4" + "@salesforce/ts-types" "^2.0.12" + ajv "^8.17.1" + change-case "^4.1.2" + fast-levenshtein "^3.0.0" + faye "^1.4.1" + form-data "^4.0.4" + js2xmlparser "^4.0.1" + jsonwebtoken "9.0.3" + jszip "3.10.1" + memfs "^4.30.1" + pino "^9.7.0" + pino-abstract-transport "^1.2.0" + pino-pretty "^11.3.0" + proper-lockfile "^4.1.2" + semver "^7.7.3" + ts-retry-promise "^0.8.1" + zod "^4.1.12" + "@salesforce/dev-config@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-4.3.1.tgz#4dac8245df79d675258b50e1d24e8c636eaa5e10" @@ -1773,10 +1814,10 @@ dependencies: "@salesforce/ts-types" "^2.0.12" -"@salesforce/o11y-reporter@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@salesforce/o11y-reporter/-/o11y-reporter-1.6.0.tgz#b2c5e538d64337c44b69ce5ec8b43c55d6c409eb" - integrity sha512-FAdmfTtRlpuCwXSwPFrpp+mYn8h47EUAaHKL1PpVZYU+DoktVXrDoKTm3MxKRarcqtK1fc4PaO3UOcL5tIT1iA== +"@salesforce/o11y-reporter@1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@salesforce/o11y-reporter/-/o11y-reporter-1.7.3.tgz#14e58bc511fa3529077eff7e6712182574c52a1c" + integrity sha512-Krd2EgHYrTW1j1Wo5q+4kya6F7WjGvJsD4oUKzefLRw92Pb5b7aMorLFsKMEvLTZSGmTZZClLmQMap0gLOV6qw== dependencies: o11y "^258.7.0" o11y_schema "256.154.0" @@ -1834,16 +1875,17 @@ cli-progress "^3.12.0" terminal-link "^3.0.0" -"@salesforce/telemetry@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@salesforce/telemetry/-/telemetry-6.4.3.tgz#593c844e992ac1684323a4f27a3e9712f35d5efc" - integrity sha512-O2/bkrBxpwm5xIKO6m9eP2N5gyCDj57J5pTmagWCMs3fwMurmi/rW3H8tlHnJBWU+iBDdPcxCY7L6QNrmupAPA== +"@salesforce/telemetry@^6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@salesforce/telemetry/-/telemetry-6.6.0.tgz#84fe9ae5da29fe002db977fe794b9865090e9f56" + integrity sha512-frFgpPKEdDuoHyOmhKwxOarULBhkyZSt0sQof7jC08uJe3XQlgET93HhHALhnYkLgoMCfdCmTAyRIgCRknJttQ== dependencies: - "@salesforce/core" "^8.24.0" + "@salesforce/core" "^8.25.1" "@salesforce/kit" "^3.2.4" - "@salesforce/o11y-reporter" "1.6.0" + "@salesforce/o11y-reporter" "1.7.3" applicationinsights "^2.9.8" got "^11" + o11y_schema "^260.41.0" proxy-agent "^6.5.0" "@salesforce/ts-sinon@^1.4.31": @@ -6473,6 +6515,11 @@ o11y_schema@256.154.0: resolved "https://registry.yarnpkg.com/o11y_schema/-/o11y_schema-256.154.0.tgz#1f2f94f1e42d07e62a3e18e09b026345b057dc0c" integrity sha512-czvU/9cibyZptbr0gLJSM70U7zLlhWC2D2L5e9nOG84Wnqmn4F5YzVjrH1ZQzAzDbBbtbeU6WTS3F/SHqtMQ5g== +o11y_schema@^260.41.0: + version "260.42.0" + resolved "https://registry.yarnpkg.com/o11y_schema/-/o11y_schema-260.42.0.tgz#7cbfa43590f4bdfaa30789c527fa02217ad6d41b" + integrity sha512-EV7Vwoh+S4S2p42sTG1mUWnt9ZdMGHeZAuypnV7u1MFp/f5VSWu6N9W/dsgC5TnWFrA1CE7+Va5S4evydjrqaw== + object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"