From 12c00733d96574acf9d8594b8f101729312ae0f4 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 23 Jun 2023 11:29:13 +0200 Subject: [PATCH] feat: count number of combinations from playground (#4077) This PR adds plausible tracking of the number of feature combinations that we get from the advanced playground API. The event type has been added to plausible Relates to #3545 --- frontend/package.json | 1 + .../AdvancedPlaygroundResultsTable.tsx | 12 +++ .../combinationCounter.test.ts | 97 +++++++++++++++++++ .../combinationCounter.ts | 12 +++ frontend/src/hooks/usePlausibleTracker.ts | 3 +- frontend/yarn.lock | 12 +++ 6 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.test.ts create mode 100644 frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.ts diff --git a/frontend/package.json b/frontend/package.json index 0c93175afe..c2eb5ae1ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,6 +62,7 @@ "@uiw/codemirror-theme-duotone": "4.21.3", "@uiw/react-codemirror": "4.21.1", "@vitejs/plugin-react": "3.1.0", + "cartesian": "^1.0.1", "chart.js": "3.9.1", "chartjs-adapter-date-fns": "3.0.0", "classnames": "2.3.2", diff --git a/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable.tsx b/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable.tsx index caed624b31..645500bbb1 100644 --- a/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable.tsx +++ b/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable.tsx @@ -36,6 +36,8 @@ import { } from 'openapi'; import { capitalizeFirst } from 'utils/capitalizeFirst'; import { AdvancedPlaygroundEnvironmentDiffCell } from './AdvancedPlaygroundEnvironmentCell/AdvancedPlaygroundEnvironmentDiffCell'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { countCombinations } from './combinationCounter'; const defaultSort: SortingRule = { id: 'name' }; const { value, setValue } = createLocalStorage( @@ -61,6 +63,16 @@ export const AdvancedPlaygroundResultsTable = ({ input, loading, }: IAdvancedPlaygroundResultsTableProps) => { + const { trackEvent } = usePlausibleTracker(); + if (features) { + trackEvent('playground', { + props: { + eventType: 'number-of-combinations', + count: countCombinations(features), + }, + }); + } + const [searchParams, setSearchParams] = useSearchParams(); const ref = useLoading(loading); const [searchValue, setSearchValue] = useState( diff --git a/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.test.ts b/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.test.ts new file mode 100644 index 0000000000..5214acad7f --- /dev/null +++ b/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.test.ts @@ -0,0 +1,97 @@ +import { countCombinations } from './combinationCounter'; +import { + AdvancedPlaygroundEnvironmentFeatureSchema, + AdvancedPlaygroundFeatureSchema, +} from 'openapi'; +// @ts-expect-error no types available +import cartesian from 'cartesian'; + +const generateFeature = ( + context: Record +): AdvancedPlaygroundEnvironmentFeatureSchema => ({ + isEnabled: false, + isEnabledInCurrentEnvironment: true, + variant: { + name: 'disabled', + enabled: false, + }, + context: { + appName: 'playground', + }, + variants: [], + name: 'default', + environment: 'development', + projectId: 'default', + strategies: { + result: false, + data: [ + { + name: 'default', + id: '7b233aae-cbc4-45ea-ace2-4c78c8e7e760', + disabled: false, + parameters: {}, + result: { + enabled: false, + evaluationStatus: 'complete' as 'complete', + }, + constraints: [ + { + inverted: false, + values: ['k'], + operator: 'IN', + contextName: 'appName', + caseInsensitive: false, + result: false, + }, + ], + segments: [], + links: { + edit: '/projects/default/features/default/strategies/edit?environmentId=development&strategyId=7b233aae-cbc4-45ea-ace2-4c78c8e7e760', + }, + }, + ], + }, +}); + +const generateInput = ( + featureCount: number, + environments: string[], + contextValues: { [key: string]: string[] } +): AdvancedPlaygroundFeatureSchema[] => { + const cartesianContext = cartesian(contextValues); + + return Array.from(Array(featureCount)).map((_, i) => ({ + name: `feature-${i}`, + projectId: 'default', + environments: Object.fromEntries( + environments.map(env => [ + env, + cartesianContext.map(generateFeature), + ]) + ), + })); +}; + +it('counts the correct number of combinations', () => { + const assertCount = ( + numberOfFeatures: number, + envs: string[], + context: { [k: string]: string[] } + ) => { + const totalCombinations = + numberOfFeatures * + envs.length * + Object.values(context) + .map(contextValues => contextValues.length) + .reduce((total, n) => total + n); + const input = generateInput(numberOfFeatures, envs, context); + expect(countCombinations(input)).toEqual(totalCombinations); + }; + + assertCount(1, ['development'], { x: ['2'] }); + assertCount(10, ['development', 'production'], { + x: ['1', '2'], + y: ['x', 'abc'], + }); + assertCount(5, ['development'], { x: ['1', '2'] }); +}); diff --git a/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.ts b/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.ts new file mode 100644 index 0000000000..6608f7d20b --- /dev/null +++ b/frontend/src/component/playground/Playground/AdvancedPlaygroundResultsTable/combinationCounter.ts @@ -0,0 +1,12 @@ +import { AdvancedPlaygroundFeatureSchema } from 'openapi'; + +export const countCombinations = ( + features: AdvancedPlaygroundFeatureSchema[] +) => + features.reduce( + (total, feature) => + total + + Object.values(feature.environments).flatMap(env => Object.keys(env)) + .length, + 0 + ); diff --git a/frontend/src/hooks/usePlausibleTracker.ts b/frontend/src/hooks/usePlausibleTracker.ts index 8c4b9eff42..9dc5c872f4 100644 --- a/frontend/src/hooks/usePlausibleTracker.ts +++ b/frontend/src/hooks/usePlausibleTracker.ts @@ -40,7 +40,8 @@ export type CustomEvents = | 'demo-open-demo-web' | 'context-usage' | 'segment-usage' - | 'strategy-add'; + | 'strategy-add' + | 'playground'; export const usePlausibleTracker = () => { const plausible = useContext(PlausibleContext); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index b0b4db4fcf..cf6c8e5fe1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3613,6 +3613,13 @@ caniuse-lite@^1.0.30001400: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001445.tgz#cf2d4eb93f2bcdf0310de9dd6d18be271bc0b447" integrity sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg== +cartesian@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cartesian/-/cartesian-1.0.1.tgz#ae3fc8a63e2ba7e2c4989ce696207457bcae65af" + integrity sha512-tR3qKRYpRJ6FXEGuoBwpuCYcwydrk1N2rduy7eWg1Msepi3i5fCxheryw4VBlCqjCbk3Vhjh3eg+IGHtl5H74A== + dependencies: + xtend "^4.0.1" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -9587,6 +9594,11 @@ xregexp@2.0.0: resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" integrity sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA== +xtend@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"