Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cc8a808
chore: plan for W-22848993
mshanemc Jun 4, 2026
83c0bf4
feat(services): seed telemetry identities on activation
mshanemc Jun 4, 2026
3a5b992
feat(services): emit cliId alongside SOQL userId on spans
mshanemc Jun 4, 2026
3de14de
refactor(utils-vscode): source telemetry identity from services API
mshanemc Jun 5, 2026
f62a97d
chore: delete UserService and shared telemetry provider plumbing
mshanemc Jun 5, 2026
b835c87
test: cover seedTelemetryIdentities, identity wiring, span attributes
mshanemc Jun 5, 2026
e29b806
refactor: review cleanups for telemetry identity sourcing
mshanemc Jun 5, 2026
b56f3af
refactor(services): drop cliId from web exporter
mshanemc Jun 5, 2026
3280343
refactor: human review
mshanemc Jun 5, 2026
aca6b25
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 5, 2026
e85a88f
Merge remote-tracking branch 'origin/sm/W-22848993-retire-userservice…
mshanemc Jun 5, 2026
55ec0a9
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 5, 2026
ca0afb0
refactor: dedupe globalState => orgRef
mshanemc Jun 7, 2026
f12823d
refactor(services,metadata): wrap URI in HashableUri instead of exten…
mshanemc Jun 5, 2026
bbe2270
feat(eslint): add no-self-barrel-import rule - W-22857708 (#7399)
mshanemc Jun 5, 2026
c521895
chore(release): eslint-plugin-vscode-extensions-v65.9.0 [skip ci]
svc-idee-bot Jun 5, 2026
bf09ff2
chore(core): remove unused i18n keys - W-22773045 (#7398)
mshanemc Jun 5, 2026
57f0bd9
feat(playwright-vscode-ext): publish to npm - W-22846720 (#7394)
mshanemc Jun 5, 2026
e374aa4
chore(release): playwright-vscode-ext-v1.1.0 [skip ci]
svc-idee-bot Jun 5, 2026
bb853c7
chore: update salesforce_metadata_api_common.xsd and metadata_types_m…
github-actions[bot] Jun 7, 2026
9f0c0cf
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 7, 2026
3458d98
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 8, 2026
1b7e303
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 8, 2026
28b1b77
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 8, 2026
8c1761c
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 8, 2026
84b85c0
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 12, 2026
c0ea2ba
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 12, 2026
679e6d9
Merge remote-tracking branch 'origin/develop' into sm/W-22848993-reti…
mshanemc Jun 13, 2026
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
160 changes: 160 additions & 0 deletions .claude/plans/W-22848993.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# W-22848993 — retire UserService; services owns telemetry identity

## Context

- delete `userService.ts`, `getSharedTelemetryUserId`, `DefaultSharedTelemetryProvider`
- services = sole telemetry identity source
- legacy reporter fields + `updateReporters` path unchanged
- `TelemetryService.{initializeService,updateReporters}` signatures unchanged
- services transitively required by all consumers

### Identity model

- **stable cliId** — `cliId-or-random`; globalState `telemetryUserId`; `defaultOrgRef.cliId`; AppInsights `ai.user.id` tag + legacy reporter `userId` ctor arg
- **SOQL User.Id** — `defaultOrgRef.userId` (`DefaultOrgInfoSchema:20-21` "the actual userID from the salesforce org"); ships on spans as `userId` prop
- **webUserId** — `sha256(orgId-SOQLuserId)`; globalState `telemetryWebUserId`; placeholder `UNAUTHENTICATED_USER` until first auth
- web: no `sf telemetry --json`; cliId via `Schema.UUID`
- behavioral shift: span `userId` was cliId-stable; becomes SOQL-per-org. New `cliId` attr is the stable replacement. Legacy reporter `userId` ctor arg keeps cliId semantics (AppInsights `ai.user.id`).

### Activation ordering (race avoidance)

- `core` declares `extensionDependencies: ["salesforcedx-vscode-services"]` → VS Code resolves services' `activate()` Promise before invoking core's `activate()`.
- services `activate()` returns the API at the **end** of the activation function only after `Effect.runPromise(activationEffect)` settles. `seedTelemetryIdentities` runs as a **blocking `yield*`** (not `Effect.fork*`) inside `activationEffect`, so by the time core's `getServicesApi` resolves, `defaultOrgRef.cliId` is populated.
- `getServicesApi` does not return mid-`activationEffect`: VS Code's `getExtension(...).activate()` resolves to the value `activate()` returned, which only happens after `await runPromise(activationEffect)`.
- Verified by Phase 5 e2e (executes a real command → reads spans → asserts both `cliId` and `webUserId` present).

## Phases

### Phase 1 — seed identities on services activation

Commit: `feat(services): seed telemetry identities on activation` + `test(core): exercise real getOrgShape effect instead of mocking runPromise`

- `cliTelemetry.ts:40-44`: keep `Effect.cached.pipe(Effect.flatten)` — caches the CLI invocation across the (possibly multiple, defensive) callers in the session. Change return type to `Effect<Option<string>>` via `Effect.map(Option.fromNullable)` so callers consume `Option`. `cached` retained because: (a) `connectionService.ts:280` still calls `getCliId()` defensively if seed didn't populate, (b) future callers shouldn't re-fork `sf telemetry`.
- new file `packages/salesforcedx-vscode-services/src/observability/seedTelemetryIdentities.ts` exporting `seedTelemetryIdentities` Effect, requires `ExtensionContextService`:
- read `telemetryUserId` from globalState → `Option.fromNullable` → `Option.match`:
- `onSome(cliId)`: set `defaultOrgRef.cliId = cliId`
- `onNone`: branch on `process.env.ESBUILD_PLATFORM === 'web'`:
- web: `Schema.decodeSync(Schema.UUID)(globalThis.crypto.randomUUID())` (UUIDv4 is the validated random-id primitive in the codebase; satisfies the "stable random per profile" requirement). Returned via `Effect.sync`.
- desktop: `getCliId()` → `Option.match` → `onNone` falls back to `Schema.UUID`. (Single random helper, used twice.)
- persist to `extensionContext.globalState.update(TELEMETRY_GLOBAL_USER_ID, cliId)` and set `defaultOrgRef.cliId`.
- read `telemetryWebUserId` from globalState → `Option.fromNullable`:
- `onNone`: write `UNAUTHENTICATED_USER` to globalState + `defaultOrgRef.webUserId`
- `onSome`: leave as-is
- `services/index.ts` ordering — `seedTelemetryIdentities` runs as a blocking `yield*` inside `activationEffect`, populating `defaultOrgRef.cliId` before any consumer (connectionService, core) can read it.
- add code comment at `services/index.ts:219`: `// seed populates defaultOrgRef.cliId before connectionService and core can read it`
- `connectionService.ts:280`: keep `existingOrgInfo.cliId ? Effect.succeed(existingOrgInfo.cliId) : getCliId()` as defensive fallback (covers seed failure → see Phase 3 degradation contract). `getCliId()` returns `Option<string>`; map `Option.getOrElse(() => undefined)` to preserve current `cliId: string | undefined` schema slot.

### Phase 2 — span exporters: split SOQL userId and stable cliId

Commit: `feat(services): emit cliId alongside SOQL userId on spans`

- `applicationInsightsWebExporter.ts:101`: change destructure to `{ userId, cliId, webUserId }`. `userId` is already the SOQL slot per `DefaultOrgInfoSchema:20-21` — no semantic swap, only an additive `cliId`. Add `...(cliId ? { cliId } : {})` to props at `:113`.
- `o11ySpanExporter.ts:73-86`: add `userId` destructure (currently absent); replace `userId: cliId` with `...(userId ? { userId } : {})`; add `...(cliId ? { cliId } : {})`.
- `spanTransformProcessor.ts:40-60`: add `userId` destructure; `["userId", cliId]` → `["userId", userId]`; add `["cliId", cliId]`.
- commit body: explicitly note "userId on spans was previously the stable cliId; this commit makes it the SOQL User.Id and adds a separate `cliId` attribute. AppInsights `ai.user.id` is unaffected (driven by reporter ctor, not span attrs)."

### Phase 3 — TelemetryService reads identity from services API

Commit: `refactor(utils-vscode): TelemetryService sources identity from services API`

- `packages/salesforcedx-utils-vscode/src/services/telemetry.ts`: add `getIdentityFromServices` Promise helper:
```
Effect.runPromise(
getServicesApi.pipe(
Effect.flatMap(api => api.services.TargetOrgRef()),
Effect.flatMap(SubscriptionRef.get),
Effect.map(({ cliId, userId, webUserId }) => ({
cliId,
userId,
webUserId: webUserId ?? UNAUTHENTICATED_USER
})),
Effect.tapError(e => Effect.log(`getIdentityFromServices error: ${String(e)}`)),
Effect.catchTags({
ServicesExtensionNotFoundError: () =>
Effect.succeed({ cliId: undefined, userId: undefined, webUserId: UNAUTHENTICATED_USER }),
InvalidServicesApiError: () =>
Effect.succeed({ cliId: undefined, userId: undefined, webUserId: UNAUTHENTICATED_USER })
})
)
)
```
- **error-flow contract** (correcting prior plan ambiguity): `tapError` is a side-effect that runs when the upstream `Effect` fails, **before** the failure reaches `catchTags`. `catchTags` then recovers `ServicesExtensionNotFoundError`/`InvalidServicesApiError` to a typed default. Outcome: both error tags are logged once via `tapError`, then recovered via `catchTags` → `runPromise` always resolves (never rejects) for those two tags. Any **other** error tag would still propagate and reject — verified by enumerating `getServicesApi`'s error channel (only those two tags per `services-extension-consumption` skill).
- **degraded-session contract**: when seed failed or services not yet ready, `cliId === undefined` and `userId === undefined`. Caller (`initializeService`) treats undefined cliId as a degraded telemetry session: passes `""` to reporter ctors and emits a single `ChannelService` warning. `webUserId` always falls back to `UNAUTHENTICATED_USER` (never undefined) so spans always have a user dimension.
- exported at file level for jest spy; **not** in `utils-vscode/src/index.ts` barrel.
- `initializeService(extensionContext)`:
- replace `UserService.getTelemetryUserId` + `getWebTelemetryUserId` with `await getIdentityFromServices()`
- destructure `{ cliId, userId, webUserId }`
- pass `cliId ?? ""` as `userId` ctor arg to AppInsights/O11yReporter (legacy contract: ctor `userId` slot = stable cliId, drives `ai.user.id`)
- if `cliId === undefined`: `ChannelService.getInstance(this.extensionName).appendLine("telemetry seed missing — degraded session")` (single line; no retry — Phase 5 e2e covers happy path; manual smoke (services disabled) covers degraded path)
- `updateReporters(extensionContext)`: identical swap; same warn.
- delete `getSharedTelemetryProvider` callsite usage; drop `UserService` / `getWebTelemetryUserId` / `DefaultSharedTelemetryProvider` imports.
- `workspaceContext.ts:90-105 handleTelemetryUpdate`: drop `UserService.getTelemetryUserId`; keep `refreshAllExtensionReporters`.

### Phase 4 — delete UserService + dead helpers

Commit: `chore: delete UserService and shared telemetry provider plumbing`

- delete `packages/salesforcedx-utils-vscode/src/services/userService.ts`
- delete `packages/salesforcedx-utils-vscode/test/jest/services/source-tracking/userService.test.ts`
- `utils-vscode/src/index.ts:67`: remove `export { UserService }`
- `helpers/telemetryUtils.ts`: delete `getSharedTelemetryUserId`, `SalesforceVSCodeCoreApi`, `hashUserIdentifier`; retain `updateUserIDOnTelemetryReporters`

### Phase 5 — tests + e2e assertion

Commit: `test: cover seedTelemetryIdentities, identity wiring, span attributes`

#### Jest

- new spec `seedTelemetryIdentities.test.ts`: globalState hit; desktop CLI fallback (`getCliId` → Some); desktop UUID fallback (`getCliId` → None); web UUID (`process.env.ESBUILD_PLATFORM = "web"`); `UNAUTHENTICATED_USER` written only when `telemetryWebUserId` missing.
- new spec `getIdentityFromServices.test.ts`:
- happy path: returns `{ cliId, userId, webUserId }` from ref
- `ServicesExtensionNotFoundError` tag → resolves to `{ cliId: undefined, userId: undefined, webUserId: UNAUTHENTICATED_USER }` (does **not** reject)
- `InvalidServicesApiError` tag → same default
- `webUserId` undefined in ref → returns `UNAUTHENTICATED_USER`
- update `core test/jest/telemetry/index.test.ts`: drop `UserService` mocks; spy `getIdentityFromServices`; assert reporter ctors receive `cliId` as their `userId` arg; assert `ChannelService.appendLine("telemetry seed missing — degraded session")` fires when cliId undefined.
- new span exporter spec: `userId` (SOQL) + `cliId` (stable) ship as separate props from `o11ySpanExporter` + `spanTransformProcessor`; `applicationInsightsWebExporter` test mocks `process.env.ESBUILD_PLATFORM = "web"`.
- extend connectionService test: `setWebUserId` fires on org change with `UNAUTHENTICATED_USER` placeholder; updates `defaultOrgRef.webUserId`.

#### E2E (real activation flow)

- new spec `packages/salesforcedx-vscode-org/test/playwright/specs/telemetryIdentitySeeding.desktop.spec.ts`:
- new fixture file `test/playwright/fixtures/orgDesktopWithOrgFixtures.ts` exporting `orgDesktopWithOrgTest = createDesktopTest({ fixturesDir, orgAlias: MINIMAL_ORG_ALIAS, additionalExtensionDirs: ['salesforcedx-vscode-core'] })` — palette-only `orgDesktopTest` is insufficient (no `.sfdx/config.json`, no real org → no command actually runs against a target → no useful spans).
- test body:
1. `waitForVSCodeWorkbench` + `closeWelcomeTabs`
2. `executeCommandWithCommandPalette(page, packageNls.config_set_org_text)` against the minimal scratch org (selects MINIMAL_ORG_ALIAS) — triggers core activation → services activation → `seedTelemetryIdentities` → reporter init → telemetry span emission for the command.
3. wait for command completion (output channel signal or status bar idle).
4. read newest `~/.sf/vscode-spans/*.jsonl` (sort filenames descending — ISO timestamps sort lexically).
5. parse JSONL; assert at least one span has both `cliId` (non-empty string) **and** `webUserId` (`UNAUTHENTICATED_USER` is acceptable pre-auth) attributes.
- span dump auto-enabled by `createDesktopTest` (per `span-file-export` skill).
- rationale: `orgCommands.desktop.spec.ts:32-34` only calls `verifyCommandExists` (palette presence check, no execution, no telemetry) — would not exercise the seeding flow. Net-new spec required.

## Skills

- typescript
- effect-best-practices
- services-extension-consumption
- external-consumers
- playwright-e2e
- span-file-export
- verification

## Verification

- `effect-advocate` subagent on diff (Phase 1, 3 introduce Effect changes)
- `npm run check:dupes`
- `npm run compile`
- jest specs added in Phase 5
- `npm run test:desktop -w salesforcedx-vscode-org -- --retries 0` — runs the new `telemetryIdentitySeeding.desktop.spec.ts` against MINIMAL scratch org; assertion proves `seedTelemetryIdentities` ran and span pipeline carries both identity dimensions

### Manual smoke (post-PR; not gated by CI)

- desktop happy path: `npm run vscode:package`; reset profile (`code --user-data-dir /tmp/freshprofile --extensions-dir /tmp/freshprofile-ext`); enable span dump; `SFDX: List Org Aliases` → `cliId` populated in spans; quit/rerun → `cliId` persists; `SFDX: Authorize an Org` → `webUserId` becomes sha256, `userId` becomes SOQL User.Id.
- desktop degraded path: launch with services extension disabled (or simulate by throwing in `seedTelemetryIdentities`); confirm core writes `"telemetry seed missing — degraded session"` to its channel and reporters initialize with empty cliId (no crash).
- web: playwright-e2e web harness; span dump shows UUID `cliId`; `webUserId = UNAUTHENTICATED_USER` until first auth.

## External-API impact

- `TelemetryService.{initializeService,updateReporters}` signatures unchanged → vscode-agents, metadata-visualizer, einstein-gpt unaffected
- `UserService` exported from `@salesforce/salesforcedx-utils-vscode`; gh search → zero external importers
- `refreshAllExtensionReporters` retained
- behavioral: span `userId` semantics shift cliId-stable → SOQL-per-org; legacy reporter `userId` ctor arg keeps cliId semantics; new `cliId` span attr is stable replacement
5 changes: 2 additions & 3 deletions .claude/skills/typescript/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ name: typescript
description: TypeScript coding standards and conventions including file naming rules
---

- new file copyright header: use year **2026** (e.g. `Copyright (c) 2026, salesforce.com, inc.`)
- no barrel files
- avoid type assertions (`as Foo` or `as unknown as` or `Foo!`). do guards or Effect.schema stuff (ex `is`) instead
- no `void` for async - use async/effect (exception [vscode-window-messages](../vscode-window-messages/SKILL.md))
- no `export *` - name exports explicitly
- prefer `undefined` over null (unless server requires null)
Expand All @@ -16,6 +14,7 @@ description: TypeScript coding standards and conventions including file naming r
- no enums or namespaces (enums compile to weird JS; use string union types instead; exception: interfaces defined outside this repo that we can't change)
- no runtime errors for developer mistakes (use types to ensure exhaustive switch/case; don't throw for null/undefined when input/consumer is within our control)
- .ts filenames: camelCase, no hyphens, no leading capitals
- preserve comments when refactoring; remove if wrong/obsolete
- preserve comments when refactoring; remove/fix if wrong/obsolete
- exported functions: single-line jsdoc /\*_ foo _/ if name unclear; no params/return (TS provides types)
- look for uses of (Object|Map).groupBy instead of older patterns
- redundant empty-collection guards: if `arr.find/some/every/map/filter/reduce` already returns the same value for an empty array, drop the `if (arr.length === 0) return …` guard. e.g. `find` on `[]` is `undefined`, so guarding `return undefined` is dead code.
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,12 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { createHash } from 'node:crypto';
import { ExtensionContext, extensions } from 'vscode';
import { ExtensionContext } from 'vscode';
import { TelemetryServiceProvider, TelemetryService } from '../services/telemetry';

// Type definition for the Core extension API
interface SalesforceVSCodeCoreApi {
getSharedTelemetryUserId?: () => Promise<string>;
}

/**
* Attempts to get the shared telemetry user ID from the Core extension.
* Returns undefined if the Core extension is not available or doesn't have the method.
*/
export const getSharedTelemetryUserId = async (): Promise<string | undefined> => {
try {
const coreExtension = extensions.getExtension<SalesforceVSCodeCoreApi>('salesforce.salesforcedx-vscode-core');
if (coreExtension?.isActive && coreExtension.exports?.getSharedTelemetryUserId) {
return await coreExtension.exports.getSharedTelemetryUserId();
}
} catch (error) {
// Silently ignore errors - we'll fall back to extension-specific storage
console.log(`Failed to get shared telemetry user ID: ${String(error)}`);
}
return undefined;
};

/**
* Creates a one-way hash of orgId and userId for telemetry compliance.
* This ensures customer data cannot be decoded while maintaining user distinction.
* The result is stored in the "webUserId" field for telemetry purposes.
*/
export const hashUserIdentifier = (orgId: string, userId: string): string =>
createHash('sha256').update(`${orgId}-${userId}`).digest('hex');

/**
* Ensures that all extensions (Core, Apex, etc.) use the updated hashed userID and webUserId in the webUserId field.
* Ensures that all extensions (Core, Apex, etc.) refresh their telemetry reporters so the next event
* uses the latest identity values sourced from the services extension.
*/
export const updateUserIDOnTelemetryReporters = async (coreExtensionContext: ExtensionContext): Promise<void> => {
console.log('Updating userID and WebID telemetry reporters for all extensions...');
Expand Down
2 changes: 1 addition & 1 deletion packages/salesforcedx-utils-vscode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ export type { ContinueResponse, CancelResponse, ParametersGatherer } from './com
export type { PreconditionChecker } from './commands/preconditionCheckers';
export type { PostconditionChecker } from './commands/postconditionCheckers';
export { ConfigAggregatorProvider } from './providers/configAggregatorProvider';
export { UserService } from './services/userService';
export { SettingsService } from './settings/settingsService';
export { code2ProtocolConverter } from './languageClients/conversion';
Loading
Loading