diff --git a/README.md b/README.md index d396718..8eaa308 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,21 @@ The authentication process will try each method in sequence until successful aut Note: For local development, Azure CLI or VS Code authentication is recommended. +## Application Insights Integration + +The extension now provides improved support for Application Insights queries: + +- **Trusted Endpoint Configuration**: The Application Insights API endpoint (`api.applicationinsights.io`) is automatically registered as a trusted endpoint, resolving previous "valid authentication not provided" errors +- **Custom API Client**: Uses a specialized client for Application Insights that properly handles API key authentication and response formats +- **Improved Error Messages**: Clearer error messages when App Insights credentials are missing or expired, with guidance on how to resolve the issue + +### Connecting to Application Insights + +1. Use the command `Configure Kusto Connection` +2. Select Application Insights as the connection type +3. Enter your Application Insights App ID and App Key +4. The extension will securely store your credentials + ## Getting Started - Open a `*.kql|*.csl` file and start typing to get code completion diff --git a/src/client/datatable.tsx b/src/client/datatable.tsx index 997bb6f..8d7989d 100644 --- a/src/client/datatable.tsx +++ b/src/client/datatable.tsx @@ -73,44 +73,108 @@ function createAgGridData(resultTable: KustoResultTable | any) { columnDataType.clear(); // Handle both KustoResultTable object and plain JSON from serialization - const isPlainJson = !resultTable.columns && resultTable.data; + // After JSON serialization, the rows() generator function is lost, so we need to check + // if rows is actually a function. If not, use the data array directly. + const hasRowsMethod = typeof resultTable.rows === 'function'; + + // Check for data in multiple possible locations (data, _rows, or raw array format) + const dataArray = resultTable.data || resultTable._rows || []; + const hasDataArray = Array.isArray(dataArray) && dataArray.length > 0; + const isPlainJson = !hasRowsMethod && hasDataArray; + + console.log('[datatable] createAgGridData - hasRowsMethod:', hasRowsMethod); + console.log('[datatable] createAgGridData - hasDataArray:', hasDataArray, 'length:', dataArray.length); + console.log('[datatable] createAgGridData - isPlainJson:', isPlainJson); + console.log('[datatable] createAgGridData - resultTable keys:', Object.keys(resultTable || {})); if (isPlainJson) { // Plain JSON structure from serialized response - const rows = resultTable.data as any[]; - if (rows.length === 0) { - return gridData; - } + const columns = resultTable.columns || []; + console.log('[datatable] Plain JSON path - columns count:', columns.length); + console.log('[datatable] Plain JSON path - first data item:', JSON.stringify(dataArray[0]).substring(0, 200)); - // Infer columns from first row - const firstRow = rows[0]; - const columnNames = Object.keys(firstRow); + // Check if rows are arrays (need conversion) or objects (ready to use) + const firstRow = dataArray[0]; + const rowsAreArrays = Array.isArray(firstRow); + console.log('[datatable] Plain JSON path - rows are arrays:', rowsAreArrays); - for (const colName of columnNames) { - const columnDef: ColDef = { - headerName: colName, - field: colName, - sortable: true, - filter: true - }; + // Build column definitions + if (columns.length > 0) { + // Use explicit columns from the response (preferred - preserves order and handles null values) + for (const col of columns) { + const colName = col.name || col.ColumnName || ''; + const colType = col.type || col.ColumnType || 'string'; + const columnDef: ColDef = { + headerName: colName, + field: colName, + sortable: true, + filter: true + }; - // Check if value is an object (dynamic type) - const sampleValue = firstRow[colName]; - if (sampleValue !== null && typeof sampleValue === 'object') { - columnDef.valueGetter = (param) => { - const cellData = param.data[colName]; - return typeof cellData === 'string' ? cellData : JSON.stringify(cellData); + // Handle dynamic type columns + if (colType === 'dynamic') { + // Use IIFE to capture colName correctly in closure + const capturedColName = colName; + columnDef.valueGetter = (param) => { + const cellData = param.data?.[capturedColName]; + return typeof cellData === 'string' ? cellData : JSON.stringify(cellData); + }; + } + columnDataType.set(colName, colType); + gridData.columnDefs.push(columnDef); + } + } else if (!rowsAreArrays && firstRow) { + // Fallback: Infer columns from first row (may miss columns with null values) + const columnNames = Object.keys(firstRow); + console.log('[datatable] Inferring columns from first row:', columnNames.slice(0, 5)); + + for (const colName of columnNames) { + const columnDef: ColDef = { + headerName: colName, + field: colName, + sortable: true, + filter: true }; - columnDataType.set(colName, 'dynamic'); - } else { - columnDataType.set(colName, typeof sampleValue); + + // Check if value is an object (dynamic type) + const sampleValue = firstRow[colName]; + if (sampleValue !== null && typeof sampleValue === 'object') { + const capturedColName = colName; + columnDef.valueGetter = (param) => { + const cellData = param.data?.[capturedColName]; + return typeof cellData === 'string' ? cellData : JSON.stringify(cellData); + }; + columnDataType.set(colName, 'dynamic'); + } else { + columnDataType.set(colName, typeof sampleValue); + } + + gridData.columnDefs.push(columnDef); } + } - gridData.columnDefs.push(columnDef); + // Convert row data if needed + if (rowsAreArrays && columns.length > 0) { + // Convert array rows to objects using column names + console.log('[datatable] Converting array rows to objects'); + gridData.rowData = dataArray.map((row: any[]) => { + const rowObj: { [key: string]: any } = {}; + columns.forEach((col: any, idx: number) => { + const colName = col.name || col.ColumnName || `Column${idx}`; + rowObj[colName] = row[idx] !== undefined ? row[idx] : null; + }); + return rowObj; + }); + } else if (!rowsAreArrays) { + // Rows are already objects - use directly + console.log('[datatable] Using object rows directly'); + gridData.rowData = dataArray; + } else { + console.warn('[datatable] Array rows but no columns - cannot convert'); } - // Rows are already in the correct format - gridData.rowData = rows; + console.log('[datatable] Final rowData count:', gridData.rowData.length); + console.log('[datatable] Final rowData[0]:', JSON.stringify(gridData.rowData[0] || {}).substring(0, 200)); } else { // Proper KustoResultTable object with columns and rows() method const columns = resultTable.columns; @@ -145,12 +209,35 @@ function createAgGridData(resultTable: KustoResultTable | any) { return gridData; } function renderDataTable(results: KustoResponseDataSet, ele: HTMLElement) { - console.log('renderDataTable', results); + console.log('[datatable] renderDataTable called with:', results); + console.log('[datatable] results keys:', Object.keys(results || {})); + console.log('[datatable] primaryResults length:', results?.primaryResults?.length); + if (!hasDataTable(results)) { - console.error('No data table'); + console.error('[datatable] No data table found in results'); return; } - const data = createAgGridData(results.primaryResults[0]); + const primaryResult = results.primaryResults[0]; + console.log('[datatable] primaryResult keys:', Object.keys(primaryResult || {})); + console.log('[datatable] primaryResult.columns length:', primaryResult?.columns?.length); + console.log('[datatable] primaryResult.data exists:', !!(primaryResult as any).data); + console.log('[datatable] primaryResult.data length:', (primaryResult as any).data?.length); + console.log('[datatable] primaryResult._rows exists:', !!(primaryResult as any)._rows); + console.log('[datatable] primaryResult._rows length:', (primaryResult as any)._rows?.length); + console.log('[datatable] primaryResult.rows is function:', typeof (primaryResult as any).rows === 'function'); + + // Log first row from data or _rows + const firstDataRow = (primaryResult as any).data?.[0]; + const firstRawRow = (primaryResult as any)._rows?.[0]; + console.log('[datatable] First data row sample:', JSON.stringify(firstDataRow)?.substring(0, 300)); + console.log('[datatable] First _rows sample:', JSON.stringify(firstRawRow)?.substring(0, 300)); + + const data = createAgGridData(primaryResult); + console.log('[datatable] AG Grid columnDefs count:', data.columnDefs.length); + console.log('[datatable] AG Grid rowData count:', data.rowData.length); + console.log('[datatable] AG Grid first columnDef:', JSON.stringify(data.columnDefs[0])); + console.log('[datatable] AG Grid first rowData:', JSON.stringify(data.rowData[0])?.substring(0, 300)); + ReactDOM.render(React.createElement(DataTable, data, null), ele); } diff --git a/src/client/utils.ts b/src/client/utils.ts index 7d7b229..e56ec59 100644 --- a/src/client/utils.ts +++ b/src/client/utils.ts @@ -26,19 +26,33 @@ export function getTabularData(results: KustoResponseDataSet): TabularData | und if (!hasDataTable(results)) { return; } - const primaryTable = results.primaryResults[0]; + const primaryTable = results.primaryResults[0] as any; const fields: Field[] = primaryTable.columns as any; const dataPoints: Datapoint[] = []; - for (const row of primaryTable.rows()) { - const rowData: Datapoint = {}; - primaryTable.columns.forEach((col) => { - if (col.name) { - const value = row.raw ? row.raw[col.ordinal] : row.toJSON()[col.name]; - rowData[col.name] = value; + + // Handle both KustoResultTable and plain JSON from notebook serialization + if (typeof primaryTable.rows === 'function') { + // KustoResultTable - use rows() generator + for (const row of primaryTable.rows()) { + const rowData: Datapoint = {}; + primaryTable.columns.forEach((col: any) => { + if (col.name) { + const value = row.raw ? row.raw[col.ordinal] : row.toJSON()[col.name]; + rowData[col.name] = value; + } + }); + dataPoints.push(rowData); + } + } else if (primaryTable.data) { + // Plain JSON from serialization - use data array directly + return { + data: primaryTable.data, + schema: { + fields: fields } - }); - dataPoints.push(rowData); + }; } + return { data: dataPoints, schema: { diff --git a/src/extension/index.ts b/src/extension/index.ts index 1e5d8db..d48d318 100644 --- a/src/extension/index.ts +++ b/src/extension/index.ts @@ -14,7 +14,8 @@ import { registerInteractiveExperience } from './interactive/interactive'; import { registerExportCommand } from './content/export'; import { StatusBarProvider } from './kernel/statusbar'; import { AzureAuthenticatedConnection } from './kusto/connections/azAuth'; -import { Client as KustoClient } from 'azure-kusto-data'; +import { Client as KustoClient, kustoTrustedEndpoints, MatchRule } from 'azure-kusto-data'; +import { KustoClient as AppInsightsKustoClient } from './kusto/webClient'; import { registerConnection } from './kusto/connections/baseConnection'; import { AppInsightsConnection } from './kusto/connections/appInsights'; import { CellCodeLensProvider } from './interactive/cells'; @@ -25,6 +26,9 @@ import { registerKqlNotebookConnectionHandler } from './content/kqlConnection'; import { regsisterQuickFixAction } from './content/quickFix'; export async function activate(context: ExtensionContext) { + // Add Application Insights API endpoint as a trusted endpoint for the Kusto SDK + kustoTrustedEndpoints.addTrustedHosts([new MatchRule('api.applicationinsights.io', true)], false); + registerDisposableRegistry(context); initializeGlobalCache(context.globalState, context.workspaceState); initializeConstants(context.extension.packageJSON.enableProposedApi); // In browser context dont use proposed API, try to always use stable stuff... @@ -38,7 +42,7 @@ export async function activate(context: ExtensionContext) { 'cluster' in info ? undefined : AppInsightsConnection.connectionInfofrom(info) ); AzureAuthenticatedConnection.registerKustoClient(KustoClient); - AppInsightsConnection.registerKustoClient(KustoClient); + AppInsightsConnection.registerKustoClient(AppInsightsKustoClient); KernelProvider.register(); StatusBarProvider.register(); ContentProvider.register(); diff --git a/src/extension/kusto/connections/appInsights/index.ts b/src/extension/kusto/connections/appInsights/index.ts index f29f6cb..2629fff 100644 --- a/src/extension/kusto/connections/appInsights/index.ts +++ b/src/extension/kusto/connections/appInsights/index.ts @@ -80,7 +80,10 @@ export class AppInsightsConnection extends BaseConnection { - const headers: { [header: string]: string } = {}; + // Merge instance headers (like x-api-key for App Insights) with request-specific headers + const headers: { [header: string]: string } = { ...this.headers }; let payload: { db: string; csl: string; properties?: any }; let clientRequestPrefix = ''; @@ -165,21 +164,109 @@ export class KustoClient implements IKustoClient { return response; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let kustoResponse: any = null; + // For Application Insights API responses, create a simple wrapper + // that provides the KustoResponseDataSet interface try { - if (executionType == ExecutionType.Query) { - kustoResponse = new KustoResponseDataSetV2(response); - } else { - kustoResponse = new KustoResponseDataSetV1(response); - } + // Application Insights returns { tables: [...] } format + const tables = response.tables || response.Tables || []; + console.log('[webClient] Raw response tables:', JSON.stringify(tables.length > 0 ? { + tableCount: tables.length, + firstTableColumns: tables[0]?.columns || tables[0]?.Columns, + firstTableRowCount: (tables[0]?.rows || tables[0]?.Rows)?.length + } : 'no tables', null, 2)); + + const primaryResults = tables.map((table: any) => { + const rawColumns = table.columns || table.Columns || []; + console.log('[webClient] Raw columns sample:', JSON.stringify(rawColumns.slice(0, 3))); + + const columns = rawColumns.map((col: any, index: number) => ({ + name: col.name || col.ColumnName || col.columnName || `Column${index}`, + type: col.type || col.ColumnType || col.DataType || col.columnType || 'string', + ordinal: index + })); + + console.log('[webClient] Parsed columns:', JSON.stringify(columns.slice(0, 3))); + + const rawRows = table.rows || table.Rows || []; + console.log('[webClient] Row count:', rawRows.length, 'First row sample:', JSON.stringify(rawRows[0])); + + // Convert rows to data format expected by renderer (array of objects) + // IMPORTANT: Convert undefined values to null to preserve them during JSON serialization + // (JSON.stringify omits undefined values, which causes columns to disappear) + const data = rawRows.map((row: any, rowIdx: number) => { + if (Array.isArray(row)) { + // Convert array row to object using column names + const rowObj: any = {}; + columns.forEach((col: any, idx: number) => { + // Use null instead of undefined to preserve the property during JSON serialization + rowObj[col.name] = row[idx] !== undefined ? row[idx] : null; + }); + if (rowIdx === 0) { + console.log('[webClient] Converted first row:', JSON.stringify(rowObj)); + } + return rowObj; + } + // For object rows, also ensure undefined values become null + // Try multiple possible property names since APIs may use different naming conventions + if (rowIdx === 0) { + console.log('[webClient] Row is already object:', JSON.stringify(row)); + } + const normalizedRow: any = {}; + const rawCols = table.columns || table.Columns || []; + columns.forEach((col: any, idx: number) => { + // Try to get value using various possible property names + const originalCol = rawCols[idx] || {}; + const possibleKeys = [ + col.name, + originalCol.name, + originalCol.ColumnName, + originalCol.columnName + ].filter(k => k !== undefined && k !== null); + + let value = undefined; + for (const key of possibleKeys) { + if (row[key] !== undefined) { + value = row[key]; + break; + } + } + normalizedRow[col.name] = value !== undefined ? value : null; + }); + return normalizedRow; + }); + + return { + name: table.name || table.TableName || 'PrimaryResult', + columns: columns, + // data property for plain JSON path in renderer + data: data, + // _rows for statusbar row count + _rows: rawRows, + // rows() method for KustoResultTable compatibility + rows: function* () { + for (let i = 0; i < rawRows.length; i++) { + const raw = Array.isArray(rawRows[i]) ? rawRows[i] : columns.map((c: any) => rawRows[i][c.name]); + yield { + raw: raw, + toJSON: () => data[i] + }; + } + } + }; + }); + + // Return a response object compatible with KustoResponseDataSet interface + return { + tables: primaryResults, + tableNames: primaryResults.map((t: any) => t.name), + primaryResults: primaryResults, + getErrorsCount: () => ({ warnings: 0, errors: 0 }), + getExceptions: () => [], + getWarnings: () => [] + } as any; } catch (ex) { throw new Error(`Failed to parse response ({${status}}) with the following error [${ex}].`); } - if (kustoResponse.getErrorsCount().errors > 0) { - throw new Error(`Kusto request had errors. ${kustoResponse.getExceptions()}`); - } - return kustoResponse; } _getClientTimeout(executionType: ExecutionType, properties?: ClientRequestProperties | null): number {