Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a92cd96
perf log automation command
cacieprins Nov 12, 2025
b938ad7
write a performance log entry for each command
cacieprins Nov 12, 2025
6ca1bb1
better log entries
cacieprins Nov 12, 2025
ef2acf7
changelog
cacieprins Nov 12, 2025
b66121a
Merge branch 'develop' into csv-benchmark
cacieprins Nov 12, 2025
39aa6f2
esc
cacieprins Nov 12, 2025
0aa7f21
fail with debug entry if error thrown initializing perf log
cacieprins Nov 12, 2025
8b2e515
reduce serialization
cacieprins Nov 12, 2025
32afcfb
Merge branch 'develop' into csv-benchmark
cacieprins Nov 13, 2025
d80bf84
ts
cacieprins Nov 13, 2025
7d56e1b
Merge branch 'develop' into csv-benchmark
cacieprins Nov 19, 2025
39d87cb
back out of async/await modification to command queue
cacieprins Nov 19, 2025
8b4879c
dont throw if automation throws
cacieprins Nov 20, 2025
d0af3eb
Merge branch 'develop' into csv-benchmark
cacieprins Nov 20, 2025
0e83ccc
rm numElements - causes serialization issues??
cacieprins Nov 20, 2025
7084eaf
refactor to performance marks, clean up telemetry on the client, make…
cacieprins Nov 21, 2025
6c79da7
fix automation spy assertion to be more precise, fix ts err
cacieprins Nov 24, 2025
40c2e5f
Merge branch 'develop' into csv-benchmark
cacieprins Nov 24, 2025
819a8aa
Merge branch 'develop' into csv-benchmark
cacieprins Dec 17, 2025
084aa3d
changelog
cacieprins Dec 17, 2025
b20cb7c
wsp
cacieprins Dec 17, 2025
e8243b9
changelog
cacieprins Dec 18, 2025
0a353b4
Merge branch 'develop' into csv-benchmark
jennifer-shehane Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 15.8.2

_Released 12/30/2025 (PENDING)_

**Misc:**

