From e088337a0f51f19c34456e50810e4c7b900763ff Mon Sep 17 00:00:00 2001 From: Kyle W Lai Date: Tue, 21 Oct 2025 11:55:17 -0400 Subject: [PATCH 01/17] Add track events --- .../collection-header-actions.spec.tsx | 184 ++++++++++------- .../collection-header-actions.tsx | 36 +++- .../mock-data-generator-modal/constants.ts | 14 ++ .../document-count-screen.tsx | 5 + .../faker-mapping-selector.spec.tsx | 31 +++ .../faker-mapping-selector.tsx | 12 +- .../mock-data-generator-modal.spec.tsx | 194 +++++++++++++++++- .../mock-data-generator-modal.tsx | 30 ++- .../script-screen.tsx | 32 ++- .../compass-telemetry/src/telemetry-events.ts | 164 ++++++++++++++- 10 files changed, 616 insertions(+), 86 deletions(-) diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx index 7040b30c8ec..422cb699b04 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.spec.tsx @@ -3,6 +3,7 @@ import React, { type ComponentProps } from 'react'; import { renderWithActiveConnection, screen, + waitFor, } from '@mongodb-js/testing-library-compass'; import sinon from 'sinon'; import { @@ -229,94 +230,133 @@ describe('CollectionHeaderActions [Component]', function () { ); }); - it('should call onOpenMockDataModal when CTA button is clicked', async function () { - const onOpenMockDataModal = sinon.stub(); - - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', + context('when in the mock data generator treatment variant', function () { + beforeEach(function () { + mockUseAssignment.returns({ + assignment: { + assignmentData: { + variant: 'mockDataGeneratorVariant', + }, }, - }, + }); }); - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - onOpenMockDataModal, - }, - {}, - atlasConnectionInfo - ); + it('should send a track event when the button is viewed', async function () { + const result = await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + }, + {}, + atlasConnectionInfo + ); - const button = screen.getByTestId( - 'collection-header-generate-mock-data-button' - ); - button.click(); + await waitFor(() => { + expect(result.track).to.have.been.calledWith( + 'Mock Data Generator CTA Button Viewed', + { + button_enabled: true, + gen_ai_features_enabled: false, + send_sample_values_enabled: false, + } + ); + }); + }); - expect(onOpenMockDataModal).to.have.been.calledOnce; - }); + it('should call onOpenMockDataModal when CTA button is clicked', async function () { + const onOpenMockDataModal = sinon.stub(); - it('should disable button for deeply nested collections', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', // Treatment variant + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + onOpenMockDataModal, }, - }, + {}, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + button.click(); + + expect(onOpenMockDataModal).to.have.been.calledOnce; }); - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - hasSchemaAnalysisData: true, - analyzedSchemaDepth: 5, // Exceeds MAX_COLLECTION_NESTING_DEPTH (3) - schemaAnalysisStatus: 'complete', - onOpenMockDataModal: sinon.stub(), - }, - {}, - atlasConnectionInfo - ); + it('sends a track event when CTA button is clicked', async function () { + const onOpenMockDataModal = sinon.stub(); - const button = screen.getByTestId( - 'collection-header-generate-mock-data-button' - ); - expect(button).to.exist; - expect(button).to.have.attribute('aria-disabled', 'true'); - }); + const result = await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + onOpenMockDataModal, + }, + {}, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + button.click(); - it('should show an error banner when the schema is in an unsupported state', async function () { - mockUseAssignment.returns({ - assignment: { - assignmentData: { - variant: 'mockDataGeneratorVariant', + await waitFor(() => { + expect(result.track).to.have.been.calledWith( + 'Mock Data Generator Opened', + { + gen_ai_features_enabled: false, + send_sample_values_enabled: false, + } + ); + }); + }); + + it('should disable button for deeply nested collections', async function () { + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + hasSchemaAnalysisData: true, + analyzedSchemaDepth: 5, // Exceeds MAX_COLLECTION_NESTING_DEPTH (3) + schemaAnalysisStatus: 'complete', + onOpenMockDataModal: sinon.stub(), }, - }, + {}, + atlasConnectionInfo + ); + + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.exist; + expect(button).to.have.attribute('aria-disabled', 'true'); }); - await renderCollectionHeaderActions( - { - namespace: 'test.collection', - isReadonly: false, - hasSchemaAnalysisData: false, - schemaAnalysisStatus: 'error', - schemaAnalysisError: { - errorType: 'unsupportedState', - errorMessage: 'Unsupported state', + it('should show an error banner when the schema is in an unsupported state', async function () { + await renderCollectionHeaderActions( + { + namespace: 'test.collection', + isReadonly: false, + hasSchemaAnalysisData: false, + schemaAnalysisStatus: 'error', + schemaAnalysisError: { + errorType: 'unsupportedState', + errorMessage: 'Unsupported state', + }, + onOpenMockDataModal: sinon.stub(), }, - onOpenMockDataModal: sinon.stub(), - }, - {}, - atlasConnectionInfo - ); + {}, + atlasConnectionInfo + ); - const button = screen.getByTestId( - 'collection-header-generate-mock-data-button' - ); - expect(button).to.exist; - expect(button).to.have.attribute('aria-disabled', 'true'); + const button = screen.getByTestId( + 'collection-header-generate-mock-data-button' + ); + expect(button).to.exist; + expect(button).to.have.attribute('aria-disabled', 'true'); + }); }); }); }); diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx index 613ab6375e0..de25274b2b1 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx @@ -8,8 +8,12 @@ import { } from '@mongodb-js/compass-components'; import { useConnectionInfo } from '@mongodb-js/compass-connections/provider'; import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider'; -import React from 'react'; -import { usePreferences } from 'compass-preferences-model/provider'; +import React, { useEffect } from 'react'; +import { + useIsAIFeatureEnabled, + usePreference, + usePreferences, +} from 'compass-preferences-model/provider'; import toNS from 'mongodb-ns'; import { wrapField } from '@mongodb-js/mongodb-constants'; import { @@ -92,6 +96,10 @@ const CollectionHeaderActions: React.FunctionComponent< const { readWrite: preferencesReadWrite, enableShell: showOpenShellButton } = usePreferences(['readWrite', 'enableShell']); const track = useTelemetry(); + const isAIFeatureEnabled = useIsAIFeatureEnabled(); + const isSampleDocumentPassingEnabled = usePreference( + 'enableGenAISampleDocumentPassing' + ); // Get experiment assignment for Mock Data Generator const mockDataGeneratorAssignment = useAssignment( @@ -122,6 +130,26 @@ const CollectionHeaderActions: React.FunctionComponent< const isView = isReadonly && sourceName && !editViewName; const showViewEdit = isView && !preferencesReadWrite; + const shouldDisableMockDataButton = + !hasSchemaAnalysisData || exceedsMaxNestingDepth; + + const onMockDataGeneratorCtaButtonClicked = () => { + track('Mock Data Generator Opened', { + gen_ai_features_enabled: isAIFeatureEnabled, + send_sample_values_enabled: isSampleDocumentPassingEnabled, + }); + onOpenMockDataModal(); + }; + + useEffect(() => { + if (shouldShowMockDataButton) { + track('Mock Data Generator CTA Button Viewed', { + button_enabled: !shouldDisableMockDataButton, + gen_ai_features_enabled: isAIFeatureEnabled, + send_sample_values_enabled: isSampleDocumentPassingEnabled, + }); + } + }); return (
} > Generate Mock Data diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts index c5935479ee6..a4b6c2b56fe 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts @@ -9,6 +9,20 @@ export const StepButtonLabelMap = { [MockDataGeneratorStep.GENERATE_DATA]: 'Done', } as const; +// Map of the current mock data generator step to the next step or 'finish' if the user is on the last step. +// For the purposes of telemetry tracking the step progression in the modal. +export const MOCK_DATA_GENERATOR_STEP_TO_NEXT_STEP_MAP: Record< + MockDataGeneratorStep, + MockDataGeneratorStep | 'finish' +> = { + [MockDataGeneratorStep.SCHEMA_CONFIRMATION]: + MockDataGeneratorStep.SCHEMA_EDITOR, + [MockDataGeneratorStep.SCHEMA_EDITOR]: MockDataGeneratorStep.DOCUMENT_COUNT, + [MockDataGeneratorStep.DOCUMENT_COUNT]: MockDataGeneratorStep.PREVIEW_DATA, + [MockDataGeneratorStep.PREVIEW_DATA]: MockDataGeneratorStep.GENERATE_DATA, + [MockDataGeneratorStep.GENERATE_DATA]: 'finish', +}; + export const DEFAULT_DOCUMENT_COUNT = 1000; export const MAX_DOCUMENT_COUNT = 100000; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx index f17816f34c8..cae442c81a4 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/document-count-screen.tsx @@ -11,6 +11,7 @@ import { connect } from 'react-redux'; import type { CollectionState } from '../../modules/collection-tab'; import type { SchemaAnalysisState } from '../../schema-analysis-types'; import { DEFAULT_DOCUMENT_COUNT, MAX_DOCUMENT_COUNT } from './constants'; +import { useTelemetry } from '@mongodb-js/compass-telemetry/provider'; const BYTE_PRECISION_THRESHOLD = 1000; @@ -67,6 +68,7 @@ const DocumentCountScreen = ({ onDocumentCountChange, schemaAnalysisState, }: Props) => { + const track = useTelemetry(); const estimatedDiskSize = useMemo( () => schemaAnalysisState.status === 'complete' && @@ -98,6 +100,9 @@ const DocumentCountScreen = ({ const value = parseInt(event.target.value, 10); if (!isNaN(value)) { onDocumentCountChange(value); + track('Mock Data Document Count Changed', { + document_count: value, + }); } }; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx index 84d7c129603..4d03779cd23 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.spec.tsx @@ -122,6 +122,37 @@ describe('FakerMappingSelector', () => { ); }); + it('should fire a track event when faker function changes', async () => { + const newFakerMethod = 'internet.email'; + const result = render( + + ); + + const fakerFunctionSelect = screen.getByLabelText('Faker Function'); + userEvent.click(fakerFunctionSelect); + + const emailOption = await screen.findByRole('option', { + name: newFakerMethod, + }); + userEvent.click(emailOption); + + await waitFor(() => { + expect(result.track).to.have.been.calledWith( + 'Mock Data Faker Method Changed', + { + field_name: mockActiveJsonType, + new_method: newFakerMethod, + } + ); + }); + }); + it('should show warning banner when faker method is unrecognized', () => { render( { + const track = useTelemetry(); const fakerMethodOptions = useMemo(() => { const methods = MONGO_TYPE_TO_FAKER_METHODS[activeJsonType] || []; @@ -84,6 +86,14 @@ const FakerMappingSelector = ({ return [activeFakerFunction, ...methods]; }, [activeJsonType, activeFakerFunction]); + const onFakerMethodChange = (newMethod: string) => { + track('Mock Data Faker Method Changed', { + field_name: activeJsonType, + new_method: newMethod, + }); + onFakerFunctionSelect(newMethod); + }; + return (
Mapping @@ -103,7 +113,7 @@ const FakerMappingSelector = ({ label="Faker Function" allowDeselect={false} value={activeFakerFunction} - onChange={onFakerFunctionSelect} + onChange={(newMethod) => onFakerMethodChange(newMethod)} > {fakerMethodOptions.map((method) => (