diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 4ee5602a56e5..5b17fc07ef10 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -7,6 +7,8 @@ _Released 03/14/2023 (PENDING)_ - It is now possible to control the number of connection attempts to the browser using the CYPRESS_CONNECT_RETRY_THRESHOLD Environment Variable. Learn more [here](https://docs.cypress.io/guides/references/advanced-installation#Environment-variables). Addressed in [#25848](https://github.com/cypress-io/cypress/pull/25848). - The Debug page is now able to show real-time results from in-progress runs. Addresses [#25759](https://github.com/cypress-io/cypress/issues/25759). +- Added the ability to control whether a request is logged to the command log via `cy.intercept()` by passing `log: false` or `log: true`. Addresses [#7362](https://github.com/cypress-io/cypress/issues/7362). + - This can be used to override Cypress's default behavior of logging all XHRs and fetches, see the [example](https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request). **Bugfixes:** diff --git a/packages/driver/cypress/e2e/cypress/proxy-logging.cy.ts b/packages/driver/cypress/e2e/cypress/proxy-logging.cy.ts index fac6608beec1..66ed1bf210ce 100644 --- a/packages/driver/cypress/e2e/cypress/proxy-logging.cy.ts +++ b/packages/driver/cypress/e2e/cypress/proxy-logging.cy.ts @@ -362,35 +362,79 @@ describe('Proxy Logging', () => { }, )) }) - }) - }) - context('Cypress.ProxyLogging', () => { - describe('.logInterception', () => { - it('creates a fake log for unmatched requests', () => { - const interception = { - id: 'request123', - request: { - url: 'http://foo', - method: 'GET', - headers: {}, - }, - } + context('with log prop', () => { + it('can hide an intercepted request for an image', () => { + const logs: any[] = [] + + cy.intercept('**/cypress.png*', { log: false }).as('image') + .then(() => { + cy.on('log:added', (log) => { + if (log.name !== 'request') return + + logs.push(log) + }) + + const img = new Image() + + img.src = `/fixtures/media/cypress.png?${Date.now()}` + }) + .wait('@image') + .then(() => { + expect(logs).to.have.length(0) + }) + }) + + it('uses the final interceptor to determine if a log should be made', (done) => { + const logs: any[] = [] + + cy.on('log:added', (log) => { + if (log.name !== 'request') return + + logs.push(log) + }) + + cy.intercept('**/cypress.png?*', { log: true }).as('log-me') + .intercept('**/cypress.png?dont-log-me-*', { log: false }).as('dont-log-me') + .then(() => { + const img = new Image() + + img.src = `/fixtures/media/cypress.png?dont-log-me-${Date.now()}` + }) + .wait('@dont-log-me') + .then(() => { + expect(logs).to.have.length(0) - const route = {} + const img = new Image() - const ret = Cypress.ProxyLogging.logInterception(interception, route) + img.src = `/fixtures/media/cypress.png?log-me-${Date.now()}` - expect(ret.preRequest).to.deep.eq({ - requestId: 'request123', - resourceType: 'other', - originalResourceType: 'Request with no browser pre-request', - url: 'http://foo', - method: 'GET', - headers: {}, + cy.once('log:added', (log) => { + expect(log.name).to.eq('request') + expect(log.displayName).to.eq('image') + done() + }) + }) }) - expect(ret.log.get('name')).to.eq('request') + it('can disable fetch logs', () => { + const logs: any[] = [] + + cy.intercept({ resourceType: 'fetch' }, { log: false }).as('fetch') + .then(() => { + cy.on('log:added', (log) => { + if (log.name !== 'request') return + + logs.push(log) + }) + + return fetch(`/foo?${Date.now()}`) + }) + .wait('@fetch') + .then(() => { + expect(logs).to.have.length(0) + }) + }) }) }) }) diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 40c32e1732d1..3237753c399d 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -15,11 +15,12 @@ import { StringMatcher, NumberMatcher, BackendStaticResponseWithArrayBuffer, + StaticResponseWithOptions, } from '@packages/net-stubbing/lib/types' import { validateStaticResponse, getBackendStaticResponse, - hasStaticResponseKeys, + hasStaticResponseWithOptionsKeys, } from './static-response-utils' import { getRouteMatcherLogConfig, @@ -187,7 +188,7 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, } else if (_.isString(handler)) { staticResponse = { body: handler } } else if (_.isObjectLike(handler)) { - if (!hasStaticResponseKeys(handler)) { + if (!hasStaticResponseWithOptionsKeys(handler)) { // the user has not supplied any of the StaticResponse keys, assume it's a JSON object // that should become the body property handler = { @@ -195,9 +196,9 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, } } - validateStaticResponse('cy.intercept', handler) + validateStaticResponse('cy.intercept', handler) - staticResponse = handler as StaticResponse + staticResponse = handler as StaticResponseWithOptions } else if (!_.isUndefined(handler)) { // a handler was passed but we dunno what it's supposed to be $errUtils.throwErrByPath('net_stubbing.intercept.invalid_handler', { args: { handler } }) diff --git a/packages/driver/src/cy/net-stubbing/events/before-request.ts b/packages/driver/src/cy/net-stubbing/events/before-request.ts index b01bc4afbc9d..fae78c6c3a80 100644 --- a/packages/driver/src/cy/net-stubbing/events/before-request.ts +++ b/packages/driver/src/cy/net-stubbing/events/before-request.ts @@ -302,7 +302,7 @@ export const onBeforeRequest: HandlerFn = (Cypre resolve = _resolve }) - request.setLogFlag = Cypress.ProxyLogging.logInterception(request, route).setFlag + request.setLogFlag = Cypress.ProxyLogging.logInterception(request, route)?.setFlag || (() => {}) // TODO: this misnomer is a holdover from XHR, should be numRequests route.log.set('numResponses', (route.log.get('numResponses') || 0) + 1) diff --git a/packages/driver/src/cy/net-stubbing/static-response-utils.ts b/packages/driver/src/cy/net-stubbing/static-response-utils.ts index 6f0d2cdae7a4..12cd1eb0d777 100644 --- a/packages/driver/src/cy/net-stubbing/static-response-utils.ts +++ b/packages/driver/src/cy/net-stubbing/static-response-utils.ts @@ -2,6 +2,7 @@ import _ from 'lodash' import type { StaticResponse, + StaticResponseWithOptions, BackendStaticResponseWithArrayBuffer, FixtureOpts, } from '@packages/net-stubbing/lib/types' @@ -13,6 +14,8 @@ import $errUtils from '../../cypress/error_utils' // user-facing StaticResponse only export const STATIC_RESPONSE_KEYS: (keyof StaticResponse)[] = ['body', 'fixture', 'statusCode', 'headers', 'forceNetworkError', 'throttleKbps', 'delay', 'delayMs'] +export const STATIC_RESPONSE_WITH_OPTIONS_KEYS: (keyof StaticResponseWithOptions)[] = [...STATIC_RESPONSE_KEYS, 'log'] + export function validateStaticResponse (cmd: string, staticResponse: StaticResponse): void { const err = (message) => { $errUtils.throwErrByPath('net_stubbing.invalid_static_response', { args: { cmd, message, staticResponse } }) @@ -102,8 +105,8 @@ function getFixtureOpts (fixture: string): FixtureOpts { return { filePath, encoding: encoding === 'null' ? null : encoding } } -export function getBackendStaticResponse (staticResponse: Readonly): BackendStaticResponseWithArrayBuffer { - const backendStaticResponse: BackendStaticResponseWithArrayBuffer = _.omit(staticResponse, 'body', 'fixture', 'delayMs') +export function getBackendStaticResponse (staticResponse: Readonly): BackendStaticResponseWithArrayBuffer { + const backendStaticResponse: BackendStaticResponseWithArrayBuffer = _.omit(staticResponse, 'body', 'fixture', 'delayMs', 'log') if (staticResponse.delayMs) { // support deprecated `delayMs` usage @@ -132,9 +135,17 @@ export function getBackendStaticResponse (staticResponse: Readonly (array: E[], filterFn: (data: E) => boolean) { - for (const i in array) { - const e = array[i] - - if (!filterFn(e)) continue - - array.splice(i as unknown as number, 1) - - return e - } - - return -} - function formatInterception ({ route, interception }: ProxyRequest['interceptions'][number]) { const ret = { 'RouteMatcher': route.options, @@ -128,10 +111,6 @@ function getRequestLogConfig (req: Omit): Partial = [] proxyRequests: Array = [] constructor (private Cypress: Cypress.Cypress) { @@ -254,7 +232,6 @@ export default class ProxyLogging { proxyRequest.log.end() } } - this.unloggedPreRequests = [] this.proxyRequests = [] }) } @@ -262,26 +239,12 @@ export default class ProxyLogging { /** * Update an existing proxy log with an interception, or create a new log if one was not created (like if shouldLog returned false) */ - logInterception (interception: Interception, route: Route): ProxyRequest { - const unloggedPreRequest = take(this.unloggedPreRequests, ({ requestId }) => requestId === interception.browserRequestId) - - if (unloggedPreRequest) { - debug('interception matched an unlogged prerequest, logging %o', { unloggedPreRequest, interception }) - this.createProxyRequestLog(unloggedPreRequest) - } - - let proxyRequest = _.find(this.proxyRequests, ({ preRequest }) => preRequest.requestId === interception.browserRequestId) + logInterception (interception: Interception, route: Route): ProxyRequest | undefined { + const proxyRequest = _.find(this.proxyRequests, ({ preRequest }) => preRequest.requestId === interception.browserRequestId) if (!proxyRequest) { - // this can happen in a race condition, if user runs Network.disable, if the browser doesn't send pre-request for some reason... - debug(`Missing pre-request/proxy log for cy.intercept to ${interception.request.url} %o`, { interception, route }) - - proxyRequest = this.createProxyRequestLog({ - requestId: interception.browserRequestId || interception.id, - resourceType: 'other', - originalResourceType: 'Request with no browser pre-request', - ..._.pick(interception.request, ['url', 'method', 'headers']), - }) + // request was never logged + return undefined } proxyRequest.interceptions.push({ interception, route }) @@ -326,16 +289,10 @@ export default class ProxyLogging { } /** - * Create a Cypress.Log for an incoming proxy request, or store the metadata for later if it is ignored. + * Create a Cypress.Log for an incoming proxy request. */ - private logIncomingRequest (preRequest: BrowserPreRequest): void { - if (!shouldLog(preRequest)) { - this.unloggedPreRequests.push(preRequest) - - return - } - - this.createProxyRequestLog(preRequest) + private logIncomingRequest (browserPreRequest: BrowserPreRequest): void { + this.createProxyRequestLog(browserPreRequest) } private createProxyRequestLog (preRequest: BrowserPreRequest): ProxyRequest { diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index cf89b834ddc8..1d013e555d23 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -395,7 +395,17 @@ export interface RouteMatcherOptionsGeneric { export type RouteHandlerController = HttpRequestInterceptor -export type RouteHandler = string | StaticResponse | RouteHandlerController | object +export type RouteHandler = string | StaticResponseWithOptions | RouteHandlerController | object + +export type InterceptOptions = { + /** + * If set to `false`, matching requests will not be shown in the Command Log. + * @default true + */ + log?: boolean +} + +export type StaticResponseWithOptions = StaticResponse & InterceptOptions /** * Describes a response that will be sent back to the browser to fulfill the request. diff --git a/packages/net-stubbing/lib/internal-types.ts b/packages/net-stubbing/lib/internal-types.ts index 8c7200f440b7..c31d6b98aa96 100644 --- a/packages/net-stubbing/lib/internal-types.ts +++ b/packages/net-stubbing/lib/internal-types.ts @@ -4,6 +4,7 @@ import type { GenericStaticResponse, Subscription, CyHttpMessages, + InterceptOptions, } from './external-types' export type FixtureOpts = { @@ -11,9 +12,9 @@ export type FixtureOpts = { filePath: string } -export type BackendStaticResponse = GenericStaticResponse +export type BackendStaticResponse = GenericStaticResponse & InterceptOptions -export type BackendStaticResponseWithArrayBuffer = GenericStaticResponse +export type BackendStaticResponseWithArrayBuffer = GenericStaticResponse & InterceptOptions export const SERIALIZABLE_REQ_PROPS = [ 'headers', diff --git a/packages/net-stubbing/lib/server/index.ts b/packages/net-stubbing/lib/server/index.ts index 0754c21ebc42..5a022b72f78f 100644 --- a/packages/net-stubbing/lib/server/index.ts +++ b/packages/net-stubbing/lib/server/index.ts @@ -2,7 +2,7 @@ export { onNetStubbingEvent } from './driver-events' export { InterceptError } from './middleware/error' -export { InterceptRequest } from './middleware/request' +export { SetMatchingRoutes, InterceptRequest } from './middleware/request' export { InterceptResponse } from './middleware/response' diff --git a/packages/net-stubbing/lib/server/intercepted-request.ts b/packages/net-stubbing/lib/server/intercepted-request.ts index cb79662d52b1..98665939acb6 100644 --- a/packages/net-stubbing/lib/server/intercepted-request.ts +++ b/packages/net-stubbing/lib/server/intercepted-request.ts @@ -39,18 +39,16 @@ export class InterceptedRequest { continueResponse?: (newResStream?: Readable) => void req: CypressIncomingRequest res: CypressOutgoingResponse - matchingRoutes: BackendRoute[] state: NetStubbingState socket: CyServer.Socket - constructor (opts: Pick) { + constructor (opts: Pick) { this.id = _.uniqueId('interceptedRequest') this.req = opts.req this.res = opts.res this.continueRequest = opts.continueRequest this.onError = opts.onError this._onResponse = opts.onResponse - this.matchingRoutes = opts.matchingRoutes this.state = opts.state this.socket = opts.socket @@ -72,7 +70,11 @@ export class InterceptedRequest { throw new Error('cannot add default subscriptions to non-empty array') } - for (const route of this.matchingRoutes) { + if (!this.req.matchingRoutes) { + return + } + + for (const route of this.req.matchingRoutes) { const subscriptionsByRoute = { routeId: route.id, immediateStaticResponse: route.staticResponse, @@ -156,7 +158,9 @@ export class InterceptedRequest { // https://github.com/cypress-io/cypress/issues/17139 // Routes should be counted before they're sent. if (eventName === 'before:request') { - const route = this.matchingRoutes.find(({ id }) => id === subscription.routeId) as BackendRoute + const route = this.req.matchingRoutes?.find(({ id }) => id === subscription.routeId) as BackendRoute + + if (!route) throw new Error(`No route by ID ${subscription.routeId} for ${eventName}`) route.matches++ @@ -197,8 +201,11 @@ export class InterceptedRequest { } } - if (eventName === 'before:request') { - if (immediateStaticResponse) { + if (eventName === 'before:request' && immediateStaticResponse) { + // Since StaticResponse is conflated with InterceptOptions, only send an immediate response if there are keys other than `log`. + const hasOnlyLog = _.isEqual(Object.keys(immediateStaticResponse), ['log']) + + if (!hasOnlyLog) { await sendStaticResponse(this, immediateStaticResponse) return data diff --git a/packages/net-stubbing/lib/server/middleware/request.ts b/packages/net-stubbing/lib/server/middleware/request.ts index bf334bed013c..a7012496db97 100644 --- a/packages/net-stubbing/lib/server/middleware/request.ts +++ b/packages/net-stubbing/lib/server/middleware/request.ts @@ -1,6 +1,5 @@ import _ from 'lodash' import { concatStream } from '@packages/network' -import Debug from 'debug' import url from 'url' import type { @@ -19,14 +18,13 @@ import { getBodyEncoding, } from '../util' import { InterceptedRequest } from '../intercepted-request' -import type { BackendRoute } from '../types' -const debug = Debug('cypress:net-stubbing:server:intercept-request') +// do not use a debug namespace in this file - use the per-request `this.debug` instead +// available as cypress-verbose:proxy:http +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const debug = null -/** - * Called when a new request is received in the proxy layer. - */ -export const InterceptRequest: RequestMiddleware = async function () { +export const SetMatchingRoutes: RequestMiddleware = async function () { if (matchesRoutePreflight(this.netStubbingState.routes, this.req)) { // send positive CORS preflight response return sendStaticResponse(this, { @@ -41,9 +39,16 @@ export const InterceptRequest: RequestMiddleware = async function () { }) } - const matchingRoutes: BackendRoute[] = [...getRoutesForRequest(this.netStubbingState.routes, this.req)] + this.req.matchingRoutes = [...getRoutesForRequest(this.netStubbingState.routes, this.req)] + + this.next() +} - if (!matchingRoutes.length) { +/** + * Called when a new request is received in the proxy layer. + */ +export const InterceptRequest: RequestMiddleware = async function () { + if (!this.req.matchingRoutes?.length) { // not intercepted, carry on normally... return this.next() } @@ -59,10 +64,9 @@ export const InterceptRequest: RequestMiddleware = async function () { res: this.res, socket: this.socket, state: this.netStubbingState, - matchingRoutes, }) - debug('intercepting request %o', { requestId: request.id, req: _.pick(this.req, 'url') }) + this.debug('cy.intercept: intercepting request') // attach requestId to the original req object for later use this.req.requestId = request.id @@ -82,7 +86,7 @@ export const InterceptRequest: RequestMiddleware = async function () { mergeChanges: _.noop, }) - debug('request/response finished, cleaning up %o', { requestId: request.id }) + this.debug('cy.intercept: request/response finished, cleaning up') delete this.netStubbingState.requests[request.id] }) @@ -124,7 +128,7 @@ export const InterceptRequest: RequestMiddleware = async function () { const bodyIsBinary = bodyEncoding === 'binary' if (bodyIsBinary) { - debug('req.body contained non-utf8 characters, treating as binary content %o', { requestId: request.id, req: _.pick(this.req, 'url') }) + this.debug('cy.intercept: req.body contained non-utf8 characters, treating as binary content') } // leave the requests that send a binary buffer unchanged diff --git a/packages/net-stubbing/test/unit/intercepted-request-spec.ts b/packages/net-stubbing/test/unit/intercepted-request-spec.ts index ab062376ecab..84de5e468ecb 100644 --- a/packages/net-stubbing/test/unit/intercepted-request-spec.ts +++ b/packages/net-stubbing/test/unit/intercepted-request-spec.ts @@ -12,24 +12,24 @@ describe('InterceptedRequest', () => { } const state = NetStubbingState() const interceptedRequest = new InterceptedRequest({ - // @ts-ignore - req: {}, + req: { + matchingRoutes: [ + // @ts-ignore + { + id: '1', + hasInterceptor: true, + routeMatcher: {}, + }, + // @ts-ignore + { + id: '2', + hasInterceptor: true, + routeMatcher: {}, + }, + ], + }, state, socket, - matchingRoutes: [ - // @ts-ignore - { - id: '1', - hasInterceptor: true, - routeMatcher: {}, - }, - // @ts-ignore - { - id: '2', - hasInterceptor: true, - routeMatcher: {}, - }, - ], }) interceptedRequest.addSubscription({ diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts index ecd1c20a6dbc..573b1aad4124 100644 --- a/packages/proxy/lib/http/request-middleware.ts +++ b/packages/proxy/lib/http/request-middleware.ts @@ -1,9 +1,10 @@ import _ from 'lodash' import { blocked, cors } from '@packages/network' -import { InterceptRequest } from '@packages/net-stubbing' +import { InterceptRequest, SetMatchingRoutes } from '@packages/net-stubbing' import type { HttpMiddleware } from './' import { getSameSiteContext, addCookieJarCookiesToRequest, shouldAttachAndSetCookies } from './util/cookies' import { doesTopNeedToBeSimulated } from './util/top-simulation' +import type { CypressIncomingRequest } from '../types' // do not use a debug namespace in this file - use the per-request `this.debug` instead // available as cypress-verbose:proxy:http @@ -147,11 +148,28 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () { })) } -const SendToDriver: RequestMiddleware = function () { - const { browserPreRequest } = this.req +function shouldLog (req: CypressIncomingRequest) { + // 1. Any matching `cy.intercept()` should cause `req` to be logged by default, unless `log: false` is passed explicitly. + if (req.matchingRoutes?.length) { + const lastMatchingRoute = req.matchingRoutes[0] + + if (!lastMatchingRoute.staticResponse) { + // No StaticResponse is set, therefore the request must be logged. + return true + } - if (browserPreRequest) { - this.socket.toDriver('request:event', 'incoming:request', browserPreRequest) + if (lastMatchingRoute.staticResponse.log !== undefined) { + return Boolean(lastMatchingRoute.staticResponse.log) + } + } + + // 2. Otherwise, only log if it is an XHR or fetch. + return req.resourceType === 'fetch' || req.resourceType === 'xhr' +} + +const SendToDriver: RequestMiddleware = function () { + if (shouldLog(this.req) && this.req.browserPreRequest) { + this.socket.toDriver('request:event', 'incoming:request', this.req.browserPreRequest) } this.next() @@ -292,6 +310,7 @@ export default { MaybeAttachCrossOriginCookies, MaybeEndRequestWithBufferedResponse, CorrelateBrowserPreRequest, + SetMatchingRoutes, SendToDriver, InterceptRequest, RedirectToClientRouteIfUnloaded, diff --git a/packages/proxy/lib/types.ts b/packages/proxy/lib/types.ts index 87508941372d..8c0020ebb62f 100644 --- a/packages/proxy/lib/types.ts +++ b/packages/proxy/lib/types.ts @@ -1,6 +1,7 @@ import type { Readable } from 'stream' import type { Request, Response } from 'express' import type { ResourceType } from '@packages/net-stubbing' +import type { BackendRoute } from '@packages/net-stubbing/lib/server/types' /** * An incoming request to the Cypress web server. @@ -20,6 +21,10 @@ export type CypressIncomingRequest = Request & { * Resource type from browserPreRequest. Copied to req so intercept matching can work. */ resourceType?: ResourceType + /** + * Stack-ordered list of `cy.intercept()`s matching this request. + */ + matchingRoutes?: BackendRoute[] } export type RequestedWithHeader = 'fetch' | 'xhr' | 'true' diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts index 58995e35c758..c18b96f22d5a 100644 --- a/packages/proxy/test/unit/http/request-middleware.spec.ts +++ b/packages/proxy/test/unit/http/request-middleware.spec.ts @@ -18,6 +18,7 @@ describe('http/request-middleware', () => { 'MaybeAttachCrossOriginCookies', 'MaybeEndRequestWithBufferedResponse', 'CorrelateBrowserPreRequest', + 'SetMatchingRoutes', 'SendToDriver', 'InterceptRequest', 'RedirectToClientRouteIfUnloaded',