Skip to content

Commit cc479fa

Browse files
chore: (cy.prompt) ensure to strip out paths from all data when reporting errors in prompt (#32134)
* fix: (cy.prompt) ensure to strip out paths from all data when reporting errors in prompt * refactor * update tests
1 parent e2b3b42 commit cc479fa

File tree

4 files changed

+289
-4
lines changed

4 files changed

+289
-4
lines changed

packages/server/lib/cloud/api/cy-prompt/report_cy-prompt_error.ts renamed to packages/server/lib/cloud/api/cy-prompt/report_cy_prompt_error.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CyPromptCloudApi } from '@packages/types/src/cy-prompt/cy-prompt-server-types'
22
import Debug from 'debug'
33
import { stripPath } from '../../strip_path'
4-
const debug = Debug('cypress:server:cloud:api:cy-prompt:report_cy-prompt_error')
4+
const debug = Debug('cypress:server:cloud:api:cy-prompt:report_cy_prompt_error')
55

66
export interface ReportCyPromptErrorOptions {
77
cloudApi: CyPromptCloudApi
@@ -38,6 +38,10 @@ export function reportCyPromptError ({
3838
}: ReportCyPromptErrorOptions): void {
3939
debug('Error reported:', error)
4040

41+
if (process.env.CYPRESS_CRASH_REPORTS === '0') {
42+
return
43+
}
44+
4145
// When developing locally, do not send to Sentry, but instead log to console.
4246
if (
4347
process.env.CYPRESS_LOCAL_CY_PROMPT_PATH ||
@@ -79,7 +83,7 @@ export function reportCyPromptError ({
7983
stack: stripPath(errorObject.stack ?? `Unknown stack`),
8084
message: stripPath(errorObject.message ?? `Unknown message`),
8185
cyPromptMethod,
82-
cyPromptMethodArgs: cyPromptMethodArgsString,
86+
cyPromptMethodArgs: cyPromptMethodArgsString ? stripPath(cyPromptMethodArgsString) : undefined,
8387
}],
8488
}
8589

packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import chokidar from 'chokidar'
1414
import { getCloudMetadata } from '../get_cloud_metadata'
1515
import type { CyPromptAuthenticatedUserShape, CyPromptServerOptions } from '@packages/types'
1616
import crypto from 'crypto'
17-
import { reportCyPromptError } from '../api/cy-prompt/report_cy-prompt_error'
17+
import { reportCyPromptError } from '../api/cy-prompt/report_cy_prompt_error'
1818

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

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { expect } from 'chai'
2+
import { sinon } from '../../../../spec_helper'
3+
import { reportCyPromptError } from '@packages/server/lib/cloud/api/cy-prompt/report_cy_prompt_error'
4+
5+
describe('lib/cloud/api/cy-prompt/report_cy_prompt_error', () => {
6+
let cloudRequestStub: sinon.SinonStub
7+
let cloudApi: any
8+
let oldNodeEnv: string | undefined
9+
10+
beforeEach(() => {
11+
oldNodeEnv = process.env.NODE_ENV
12+
cloudRequestStub = sinon.stub()
13+
cloudApi = {
14+
cloudUrl: 'http://localhost:1234',
15+
cloudHeaders: { 'x-cypress-version': '1.2.3' },
16+
CloudRequest: {
17+
post: cloudRequestStub,
18+
},
19+
}
20+
})
21+
22+
afterEach(() => {
23+
sinon.restore()
24+
delete process.env.CYPRESS_CRASH_REPORTS
25+
delete process.env.CYPRESS_LOCAL_CY_PROMPT_PATH
26+
delete process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF
27+
if (oldNodeEnv) {
28+
process.env.NODE_ENV = oldNodeEnv
29+
} else {
30+
delete process.env.NODE_ENV
31+
}
32+
})
33+
34+
describe('reportCyPromptError', () => {
35+
it('logs error when CYPRESS_LOCAL_CY_PROMPT_PATH is set', () => {
36+
sinon.stub(console, 'error')
37+
process.env.CYPRESS_LOCAL_CY_PROMPT_PATH = '/path/to/cy-prompt'
38+
const error = new Error('test error')
39+
40+
reportCyPromptError({
41+
cloudApi,
42+
cyPromptHash: 'abc123',
43+
projectSlug: 'test-project',
44+
error,
45+
cyPromptMethod: 'testMethod',
46+
})
47+
48+
// eslint-disable-next-line no-console
49+
expect(console.error).to.have.been.calledWith(
50+
'Error in testMethod:',
51+
error,
52+
)
53+
})
54+
55+
it('logs error when NODE_ENV is development', () => {
56+
sinon.stub(console, 'error')
57+
process.env.NODE_ENV = 'development'
58+
const error = new Error('test error')
59+
60+
reportCyPromptError({
61+
cloudApi,
62+
cyPromptHash: 'abc123',
63+
projectSlug: 'test-project',
64+
error,
65+
cyPromptMethod: 'testMethod',
66+
})
67+
68+
// eslint-disable-next-line no-console
69+
expect(console.error).to.have.been.calledWith(
70+
'Error in testMethod:',
71+
error,
72+
)
73+
})
74+
75+
it('logs error when CYPRESS_INTERNAL_E2E_TESTING_SELF is set', () => {
76+
sinon.stub(console, 'error')
77+
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
78+
const error = new Error('test error')
79+
80+
reportCyPromptError({
81+
cloudApi,
82+
cyPromptHash: 'abc123',
83+
projectSlug: 'test-project',
84+
error,
85+
cyPromptMethod: 'testMethod',
86+
})
87+
88+
// eslint-disable-next-line no-console
89+
expect(console.error).to.have.been.calledWith(
90+
'Error in testMethod:',
91+
error,
92+
)
93+
})
94+
95+
it('does not report error when CYPRESS_CRASH_REPORTS is 0', () => {
96+
process.env.CYPRESS_CRASH_REPORTS = '0'
97+
const error = new Error('test error')
98+
99+
reportCyPromptError({
100+
cloudApi,
101+
cyPromptHash: 'abc123',
102+
projectSlug: 'test-project',
103+
error,
104+
cyPromptMethod: 'testMethod',
105+
})
106+
107+
expect(cloudRequestStub).to.not.have.been.called
108+
})
109+
110+
it('converts non-Error objects to Error', () => {
111+
const error = 'string error'
112+
113+
reportCyPromptError({
114+
cloudApi,
115+
cyPromptHash: 'abc123',
116+
projectSlug: 'test-project',
117+
error,
118+
cyPromptMethod: 'testMethod',
119+
})
120+
121+
expect(cloudRequestStub).to.be.calledWithMatch(
122+
'http://localhost:1234/cy-prompt/errors',
123+
{
124+
cyPromptHash: 'abc123',
125+
projectSlug: 'test-project',
126+
errors: [{
127+
name: 'Error',
128+
message: 'string error',
129+
stack: sinon.match((stack) => stack.includes('<stripped-path>report_cy_prompt_error_spec.ts')),
130+
cyPromptMethod: 'testMethod',
131+
cyPromptMethodArgs: undefined,
132+
}],
133+
},
134+
{
135+
headers: {
136+
'Content-Type': 'application/json',
137+
},
138+
},
139+
)
140+
})
141+
142+
it('handles Error objects correctly', () => {
143+
const error = new Error('test error')
144+
145+
error.stack = 'test stack'
146+
147+
reportCyPromptError({
148+
cloudApi,
149+
cyPromptHash: 'abc123',
150+
projectSlug: 'test-project',
151+
error,
152+
cyPromptMethod: 'testMethod',
153+
})
154+
155+
expect(cloudRequestStub).to.be.calledWithMatch(
156+
'http://localhost:1234/cy-prompt/errors',
157+
{
158+
cyPromptHash: 'abc123',
159+
projectSlug: 'test-project',
160+
errors: [{
161+
name: 'Error',
162+
message: 'test error',
163+
stack: 'test stack',
164+
cyPromptMethod: 'testMethod',
165+
cyPromptMethodArgs: undefined,
166+
}],
167+
},
168+
{
169+
headers: {
170+
'Content-Type': 'application/json',
171+
},
172+
},
173+
)
174+
})
175+
176+
it('includes cyPromptMethodArgs when provided', () => {
177+
const error = new Error('test error')
178+
const args = ['arg1', { key: '/path/to/file.js' }]
179+
180+
reportCyPromptError({
181+
cloudApi,
182+
cyPromptHash: 'abc123',
183+
projectSlug: 'test-project',
184+
error,
185+
cyPromptMethod: 'testMethod',
186+
cyPromptMethodArgs: args,
187+
})
188+
189+
expect(cloudRequestStub).to.be.calledWithMatch(
190+
'http://localhost:1234/cy-prompt/errors',
191+
{
192+
cyPromptHash: 'abc123',
193+
projectSlug: 'test-project',
194+
errors: [{
195+
name: 'Error',
196+
message: 'test error',
197+
stack: sinon.match((stack) => stack.includes('<stripped-path>report_cy_prompt_error_spec.ts')),
198+
cyPromptMethod: 'testMethod',
199+
cyPromptMethodArgs: JSON.stringify({ args: ['arg1', { key: '<stripped-path>file.js' }] }),
200+
}],
201+
},
202+
{
203+
headers: {
204+
'Content-Type': 'application/json',
205+
},
206+
},
207+
)
208+
})
209+
210+
it('handles errors in JSON.stringify for cyPromptMethodArgs', () => {
211+
const error = new Error('test error')
212+
const circularObj: any = {}
213+
214+
circularObj.self = circularObj
215+
216+
reportCyPromptError({
217+
cloudApi,
218+
cyPromptHash: 'abc123',
219+
projectSlug: 'test-project',
220+
error,
221+
cyPromptMethod: 'testMethod',
222+
cyPromptMethodArgs: [circularObj],
223+
})
224+
225+
expect(cloudRequestStub).to.be.calledWithMatch(
226+
'http://localhost:1234/cy-prompt/errors',
227+
{
228+
cyPromptHash: 'abc123',
229+
projectSlug: 'test-project',
230+
errors: [{
231+
name: 'Error',
232+
message: 'test error',
233+
stack: sinon.match((stack) => stack.includes('<stripped-path>report_cy_prompt_error_spec.ts')),
234+
cyPromptMethod: 'testMethod',
235+
cyPromptMethodArgs: sinon.match(/Unknown args/),
236+
}],
237+
},
238+
{
239+
headers: {
240+
'Content-Type': 'application/json',
241+
},
242+
},
243+
)
244+
})
245+
246+
it('handles errors in CloudRequest.post', () => {
247+
const error = new Error('test error')
248+
const postError = new Error('post error')
249+
250+
cloudRequestStub.rejects(postError)
251+
252+
reportCyPromptError({
253+
cloudApi,
254+
cyPromptHash: 'abc123',
255+
projectSlug: 'test-project',
256+
error,
257+
cyPromptMethod: 'testMethod',
258+
})
259+
260+
// Just verify the post was called, don't check debug output
261+
expect(cloudRequestStub).to.be.called
262+
})
263+
264+
it('handles errors in payload construction', () => {
265+
const error = new Error('test error')
266+
267+
sinon.stub(JSON, 'stringify').throws(new Error('JSON error'))
268+
269+
reportCyPromptError({
270+
cloudApi,
271+
cyPromptHash: 'abc123',
272+
projectSlug: 'test-project',
273+
error,
274+
cyPromptMethod: 'testMethod',
275+
})
276+
277+
// Just verify the post was called, don't check debug output
278+
expect(cloudRequestStub).to.be.called
279+
})
280+
})
281+
})

packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import os from 'os'
99
import { CloudRequest, createCloudRequest } from '../../../../lib/cloud/api/cloud_request'
1010
import { isRetryableError } from '../../../../lib/cloud/network/is_retryable_error'
1111
import { asyncRetry } from '../../../../lib/util/async_retry'
12-
import * as reportCyPromptErrorPath from '../../../../lib/cloud/api/cy-prompt/report_cy-prompt_error'
12+
import * as reportCyPromptErrorPath from '../../../../lib/cloud/api/cy-prompt/report_cy_prompt_error'
1313

1414
describe('CyPromptLifecycleManager', () => {
1515
let cyPromptLifecycleManager: CyPromptLifecycleManager

0 commit comments

Comments
 (0)