Skip to content
Draft
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
51 changes: 44 additions & 7 deletions src/panels/migration/helpers/migrationTelemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand Down
20 changes: 11 additions & 9 deletions src/panels/migration/steps/phase3SchemaConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/panels/migration/steps/phase4Provisioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ export async function runProvisioning(ctx: Phase4Context): Promise<void> {
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;
Expand All @@ -198,7 +201,6 @@ export async function runProvisioning(ctx: Phase4Context): Promise<void> {
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;
}
}
Expand Down Expand Up @@ -660,7 +662,6 @@ export async function runProvisioning(ctx: Phase4Context): Promise<void> {

// Structural metrics
context.telemetry.measurements.containersCreated = containersCreated.length;
context.telemetry.properties.sampleDataInserted = 'true';

await sendPhaseEvent(channel, 'provisioningCompleted', [
{
Expand Down
Loading