Skip to content

Commit d2b8068

Browse files
authored
chore(editor): Gate CodeMirror editor with Firebase remote config (#2237)
* Integrate Firebase remote config * Add remote config to UI state * Add remote config to UI state * Track remote config in Mixpanel and Bugsnag * Fix lint * Add a default * Lazy-load both editors * Use remote config to choose editor implementation in console * Remove minimum fetch interval * Remove unused import
1 parent 92a6591 commit d2b8068

File tree

17 files changed

+114
-33
lines changed

17 files changed

+114
-33
lines changed

src/clients/firebase.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import get from 'lodash-es/get';
44
import isEmpty from 'lodash-es/isEmpty';
55
import isNil from 'lodash-es/isNil';
66
import isNull from 'lodash-es/isNull';
7+
import mapValues from 'lodash-es/mapValues';
78
import omit from 'lodash-es/omit';
89
import once from 'lodash-es/once';
910
import values from 'lodash-es/values';
1011
import {v4 as uuid} from 'uuid';
1112
import 'firebase/analytics';
1213
import 'firebase/auth';
1314
import 'firebase/performance';
15+
import 'firebase/remote-config';
1416

1517
import config from '../config';
1618
import {
@@ -35,7 +37,7 @@ for (const scope of GOOGLE_SCOPES) {
3537
googleAuthProvider.addScope(scope);
3638
}
3739

38-
const {auth, loadDatabase} = buildFirebase();
40+
const {auth, loadDatabase, remoteConfig} = buildFirebase();
3941

4042
async function loadDatabaseSdk() {
4143
return retryingFailedImports(() =>
@@ -61,8 +63,12 @@ function buildFirebase(appName = undefined) {
6163

6264
const rest =
6365
appName === undefined
64-
? {perf: firebase.performance(app), analytics: firebase.analytics()}
65-
: {perf: null, analytics: null};
66+
? {
67+
perf: firebase.performance(app),
68+
analytics: firebase.analytics(),
69+
remoteConfig: firebase.remoteConfig(),
70+
}
71+
: {perf: null, analytics: null, remoteConfig: null};
6672

6773
return {
6874
auth: firebase.auth(app),
@@ -284,3 +290,9 @@ export function setSessionUid() {
284290
});
285291
}
286292
}
293+
294+
export async function loadRemoteConfig() {
295+
await remoteConfig.fetchAndActivate();
296+
297+
return mapValues(remoteConfig.getAll(), value => value.asString());
298+
}

src/components/Console.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export default function Console({
1717
currentInputValue,
1818
currentProjectKey,
1919
history,
20-
isExperimental,
2120
isHidden,
2221
isOpen,
2322
isTextSizeLarge,
23+
useCodeMirror,
2424
onChange,
2525
onClearConsoleEntries,
2626
onConsoleClicked,
@@ -47,9 +47,9 @@ export default function Console({
4747
>
4848
<ConsoleInput
4949
currentInputValue={currentInputValue}
50-
isExperimental={isExperimental}
5150
isTextSizeLarge={isTextSizeLarge}
5251
requestedFocusedLine={requestedFocusedLine}
52+
useCodeMirror={useCodeMirror}
5353
onChange={onChange}
5454
onInput={onInput}
5555
onNextConsoleHistory={onNextConsoleHistory}
@@ -97,11 +97,11 @@ Console.propTypes = {
9797
currentInputValue: PropTypes.string.isRequired,
9898
currentProjectKey: PropTypes.string.isRequired,
9999
history: ImmutablePropTypes.iterable.isRequired,
100-
isExperimental: PropTypes.bool.isRequired,
101100
isHidden: PropTypes.bool.isRequired,
102101
isOpen: PropTypes.bool.isRequired,
103102
isTextSizeLarge: PropTypes.bool,
104103
requestedFocusedLine: PropTypes.instanceOf(EditorLocation),
104+
useCodeMirror: PropTypes.bool.isRequired,
105105
onChange: PropTypes.func.isRequired,
106106
onClearConsoleEntries: PropTypes.func.isRequired,
107107
onConsoleClicked: PropTypes.func.isRequired,

src/components/ConsoleInput.jsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
import PropTypes from 'prop-types';
22
import React, {lazy, Suspense} from 'react';
33

4-
import AceConsoleInput from './AceConsoleInput';
4+
const AceConsoleInput = lazy(() =>
5+
import(
6+
/* webpackChunkName: "editor-ace" */
7+
'./AceConsoleInput'
8+
),
9+
);
510

611
const CodeMirrorConsoleInput = lazy(() =>
712
import(
8-
/* webpackChunkName: "experimentalOnly" */
13+
/* webpackChunkName: "editor-codemirror" */
914
'./CodeMirrorConsoleInput'
1015
),
1116
);
1217

13-
export default function ConsoleInput({isExperimental, ...props}) {
14-
if (isExperimental) {
15-
return (
16-
<Suspense>
18+
export default function ConsoleInput({useCodeMirror, ...props}) {
19+
return (
20+
<Suspense>
21+
{useCodeMirror ? (
1722
<CodeMirrorConsoleInput {...props} />
18-
</Suspense>
19-
);
20-
}
21-
return <AceConsoleInput {...props} />;
23+
) : (
24+
<AceConsoleInput {...props} />
25+
)}
26+
</Suspense>
27+
);
2228
}
2329

2430
ConsoleInput.propTypes = {
25-
isExperimental: PropTypes.bool.isRequired,
31+
useCodeMirror: PropTypes.bool.isRequired,
2632
};

src/components/Editor.jsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import PropTypes from 'prop-types';
22
import React, {lazy} from 'react';
33

4-
import AceEditor from './AceEditor';
4+
const AceEditor = lazy(() =>
5+
import(
6+
/* webpackChunkName: "editor-ace" */
7+
'./AceEditor'
8+
),
9+
);
510

611
const CodeMirrorEditor = lazy(() =>
712
import(
8-
/* webpackChunkName: "experimentalOnly" */
13+
/* webpackChunkName: "editor-codemirror" */
914
'./CodeMirrorEditor'
1015
),
1116
);

src/components/EditorsColumn.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function EditorsColumn({
1717
errors,
1818
resizableFlexGrow,
1919
resizableFlexRefs,
20-
isExperimental,
20+
implementation,
2121
isFlexResizingSupported,
2222
isTextSizeLarge,
2323
requestedFocusedLine,
@@ -62,7 +62,7 @@ export default function EditorsColumn({
6262
requestedFocusedLine={requestedFocusedLine}
6363
source={currentProject.sources[language]}
6464
textSizeIsLarge={isTextSizeLarge}
65-
useCodeMirror={isExperimental}
65+
useCodeMirror={implementation === 'codemirror'}
6666
onAutoFormat={onAutoFormat}
6767
onInput={handleInputForLanguage(language)}
6868
onReady={partial(onEditorReady, language)}
@@ -98,7 +98,7 @@ export default function EditorsColumn({
9898
EditorsColumn.propTypes = {
9999
currentProject: PropTypes.object.isRequired,
100100
errors: PropTypes.object.isRequired,
101-
isExperimental: PropTypes.bool.isRequired,
101+
implementation: PropTypes.oneOf(['ace', 'codemirror']),
102102
isFlexResizingSupported: PropTypes.bool.isRequired,
103103
isTextSizeLarge: PropTypes.bool.isRequired,
104104
requestedFocusedLine: PropTypes.instanceOf(EditorLocation),
@@ -115,5 +115,6 @@ EditorsColumn.propTypes = {
115115
};
116116

117117
EditorsColumn.defaultProps = {
118+
implementation: 'ace',
118119
requestedFocusedLine: null,
119120
};

src/containers/Console.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import {
1717
getCurrentConsoleInputValue,
1818
getCurrentProjectKey,
1919
getHiddenUIComponents,
20+
getRemoteConfig,
2021
getRequestedFocusedLine,
2122
isCurrentProjectSyntacticallyValid,
22-
isExperimental,
2323
isTextSizeLarge,
2424
} from '../selectors';
2525

@@ -29,7 +29,7 @@ function mapStateToProps(state) {
2929
currentProjectKey: getCurrentProjectKey(state),
3030
history: getConsoleHistory(state),
3131
currentInputValue: getCurrentConsoleInputValue(state),
32-
isExperimental: isExperimental(state),
32+
useCodeMirror: getRemoteConfig(state).get('editor', 'ace') === 'codemirror',
3333
isOpen: !getHiddenUIComponents(state).includes('console'),
3434
isTextSizeLarge: isTextSizeLarge(state),
3535
requestedFocusedLine: getRequestedFocusedLine(state),

src/containers/EditorsColumn.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
getCurrentProject,
1515
getErrors,
1616
getHiddenAndVisibleLanguages,
17+
getRemoteConfig,
1718
getRequestedFocusedLine,
18-
isExperimental,
1919
isTextSizeLarge,
2020
} from '../selectors';
2121

@@ -24,7 +24,7 @@ function mapStateToProps(state) {
2424
return {
2525
currentProject: getCurrentProject(state),
2626
errors: getErrors(state),
27-
isExperimental: isExperimental(state),
27+
implementation: getRemoteConfig(state).get('editor', 'ace'),
2828
isTextSizeLarge: isTextSizeLarge(state),
2929
requestedFocusedLine: getRequestedFocusedLine(state),
3030
visibleLanguages,

src/init/__tests__/index.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {v4 as uuid} from 'uuid';
66
import init from '..';
77
import {firebaseProjectFactory} from '../../../__factories__/data/firebase';
88
import {applicationLoaded} from '../../actions';
9+
import {loadRemoteConfig} from '../../clients/firebase';
910
import {rehydrateProject} from '../../clients/localStorage';
1011
import config from '../../config';
1112
import createApplicationStore from '../../createApplicationStore';
@@ -103,5 +104,22 @@ describe('init()', () => {
103104
} = await dispatchedAction();
104105
expect(rehydratedProject).toBe(project);
105106
});
107+
108+
test('sends remote config parameters from remoteConfig', async () => {
109+
const remoteConfig = {color: 'orange'};
110+
loadRemoteConfig.mockReturnValue(remoteConfig);
111+
init();
112+
const {payload} = await dispatchedAction();
113+
expect(payload.remoteConfig).toEqual(remoteConfig);
114+
});
115+
116+
test('overrides remote config parameters from query string', async () => {
117+
history.pushState(null, '', '/?rco.color=blue');
118+
const remoteConfig = {color: 'orange', temperature: 'warm'};
119+
loadRemoteConfig.mockReturnValue(remoteConfig);
120+
init();
121+
const {payload} = await dispatchedAction();
122+
expect(payload.remoteConfig).toEqual({...remoteConfig, color: 'blue'});
123+
});
106124
});
107125
});

src/init/index.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import installDevTools from 'immutable-devtools';
33
import {install as installOfflinePlugin} from 'offline-plugin/runtime';
44

55
import {applicationLoaded} from '../actions';
6+
import {loadRemoteConfig} from '../clients/firebase';
67
import {rehydrateProject} from '../clients/localStorage';
78
import {initMixpanel} from '../clients/mixpanel';
89
import createApplicationStore from '../createApplicationStore';
@@ -12,18 +13,26 @@ import {getQueryParameters, setQueryParameters} from '../util/queryParams';
1213
import initI18n from './initI18n';
1314

1415
async function initApplication(store) {
15-
const {gistId, snapshotKey, isExperimental} = getQueryParameters(
16-
location.search,
17-
);
16+
const {
17+
gistId,
18+
snapshotKey,
19+
isExperimental,
20+
remoteConfig: remoteConfigOverrides,
21+
} = getQueryParameters(location.search);
1822
setQueryParameters({isExperimental});
1923
const rehydratedProject = rehydrateProject();
24+
const remoteConfig = await loadRemoteConfig();
2025

2126
store.dispatch(
2227
applicationLoaded({
2328
snapshotKey,
2429
gistId,
2530
isExperimental,
2631
rehydratedProject,
32+
remoteConfig: {
33+
...remoteConfig,
34+
...remoteConfigOverrides,
35+
},
2736
}),
2837
);
2938
}

src/logic/instrumentApplicationLoaded.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import {loadMixpanel} from '../clients/mixpanel';
66
export default createLogic({
77
type: applicationLoaded,
88

9-
async process({action: {payload: {isExperimental} = {}}}) {
9+
async process({action: {payload: {isExperimental, remoteConfig = {}} = {}}}) {
1010
const mixpanel = await loadMixpanel();
11-
mixpanel.register({
11+
const payload = {
1212
'Experimental Mode': Boolean(isExperimental),
13-
});
13+
};
14+
for (const [name, value] of Object.entries(remoteConfig)) {
15+
payload[`Remote Config: ${name}`] = value;
16+
}
17+
mixpanel.register(payload);
1418
},
1519
});

0 commit comments

Comments
 (0)