Skip to content
Merged
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 @@ -42,6 +42,7 @@ describe('CollectionHeaderActions [Component]', function () {
return renderWithActiveConnection(
<CompassExperimentationProvider
useAssignment={mockUseAssignment}
useTrackInSample={sinon.stub()}
assignExperiment={sinon.stub()}
getAssignment={sinon.stub().resolves(null)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ describe('CollectionHeader [Component]', function () {
return renderWithActiveConnection(
<CompassExperimentationProvider
useAssignment={mockUseAssignment}
useTrackInSample={Sinon.stub()}
assignExperiment={Sinon.stub()}
getAssignment={Sinon.stub().resolves(null)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
import { expect } from 'chai';
import sinon from 'sinon';

import { AtlasSkillsBanner } from './atlas-skills-banner';

describe('AtlasSkillsBanner Component', function () {
const defaultProps = {
ctaText:
'New to MongoDB? Document modeling skills will accelerate your progress.',
skillsUrl: 'https://www.mongodb.com/skills',
onCloseSkillsBanner: sinon.spy(),
showBanner: true,
};

it('should render the banner with correct text', function () {
render(<AtlasSkillsBanner {...defaultProps} />);

expect(
screen.getByText(
'New to MongoDB? Document modeling skills will accelerate your progress.'
)
).to.be.visible;
});

it('should render the badge with award icon', function () {
render(<AtlasSkillsBanner {...defaultProps} />);

// Check for the award icon
const awardIcon = screen.getByLabelText('Award Icon');
expect(awardIcon).to.be.visible;
});

it('should render the "Go to Skills" button with correct href', function () {
render(<AtlasSkillsBanner {...defaultProps} />);

const goToSkillsButton = screen.getByRole('link', {
name: /go to skills/i,
});
expect(goToSkillsButton).to.be.visible;
expect(goToSkillsButton.getAttribute('href')).to.equal(
'https://www.mongodb.com/skills'
);
expect(goToSkillsButton.getAttribute('target')).to.equal('_blank');
});

it('should call onCtaClick when "Go to Skills" button is clicked', function () {
const onCtaClick = sinon.spy();
render(<AtlasSkillsBanner {...defaultProps} onCtaClick={onCtaClick} />);

const goToSkillsButton = screen.getByRole('link', {
name: /go to skills/i,
});

userEvent.click(goToSkillsButton);
expect(onCtaClick).to.have.been.calledOnce;
});

it('should render the close button and call onCloseSkillsBanner when clicked', function () {
const onCloseSkillsBanner = sinon.spy();
render(
<AtlasSkillsBanner
{...defaultProps}
onCloseSkillsBanner={onCloseSkillsBanner}
/>
);

const closeButton = screen.getByRole('button', {
name: 'Dismiss Skills Banner',
});
expect(closeButton).to.be.visible;
expect(closeButton.getAttribute('title')).to.equal('Dismiss Skills Banner');

userEvent.click(closeButton);
expect(onCloseSkillsBanner).to.have.been.calledOnce;
});

it('should not render when showBanner is false', function () {
render(<AtlasSkillsBanner {...defaultProps} showBanner={false} />);

// Banner should not be visible
expect(
screen.queryByText(
'New to MongoDB? Document modeling skills will accelerate your progress.'
)
).to.not.exist;
expect(screen.queryByLabelText('Award Icon')).to.not.exist;
expect(screen.queryByRole('link', { name: /go to skills/i })).to.not.exist;
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import { css } from '@leafygreen-ui/emotion';
import IconButton from '@leafygreen-ui/icon-button';
import Badge, { Variant as BadgeVariant } from '@leafygreen-ui/badge';
import Icon from '@leafygreen-ui/icon';
import { spacing } from '@leafygreen-ui/tokens';
import Button from '@leafygreen-ui/button';
import { palette } from '@leafygreen-ui/palette';

const skillsCTAContent = css({
border: `1px ${palette.gray.light1} solid`,
borderRadius: spacing[300],
padding: spacing[300],
paddingLeft: spacing[400],
display: 'flex',
width: '100%',
alignItems: 'center',
});

const skillsCTAText = css({
display: 'flex',
alignSelf: 'center',
paddingLeft: spacing[200],
});

const badgeStyles = css({
padding: '0 10px',
minHeight: spacing[600],
});

const learnMoreBtnStyles = css({
marginLeft: spacing[200],
});

const closeButtonStyles = css({
marginLeft: 'auto',
});

// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311
export const AtlasSkillsBanner: React.FunctionComponent<{
ctaText: string;
onCloseSkillsBanner: () => void;
onCtaClick?: () => void;
skillsUrl: string;
showBanner: boolean;
}> = ({ ctaText, skillsUrl, onCloseSkillsBanner, onCtaClick, showBanner }) => {
return showBanner ? (
<div className={skillsCTAContent}>
<Badge variant={BadgeVariant.Green} className={badgeStyles}>
<Icon glyph="Award" />
</Badge>
<div className={skillsCTAText}>{ctaText}</div>

<Button
value="Go to Skills"
size="xsmall"
href={skillsUrl}
target="_blank"
onClick={onCtaClick}
leftGlyph={<Icon glyph="OpenNewTab"></Icon>}
title="Go to Skills"
className={learnMoreBtnStyles}
>
Go to Skills
</Button>
<IconButton
className={closeButtonStyles}
title="Dismiss Skills Banner"
aria-label="Dismiss Skills Banner"
onClick={onCloseSkillsBanner}
>
<Icon glyph="X" />
</IconButton>
</div>
) : null;
};
2 changes: 2 additions & 0 deletions packages/compass-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,5 @@ export type {
NodeField,
NodeGlyph,
} from '@mongodb-js/diagramming';
// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311
export { AtlasSkillsBanner } from './components/atlas-skills-banner';
30 changes: 30 additions & 0 deletions packages/compass-telemetry/src/atlas-skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ExperimentTestGroup, ExperimentTestName } from './growth-experiments';
import { useAssignment, useTrackInSample } from './experimentation-provider';

export enum SkillsBannerContextEnum {
Documents = 'documents',
Aggregation = 'aggregation',
Indexes = 'indexes',
Schema = 'schema',
}

// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311
export const useAtlasSkillsBanner = (context: SkillsBannerContextEnum) => {
const atlasSkillsAssignment = useAssignment(
ExperimentTestName.atlasSkills,
false
);

const isInSkillsVariant =
atlasSkillsAssignment?.assignment?.assignmentData?.variant ===
ExperimentTestGroup.atlasSkillsVariant;

// Track users who are assigned to the skills experiment (variant or control)
useTrackInSample(ExperimentTestName.atlasSkills, !!atlasSkillsAssignment, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Generally we also track is in sample when the user is in the control as well. Is there a reason you're limiting to the variant here?

Copy link
Collaborator Author

@carolynmcca carolynmcca Oct 14, 2025

Choose a reason for hiding this comment

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

It'll fire if they're in variant or control - it won't fire if atlasSkillsAssignment is null or undefined
I'll update comment since it's confusing

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, and banner would be shown is incorrect then right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah updated comment!

screen: context,
});

return {
shouldShowAtlasSkillsBanner: isInSkillsVariant,
};
};
33 changes: 32 additions & 1 deletion packages/compass-telemetry/src/experimentation-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ type GetAssignmentFn = (
options?: types.GetAssignmentOptions<types.TypeData>
) => Promise<types.SDKAssignment<ExperimentTestName, string> | null>;

type UseTrackInSampleHook = (
experimentName: ExperimentTestName,
shouldFireEvent?: boolean,
customProperties?: types.TypeData['experimentViewedProps'],
team?: types.TypeData['loggerTeam']
) => typesReact.BasicHookResponse;

interface CompassExperimentationProviderContextValue {
useAssignment: UseAssignmentHook;
assignExperiment: AssignExperimentFn;
getAssignment: GetAssignmentFn;
useTrackInSample: UseTrackInSampleHook;
}

const initialContext: CompassExperimentationProviderContextValue = {
Expand All @@ -43,6 +51,15 @@ const initialContext: CompassExperimentationProviderContextValue = {
getAssignment() {
return Promise.resolve(null);
},
useTrackInSample() {
return {
asyncStatus: null,
error: null,
isLoading: false,
isError: false,
isSuccess: true,
};
},
};

export const ExperimentationContext =
Expand All @@ -54,17 +71,26 @@ export const CompassExperimentationProvider: React.FC<{
useAssignment: UseAssignmentHook;
assignExperiment: AssignExperimentFn;
getAssignment: GetAssignmentFn;
}> = ({ children, useAssignment, assignExperiment, getAssignment }) => {
useTrackInSample: UseTrackInSampleHook;
}> = ({
children,
useAssignment,
assignExperiment,
getAssignment,
useTrackInSample,
}) => {
// 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,
getAssignment,
useTrackInSample,
});
contextValue.useAssignment = useAssignment;
contextValue.assignExperiment = assignExperiment;
contextValue.getAssignment = getAssignment;
contextValue.useTrackInSample = useTrackInSample;

return (
<ExperimentationContext.Provider value={contextValue}>
Expand All @@ -77,3 +103,8 @@ export const CompassExperimentationProvider: React.FC<{
export const useAssignment = (...args: Parameters<UseAssignmentHook>) => {
return useContext(ExperimentationContext).useAssignment(...args);
};

// Hook for components to access experiment assignment
export const useTrackInSample = (...args: Parameters<UseTrackInSampleHook>) => {
return useContext(ExperimentationContext).useTrackInSample(...args);
};
3 changes: 3 additions & 0 deletions packages/compass-telemetry/src/growth-experiments.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export enum ExperimentTestName {
earlyJourneyIndexesGuidance = 'EARLY_JOURNEY_INDEXES_GUIDANCE_20250328',
mockDataGenerator = 'MOCK_DATA_GENERATOR_20251001',
atlasSkills = 'ATLAS_SKILLS_EXPERIMENT_20251007',
}

export enum ExperimentTestGroup {
mockDataGeneratorVariant = 'mockDataGeneratorVariant',
mockDataGeneratorControl = 'mockDataGeneratorControl',
atlasSkillsVariant = 'atlasSkillsVariant',
atlasSkillsControl = 'atlasSkillsControl',
}
3 changes: 3 additions & 0 deletions packages/compass-telemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export type {

export { CompassExperimentationProvider } from './experimentation-provider';
export { ExperimentTestName, ExperimentTestGroup } from './growth-experiments';

// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311
export { SkillsBannerContextEnum, useAtlasSkillsBanner } from './atlas-skills';
5 changes: 4 additions & 1 deletion packages/compass-telemetry/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,7 @@ export const useFireExperimentViewed = ({

export type { TrackFunction };
export { ExperimentTestName, ExperimentTestGroup } from './growth-experiments';
export { useAssignment } from './experimentation-provider';
export { useAssignment, useTrackInSample } from './experimentation-provider';

// @experiment Skills in Atlas | Jira Epic: CLOUDP-346311
export { SkillsBannerContextEnum, useAtlasSkillsBanner } from './atlas-skills';
Loading