Skip to main content
Glama
reflagcom

Bucket Feature Flags MCP Server

Official
by reflagcom
feedback.ts11.9 kB
import { HttpClient } from "../httpClient"; import { Logger } from "../logger"; import { AblySSEChannel, openAblySSEChannel } from "../sse"; import { Position } from "../ui/types"; import { FeedbackSubmission, FeedbackTranslations, OpenFeedbackFormOptions, } from "./ui/types"; import { FeedbackPromptCompletionHandler, parsePromptMessage, processPromptMessage, } from "./prompts"; import { getAuthToken } from "./promptStorage"; import * as feedbackLib from "./ui"; import { DEFAULT_POSITION } from "./ui"; export type Key = string; export type FeedbackOptions = { /** * Enables automatic feedback prompting if it's set up in Reflag */ enableAutoFeedback?: boolean; /** * */ autoFeedbackHandler?: FeedbackPromptHandler; /** * With these options you can override the look of the feedback prompt */ ui?: { /** * Control the placement and behavior of the feedback form. */ position?: Position; /** * Add your own custom translations for the feedback form. * Undefined translation keys fall back to english defaults. */ translations?: Partial<FeedbackTranslations>; }; }; export type RequestFeedbackData = Omit< OpenFeedbackFormOptions, "key" | "onSubmit" > & { /** * Company ID from your own application. */ companyId?: string; /** * Allows you to handle a copy of the already submitted * feedback. * * This can be used for side effects, such as storing a * copy of the feedback in your own application or CRM. * * @param {Object} data */ onAfterSubmit?: (data: FeedbackSubmission) => void; /** * Flag key. */ flagKey: string; }; export type RequestFeedbackOptions = RequestFeedbackData & { /** * User ID from your own application. */ userId: string; }; export type UnassignedFeedback = { /** * Flag key. */ flagKey: string; /** * Reflag feedback ID */ feedbackId?: string; /** * The question that was presented to the user. */ question?: string; /** * The original question. * This only needs to be populated if the feedback was submitted through the automated feedback surveys channel. */ promptedQuestion?: string; /** * Customer satisfaction score. */ score?: number; /** * User supplied comment about your flag. */ comment?: string; /** * Reflag feedback prompt ID. * * This only exists if the feedback was submitted * as part of an automated prompt from Reflag. * * Used for internal state management of automated * feedback. */ promptId?: string; /** * Source of the feedback, depending on how the user was asked * - `prompt` - Feedback submitted by way of an automated feedback survey (prompted) * - `widget` - Feedback submitted via `requestFeedback` * - `sdk` - Feedback submitted via `feedback` */ source?: "prompt" | "sdk" | "widget"; }; export type Feedback = UnassignedFeedback & { /** * User ID from your own application. */ userId?: string; /** * Company ID from your own application. */ companyId?: string; }; export type FeedbackPrompt = { /** * Specific question user was asked */ question: string; /** * Feedback prompt should appear only after this time */ showAfter: Date; /** * Feedback prompt will not be shown after this time */ showBefore: Date; /** * Id of the prompt */ promptId: string; /** * Feature ID from Reflag */ featureId: string; }; export type FeedbackPromptReply = { question: string; companyId?: string; score?: number; comment?: string; }; export type FeedbackPromptReplyHandler = <T extends FeedbackPromptReply | null>( reply: T, ) => T extends null ? Promise<void> : Promise<{ feedbackId: string }>; export type FeedbackPromptHandlerOpenFeedbackFormOptions = Omit< RequestFeedbackOptions, "featureId" | "flagKey" | "userId" | "companyId" | "onClose" | "onDismiss" >; export type FeedbackPromptHandlerCallbacks = { reply: FeedbackPromptReplyHandler; openFeedbackForm: ( options: FeedbackPromptHandlerOpenFeedbackFormOptions, ) => void; }; export type FeedbackPromptHandler = ( prompt: FeedbackPrompt, handlers: FeedbackPromptHandlerCallbacks, ) => void; export const createDefaultFeedbackPromptHandler = ( options: FeedbackPromptHandlerOpenFeedbackFormOptions = {}, ): FeedbackPromptHandler => { return (_prompt: FeedbackPrompt, handlers) => { handlers.openFeedbackForm(options); }; }; export const DEFAULT_FEEDBACK_CONFIG = { promptHandler: createDefaultFeedbackPromptHandler(), feedbackPosition: DEFAULT_POSITION, translations: {}, autoFeedbackEnabled: true, }; // Payload can include featureId or flagKey, but the public API only exposes flagKey // We use featureId internally because prompting is based on featureId type FeedbackPayload = Omit<Feedback, "flagKey"> & { featureId?: string; flagKey?: string; }; export async function feedback( httpClient: HttpClient, logger: Logger, payload: FeedbackPayload, ) { if (!payload.score && !payload.comment) { logger.error( "`feedback` call ignored, either `score` or `comment` must be provided", ); return; } if (!payload.userId) { logger.error("`feedback` call ignored, no `userId` provided"); return; } const featureId = "featureId" in payload ? payload.featureId : undefined; const flagKey = "flagKey" in payload ? payload.flagKey : undefined; if (!featureId && !flagKey) { logger.error( "`feedback` call ignored. Neither `featureId` nor `flagKey` have been provided", ); return; } // set default source to sdk const feedbackPayload = { ...payload, flagKey: undefined, source: payload.source ?? "sdk", featureId, key: flagKey, }; const res = await httpClient.post({ path: `/feedback`, body: feedbackPayload, }); logger.debug(`sent feedback`, res); return res; } export class AutoFeedback { private initialized = false; private sseChannel: AblySSEChannel | null = null; constructor( private sseBaseUrl: string, private logger: Logger, private httpClient: HttpClient, private feedbackPromptHandler: FeedbackPromptHandler = createDefaultFeedbackPromptHandler(), private userId: string, private position: Position = DEFAULT_POSITION, private feedbackTranslations: Partial<FeedbackTranslations> = {}, ) {} /** * Start receiving automated feedback surveys. */ async initialize() { if (this.initialized) { this.logger.warn("automatic feedback client already initialized"); return; } this.initialized = true; const channel = await this.getChannel(); if (!channel) return; try { this.logger.debug(`automatic feedback enabled`, channel); this.sseChannel = openAblySSEChannel({ userId: this.userId, channel, httpClient: this.httpClient, callback: (message) => this.handleFeedbackPromptRequest(this.userId, message), logger: this.logger, sseBaseUrl: this.sseBaseUrl, }); this.logger.debug(`automatic feedback connection established`); } catch (e) { this.logger.error(`error initializing automatic feedback client`, e); } } stop() { if (this.sseChannel) { this.sseChannel.close(); this.sseChannel = null; } } async setUser(userId: string) { this.stop(); this.initialized = false; this.userId = userId; await this.initialize(); } handleFeedbackPromptRequest(userId: string, message: any) { const parsed = parsePromptMessage(message); if (!parsed) { this.logger.error(`invalid feedback prompt message received`, message); } else { if ( !processPromptMessage(userId, parsed, async (u, m, cb) => { await this.feedbackPromptEvent({ promptId: parsed.promptId, featureId: parsed.featureId, promptedQuestion: parsed.question, event: "received", userId, }); await this.triggerFeedbackPrompt(u, m, cb); }) ) { this.logger.info( `feedback prompt not shown, it was either expired or already processed`, message, ); } } } async triggerFeedbackPrompt( userId: string, message: FeedbackPrompt, completionHandler: FeedbackPromptCompletionHandler, ) { let feedbackId: string | undefined = undefined; await this.feedbackPromptEvent({ promptId: message.promptId, featureId: message.featureId, promptedQuestion: message.question, event: "shown", userId, }); const replyCallback: FeedbackPromptReplyHandler = async (reply) => { if (!reply) { await this.feedbackPromptEvent({ promptId: message.promptId, featureId: message.featureId, event: "dismissed", userId, promptedQuestion: message.question, }); completionHandler(); return; } const feedbackPayload = { feedbackId: feedbackId, featureId: message.featureId, userId, companyId: reply.companyId, score: reply.score, comment: reply.comment, promptId: message.promptId, question: reply.question, promptedQuestion: message.question, source: "prompt", } satisfies FeedbackPayload; const response = await feedback( this.httpClient, this.logger, feedbackPayload, ); completionHandler(); if (response && response.ok) { return await response?.json(); } return; }; const handlers: FeedbackPromptHandlerCallbacks = { reply: replyCallback, openFeedbackForm: (options) => { feedbackLib.openFeedbackForm({ key: message.featureId, title: message.question, onScoreSubmit: async (data) => { const res = await replyCallback(data); feedbackId = res.feedbackId; return { feedbackId: res.feedbackId }; }, onSubmit: async (data) => { await replyCallback(data); options.onAfterSubmit?.(data); }, onDismiss: () => replyCallback(null), position: this.position, translations: this.feedbackTranslations, ...options, }); }, }; this.feedbackPromptHandler(message, handlers); } async feedbackPromptEvent(args: { event: "received" | "shown" | "dismissed"; featureId: string; promptId: string; promptedQuestion: string; userId: string; }) { const payload = { action: args.event, featureId: args.featureId, promptId: args.promptId, userId: args.userId, promptedQuestion: args.promptedQuestion, }; const res = await this.httpClient.post({ path: `/feedback/prompt-events`, body: payload, }); this.logger.debug(`sent prompt event`, res); return res; } private async getChannel() { const existingAuth = getAuthToken(this.userId); const channel = existingAuth?.channel; if (channel) { return channel; } try { if (!channel) { const res = await this.httpClient.post({ path: `/feedback/prompting-init`, body: { userId: this.userId, }, }); this.logger.debug(`automatic feedback status sent`, res); if (res.ok) { const body: { success: boolean; channel?: string } = await res.json(); if (body.success && body.channel) { return body.channel; } } } } catch (e) { this.logger.error(`error initializing automatic feedback`, e); return; } return; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/reflagcom/bucket-javascript-sdk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server