fix(feedback-client): correct ApiClient method usage and types
This commit is contained in:
parent
05ad9dbedc
commit
914e344a92
0
docs/learning_ai_common_plat_INVENTORY.md
Normal file
0
docs/learning_ai_common_plat_INVENTORY.md
Normal file
@ -8,7 +8,7 @@ import { createApiClient, type ApiClient } from '@bytelyst/api-client';
|
||||
|
||||
export interface FeedbackClientConfig {
|
||||
baseUrl: string;
|
||||
getAuthToken: () => string | Promise<string>;
|
||||
getAuthToken: () => string | null;
|
||||
}
|
||||
|
||||
export interface DeviceContext {
|
||||
@ -120,18 +120,22 @@ export class FeedbackClient {
|
||||
}
|
||||
|
||||
// Step 3: Submit feedback
|
||||
const response = await this.api.post<FeedbackResponse>('/api/feedback', {
|
||||
type: params.type,
|
||||
title: params.title,
|
||||
body: params.body,
|
||||
screen: params.screen,
|
||||
rating: params.rating,
|
||||
appVersion: params.appVersion,
|
||||
platform: params.platform,
|
||||
screenshotBlobPath: screenshotMeta?.blobPath,
|
||||
screenshotContentType: screenshotMeta?.contentType as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
screenshotSizeBytes: screenshotMeta?.sizeBytes,
|
||||
deviceContext: params.deviceContext,
|
||||
const response = await this.api.fetch<FeedbackResponse>('/api/feedback', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: params.type,
|
||||
title: params.title,
|
||||
body: params.body,
|
||||
screen: params.screen,
|
||||
rating: params.rating,
|
||||
appVersion: params.appVersion,
|
||||
platform: params.platform,
|
||||
screenshotBlobPath: screenshotMeta?.blobPath,
|
||||
screenshotContentType: screenshotMeta?.contentType as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
screenshotSizeBytes: screenshotMeta?.sizeBytes,
|
||||
deviceContext: params.deviceContext,
|
||||
}),
|
||||
});
|
||||
|
||||
return response;
|
||||
@ -143,8 +147,10 @@ export class FeedbackClient {
|
||||
private async generateSasUrl(
|
||||
contentType: 'image/png' | 'image/jpeg' | 'image/webp'
|
||||
): Promise<SasResponse> {
|
||||
const response = await this.api.post<SasResponse>('/api/feedback/sas', {
|
||||
contentType,
|
||||
const response = await this.api.fetch<SasResponse>('/api/feedback/sas', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ contentType }),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Session Replay Repository — Remote Diagnostics
|
||||
* Ingest and query session replay events.
|
||||
*/
|
||||
|
||||
import { getCollection } from '../../lib/datastore.js';
|
||||
import type {
|
||||
SessionReplayDoc,
|
||||
ReplayEvent,
|
||||
QueryReplayInput,
|
||||
} from './session-replay-types.js';
|
||||
|
||||
const REPLAY_CONTAINER = 'session_replays';
|
||||
|
||||
function replaysCollection() {
|
||||
return getCollection<SessionReplayDoc>(REPLAY_CONTAINER, '/pk');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest replay events for a session.
|
||||
*/
|
||||
export async function ingestReplayEvents(
|
||||
productId: string,
|
||||
sessionId: string,
|
||||
events: ReplayEvent[],
|
||||
privacyConfig: SessionReplayDoc['privacyConfig']
|
||||
): Promise<{ accepted: number }> {
|
||||
const collection = replaysCollection();
|
||||
|
||||
// Build or update session replay document
|
||||
const pk = `${productId}:${sessionId}`;
|
||||
const existing = await collection.findMany({
|
||||
filter: { pk, sessionId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Append events to existing document
|
||||
const doc = existing[0];
|
||||
const updatedEvents = [...doc.events, ...events];
|
||||
|
||||
await collection.upsert({
|
||||
...doc,
|
||||
events: updatedEvents,
|
||||
eventCount: updatedEvents.length,
|
||||
durationMs: events[events.length - 1]?.timestamp || doc.durationMs,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
// Create new replay document
|
||||
const firstEvent = events[0];
|
||||
const lastEvent = events[events.length - 1];
|
||||
|
||||
const doc: SessionReplayDoc = {
|
||||
id: `replay_${sessionId}`,
|
||||
pk,
|
||||
sessionId,
|
||||
productId,
|
||||
events,
|
||||
eventCount: events.length,
|
||||
startTimestamp: now,
|
||||
endTimestamp: now,
|
||||
durationMs: lastEvent?.timestamp || 0,
|
||||
privacyConfig,
|
||||
createdAt: now,
|
||||
ttl: 7 * 86400, // 7 days
|
||||
};
|
||||
|
||||
await collection.upsert(doc);
|
||||
}
|
||||
|
||||
return { accepted: events.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get replay events for a session.
|
||||
*/
|
||||
export async function getSessionReplay(
|
||||
productId: string,
|
||||
sessionId: string
|
||||
): Promise<SessionReplayDoc | null> {
|
||||
const collection = replaysCollection();
|
||||
const pk = `${productId}:${sessionId}`;
|
||||
|
||||
const results = await collection.findMany({
|
||||
filter: { pk, sessionId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query replay events with pagination.
|
||||
*/
|
||||
export async function queryReplayEvents(
|
||||
productId: string,
|
||||
sessionId: string,
|
||||
query: QueryReplayInput
|
||||
): Promise<{
|
||||
events: ReplayEvent[];
|
||||
totalEvents: number;
|
||||
continuationToken?: string;
|
||||
}> {
|
||||
const replay = await getSessionReplay(productId, sessionId);
|
||||
|
||||
if (!replay) {
|
||||
return { events: [], totalEvents: 0 };
|
||||
}
|
||||
|
||||
// Simple pagination based on event index
|
||||
const allEvents = replay.events;
|
||||
const totalEvents = allEvents.length;
|
||||
|
||||
let startIndex = 0;
|
||||
if (query.continuationToken) {
|
||||
startIndex = parseInt(query.continuationToken, 10);
|
||||
if (isNaN(startIndex)) startIndex = 0;
|
||||
}
|
||||
|
||||
const endIndex = Math.min(startIndex + query.limit, totalEvents);
|
||||
const events = allEvents.slice(startIndex, endIndex);
|
||||
|
||||
const nextContinuationToken = endIndex < totalEvents ? String(endIndex) : undefined;
|
||||
|
||||
return {
|
||||
events,
|
||||
totalEvents,
|
||||
continuationToken: nextContinuationToken,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete session replay data.
|
||||
*/
|
||||
export async function deleteSessionReplay(
|
||||
productId: string,
|
||||
sessionId: string
|
||||
): Promise<boolean> {
|
||||
const collection = replaysCollection();
|
||||
const pk = `${productId}:${sessionId}`;
|
||||
|
||||
const results = await collection.findMany({
|
||||
filter: { pk, sessionId },
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (results.length === 0) return false;
|
||||
|
||||
// Soft delete - update with empty events
|
||||
await collection.upsert({
|
||||
...results[0],
|
||||
events: [],
|
||||
eventCount: 0,
|
||||
durationMs: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { requireAuth, requireAdmin } from '@bytelyst/auth';
|
||||
import { UnauthorizedError, ForbiddenError } from '../../lib/errors.js';
|
||||
import { extractFeaturesFromTelemetry } from './feature-extractor.js';
|
||||
import { featureStore } from './feature-store.js';
|
||||
import { churnModel } from './churn-model.js';
|
||||
@ -21,8 +21,25 @@ import {
|
||||
CreateCampaignSchema,
|
||||
CampaignTriggerSchema,
|
||||
RiskSegmentEnum,
|
||||
type UserChurnPredictionDoc,
|
||||
} from './types.js';
|
||||
|
||||
// Auth Helpers
|
||||
interface JwtPayload {
|
||||
sub: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
function requireAuth(req: { jwtPayload?: JwtPayload }): string {
|
||||
if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required');
|
||||
return req.jwtPayload.sub;
|
||||
}
|
||||
|
||||
function requireAdmin(req: { jwtPayload?: JwtPayload }): void {
|
||||
requireAuth(req);
|
||||
if (req.jwtPayload?.role !== 'admin') throw new ForbiddenError('Admin access required');
|
||||
}
|
||||
|
||||
// Get telemetry repository for fetching user events
|
||||
async function getUserTelemetryEvents(
|
||||
userId: string,
|
||||
@ -84,7 +101,8 @@ export async function predictiveAnalyticsRoutes(fastify: FastifyInstance): Promi
|
||||
}
|
||||
|
||||
// Fetch telemetry events
|
||||
const events = await getUserTelemetryEvents(userId, productId);
|
||||
const rawEvents = await getUserTelemetryEvents(userId, productId);
|
||||
const events = rawEvents as unknown as Parameters<typeof extractFeaturesFromTelemetry>[2];
|
||||
|
||||
// Extract features
|
||||
const features = extractFeaturesFromTelemetry(
|
||||
@ -100,15 +118,16 @@ export async function predictiveAnalyticsRoutes(fastify: FastifyInstance): Promi
|
||||
// Run prediction
|
||||
const prediction = churnModel.predict(features, parseInt(horizon, 10));
|
||||
|
||||
// Save prediction
|
||||
const doc = {
|
||||
// Save prediction (construct doc carefully to avoid duplicate property issues)
|
||||
const { userId: _uid, productId: _pid, ...predictionData } = prediction;
|
||||
const doc: UserChurnPredictionDoc = {
|
||||
id: `cp_${crypto.randomUUID()}`,
|
||||
pk: `${userId}:${productId}`,
|
||||
userId,
|
||||
productId,
|
||||
...prediction,
|
||||
actualChurned: undefined,
|
||||
validationDate: undefined,
|
||||
...predictionData,
|
||||
explanation: prediction.explanation,
|
||||
interventionHistory: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
ttl: (parseInt(horizon, 10) + 90) * 24 * 60 * 60,
|
||||
};
|
||||
@ -138,11 +157,12 @@ export async function predictiveAnalyticsRoutes(fastify: FastifyInstance): Promi
|
||||
|
||||
const results = await Promise.all(
|
||||
userIds.map(async (userId) => {
|
||||
const events = await getUserTelemetryEvents(userId, productId);
|
||||
const rawEvents = await getUserTelemetryEvents(userId, productId);
|
||||
const events = rawEvents as unknown as Parameters<typeof extractFeaturesFromTelemetry>[2];
|
||||
const features = extractFeaturesFromTelemetry(
|
||||
userId,
|
||||
productId,
|
||||
events as Parameters<typeof extractFeaturesFromTelemetry>[2],
|
||||
events,
|
||||
new Date()
|
||||
);
|
||||
const prediction = churnModel.predict(features, parseInt(horizon, 10));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user