feat: add in-product survey system (#2008)

* feat: add in-product survey system

- SurveyManager: event-based trigger, Space API communication
- Trigger on first successful non-WebSocket response
- Backend API: /api/v1/survey/{pending,respond,dismiss}
- Frontend: floating survey widget with progressive questions
- Flat radio/checkbox style (not dropdown Select)

* fix: persist triggered survey events to disk across restarts

Store triggered events in data/survey_triggered_events.json so that
restarting the process doesn't re-query Space for already-triggered events.

* fix: use metadata table for survey event persistence instead of file

Store triggered events in the existing metadata KV table
(key='survey_triggered_events') instead of a standalone JSON file.

* fix: ruff format and prettier fixes
This commit is contained in:
Junyan Chin
2026-02-26 13:50:14 +08:00
committed by GitHub
parent 2ded774747
commit 2d837c9cb4
9 changed files with 650 additions and 0 deletions

View File

@@ -1007,4 +1007,50 @@ export class BackendClient extends BaseHttpClient {
return this.get(`/api/v1/monitoring/overview?${queryParams.toString()}`);
}
// ============ Survey API ============
public getSurveyPending(): Promise<{
survey: {
survey_id: string;
version: number;
title: Record<string, string>;
description: Record<string, string>;
questions: SurveyQuestion[];
} | null;
}> {
return this.get('/api/v1/survey/pending');
}
public submitSurveyResponse(
surveyId: string,
answers: Record<string, unknown>,
completed: boolean = true,
): Promise<object> {
return this.post('/api/v1/survey/respond', {
survey_id: surveyId,
answers,
completed,
});
}
public dismissSurvey(surveyId: string): Promise<object> {
return this.post('/api/v1/survey/dismiss', { survey_id: surveyId });
}
}
export interface SurveyQuestion {
id: string;
type: 'single_select' | 'multi_select' | 'text';
title: Record<string, string>;
subtitle?: Record<string, string>;
required: boolean;
options?: SurveyOption[];
placeholder?: Record<string, string>;
max_length?: number;
}
export interface SurveyOption {
id: string;
label: Record<string, string>;
has_input?: boolean;
}