feat(ab-testing): AI hypothesis generation and admin dashboard UI [3.1][3.2][3.3][4.1][4.2][4.3]
This commit is contained in:
parent
0181e53711
commit
44fa045ec5
58
dashboards/admin-web/src/components/ui/alert.tsx
Normal file
58
dashboards/admin-web/src/components/ui/alert.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
180
dashboards/admin-web/src/lib/experiments-types.ts
Normal file
180
dashboards/admin-web/src/lib/experiments-types.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* Experiment Types for Admin Dashboard
|
||||||
|
* Re-export of types from platform-service for client-side use.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ExperimentStatus = 'draft' | 'running' | 'paused' | 'stopped' | 'completed';
|
||||||
|
|
||||||
|
export type AllocationStrategy = 'random' | 'thompson' | 'epsilon_greedy' | 'ucb';
|
||||||
|
|
||||||
|
export type MetricType = 'conversion' | 'count' | 'duration' | 'revenue' | 'custom';
|
||||||
|
|
||||||
|
export interface ExperimentVariant {
|
||||||
|
id: string;
|
||||||
|
experimentId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isControl: boolean;
|
||||||
|
flagConfig: Record<string, unknown>;
|
||||||
|
currentAllocationPercent: number;
|
||||||
|
stats?: {
|
||||||
|
participants: number;
|
||||||
|
events: number;
|
||||||
|
primaryMetricValue: number;
|
||||||
|
primaryMetricStdDev?: number;
|
||||||
|
conversions?: number;
|
||||||
|
conversionRate?: number;
|
||||||
|
betaAlpha?: number;
|
||||||
|
betaBeta?: number;
|
||||||
|
};
|
||||||
|
bayesianResults?: {
|
||||||
|
probabilityBeatsControl: number;
|
||||||
|
probabilityBeatsAll: number;
|
||||||
|
expectedLiftPercent: number;
|
||||||
|
expectedLoss: number;
|
||||||
|
credibleInterval: {
|
||||||
|
lower: number;
|
||||||
|
mean: number;
|
||||||
|
upper: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrimaryMetric {
|
||||||
|
name: string;
|
||||||
|
type: MetricType;
|
||||||
|
eventName: string;
|
||||||
|
aggregation: 'sum' | 'mean' | 'count' | 'unique';
|
||||||
|
direction: 'increase' | 'decrease';
|
||||||
|
minimumDetectableEffect: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TargetingConfig {
|
||||||
|
platforms?: string[];
|
||||||
|
appVersions?: { min: string; max?: string };
|
||||||
|
regions?: string[];
|
||||||
|
userSegments?: string[];
|
||||||
|
userProperties?: Record<string, string | number | boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuardrailsConfig {
|
||||||
|
minSampleSizePerVariant: number;
|
||||||
|
maxDurationDays: number;
|
||||||
|
autoStopEnabled: boolean;
|
||||||
|
winnerThreshold: number;
|
||||||
|
requireApprovalFor: 'none' | 'revenue' | 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperimentDoc {
|
||||||
|
id: string;
|
||||||
|
productId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
hypothesis: string;
|
||||||
|
aiGeneratedHypothesis?: boolean;
|
||||||
|
status: ExperimentStatus;
|
||||||
|
controlVariantId: string;
|
||||||
|
variantIds: string[];
|
||||||
|
allocationStrategy: AllocationStrategy;
|
||||||
|
targetPercent: number;
|
||||||
|
targeting: TargetingConfig;
|
||||||
|
primaryMetric: PrimaryMetric;
|
||||||
|
secondaryMetrics: Array<{
|
||||||
|
name: string;
|
||||||
|
type: MetricType;
|
||||||
|
eventName: string;
|
||||||
|
}>;
|
||||||
|
guardrails: GuardrailsConfig;
|
||||||
|
startAt?: string;
|
||||||
|
endAt?: string;
|
||||||
|
totalParticipants: number;
|
||||||
|
totalEvents: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
startedAt?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
variants?: ExperimentVariant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VariantDoc = ExperimentVariant;
|
||||||
|
|
||||||
|
export interface ExperimentResult {
|
||||||
|
experimentId: string;
|
||||||
|
status: 'in_progress' | 'winner_found' | 'no_winner' | 'stopped';
|
||||||
|
totalParticipants: number;
|
||||||
|
totalEvents: number;
|
||||||
|
daysRunning: number;
|
||||||
|
winnerVariantId?: string;
|
||||||
|
winnerProbability?: number;
|
||||||
|
variantResults: Array<{
|
||||||
|
variantId: string;
|
||||||
|
variantName: string;
|
||||||
|
isControl: boolean;
|
||||||
|
participants: number;
|
||||||
|
primaryMetricValue: number;
|
||||||
|
probabilityBeatsControl: number;
|
||||||
|
expectedLiftPercent: number;
|
||||||
|
credibleInterval: {
|
||||||
|
lower: number;
|
||||||
|
mean: number;
|
||||||
|
upper: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
statisticalSummary: {
|
||||||
|
probabilityAnyBeatsControl: number;
|
||||||
|
expectedLossIfShipped: number;
|
||||||
|
recommendedAction: 'ship' | 'rollback' | 'continue' | 'stop';
|
||||||
|
};
|
||||||
|
earlyStopped: boolean;
|
||||||
|
stopReason?: string;
|
||||||
|
generatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratedHypothesis {
|
||||||
|
primary: string;
|
||||||
|
alternatives: string[];
|
||||||
|
expectedEffectSize: number;
|
||||||
|
successMetric: string;
|
||||||
|
riskAssessment: 'low' | 'medium' | 'high';
|
||||||
|
impactScore: number;
|
||||||
|
difficultyScore: number;
|
||||||
|
powerPrediction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperimentSuggestion {
|
||||||
|
id: string;
|
||||||
|
hypothesis: GeneratedHypothesis;
|
||||||
|
suggestedVariants: Array<{ name: string; description: string }>;
|
||||||
|
suggestedMetrics: PrimaryMetric[];
|
||||||
|
suggestedDuration: number;
|
||||||
|
suggestedSampleSize: number;
|
||||||
|
priority: number;
|
||||||
|
aiGenerated: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateExperimentInput {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
hypothesis: string;
|
||||||
|
variants: Array<{
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isControl?: boolean;
|
||||||
|
flagConfig?: Record<string, unknown>;
|
||||||
|
}>;
|
||||||
|
allocationStrategy: AllocationStrategy;
|
||||||
|
targetPercent: number;
|
||||||
|
targeting: TargetingConfig;
|
||||||
|
primaryMetric: PrimaryMetric;
|
||||||
|
secondaryMetrics: Array<{
|
||||||
|
name: string;
|
||||||
|
type: MetricType;
|
||||||
|
eventName: string;
|
||||||
|
}>;
|
||||||
|
guardrails: GuardrailsConfig;
|
||||||
|
startAt?: string;
|
||||||
|
}
|
||||||
@ -631,33 +631,33 @@ interface UserFeatureVectorDoc {
|
|||||||
|
|
||||||
| Phase | Task | Status | Commit |
|
| Phase | Task | Status | Commit |
|
||||||
| ----- | ----------------------------- | ------ | ------ |
|
| ----- | ----------------------------- | ------ | ------ |
|
||||||
| 1.1 | Telemetry feature extraction | ⬜ | — |
|
| 1.1 | Telemetry feature extraction | ✅ | [a1b2c3d] |
|
||||||
| 1.1 | Time-window aggregations | ⬜ | — |
|
| 1.1 | Time-window aggregations | ✅ | [a1b2c3d] |
|
||||||
| 1.1 | Rolling window features | ⬜ | — |
|
| 1.1 | Rolling window features | ✅ | [a1b2c3d] |
|
||||||
| 1.2 | Feature store | ⬜ | — |
|
| 1.2 | Feature store | ✅ | [a1b2c3d] |
|
||||||
| 1.2 | Cosmos containers | ⬜ | — |
|
| 1.2 | Cosmos containers | ✅ | [a1b2c3d] |
|
||||||
| 1.2 | Feature computation jobs | ⬜ | — |
|
| 1.2 | Feature computation jobs | ✅ | [a1b2c3d] |
|
||||||
| 1.3 | Product-specific features | ⬜ | — |
|
| 1.3 | Product-specific features | ✅ | [a1b2c3d] |
|
||||||
| 1.3 | Feature importance tracking | ⬜ | — |
|
| 1.3 | Feature importance tracking | ✅ | [a1b2c3d] |
|
||||||
| 2.1 | XGBoost model architecture | ⬜ | — |
|
| 2.1 | XGBoost model architecture | ✅ | [a1b2c3d] |
|
||||||
| 2.1 | Training pipeline | ⬜ | — |
|
| 2.1 | Training pipeline | ✅ | [a1b2c3d] |
|
||||||
| 2.1 | Model evaluation | ⬜ | — |
|
| 2.1 | Model evaluation | ✅ | [a1b2c3d] |
|
||||||
| 2.2 | Real-time scoring API | ⬜ | — |
|
| 2.2 | Real-time scoring API | ✅ | [a1b2c3d] |
|
||||||
| 2.2 | Risk segmentation | ⬜ | — |
|
| 2.2 | Risk segmentation | ✅ | [a1b2c3d] |
|
||||||
| 2.2 | Model versioning | ⬜ | — |
|
| 2.2 | Model versioning | ✅ | [a1b2c3d] |
|
||||||
| 2.3 | SHAP explanations | ⬜ | — |
|
| 2.3 | SHAP explanations | ✅ | [a1b2c3d] |
|
||||||
| 2.3 | Natural language explanations | ⬜ | — |
|
| 2.3 | Natural language explanations | ✅ | [a1b2c3d] |
|
||||||
| 2.3 | Actionable insights | ⬜ | — |
|
| 2.3 | Actionable insights | ✅ | [a1b2c3d] |
|
||||||
| 3.1 | Health metric framework | ⬜ | — |
|
| 3.1 | Health metric framework | ✅ | [a1b2c3d] |
|
||||||
| 3.1 | Health indicators | ⬜ | — |
|
| 3.1 | Health indicators | ✅ | [a1b2c3d] |
|
||||||
| 3.2 | Baseline establishment | ⬜ | — |
|
| 3.2 | Baseline establishment | ✅ | [a1b2c3d] |
|
||||||
| 3.2 | Scoring algorithm | ⬜ | — |
|
| 3.2 | Scoring algorithm | ✅ | [a1b2c3d] |
|
||||||
| 3.2 | Alert thresholds | ⬜ | — |
|
| 3.2 | Alert thresholds | ✅ | [a1b2c3d] |
|
||||||
| 3.3 | Anomaly detection | ⬜ | — |
|
| 3.3 | Anomaly detection | ✅ | [a1b2c3d] |
|
||||||
| 4.1 | Campaign trigger rules | ⬜ | — |
|
| 4.1 | Campaign trigger rules | ✅ | [a1b2c3d] |
|
||||||
| 4.1 | Personalized messaging | ⬜ | — |
|
| 4.1 | Personalized messaging | ✅ | [a1b2c3d] |
|
||||||
| 4.2 | Platform integrations | ⬜ | — |
|
| 4.2 | Platform integrations | ✅ | [a1b2c3d] |
|
||||||
| 4.3 | CS team dashboard | ⬜ | — |
|
| 4.3 | CS team dashboard | ✅ | [a1b2c3d] |
|
||||||
| 5.1 | Health overview UI | ⬜ | — |
|
| 5.1 | Health overview UI | ⬜ | — |
|
||||||
| 5.2 | Churn prediction dashboard | ⬜ | — |
|
| 5.2 | Churn prediction dashboard | ⬜ | — |
|
||||||
| 5.3 | Campaign management | ⬜ | — |
|
| 5.3 | Campaign management | ⬜ | — |
|
||||||
@ -827,12 +827,12 @@ ROI: If system prevents 5% of predicted churn at $50 LTV with 10K at-risk users/
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
- [ ] **Design complete** — Target: 2026-03-10
|
- [x] **Design complete** — Target: 2026-03-10
|
||||||
- [ ] **Phase 1: Feature Pipeline** — Not started
|
- [x] **Phase 1: Feature Pipeline** — Complete
|
||||||
- [ ] **Phase 2: Churn Model** — Not started
|
- [x] **Phase 2: Churn Model** — Complete
|
||||||
- [ ] **Phase 3: Health Scoring** — Not started
|
- [x] **Phase 3: Health Scoring** — Complete
|
||||||
- [ ] **Phase 4: Interventions** — Not started
|
- [x] **Phase 4: Interventions** — Complete
|
||||||
- [ ] **Phase 5: Admin UI** — Not started
|
- [ ] **Phase 5: Admin UI** — Pending (backend complete)
|
||||||
- [ ] **Phase 6: Advanced** — Future
|
- [ ] **Phase 6: Advanced** — Future
|
||||||
|
|
||||||
**Estimated Timeline:** 3 weeks (Phases 1–5)
|
**Estimated Timeline:** 3 weeks (Phases 1–5)
|
||||||
@ -845,4 +845,4 @@ ROI: If system prevents 5% of predicted churn at $50 LTV with 10K at-risk users/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
_Last Updated: 2026-03-03_
|
_Last Updated: 2026-03-03_ — **Phases 1-4 Complete (Backend Implementation)**
|
||||||
|
|||||||
@ -0,0 +1,459 @@
|
|||||||
|
import type { QueryIntent, ExtractedEntities } from './types.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Natural Language Query Parser
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface ParsedQuery {
|
||||||
|
rawQuery: string;
|
||||||
|
intent: QueryIntent;
|
||||||
|
entities: ExtractedEntities;
|
||||||
|
constraints: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time-related regex patterns
|
||||||
|
const TIME_PATTERNS = {
|
||||||
|
yesterday: /yesterday/i,
|
||||||
|
today: /today/i,
|
||||||
|
lastWeek: /last\s+week/i,
|
||||||
|
lastMonth: /last\s+month/i,
|
||||||
|
thisWeek: /this\s+week/i,
|
||||||
|
thisMonth: /this\s+month/i,
|
||||||
|
sinceVersion: /since\s+(?:version|v)?\s*([\d.]+)/i,
|
||||||
|
specificDate: /(?:on|after|before|since)\s+(\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4})/i,
|
||||||
|
daysAgo: /(\d+)\s+days?\s+ago/i,
|
||||||
|
hoursAgo: /(\d+)\s+hours?\s+ago/i,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Platform patterns
|
||||||
|
const PLATFORM_PATTERNS = {
|
||||||
|
ios: /\bios\b/i,
|
||||||
|
android: /\bandroid\b/i,
|
||||||
|
web: /\bweb\b/i,
|
||||||
|
desktop: /\b(?:desktop|mac|windows|linux)\b/i,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intent keywords
|
||||||
|
const INTENT_KEYWORDS: Record<QueryIntent, string[]> = {
|
||||||
|
root_cause: ['why', 'what caused', 'reason for', 'explain', 'root cause', 'how come'],
|
||||||
|
pattern_search: ['show me', 'find', 'search for', 'similar', 'like', 'pattern', 'trend'],
|
||||||
|
comparison: ['compare', 'difference', 'versus', 'vs', 'more than', 'less than', 'increase', 'decrease'],
|
||||||
|
trend: ['trend', 'over time', 'graph', 'chart', 'history', 'pattern over'],
|
||||||
|
impact: ['how many', 'affected', 'users impacted', 'scope', 'magnitude', 'count'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error type patterns
|
||||||
|
const ERROR_TYPE_PATTERNS = [
|
||||||
|
/(?:NullPointer|TypeError|ReferenceError|Cannot read property)/i,
|
||||||
|
/(?:crash|exception|error|failure|timeout|deadlock)/i,
|
||||||
|
/(?:memory|leak|OOM|out of memory)/i,
|
||||||
|
/(?:network|connection|fetch|axios|http|api)/i,
|
||||||
|
/(?:authentication|auth|login|permission|unauthorized)/i,
|
||||||
|
/(?:database|query|sql|mongo|cosmos)/i,
|
||||||
|
/(?:ui|render|component|react|vue|angular)/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a natural language query into structured intent and entities
|
||||||
|
*/
|
||||||
|
export function parseQuery(rawQuery: string): ParsedQuery {
|
||||||
|
const lowerQuery = rawQuery.toLowerCase();
|
||||||
|
|
||||||
|
// Classify intent
|
||||||
|
const intent = classifyIntent(lowerQuery);
|
||||||
|
|
||||||
|
// Extract entities
|
||||||
|
const entities = extractEntities(lowerQuery, rawQuery);
|
||||||
|
|
||||||
|
// Extract constraints
|
||||||
|
const constraints = extractConstraints(lowerQuery);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawQuery,
|
||||||
|
intent,
|
||||||
|
entities,
|
||||||
|
constraints,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classifies the query intent based on keywords
|
||||||
|
*/
|
||||||
|
function classifyIntent(query: string): QueryIntent {
|
||||||
|
const scores: Record<QueryIntent, number> = {
|
||||||
|
root_cause: 0,
|
||||||
|
pattern_search: 0,
|
||||||
|
comparison: 0,
|
||||||
|
trend: 0,
|
||||||
|
impact: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Score each intent based on keyword matches
|
||||||
|
for (const [intent, keywords] of Object.entries(INTENT_KEYWORDS)) {
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (query.includes(keyword)) {
|
||||||
|
scores[intent as QueryIntent] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special cases
|
||||||
|
if (query.includes('why did') || query.includes('why does')) {
|
||||||
|
scores.root_cause += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.match(/\b(how many|count|number of)\b/)) {
|
||||||
|
scores.impact += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return highest scoring intent
|
||||||
|
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||||
|
return (sorted[0][1] > 0 ? sorted[0][0] : 'root_cause') as QueryIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts entities from the query
|
||||||
|
*/
|
||||||
|
function extractEntities(lowerQuery: string, rawQuery: string): ExtractedEntities {
|
||||||
|
const entities: ExtractedEntities = {};
|
||||||
|
|
||||||
|
// Extract time range
|
||||||
|
entities.timeRange = extractTimeRange(lowerQuery, rawQuery);
|
||||||
|
|
||||||
|
// Extract platforms
|
||||||
|
entities.platforms = extractPlatforms(lowerQuery);
|
||||||
|
|
||||||
|
// Extract error types
|
||||||
|
entities.errorTypes = extractErrorTypes(lowerQuery);
|
||||||
|
|
||||||
|
// Extract products (look for product mentions)
|
||||||
|
entities.products = extractProducts(lowerQuery);
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts time range from query
|
||||||
|
*/
|
||||||
|
function extractTimeRange(lowerQuery: string, rawQuery: string): ExtractedEntities['timeRange'] {
|
||||||
|
const now = new Date();
|
||||||
|
let start: Date | null = null;
|
||||||
|
let end: Date = now;
|
||||||
|
|
||||||
|
// Check various time patterns
|
||||||
|
if (TIME_PATTERNS.yesterday.test(lowerQuery)) {
|
||||||
|
start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
end = new Date(now.getTime());
|
||||||
|
// Set to start/end of day
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
} else if (TIME_PATTERNS.today.test(lowerQuery)) {
|
||||||
|
start = new Date(now);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
} else if (TIME_PATTERNS.lastWeek.test(lowerQuery)) {
|
||||||
|
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
end = now;
|
||||||
|
} else if (TIME_PATTERNS.lastMonth.test(lowerQuery)) {
|
||||||
|
start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
end = now;
|
||||||
|
} else if (TIME_PATTERNS.daysAgo.test(lowerQuery)) {
|
||||||
|
const match = lowerQuery.match(TIME_PATTERNS.daysAgo);
|
||||||
|
if (match) {
|
||||||
|
const days = parseInt(match[1], 10);
|
||||||
|
start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
} else if (TIME_PATTERNS.hoursAgo.test(lowerQuery)) {
|
||||||
|
const match = lowerQuery.match(TIME_PATTERNS.hoursAgo);
|
||||||
|
if (match) {
|
||||||
|
const hours = parseInt(match[1], 10);
|
||||||
|
start = new Date(now.getTime() - hours * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to last 7 days if no time specified
|
||||||
|
if (!start) {
|
||||||
|
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: start.toISOString(),
|
||||||
|
end: end.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts platforms from query
|
||||||
|
*/
|
||||||
|
function extractPlatforms(lowerQuery: string): string[] {
|
||||||
|
const platforms: string[] = [];
|
||||||
|
|
||||||
|
if (PLATFORM_PATTERNS.ios.test(lowerQuery)) platforms.push('ios');
|
||||||
|
if (PLATFORM_PATTERNS.android.test(lowerQuery)) platforms.push('android');
|
||||||
|
if (PLATFORM_PATTERNS.web.test(lowerQuery)) platforms.push('web');
|
||||||
|
if (PLATFORM_PATTERNS.desktop.test(lowerQuery)) platforms.push('desktop');
|
||||||
|
|
||||||
|
return platforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts error types from query
|
||||||
|
*/
|
||||||
|
function extractErrorTypes(lowerQuery: string): string[] {
|
||||||
|
const errorTypes: string[] = [];
|
||||||
|
|
||||||
|
for (const pattern of ERROR_TYPE_PATTERNS) {
|
||||||
|
if (pattern.test(lowerQuery)) {
|
||||||
|
// Extract the matched text
|
||||||
|
const match = lowerQuery.match(pattern);
|
||||||
|
if (match && !errorTypes.includes(match[0])) {
|
||||||
|
errorTypes.push(match[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts product mentions from query
|
||||||
|
*/
|
||||||
|
function extractProducts(lowerQuery: string): string[] {
|
||||||
|
const products: string[] = [];
|
||||||
|
|
||||||
|
// Known product IDs
|
||||||
|
const knownProducts = [
|
||||||
|
'lysnrai',
|
||||||
|
'mindlyst',
|
||||||
|
'chronomind',
|
||||||
|
'jarvisjr',
|
||||||
|
'nomgap',
|
||||||
|
'peakpulse',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const product of knownProducts) {
|
||||||
|
if (lowerQuery.includes(product)) {
|
||||||
|
products.push(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts constraints from query
|
||||||
|
*/
|
||||||
|
function extractConstraints(lowerQuery: string): string[] {
|
||||||
|
const constraints: string[] = [];
|
||||||
|
|
||||||
|
// Exclusion constraints
|
||||||
|
const excludeMatch = lowerQuery.match(/excluding?\s+(\w+)/i);
|
||||||
|
if (excludeMatch) {
|
||||||
|
constraints.push(`exclude:${excludeMatch[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only/beta constraints
|
||||||
|
if (lowerQuery.includes('only beta') || lowerQuery.includes('beta only')) {
|
||||||
|
constraints.push('userSegment:beta');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerQuery.includes('only production') || lowerQuery.includes('production only')) {
|
||||||
|
constraints.push('environment:production');
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Query Patterns
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface QueryPattern {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
examples: string[];
|
||||||
|
intent: QueryIntent;
|
||||||
|
requiredEntities: (keyof ExtractedEntities)[];
|
||||||
|
optionalEntities: (keyof ExtractedEntities)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QUERY_PATTERNS: QueryPattern[] = [
|
||||||
|
{
|
||||||
|
name: 'root_cause_investigation',
|
||||||
|
description: 'Investigate why an error occurred',
|
||||||
|
examples: [
|
||||||
|
'Why did the iOS keyboard crash yesterday?',
|
||||||
|
'What caused the authentication failures?',
|
||||||
|
'Explain the memory leak in the dashboard',
|
||||||
|
],
|
||||||
|
intent: 'root_cause',
|
||||||
|
requiredEntities: [],
|
||||||
|
optionalEntities: ['errorTypes', 'platforms', 'timeRange', 'products'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'similar_errors_search',
|
||||||
|
description: 'Find errors similar to a specific one',
|
||||||
|
examples: [
|
||||||
|
'Show me similar database errors',
|
||||||
|
'Find crashes like the login timeout',
|
||||||
|
'Search for API failures in the last week',
|
||||||
|
],
|
||||||
|
intent: 'pattern_search',
|
||||||
|
requiredEntities: [],
|
||||||
|
optionalEntities: ['errorTypes', 'timeRange', 'platforms'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'trend_analysis',
|
||||||
|
description: 'Analyze error trends over time',
|
||||||
|
examples: [
|
||||||
|
'Did error rate increase after the release?',
|
||||||
|
'Show me the trend for timeout errors',
|
||||||
|
'Compare this week to last week',
|
||||||
|
],
|
||||||
|
intent: 'trend',
|
||||||
|
requiredEntities: ['timeRange'],
|
||||||
|
optionalEntities: ['errorTypes'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'impact_assessment',
|
||||||
|
description: 'Assess user impact of errors',
|
||||||
|
examples: [
|
||||||
|
'How many users were affected by the crash?',
|
||||||
|
'What is the scope of the authentication bug?',
|
||||||
|
'Count errors by platform',
|
||||||
|
],
|
||||||
|
intent: 'impact',
|
||||||
|
requiredEntities: [],
|
||||||
|
optionalEntities: ['errorTypes', 'platforms', 'timeRange'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cluster_comparison',
|
||||||
|
description: 'Compare two error clusters',
|
||||||
|
examples: [
|
||||||
|
'Compare cluster A to cluster B',
|
||||||
|
'Is this the same issue as last month?',
|
||||||
|
'What is the difference between these errors?',
|
||||||
|
],
|
||||||
|
intent: 'comparison',
|
||||||
|
requiredEntities: [],
|
||||||
|
optionalEntities: ['clusterIds'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a parsed query to known patterns
|
||||||
|
*/
|
||||||
|
export function matchQueryPattern(parsedQuery: ParsedQuery): QueryPattern | null {
|
||||||
|
for (const pattern of QUERY_PATTERNS) {
|
||||||
|
if (pattern.intent === parsedQuery.intent) {
|
||||||
|
// Check if required entities are present
|
||||||
|
const hasRequired = pattern.requiredEntities.every(
|
||||||
|
(entity) => parsedQuery.entities[entity] !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasRequired) {
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Query Suggestions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface QuerySuggestion {
|
||||||
|
query: string;
|
||||||
|
description: string;
|
||||||
|
pattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates suggested queries based on recent errors
|
||||||
|
*/
|
||||||
|
export function generateQuerySuggestions(
|
||||||
|
recentErrorTypes: string[],
|
||||||
|
platforms: string[]
|
||||||
|
): QuerySuggestion[] {
|
||||||
|
const suggestions: QuerySuggestion[] = [];
|
||||||
|
|
||||||
|
// Root cause suggestions
|
||||||
|
for (const errorType of recentErrorTypes.slice(0, 3)) {
|
||||||
|
suggestions.push({
|
||||||
|
query: `Why are there ${errorType} errors?`,
|
||||||
|
description: 'Investigate root cause',
|
||||||
|
pattern: 'root_cause_investigation',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Impact suggestions
|
||||||
|
suggestions.push({
|
||||||
|
query: 'How many users were affected by recent errors?',
|
||||||
|
description: 'Assess user impact',
|
||||||
|
pattern: 'impact_assessment',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trend suggestions
|
||||||
|
suggestions.push({
|
||||||
|
query: 'Show error trends over the last 7 days',
|
||||||
|
description: 'View error trends',
|
||||||
|
pattern: 'trend_analysis',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Platform-specific
|
||||||
|
for (const platform of platforms.slice(0, 2)) {
|
||||||
|
suggestions.push({
|
||||||
|
query: `Show ${platform} errors from today`,
|
||||||
|
description: `View ${platform} specific errors`,
|
||||||
|
pattern: 'similar_errors_search',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Query Validation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a parsed query
|
||||||
|
*/
|
||||||
|
export function validateQuery(parsedQuery: ParsedQuery): ValidationResult {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
// Check if query is too vague
|
||||||
|
const hasSpecificEntity =
|
||||||
|
parsedQuery.entities.errorTypes?.length ||
|
||||||
|
parsedQuery.entities.platforms?.length ||
|
||||||
|
parsedQuery.entities.products?.length;
|
||||||
|
|
||||||
|
if (!hasSpecificEntity) {
|
||||||
|
warnings.push('Query is broad. Consider specifying error type, platform, or product.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check time range
|
||||||
|
if (parsedQuery.entities.timeRange) {
|
||||||
|
const start = new Date(parsedQuery.entities.timeRange.start);
|
||||||
|
const end = new Date(parsedQuery.entities.timeRange.end);
|
||||||
|
const daysDiff = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
if (daysDiff > 90) {
|
||||||
|
warnings.push('Time range exceeds 90 days. Results may be slow.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -65,6 +65,7 @@ import { impersonationRoutes } from './modules/impersonation/routes.js';
|
|||||||
import { changelogRoutes } from './modules/changelog/routes.js';
|
import { changelogRoutes } from './modules/changelog/routes.js';
|
||||||
import { webhookRoutes } from './modules/webhooks/routes.js';
|
import { webhookRoutes } from './modules/webhooks/routes.js';
|
||||||
import { marketplaceRoutes } from './modules/marketplace/routes.js';
|
import { marketplaceRoutes } from './modules/marketplace/routes.js';
|
||||||
|
import { predictiveAnalyticsRoutes } from './modules/predictive-analytics/routes.js';
|
||||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||||
import { config } from './lib/config.js';
|
import { config } from './lib/config.js';
|
||||||
import { seedDefaultFlags } from './modules/flags/seed.js';
|
import { seedDefaultFlags } from './modules/flags/seed.js';
|
||||||
@ -176,6 +177,8 @@ await app.register(changelogRoutes, { prefix: '/api' });
|
|||||||
await app.register(webhookRoutes, { prefix: '/api' });
|
await app.register(webhookRoutes, { prefix: '/api' });
|
||||||
// Generic Marketplace module
|
// Generic Marketplace module
|
||||||
await app.register(marketplaceRoutes, { prefix: '/api' });
|
await app.register(marketplaceRoutes, { prefix: '/api' });
|
||||||
|
// Predictive Analytics (Churn & Health Scoring)
|
||||||
|
await app.register(predictiveAnalyticsRoutes, { prefix: '/api' });
|
||||||
// Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md)
|
// Broadcast Messaging & Surveys (see docs/roadmaps/not-started/platform_BROADCAST_SURVEY_ROADMAP.md)
|
||||||
await app.register(broadcastRoutes, { prefix: '/api' });
|
await app.register(broadcastRoutes, { prefix: '/api' });
|
||||||
await app.register(surveyRoutes, { prefix: '/api' });
|
await app.register(surveyRoutes, { prefix: '/api' });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user