From c9de30302e32a678a9207802ed199441162b91d5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 3 Apr 2023 09:09:42 +0200 Subject: [PATCH] feat(settings): Ensure JS Loader validates SDK version & products --- .../projectKeys/details/keySettings.spec.tsx | 171 ++++++++++++++++++ .../projectKeys/details/keySettings.tsx | 70 ++++++- 2 files changed, 235 insertions(+), 6 deletions(-) diff --git a/static/app/views/settings/project/projectKeys/details/keySettings.spec.tsx b/static/app/views/settings/project/projectKeys/details/keySettings.spec.tsx index c890bb9a659799..3ba98b42c20a56 100644 --- a/static/app/views/settings/project/projectKeys/details/keySettings.spec.tsx +++ b/static/app/views/settings/project/projectKeys/details/keySettings.spec.tsx @@ -16,6 +16,12 @@ const dynamicSdkLoaderOptions = { [DynamicSDKLoaderOption.HAS_DEBUG]: false, }; +const fullDynamicSdkLoaderOptions = { + [DynamicSDKLoaderOption.HAS_PERFORMANCE]: true, + [DynamicSDKLoaderOption.HAS_REPLAY]: true, + [DynamicSDKLoaderOption.HAS_DEBUG]: true, +}; + function renderMockRequests( organizationSlug: Organization['slug'], projectSlug: Project['slug'], @@ -120,5 +126,170 @@ describe('Key Settings', function () { ); }); }); + + it('resets performance & replay when selecting SDK version <7', async function () { + const params = { + projectId: '1', + keyId: '1', + }; + + const {organization} = initializeOrg({ + ...initializeOrg(), + organization: { + ...initializeOrg().organization, + features: ORG_FEATURES, + }, + router: { + params, + }, + }); + + const data = { + ...(TestStubs.ProjectKeys()[0] as ProjectKey), + dynamicSdkLoaderOptions: fullDynamicSdkLoaderOptions, + } as ProjectKey; + + const mockRequests = renderMockRequests( + organization.slug, + params.projectId, + params.keyId + ); + + render( + + ); + + // Update SDK version - should reset performance & replay + await selectEvent.select(screen.getByText('latest'), '6.x'); + + await waitFor(() => { + expect(mockRequests.projectKeys).toHaveBeenCalledWith( + `/projects/${organization.slug}/${params.projectId}/keys/${params.keyId}/`, + expect.objectContaining({ + data: { + browserSdkVersion: '6.x', + dynamicSdkLoaderOptions: { + ...fullDynamicSdkLoaderOptions, + [DynamicSDKLoaderOption.HAS_PERFORMANCE]: false, + [DynamicSDKLoaderOption.HAS_REPLAY]: false, + [DynamicSDKLoaderOption.HAS_DEBUG]: true, + }, + }, + }) + ); + }); + }); + + it('disabled performance & replay when SDK version <7 is selected', function () { + const params = { + projectId: '1', + keyId: '1', + }; + + const {organization} = initializeOrg({ + ...initializeOrg(), + organization: { + ...initializeOrg().organization, + features: ORG_FEATURES, + }, + router: { + params, + }, + }); + + const data = { + ...(TestStubs.ProjectKeys()[0] as ProjectKey), + dynamicSdkLoaderOptions: { + [DynamicSDKLoaderOption.HAS_PERFORMANCE]: false, + [DynamicSDKLoaderOption.HAS_REPLAY]: false, + [DynamicSDKLoaderOption.HAS_DEBUG]: true, + }, + browserSdkVersion: '6.x', + } as ProjectKey; + + render( + + ); + + for (const key of Object.keys(sdkLoaderOptions)) { + const toggle = screen.getByRole('checkbox', {name: sdkLoaderOptions[key].label}); + + if (key === DynamicSDKLoaderOption.HAS_DEBUG) { + expect(toggle).toBeEnabled(); + expect(toggle).toBeChecked(); + } else { + expect(toggle).toBeDisabled(); + expect(toggle).not.toBeChecked(); + } + } + + const infos = screen.getAllByText('Only available in SDK version 7.x and above'); + expect(infos.length).toBe(2); + }); + + it('shows replay message when it is enabled', function () { + const params = { + projectId: '1', + keyId: '1', + }; + + const {organization} = initializeOrg({ + ...initializeOrg(), + organization: { + ...initializeOrg().organization, + features: ORG_FEATURES, + }, + router: { + params, + }, + }); + + const data = { + ...(TestStubs.ProjectKeys()[0] as ProjectKey), + dynamicSdkLoaderOptions: fullDynamicSdkLoaderOptions, + } as ProjectKey; + + const {rerender} = render( + + ); + + expect( + screen.queryByText( + 'When using Replay, the loader will load the ES6 bundle instead of the ES5 bundle.' + ) + ).toBeInTheDocument(); + + data.dynamicSdkLoaderOptions.hasReplay = false; + + rerender( + + ); + + expect( + screen.queryByText( + 'When using Replay, the loader will load the ES6 bundle instead of the ES5 bundle.' + ) + ).not.toBeInTheDocument(); + }); }); }); diff --git a/static/app/views/settings/project/projectKeys/details/keySettings.tsx b/static/app/views/settings/project/projectKeys/details/keySettings.tsx index 151dbf8c48d253..c614fad5d8c29a 100644 --- a/static/app/views/settings/project/projectKeys/details/keySettings.tsx +++ b/static/app/views/settings/project/projectKeys/details/keySettings.tsx @@ -51,12 +51,15 @@ export enum DynamicSDKLoaderOption { export const sdkLoaderOptions = { [DynamicSDKLoaderOption.HAS_PERFORMANCE]: { label: t('Enable Performance Monitoring'), + requiresV7: true, }, [DynamicSDKLoaderOption.HAS_REPLAY]: { label: t('Enable Session Replay'), + requiresV7: true, }, [DynamicSDKLoaderOption.HAS_DEBUG]: { label: t('Enable Debug Bundles'), + requiresV7: false, }, }; @@ -145,16 +148,40 @@ export function KeySettings({onRemove, organization, params, data}: Props) { async (newBrowserSDKVersion: typeof browserSdkVersion) => { addLoadingMessage(); + const apiData: { + browserSdkVersion: typeof browserSdkVersion; + dynamicSdkLoaderOptions?: Partial>; + } = { + browserSdkVersion: newBrowserSDKVersion, + }; + + const shouldRestrictDynamicSdkLoaderOptions = + hasJSSDKDynamicLoaderFeatureFlag && + !sdkVersionSupportsPerformanceAndReplay(newBrowserSDKVersion); + + if (shouldRestrictDynamicSdkLoaderOptions) { + // Performance & Replay are not supported before 7.x + const newDynamicSdkLoaderOptions = { + ...dynamicSDKLoaderOptions, + hasPerformance: false, + hasReplay: false, + }; + + apiData.dynamicSdkLoaderOptions = newDynamicSdkLoaderOptions; + } + try { const response = await api.requestPromise(apiEndpoint, { method: 'PUT', - data: { - browserSdkVersion: newBrowserSDKVersion, - }, + data: apiData, }); setBrowserSdkVersion(response.browserSdkVersion); + if (shouldRestrictDynamicSdkLoaderOptions) { + setDynamicSDKLoaderOptions(response.dynamicSdkLoaderOptions); + } + addSuccessMessage(t('Successfully updated SDK version')); } catch (error) { const message = t('Unable to updated SDK version'); @@ -162,7 +189,14 @@ export function KeySettings({onRemove, organization, params, data}: Props) { addErrorMessage(message); } }, - [api, apiEndpoint, setBrowserSdkVersion] + [ + api, + apiEndpoint, + setBrowserSdkVersion, + setDynamicSDKLoaderOptions, + hasJSSDKDynamicLoaderFeatureFlag, + dynamicSDKLoaderOptions, + ] ); return ( @@ -267,14 +301,34 @@ export function KeySettings({onRemove, organization, params, data}: Props) { label={value.label} key={key} name={key} - value={dynamicSDKLoaderOptions[sdkLoaderOption]} + value={ + value.requiresV7 && + !sdkVersionSupportsPerformanceAndReplay(browserSdkVersion) + ? false + : dynamicSDKLoaderOptions[sdkLoaderOption] + } onChange={() => handleToggleDynamicSDKLoaderOption( sdkLoaderOption as DynamicSDKLoaderOption, !dynamicSDKLoaderOptions[sdkLoaderOption] ) } - disabled={!hasAccess} + disabled={ + !hasAccess || + (value.requiresV7 && + !sdkVersionSupportsPerformanceAndReplay(browserSdkVersion)) + } + help={ + value.requiresV7 && + !sdkVersionSupportsPerformanceAndReplay(browserSdkVersion) + ? t('Only available in SDK version 7.x and above') + : key === DynamicSDKLoaderOption.HAS_REPLAY && + dynamicSDKLoaderOptions[sdkLoaderOption] + ? t( + 'When using Replay, the loader will load the ES6 bundle instead of the ES5 bundle.' + ) + : undefined + } disabledReason={ !hasAccess ? t('You do not have permission to edit this setting') @@ -337,3 +391,7 @@ export function KeySettings({onRemove, organization, params, data}: Props) { ); } + +function sdkVersionSupportsPerformanceAndReplay(sdkVersion: string): boolean { + return sdkVersion === 'latest' || sdkVersion === '7.x'; +}