fix(ai-diagnostics): keep cluster filters numeric
This commit is contained in:
parent
ef246989b6
commit
59f6ac1b9a
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user