From d1ff4f72288e7aaabb81b86b7c82946b33a33e8d Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 29 Jul 2025 13:11:10 -0400 Subject: [PATCH 1/9] WIP --- .../src/experimentation-provider.tsx | 83 +++++++++++++++++++ packages/compass-telemetry/src/index.ts | 2 + 2 files changed, 85 insertions(+) create mode 100644 packages/compass-telemetry/src/experimentation-provider.tsx diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx new file mode 100644 index 00000000000..9098874376e --- /dev/null +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -0,0 +1,83 @@ +import React, { createContext, useContext, useRef } from 'react'; + +interface ExperimentAssignmentData { + variant: string | null; + isInSample: boolean; +} + +interface ExperimentData { + assignmentDate: Date; + entityId: string; + entityType: string; + id: string; + tag: string; + testGroupDatabaseId: string; + testGroupId: string; + testId: string; + testName: string; +} + +interface SDKAssignment { + assignmentData: ExperimentAssignmentData; + experimentData: ExperimentData | null; +} + +interface UseAssignmentResponse { + assignment: SDKAssignment | null; + asyncStatus: 'LOADING' | 'SUCCESS' | 'ERROR'; + error?: Error; +} + +interface BasicAPICallingFunctionOptions { + timeoutMs?: number; + team?: string; +} + +type UseAssignmentHookFn = ( + experimentName: string, + trackIsInSample: boolean, + options?: BasicAPICallingFunctionOptions +) => UseAssignmentResponse; + +type AssignExperimentFn = ( + experimentName: string, + options?: BasicAPICallingFunctionOptions +) => Promise<'SUCCESS' | 'ERROR' | null>; + +interface ExperimentationProviderContextValue { + useAssignment: UseAssignmentHookFn; + assignExperiment: AssignExperimentFn; +} + +const ExperimentationContext = + createContext({ + useAssignment() { + return { + assignment: null, + asyncStatus: 'SUCCESS' as const, + }; + }, + assignExperiment() { + return Promise.resolve(null); + }, + }); + +// Provider component that accepts MMS experiment utils as props +export const ExperimentationProvider: React.FC<{ + children: React.ReactNode; + useAssignment: UseAssignmentHookFn; + assignExperiment: AssignExperimentFn; +}> = ({ children, useAssignment, assignExperiment }) => { + const contextValue = useRef({ useAssignment, assignExperiment }); + + return ( + + {children} + + ); +}; + +// Hook for components to access experiment assignment +export const useAssignment = (...args: Parameters) => { + return useContext(ExperimentationContext).useAssignment(...args); +}; diff --git a/packages/compass-telemetry/src/index.ts b/packages/compass-telemetry/src/index.ts index a2e155c8073..271a051f53d 100644 --- a/packages/compass-telemetry/src/index.ts +++ b/packages/compass-telemetry/src/index.ts @@ -5,3 +5,5 @@ export type { IdentifyTraits, ExtraConnectionData, } from './types'; + +export { ExperimentationProvider } from './experimentation-provider'; From 8a3704e88440c591f96ee10b3cee6b2023b373a4 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 29 Jul 2025 13:30:03 -0400 Subject: [PATCH 2/9] Copilot review comment --- .../compass-telemetry/src/experimentation-provider.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 9098874376e..0a05e0ebf7e 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useRef } from 'react'; +import React, { createContext, useContext, useMemo } from 'react'; interface ExperimentAssignmentData { variant: string | null; @@ -68,10 +68,13 @@ export const ExperimentationProvider: React.FC<{ useAssignment: UseAssignmentHookFn; assignExperiment: AssignExperimentFn; }> = ({ children, useAssignment, assignExperiment }) => { - const contextValue = useRef({ useAssignment, assignExperiment }); + const contextValue = useMemo( + () => ({ useAssignment, assignExperiment }), + [useAssignment, assignExperiment] + ); return ( - + {children} ); From 34155ed4fc39c1dcfc0725e95973fac0ba3345e0 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 29 Jul 2025 14:38:26 -0400 Subject: [PATCH 3/9] Rename provider to CompassExperimentationProvider --- packages/compass-telemetry/src/experimentation-provider.tsx | 6 +++--- packages/compass-telemetry/src/index.ts | 2 +- packages/compass-web/src/index.tsx | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 0a05e0ebf7e..044cbaacabd 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -44,13 +44,13 @@ type AssignExperimentFn = ( options?: BasicAPICallingFunctionOptions ) => Promise<'SUCCESS' | 'ERROR' | null>; -interface ExperimentationProviderContextValue { +interface CompassExperimentationProviderContextValue { useAssignment: UseAssignmentHookFn; assignExperiment: AssignExperimentFn; } const ExperimentationContext = - createContext({ + createContext({ useAssignment() { return { assignment: null, @@ -63,7 +63,7 @@ const ExperimentationContext = }); // Provider component that accepts MMS experiment utils as props -export const ExperimentationProvider: React.FC<{ +export const CompassExperimentationProvider: React.FC<{ children: React.ReactNode; useAssignment: UseAssignmentHookFn; assignExperiment: AssignExperimentFn; diff --git a/packages/compass-telemetry/src/index.ts b/packages/compass-telemetry/src/index.ts index 271a051f53d..e7cadf9d208 100644 --- a/packages/compass-telemetry/src/index.ts +++ b/packages/compass-telemetry/src/index.ts @@ -6,4 +6,4 @@ export type { ExtraConnectionData, } from './types'; -export { ExperimentationProvider } from './experimentation-provider'; +export { CompassExperimentationProvider } from './experimentation-provider'; diff --git a/packages/compass-web/src/index.tsx b/packages/compass-web/src/index.tsx index 2c6525f50c2..e9078380492 100644 --- a/packages/compass-web/src/index.tsx +++ b/packages/compass-web/src/index.tsx @@ -4,3 +4,5 @@ export type { OpenWorkspaceOptions, WorkspaceTab, } from '@mongodb-js/compass-workspaces'; + +export { CompassExperimentationProvider } from '@mongodb-js/compass-telemetry'; From 87680b751d1656ab0274eddee80fb2d768125387 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 30 Jul 2025 11:02:07 -0400 Subject: [PATCH 4/9] useMemo -> useRef --- .../src/experimentation-provider.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 044cbaacabd..3c8d05350ed 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useMemo } from 'react'; +import React, { createContext, useContext, useRef } from 'react'; interface ExperimentAssignmentData { variant: string | null; @@ -68,10 +68,11 @@ export const CompassExperimentationProvider: React.FC<{ useAssignment: UseAssignmentHookFn; assignExperiment: AssignExperimentFn; }> = ({ children, useAssignment, assignExperiment }) => { - const contextValue = useMemo( - () => ({ useAssignment, assignExperiment }), - [useAssignment, assignExperiment] - ); + // Use useRef to keep the functions up-to-date; maintain same object reference for context value + // to prevent unnecessary re-renders of consuming components, + const { current: contextValue } = useRef({ useAssignment, assignExperiment }); + contextValue.useAssignment = useAssignment; + contextValue.assignExperiment = assignExperiment; return ( From 19b0953b1a210df68c094ba698617c21ac21b985 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 30 Jul 2025 14:39:57 -0400 Subject: [PATCH 5/9] Import SDK --- packages/compass-telemetry/.eslintrc.js | 21 ++++++++ packages/compass-telemetry/package.json | 3 +- .../src/experimentation-provider.tsx | 51 +++++-------------- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/packages/compass-telemetry/.eslintrc.js b/packages/compass-telemetry/.eslintrc.js index e4cf824b6ac..0445f96e1a7 100644 --- a/packages/compass-telemetry/.eslintrc.js +++ b/packages/compass-telemetry/.eslintrc.js @@ -5,4 +5,25 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig-lint.json'], }, + overrides: [ + { + files: ['./src/**/*.ts', './src/**/*.tsx'], + rules: { + 'no-restricted-imports': 'off', + '@typescript-eslint/no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@mongodb-js/mdb-experiment-js', + message: + 'Use type-only imports from @mongodb-js/mdb-experiment-js', + allowTypeImports: true, + }, + ], + }, + ], + }, + }, + ], }; diff --git a/packages/compass-telemetry/package.json b/packages/compass-telemetry/package.json index 0851b88b0cd..725f5717981 100644 --- a/packages/compass-telemetry/package.json +++ b/packages/compass-telemetry/package.json @@ -55,7 +55,8 @@ "@mongodb-js/compass-logging": "^1.7.9", "@mongodb-js/compass-app-registry": "^9.4.18", "hadron-ipc": "^3.5.8", - "react": "^17.0.2" + "react": "^17.0.2", + "@mongodb-js/mdb-experiment-js": "1.9.0" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.4.5", diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 3c8d05350ed..503db85c5e6 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -1,47 +1,16 @@ import React, { createContext, useContext, useRef } from 'react'; - -interface ExperimentAssignmentData { - variant: string | null; - isInSample: boolean; -} - -interface ExperimentData { - assignmentDate: Date; - entityId: string; - entityType: string; - id: string; - tag: string; - testGroupDatabaseId: string; - testGroupId: string; - testId: string; - testName: string; -} - -interface SDKAssignment { - assignmentData: ExperimentAssignmentData; - experimentData: ExperimentData | null; -} - -interface UseAssignmentResponse { - assignment: SDKAssignment | null; - asyncStatus: 'LOADING' | 'SUCCESS' | 'ERROR'; - error?: Error; -} - -interface BasicAPICallingFunctionOptions { - timeoutMs?: number; - team?: string; -} +import type { types } from '@mongodb-js/mdb-experiment-js'; +import type { typesReact } from '@mongodb-js/mdb-experiment-js/react'; type UseAssignmentHookFn = ( experimentName: string, trackIsInSample: boolean, - options?: BasicAPICallingFunctionOptions -) => UseAssignmentResponse; + options?: types.GetAssignmentOptions +) => typesReact.UseAssignmentResponse; type AssignExperimentFn = ( experimentName: string, - options?: BasicAPICallingFunctionOptions + options?: types.AssignOptions ) => Promise<'SUCCESS' | 'ERROR' | null>; interface CompassExperimentationProviderContextValue { @@ -54,7 +23,11 @@ const ExperimentationContext = useAssignment() { return { assignment: null, - asyncStatus: 'SUCCESS' as const, + asyncStatus: null, + error: null, + isLoading: false, + isError: false, + isSuccess: true, }; }, assignExperiment() { @@ -68,8 +41,8 @@ export const CompassExperimentationProvider: React.FC<{ useAssignment: UseAssignmentHookFn; assignExperiment: AssignExperimentFn; }> = ({ children, useAssignment, assignExperiment }) => { - // Use useRef to keep the functions up-to-date; maintain same object reference for context value - // to prevent unnecessary re-renders of consuming components, + // Maintain stable object reference for context value to prevent unnecessary re-renders + // of consuming components, while keeping the function implementations up-to-date const { current: contextValue } = useRef({ useAssignment, assignExperiment }); contextValue.useAssignment = useAssignment; contextValue.assignExperiment = assignExperiment; From 533719e0d3a5265071e00d4f8f97ef0714e6cff5 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 30 Jul 2025 14:58:36 -0400 Subject: [PATCH 6/9] initialContext nit comment --- .../src/experimentation-provider.tsx | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 503db85c5e6..180b994a05d 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -18,22 +18,24 @@ interface CompassExperimentationProviderContextValue { assignExperiment: AssignExperimentFn; } +const initialContext: CompassExperimentationProviderContextValue = { + useAssignment() { + return { + assignment: null, + asyncStatus: null, + error: null, + isLoading: false, + isError: false, + isSuccess: true, + }; + }, + assignExperiment() { + return Promise.resolve(null); + }, +}; + const ExperimentationContext = - createContext({ - useAssignment() { - return { - assignment: null, - asyncStatus: null, - error: null, - isLoading: false, - isError: false, - isSuccess: true, - }; - }, - assignExperiment() { - return Promise.resolve(null); - }, - }); + createContext(initialContext); // Provider component that accepts MMS experiment utils as props export const CompassExperimentationProvider: React.FC<{ From 4614599a9d5eb79165a5335da9652bdb70a7671b Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 30 Jul 2025 15:55:50 -0400 Subject: [PATCH 7/9] Package Lock --- package-lock.json | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/package-lock.json b/package-lock.json index 5c416678b65..07e10125a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8755,6 +8755,20 @@ "resolved": "packages/explain-plan-helper", "link": true }, + "node_modules/@mongodb-js/mdb-experiment-js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/mdb-experiment-js/-/mdb-experiment-js-1.9.0.tgz", + "integrity": "sha512-4JcsdyjmbUxzBRADGCPWH9ySif5nda7JW6wNChqd7gYagEK7+I76p24sd4rTo9Ub8+JVkNyfB+eeF9CHuM4y3Q==", + "license": "Apache-2.0", + "dependencies": { + "deepmerge": "^4.2.2", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" + } + }, "node_modules/@mongodb-js/mocha-config-compass": { "resolved": "configs/mocha-config-compass", "link": true @@ -20359,6 +20373,15 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deepmerge-ts": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.4.tgz", @@ -47683,6 +47706,7 @@ "dependencies": { "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-logging": "^1.7.9", + "@mongodb-js/mdb-experiment-js": "1.9.0", "hadron-ipc": "^3.5.8", "react": "^17.0.2" }, @@ -59411,6 +59435,7 @@ "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-logging": "^1.7.9", "@mongodb-js/eslint-config-compass": "^1.4.5", + "@mongodb-js/mdb-experiment-js": "1.9.0", "@mongodb-js/mocha-config-compass": "^1.7.0", "@mongodb-js/prettier-config-compass": "^1.2.8", "@mongodb-js/tsconfig-compass": "^1.2.9", @@ -60696,6 +60721,15 @@ } } }, + "@mongodb-js/mdb-experiment-js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/mdb-experiment-js/-/mdb-experiment-js-1.9.0.tgz", + "integrity": "sha512-4JcsdyjmbUxzBRADGCPWH9ySif5nda7JW6wNChqd7gYagEK7+I76p24sd4rTo9Ub8+JVkNyfB+eeF9CHuM4y3Q==", + "requires": { + "deepmerge": "^4.2.2", + "use-sync-external-store": "^1.2.0" + } + }, "@mongodb-js/mocha-config-compass": { "version": "file:configs/mocha-config-compass", "requires": { @@ -70625,6 +70659,11 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, "deepmerge-ts": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.4.tgz", From 7c72f4b275b9afb9e8af65b27f0a60580d9bf54d Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 30 Jul 2025 16:06:29 -0400 Subject: [PATCH 8/9] Tweak --- .../src/experimentation-provider.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 180b994a05d..4e49dc2c649 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -2,19 +2,19 @@ import React, { createContext, useContext, useRef } from 'react'; import type { types } from '@mongodb-js/mdb-experiment-js'; import type { typesReact } from '@mongodb-js/mdb-experiment-js/react'; -type UseAssignmentHookFn = ( +type UseAssignmentHook = ( experimentName: string, trackIsInSample: boolean, - options?: types.GetAssignmentOptions + options?: typesReact.UseAssignmentOptions ) => typesReact.UseAssignmentResponse; type AssignExperimentFn = ( experimentName: string, options?: types.AssignOptions -) => Promise<'SUCCESS' | 'ERROR' | null>; +) => Promise; interface CompassExperimentationProviderContextValue { - useAssignment: UseAssignmentHookFn; + useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; } @@ -40,7 +40,7 @@ const ExperimentationContext = // Provider component that accepts MMS experiment utils as props export const CompassExperimentationProvider: React.FC<{ children: React.ReactNode; - useAssignment: UseAssignmentHookFn; + useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; }> = ({ children, useAssignment, assignExperiment }) => { // Maintain stable object reference for context value to prevent unnecessary re-renders @@ -57,6 +57,6 @@ export const CompassExperimentationProvider: React.FC<{ }; // Hook for components to access experiment assignment -export const useAssignment = (...args: Parameters) => { +export const useAssignment = (...args: Parameters) => { return useContext(ExperimentationContext).useAssignment(...args); }; From 9f9b0ac8c849537c1757ad04d26d9cdfe3110525 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 30 Jul 2025 16:12:45 -0400 Subject: [PATCH 9/9] WIP --- packages/compass-telemetry/.eslintrc.js | 1 + packages/compass-telemetry/src/experimentation-provider.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/compass-telemetry/.eslintrc.js b/packages/compass-telemetry/.eslintrc.js index 0445f96e1a7..49d6d33c515 100644 --- a/packages/compass-telemetry/.eslintrc.js +++ b/packages/compass-telemetry/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { ], }, ], + '@typescript-eslint/no-redundant-type-constituents': 'off', }, }, ], diff --git a/packages/compass-telemetry/src/experimentation-provider.tsx b/packages/compass-telemetry/src/experimentation-provider.tsx index 4e49dc2c649..6609565fa0a 100644 --- a/packages/compass-telemetry/src/experimentation-provider.tsx +++ b/packages/compass-telemetry/src/experimentation-provider.tsx @@ -43,8 +43,8 @@ export const CompassExperimentationProvider: React.FC<{ useAssignment: UseAssignmentHook; assignExperiment: AssignExperimentFn; }> = ({ children, useAssignment, assignExperiment }) => { - // Maintain stable object reference for context value to prevent unnecessary re-renders - // of consuming components, while keeping the function implementations up-to-date + // Use useRef to keep the functions up-to-date; Use mutation pattern to maintain the + // same object reference to prevent unnecessary re-renders of consuming components const { current: contextValue } = useRef({ useAssignment, assignExperiment }); contextValue.useAssignment = useAssignment; contextValue.assignExperiment = assignExperiment;