Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CyPromptCloudApi } from '@packages/types/src/cy-prompt/cy-prompt-server-types'
import Debug from 'debug'
import { stripPath } from '../../strip_path'
const debug = Debug('cypress:server:cloud:api:cy-prompt:report_cy-prompt_error')
const debug = Debug('cypress:server:cloud:api:cy-prompt:report_cy_prompt_error')

export interface ReportCyPromptErrorOptions {
cloudApi: CyPromptCloudApi
Expand Down Expand Up @@ -38,6 +38,10 @@ export function reportCyPromptError ({
}: ReportCyPromptErrorOptions): void {
debug('Error reported:', error)

if (process.env.CYPRESS_CRASH_REPORTS === '0') {
return
}

// When developing locally, do not send to Sentry, but instead log to console.
if (
process.env.CYPRESS_LOCAL_CY_PROMPT_PATH ||
Expand Down Expand Up @@ -79,7 +83,7 @@ export function reportCyPromptError ({
stack: stripPath(errorObject.stack ?? `Unknown stack`),
message: stripPath(errorObject.message ?? `Unknown message`),
cyPromptMethod,
cyPromptMethodArgs: cyPromptMethodArgsString,
cyPromptMethodArgs: cyPromptMethodArgsString ? stripPath(cyPromptMethodArgsString) : undefined,
}],
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import chokidar from 'chokidar'
import { getCloudMetadata } from '../get_cloud_metadata'
import type { CyPromptAuthenticatedUserShape, CyPromptServerOptions } from '@packages/types'
import crypto from 'crypto'
import { reportCyPromptError } from '../api/cy-prompt/report_cy-prompt_error'
import { reportCyPromptError } from '../api/cy-prompt/report_cy_prompt_error'

const debug = Debug('cypress:server:cy-prompt-lifecycle-manager')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import { expect } from 'chai'
import { sinon } from '../../../../spec_helper'
import { reportCyPromptError } from '@packages/server/lib/cloud/api/cy-prompt/report_cy_prompt_error'

describe('lib/cloud/api/cy-prompt/report_cy_prompt_error', () => {
let cloudRequestStub: sinon.SinonStub
let cloudApi: any
let oldNodeEnv: string | undefined

beforeEach(() => {
oldNodeEnv = process.env.NODE_ENV
cloudRequestStub = sinon.stub()
cloudApi = {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { 'x-cypress-version': '1.2.3' },
CloudRequest: {
post: cloudRequestStub,
},
}
})

afterEach(() => {
sinon.restore()
delete process.env.CYPRESS_CRASH_REPORTS
delete process.env.CYPRESS_LOCAL_CY_PROMPT_PATH
delete process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF
if (oldNodeEnv) {
process.env.NODE_ENV = oldNodeEnv
} else {
delete process.env.NODE_ENV
}
})

describe('reportCyPromptError', () => {
it('logs error when CYPRESS_LOCAL_CY_PROMPT_PATH is set', () => {
sinon.stub(console, 'error')
process.env.CYPRESS_LOCAL_CY_PROMPT_PATH = '/path/to/cy-prompt'
const error = new Error('test error')

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(
'Error in testMethod:',
error,
)
})

it('logs error when NODE_ENV is development', () => {
sinon.stub(console, 'error')
process.env.NODE_ENV = 'development'
const error = new Error('test error')

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(
'Error in testMethod:',
error,
)
})

it('logs error when CYPRESS_INTERNAL_E2E_TESTING_SELF is set', () => {
sinon.stub(console, 'error')
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
const error = new Error('test error')

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(
'Error in testMethod:',
error,
)
})

it('does not report error when CYPRESS_CRASH_REPORTS is 0', () => {
process.env.CYPRESS_CRASH_REPORTS = '0'
const error = new Error('test error')

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

expect(cloudRequestStub).to.not.have.been.called
})

it('converts non-Error objects to Error', () => {
const error = 'string error'

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/cy-prompt/errors',
{
cyPromptHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'string error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_cy_prompt_error_spec.ts')),
cyPromptMethod: 'testMethod',
cyPromptMethodArgs: undefined,
}],
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
})

it('handles Error objects correctly', () => {
const error = new Error('test error')

error.stack = 'test stack'

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/cy-prompt/errors',
{
cyPromptHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: 'test stack',
cyPromptMethod: 'testMethod',
cyPromptMethodArgs: undefined,
}],
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
})

it('includes cyPromptMethodArgs when provided', () => {
const error = new Error('test error')
const args = ['arg1', { key: '/path/to/file.js' }]

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
cyPromptMethodArgs: args,
})

expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/cy-prompt/errors',
{
cyPromptHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_cy_prompt_error_spec.ts')),
cyPromptMethod: 'testMethod',
cyPromptMethodArgs: JSON.stringify({ args: ['arg1', { key: '<stripped-path>file.js' }] }),
}],
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
})

it('handles errors in JSON.stringify for cyPromptMethodArgs', () => {
const error = new Error('test error')
const circularObj: any = {}

circularObj.self = circularObj

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
cyPromptMethodArgs: [circularObj],
})

expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/cy-prompt/errors',
{
cyPromptHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_cy_prompt_error_spec.ts')),
cyPromptMethod: 'testMethod',
cyPromptMethodArgs: sinon.match(/Unknown args/),
}],
},
{
headers: {
'Content-Type': 'application/json',
},
},
)
})

it('handles errors in CloudRequest.post', () => {
const error = new Error('test error')
const postError = new Error('post error')

cloudRequestStub.rejects(postError)

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

// Just verify the post was called, don't check debug output
expect(cloudRequestStub).to.be.called
})

it('handles errors in payload construction', () => {
const error = new Error('test error')

sinon.stub(JSON, 'stringify').throws(new Error('JSON error'))

reportCyPromptError({
cloudApi,
cyPromptHash: 'abc123',
projectSlug: 'test-project',
error,
cyPromptMethod: 'testMethod',
})

// Just verify the post was called, don't check debug output
expect(cloudRequestStub).to.be.called
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import os from 'os'
import { CloudRequest, createCloudRequest } from '../../../../lib/cloud/api/cloud_request'
import { isRetryableError } from '../../../../lib/cloud/network/is_retryable_error'
import { asyncRetry } from '../../../../lib/util/async_retry'
import * as reportCyPromptErrorPath from '../../../../lib/cloud/api/cy-prompt/report_cy-prompt_error'
import * as reportCyPromptErrorPath from '../../../../lib/cloud/api/cy-prompt/report_cy_prompt_error'

describe('CyPromptLifecycleManager', () => {
let cyPromptLifecycleManager: CyPromptLifecycleManager
Expand Down