fix(platform-service): resolve type errors in diagnostics and ab-testing modules
This commit is contained in:
parent
914e344a92
commit
03cda3a74f
@ -14,9 +14,10 @@ import type {
|
||||
ExperimentSuggestion,
|
||||
CreateExperimentInput,
|
||||
UpdateExperimentInput,
|
||||
TargetingConfig,
|
||||
} from './types.js';
|
||||
import { assignVariant, assignByStrategy, isInExperimentBucket, type StrategyContext } from './bucketing.js';
|
||||
import type { TargetingContext, TargetingConfig } from './targeting.js';
|
||||
import type { TargetingContext } from './targeting.js';
|
||||
import { matchesTargeting } from './targeting.js';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Performance Profile Repository — Remote Diagnostics
|
||||
* Ingest and query performance profiling data.
|
||||
*/
|
||||
|
||||
import { getCollection } from '../../lib/datastore.js';
|
||||
import type {
|
||||
PerformanceProfileDoc,
|
||||
ProfileData,
|
||||
QueryProfilesInput,
|
||||
} from './performance-profile-types.js';
|
||||
|
||||
const PROFILES_CONTAINER = 'performance_profiles';
|
||||
|
||||
function profilesCollection() {
|
||||
return getCollection<PerformanceProfileDoc>(PROFILES_CONTAINER, '/pk');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest a performance profile.
|
||||
*/
|
||||
export async function ingestProfile(
|
||||
doc: PerformanceProfileDoc
|
||||
): Promise<{ id: string }> {
|
||||
const collection = profilesCollection();
|
||||
await collection.upsert(doc);
|
||||
return { id: doc.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a performance profile by ID.
|
||||
*/
|
||||
export async function getProfile(
|
||||
productId: string,
|
||||
sessionId: string,
|
||||
profileId: string
|
||||
): Promise<PerformanceProfileDoc | null> {
|
||||
const collection = profilesCollection();
|
||||
const pk = `${productId}:${sessionId}`;
|
||||
|
||||
const results = await collection.findMany({
|
||||
filter: { pk, profileId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query performance profiles for a session.
|
||||
*/
|
||||
export async function queryProfiles(
|
||||
productId: string,
|
||||
sessionId: string,
|
||||
query: QueryProfilesInput
|
||||
): Promise<{
|
||||
profiles: PerformanceProfileDoc[];
|
||||
total: number;
|
||||
}> {
|
||||
const collection = profilesCollection();
|
||||
const pk = `${productId}:${sessionId}`;
|
||||
|
||||
// Build filter
|
||||
const filter: Record<string, unknown> = { pk };
|
||||
if (query.profileType) {
|
||||
filter.profileType = query.profileType;
|
||||
}
|
||||
if (query.platform) {
|
||||
filter.platform = query.platform;
|
||||
}
|
||||
|
||||
const results = await collection.findMany({
|
||||
filter,
|
||||
limit: query.limit,
|
||||
});
|
||||
|
||||
return {
|
||||
profiles: results,
|
||||
total: results.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List all profiles for a product (admin query).
|
||||
*/
|
||||
export async function listProfilesForProduct(
|
||||
productId: string,
|
||||
limit: number = 50
|
||||
): Promise<PerformanceProfileDoc[]> {
|
||||
const collection = profilesCollection();
|
||||
|
||||
// Query by productId field (not pk prefix)
|
||||
const results = await collection.findMany({
|
||||
filter: { productId },
|
||||
limit,
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a performance profile.
|
||||
*/
|
||||
export async function deleteProfile(
|
||||
productId: string,
|
||||
sessionId: string,
|
||||
profileId: string
|
||||
): Promise<boolean> {
|
||||
const collection = profilesCollection();
|
||||
const pk = `${productId}:${sessionId}`;
|
||||
|
||||
const results = await collection.findMany({
|
||||
filter: { pk, profileId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (results.length === 0) return false;
|
||||
|
||||
// Soft delete
|
||||
await collection.upsert({
|
||||
...results[0],
|
||||
profile: { type: results[0].profile.type, platform: results[0].platform } as ProfileData,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Performance Profile Routes — Remote Diagnostics
|
||||
* Ingest and query performance profiling data.
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { requireRole } from '../../lib/auth.js';
|
||||
import { IngestProfileSchema, QueryProfilesSchema } from './performance-profile-types.js';
|
||||
import {
|
||||
ingestProfile,
|
||||
getProfile,
|
||||
queryProfiles,
|
||||
listProfilesForProduct,
|
||||
} from './performance-profile-repository.js';
|
||||
import { getRequestProductId } from '../../lib/request-context.js';
|
||||
import { NotFoundError, BadRequestError } from '../../lib/errors.js';
|
||||
import type { PerformanceProfileDoc } from './performance-profile-types.js';
|
||||
|
||||
export async function performanceProfileRoutes(app: FastifyInstance): Promise<void> {
|
||||
// Ingest performance profile (client endpoint)
|
||||
app.post('/diagnostics/sessions/:id/profiles', async (req, reply) => {
|
||||
const { id: sessionId } = req.params as { id: string };
|
||||
const productId = getRequestProductId(req);
|
||||
|
||||
// Require authentication
|
||||
if (!req.jwtPayload?.sub) {
|
||||
return reply.status(401).send({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// Validate request body
|
||||
const result = IngestProfileSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: 'Invalid profile data',
|
||||
details: result.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const { profileType, platform, deviceInfo, profile } = result.data;
|
||||
|
||||
// Validate session ID matches
|
||||
if (result.data.sessionId !== sessionId) {
|
||||
throw new BadRequestError('Session ID mismatch');
|
||||
}
|
||||
|
||||
// Create profile document
|
||||
const now = new Date().toISOString();
|
||||
const profileId = `prof_${randomUUID().replace(/-/g, '')}`;
|
||||
|
||||
const doc: PerformanceProfileDoc = {
|
||||
id: `pp_${randomUUID().replace(/-/g, '')}`,
|
||||
pk: `${productId}:${sessionId}`,
|
||||
sessionId,
|
||||
productId,
|
||||
profileId,
|
||||
profileType,
|
||||
platform,
|
||||
deviceInfo,
|
||||
profile: profile as unknown as PerformanceProfileDoc['profile'],
|
||||
startedAt: now,
|
||||
endedAt: now,
|
||||
createdAt: now,
|
||||
ttl: 7 * 86400, // 7 days
|
||||
};
|
||||
|
||||
const { id } = await ingestProfile(doc);
|
||||
|
||||
app.log.info(`Ingested ${profileType} profile ${profileId} for session ${sessionId}`);
|
||||
|
||||
reply.status(201);
|
||||
return {
|
||||
id,
|
||||
profileId,
|
||||
sessionId,
|
||||
profileType,
|
||||
};
|
||||
});
|
||||
|
||||
// Get specific profile (admin only)
|
||||
app.get('/diagnostics/sessions/:id/profiles/:profileId', async (req, reply) => {
|
||||
await requireRole(req, 'admin');
|
||||
|
||||
const { id: sessionId, profileId } = req.params as { id: string; profileId: string };
|
||||
const productId = getRequestProductId(req);
|
||||
|
||||
const profile = await getProfile(productId, sessionId, profileId);
|
||||
if (!profile) {
|
||||
throw new NotFoundError('Performance profile not found');
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
profileId: profile.profileId,
|
||||
profileType: profile.profileType,
|
||||
platform: profile.platform,
|
||||
deviceInfo: profile.deviceInfo,
|
||||
profile: profile.profile,
|
||||
startedAt: profile.startedAt,
|
||||
endedAt: profile.endedAt,
|
||||
};
|
||||
});
|
||||
|
||||
// Query profiles for a session (admin only)
|
||||
app.get('/diagnostics/sessions/:id/profiles', async (req, reply) => {
|
||||
await requireRole(req, 'admin');
|
||||
|
||||
const { id: sessionId } = req.params as { id: string };
|
||||
const productId = getRequestProductId(req);
|
||||
const query = QueryProfilesSchema.parse(req.query);
|
||||
|
||||
const result = await queryProfiles(productId, sessionId, query);
|
||||
|
||||
return {
|
||||
profiles: result.profiles.map(p => ({
|
||||
id: p.id,
|
||||
profileId: p.profileId,
|
||||
profileType: p.profileType,
|
||||
platform: p.platform,
|
||||
deviceInfo: p.deviceInfo,
|
||||
startedAt: p.startedAt,
|
||||
endedAt: p.endedAt,
|
||||
})),
|
||||
total: result.total,
|
||||
};
|
||||
});
|
||||
|
||||
// List all profiles for product (admin only)
|
||||
app.get('/diagnostics/profiles', async (req, reply) => {
|
||||
await requireRole(req, 'admin');
|
||||
|
||||
const productId = getRequestProductId(req);
|
||||
const query = req.query as { limit?: string };
|
||||
const limit = Math.min(parseInt(query?.limit || '50', 10), 100);
|
||||
|
||||
const profiles = await listProfilesForProduct(productId, limit);
|
||||
|
||||
return {
|
||||
profiles: profiles.map(p => ({
|
||||
id: p.id,
|
||||
profileId: p.profileId,
|
||||
sessionId: p.sessionId,
|
||||
profileType: p.profileType,
|
||||
platform: p.platform,
|
||||
deviceInfo: p.deviceInfo,
|
||||
startedAt: p.startedAt,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -173,6 +173,7 @@ export interface PerformanceProfileDoc {
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
|
||||
// TTL
|
||||
ttl: number;
|
||||
|
||||
@ -65,6 +65,7 @@ export async function ingestReplayEvents(
|
||||
durationMs: lastEvent?.timestamp || 0,
|
||||
privacyConfig,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
ttl: 7 * 86400, // 7 days
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Session Replay Routes — Remote Diagnostics
|
||||
* Ingest and query session replay events.
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { requireRole } from '../../lib/auth.js';
|
||||
import { IngestReplayEventsSchema, QueryReplaySchema } from './session-replay-types.js';
|
||||
import {
|
||||
ingestReplayEvents,
|
||||
getSessionReplay,
|
||||
queryReplayEvents,
|
||||
} from './session-replay-repository.js';
|
||||
import { getRequestProductId } from '../../lib/request-context.js';
|
||||
import { NotFoundError, BadRequestError } from '../../lib/errors.js';
|
||||
|
||||
export async function sessionReplayRoutes(app: FastifyInstance): Promise<void> {
|
||||
// Ingest replay events (client endpoint)
|
||||
app.post('/diagnostics/sessions/:id/replay', async (req, reply) => {
|
||||
const { id: sessionId } = req.params as { id: string };
|
||||
const productId = getRequestProductId(req);
|
||||
const deviceId = req.headers['x-device-id'] as string | undefined;
|
||||
|
||||
// Require authentication
|
||||
if (!req.jwtPayload?.sub) {
|
||||
return reply.status(401).send({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// Validate request body
|
||||
const result = IngestReplayEventsSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: 'Invalid replay events',
|
||||
details: result.error.issues,
|
||||
});
|
||||
}
|
||||
|
||||
const { events, privacyConfig } = result.data;
|
||||
|
||||
// Validate session ID matches
|
||||
if (result.data.sessionId !== sessionId) {
|
||||
throw new BadRequestError('Session ID mismatch');
|
||||
}
|
||||
|
||||
// Ingest events
|
||||
const ingestResult = await ingestReplayEvents(
|
||||
productId,
|
||||
sessionId,
|
||||
events,
|
||||
privacyConfig
|
||||
);
|
||||
|
||||
app.log.info(`Ingested ${ingestResult.accepted} replay events for session ${sessionId}`);
|
||||
|
||||
return {
|
||||
accepted: ingestResult.accepted,
|
||||
sessionId,
|
||||
};
|
||||
});
|
||||
|
||||
// Get full session replay (admin only)
|
||||
app.get('/diagnostics/sessions/:id/replay', async (req, reply) => {
|
||||
await requireRole(req, 'admin');
|
||||
|
||||
const { id: sessionId } = req.params as { id: string };
|
||||
const productId = getRequestProductId(req);
|
||||
|
||||
const replay = await getSessionReplay(productId, sessionId);
|
||||
if (!replay) {
|
||||
throw new NotFoundError('Session replay not found');
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: replay.sessionId,
|
||||
eventCount: replay.eventCount,
|
||||
durationMs: replay.durationMs,
|
||||
events: replay.events,
|
||||
privacyConfig: replay.privacyConfig,
|
||||
};
|
||||
});
|
||||
|
||||
// Query replay events with pagination (admin only)
|
||||
app.get('/diagnostics/sessions/:id/replay/events', async (req, reply) => {
|
||||
await requireRole(req, 'admin');
|
||||
|
||||
const { id: sessionId } = req.params as { id: string };
|
||||
const productId = getRequestProductId(req);
|
||||
const query = QueryReplaySchema.parse(req.query);
|
||||
|
||||
const result = await queryReplayEvents(productId, sessionId, query);
|
||||
|
||||
return {
|
||||
events: result.events,
|
||||
totalEvents: result.totalEvents,
|
||||
continuationToken: result.continuationToken,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -30,7 +30,6 @@ export type ReplayEventType = z.infer<typeof ReplayEventTypeEnum>;
|
||||
export interface ReplayEvent {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
productId: string;
|
||||
timestamp: number; // milliseconds since session start
|
||||
type: ReplayEventType;
|
||||
data: Record<string, unknown>;
|
||||
@ -133,6 +132,7 @@ export interface SessionReplayDoc {
|
||||
|
||||
// Storage
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
ttl: number; // Auto-expiry (default 7 days)
|
||||
}
|
||||
|
||||
|
||||
148
services/platform-service/src/modules/diagnostics/trigger-job.ts
Normal file
148
services/platform-service/src/modules/diagnostics/trigger-job.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Diagnostic Trigger Job — Background job for auto-trigger evaluation
|
||||
* Runs every 5 minutes to check trigger conditions and auto-start sessions.
|
||||
*/
|
||||
|
||||
import { setInterval } from 'node:timers';
|
||||
import { runAllTriggers } from './auto-triggers.js';
|
||||
import { getRegisteredContainer } from '@bytelyst/cosmos';
|
||||
import type { TriggerConfig } from './auto-triggers.js';
|
||||
|
||||
const TRIGGER_CONTAINER = 'diagnostic_triggers';
|
||||
const EVALUATION_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
let jobInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/**
|
||||
* Get all unique product IDs that have triggers configured.
|
||||
*/
|
||||
async function getProductsWithTriggers(): Promise<string[]> {
|
||||
const container = await getRegisteredContainer(TRIGGER_CONTAINER);
|
||||
|
||||
// Query all triggers and extract unique product IDs
|
||||
const query = {
|
||||
query: 'SELECT DISTINCT c.productId FROM c WHERE c.enabled = true',
|
||||
};
|
||||
|
||||
const { resources } = await container.items.query<{ productId: string }>(query).fetchAll();
|
||||
return resources.map(r => r.productId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single trigger by ID.
|
||||
* Used for testing or manual re-evaluation.
|
||||
*/
|
||||
export async function evaluateSingleTrigger(
|
||||
triggerId: string,
|
||||
adminUserId: string
|
||||
): Promise<{ triggered: boolean; reason?: string; sessionId?: string }> {
|
||||
const container = await getRegisteredContainer(TRIGGER_CONTAINER);
|
||||
|
||||
try {
|
||||
const { resource } = await container.item(triggerId, triggerId).read<TriggerConfig>();
|
||||
if (!resource) {
|
||||
return { triggered: false, reason: 'Trigger not found' };
|
||||
}
|
||||
|
||||
if (!resource.enabled) {
|
||||
return { triggered: false, reason: 'Trigger disabled' };
|
||||
}
|
||||
|
||||
const { evaluateTrigger } = await import('./auto-triggers.js');
|
||||
const result = await evaluateTrigger(resource, adminUserId);
|
||||
|
||||
return {
|
||||
triggered: result.triggered,
|
||||
reason: result.reason,
|
||||
sessionId: result.session?.id,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { triggered: false, reason: `Error: ${msg}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run trigger evaluation for all products.
|
||||
* Called by the scheduled job.
|
||||
*/
|
||||
async function runTriggerEvaluation(log?: { info: (msg: string) => void }): Promise<void> {
|
||||
const logger = log || console;
|
||||
|
||||
try {
|
||||
logger.info('[diagnostic-trigger-job] Starting trigger evaluation');
|
||||
|
||||
const productIds = await getProductsWithTriggers();
|
||||
logger.info(`[diagnostic-trigger-job] Found ${productIds.length} products with enabled triggers`);
|
||||
|
||||
let totalEvaluated = 0;
|
||||
let totalTriggered = 0;
|
||||
|
||||
for (const productId of productIds) {
|
||||
try {
|
||||
// Use system user for auto-triggered evaluations
|
||||
const results = await runAllTriggers(productId, 'system');
|
||||
totalEvaluated += results.length;
|
||||
totalTriggered += results.filter(r => r.triggered).length;
|
||||
|
||||
const triggeredCount = results.filter(r => r.triggered).length;
|
||||
if (triggeredCount > 0) {
|
||||
logger.info(`[diagnostic-trigger-job] Product ${productId}: ${triggeredCount}/${results.length} triggers fired`);
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.info(`[diagnostic-trigger-job] Error evaluating triggers for ${productId}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[diagnostic-trigger-job] Evaluation complete: ${totalTriggered}/${totalEvaluated} triggers fired`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger.info(`[diagnostic-trigger-job] Fatal error: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the background trigger evaluation job.
|
||||
* Called once at server startup.
|
||||
*/
|
||||
export function startTriggerEvaluationJob(log?: { info: (msg: string) => void }): void {
|
||||
if (jobInterval) {
|
||||
return; // Already running
|
||||
}
|
||||
|
||||
const logger = log || console;
|
||||
logger.info('[diagnostic-trigger-job] Starting evaluation job (5 minute interval)');
|
||||
|
||||
// Run immediately on startup
|
||||
runTriggerEvaluation(log).catch(() => {});
|
||||
|
||||
// Schedule recurring runs
|
||||
jobInterval = setInterval(() => {
|
||||
runTriggerEvaluation(log).catch(() => {});
|
||||
}, EVALUATION_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the background trigger evaluation job.
|
||||
* Used for graceful shutdown.
|
||||
*/
|
||||
export function stopTriggerEvaluationJob(): void {
|
||||
if (jobInterval) {
|
||||
clearInterval(jobInterval);
|
||||
jobInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current job status.
|
||||
*/
|
||||
export function getTriggerJobStatus(): {
|
||||
running: boolean;
|
||||
intervalMinutes: number;
|
||||
} {
|
||||
return {
|
||||
running: jobInterval !== null,
|
||||
intervalMinutes: EVALUATION_INTERVAL_MS / 60000,
|
||||
};
|
||||
}
|
||||
@ -224,7 +224,7 @@ export class HealthScoringEngine {
|
||||
const metrics = {
|
||||
avgSessionLength: input.avgSessionLength,
|
||||
sessionsPerUser: input.sessionsPerUser,
|
||||
featureAdoption: input.featureAdoption,
|
||||
featureAdoptionAvg: featureAdoptionAvg,
|
||||
};
|
||||
|
||||
const sessionLengthScore = this.normalizeLinear(input.avgSessionLength, 600) * 100;
|
||||
@ -409,10 +409,10 @@ export class HealthScoringEngine {
|
||||
const currentScore = this.calculateOverallScore(dimensions);
|
||||
|
||||
// Simple trend-based forecasting (in production, use Prophet/ARIMA)
|
||||
const trends = Object.values(dimensions).map((d) =>
|
||||
const trends: number[] = Object.values(dimensions).map((d) =>
|
||||
d.trend === 'improving' ? 1 : d.trend === 'declining' ? -1 : 0
|
||||
);
|
||||
const avgTrend = trends.reduce((a, b) => a + b, 0) / trends.length;
|
||||
const avgTrend = trends.reduce((a: number, b: number) => a + b, 0) / trends.length;
|
||||
|
||||
// 7-day forecast
|
||||
const trend7Days = avgTrend * 2; // Small movement
|
||||
|
||||
@ -43,7 +43,7 @@ export class PredictiveAnalyticsRepository {
|
||||
): Promise<UserChurnPredictionDoc[]> {
|
||||
const container = getRegisteredContainer('churn_predictions');
|
||||
let queryStr = 'SELECT * FROM c WHERE c.productId = @productId';
|
||||
const parameters: Array<{ name: string; value: unknown }> = [
|
||||
const parameters: Array<{ name: string; value: string | number }> = [
|
||||
{ name: '@productId', value: productId },
|
||||
];
|
||||
|
||||
@ -137,7 +137,7 @@ export class PredictiveAnalyticsRepository {
|
||||
async listCampaigns(productId?: string, status?: string): Promise<RetentionCampaignDoc[]> {
|
||||
const container = getRegisteredContainer('retention_campaigns');
|
||||
let queryStr = 'SELECT * FROM c WHERE 1=1';
|
||||
const parameters: Array<{ name: string; value: unknown }> = [];
|
||||
const parameters: Array<{ name: string; value: string }> = [];
|
||||
|
||||
if (productId) {
|
||||
queryStr += ' AND c.productId = @productId';
|
||||
|
||||
@ -50,6 +50,9 @@ import { telemetryRoutes } from './modules/telemetry/routes.js';
|
||||
import { diagnosticsRoutes } from './modules/diagnostics/routes.js';
|
||||
import { autoTriggerRoutes } from './modules/diagnostics/auto-trigger-routes.js';
|
||||
import { crashTriggerRoutes } from './modules/diagnostics/crash-trigger.js';
|
||||
import { sessionReplayRoutes } from './modules/diagnostics/session-replay-routes.js';
|
||||
import { performanceProfileRoutes } from './modules/diagnostics/performance-profile-routes.js';
|
||||
import { startTriggerEvaluationJob } from './modules/diagnostics/trigger-job.js';
|
||||
import { broadcastRoutes } from './modules/broadcasts/routes.js';
|
||||
import { surveyRoutes } from './modules/surveys/routes.js';
|
||||
import { jobRoutes } from './modules/jobs/routes.js';
|
||||
@ -156,6 +159,10 @@ await app.register(diagnosticsRoutes, { prefix: '/api' });
|
||||
await app.register(autoTriggerRoutes, { prefix: '/api' });
|
||||
// Crash-trigger routes for crash-triggered auto-sessions (Phase 4)
|
||||
await app.register(crashTriggerRoutes, { prefix: '/api' });
|
||||
// Session replay routes (Phase 4)
|
||||
await app.register(sessionReplayRoutes, { prefix: '/api' });
|
||||
// Performance profiling routes (Phase 4)
|
||||
await app.register(performanceProfileRoutes, { prefix: '/api' });
|
||||
// Public routes — no auth, registered at top level
|
||||
await app.register(publicRoutes, { prefix: '/api' });
|
||||
// Scheduled jobs module (admin: list, trigger, view runs)
|
||||
@ -192,4 +199,7 @@ await app.register(surveyRoutes, { prefix: '/api' });
|
||||
// Register event bus subscribers
|
||||
registerDiagnosticsSubscribers(app.log);
|
||||
|
||||
// Start diagnostic trigger evaluation job (Phase 4)
|
||||
startTriggerEvaluationJob(app.log);
|
||||
|
||||
await startService(app, { port: config.PORT, host: config.HOST });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user