- Command execution can be benchmarked by setting the `CYPRESS_INTERNAL_COMMAND_PERFORMANCE_LOGGING` environment variable to `1` or `true`. The performance log is recorded to `./cypress/logs/performance-{UUID_V4}.log`, a new uuid generated at the beginning of each run. Addresses [#33148](https://github.com/cypress-io/cypress/issues/33148). Addressed in [#32938](https://github.com/cypress-io/cypress/pull/32938).

## 15.8.1

_Released 12/18/2025_
Expand Down
205 changes: 125 additions & 80 deletions packages/app/src/runner/events/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,149 @@
import { telemetry } from '@packages/telemetry/browser/client'

export const addTelemetryListeners = (Cypress: Cypress.Cypress) => {
Cypress.on('test:before:run', (attributes, test) => {
// we emit the 'test:before:run' events within various driver tests
try {
// If a span for a previous test hasn't been ended, end it before starting the new test span
const previousTestSpan = telemetry.findActiveSpan((span) => {
return span.name.startsWith('test:')
})

if (previousTestSpan) {
telemetry.endActiveSpanAndChildren(previousTestSpan)
}
function startTestOtelSpan (attributes: Cypress.ObjectLike, test: Mocha.Test) {
// we emit the 'test:before:run' events within various driver tests
try {
// If a span for a previous test hasn't been ended, end it before starting the new test span
const previousTestSpan = telemetry.findActiveSpan((span) => {
return span.name.startsWith('test:')
})

if (previousTestSpan) {
telemetry.endActiveSpanAndChildren(previousTestSpan)
}

const span = telemetry.startSpan({ name: `test:${test.fullTitle()}`, active: true })
const span = telemetry.startSpan({ name: `test:${test.fullTitle()}`, active: true })

span?.setAttributes({
currentRetry: attributes.currentRetry,
})
} catch (error) {
// TODO: log error when client side debug logging is available
}
})
span?.setAttributes({
currentRetry: attributes.currentRetry,
})
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

Cypress.on('test:after:run', (attributes, test) => {
try {
const span = telemetry.getSpan(`test:${test.fullTitle()}`)
function endTestOtelSpan (attributes: Cypress.ObjectLike, test: Mocha.Test) {
try {
const span = telemetry.getSpan(`test:${test.fullTitle()}`)

span?.setAttributes({
timings: JSON.stringify(attributes.timings),
})
span?.setAttributes({
timings: JSON.stringify(attributes.timings),
})

span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
})
span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

const commandSpanInfo = (command: Cypress.CommandQueue) => {
const runnable = Cypress.state('runnable')
const runnableType = runnable.type === 'hook' ? runnable.hookName : runnable.type
const commandSpanInfo = (command: Cypress.CommandQueue) => {
const runnable = Cypress.state('runnable')
const runnableType = runnable.type === 'hook' ? runnable.hookName : runnable.type

return {
name: `${runnableType}: ${command.attributes.name}(${command.attributes.args.join(',')})`,
runnable,
runnableType,
}
return {
name: `${runnableType}: ${command.attributes.name}(${command.attributes.args.join(',')})`,
runnable,
runnableType,
}
}

Cypress.on('command:start', (command: Cypress.CommandQueue) => {
try {
const test = Cypress.state('test')

const { name, runnable, runnableType } = commandSpanInfo(command)

const span = telemetry.startSpan({
name,
opts: {
attributes: {
spec: runnable.invocationDetails.relativeFile,
test: `test:${test.fullTitle()}`,
'runnable-type': runnableType,
},
function startCommandOtelSpan (command: Cypress.CommandQueue) {
try {
const test = Cypress.state('test')

const { name, runnable, runnableType } = commandSpanInfo(command)

const span = telemetry.startSpan({
name,
opts: {
attributes: {
spec: runnable.invocationDetails.relativeFile,
test: `test:${test.fullTitle()}`,
'runnable-type': runnableType,
},
isVerbose: true,
})
},
isVerbose: true,
})

span?.setAttribute('command-name', command.attributes.name)
} catch (error) {
// TODO: log error when client side debug logging is available
}
})
span?.setAttribute('command-name', command.attributes.name)
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

const onCommandEnd = (command: Cypress.CommandQueue) => {
try {
const span = telemetry.getSpan(commandSpanInfo(command).name)
function endCommandOtelSpan (command: Cypress.CommandQueue) {
try {
const span = telemetry.getSpan(commandSpanInfo(command).name)

span?.setAttribute('state', command.state)
span?.setAttribute('numLogs', command.logs?.length || 0)
span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
span?.setAttribute('state', command.state)
span?.setAttribute('numLogs', command.logs?.length || 0)
span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

Cypress.on('command:end', onCommandEnd)
function failCommandOtelSpan (command: Cypress.CommandQueue, error: Error) {
try {
const span = telemetry.getSpan(commandSpanInfo(command).name)

Cypress.on('skipped:command:end', onCommandEnd)
span?.setAttribute('state', command.state)
span?.setAttribute('numLogs', command.logs?.length || 0)
span?.setAttribute('error.name', error.name)
span?.setAttribute('error.message', error.message)
span?.end()
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

Cypress.on('command:failed', (command: Cypress.CommandQueue, error: Error) => {
function startCommandPerformanceMark (command: Cypress.CommandQueue) {
try {
performance.mark(`cy:command:${command.attributes.id}:start`)
} catch (error) {
// TODO: log error when client side debug logging is available
}
}

function endCommandPerformanceMark (Cypress: Cypress.Cypress) {
return (command: Cypress.CommandQueue) => {
try {
const span = telemetry.getSpan(commandSpanInfo(command).name)
const { id } = command.attributes

performance.mark(`cy:command:${id}:end`)
const measure = performance.measure(`cy:command:${id}:measure`, {
start: `cy:command:${id}:start`,
end: `cy:command:${id}:end`,
})

if (!measure) {
return
}

span?.setAttribute('state', command.state)
span?.setAttribute('numLogs', command.logs?.length || 0)
span?.setAttribute('error.name', error.name)
span?.setAttribute('error.message', error.message)
span?.end()
Cypress.automation('log:command:performance', {
name: command.attributes.name,
startTime: measure.startTime,
duration: measure.duration,
}).catch(() => {}).finally(() => {
performance.clearMarks(`cy:command:${id}:start`)
performance.clearMarks(`cy:command:${id}:end`)
performance.clearMeasures(`cy:command:${id}:measure`)
})
} catch (error) {
// TODO: log error when client side debug logging is available
// noop
}
})
}
}

export const addTelemetryListeners = (Cypress: Cypress.Cypress) => {
Cypress.on('test:before:run', startTestOtelSpan)
Cypress.on('test:after:run', endTestOtelSpan)
Cypress.on('command:start', startCommandOtelSpan)
Cypress.on('command:end', endCommandOtelSpan)
Cypress.on('command:failed', failCommandOtelSpan)
Cypress.on('skipped:command:end', endCommandOtelSpan)

Cypress.on('command:start', startCommandPerformanceMark)
Cypress.on('command:end', endCommandPerformanceMark(Cypress))
Cypress.on('skipped:command:end', endCommandPerformanceMark(Cypress))
}
2 changes: 1 addition & 1 deletion packages/driver/cypress/e2e/commands/location.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ describe('src/cy/commands/location', () => {
// 1. initial call cy.location('pathname')
// 2. the should() assertion
// 3. the then() callback
expect(Cypress.automation).to.have.been.calledThrice
expect(Cypress.automation.withArgs('get:aut:url')).to.have.been.calledThrice
})
})

Expand Down
2 changes: 1 addition & 1 deletion packages/driver/src/cypress/command_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ export class CommandQueue extends Queue<$Command> {

return ret
})
.then((subject) => {
.then(async (subject) => {
// we may be given a regular array here so
// we need to re-wrap the array in jquery
// if that's the case if the first item
Expand Down
3 changes: 3 additions & 0 deletions packages/server/lib/automation/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cookieJar } from '../util/cookies'
import type { ServiceWorkerEventHandler } from '@packages/proxy/lib/http/util/service-worker-manager'
import Debug from 'debug'
import { AutomationNotImplemented } from './automation_not_implemented'
import { PerformanceLogger } from './performance_logger'

const debug = Debug('cypress:server:automation')

Expand Down Expand Up @@ -174,6 +175,8 @@ export class Automation {
case 'canceled:download':
case 'complete:download':
return data
case 'log:command:performance':
return PerformanceLogger.write(data)
default:
return automate(data)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const AutomationNotImplementedKind = 'AutomationNotImplemented'

export class AutomationNotImplemented extends Error {
readonly kind = AutomationNotImplementedKind
constructor (message: string, automationType: string, ...args) {
constructor (message: any, automationType: string, ...args) {
super(`Automation command '${message}' not implemented by ${automationType}`)
}

Expand Down
Loading
Loading