Skip to content

Commit db7e9b8

Browse files
authored
feat(compass-collection): Collection Plugin Experimentation Assignment Integration – CLOUDP-333845 (#7165)
* WIP * Org level flag and tests * WIP * WIP * Absolute import * Remove unneeded check * Comment address * Change export * Comment * Change exports * Rename existing test name enum
1 parent 88b2077 commit db7e9b8

File tree

9 files changed

+233
-14
lines changed

9 files changed

+233
-14
lines changed

packages/compass-collection/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import {
66
dataServiceLocator,
77
type DataServiceLocator,
88
type DataService,
9+
connectionInfoRefLocator,
910
} from '@mongodb-js/compass-connections/provider';
1011
import { collectionModelLocator } from '@mongodb-js/compass-app-stores/provider';
1112
import type { WorkspacePlugin } from '@mongodb-js/compass-workspaces';
1213
import { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
14+
import { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider';
15+
import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
16+
import { preferencesLocator } from 'compass-preferences-model/provider';
1317
import {
1418
CollectionWorkspaceTitle,
1519
CollectionPluginTitleComponent,
@@ -29,6 +33,10 @@ export const WorkspaceTab: WorkspacePlugin<typeof CollectionWorkspaceTitle> = {
2933
dataService: dataServiceLocator as DataServiceLocator<keyof DataService>,
3034
collection: collectionModelLocator,
3135
workspaces: workspacesServiceLocator,
36+
experimentationServices: experimentationServiceLocator,
37+
connectionInfoRef: connectionInfoRefLocator,
38+
logger: createLoggerLocator('COMPASS-COLLECTION'),
39+
preferences: preferencesLocator,
3240
}
3341
),
3442
content: CollectionTab,

packages/compass-collection/src/modules/collection-tab.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type AppRegistry from '@mongodb-js/compass-app-registry';
55
import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
66
import type { CollectionSubtab } from '@mongodb-js/compass-workspaces';
77
import type { DataService } from '@mongodb-js/compass-connections/provider';
8+
import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider';
89

910
function isAction<A extends AnyAction>(
1011
action: AnyAction,
@@ -20,6 +21,7 @@ type CollectionThunkAction<R, A extends AnyAction = AnyAction> = ThunkAction<
2021
localAppRegistry: AppRegistry;
2122
dataService: DataService;
2223
workspaces: ReturnType<typeof workspacesServiceLocator>;
24+
experimentationServices: ReturnType<typeof experimentationServiceLocator>;
2325
},
2426
A
2527
>;

packages/compass-collection/src/stores/collection-tab.spec.ts

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import Sinon from 'sinon';
66
import AppRegistry from '@mongodb-js/compass-app-registry';
77
import { expect } from 'chai';
88
import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
9+
import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry';
10+
import type { connectionInfoRefLocator } from '@mongodb-js/compass-connections/provider';
11+
import { createNoopLogger } from '@mongodb-js/compass-logging/provider';
12+
import { ReadOnlyPreferenceAccess } from 'compass-preferences-model/provider';
13+
import { ExperimentTestName } from '@mongodb-js/compass-telemetry/provider';
914

1015
const defaultMetadata = {
1116
namespace: 'test.foo',
@@ -32,6 +37,32 @@ const mockCollection = {
3237
},
3338
};
3439

40+
const mockAtlasConnectionInfo = {
41+
current: {
42+
id: 'test-connection',
43+
title: 'Test Connection',
44+
connectionOptions: {
45+
connectionString: 'mongodb://localhost:27017',
46+
},
47+
atlasMetadata: {
48+
clusterName: 'test-cluster',
49+
projectId: 'test-project',
50+
orgId: 'test-org',
51+
clusterUniqueId: 'test-cluster-unique-id',
52+
clusterType: 'REPLICASET' as const,
53+
clusterState: 'IDLE' as const,
54+
metricsId: 'test-metrics-id',
55+
metricsType: 'replicaSet' as const,
56+
regionalBaseUrl: null,
57+
instanceSize: 'M10',
58+
supports: {
59+
globalWrites: false,
60+
rollingIndexes: true,
61+
},
62+
},
63+
},
64+
};
65+
3566
describe('Collection Tab Content store', function () {
3667
const sandbox = Sinon.createSandbox();
3768

@@ -42,7 +73,19 @@ describe('Collection Tab Content store', function () {
4273

4374
const configureStore = async (
4475
options: Partial<CollectionTabOptions> = {},
45-
workspaces: Partial<ReturnType<typeof workspacesServiceLocator>> = {}
76+
workspaces: Partial<ReturnType<typeof workspacesServiceLocator>> = {},
77+
experimentationServices: Partial<
78+
ReturnType<typeof experimentationServiceLocator>
79+
> = {},
80+
connectionInfoRef: Partial<
81+
ReturnType<typeof connectionInfoRefLocator>
82+
> = {},
83+
logger = createNoopLogger('COMPASS-COLLECTION-TEST'),
84+
preferences = new ReadOnlyPreferenceAccess({
85+
enableGenAIFeatures: true,
86+
enableGenAIFeaturesAtlasOrg: true,
87+
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true },
88+
})
4689
) => {
4790
({ store, deactivate } = activatePlugin(
4891
{
@@ -54,6 +97,10 @@ describe('Collection Tab Content store', function () {
5497
localAppRegistry,
5598
collection: mockCollection as any,
5699
workspaces: workspaces as any,
100+
experimentationServices: experimentationServices as any,
101+
connectionInfoRef: connectionInfoRef as any,
102+
logger,
103+
preferences,
57104
},
58105
{ on() {}, cleanup() {} } as any
59106
));
@@ -76,11 +123,112 @@ describe('Collection Tab Content store', function () {
76123
const store = await configureStore(undefined, {
77124
openCollectionWorkspaceSubtab,
78125
});
79-
store.dispatch(selectTab('Documents'));
126+
store.dispatch(selectTab('Documents') as any);
80127
expect(openCollectionWorkspaceSubtab).to.have.been.calledWith(
81128
'workspace-tab-id',
82129
'Documents'
83130
);
84131
});
85132
});
133+
134+
describe('experimentation integration', function () {
135+
it('should assign experiment when Atlas metadata is available', async function () {
136+
const assignExperiment = sandbox.spy(() => Promise.resolve(null));
137+
138+
await configureStore(
139+
undefined,
140+
{},
141+
{ assignExperiment },
142+
mockAtlasConnectionInfo
143+
);
144+
145+
await waitFor(() => {
146+
expect(assignExperiment).to.have.been.calledOnceWith(
147+
ExperimentTestName.mockDataGenerator,
148+
{
149+
team: 'Atlas Growth',
150+
}
151+
);
152+
});
153+
});
154+
155+
it('should not assign experiment when Atlas metadata is missing', async function () {
156+
const assignExperiment = sandbox.spy(() => Promise.resolve(null));
157+
const mockConnectionInfoRef = {
158+
current: {
159+
id: 'test-connection',
160+
title: 'Test Connection',
161+
connectionOptions: {
162+
connectionString: 'mongodb://localhost:27017',
163+
},
164+
// No atlasMetadata
165+
},
166+
};
167+
168+
await configureStore(
169+
undefined,
170+
{},
171+
{ assignExperiment },
172+
mockConnectionInfoRef
173+
);
174+
175+
// Wait a bit to ensure assignment would have happened if it was going to
176+
await new Promise((resolve) => setTimeout(resolve, 50));
177+
expect(assignExperiment).to.not.have.been.called;
178+
});
179+
180+
it('should not assign experiment when AI features are disabled at the org level', async function () {
181+
const assignExperiment = sandbox.spy(() => Promise.resolve(null));
182+
183+
const mockPreferences = new ReadOnlyPreferenceAccess({
184+
enableGenAIFeatures: true,
185+
enableGenAIFeaturesAtlasOrg: false, // Disabled at org level
186+
cloudFeatureRolloutAccess: { GEN_AI_COMPASS: true },
187+
});
188+
189+
const store = await configureStore(
190+
undefined,
191+
{},
192+
{ assignExperiment },
193+
mockAtlasConnectionInfo,
194+
undefined,
195+
mockPreferences
196+
);
197+
198+
// Wait a bit to ensure assignment would have happened if it was going to
199+
await new Promise((resolve) => setTimeout(resolve, 50));
200+
expect(assignExperiment).to.not.have.been.called;
201+
202+
// Store should still be functional
203+
await waitFor(() => {
204+
expect(store.getState())
205+
.to.have.property('metadata')
206+
.deep.eq(defaultMetadata);
207+
});
208+
});
209+
210+
it('should handle assignment errors gracefully', async function () {
211+
const assignExperiment = sandbox.spy(() =>
212+
Promise.reject(new Error('Assignment failed'))
213+
);
214+
215+
await configureStore(
216+
undefined,
217+
{},
218+
{ assignExperiment },
219+
mockAtlasConnectionInfo
220+
);
221+
222+
await waitFor(() => {
223+
expect(assignExperiment).to.have.been.calledOnce;
224+
});
225+
226+
// Store should still be functional despite assignment error
227+
await waitFor(() => {
228+
expect(store.getState())
229+
.to.have.property('metadata')
230+
.deep.eq(defaultMetadata);
231+
});
232+
});
233+
});
86234
});

packages/compass-collection/src/stores/collection-tab.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import reducer, {
99
import type { Collection } from '@mongodb-js/compass-app-stores/provider';
1010
import type { ActivateHelpers } from '@mongodb-js/compass-app-registry';
1111
import type { workspacesServiceLocator } from '@mongodb-js/compass-workspaces/provider';
12+
import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider';
13+
import type { connectionInfoRefLocator } from '@mongodb-js/compass-connections/provider';
14+
import type { Logger } from '@mongodb-js/compass-logging/provider';
15+
import {
16+
isAIFeatureEnabled,
17+
type PreferencesAccess,
18+
} from 'compass-preferences-model/provider';
19+
import { ExperimentTestName } from '@mongodb-js/compass-telemetry/provider';
1220

1321
export type CollectionTabOptions = {
1422
/**
@@ -31,18 +39,29 @@ export type CollectionTabServices = {
3139
collection: Collection;
3240
localAppRegistry: AppRegistry;
3341
workspaces: ReturnType<typeof workspacesServiceLocator>;
42+
experimentationServices: ReturnType<typeof experimentationServiceLocator>;
43+
connectionInfoRef: ReturnType<typeof connectionInfoRefLocator>;
44+
logger: Logger;
45+
preferences: PreferencesAccess;
3446
};
3547

3648
export function activatePlugin(
3749
{ namespace, editViewName, tabId }: CollectionTabOptions,
3850
services: CollectionTabServices,
3951
{ on, cleanup }: ActivateHelpers
40-
) {
52+
): {
53+
store: ReturnType<typeof createStore>;
54+
deactivate: () => void;
55+
} {
4156
const {
4257
dataService,
4358
collection: collectionModel,
4459
localAppRegistry,
4560
workspaces,
61+
experimentationServices,
62+
connectionInfoRef,
63+
logger,
64+
preferences,
4665
} = services;
4766

4867
if (!collectionModel) {
@@ -64,6 +83,7 @@ export function activatePlugin(
6483
dataService,
6584
workspaces,
6685
localAppRegistry,
86+
experimentationServices,
6787
})
6888
)
6989
);
@@ -86,6 +106,25 @@ export function activatePlugin(
86106

87107
void collectionModel.fetchMetadata({ dataService }).then((metadata) => {
88108
store.dispatch(collectionMetadataFetched(metadata));
109+
110+
// Assign experiment for Mock Data Generator
111+
// Only assign when we're connected to Atlas and the org-level setting for AI features is enabled
112+
if (
113+
connectionInfoRef.current?.atlasMetadata?.clusterName && // Ensures we only assign in Atlas
114+
isAIFeatureEnabled(preferences.getPreferences()) // Ensures org-level AI features setting is enabled
115+
) {
116+
void experimentationServices
117+
.assignExperiment(ExperimentTestName.mockDataGenerator, {
118+
team: 'Atlas Growth',
119+
})
120+
.catch((error) => {
121+
logger.debug('Mock Data Generator experiment assignment failed', {
122+
experiment: ExperimentTestName.mockDataGenerator,
123+
namespace: namespace,
124+
error: error instanceof Error ? error.message : String(error),
125+
});
126+
});
127+
}
89128
});
90129

91130
return {

packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
useTrackOnChange,
2525
type TrackFunction,
2626
useFireExperimentViewed,
27-
TestName,
27+
ExperimentTestName,
2828
useTelemetry,
2929
} from '@mongodb-js/compass-telemetry/provider';
3030
import { useConnectionInfoRef } from '@mongodb-js/compass-connections/provider';
@@ -91,7 +91,7 @@ function CreateIndexModal({
9191
usePreference('showIndexesGuidanceVariant') && enableInIndexesGuidanceExp;
9292

9393
useFireExperimentViewed({
94-
testName: TestName.earlyJourneyIndexesGuidance,
94+
testName: ExperimentTestName.earlyJourneyIndexesGuidance,
9595
shouldFire: enableInIndexesGuidanceExp && isVisible,
9696
});
9797

packages/compass-telemetry/src/experimentation-provider.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import React, { createContext, useContext, useRef } from 'react';
22
import type { types } from '@mongodb-js/mdb-experiment-js';
33
import type { typesReact } from '@mongodb-js/mdb-experiment-js/react';
4+
import type { ExperimentTestName } from './growth-experiments';
45

56
type UseAssignmentHook = (
6-
experimentName: string,
7+
experimentName: ExperimentTestName,
78
trackIsInSample: boolean,
89
options?: typesReact.UseAssignmentOptions<types.TypeData>
910
) => typesReact.UseAssignmentResponse<types.TypeData>;
1011

1112
type AssignExperimentFn = (
12-
experimentName: string,
13+
experimentName: ExperimentTestName,
1314
options?: types.AssignOptions<string>
1415
) => Promise<types.AsyncStatus | null>;
1516

@@ -34,7 +35,7 @@ const initialContext: CompassExperimentationProviderContextValue = {
3435
},
3536
};
3637

37-
const ExperimentationContext =
38+
export const ExperimentationContext =
3839
createContext<CompassExperimentationProviderContextValue>(initialContext);
3940

4041
// Provider component that accepts MMS experiment utils as props
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
export enum TestName {
1+
export enum ExperimentTestName {
22
earlyJourneyIndexesGuidance = 'EARLY_JOURNEY_INDEXES_GUIDANCE_20250328',
3+
mockDataGenerator = 'MOCK_DATA_GENERATOR_20251001',
34
}

packages/compass-telemetry/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export type {
77
} from './types';
88

99
export { CompassExperimentationProvider } from './experimentation-provider';
10+
export { experimentationServiceLocator } from './provider';
11+
export { ExperimentTestName } from './growth-experiments';

0 commit comments

Comments
 (0)