From 2a97e53a684e1ab8bc91fe0bba5f40f8c8a715e4 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 20:45:38 -0400 Subject: [PATCH 01/19] rename --- src/sentry/static/sentry/app/{api.jsx => api.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/sentry/static/sentry/app/{api.jsx => api.tsx} (100%) diff --git a/src/sentry/static/sentry/app/api.jsx b/src/sentry/static/sentry/app/api.tsx similarity index 100% rename from src/sentry/static/sentry/app/api.jsx rename to src/sentry/static/sentry/app/api.tsx From de0e5b2f25534ac035884edb6b68ebcf3ecf8fb1 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 25 Jul 2019 20:51:45 -0400 Subject: [PATCH 02/19] type api.tsx --- src/sentry/static/sentry/app/api.tsx | 156 ++++++++++++++++++++------- 1 file changed, 120 insertions(+), 36 deletions(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 2cf6e0d9c890dd..960ce9634c3dd3 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -1,4 +1,4 @@ -import {isUndefined, isNil, get} from 'lodash'; +import {isUndefined, isNil, get, isFunction} from 'lodash'; import $ from 'jquery'; import * as Sentry from '@sentry/browser'; @@ -14,7 +14,10 @@ import GroupActions from 'app/actions/groupActions'; import createRequestError from 'app/utils/requestError/createRequestError'; export class Request { - constructor(xhr) { + alive: boolean; + xhr: JQueryXHR; + + constructor(xhr: JQueryXHR) { this.xhr = xhr; this.alive = true; } @@ -26,12 +29,35 @@ export class Request { } } +type ParamsType = { + itemIds?: Array; + query?: string; + environment?: string | null; + project?: Array | null; +}; + +type QueryArgs = + | { + query: string; + environment?: string; + project?: Array; + } + | { + id: Array; + environment?: string; + project?: Array; + } + | { + environment?: string; + project?: Array; + }; + /** * Converts input parameters to API-compatible query arguments * @param params */ -export function paramsToQueryArgs(params) { - const p = params.itemIds +export function paramsToQueryArgs(params: ParamsType): QueryArgs { + const p: QueryArgs = params.itemIds ? {id: params.itemIds} // items matching array of itemids : params.query ? {query: params.query} // items matching search query @@ -58,7 +84,28 @@ export function paramsToQueryArgs(params) { return p; } +// TODO: move this somewhere +type APIRequestMethod = 'POST' | 'GET' | 'DELETE' | 'PUT'; + +type FunctionCallback = (...args: any[]) => void; + +type RequestCallbacks = { + success?: (data: any, textStatus?: string, xhr?: JQueryXHR) => void; + complete?: FunctionCallback; + error?: FunctionCallback; +}; + +type RequestOptions = { + method?: APIRequestMethod; + data?: any; + query?: Array | object; + preservedError?: Error; +} & RequestCallbacks; + export class Client { + baseUrl: string; + activeRequests: {[ids: string]: Request}; + constructor(options) { if (isUndefined(options)) { options = {}; @@ -71,7 +118,7 @@ export class Client { * Check if the API response says project has been renamed. * If so, redirect user to new project slug */ - hasProjectBeenRenamed(response) { + hasProjectBeenRenamed(response: JQueryXHR) { const code = get(response, 'responseJSON.detail.code'); // XXX(billy): This actually will never happen because we can't intercept the 302 @@ -86,7 +133,7 @@ export class Client { return true; } - wrapCallback(id, func, cleanup) { + wrapCallback(id: string, func: FunctionCallback | undefined, cleanup: boolean = false) { return (...args) => { const req = this.activeRequests[id]; if (cleanup === true) { @@ -96,6 +143,7 @@ export class Client { if (req && req.alive) { // Check if API response is a 302 -- means project slug was renamed and user // needs to be redirected + // @ts-ignore if (this.hasProjectBeenRenamed(...args)) { return; } @@ -113,13 +161,22 @@ export class Client { /** * Attempt to cancel all active XHR requests */ - clear() { + clear(): void { for (const id in this.activeRequests) { this.activeRequests[id].cancel(); } } - handleRequestError({id, path, requestOptions}, response, ...responseArgs) { + handleRequestError( + { + id, + path, + requestOptions, + }: {id: string; path: string; requestOptions: Readonly}, + response: JQueryXHR, + textStatus: string, + errorThrown: string + ) { const code = get(response, 'responseJSON.detail.code'); const isSudoRequired = code === SUDO_REQUIRED || code === SUPERUSER_REQUIRED; @@ -129,18 +186,18 @@ export class Client { sudo: code === SUDO_REQUIRED, retryRequest: () => { return this.requestPromise(path, requestOptions) - .then((...args) => { + .then((data: any) => { if (typeof requestOptions.success !== 'function') { return; } - requestOptions.success(...args); + requestOptions.success(data); }) - .catch((...args) => { + .catch(err => { if (typeof requestOptions.error !== 'function') { return; } - requestOptions.error(...args); + requestOptions.error(err); }); }, onClose: () => { @@ -159,10 +216,10 @@ export class Client { if (typeof errorCb !== 'function') { return; } - errorCb(response, ...responseArgs); + errorCb(response, textStatus, errorThrown); } - request(path, options = {}) { + request(path: string, options: Readonly = {}) { const method = options.method || (options.data ? 'POST' : 'GET'); let data = options.data; @@ -182,10 +239,10 @@ export class Client { throw err; } - const id = uniqueId(); + const id: string = uniqueId(); metric.mark(`api-request-start-${id}`); - let fullUrl; + let fullUrl: string; if (path.indexOf(this.baseUrl) === -1) { fullUrl = this.baseUrl + path; } else { @@ -218,8 +275,7 @@ export class Client { headers: { Accept: 'application/json; charset=utf-8', }, - success: (...args) => { - const [, , xhr] = args || []; + success: (responseData: any, textStatus: string, xhr: JQueryXHR) => { metric.measure({ name: 'app.api.request-success', start: `api-request-start-${id}`, @@ -228,11 +284,10 @@ export class Client { }, }); if (!isUndefined(options.success)) { - this.wrapCallback(id, options.success)(...args); + this.wrapCallback(id, options.success)(responseData, textStatus, xhr); } }, - error: (...args) => { - const [resp] = args || []; + error: (resp: JQueryXHR, textStatus: string, errorThrown: string) => { metric.measure({ name: 'app.api.request-error', start: `api-request-start-${id}`, @@ -248,15 +303,15 @@ export class Client { const errorObjectToUse = createRequestError( resp, preservedError.stack, - options.method, + method, path ); errorObjectToUse.removeFrames(2); // Setting this to warning because we are going to capture all failed requests - scope.setLevel('warning'); - scope.setTag('http.statusCode', resp.status); + scope.setLevel(Sentry.Severity.Warning); + scope.setTag('http.statusCode', String(resp.status)); Sentry.captureException(errorObjectToUse); }); @@ -266,12 +321,14 @@ export class Client { path, requestOptions: options, }, - ...args + resp, + textStatus, + errorThrown ); }, - complete: (...args) => { + complete: (jqXHR: JQueryXHR, textStatus: string) => { Sentry.finishSpan(requestSpan); - return this.wrapCallback(id, options.complete, true)(...args); + return this.wrapCallback(id, options.complete, true)(jqXHR, textStatus); }, }) ); @@ -279,7 +336,13 @@ export class Client { return this.activeRequests[id]; } - requestPromise(path, {includeAllArgs, ...options} = {}) { + requestPromise( + path: string, + { + includeAllArgs, + ...options + }: {includeAllArgs?: boolean} & Readonly = {} + ) { // Create an error object here before we make any async calls so // that we have a helpful stacktrace if it errors // @@ -295,7 +358,7 @@ export class Client { success: (data, ...args) => { includeAllArgs ? resolve([data, ...args]) : resolve(data); }, - error: (resp, ...args) => { + error: (resp: JQueryXHR) => { const errorObjectToUse = createRequestError( resp, preservedError.stack, @@ -312,16 +375,20 @@ export class Client { }); } - _chain(...funcs) { - funcs = funcs.filter(f => !isUndefined(f) && f); + _chain(...funcs: Array<((...args: any[]) => any) | undefined>) { + const filteredFuncs = funcs.filter( + (f): f is (...args: any[]) => any => { + return isFunction(f); + } + ); return (...args) => { - funcs.forEach(func => { + filteredFuncs.forEach(func => { func.apply(funcs, args); }); }; } - _wrapRequest(path, options, extraParams) { + _wrapRequest(path: string, options: RequestOptions, extraParams: RequestCallbacks) { if (isUndefined(extraParams)) { extraParams = {}; } @@ -333,7 +400,10 @@ export class Client { return this.request(path, options); } - bulkDelete(params, options) { + bulkDelete( + params: ParamsType & {orgId: string; projectId?: string}, + options: RequestCallbacks + ) { const path = params.projectId ? `/projects/${params.orgId}/${params.projectId}/issues/` : `/organizations/${params.orgId}/issues/`; @@ -359,7 +429,15 @@ export class Client { ); } - bulkUpdate(params, options) { + bulkUpdate( + params: ParamsType & { + orgId: string; + projectId?: string; + failSilently?: boolean; + data?: any; + }, + options: RequestCallbacks + ) { const path = params.projectId ? `/projects/${params.orgId}/${params.projectId}/issues/` : `/organizations/${params.orgId}/issues/`; @@ -386,7 +464,13 @@ export class Client { ); } - merge(params, options) { + merge( + params: ParamsType & { + orgId: string; + projectId?: string; + }, + options: RequestCallbacks + ) { const path = params.projectId ? `/projects/${params.orgId}/${params.projectId}/issues/` : `/organizations/${params.orgId}/issues/`; From 0d7427a1dfe503e4470ae316cf7efc42c58e15bb Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 16:08:15 -0400 Subject: [PATCH 03/19] first pass on generics stuffs for wrapCallback --- src/sentry/static/sentry/app/api.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 960ce9634c3dd3..bc86fb376e05d1 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -87,7 +87,7 @@ export function paramsToQueryArgs(params: ParamsType): QueryArgs { // TODO: move this somewhere type APIRequestMethod = 'POST' | 'GET' | 'DELETE' | 'PUT'; -type FunctionCallback = (...args: any[]) => void; +type FunctionCallback = (...args: Args) => void; type RequestCallbacks = { success?: (data: any, textStatus?: string, xhr?: JQueryXHR) => void; @@ -133,8 +133,12 @@ export class Client { return true; } - wrapCallback(id: string, func: FunctionCallback | undefined, cleanup: boolean = false) { - return (...args) => { + wrapCallback( + id: string, + func: FunctionCallback | undefined, + cleanup: boolean = false + ) { + return (...args: T) => { const req = this.activeRequests[id]; if (cleanup === true) { delete this.activeRequests[id]; @@ -211,8 +215,10 @@ export class Client { return; } + type ErrorCallbackArgs = [JQueryXHR, string, string]; + // Call normal error callback - const errorCb = this.wrapCallback(id, requestOptions.error); + const errorCb = this.wrapCallback(id, requestOptions.error); if (typeof errorCb !== 'function') { return; } From 260b064d1515c58e457b1942cac70505f7096d7c Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 17:07:54 -0400 Subject: [PATCH 04/19] default initializer for Client --- src/sentry/static/sentry/app/api.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index bc86fb376e05d1..377c0f3b824506 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -106,7 +106,7 @@ export class Client { baseUrl: string; activeRequests: {[ids: string]: Request}; - constructor(options) { + constructor(options: {baseUrl?: string} = {}) { if (isUndefined(options)) { options = {}; } From c3886727b1f78dca5f83ad86c84b5d3bf65137eb Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 17:53:19 -0400 Subject: [PATCH 05/19] use conditional types --- src/sentry/static/sentry/app/api.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 377c0f3b824506..64430500e26ab9 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -342,13 +342,19 @@ export class Client { return this.activeRequests[id]; } - requestPromise( + // Promise<[any, string | undefined, JQueryXHR | undefined]> + + requestPromise( path: string, { includeAllArgs, ...options - }: {includeAllArgs?: boolean} & Readonly = {} - ) { + }: {includeAllArgs?: IncludeAllArgsType} & Readonly = {} + ): Promise< + IncludeAllArgsType extends true + ? [any, string | undefined, JQueryXHR | undefined] + : any + > { // Create an error object here before we make any async calls so // that we have a helpful stacktrace if it errors // @@ -361,8 +367,8 @@ export class Client { this.request(path, { ...options, preservedError, - success: (data, ...args) => { - includeAllArgs ? resolve([data, ...args]) : resolve(data); + success: (data, textStatus, xhr) => { + includeAllArgs ? resolve([data, textStatus, xhr] as any) : resolve(data); }, error: (resp: JQueryXHR) => { const errorObjectToUse = createRequestError( From b1a80fce5bca50c0974e5b75e6b79708f05e7538 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 20:19:09 -0400 Subject: [PATCH 06/19] stricter types --- src/sentry/static/sentry/app/api.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 64430500e26ab9..10398626ddcd08 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -91,7 +91,7 @@ type FunctionCallback = (...args: Args) => void; type RequestCallbacks = { success?: (data: any, textStatus?: string, xhr?: JQueryXHR) => void; - complete?: FunctionCallback; + complete?: (jqXHR: JQueryXHR, textStatus: string) => void; error?: FunctionCallback; }; @@ -190,7 +190,7 @@ export class Client { sudo: code === SUDO_REQUIRED, retryRequest: () => { return this.requestPromise(path, requestOptions) - .then((data: any) => { + .then(data => { if (typeof requestOptions.success !== 'function') { return; } @@ -290,7 +290,11 @@ export class Client { }, }); if (!isUndefined(options.success)) { - this.wrapCallback(id, options.success)(responseData, textStatus, xhr); + this.wrapCallback<[any, string, JQueryXHR]>(id, options.success)( + responseData, + textStatus, + xhr + ); } }, error: (resp: JQueryXHR, textStatus: string, errorThrown: string) => { @@ -334,7 +338,10 @@ export class Client { }, complete: (jqXHR: JQueryXHR, textStatus: string) => { Sentry.finishSpan(requestSpan); - return this.wrapCallback(id, options.complete, true)(jqXHR, textStatus); + return this.wrapCallback<[JQueryXHR, string]>(id, options.complete, true)( + jqXHR, + textStatus + ); }, }) ); @@ -387,13 +394,13 @@ export class Client { }); } - _chain(...funcs: Array<((...args: any[]) => any) | undefined>) { + _chain(...funcs: Array<((...args: Args) => any) | undefined>) { const filteredFuncs = funcs.filter( - (f): f is (...args: any[]) => any => { + (f): f is (...args: Args) => any => { return isFunction(f); } ); - return (...args) => { + return (...args: Args) => { filteredFuncs.forEach(func => { func.apply(funcs, args); }); From 3c17ba9059a5e93bef2b9a8d61adb4ea0c5ef747 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 20:33:25 -0400 Subject: [PATCH 07/19] more type hints --- src/sentry/static/sentry/app/api.tsx | 45 +++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 10398626ddcd08..e35d822fe1412e 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -215,17 +215,18 @@ export class Client { return; } - type ErrorCallbackArgs = [JQueryXHR, string, string]; - // Call normal error callback - const errorCb = this.wrapCallback(id, requestOptions.error); + const errorCb = this.wrapCallback<[JQueryXHR, string, string]>( + id, + requestOptions.error + ); if (typeof errorCb !== 'function') { return; } errorCb(response, textStatus, errorThrown); } - request(path: string, options: Readonly = {}) { + request(path: string, options: Readonly = {}): Request { const method = options.method || (options.data ? 'POST' : 'GET'); let data = options.data; @@ -233,7 +234,7 @@ export class Client { data = JSON.stringify(data); } - let query; + let query: string; try { query = $.param(options.query || [], true); } catch (err) { @@ -349,8 +350,6 @@ export class Client { return this.activeRequests[id]; } - // Promise<[any, string | undefined, JQueryXHR | undefined]> - requestPromise( path: string, { @@ -400,14 +399,18 @@ export class Client { return isFunction(f); } ); - return (...args: Args) => { + return (...args: Args): void => { filteredFuncs.forEach(func => { func.apply(funcs, args); }); }; } - _wrapRequest(path: string, options: RequestOptions, extraParams: RequestCallbacks) { + _wrapRequest( + path: string, + options: RequestOptions, + extraParams: RequestCallbacks + ): Request { if (isUndefined(extraParams)) { extraParams = {}; } @@ -422,13 +425,13 @@ export class Client { bulkDelete( params: ParamsType & {orgId: string; projectId?: string}, options: RequestCallbacks - ) { - const path = params.projectId + ): Request { + const path: string = params.projectId ? `/projects/${params.orgId}/${params.projectId}/issues/` : `/organizations/${params.orgId}/issues/`; - const query = paramsToQueryArgs(params); - const id = uniqueId(); + const query: QueryArgs = paramsToQueryArgs(params); + const id: string = uniqueId(); GroupActions.delete(id, params.itemIds); @@ -456,13 +459,13 @@ export class Client { data?: any; }, options: RequestCallbacks - ) { - const path = params.projectId + ): Request { + const path: string = params.projectId ? `/projects/${params.orgId}/${params.projectId}/issues/` : `/organizations/${params.orgId}/issues/`; - const query = paramsToQueryArgs(params); - const id = uniqueId(); + const query: QueryArgs = paramsToQueryArgs(params); + const id: string = uniqueId(); GroupActions.update(id, params.itemIds, params.data); @@ -489,13 +492,13 @@ export class Client { projectId?: string; }, options: RequestCallbacks - ) { - const path = params.projectId + ): Request { + const path: string = params.projectId ? `/projects/${params.orgId}/${params.projectId}/issues/` : `/organizations/${params.orgId}/issues/`; - const query = paramsToQueryArgs(params); - const id = uniqueId(); + const query: QueryArgs = paramsToQueryArgs(params); + const id: string = uniqueId(); GroupActions.merge(id, params.itemIds); From c95e5f9598915d9abad6f6102d9b39e1ed78aec3 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 20:39:34 -0400 Subject: [PATCH 08/19] sdfasf --- src/sentry/static/sentry/app/api.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index e35d822fe1412e..6838d0c6e3f8cc 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -92,6 +92,7 @@ type FunctionCallback = (...args: Args) => void; type RequestCallbacks = { success?: (data: any, textStatus?: string, xhr?: JQueryXHR) => void; complete?: (jqXHR: JQueryXHR, textStatus: string) => void; + // TODO: refine this type later error?: FunctionCallback; }; @@ -118,6 +119,7 @@ export class Client { * Check if the API response says project has been renamed. * If so, redirect user to new project slug */ + // TODO: refine this type later hasProjectBeenRenamed(response: JQueryXHR) { const code = get(response, 'responseJSON.detail.code'); From ae2503f68997592dfc7ae2249d5c07c03ffd52a2 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Mon, 29 Jul 2019 20:44:14 -0400 Subject: [PATCH 09/19] people dont like lodash around here --- src/sentry/static/sentry/app/api.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 6838d0c6e3f8cc..5d707932d78e50 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -1,4 +1,4 @@ -import {isUndefined, isNil, get, isFunction} from 'lodash'; +import {isUndefined, isNil, get} from 'lodash'; import $ from 'jquery'; import * as Sentry from '@sentry/browser'; @@ -398,7 +398,7 @@ export class Client { _chain(...funcs: Array<((...args: Args) => any) | undefined>) { const filteredFuncs = funcs.filter( (f): f is (...args: Args) => any => { - return isFunction(f); + return typeof f === 'function'; } ); return (...args: Args): void => { From 11ddab18ba3ed6ed05fc79af9b515d30f6b2bd4e Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 8 Aug 2019 22:08:34 -0400 Subject: [PATCH 10/19] update todo --- src/sentry/static/sentry/app/api.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index 5d707932d78e50..c884bb74d2324e 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -92,7 +92,7 @@ type FunctionCallback = (...args: Args) => void; type RequestCallbacks = { success?: (data: any, textStatus?: string, xhr?: JQueryXHR) => void; complete?: (jqXHR: JQueryXHR, textStatus: string) => void; - // TODO: refine this type later + // TODO(ts): Update this when sentry is mostly migrated to TS error?: FunctionCallback; }; From ef257be0af5355fdedeb69d236c39b0936221df0 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 8 Aug 2019 23:31:48 -0400 Subject: [PATCH 11/19] attempt to type mocks --- package.json | 1 + .../sentry/app/__mocks__/{api.jsx => api.tsx} | 92 +++++++++++++------ src/sentry/static/sentry/app/api.tsx | 2 +- yarn.lock | 12 +++ 4 files changed, 80 insertions(+), 27 deletions(-) rename src/sentry/static/sentry/app/__mocks__/{api.jsx => api.tsx} (57%) diff --git a/package.json b/package.json index f55b7a9565f6b9..93adda6c878fe5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@sentry/integrations": "5.6.0-beta.4", "@types/echarts": "^4.1.10", "@types/classnames": "^2.2.0", + "@types/jest": "^24.0.17", "@types/jquery": "^2.0.53", "@types/lodash": "^4.14.134", "@types/moment-timezone": "^0.5.12", diff --git a/src/sentry/static/sentry/app/__mocks__/api.jsx b/src/sentry/static/sentry/app/__mocks__/api.tsx similarity index 57% rename from src/sentry/static/sentry/app/__mocks__/api.jsx rename to src/sentry/static/sentry/app/__mocks__/api.tsx index 67f0258bcfd9e3..78780a9559f3e6 100644 --- a/src/sentry/static/sentry/app/__mocks__/api.jsx +++ b/src/sentry/static/sentry/app/__mocks__/api.tsx @@ -1,8 +1,9 @@ -const RealClient = require.requireActual('app/api'); +import * as ImportedClient from 'app/api'; +const RealClient: typeof ImportedClient = jest.requireActual('app/api'); export class Request {} -const respond = (isAsync, fn, ...args) => { +const respond = (isAsync: boolean, fn, ...args): void => { if (fn) { if (isAsync) { setTimeout(() => fn(...args), 1); @@ -16,8 +17,23 @@ const DEFAULT_MOCK_RESPONSE_OPTIONS = { predicate: () => true, }; +type ResponseType = JQueryXHR & { + url: string; + statusCode: number; + method: string; + callCount: 0; + body: any; + headers: {[key: string]: string}; +}; + class Client { - static mockResponses = []; + static mockResponses: Array< + [ + ResponseType, + jest.Mock, + (url: string, options: Readonly) => boolean + ] + > = []; static clearMockResponses() { Client.mockResponses = []; @@ -42,8 +58,8 @@ class Client { return mock; } - static findMockResponse(url, options) { - return Client.mockResponses.find(([response, mock, predicate]) => { + static findMockResponse(url: string, options: Readonly) { + return Client.mockResponses.find(([response, _mock, predicate]) => { const matchesURL = url === response.url; const matchesMethod = (options.method || 'GET') === response.method; const matchesPredicate = predicate(url, options); @@ -61,8 +77,9 @@ class Client { static mockAsync = false; - wrapCallback(id, error) { + wrapCallback(_id, error) { return (...args) => { + // @ts-ignore if (this.hasProjectBeenRenamed(...args)) { return; } @@ -70,24 +87,33 @@ class Client { }; } - requestPromise(path, {includeAllArgs, ...options} = {}) { + requestPromise( + path, + { + includeAllArgs, + ...options + }: {includeAllArgs?: boolean} & Readonly = {} + ) { return new Promise((resolve, reject) => { this.request(path, { ...options, success: (data, ...args) => { includeAllArgs ? resolve([data, ...args]) : resolve(data); }, - error: (error, ...args) => { + error: (error, ..._args) => { reject(error); }, }); }); } - request(url, options) { - const [response, mock] = Client.findMockResponse(url, options) || []; + request(url, options: Readonly = {}) { + const [response, mock] = Client.findMockResponse(url, options) || [ + undefined, + undefined, + ]; - if (!response) { + if (!response || !mock) { // Endpoints need to be mocked throw new Error( `No mocked response found for request:\n\t${options.method || 'GET'} ${url}` @@ -103,17 +129,33 @@ class Client { if (response.statusCode !== 200) { response.callCount++; - const resp = { - status: response.statusCode, - responseText: JSON.stringify(body), - responseJSON: body, - }; + + const deferred = $.Deferred(); + + const errorResponse: JQueryXHR = Object.assign( + { + status: response.statusCode, + responseText: JSON.stringify(body), + responseJSON: body, + }, + { + overrideMimeType: () => {}, + abort: () => {}, + then: () => {}, + error: () => {}, + }, + deferred, + new XMLHttpRequest() + ); this.handleRequestError( { + id: '1234', path: url, requestOptions: options, }, - resp + errorResponse, + 'error', + 'error' ); } else { response.callCount++; @@ -131,15 +173,13 @@ class Client { respond(Client.mockAsync, options.complete); } -} -Client.prototype.handleRequestError = RealClient.Client.prototype.handleRequestError; -Client.prototype.uniqueId = RealClient.Client.prototype.uniqueId; -Client.prototype.bulkUpdate = RealClient.Client.prototype.bulkUpdate; -Client.prototype._chain = RealClient.Client.prototype._chain; -Client.prototype._wrapRequest = RealClient.Client.prototype._wrapRequest; -Client.prototype.hasProjectBeenRenamed = - RealClient.Client.prototype.hasProjectBeenRenamed; -Client.prototype.merge = RealClient.Client.prototype.merge; + hasProjectBeenRenamed = RealClient.Client.prototype.hasProjectBeenRenamed; + handleRequestError = RealClient.Client.prototype.handleRequestError; + bulkUpdate = RealClient.Client.prototype.bulkUpdate; + _chain = RealClient.Client.prototype._chain; + _wrapRequest = RealClient.Client.prototype._wrapRequest; + merge = RealClient.Client.prototype.merge; +} export {Client}; diff --git a/src/sentry/static/sentry/app/api.tsx b/src/sentry/static/sentry/app/api.tsx index c884bb74d2324e..75e399042cde99 100644 --- a/src/sentry/static/sentry/app/api.tsx +++ b/src/sentry/static/sentry/app/api.tsx @@ -96,7 +96,7 @@ type RequestCallbacks = { error?: FunctionCallback; }; -type RequestOptions = { +export type RequestOptions = { method?: APIRequestMethod; data?: any; query?: Array | object; diff --git a/yarn.lock b/yarn.lock index 8284b9c04fca7e..ca8c2d2ecde5c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1755,6 +1755,18 @@ resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.29.tgz#aa845204cd0a289f65d47e0de63a6a815e30cc66" integrity sha512-lRVw09gOvgviOfeUrKc/pmTiRZ7g7oDOU6OAutyuSHpm1/o2RaBQvRhgK8QEdu+FFuw/wnWb29A/iuxv9i8OpQ== +"@types/jest-diff@*": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" + integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== + +"@types/jest@^24.0.17": + version "24.0.17" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.17.tgz#b66ea026efb746eb5db1356ee28518aaff7af416" + integrity sha512-1cy3xkOAfSYn78dsBWy4M3h/QF/HeWPchNFDjysVtp3GHeTdSmtluNnELfCmfNRRHo0OWEcpf+NsEJQvwQfdqQ== + dependencies: + "@types/jest-diff" "*" + "@types/jquery@^2.0.53": version "2.0.53" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.53.tgz#89c53bc83d820e50c3b667ae2fdf4276df8b3aba" From 9756a428881f9765a1709cd1265059f0a17f214f Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Fri, 9 Aug 2019 11:05:42 -0400 Subject: [PATCH 12/19] lint fix --- src/sentry/static/sentry/app/__mocks__/api.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/static/sentry/app/__mocks__/api.tsx b/src/sentry/static/sentry/app/__mocks__/api.tsx index 78780a9559f3e6..683a7e90260807 100644 --- a/src/sentry/static/sentry/app/__mocks__/api.tsx +++ b/src/sentry/static/sentry/app/__mocks__/api.tsx @@ -1,4 +1,5 @@ import * as ImportedClient from 'app/api'; + const RealClient: typeof ImportedClient = jest.requireActual('app/api'); export class Request {} From d5617a537277f8cc0b64f50722e246dfbfc32376 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Fri, 9 Aug 2019 11:46:44 -0400 Subject: [PATCH 13/19] fix mocks --- src/sentry/static/sentry/app/__mocks__/api.tsx | 11 +++++++++-- tests/js/spec/stores/groupingStore.spec.jsx | 5 ++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/sentry/static/sentry/app/__mocks__/api.tsx b/src/sentry/static/sentry/app/__mocks__/api.tsx index 683a7e90260807..c17162ae13c698 100644 --- a/src/sentry/static/sentry/app/__mocks__/api.tsx +++ b/src/sentry/static/sentry/app/__mocks__/api.tsx @@ -18,6 +18,8 @@ const DEFAULT_MOCK_RESPONSE_OPTIONS = { predicate: () => true, }; +const mergeMock = jest.fn(); + type ResponseType = JQueryXHR & { url: string; statusCode: number; @@ -180,7 +182,12 @@ class Client { bulkUpdate = RealClient.Client.prototype.bulkUpdate; _chain = RealClient.Client.prototype._chain; _wrapRequest = RealClient.Client.prototype._wrapRequest; - merge = RealClient.Client.prototype.merge; + + merge(params, options) { + mergeMock(params, options); + + return RealClient.Client.prototype.merge.call(this, params, options); + } } -export {Client}; +export {Client, mergeMock}; diff --git a/tests/js/spec/stores/groupingStore.spec.jsx b/tests/js/spec/stores/groupingStore.spec.jsx index b72cd7131de33f..ee9704c34633a9 100644 --- a/tests/js/spec/stores/groupingStore.spec.jsx +++ b/tests/js/spec/stores/groupingStore.spec.jsx @@ -1,5 +1,5 @@ import GroupingStore from 'app/stores/groupingStore'; -import {Client} from 'app/api'; +import {Client, mergeMock} from 'app/api'; describe('Grouping Store', function() { let trigger; @@ -318,7 +318,6 @@ describe('Grouping Store', function() { describe('onMerge', function() { beforeEach(function() { - jest.spyOn(Client.prototype, 'merge'); Client.clearMockResponses(); Client.addMockResponse({ method: 'PUT', @@ -360,7 +359,7 @@ describe('Grouping Store', function() { await promise; - expect(Client.prototype.merge).toHaveBeenCalledWith( + expect(mergeMock).toHaveBeenCalledWith( { orgId: 'orgId', projectId: 'projectId', From ec0cc05d6c98e944f9690bef6528d45633d23cf4 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Fri, 9 Aug 2019 11:46:54 -0400 Subject: [PATCH 14/19] update all the snapshots --- .../__snapshots__/createProject.spec.jsx.snap | 30 ++++++++++++-- .../__snapshots__/issueDiff.spec.jsx.snap | 20 +++++++++- .../__snapshots__/formField.spec.jsx.snap | 24 +++++++++-- .../__snapshots__/bookmarkStar.spec.jsx.snap | 10 ++++- .../sidebar/__snapshots__/index.spec.jsx.snap | 20 +++++++++- .../organizationTeamProjects.spec.jsx.snap | 40 +++++++++++++++++-- .../projectDebugFiles.spec.jsx.snap | 30 ++++++++++++-- .../projectPluginDetails.spec.jsx.snap | 10 ++++- .../__snapshots__/inviteMember.spec.jsx.snap | 20 +++++++++- .../__snapshots__/actions.spec.jsx.snap | 10 ++++- .../releaseCommits.spec.jsx.snap | 20 +++++++++- .../__snapshots__/projectCard.spec.jsx.snap | 10 ++++- .../organizationProjects.spec.jsx.snap | 30 ++++++++++++-- .../organizationRepositories.spec.jsx.snap | 10 ++++- ...izationRepositoriesContainer.spec.jsx.snap | 10 ++++- .../projectEnvironments.spec.jsx.snap | 30 ++++++++++++-- .../sentryAppInstallations.spec.jsx.snap | 20 +++++++++- .../__snapshots__/index.spec.jsx.snap | 20 +++++++++- 18 files changed, 327 insertions(+), 37 deletions(-) diff --git a/tests/js/spec/components/__snapshots__/createProject.spec.jsx.snap b/tests/js/spec/components/__snapshots__/createProject.spec.jsx.snap index 9895aff0fca951..26bf291176173c 100644 --- a/tests/js/spec/components/__snapshots__/createProject.spec.jsx.snap +++ b/tests/js/spec/components/__snapshots__/createProject.spec.jsx.snap @@ -92,7 +92,15 @@ exports[`CreateProject should block if you have access to no teams 1`] = ` exports[`CreateProject should deal with incorrect platform name if its provided by url 1`] = ` Object { @@ -102,7 +108,13 @@ exports[`FormField + model renders with Form 1`] = ` Object { @@ -247,7 +259,13 @@ exports[`FormField + model renders with Form 1`] = ` Object { diff --git a/tests/js/spec/components/projects/__snapshots__/bookmarkStar.spec.jsx.snap b/tests/js/spec/components/projects/__snapshots__/bookmarkStar.spec.jsx.snap index 70161680b6c1a5..0c812a74824543 100644 --- a/tests/js/spec/components/projects/__snapshots__/bookmarkStar.spec.jsx.snap +++ b/tests/js/spec/components/projects/__snapshots__/bookmarkStar.spec.jsx.snap @@ -44,7 +44,15 @@ exports[`BookmarkStar renders 1`] = ` } > Projects