Skip to content
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

Ensure sync-xhr is allowed before reload and profile #20879

Merged
merged 13 commits into from
Mar 11, 2021
Merged
23 changes: 14 additions & 9 deletions packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
RendererInterface,
} from './types';
import type {ComponentFilter} from '../types';
import {isSynchronousXHRSupported} from './utils';

const debug = (methodName, ...args) => {
if (__DEBUG__) {
Expand Down Expand Up @@ -492,16 +493,20 @@ export default class Agent extends EventEmitter<{|
};

reloadAndProfile = (recordChangeDescriptions: boolean) => {
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
sessionStorageSetItem(
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
recordChangeDescriptions ? 'true' : 'false',
);
if (isSynchronousXHRSupported()) {
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
sessionStorageSetItem(
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
recordChangeDescriptions ? 'true' : 'false',
);

// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
// The agent can't do this in a way that is renderer agnostic.
this._bridge.send('reloadAppForProfiling');
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
// The agent can't do this in a way that is renderer agnostic.
this._bridge.send('reloadAppForProfiling');
} else {
this._bridge.send('isSynchronousXHRSupported', false);
}
};

renamePath = ({
Expand Down
7 changes: 7 additions & 0 deletions packages/react-devtools-shared/src/backend/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,10 @@ export function format(

return '' + formatted;
}

export function isSynchronousXHRSupported() {
return (
window.document.featurePolicy &&
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
window.document.featurePolicy.allowsFeature('sync-xhr')
);
}
1 change: 1 addition & 0 deletions packages/react-devtools-shared/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export type BackendEvents = {|
syncSelectionFromNativeElementsPanel: [],
syncSelectionToNativeElementsPanel: [],
unsupportedRendererVersion: [RendererID],
isSynchronousXHRSupported: [boolean],

// React Native style editor plug-in.
isNativeStyleEditorSupported: [
Expand Down
17 changes: 17 additions & 0 deletions packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default class Store extends EventEmitter<{|
supportsProfiling: [],
supportsReloadAndProfile: [],
unsupportedRendererVersionDetected: [],
supportsSynchronousXHR: [],
|}> {
_bridge: FrontendBridge;

Expand Down Expand Up @@ -141,6 +142,9 @@ export default class Store extends EventEmitter<{|
_supportsReloadAndProfile: boolean = false;
_supportsTraceUpdates: boolean = false;

// Needed for reload and profile - assume supported until told otherwise
_supportsSynchronousXHR: boolean = true;

_unsupportedRendererVersionDetected: boolean = false;

// Total number of visible elements (within all roots).
Expand Down Expand Up @@ -205,6 +209,10 @@ export default class Store extends EventEmitter<{|
'unsupportedRendererVersion',
this.onBridgeUnsupportedRendererVersion,
);
bridge.addListener(
'isSynchronousXHRSupported',
this.onBridgeSynchronousXHRSupported,
);

this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
}
Expand Down Expand Up @@ -365,6 +373,9 @@ export default class Store extends EventEmitter<{|
// Both of these are required for the reload-and-profile feature to work.
return this._supportsReloadAndProfile && this._isBackendStorageAPISupported;
}
get supportsSynchronousXHR(): boolean {
return this._supportsSynchronousXHR;
}

get supportsTraceUpdates(): boolean {
return this._supportsTraceUpdates;
Expand Down Expand Up @@ -1149,4 +1160,10 @@ export default class Store extends EventEmitter<{|

this.emit('unsupportedRendererVersionDetected');
};

onBridgeSynchronousXHRSupported = (isSynchronousXHRSupported: boolean) => {
this._supportsSynchronousXHR = isSynchronousXHRSupported;

this.emit('supportsSynchronousXHR');
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
*/

import * as React from 'react';
import {useCallback, useContext, useMemo} from 'react';
import {useCallback, useContext, useMemo, useEffect} from 'react';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import {BridgeContext, StoreContext} from '../context';
import {useSubscription} from '../hooks';
import {ModalDialogContext} from '../ModalDialog';

type SubscriptionData = {|
recordChangeDescriptions: boolean,
supportsReloadAndProfile: boolean,
supportsSynchronousXHR: boolean,
|};

export default function ReloadAndProfileButton() {
Expand All @@ -28,13 +30,16 @@ export default function ReloadAndProfileButton() {
getCurrentValue: () => ({
recordChangeDescriptions: store.recordChangeDescriptions,
supportsReloadAndProfile: store.supportsReloadAndProfile,
supportsSynchronousXHR: store.supportsSynchronousXHR,
}),
subscribe: (callback: Function) => {
store.addListener('recordChangeDescriptions', callback);
store.addListener('supportsReloadAndProfile', callback);
store.addListener('supportsSynchronousXHR', callback);
return () => {
store.removeListener('recordChangeDescriptions', callback);
store.removeListener('supportsReloadAndProfile', callback);
store.removeListener('supportsSynchronousXHR', callback);
};
},
}),
Expand All @@ -43,8 +48,20 @@ export default function ReloadAndProfileButton() {
const {
recordChangeDescriptions,
supportsReloadAndProfile,
supportsSynchronousXHR,
} = useSubscription<SubscriptionData>(subscription);

const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
useEffect(() => {
if (!supportsSynchronousXHR) {
modalDialogDispatch({
type: 'SHOW',
content:
'Synchronous XHR is required for reload and profile but is disabled on this site.',
});
}
}, [supportsSynchronousXHR, modalDialogDispatch]);

bvaughn marked this conversation as resolved.
Show resolved Hide resolved
const reloadAndProfile = useCallback(() => {
// TODO If we want to support reload-and-profile for e.g. React Native,
// we might need to also start profiling here before reloading the app (since DevTools itself isn't reloaded).
Expand All @@ -61,7 +78,7 @@ export default function ReloadAndProfileButton() {

return (
<Button
disabled={!store.supportsProfiling}
disabled={!store.supportsProfiling || !supportsSynchronousXHR}
onClick={reloadAndProfile}
title="Reload and start profiling">
<ButtonIcon type="reload" />
Expand Down