Skip to content

chore(compass-preferences-model): use optInGenAIFeatures for Compass COMPASS-9593 #7129

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('Collection ai query', function () {
true
);
await browser.setFeature('enableGenAIFeaturesAtlasOrg', true);
await browser.setFeature('optInDataExplorerGenAIFeatures', true);
await browser.setFeature('optInGenAIFeatures', true);
});

describe('on the documents tab', function () {
Expand Down Expand Up @@ -170,7 +170,7 @@ describe('Collection ai query', function () {
true
);
await browser.setFeature('enableGenAIFeaturesAtlasOrg', false);
await browser.setFeature('optInDataExplorerGenAIFeatures', true);
await browser.setFeature('optInGenAIFeatures', true);
});

it('should not show the gen ai intro button', async function () {
Expand Down
48 changes: 48 additions & 0 deletions packages/compass-generative-ai/src/atlas-ai-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,54 @@ describe('AtlasAiService', function () {
});
});
});

describe('optIntoGenAIFeatures', function () {
beforeEach(async function () {
// Reset preferences
await preferences.savePreferences({
optInGenAIFeatures: false,
enableUnauthenticatedGenAI: true,
});
});

afterEach(async function () {
await preferences.savePreferences({
enableUnauthenticatedGenAI: false,
});
});

it('should save preference when cloud preset', async function () {
const fetchStub = sandbox.stub().resolves(makeResponse({}));
global.fetch = fetchStub;

await atlasAiService.optIntoGenAIFeatures();

// In Data Explorer, make a POST request to cloud endpoint and save preference
if (apiURLPreset === 'cloud') {
// Verify fetch was called with correct parameters
expect(fetchStub).to.have.been.calledOnce;

expect(fetchStub).to.have.been.calledWith(
'/cloud/settings/optInDataExplorerGenAIFeatures',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: new URLSearchParams([['value', 'true']]),
}
);
} else {
// In Compass, no fetch is made, only stored locally
expect(fetchStub).to.not.have.been.called;
}

// Verify preference was saved
const currentPreferences = preferences.getPreferences();
expect(currentPreferences.optInGenAIFeatures).to.equal(true);
});
});
});
}
});
40 changes: 23 additions & 17 deletions packages/compass-generative-ai/src/atlas-ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider';
import type { Document } from 'mongodb';
import type { Logger } from '@mongodb-js/compass-logging';
import { EJSON } from 'bson';
import { signIntoAtlasWithModalPrompt } from './store/atlas-signin-reducer';
import { getStore } from './store/atlas-ai-store';
import { optIntoGenAIWithModalPrompt } from './store/atlas-optin-reducer';
import { signIntoAtlasWithModalPrompt } from './store/atlas-signin-reducer';

