fix(ai-diagnostics): keep cluster filters numeric

This commit is contained in:
saravanakumardb1 2026-03-23 16:21:08 -07:00
parent ef246989b6
commit 59f6ac1b9a
2 changed files with 71 additions and 91 deletions

View File

@ -1,6 +1,4 @@
import { getRegisteredContainer } from '@bytelyst/cosmos'; import { getRegisteredContainer } from '@bytelyst/cosmos';
import { CosmosClient, Container } from '@azure/cosmos';
import { config } from '../../lib/config.js';
import type { import type {
ErrorClusterDoc, ErrorClusterDoc,
ErrorFingerprint, ErrorFingerprint,
@ -13,23 +11,23 @@ import type {
// Container Access // Container Access
// ============================================================================ // ============================================================================
function getErrorClustersContainer(): Container { function getErrorClustersContainer() {
return getRegisteredContainer('error_clusters'); return getRegisteredContainer('error_clusters');
} }
function getErrorFingerprintsContainer(): Container { function getErrorFingerprintsContainer() {
return getRegisteredContainer('error_fingerprints'); return getRegisteredContainer('error_fingerprints');
} }
function getDiagnosticInsightsContainer(): Container { function getDiagnosticInsightsContainer() {
return getRegisteredContainer('diagnostic_insights'); return getRegisteredContainer('diagnostic_insights');
} }
function getDiagnosticQueriesContainer(): Container { function getDiagnosticQueriesContainer() {
return getRegisteredContainer('diagnostic_queries'); return getRegisteredContainer('diagnostic_queries');
} }
function getProactiveAlertsContainer(): Container { function getProactiveAlertsContainer() {
return getRegisteredContainer('proactive_alerts'); return getRegisteredContainer('proactive_alerts');
} }
@ -37,9 +35,7 @@ function getProactiveAlertsContainer(): Container {
// Error Cluster Repository // Error Cluster Repository
// ============================================================================ // ============================================================================
export async function createErrorCluster( export async function createErrorCluster(cluster: ErrorClusterDoc): Promise<ErrorClusterDoc> {
cluster: ErrorClusterDoc
): Promise<ErrorClusterDoc> {
const container = getErrorClustersContainer(); const container = getErrorClustersContainer();
const { resource } = await container.items.create(cluster); const { resource } = await container.items.create(cluster);
return resource as ErrorClusterDoc; return resource as ErrorClusterDoc;
@ -58,9 +54,7 @@ export async function getErrorClusterById(
} }
} }
export async function updateErrorCluster( export async function updateErrorCluster(cluster: ErrorClusterDoc): Promise<ErrorClusterDoc> {
cluster: ErrorClusterDoc
): Promise<ErrorClusterDoc> {
const container = getErrorClustersContainer(); const container = getErrorClustersContainer();
const { resource } = await container.items.upsert(cluster); const { resource } = await container.items.upsert(cluster);
return resource as unknown as ErrorClusterDoc; return resource as unknown as ErrorClusterDoc;
@ -77,7 +71,12 @@ export async function findClustersByProduct(
const container = getErrorClustersContainer(); const container = getErrorClustersContainer();
let query = 'SELECT * FROM c WHERE c.productId = @productId'; let query = 'SELECT * FROM c WHERE c.productId = @productId';
const parameters = [{ name: '@productId', value: productId }]; const parameters: Array<{ name: string; value: string | number }> = [
{
name: '@productId',
value: productId,
},
];
if (options.status) { if (options.status) {
query += ' AND c.status = @status'; query += ' AND c.status = @status';
@ -86,7 +85,7 @@ export async function findClustersByProduct(
if (options.minOccurrences) { if (options.minOccurrences) {
query += ' AND c.occurrenceCount >= @minOccurrences'; query += ' AND c.occurrenceCount >= @minOccurrences';
parameters.push({ name: '@minOccurrences', value: options.minOccurrences.toString() }); parameters.push({ name: '@minOccurrences', value: options.minOccurrences });
} }
query += ' ORDER BY c.occurrenceCount DESC'; query += ' ORDER BY c.occurrenceCount DESC';
@ -150,13 +149,11 @@ export async function searchSimilarClusters(
// Calculate cosine similarity for each cluster // Calculate cosine similarity for each cluster
const results: VectorSearchResult[] = clusters const results: VectorSearchResult[] = clusters
.map((cluster) => ({ .map(cluster => ({
cluster, cluster,
similarity: cluster.embedding similarity: cluster.embedding ? cosineSimilarity(queryEmbedding, cluster.embedding) : 0,
? cosineSimilarity(queryEmbedding, cluster.embedding)
: 0,
})) }))
.filter((result) => result.similarity >= threshold) .filter(result => result.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity) .sort((a, b) => b.similarity - a.similarity)
.slice(0, limit); .slice(0, limit);
@ -212,7 +209,7 @@ export async function findRelatedClusters(
excludeClusterId: clusterId, excludeClusterId: clusterId,
}); });
return results.map((r) => r.cluster); return results.map(r => r.cluster);
} }
// ============================================================================ // ============================================================================
@ -231,9 +228,7 @@ export async function getFingerprintByHash(
} }
} }
export async function saveFingerprint( export async function saveFingerprint(fingerprint: ErrorFingerprint): Promise<ErrorFingerprint> {
fingerprint: ErrorFingerprint
): Promise<ErrorFingerprint> {
const container = getErrorFingerprintsContainer(); const container = getErrorFingerprintsContainer();
const { resource } = await container.items.upsert(fingerprint); const { resource } = await container.items.upsert(fingerprint);
return resource as unknown as ErrorFingerprint; return resource as unknown as ErrorFingerprint;
@ -390,7 +385,7 @@ export async function getActiveAlerts(
} }
if (options?.severity) { if (options?.severity) {
whereClause += " AND c.severity = @severity"; whereClause += ' AND c.severity = @severity';
} }
const parameters: Array<{ name: string; value: string | number | boolean }> = []; const parameters: Array<{ name: string; value: string | number | boolean }> = [];
@ -449,7 +444,6 @@ export async function acknowledgeAlert(
if (resources.length === 0) return; if (resources.length === 0) return;
const alert = resources[0] as ProactiveAlert; const alert = resources[0] as ProactiveAlert;
const partitionKey = alert.productId;
await container.items.upsert({ await container.items.upsert({
...alert, ...alert,
@ -477,7 +471,6 @@ export async function resolveAlert(alertId: string): Promise<void> {
if (resources.length === 0) return; if (resources.length === 0) return;
const alert = resources[0] as ProactiveAlert; const alert = resources[0] as ProactiveAlert;
const partitionKey = alert.productId;
await container.items.upsert({ await container.items.upsert({
...alert, ...alert,

View File

@ -1,5 +1,4 @@
import { getRegisteredContainer } from '@bytelyst/cosmos'; import { getRegisteredContainer } from '@bytelyst/cosmos';
import type { Container } from '@azure/cosmos';
import type { ErrorEvent, ErrorClusterDoc } from './types.js'; import type { ErrorEvent, ErrorClusterDoc } from './types.js';
// ============================================================================ // ============================================================================
@ -83,7 +82,7 @@ interface SessionState {
// Container Access // Container Access
// ============================================================================ // ============================================================================
function getTelemetryContainer(): Container { function getTelemetryContainer() {
return getRegisteredContainer('telemetry_events'); return getRegisteredContainer('telemetry_events');
} }
@ -137,13 +136,9 @@ export async function linkErrorToTelemetry(
} }
// Find the error event position // Find the error event position
const errorIndex = events.findIndex( const errorIndex = events.findIndex(e => e.timestamp === errorEvent.timestamp);
(e) => e.timestamp === errorEvent.timestamp
);
const windowStart = errorIndex >= 0 const windowStart = errorIndex >= 0 ? Math.max(0, errorIndex - Math.floor(maxEvents / 2)) : 0;
? Math.max(0, errorIndex - Math.floor(maxEvents / 2))
: 0;
const windowEnd = Math.min(events.length, windowStart + maxEvents); const windowEnd = Math.min(events.length, windowStart + maxEvents);
const windowEvents = events.slice(windowStart, windowEnd); const windowEvents = events.slice(windowStart, windowEnd);
@ -202,7 +197,8 @@ async function linkByUserAndTime(
`; `;
const { resources } = await container.items const { resources } = await container.items
.query({ .query(
{
query, query,
parameters: [ parameters: [
{ name: '@userId', value: errorEvent.userId }, { name: '@userId', value: errorEvent.userId },
@ -210,7 +206,9 @@ async function linkByUserAndTime(
{ name: '@windowStart', value: windowStart }, { name: '@windowStart', value: windowStart },
{ name: '@windowEnd', value: windowEnd }, { name: '@windowEnd', value: windowEnd },
], ],
}, { maxItemCount: maxEvents }) },
{ maxItemCount: maxEvents }
)
.fetchAll(); .fetchAll();
const events = resources as TelemetryEvent[]; const events = resources as TelemetryEvent[];
@ -224,9 +222,7 @@ async function linkByUserAndTime(
// Find events before and after error timestamp // Find events before and after error timestamp
const errorTimeMs = errorTime.getTime(); const errorTimeMs = errorTime.getTime();
const errorIndex = events.findIndex( const errorIndex = events.findIndex(e => new Date(e.timestamp).getTime() >= errorTimeMs);
(e) => new Date(e.timestamp).getTime() >= errorTimeMs
);
const precedingEvents = errorIndex > 0 ? events.slice(0, errorIndex) : []; const precedingEvents = errorIndex > 0 ? events.slice(0, errorIndex) : [];
const followingEvents = errorIndex >= 0 ? events.slice(errorIndex + 1) : events; const followingEvents = errorIndex >= 0 ? events.slice(errorIndex + 1) : events;
@ -259,7 +255,7 @@ function extractUserJourney(events: TelemetryEvent[]): UserJourneyStep[] {
journey.push({ journey.push({
timestamp: event.timestamp, timestamp: event.timestamp,
screen: event.screen || event.properties?.screen as string || 'unknown', screen: event.screen || (event.properties?.screen as string) || 'unknown',
action: event.eventName, action: event.eventName,
durationMs, durationMs,
}); });
@ -300,12 +296,10 @@ function extractDeviceContext(event?: TelemetryEvent): DeviceContext {
function extractApiCalls(events: TelemetryEvent[]): ApiCall[] { function extractApiCalls(events: TelemetryEvent[]): ApiCall[] {
return events return events
.filter( .filter(
(e) => e =>
e.eventType === 'api_call' || e.eventType === 'api_call' || e.eventName.includes('api') || e.eventName.includes('request')
e.eventName.includes('api') ||
e.eventName.includes('request')
) )
.map((e) => ({ .map(e => ({
endpoint: (e.properties?.endpoint as string) || 'unknown', endpoint: (e.properties?.endpoint as string) || 'unknown',
method: (e.properties?.method as string) || 'GET', method: (e.properties?.method as string) || 'GET',
statusCode: e.properties?.statusCode as number, statusCode: e.properties?.statusCode as number,
@ -322,9 +316,7 @@ function extractApiCalls(events: TelemetryEvent[]): ApiCall[] {
/** /**
* Enriches error event with full telemetry context * Enriches error event with full telemetry context
*/ */
export async function enrichErrorContext( export async function enrichErrorContext(errorEvent: ErrorEvent): Promise<EnrichedErrorContext> {
errorEvent: ErrorEvent
): Promise<EnrichedErrorContext> {
// Link to telemetry // Link to telemetry
const telemetryContext = await linkErrorToTelemetry(errorEvent, { const telemetryContext = await linkErrorToTelemetry(errorEvent, {
windowMinutes: 5, windowMinutes: 5,
@ -338,8 +330,9 @@ export async function enrichErrorContext(
const recentActions = extractRecentActions(telemetryContext); const recentActions = extractRecentActions(telemetryContext);
// Extract API failures // Extract API failures
const apiFailures = telemetryContext?.apiCalls.filter((call) => const apiFailures =
call.error || (call.statusCode && call.statusCode >= 400) telemetryContext?.apiCalls.filter(
call => call.error || (call.statusCode && call.statusCode >= 400)
) || []; ) || [];
// Extract feature flags from telemetry // Extract feature flags from telemetry
@ -373,15 +366,17 @@ function buildSessionState(telemetryContext: TelemetryContext | null): SessionSt
// Find last screen view // Find last screen view
const screenViews = events.filter( const screenViews = events.filter(
(e) => e.eventType === 'screen_view' || e.eventName.includes('screen') e => e.eventType === 'screen_view' || e.eventName.includes('screen')
); );
const currentScreen = screenViews.length > 0 const currentScreen =
screenViews.length > 0
? screenViews[screenViews.length - 1].screen || ? screenViews[screenViews.length - 1].screen ||
(screenViews[screenViews.length - 1].properties?.screen as string) (screenViews[screenViews.length - 1].properties?.screen as string)
: 'unknown'; : 'unknown';
const previousScreen = screenViews.length > 1 const previousScreen =
screenViews.length > 1
? screenViews[screenViews.length - 2].screen || ? screenViews[screenViews.length - 2].screen ||
(screenViews[screenViews.length - 2].properties?.screen as string) (screenViews[screenViews.length - 2].properties?.screen as string)
: undefined; : undefined;
@ -396,8 +391,8 @@ function buildSessionState(telemetryContext: TelemetryContext | null): SessionSt
// Extract user actions // Extract user actions
const userActions = events const userActions = events
.filter((e) => e.eventType === 'action' || e.eventType === 'interaction') .filter(e => e.eventType === 'action' || e.eventType === 'interaction')
.map((e) => e.eventName); .map(e => e.eventName);
return { return {
screen: currentScreen || 'unknown', screen: currentScreen || 'unknown',
@ -414,13 +409,11 @@ function extractRecentActions(telemetryContext: TelemetryContext | null): string
return telemetryContext.precedingEvents return telemetryContext.precedingEvents
.slice(-10) // Last 10 actions before error .slice(-10) // Last 10 actions before error
.filter((e) => e.eventType === 'action' || e.eventType === 'interaction') .filter(e => e.eventType === 'action' || e.eventType === 'interaction')
.map((e) => e.eventName); .map(e => e.eventName);
} }
function extractFeatureFlags( function extractFeatureFlags(telemetryContext: TelemetryContext | null): Record<string, boolean> {
telemetryContext: TelemetryContext | null
): Record<string, boolean> {
if (!telemetryContext) return {}; if (!telemetryContext) return {};
const flags: Record<string, boolean> = {}; const flags: Record<string, boolean> = {};
@ -434,9 +427,7 @@ function extractFeatureFlags(
return flags; return flags;
} }
function buildConfigSnapshot( function buildConfigSnapshot(telemetryContext: TelemetryContext | null): Record<string, unknown> {
telemetryContext: TelemetryContext | null
): Record<string, unknown> {
if (!telemetryContext) return {}; if (!telemetryContext) return {};
const config: Record<string, unknown> = {}; const config: Record<string, unknown> = {};
@ -464,9 +455,7 @@ export interface Breadcrumb {
/** /**
* Generates a breadcrumb trail from telemetry context * Generates a breadcrumb trail from telemetry context
*/ */
export function generateBreadcrumbTrail( export function generateBreadcrumbTrail(telemetryContext: TelemetryContext | null): Breadcrumb[] {
telemetryContext: TelemetryContext | null
): Breadcrumb[] {
if (!telemetryContext) return []; if (!telemetryContext) return [];
const breadcrumbs: Breadcrumb[] = []; const breadcrumbs: Breadcrumb[] = [];
@ -496,9 +485,7 @@ export function generateBreadcrumbTrail(
} }
// Sort by timestamp // Sort by timestamp
breadcrumbs.sort( breadcrumbs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
return breadcrumbs; return breadcrumbs;
} }
@ -538,8 +525,8 @@ export async function aggregateClusterContext(
const apiErrors = new Map<string, { count: number; errors: string[] }>(); const apiErrors = new Map<string, { count: number; errors: string[] }>();
const flagCorrelations = new Map<string, { enabled: number; total: number }>(); const flagCorrelations = new Map<string, { enabled: number; total: number }>();
let earliestTime = new Date(cluster.firstSeenAt); const earliestTime = new Date(cluster.firstSeenAt);
let latestTime = new Date(cluster.lastSeenAt); const latestTime = new Date(cluster.lastSeenAt);
for (const error of errorEvents) { for (const error of errorEvents) {
if (error.userId) { if (error.userId) {
@ -607,7 +594,7 @@ export async function aggregateClusterContext(
enabled: data.enabled > 0, enabled: data.enabled > 0,
errorCorrelation: data.enabled / data.total, errorCorrelation: data.enabled / data.total,
})) }))
.filter((f) => f.errorCorrelation > 0.5) // Only include flags with >50% correlation .filter(f => f.errorCorrelation > 0.5) // Only include flags with >50% correlation
.sort((a, b) => b.errorCorrelation - a.errorCorrelation) .sort((a, b) => b.errorCorrelation - a.errorCorrelation)
.slice(0, 5); .slice(0, 5);