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 {
|
export interface FeedbackClientConfig {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
getAuthToken: () => string | Promise<string>;
|
getAuthToken: () => string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceContext {
|
export interface DeviceContext {
|
||||||
@ -120,18 +120,22 @@ export class FeedbackClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Submit feedback
|
// Step 3: Submit feedback
|
||||||
const response = await this.api.post<FeedbackResponse>('/api/feedback', {
|
const response = await this.api.fetch<FeedbackResponse>('/api/feedback', {
|
||||||
type: params.type,
|
method: 'POST',
|
||||||
title: params.title,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: params.body,
|
body: JSON.stringify({
|
||||||
screen: params.screen,
|
type: params.type,
|
||||||
rating: params.rating,
|
title: params.title,
|
||||||
appVersion: params.appVersion,
|
body: params.body,
|
||||||
platform: params.platform,
|
screen: params.screen,
|
||||||
screenshotBlobPath: screenshotMeta?.blobPath,
|
rating: params.rating,
|
||||||
screenshotContentType: screenshotMeta?.contentType as 'image/png' | 'image/jpeg' | 'image/webp',
|
appVersion: params.appVersion,
|
||||||
screenshotSizeBytes: screenshotMeta?.sizeBytes,
|
platform: params.platform,
|
||||||
deviceContext: params.deviceContext,
|
screenshotBlobPath: screenshotMeta?.blobPath,
|
||||||
|
screenshotContentType: screenshotMeta?.contentType as 'image/png' | 'image/jpeg' | 'image/webp',
|
||||||
|
screenshotSizeBytes: screenshotMeta?.sizeBytes,
|
||||||
|
deviceContext: params.deviceContext,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -143,8 +147,10 @@ export class FeedbackClient {
|
|||||||
private async generateSasUrl(
|
private async generateSasUrl(
|
||||||
contentType: 'image/png' | 'image/jpeg' | 'image/webp'
|
contentType: 'image/png' | 'image/jpeg' | 'image/webp'
|
||||||
): Promise<SasResponse> {
|
): Promise<SasResponse> {
|
||||||
const response = await this.api.post<SasResponse>('/api/feedback/sas', {
|
const response = await this.api.fetch<SasResponse>('/api/feedback/sas', {
|
||||||
contentType,
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ contentType }),
|
||||||
});
|
});
|
||||||
return response;
|
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 type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { requireAuth, requireAdmin } from '@bytelyst/auth';
|
import { UnauthorizedError, ForbiddenError } from '../../lib/errors.js';
|
||||||
import { extractFeaturesFromTelemetry } from './feature-extractor.js';
|
import { extractFeaturesFromTelemetry } from './feature-extractor.js';
|
||||||
import { featureStore } from './feature-store.js';
|
import { featureStore } from './feature-store.js';
|
||||||
import { churnModel } from './churn-model.js';
|
import { churnModel } from './churn-model.js';
|
||||||
@ -21,8 +21,25 @@ import {
|
|||||||
CreateCampaignSchema,
|
CreateCampaignSchema,
|
||||||
CampaignTriggerSchema,
|
CampaignTriggerSchema,
|
||||||
RiskSegmentEnum,
|
RiskSegmentEnum,
|
||||||
|
type UserChurnPredictionDoc,
|
||||||
} from './types.js';
|
} 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
|
// Get telemetry repository for fetching user events
|
||||||
async function getUserTelemetryEvents(
|
async function getUserTelemetryEvents(
|
||||||
userId: string,
|
userId: string,
|
||||||
@ -84,7 +101,8 @@ export async function predictiveAnalyticsRoutes(fastify: FastifyInstance): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch telemetry events
|
// 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
|
// Extract features
|
||||||
const features = extractFeaturesFromTelemetry(
|
const features = extractFeaturesFromTelemetry(
|
||||||
@ -100,15 +118,16 @@ export async function predictiveAnalyticsRoutes(fastify: FastifyInstance): Promi
|
|||||||
// Run prediction
|
// Run prediction
|
||||||
const prediction = churnModel.predict(features, parseInt(horizon, 10));
|
const prediction = churnModel.predict(features, parseInt(horizon, 10));
|
||||||
|
|
||||||
// Save prediction
|
// Save prediction (construct doc carefully to avoid duplicate property issues)
|
||||||
const doc = {
|
const { userId: _uid, productId: _pid, ...predictionData } = prediction;
|
||||||
|
const doc: UserChurnPredictionDoc = {
|
||||||
id: `cp_${crypto.randomUUID()}`,
|
id: `cp_${crypto.randomUUID()}`,
|
||||||
pk: `${userId}:${productId}`,
|
pk: `${userId}:${productId}`,
|
||||||
userId,
|
userId,
|
||||||
productId,
|
productId,
|
||||||
...prediction,
|
...predictionData,
|
||||||
actualChurned: undefined,
|
explanation: prediction.explanation,
|
||||||
validationDate: undefined,
|
interventionHistory: [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
ttl: (parseInt(horizon, 10) + 90) * 24 * 60 * 60,
|
ttl: (parseInt(horizon, 10) + 90) * 24 * 60 * 60,
|
||||||
};
|
};
|
||||||
@ -138,11 +157,12 @@ export async function predictiveAnalyticsRoutes(fastify: FastifyInstance): Promi
|
|||||||
|
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
userIds.map(async (userId) => {
|
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(
|
const features = extractFeaturesFromTelemetry(
|
||||||
userId,
|
userId,
|
||||||
productId,
|
productId,
|
||||||
events as Parameters<typeof extractFeaturesFromTelemetry>[2],
|
events,
|
||||||
new Date()
|
new Date()
|
||||||
);
|
);
|
||||||
const prediction = churnModel.predict(features, parseInt(horizon, 10));
|
const prediction = churnModel.predict(features, parseInt(horizon, 10));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user