type GenerativeAiInput = {
userInput: string;
Expand Down Expand Up @@ -277,6 +277,10 @@ export class AtlasAiService {
}

async ensureAiFeatureAccess({ signal }: { signal?: AbortSignal } = {}) {
if (this.preferences.getPreferences().enableUnauthenticatedGenAI) {
return getStore().dispatch(optIntoGenAIWithModalPrompt({ signal }));
}

// When the ai feature is attempted to be opened we make sure
// the user is signed into Atlas and opted in.

Expand Down Expand Up @@ -391,23 +395,25 @@ export class AtlasAiService {
);
}

// Performs a post request to atlas to set the user opt in preference to true.
async optIntoGenAIFeaturesAtlas() {
await this.atlasService.authenticatedFetch(
this.atlasService.cloudEndpoint(
'/settings/optInDataExplorerGenAIFeatures'
),
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: new URLSearchParams([['value', 'true']]),
}
);
async optIntoGenAIFeatures() {
if (this.apiURLPreset === 'cloud') {
// Performs a post request to Atlas to set the user opt in preference to true.
await this.atlasService.authenticatedFetch(
this.atlasService.cloudEndpoint(
'settings/optInDataExplorerGenAIFeatures'
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL path still references 'optInDataExplorerGenAIFeatures' but the preference has been renamed to 'optInGenAIFeatures'. Consider updating the API endpoint to match the new naming convention for consistency.

Suggested change
'settings/optInDataExplorerGenAIFeatures'
'settings/optInGenAIFeatures'

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we ensure backwards-compatibility

),
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: new URLSearchParams([['value', 'true']]),
}
);
}
await this.preferences.savePreferences({
optInDataExplorerGenAIFeatures: true,
optInGenAIFeatures: true,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('atlasOptInReducer', function () {
beforeEach(async function () {
mockPreferences = await createSandboxFromDefaultPreferences();
await mockPreferences.savePreferences({
optInDataExplorerGenAIFeatures: false,
optInGenAIFeatures: false,
});
});

Expand All @@ -31,7 +31,7 @@ describe('atlasOptInReducer', function () {
describe('optIn', function () {
it('should check state and set state to success if already opted in', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }),
optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -45,8 +45,7 @@ describe('atlasOptInReducer', function () {
);
void store.dispatch(atlasAiServiceOptedIn());
await store.dispatch(optIn());
expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).not.to.have.been
.called;
expect(mockAtlasAiService.optIntoGenAIFeatures).not.to.have.been.called;
expect(store.getState().optIn).to.have.nested.property(
'state',
'optin-success'
Expand All @@ -55,7 +54,7 @@ describe('atlasOptInReducer', function () {

it('should start opt in, and set state to success', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }),
optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -69,8 +68,7 @@ describe('atlasOptInReducer', function () {
);
void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {});
await store.dispatch(optIn());
expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been
.calledOnce;
expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce;
expect(store.getState().optIn).to.have.nested.property(
'state',
'optin-success'
Expand All @@ -81,13 +79,13 @@ describe('atlasOptInReducer', function () {
beforeEach(async function () {
await mockPreferences.savePreferences({
enableGenAIFeaturesAtlasProject: false,
optInDataExplorerGenAIFeatures: true,
optInGenAIFeatures: true,
});
});

it('should start the opt in flow', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }),
optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -101,8 +99,7 @@ describe('atlasOptInReducer', function () {
);
void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {});
await store.dispatch(optIn());
expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been
.calledOnce;
expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce;
expect(store.getState().optIn).to.have.nested.property(
'state',
'optin-success'
Expand All @@ -112,9 +109,7 @@ describe('atlasOptInReducer', function () {

it('should fail opt in if opt in failed', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox
.stub()
.rejects(new Error('Whooops!')),
optIntoGenAIFeatures: sandbox.stub().rejects(new Error('Whooops!')),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -127,8 +122,7 @@ describe('atlasOptInReducer', function () {
// Avoid unhandled rejections.
AttemptStateMap.get(attemptId)?.promise.catch(() => {});
await optInPromise;
expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been
.calledOnce;
expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce;
expect(store.getState().optIn).to.have.nested.property('state', 'error');
});
});
Expand All @@ -153,7 +147,7 @@ describe('atlasOptInReducer', function () {

it('should cancel opt in if opt in is in progress', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox
optIntoGenAIFeatures: sandbox
.stub()
.callsFake(({ signal }: { signal: AbortSignal }) => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -183,10 +177,10 @@ describe('atlasOptInReducer', function () {
});
});

describe('optIntoAtlasWithModalPrompt', function () {
describe('optIntoGenAIWithModalPrompt', function () {
it('should resolve when user finishes opt in with prompt flow', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }),
optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -203,7 +197,7 @@ describe('atlasOptInReducer', function () {

it('should reject if opt in flow fails', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().rejects(new Error('Whoops!')),
optIntoGenAIFeatures: sandbox.stub().rejects(new Error('Whoops!')),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -226,7 +220,7 @@ describe('atlasOptInReducer', function () {

it('should reject if user dismissed the modal', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }),
optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand All @@ -249,7 +243,7 @@ describe('atlasOptInReducer', function () {

it('should reject if provided signal was aborted', async function () {
const mockAtlasAiService = {
optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }),
optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }),
};
const store = configureStore({
atlasAuthService: {} as any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export const optIntoGenAIWithModalPrompt = ({
const { state } = getState().optIn;
if (
(state === 'optin-success' ||
preferences.getPreferences().optInDataExplorerGenAIFeatures) &&
preferences.getPreferences().optInGenAIFeatures) &&
preferences.getPreferences().enableGenAIFeaturesAtlasProject
) {
return Promise.resolve();
Expand Down Expand Up @@ -265,7 +265,7 @@ export const optIn = (): GenAIAtlasOptInThunkAction<Promise<void>> => {

try {
throwIfAborted(signal);
await atlasAiService.optIntoGenAIFeaturesAtlas();
await atlasAiService.optIntoGenAIFeatures();
dispatch(atlasAiServiceOptedIn());
resolve();
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getActiveUser } from './utils';

const editablePreferences: (keyof UserPreferences)[] = [
// Value can change from false to true during allocation / checking
'optInDataExplorerGenAIFeatures',
'optInGenAIFeatures',
'cloudFeatureRolloutAccess',
// TODO(COMPASS-9353): Provide a standard for updating Compass preferences in web
'enableIndexesGuidanceExp',
Expand Down
8 changes: 8 additions & 0 deletions packages/compass-preferences-model/src/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type FeatureFlags = {
showIndexesGuidanceVariant: boolean;
enableContextMenus: boolean;
enableSearchActivationProgramP1: boolean;
enableUnauthenticatedGenAI: boolean;
};

export const featureFlags: Required<{
Expand Down Expand Up @@ -150,6 +151,13 @@ export const featureFlags: Required<{
},
},

enableUnauthenticatedGenAI: {
stage: 'development',
description: {
short: 'Enable GenAI for unauthenticated users',
},
},

/**
* Feature flag for CLOUDP-308952.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags &
| 'web-sandbox-atlas-dev'
| 'web-sandbox-atlas-qa'
| 'web-sandbox-atlas';
optInDataExplorerGenAIFeatures: boolean;
optInGenAIFeatures: boolean;
// Features that are enabled by default in Compass, but are disabled in Data
// Explorer
enableExplainPlan: boolean;
Expand Down Expand Up @@ -810,17 +810,16 @@ export const storedUserPreferencesProps: Required<{
.default('atlas'),
type: 'string',
},
optInDataExplorerGenAIFeatures: {
optInGenAIFeatures: {
ui: true,
cli: false,
global: false,
description: {
short: 'User Opt-in for Data Explorer Gen AI Features',
short: 'User or Client Opt-in for Gen AI Features',
},
validator: z.boolean().default(true),
validator: z.boolean().default(false),
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the default value from true to false is a breaking change that could affect existing users who had the feature enabled by default. This could disable AI features for users who were previously opted in.

Suggested change
validator: z.boolean().default(false),
validator: z.boolean().default(true),

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was likely an unintentional default which always gets overriden by the 'settings/optInDataExplorerGenAIFeatures' fetch

Copy link
Contributor Author

@gagik gagik Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct me if not so but this change shouldn't have any side effects on DE. The state of this was always being overriden by the API request and we'd likely not have wanted this to be true anyhow.

type: 'boolean',
},

enableAtlasSearchIndexes: {
ui: true,
cli: true,
Expand Down
5 changes: 2 additions & 3 deletions packages/compass-web/sandbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const App = () => {
enableGenAIFeaturesAtlasProject,
enableGenAISampleDocumentPassingOnAtlasProject,
enableGenAIFeaturesAtlasOrg,
optInDataExplorerGenAIFeatures,
optInGenAIFeatures,
} = projectParams ?? {};

const atlasServiceSandboxBackendVariant =
Expand Down Expand Up @@ -135,8 +135,7 @@ const App = () => {
isAtlas && !!enableGenAISampleDocumentPassingOnAtlasProject,
enableGenAIFeaturesAtlasOrg:
isAtlas && !!enableGenAIFeaturesAtlasOrg,
optInDataExplorerGenAIFeatures:
isAtlas && !!optInDataExplorerGenAIFeatures,
optInGenAIFeatures: isAtlas && !!optInGenAIFeatures,
enableDataModeling: true,
}}
onTrack={sandboxTelemetry.track}
Expand Down
5 changes: 2 additions & 3 deletions packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type ProjectParams = {
enableGenAIFeaturesAtlasProject: boolean;
enableGenAISampleDocumentPassingOnAtlasProject: boolean;
enableGenAIFeaturesAtlasOrg: boolean;
optInDataExplorerGenAIFeatures: boolean;
optInGenAIFeatures: boolean;
};

type AtlasLoginReturnValue =
Expand Down Expand Up @@ -129,8 +129,7 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue {
projectId,
csrfToken,
csrfTime,
optInDataExplorerGenAIFeatures:
isOptedIntoDataExplorerGenAIFeatures,
optInGenAIFeatures: isOptedIntoDataExplorerGenAIFeatures,
enableGenAIFeaturesAtlasOrg: genAIFeaturesEnabled,
enableGenAISampleDocumentPassingOnAtlasProject:
groupEnabledFeatureFlags.includes(
Expand Down
Loading