fix(feedback-client): correct ApiClient method usage and types

This commit is contained in:
saravanakumardb1 2026-03-03 12:20:43 -08:00
parent 05ad9dbedc
commit 914e344a92
4 changed files with 212 additions and 24 deletions

View 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;
}

View File

@ -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;
}

View File

@ -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));