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 { 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,
|
||||||
|
|||||||
@ -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,15 +197,18 @@ async function linkByUserAndTime(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const { resources } = await container.items
|
const { resources } = await container.items
|
||||||
.query({
|
.query(
|
||||||
query,
|
{
|
||||||
parameters: [
|
query,
|
||||||
{ name: '@userId', value: errorEvent.userId },
|
parameters: [
|
||||||
{ name: '@productId', value: errorEvent.productId },
|
{ name: '@userId', value: errorEvent.userId },
|
||||||
{ name: '@windowStart', value: windowStart },
|
{ name: '@productId', value: errorEvent.productId },
|
||||||
{ name: '@windowEnd', value: windowEnd },
|
{ name: '@windowStart', value: windowStart },
|
||||||
],
|
{ 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,9 +330,10 @@ 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
|
||||||
const featureFlags = extractFeatureFlags(telemetryContext);
|
const featureFlags = extractFeatureFlags(telemetryContext);
|
||||||
@ -373,18 +366,20 @@ 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[screenViews.length - 1].screen ||
|
screenViews.length > 0
|
||||||
(screenViews[screenViews.length - 1].properties?.screen as string)
|
? screenViews[screenViews.length - 1].screen ||
|
||||||
: 'unknown';
|
(screenViews[screenViews.length - 1].properties?.screen as string)
|
||||||
|
: 'unknown';
|
||||||
|
|
||||||
const previousScreen = screenViews.length > 1
|
const previousScreen =
|
||||||
? screenViews[screenViews.length - 2].screen ||
|
screenViews.length > 1
|
||||||
(screenViews[screenViews.length - 2].properties?.screen as string)
|
? screenViews[screenViews.length - 2].screen ||
|
||||||
: undefined;
|
(screenViews[screenViews.length - 2].properties?.screen as string)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Calculate duration on current screen
|
// Calculate duration on current screen
|
||||||
let durationOnScreen = 0;
|
let durationOnScreen = 0;
|
||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user