diff --git a/src/panels/migration/helpers/migrationTelemetry.ts b/src/panels/migration/helpers/migrationTelemetry.ts index f5825f94e..ef6c0820a 100644 --- a/src/panels/migration/helpers/migrationTelemetry.ts +++ b/src/panels/migration/helpers/migrationTelemetry.ts @@ -48,10 +48,24 @@ export function setMigrationTelemetryContext( context.telemetry.properties.sourceDbType = dbType; } - // Mask only tenantId — other Azure org info is safe to send as plain text - const tenantId = project.phases.targetEnvironment?.tenantId; - if (tenantId) { - context.valuesToMask.push(tenantId); + // OII identifiers are emitted as-is under their predefined property names + // (`subscriptionId`, `tenantId`, `accountName`), but they should still be + // redacted from any error messages emitted alongside the event. + const targetEnv = project.phases.targetEnvironment; + if (targetEnv?.tenantId) { + context.valuesToMask.push(targetEnv.tenantId); + } + if (targetEnv?.subscriptionId) { + context.valuesToMask.push(targetEnv.subscriptionId); + } + const targetAccountName = + targetEnv?.accountName || + (targetEnv?.endpoint ? extractAccountNameFromEndpoint(targetEnv.endpoint) : undefined); + if (targetAccountName) { + context.valuesToMask.push(targetAccountName); + } + if (targetEnv?.resourceGroup) { + context.valuesToMask.push(targetEnv.resourceGroup); } // Stamp issueProperties so Report Issue always includes basic migration context @@ -142,10 +156,33 @@ export function enrichErrorContext(context: IActionContext, error: unknown): voi context.telemetry.properties.aiErrorCode = error.code; context.errorHandling.issueProperties.errorCategory = 'ai'; context.errorHandling.issueProperties.aiErrorCode = error.code; - if (error.code === 'Unknown' && error.cause) { - // Only in telemetry — not in issueProperties to avoid leaking model internals + if (error.code === 'Unknown' && error.cause !== undefined && error.cause !== null) { const cause = error.cause; - context.telemetry.properties.aiErrorCause = cause instanceof Error ? cause.message : JSON.stringify(cause); + + // Bounded: constructor name for Errors, typeof for everything else. + // Both come from the JS runtime, not from user/model input. + context.telemetry.properties.aiErrorCauseType = + cause instanceof Error ? cause.constructor.name : typeof cause; + + // Bounded: a short alphanumeric code if the cause exposes one. + // The regex enforces the allowlist — anything else is dropped. + const rawCode = + (cause as { code?: unknown }).code ?? + (cause as { name?: unknown }).name ?? + (cause as { status?: unknown }).status; + if (typeof rawCode === 'string' || typeof rawCode === 'number') { + const code = String(rawCode); + if (/^[A-Za-z0-9_.-]{1,64}$/.test(code)) { + context.telemetry.properties.aiErrorCauseCode = code; + } + } + + // Raw message: issue body only, never telemetry. The Report Issue + // flow is user-consented and applies `valuesToMask` to redact + // anything the action already marked sensitive. + if (cause instanceof Error && cause.message) { + context.errorHandling.issueProperties.aiErrorCauseMessage = cause.message; + } } } else { context.telemetry.properties.errorCategory = 'infrastructure'; diff --git a/src/panels/migration/steps/phase3SchemaConversion.ts b/src/panels/migration/steps/phase3SchemaConversion.ts index e2cb60d3b..91a7c7385 100644 --- a/src/panels/migration/steps/phase3SchemaConversion.ts +++ b/src/panels/migration/steps/phase3SchemaConversion.ts @@ -829,7 +829,9 @@ export async function runSchemaConversion( const domainOutputPath = path.join(domainsPath, domainFileName); await vscode.workspace.fs.createDirectory(vscode.Uri.file(domainOutputPath)); - const domainLabel = `domain${di + 1}of${filteredDomainFiles.length}`; + // Track which domain (by index, not name) the most recent step belongs to in telemetry. + context.telemetry.measurements.domainIndex = di + 1; + context.telemetry.measurements.domainTotal = filteredDomainFiles.length; const progress = (step: string) => `${domainName} (${di + 1}/${filteredDomainFiles.length}): ${step}`; const domainDebugDir = path.join(domainOutputPath, 'debug-prompts'); @@ -854,7 +856,7 @@ export async function runSchemaConversion( // ── Thorough mode: 7 sequential sub-steps ──────────── // ── Sub-step 1: Container Design ───────────────────────── - context.telemetry.properties.lastStep = `${domainLabel}.containerDesign`; + context.telemetry.properties.lastStep = 'containerDesign'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -880,7 +882,7 @@ export async function runSchemaConversion( if (token.isCancellationRequested) return; // ── Sub-step 2: Partition Key Selection ─────────────────── - context.telemetry.properties.lastStep = `${domainLabel}.partitionKey`; + context.telemetry.properties.lastStep = 'partitionKey'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -908,7 +910,7 @@ export async function runSchemaConversion( if (token.isCancellationRequested) return; // ── Sub-step 3: Embedding Decisions ────────────────────── - context.telemetry.properties.lastStep = `${domainLabel}.embedding`; + context.telemetry.properties.lastStep = 'embedding'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -943,7 +945,7 @@ export async function runSchemaConversion( if (token.isCancellationRequested) return; // ── Sub-step 4: Access Patterns ────────────────────────── - context.telemetry.properties.lastStep = `${domainLabel}.accessPatterns`; + context.telemetry.properties.lastStep = 'accessPatterns'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -986,7 +988,7 @@ export async function runSchemaConversion( if (token.isCancellationRequested) return; // ── Sub-step 5: Cross-Partition Analysis ───────────────── - context.telemetry.properties.lastStep = `${domainLabel}.crossPartition`; + context.telemetry.properties.lastStep = 'crossPartition'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -1029,7 +1031,7 @@ export async function runSchemaConversion( if (token.isCancellationRequested) return; // ── Sub-step 6: Indexing Design ────────────────────────── - context.telemetry.properties.lastStep = `${domainLabel}.indexing`; + context.telemetry.properties.lastStep = 'indexing'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -1070,7 +1072,7 @@ export async function runSchemaConversion( if (token.isCancellationRequested) return; // ── Sub-step 7: Summary ────────────────────────────────── - context.telemetry.properties.lastStep = `${domainLabel}.summary`; + context.telemetry.properties.lastStep = 'summary'; await sendPhaseProgress( channel, 'SchemaConversion', @@ -1111,7 +1113,7 @@ export async function runSchemaConversion( ); } else { // ── Fast mode: single-pass conversion ──────────────── - context.telemetry.properties.lastStep = `${domainLabel}.fastConversion`; + context.telemetry.properties.lastStep = 'fastConversion'; await sendPhaseProgress( channel, diff --git a/src/panels/migration/steps/phase4Provisioning.ts b/src/panels/migration/steps/phase4Provisioning.ts index e163c4779..6f0e18ab9 100644 --- a/src/panels/migration/steps/phase4Provisioning.ts +++ b/src/panels/migration/steps/phase4Provisioning.ts @@ -189,7 +189,10 @@ export async function runProvisioning(ctx: Phase4Context): Promise { context.errorHandling.forceIncludeInReportIssueCommand = true; incrementRunCount(project, 'provisioning'); - // Log target environment info + // Log target environment info. Only OII identifiers under their + // predefined property names are emitted (`accountName`, `subscriptionId`). + // The resource group is part of the resource path — do not emit + // its parts under separate keys; it stays out of telemetry. const targetEnv = project.phases.targetEnvironment; if (targetEnv) { context.telemetry.properties.targetType = targetEnv.type; @@ -198,7 +201,6 @@ export async function runProvisioning(ctx: Phase4Context): Promise { targetEnv.accountName || (targetEnv.endpoint ? extractAccountNameFromEndpoint(targetEnv.endpoint) : undefined); if (acctName) context.telemetry.properties.accountName = acctName; - if (targetEnv.resourceGroup) context.telemetry.properties.resourceGroup = targetEnv.resourceGroup; if (targetEnv.subscriptionId) context.telemetry.properties.subscriptionId = targetEnv.subscriptionId; } } @@ -660,7 +662,6 @@ export async function runProvisioning(ctx: Phase4Context): Promise { // Structural metrics context.telemetry.measurements.containersCreated = containersCreated.length; - context.telemetry.properties.sampleDataInserted = 'true'; await sendPhaseEvent(channel, 'provisioningCompleted', [ {