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 { CosmosClient, Container } from '@azure/cosmos';
import { config } from '../../lib/config.js';
import type {
ErrorClusterDoc,
ErrorFingerprint,
@ -13,23 +11,23 @@ import type {
// Container Access
// ============================================================================
function getErrorClustersContainer(): Container {
function getErrorClustersContainer() {
return getRegisteredContainer('error_clusters');
}
function getErrorFingerprintsContainer(): Container {
function getErrorFingerprintsContainer() {
return getRegisteredContainer('error_fingerprints');
}
function getDiagnosticInsightsContainer(): Container {
function getDiagnosticInsightsContainer() {
return getRegisteredContainer('diagnostic_insights');
}
function getDiagnosticQueriesContainer(): Container {
function getDiagnosticQueriesContainer() {
return getRegisteredContainer('diagnostic_queries');
}
function getProactiveAlertsContainer(): Container {
function getProactiveAlertsContainer() {
return getRegisteredContainer('proactive_alerts');
}
@ -37,9 +35,7 @@ function getProactiveAlertsContainer(): Container {
// Error Cluster Repository
// ============================================================================
export async function createErrorCluster(
cluster: ErrorClusterDoc
): Promise<ErrorClusterDoc> {
export async function createErrorCluster(cluster: ErrorClusterDoc): Promise<ErrorClusterDoc> {
const container = getErrorClustersContainer();
const { resource } = await container.items.create(cluster);
return resource as ErrorClusterDoc;
@ -58,9 +54,7 @@ export async function getErrorClusterById(
}
}
export async function updateErrorCluster(
cluster: ErrorClusterDoc
): Promise<ErrorClusterDoc> {
export async function updateErrorCluster(cluster: ErrorClusterDoc): Promise<ErrorClusterDoc> {
const container = getErrorClustersContainer();
const { resource } = await container.items.upsert(cluster);
return resource as unknown as ErrorClusterDoc;
@ -77,7 +71,12 @@ export async function findClustersByProduct(
const container = getErrorClustersContainer();
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) {
query += ' AND c.status = @status';
@ -86,7 +85,7 @@ export async function findClustersByProduct(
if (options.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';
@ -150,13 +149,11 @@ export async function searchSimilarClusters(
// Calculate cosine similarity for each cluster
const results: VectorSearchResult[] = clusters
.map((cluster) => ({
.map(cluster => ({
cluster,
similarity: cluster.embedding
? cosineSimilarity(queryEmbedding, cluster.embedding)
: 0,
similarity: cluster.embedding ? cosineSimilarity(queryEmbedding, cluster.embedding) : 0,
}))
.filter((result) => result.similarity >= threshold)
.filter(result => result.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit);
@ -212,7 +209,7 @@ export async function findRelatedClusters(
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(
fingerprint: ErrorFingerprint
): Promise<ErrorFingerprint> {
export async function saveFingerprint(fingerprint: ErrorFingerprint): Promise<ErrorFingerprint> {
const container = getErrorFingerprintsContainer();
const { resource } = await container.items.upsert(fingerprint);
return resource as unknown as ErrorFingerprint;
@ -378,19 +373,19 @@ export async function getActiveAlerts(
const container = getProactiveAlertsContainer();
let whereClause = 'NOT IS_DEFINED(c.resolvedAt)';
if (productId) {
whereClause += ' AND c.productId = @productId';
}
if (options?.acknowledged === false) {
whereClause += ' AND NOT IS_DEFINED(c.acknowledgedAt)';
} else if (options?.acknowledged === true) {
whereClause += ' AND IS_DEFINED(c.acknowledgedAt)';
}
if (options?.severity) {
whereClause += " AND c.severity = @severity";
whereClause += ' AND c.severity = @severity';
}
const parameters: Array<{ name: string; value: string | number | boolean }> = [];
@ -415,7 +410,7 @@ export async function getActiveAlerts(
c.createdAt DESC
OFFSET 0 LIMIT @limit
`;
parameters.push({ name: '@limit', value: options?.limit ?? 50 });
const { resources } = await container.items
@ -449,7 +444,6 @@ export async function acknowledgeAlert(
if (resources.length === 0) return;
const alert = resources[0] as ProactiveAlert;
const partitionKey = alert.productId;
await container.items.upsert({
...alert,
@ -477,7 +471,6 @@ export async function resolveAlert(alertId: string): Promise<void> {
if (resources.length === 0) return;
const alert = resources[0] as ProactiveAlert;
const partitionKey = alert.productId;
await container.items.upsert({
...alert,

View File

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