Skip to content
This repository was archived by the owner on Sep 29, 2025. It is now read-only.

Commit 0433bd4

Browse files
authored
Segment fixes (#690)
1 parent 98e29fe commit 0433bd4

File tree

8 files changed

+341
-97
lines changed

8 files changed

+341
-97
lines changed

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mongodb-chatbot-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
"@types/react": "^18.0.37",
126126
"@types/react-dom": "^18.0.11",
127127
"@types/react-transition-group": "^4.4.6",
128+
"@types/segment-analytics": "^0.0.38",
128129
"@types/unist": "^3.0.2",
129130
"@typescript-eslint/eslint-plugin": "^5.59.0",
130131
"@typescript-eslint/parser": "^5.59.0",

packages/mongodb-chatbot-ui/src/App.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ function App() {
6969
serverBaseUrl={serverBaseUrl}
7070
shouldStream={shouldStream}
7171
darkMode={darkMode}
72-
fetchOptions={{ credentials: "include" }}
72+
fetchOptions={() => ({
73+
credentials: "include",
74+
headers: getSegmentIdHeaders(),
75+
})}
7376
onOpen={() => {
7477
console.log("Docs Chatbot opened");
7578
}}
@@ -85,10 +88,10 @@ function App() {
8588
serverBaseUrl={serverBaseUrl}
8689
shouldStream={shouldStream}
8790
darkMode={darkMode}
88-
fetchOptions={{
91+
fetchOptions={() => ({
8992
credentials: "include",
9093
headers: getSegmentIdHeaders(),
91-
}}
94+
})}
9295
getClientContext={() => ({
9396
user: "test-user-pls-ignore",
9497
})}

packages/mongodb-chatbot-ui/src/segment.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
function stripLiteralQuotes(str?: string) {
2+
return str?.replace(/^"|"$/g, "");
3+
}
4+
15
/**
26
* Get Segment IDs from the browser's local storage.
37
*/
48
export function getSegmentIds() {
5-
const userId = localStorage.getItem("ajs_user_id") ?? undefined;
6-
const anonymousId = localStorage.getItem("ajs_anonymous_id") ?? undefined;
9+
const analytics =
10+
typeof window !== "undefined" && window.analytics ? window.analytics : null;
11+
12+
const userId = stripLiteralQuotes(
13+
analytics?.user().id() ?? localStorage.getItem("ajs_user_id") ?? undefined
14+
);
15+
const anonymousId = stripLiteralQuotes(
16+
analytics?.user().anonymousId() ??
17+
localStorage.getItem("ajs_anonymous_id") ??
18+
undefined
19+
);
720
return {
821
userId,
922
anonymousId,

packages/mongodb-chatbot-ui/src/services/conversations.test.ts

Lines changed: 166 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -648,76 +648,6 @@ describe("ConversationService", () => {
648648
expect(commentPromise).resolves.toBeUndefined();
649649
});
650650

651-
it("appends custom fetch options", async () => {
652-
const conversationService = new ConversationService({
653-
serverUrl,
654-
fetchOptions: {
655-
headers: new Headers({
656-
foo: "bar",
657-
}),
658-
credentials: "include",
659-
},
660-
});
661-
let getOptions = mockFetchResponse({
662-
data: {
663-
_id: "650b4b260f975ef031016c8d",
664-
messages: [],
665-
createdAt: new Date().getTime(),
666-
},
667-
});
668-
await conversationService.createConversation();
669-
const createOptions = getOptions();
670-
671-
expect(createOptions.headers?.get("foo")).toBe("bar");
672-
expect(createOptions.headers?.get("content-type")).toBe("application/json");
673-
expect(createOptions.credentials).toBe("include");
674-
675-
await conversationService.addMessage({ conversationId: "", message: "" });
676-
const addOptions = getOptions();
677-
expect(addOptions.headers?.get("foo")).toBe("bar");
678-
expect(addOptions.headers?.get("content-type")).toBe("application/json");
679-
expect(addOptions.credentials).toBe("include");
680-
681-
await conversationService.addMessageStreaming({
682-
conversationId: "",
683-
message: "",
684-
onResponseDelta: vi.fn(),
685-
onResponseFinished: vi.fn(),
686-
onReferences: vi.fn(),
687-
onMetadata: vi.fn(),
688-
});
689-
const addStreamingOptions = getOptions();
690-
expect(addStreamingOptions.headers?.get("foo")).toBe("bar");
691-
expect(addStreamingOptions.headers?.get("content-type")).toBe(
692-
"application/json"
693-
);
694-
expect(addStreamingOptions.credentials).toBe("include");
695-
696-
// Updating the mock fetch to return 204, which is used by the following two calls.
697-
getOptions = mockFetchResponse({ data: {}, status: 204 });
698-
await conversationService.rateMessage({
699-
conversationId: "a",
700-
messageId: "b",
701-
rating: true,
702-
});
703-
const ratingOptions = getOptions();
704-
expect(ratingOptions.headers?.get("foo")).toBe("bar");
705-
expect(ratingOptions.headers?.get("content-type")).toBe("application/json");
706-
expect(ratingOptions.credentials).toBe("include");
707-
708-
await conversationService.commentMessage({
709-
conversationId: "",
710-
comment: "",
711-
messageId: "",
712-
});
713-
const commentOptions = getOptions();
714-
expect(commentOptions.headers?.get("foo")).toBe("bar");
715-
expect(commentOptions.headers?.get("content-type")).toBe(
716-
"application/json"
717-
);
718-
expect(commentOptions.credentials).toBe("include");
719-
});
720-
721651
it("throws on invalid inputs", async () => {
722652
const conversationId = "650b4b260f975ef031016c8d";
723653
const messageId = "650b4be0d5a57dd66be2ccb9";
@@ -789,6 +719,172 @@ describe("ConversationService", () => {
789719
});
790720
}).rejects.toThrow(internalServerErrorMessage);
791721
});
722+
723+
describe("fetchOptions", () => {
724+
it("supports static fetchOptions", async () => {
725+
const conversationService = new ConversationService({
726+
serverUrl,
727+
fetchOptions: {
728+
headers: new Headers({
729+
foo: "bar",
730+
}),
731+
credentials: "include",
732+
},
733+
});
734+
let getOptions = mockFetchResponse({
735+
data: {
736+
_id: "650b4b260f975ef031016c8d",
737+
messages: [],
738+
createdAt: new Date().getTime(),
739+
},
740+
});
741+
await conversationService.createConversation();
742+
const createOptions = getOptions();
743+
const createOptionsHeaders = new Headers(createOptions.headers);
744+
745+
expect(createOptionsHeaders.get("foo")).toBe("bar");
746+
expect(createOptionsHeaders.get("content-type")).toBe("application/json");
747+
expect(createOptions.credentials).toBe("include");
748+
749+
await conversationService.addMessage({ conversationId: "", message: "" });
750+
const addOptions = getOptions();
751+
const addOptionsHeaders = new Headers(addOptions.headers);
752+
expect(addOptionsHeaders.get("foo")).toBe("bar");
753+
expect(addOptionsHeaders.get("content-type")).toBe("application/json");
754+
expect(addOptions.credentials).toBe("include");
755+
756+
await conversationService.addMessageStreaming({
757+
conversationId: "",
758+
message: "",
759+
onResponseDelta: vi.fn(),
760+
onResponseFinished: vi.fn(),
761+
onReferences: vi.fn(),
762+
onMetadata: vi.fn(),
763+
});
764+
const addStreamingOptions = getOptions();
765+
const addStreamingOptionsHeaders = new Headers(
766+
addStreamingOptions.headers
767+
);
768+
expect(addStreamingOptionsHeaders.get("foo")).toBe("bar");
769+
expect(addStreamingOptionsHeaders.get("content-type")).toBe(
770+
"application/json"
771+
);
772+
expect(addStreamingOptions.credentials).toBe("include");
773+
774+
// Updating the mock fetch to return 204, which is used by the following two calls.
775+
getOptions = mockFetchResponse({ data: {}, status: 204 });
776+
await conversationService.rateMessage({
777+
conversationId: "a",
778+
messageId: "b",
779+
rating: true,
780+
});
781+
const ratingOptions = getOptions();
782+
const ratingOptionsHeaders = new Headers(ratingOptions.headers);
783+
expect(ratingOptionsHeaders.get("foo")).toBe("bar");
784+
expect(ratingOptionsHeaders.get("content-type")).toBe("application/json");
785+
expect(ratingOptions.credentials).toBe("include");
786+
787+
await conversationService.commentMessage({
788+
conversationId: "",
789+
comment: "",
790+
messageId: "",
791+
});
792+
const commentOptions = getOptions();
793+
const commentOptionsHeaders = new Headers(commentOptions.headers);
794+
expect(commentOptionsHeaders.get("foo")).toBe("bar");
795+
expect(commentOptionsHeaders.get("content-type")).toBe(
796+
"application/json"
797+
);
798+
expect(commentOptions.credentials).toBe("include");
799+
});
800+
801+
it("generates new headers on every request with dynamic fetchOptions", async () => {
802+
let callCount = 0;
803+
const conversationService = new ConversationService({
804+
serverUrl,
805+
fetchOptions: () => ({
806+
headers: new Headers({
807+
foo: `dynamic-${++callCount}`,
808+
}),
809+
credentials: "include",
810+
}),
811+
});
812+
let getOptions = mockFetchResponse({
813+
data: {
814+
_id: "650b4b260f975ef031016c8d",
815+
messages: [],
816+
createdAt: new Date().getTime(),
817+
},
818+
});
819+
await conversationService.createConversation();
820+
const createOptions = getOptions();
821+
const createOptionsHeaders = new Headers(createOptions.headers);
822+
823+
expect(createOptionsHeaders.get("foo")).toBe("dynamic-1");
824+
expect(createOptionsHeaders.get("content-type")).toBe("application/json");
825+
expect(createOptions.credentials).toBe("include");
826+
827+
await conversationService.addMessage({ conversationId: "", message: "" });
828+
const addOptions = getOptions();
829+
const addOptionsHeaders = new Headers(addOptions.headers);
830+
expect(addOptionsHeaders.get("foo")).toBe("dynamic-2");
831+
expect(addOptionsHeaders.get("content-type")).toBe("application/json");
832+
expect(addOptions.credentials).toBe("include");
833+
834+
// For addMessageStreaming, we need to mock fetchEventSource to capture headers
835+
let streamingOptions: any;
836+
(FetchEventSource.fetchEventSource as jest.Mock).mockImplementation(
837+
async (_url, options) => {
838+
streamingOptions = options;
839+
return {
840+
ok: true,
841+
status: 200,
842+
headers: new Headers({ "content-type": "text/event-stream" }),
843+
};
844+
}
845+
);
846+
847+
await conversationService.addMessageStreaming({
848+
conversationId: "",
849+
message: "",
850+
onResponseDelta: vi.fn(),
851+
onResponseFinished: vi.fn(),
852+
onReferences: vi.fn(),
853+
onMetadata: vi.fn(),
854+
});
855+
856+
// Check the headers from fetchEventSource
857+
expect(streamingOptions.headers.foo).toBe("dynamic-3");
858+
expect(streamingOptions.headers["content-type"]).toBe("application/json");
859+
expect(streamingOptions.credentials).toBe("include");
860+
861+
// Updating the mock fetch to return 204, which is used by the following two calls.
862+
getOptions = mockFetchResponse({ data: {}, status: 204 });
863+
await conversationService.rateMessage({
864+
conversationId: "a",
865+
messageId: "b",
866+
rating: true,
867+
});
868+
const ratingOptions = getOptions();
869+
const ratingOptionsHeaders = new Headers(ratingOptions.headers);
870+
expect(ratingOptionsHeaders.get("foo")).toBe("dynamic-4");
871+
expect(ratingOptionsHeaders.get("content-type")).toBe("application/json");
872+
expect(ratingOptions.credentials).toBe("include");
873+
874+
await conversationService.commentMessage({
875+
conversationId: "",
876+
comment: "",
877+
messageId: "",
878+
});
879+
const commentOptions = getOptions();
880+
const commentOptionsHeaders = new Headers(commentOptions.headers);
881+
expect(commentOptionsHeaders.get("foo")).toBe("dynamic-5");
882+
expect(commentOptionsHeaders.get("content-type")).toBe(
883+
"application/json"
884+
);
885+
expect(commentOptions.credentials).toBe("include");
886+
});
887+
});
792888
});
793889

794890
describe("getCustomRequestOrigin", () => {

0 commit comments

Comments
 (0)