Skip to main content
Glama

Bucket Feature Flags MCP Server

Official
by reflagcom
usage.test.ts17.3 kB
import { http, HttpResponse } from "msw"; import { afterEach, beforeAll, beforeEach, describe, expect, it, test, vi, } from "vitest"; import { ReflagClient } from "../src"; import { API_BASE_URL } from "../src/config"; import { FeedbackPromptHandler } from "../src/feedback/feedback"; import { checkPromptMessageCompleted, getAuthToken, markPromptMessageCompleted, } from "../src/feedback/promptStorage"; import { FlagsClient } from "../src/flag/flags"; import { HttpClient } from "../src/httpClient"; import { AblySSEChannel, closeAblySSEChannel, openAblySSEChannel, } from "../src/sse"; import { flagsResult } from "./mocks/handlers"; import { server } from "./mocks/server"; const KEY = "123"; vi.mock("../src/sse"); vi.mock("../src/feedback/promptStorage", () => { return { markPromptMessageCompleted: vi.fn(), checkPromptMessageCompleted: vi.fn(), rememberAuthToken: vi.fn(), getAuthToken: vi.fn(), }; }); // Treat test environment as desktop window.innerWidth = 1024; afterEach(() => { server.resetHandlers(); }); describe("usage", () => { afterEach(() => { vi.clearAllMocks(); }); test("golden path - register `user`, `company`, send `event`, send `feedback`, get `flags`", async () => { const reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo " }, company: { id: "bar", name: "bar corp" }, }); await reflagInstance.initialize(); await reflagInstance.track("baz", { baz: true }); await reflagInstance.feedback({ flagKey: "huddles", score: 5, comment: "Sunt bine!", question: "Cum esti?", promptedQuestion: "How are you?", }); const flags = reflagInstance.getFlags(); expect(flags).toEqual(flagsResult); const flag = reflagInstance.getFlag("flag-1"); expect(flag).toStrictEqual({ isEnabled: false, track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, isEnabledOverride: null, setIsEnabledOverride: expect.any(Function), }); }); test("accepts `flagKey` instead of `featureId` for manual feedback", async () => { const reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo" }, company: { id: "bar" }, }); await reflagInstance.initialize(); await reflagInstance.feedback({ flagKey: "flag-key", score: 5, question: "What's up?", promptedQuestion: "How are you?", }); }); }); // TODO: // Since we now have AutoFeedback as it's own class, we should rewrite these tests // to test that class instead of the ReflagClient class. // Same for feedback state management below describe("feedback prompting", () => { const closeChannel = vi.fn(); beforeAll(() => { vi.mocked(openAblySSEChannel).mockReturnValue({ close: closeChannel, } as unknown as AblySSEChannel); vi.mocked(closeAblySSEChannel).mockResolvedValue(undefined); }); afterEach(() => { vi.clearAllMocks(); vi.mocked(getAuthToken).mockReturnValue(undefined); }); test("initiates and stops feedback prompting", async () => { const reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo" }, }); await reflagInstance.initialize(); expect(openAblySSEChannel).toBeCalledTimes(1); // call twice, expect only one reset to go through await reflagInstance.stop(); await reflagInstance.stop(); expect(closeChannel).toBeCalledTimes(1); }); test("does not call tracking endpoints if token cached", async () => { const specialChannel = "special-channel"; vi.mocked(getAuthToken).mockReturnValue({ channel: specialChannel, token: "something", }); server.use( http.post(`${API_BASE_URL}/feedback/prompting-init`, () => { throw new Error("should not be called"); }), ); const reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo" }, }); await reflagInstance.initialize(); expect(openAblySSEChannel).toBeCalledTimes(1); const args = vi.mocked(openAblySSEChannel).mock.calls[0][0]; expect(args.channel).toBe(specialChannel); expect(args.userId).toBe("foo"); }); test("does not initiate feedback prompting if server does not agree", async () => { server.use( http.post(`${API_BASE_URL}/feedback/prompting-init`, () => { return HttpResponse.json({ success: false }); }), ); const reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo" }, }); await reflagInstance.initialize(); expect(openAblySSEChannel).toBeCalledTimes(0); }); test("skip feedback prompting if no user id configured", async () => { const reflagInstance = new ReflagClient({ publishableKey: KEY }); await reflagInstance.initialize(); expect(openAblySSEChannel).toBeCalledTimes(0); }); test("skip feedback prompting if automated feedback surveys are disabled", async () => { const reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo" }, feedback: { enableAutoFeedback: false }, }); await reflagInstance.initialize(); expect(openAblySSEChannel).toBeCalledTimes(0); }); }); describe("feedback state management", () => { const message = { question: "How are you?", showAfter: new Date(Date.now() - 10000).valueOf(), showBefore: new Date(Date.now() + 10000).valueOf(), promptId: "123", featureId: "456", }; let events: string[] = []; let reflagInstance: ReflagClient | null = null; beforeEach(() => { vi.mocked(openAblySSEChannel).mockImplementation(({ callback }) => { callback(message); // eslint-disable-next-line @typescript-eslint/no-empty-function return { close: () => {} } as AblySSEChannel; }); events = []; server.use( http.post( `${API_BASE_URL}/feedback/prompt-events`, async ({ request }) => { const body = await request.json(); if (!(body && typeof body === "object" && "action" in body)) { throw new Error("invalid request"); } events.push(String(body["action"])); return HttpResponse.json({ success: true }); }, ), ); }); afterEach(async () => { if (reflagInstance) await reflagInstance.stop(); vi.resetAllMocks(); }); const createReflagInstance = async (callback: FeedbackPromptHandler) => { reflagInstance = new ReflagClient({ publishableKey: KEY, user: { id: "foo" }, feedback: { autoFeedbackHandler: callback, }, }); await reflagInstance.initialize(); return reflagInstance; }; test("ignores prompt if expired", async () => { vi.useFakeTimers(); vi.setSystemTime(message.showAfter - 10000); const callback = vi.fn(); await createReflagInstance(callback); expect(callback).not.toHaveBeenCalled(); expect(markPromptMessageCompleted).not.toHaveBeenCalledOnce(); vi.clearAllTimers(); vi.useRealTimers(); }); test("ignores prompt if already seen", async () => { vi.mocked(checkPromptMessageCompleted).mockReturnValue(true); expect(checkPromptMessageCompleted).not.toHaveBeenCalled(); const callback = vi.fn(); await createReflagInstance(callback); expect(callback).not.toHaveBeenCalled(); await vi.waitFor(() => expect(checkPromptMessageCompleted).toHaveBeenCalledOnce(), ); expect(checkPromptMessageCompleted).toHaveBeenCalledWith("foo", "123"); }); test("propagates prompt to the callback", async () => { const callback = vi.fn(); await createReflagInstance(callback); await vi.waitUntil(() => callback.mock.calls.length > 0); await vi.waitUntil(() => events.length > 1); expect(events).toEqual(["received", "shown"]); expect(callback).toBeCalledTimes(1); expect(callback).toBeCalledWith( { question: "How are you?", showAfter: new Date(message.showAfter), showBefore: new Date(message.showBefore), promptId: "123", featureId: "456", }, expect.anything(), ); expect(markPromptMessageCompleted).not.toHaveBeenCalled(); }); test("propagates timed prompt to the callback", async () => { const callback = vi.fn(); vi.useFakeTimers(); vi.setSystemTime(message.showAfter - 500); await createReflagInstance(callback); expect(callback).not.toBeCalled(); vi.runAllTimers(); await vi.waitUntil(() => callback.mock.calls.length > 0); await vi.waitUntil(() => events.length > 1); expect(events).toEqual(["received", "shown"]); expect(callback).toBeCalledTimes(1); expect(markPromptMessageCompleted).not.toHaveBeenCalled(); vi.clearAllTimers(); vi.useRealTimers(); }); test("propagates prompt to the callback and reacts to dismissal", async () => { const callback: FeedbackPromptHandler = async (_, handlers) => { await handlers.reply(null); }; await createReflagInstance(callback); await vi.waitUntil(() => events.length > 2); expect(events).toEqual(["received", "shown", "dismissed"]); expect(markPromptMessageCompleted).toHaveBeenCalledOnce(); expect(markPromptMessageCompleted).toHaveBeenCalledWith( "foo", "123", new Date(message.showBefore), ); }); test("propagates prompt to the callback and reacts to feedback", async () => { const callback: FeedbackPromptHandler = async (_, handlers) => { await handlers.reply({ companyId: "bar", score: 5, comment: "hello", question: "Cum esti?", }); }; await createReflagInstance(callback); await vi.waitUntil(() => events.length > 1); expect(events).toEqual(["received", "shown"]); expect(markPromptMessageCompleted).toHaveBeenCalledOnce(); expect(markPromptMessageCompleted).toHaveBeenCalledWith( "foo", "123", new Date(message.showBefore), ); }); }); describe(`sends "check" events `, () => { test("getFlags() does not send `check` events", async () => { vi.spyOn(FlagsClient.prototype, "sendCheckEvent"); const client = new ReflagClient({ publishableKey: KEY, user: { id: "123" }, }); await client.initialize(); expect( vi.mocked(FlagsClient.prototype.sendCheckEvent), ).toHaveBeenCalledTimes(0); const flagA = client.getFlags()?.flagA; expect(flagA?.isEnabled).toBe(true); expect( vi.mocked(FlagsClient.prototype.sendCheckEvent), ).toHaveBeenCalledTimes(0); }); describe("getFlag", async () => { afterEach(() => { vi.clearAllMocks(); }); it(`returns get the expected flag details`, async () => { const client = new ReflagClient({ publishableKey: KEY, user: { id: "uid" }, company: { id: "cid" }, }); await client.initialize(); expect(client.getFlag("flagA")).toStrictEqual({ isEnabled: true, config: { key: undefined, payload: undefined }, track: expect.any(Function), requestFeedback: expect.any(Function), isEnabledOverride: null, setIsEnabledOverride: expect.any(Function), }); expect(client.getFlag("flagB")).toStrictEqual({ isEnabled: true, config: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5, }, }, track: expect.any(Function), requestFeedback: expect.any(Function), isEnabledOverride: null, setIsEnabledOverride: expect.any(Function), }); expect(client.getFlag("flagC")).toStrictEqual({ isEnabled: false, config: { key: undefined, payload: undefined }, track: expect.any(Function), requestFeedback: expect.any(Function), isEnabledOverride: null, setIsEnabledOverride: expect.any(Function), }); }); it(`does not send check events when offline`, async () => { const postSpy = vi.spyOn(HttpClient.prototype, "post"); const client = new ReflagClient({ publishableKey: KEY, user: { id: "uid" }, company: { id: "cid" }, offline: true, }); await client.initialize(); const flagA = client.getFlag("flagA"); expect(flagA.isEnabled).toBe(false); expect(postSpy).not.toHaveBeenCalled(); }); it(`sends check event when accessing "isEnabled"`, async () => { const sendCheckEventSpy = vi.spyOn( FlagsClient.prototype, "sendCheckEvent", ); const postSpy = vi.spyOn(HttpClient.prototype, "post"); const client = new ReflagClient({ publishableKey: KEY, user: { id: "uid" }, company: { id: "cid" }, }); await client.initialize(); const flagA = client.getFlag("flagA"); expect(sendCheckEventSpy).toHaveBeenCalledTimes(0); expect(flagA.isEnabled).toBe(true); expect(sendCheckEventSpy).toHaveBeenCalledTimes(1); expect(sendCheckEventSpy).toHaveBeenCalledWith( { action: "check-is-enabled", key: "flagA", value: true, version: 1, missingContextFields: ["field1", "field2"], ruleEvaluationResults: [false, true], }, expect.any(Function), ); expect(postSpy).toHaveBeenCalledWith({ body: { action: "check-is-enabled", evalContext: { company: { id: "cid", }, other: {}, user: { id: "uid", }, }, evalResult: true, evalRuleResults: [false, true], evalMissingFields: ["field1", "field2"], key: "flagA", targetingVersion: 1, }, path: "features/events", }); }); it(`sends check event when accessing "config"`, async () => { const postSpy = vi.spyOn(HttpClient.prototype, "post"); const client = new ReflagClient({ publishableKey: KEY, user: { id: "uid" }, }); await client.initialize(); const flagB = client.getFlag("flagB"); expect(flagB.config).toMatchObject({ key: "gpt3", }); expect(postSpy).toHaveBeenCalledWith({ body: { action: "check-config", evalContext: { company: undefined, other: {}, user: { id: "uid", }, }, evalResult: { key: "gpt3", payload: { model: "gpt-something", temperature: 0.5 }, }, evalRuleResults: [true, false, false], evalMissingFields: ["field3"], key: "flagB", targetingVersion: 12, }, path: "features/events", }); }); it("sends check event for not-enabled flags", async () => { // disabled flags don't appear in the API response vi.spyOn(FlagsClient.prototype, "sendCheckEvent"); const client = new ReflagClient({ publishableKey: KEY }); await client.initialize(); const nonExistentFlag = client.getFlag("non-existent"); expect( vi.mocked(FlagsClient.prototype.sendCheckEvent), ).toHaveBeenCalledTimes(0); expect(nonExistentFlag.isEnabled).toBe(false); expect( vi.mocked(FlagsClient.prototype.sendCheckEvent), ).toHaveBeenCalledTimes(1); expect( vi.mocked(FlagsClient.prototype.sendCheckEvent), ).toHaveBeenCalledWith( { action: "check-is-enabled", value: false, key: "non-existent", version: undefined, }, expect.any(Function), ); }); it("calls client.track with the flagKey", async () => { const client = new ReflagClient({ publishableKey: KEY }); await client.initialize(); const flag = client.getFlag("flag-1"); expect(flag).toStrictEqual({ isEnabled: false, track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, isEnabledOverride: null, setIsEnabledOverride: expect.any(Function), }); vi.spyOn(client, "track"); await flag.track(); expect(client.track).toHaveBeenCalledWith("flag-1"); }); it("calls client.requestFeedback with the flagKey", async () => { const client = new ReflagClient({ publishableKey: KEY }); await client.initialize(); const flag = client.getFlag("flag-1"); expect(flag).toStrictEqual({ isEnabled: false, track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, isEnabledOverride: null, setIsEnabledOverride: expect.any(Function), }); vi.spyOn(client, "requestFeedback"); flag.requestFeedback({ title: "Feedback", }); expect(client.requestFeedback).toHaveBeenCalledWith({ flagKey: "flag-1", title: "Feedback", }); }); }); });

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