-
Notifications
You must be signed in to change notification settings - Fork 229
feat: Compass Experimentation Provider CLOUDP-333843 #7151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
} | ||
|
||
interface ExperimentData { | ||
assignmentDate: Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these types supposed to identically match the types in the experiment sdk?
Some of these values are typed slightly differently. For example assignmentDate
is typed as a string
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few others including testGroupId
are nullable in that types.ts
file too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Alternatively, maybe the mdb-experiment-js-sdk
can be imported into compass's dev deps and then the types imported/derived from there to here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea @kpamaran! If it's a heavy-lift, we should consider it as a wrap-up item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add the package as a dependency for the purposes of sharing types, this makes sense, but to make sure it's not being used for anything else, let's add a eslint rule to only allow type imports from it (we do this for a couple of other packages, so you can look at those rules as an example)
useAssignment() { | ||
return { | ||
assignment: null, | ||
asyncStatus: 'SUCCESS' as const, | ||
}; | ||
}, | ||
assignExperiment() { | ||
return Promise.resolve(null); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Consider making this its own const
e.g. initialContext
or so.
const contextValue = useMemo( | ||
() => ({ useAssignment, assignExperiment }), | ||
[useAssignment, assignExperiment] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be a memo, memo is too unstable for something that can cause erroneous re-renders across the whole application and this can be easily broken by someone changing code outside of compass-web control. We know that these functions are not changed through the application lifecycle, so this can be a simple ref that's initialized once and never changes while component is mounted:
const contextValue = useMemo( | |
() => ({ useAssignment, assignExperiment }), | |
[useAssignment, assignExperiment] | |
); | |
const { current: contextValue } = useRef({ useAssignment, assignExperiment }); |
If you want to make sure that the latest function is passed (somewhat wrong in this case because hook implementation should never change like that), you can keep the current value updated on renders, but still the value that gets passed to provider should be as stable as possible:
const contextValue = useMemo( | |
() => ({ useAssignment, assignExperiment }), | |
[useAssignment, assignExperiment] | |
); | |
const { current: contextValue } = useRef({ useAssignment, assignExperiment }); | |
contextValue.useAssignment = useAssignment; | |
contextValue.assignExperiment = assignExperiment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok! Makes sense to me. I was responding to this Copilot Review comment. I will revert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a React-based experimentation provider that enables Compass to receive experimentation utilities from MMS through dependency injection. The implementation follows conventional React patterns for sharing experiment assignment functionality across the Compass application.
Key changes:
- Created
CompassExperimentationProvider
React context component for dependency injection - Added type definitions for experiment assignment hooks and functions
- Exported the provider through the compass-web package for MMS integration
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
File | Description |
---|---|
packages/compass-web/src/index.tsx | Exports the CompassExperimentationProvider for external consumption |
packages/compass-telemetry/src/index.ts | Re-exports the experimentation provider from the telemetry package |
packages/compass-telemetry/src/experimentation-provider.tsx | Implements the React context provider with experiment assignment functionality |
packages/compass-telemetry/package.json | Adds dependency on @mongodb-js/mdb-experiment-js library |
packages/compass-telemetry/.eslintrc.js | Configures ESLint to only allow type imports from the experiment library |
Comments suppressed due to low confidence (1)
packages/compass-telemetry/package.json:59
- Please verify that version 1.9.0 of @mongodb-js/mdb-experiment-js exists. Given that my knowledge cutoff is January 2025 and it's currently July 2025, this version may have been released after my training data, but it's worth confirming the version exists in the registry.
"@mongodb-js/mdb-experiment-js": "1.9.0"
const { current: contextValue } = useRef({ useAssignment, assignExperiment }); | ||
contextValue.useAssignment = useAssignment; | ||
contextValue.assignExperiment = assignExperiment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useRef pattern here won't prevent re-renders as intended. The context value object is recreated on every render because it captures the current prop values. Consider using useMemo with proper dependencies instead: const contextValue = useMemo(() => ({ useAssignment, assignExperiment }), [useAssignment, assignExperiment]);
const { current: contextValue } = useRef({ useAssignment, assignExperiment }); | |
contextValue.useAssignment = useAssignment; | |
contextValue.assignExperiment = assignExperiment; | |
const contextValue = React.useMemo( | |
() => ({ useAssignment, assignExperiment }), | |
[useAssignment, assignExperiment] | |
); |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignoring this, see above discussion
@@ -5,4 +5,26 @@ module.exports = { | |||
tsconfigRootDir: __dirname, | |||
project: ['./tsconfig-lint.json'], | |||
}, | |||
overrides: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]: if this package is intended to be used within compass-telemetry
only then it makes sense to add it here, or else you can add it to base eslint config file: https://github.com/mongodb-js/compass/blob/main/configs/eslint-config-compass/index.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we'll need these types outside this package! We'll import useAssignment
or assignExperiment
, but I don't think we'll have to define any other objects with the types.
type UseAssignmentHook = ( | ||
experimentName: string, | ||
trackIsInSample: boolean, | ||
options?: typesReact.UseAssignmentOptions<types.TypeData> | ||
) => typesReact.UseAssignmentResponse<types.TypeData>; | ||
|
||
type AssignExperimentFn = ( | ||
experimentName: string, | ||
options?: types.AssignOptions<string> | ||
) => Promise<types.AsyncStatus | null>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to note that these should be kept in sync with the signatures in the SDK (with a link to where the SDK defines these)? I assume so, since these values are coming from MMS, which will be following the SDK conventions.
This does feel a little brittle, but it'll prob do for now til if/when we make the SDK a first-order dep in this repo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that the parent type signatures (UseAssignmentHook
and AssignExperimentFn
) are actually defined in MMS to type the useAssignment
and assign
functions respectively (to match the SDK functions), and that MMS uses the SDK types (typesReact.UseAssignmentOptions
, typesReact.UseAssignmentResponse
, etc.) as elements in those parent type signatures, but the parent type signatures are not exported by the SDK or MMS. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So should we link to the MMS or SDK useAssignment
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to merge this in to unblock tickets and add the comment on next compass PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just had the 1 nit about adding a comment 🧪
CLOUDP-333843
Description
Motivation and Context
To re-use experimentation logic built on top of the Experiment SDK in MMS, we will allow mms to provide required hooks and callback methods to compass through a React-context-based dependency-injection pattern.
Compass will define an CompassExperimentationProvider interface, which MMS will provide the implementation functions for. This follows conventional React dependency injection patterns and allows Compass to define what it needs from MMS for its experimentation architecture.
Compass will define the experimentation provider in the compass-telemetry package and export it through compass-web. MMS will then wrap the Compass application with this provider while supplying the necessary experiment functions.
(We have already started to accumulate experiment-related code in the compass-telemetry package; this integration creates a dedicated, shared place for experimentation code in the compass repository.)
Open Questions
Do we like defining these experiment types in Compass? Or should we import from MMS?
Dependents
Types of changes