internal: (studio) improve error reporting (#31546)

* internal: (studio) improvements to how studio can access the protocol database

* fix

* fix build

* refactor due to downstream changes

* feat: capture errors with studio

* types

* add tests

* fix types

* fix typescript

* strip path

* fix tests
This commit is contained in:
Ryan Manuel
2025-04-23 15:54:30 -05:00
committed by GitHub
parent 9697a8639b
commit 38f980e0c4
14 changed files with 457 additions and 148 deletions
@@ -1,21 +1,21 @@
import path from 'path'
import os from 'os'
import { ensureDir, copy, readFile } from 'fs-extra'
import { StudioManager } from '../studio'
import { StudioManager } from '../../studio'
import tar from 'tar'
import { verifySignatureFromFile } from '../encryption'
import { verifySignatureFromFile } from '../../encryption'
import crypto from 'crypto'
import fs from 'fs'
import fetch from 'cross-fetch'
import { agent } from '@packages/network'
import { asyncRetry, linearDelay } from '../../util/async_retry'
import { isRetryableError } from '../network/is_retryable_error'
import { PUBLIC_KEY_VERSION } from '../constants'
import { CloudRequest } from './cloud_request'
import { asyncRetry, linearDelay } from '../../../util/async_retry'
import { isRetryableError } from '../../network/is_retryable_error'
import { PUBLIC_KEY_VERSION } from '../../constants'
import { CloudRequest } from '../cloud_request'
import type { CloudDataSource } from '@packages/data-context/src/sources'
const pkg = require('@packages/root')
const routes = require('../routes')
const routes = require('../../routes')
const _delay = linearDelay(500)
@@ -121,17 +121,19 @@ export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId?
export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource }: { projectId?: string, cloudDataSource: CloudDataSource }): Promise<StudioManager> => {
let script: string
const cloudEnv = (process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = cloudDataSource.getCloudUrl(cloudEnv)
const cloudHeaders = await cloudDataSource.additionalHeaders()
let studioHash: string | undefined
try {
const { studioHash } = await retrieveAndExtractStudioBundle({ projectId })
({ studioHash } = await retrieveAndExtractStudioBundle({ projectId }))
script = await readFile(serverFilePath, 'utf8')
const studioManager = new StudioManager()
const cloudEnv = (process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = cloudDataSource.getCloudUrl(cloudEnv)
const cloudHeaders = await cloudDataSource.additionalHeaders()
await studioManager.setup({
script,
studioPath,
@@ -156,7 +158,19 @@ export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource
actualError = error
}
return StudioManager.createInErrorManager(actualError)
return StudioManager.createInErrorManager({
cloudApi: {
cloudUrl,
cloudHeaders,
CloudRequest,
isRetryableError,
asyncRetry,
},
studioHash,
projectSlug: projectId,
error: actualError,
studioMethod: 'getAndInitializeStudioManager',
})
} finally {
await fs.promises.rm(bundlePath, { force: true })
}
@@ -0,0 +1,101 @@
import type { StudioCloudApi } from '@packages/types/src/studio/studio-server-types'
import Debug from 'debug'
import { stripPath } from '../../strip_path'
const debug = Debug('cypress:server:cloud:api:studio:report_studio_errors')
export interface ReportStudioErrorOptions {
cloudApi: StudioCloudApi
studioHash: string | undefined
projectSlug: string | undefined
error: unknown
studioMethod: string
studioMethodArgs?: unknown[]
}
interface StudioError {
name: string
stack: string
message: string
studioMethod: string
studioMethodArgs?: string
}
interface StudioErrorPayload {
studioHash: string | undefined
projectSlug: string | undefined
errors: StudioError[]
}
export function reportStudioError ({
cloudApi,
studioHash,
projectSlug,
error,
studioMethod,
studioMethodArgs,
}: ReportStudioErrorOptions): void {
debug('Error reported:', error)
// When developing locally, we want to throw the error so we can see it in the console
if (process.env.CYPRESS_LOCAL_STUDIO_PATH) {
throw error
}
let errorObject: Error
if (!(error instanceof Error)) {
errorObject = new Error(String(error))
} else {
errorObject = error
}
let studioMethodArgsString: string | undefined
if (studioMethodArgs) {
try {
studioMethodArgsString = JSON.stringify({
args: studioMethodArgs,
})
} catch (e: unknown) {
studioMethodArgsString = `Unknown args: ${e}`
}
}
try {
const payload: StudioErrorPayload = {
studioHash,
projectSlug,
errors: [{
name: stripPath(errorObject.name ?? `Unknown name`),
stack: stripPath(errorObject.stack ?? `Unknown stack`),
message: stripPath(errorObject.message ?? `Unknown message`),
studioMethod,
studioMethodArgs: studioMethodArgsString,
}],
}
cloudApi.CloudRequest.post(
`${cloudApi.cloudUrl}/studio/errors`,
payload,
{
headers: {
'Content-Type': 'application/json',
...cloudApi.cloudHeaders,
},
},
).catch((e: unknown) => {
debug(
`Error calling StudioManager.reportError: %o, original error %o`,
e,
error,
)
})
} catch (e: unknown) {
debug(
`Error calling StudioManager.reportError: %o, original error %o`,
e,
error,
)
}
}
+1 -12
View File
@@ -4,18 +4,7 @@ const pkg = require('@packages/root')
const api = require('./api').default
const user = require('./user')
const system = require('../util/system')
// strip everything but the file name to remove any sensitive
// data in the path
const pathRe = /'?((\/|\\+|[a-z]:\\)[^\s']+)+'?/ig
const pathSepRe = /[\/\\]+/
const stripPath = (text) => {
return (text || '').replace(pathRe, (path) => {
const fileName = _.last(path.split(pathSepRe)) || ''
return `<stripped-path>${fileName}`
})
}
const { stripPath } = require('./strip_path')
export = {
getErr (err: Error) {
+14
View File
@@ -0,0 +1,14 @@
import { last } from 'lodash'
// strip everything but the file name to remove any sensitive
// data in the path
const pathRe = /'?((\/|\\+|[a-z]:\\)[^\s']+)+'?/ig
const pathSepRe = /[\/\\]+/
export const stripPath = (text: string) => {
return (text || '').replace(pathRe, (path) => {
const fileName = last(path.split(pathSepRe)) || ''
return `<stripped-path>${fileName}`
})
}
+17 -39
View File
@@ -1,13 +1,10 @@
import type { StudioErrorReport, StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions } from '@packages/types'
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions } from '@packages/types'
import type { Router } from 'express'
import type { Socket } from 'socket.io'
import fetch from 'cross-fetch'
import pkg from '@packages/root'
import os from 'os'
import { agent } from '@packages/network'
import Debug from 'debug'
import { requireScript } from './require_script'
import path from 'path'
import { reportStudioError, ReportStudioErrorOptions } from './api/studio/report_studio_error'
interface StudioServer { default: StudioServerDefaultShape }
@@ -20,21 +17,26 @@ interface SetupOptions {
}
const debug = Debug('cypress:server:studio')
const routes = require('./routes')
export class StudioManager implements StudioManagerShape {
status: StudioStatus = 'NOT_INITIALIZED'
isProtocolEnabled: boolean = false
protocolManager: ProtocolManagerShape | undefined
private _studioServer: StudioServerShape | undefined
private _studioHash: string | undefined
static createInErrorManager (error: Error): StudioManager {
static createInErrorManager ({ cloudApi, studioHash, projectSlug, error, studioMethod, studioMethodArgs }: ReportStudioErrorOptions): StudioManager {
const manager = new StudioManager()
manager.status = 'IN_ERROR'
manager.reportError(error).catch(() => { })
reportStudioError({
cloudApi,
studioHash,
projectSlug,
error,
studioMethod,
studioMethodArgs,
})
return manager
}
@@ -43,13 +45,13 @@ export class StudioManager implements StudioManagerShape {
const { createStudioServer } = requireScript<StudioServer>(script).default
this._studioServer = await createStudioServer({
studioHash,
studioPath,
projectSlug,
cloudApi,
betterSqlite3Path: path.dirname(require.resolve('better-sqlite3/package.json')),
})
this._studioHash = studioHash
this.status = 'INITIALIZED'
}
@@ -77,32 +79,11 @@ export class StudioManager implements StudioManagerShape {
await this.invokeAsync('destroy', { isEssential: true })
}
private async reportError (error: Error): Promise<void> {
reportError (error: unknown, studioMethod: string, ...studioMethodArgs: unknown[]): void {
try {
const payload: StudioErrorReport = {
studioHash: this._studioHash,
errors: [{
name: error.name ?? `Unknown name`,
stack: error.stack ?? `Unknown stack`,
message: error.message ?? `Unknown message`,
}],
}
const body = JSON.stringify(payload)
await fetch(routes.apiRoutes.studioErrors() as string, {
// @ts-expect-error - this is supported
agent,
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': os.platform(),
'x-arch': os.arch(),
},
})
this._studioServer?.reportError(error, studioMethod, ...studioMethodArgs)
} catch (e) {
// If we fail to report the error, we shouldn't try and report it again
debug(`Error calling StudioManager.reportError: %o, original error %o`, e, error)
}
}
@@ -129,8 +110,7 @@ export class StudioManager implements StudioManagerShape {
}
this.status = 'IN_ERROR'
// Call and forget this, we don't want to block the main thread
this.reportError(actualError).catch(() => { })
this.reportError(actualError, method, ...args)
}
}
@@ -156,10 +136,8 @@ export class StudioManager implements StudioManagerShape {
}
this.status = 'IN_ERROR'
// Call and forget this, we don't want to block the main thread
this.reportError(actualError).catch(() => { })
this.reportError(actualError, method, ...args)
// TODO: Figure out errors
return undefined
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ import ProtocolManager from './cloud/protocol'
import { ServerBase } from './server-base'
import type Protocol from 'devtools-protocol'
import type { ServiceWorkerClientEvent } from '@packages/proxy/lib/http/util/service-worker-manager'
import { getAndInitializeStudioManager } from './cloud/api/get_and_initialize_studio_manager'
import { getAndInitializeStudioManager } from './cloud/api/studio/get_and_initialize_studio_manager'
import api from './cloud/api'
import type { StudioManager } from './cloud/studio'
import { v4 } from 'uuid'
@@ -17,10 +17,14 @@ class StudioServer implements StudioServerShape {
return Promise.resolve()
}
reportError (error: Error, method: string, ...args: any[]): void {
// This is a test implementation that does nothing
}
destroy (): Promise<void> {
return Promise.resolve()
}
addSocketListeners (socket: Socket): void {
// This is a test implementation that does nothing
}
@@ -1,13 +1,13 @@
import { Readable, Writable } from 'stream'
import { proxyquire, sinon } from '../../../spec_helper'
import { HttpError } from '../../../../lib/cloud/network/http_error'
import { CloudRequest } from '../../../../lib/cloud/api/cloud_request'
import { isRetryableError } from '../../../../lib/cloud/network/is_retryable_error'
import { asyncRetry } from '../../../../lib/util/async_retry'
import { proxyquire, sinon } from '../../../../spec_helper'
import { HttpError } from '../../../../../lib/cloud/network/http_error'
import { CloudRequest } from '../../../../../lib/cloud/api/cloud_request'
import { isRetryableError } from '../../../../../lib/cloud/network/is_retryable_error'
import { asyncRetry } from '../../../../../lib/util/async_retry'
import { CloudDataSource } from '@packages/data-context/src/sources'
describe('getAndInitializeStudioManager', () => {
let getAndInitializeStudioManager: typeof import('@packages/server/lib/cloud/api/get_and_initialize_studio_manager').getAndInitializeStudioManager
let getAndInitializeStudioManager: typeof import('@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager').getAndInitializeStudioManager
let rmStub: sinon.SinonStub = sinon.stub()
let ensureStub: sinon.SinonStub = sinon.stub()
let copyStub: sinon.SinonStub = sinon.stub()
@@ -34,7 +34,7 @@ describe('getAndInitializeStudioManager', () => {
createInErrorManagerStub = sinon.stub()
studioManagerSetupStub = sinon.stub()
getAndInitializeStudioManager = (proxyquire('../lib/cloud/api/get_and_initialize_studio_manager', {
getAndInitializeStudioManager = (proxyquire('../lib/cloud/api/studio/get_and_initialize_studio_manager', {
fs: {
promises: {
rm: rmStub.resolves(),
@@ -54,10 +54,10 @@ describe('getAndInitializeStudioManager', () => {
tar: {
extract: extractStub.resolves(),
},
'../encryption': {
'../../encryption': {
verifySignatureFromFile: verifySignatureFromFileStub,
},
'../studio': {
'../../studio': {
StudioManager: class StudioManager {
static createInErrorManager = createInErrorManagerStub
setup = (...options) => studioManagerSetupStub(...options)
@@ -67,7 +67,7 @@ describe('getAndInitializeStudioManager', () => {
'@packages/root': {
version: '1.2.3',
},
}) as typeof import('@packages/server/lib/cloud/api/get_and_initialize_studio_manager')).getAndInitializeStudioManager
}) as typeof import('@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager')).getAndInitializeStudioManager
})
afterEach(() => {
@@ -314,7 +314,19 @@ describe('getAndInitializeStudioManager', () => {
encrypt: 'signed',
})
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(AggregateError))
expect(createInErrorManagerStub).to.be.calledWithMatch({
error: sinon.match.instanceOf(AggregateError),
cloudApi: {
cloudUrl: 'http://localhost:1234',
cloudHeaders: {
a: 'b',
c: 'd',
},
},
studioHash: undefined,
projectSlug: '12345',
studioMethod: 'getAndInitializeStudioManager',
})
})
it('throws an error and returns a studio manager in error state if the signature verification fails', async () => {
@@ -367,7 +379,16 @@ describe('getAndInitializeStudioManager', () => {
})
expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/studio/bundle.tar', '159')
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to verify studio signature')))
expect(createInErrorManagerStub).to.be.calledWithMatch({
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to verify studio signature')),
cloudApi: {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { a: 'b', c: 'd' },
},
studioHash: undefined,
projectSlug: '12345',
studioMethod: 'getAndInitializeStudioManager',
})
})
it('throws an error if there is no signature in the response headers', async () => {
@@ -397,7 +418,16 @@ describe('getAndInitializeStudioManager', () => {
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to get studio signature')))
expect(createInErrorManagerStub).to.be.calledWithMatch({
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to get studio signature')),
cloudApi: {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { a: 'b', c: 'd' },
},
studioHash: undefined,
projectSlug: '12345',
studioMethod: 'getAndInitializeStudioManager',
})
})
})
})
@@ -0,0 +1,215 @@
import { expect } from 'chai'
import { sinon } from '../../../../spec_helper'
import { reportStudioError } from '@packages/server/lib/cloud/api/studio/report_studio_error'
describe('lib/cloud/api/studio/report_studio_error', () => {
let cloudRequestStub: sinon.SinonStub
let cloudApi: any
beforeEach(() => {
cloudRequestStub = sinon.stub()
cloudApi = {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { 'x-cypress-version': '1.2.3' },
CloudRequest: {
post: cloudRequestStub,
},
}
})
afterEach(() => {
sinon.restore()
})
describe('reportStudioError', () => {
it('throws error when CYPRESS_LOCAL_STUDIO_PATH is set', () => {
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio'
const error = new Error('test error')
expect(() => {
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
}).to.throw('test error')
})
it('converts non-Error objects to Error', () => {
const error = 'string error'
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'string error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_studio_error_spec.ts')),
studioMethod: 'testMethod',
studioMethodArgs: undefined,
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('handles Error objects correctly', () => {
const error = new Error('test error')
error.stack = 'test stack'
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: 'test stack',
studioMethod: 'testMethod',
studioMethodArgs: undefined,
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('includes studioMethodArgs when provided', () => {
const error = new Error('test error')
const args = ['arg1', { key: 'value' }]
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
studioMethodArgs: args,
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_studio_error_spec.ts')),
studioMethod: 'testMethod',
studioMethodArgs: JSON.stringify({ args }),
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('handles errors in JSON.stringify for studioMethodArgs', () => {
const error = new Error('test error')
const circularObj: any = {}
circularObj.self = circularObj
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
studioMethodArgs: [circularObj],
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_studio_error_spec.ts')),
studioMethod: 'testMethod',
studioMethodArgs: sinon.match(/Unknown args/),
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('handles errors in CloudRequest.post', () => {
const error = new Error('test error')
const postError = new Error('post error')
cloudRequestStub.rejects(postError)
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: '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'))
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
// Just verify the post was called, don't check debug output
expect(cloudRequestStub).to.be.called
})
})
})
+24 -66
View File
@@ -6,8 +6,6 @@ import esbuild from 'esbuild'
import type { StudioManager as StudioManagerShape } from '@packages/server/lib/cloud/studio'
import os from 'os'
const pkg = require('@packages/root')
const { outputFiles: [{ contents: stubStudioRaw }] } = esbuild.buildSync({
entryPoints: [path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'studio', 'test-studio.ts')],
bundle: true,
@@ -18,15 +16,17 @@ const { outputFiles: [{ contents: stubStudioRaw }] } = esbuild.buildSync({
const stubStudio = new TextDecoder('utf-8').decode(stubStudioRaw)
describe('lib/cloud/studio', () => {
let stubbedCrossFetch: sinon.SinonStub
let studioManager: StudioManagerShape
let studio: StudioServerShape
let StudioManager: typeof import('@packages/server/lib/cloud/studio').StudioManager
let reportStudioError: sinon.SinonStub
beforeEach(async () => {
stubbedCrossFetch = sinon.stub()
reportStudioError = sinon.stub()
StudioManager = (proxyquire('../lib/cloud/studio', {
'cross-fetch': stubbedCrossFetch,
'./api/studio/report_studio_error': {
reportStudioError,
},
}) as typeof import('@packages/server/lib/cloud/studio')).StudioManager
studioManager = new StudioManager()
@@ -53,30 +53,12 @@ describe('lib/cloud/studio', () => {
const error = new Error('foo')
sinon.stub(studio, 'initializeRoutes').throws(error)
sinon.stub(studio, 'reportError')
studioManager.initializeRoutes({} as any)
expect(studioManager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.eq('abcdefg')
expect(parsedBody.errors[0].name).to.eq(error.name)
expect(parsedBody.errors[0].stack).to.eq(error.stack)
expect(parsedBody.errors[0].message).to.eq(error.message)
return true
}),
})
expect(studio.reportError).to.be.calledWithMatch(error, 'initializeRoutes', {})
})
})
@@ -85,58 +67,34 @@ describe('lib/cloud/studio', () => {
const error = new Error('foo')
sinon.stub(studio, 'canAccessStudioAI').throws(error)
sinon.stub(studio, 'reportError')
await studioManager.canAccessStudioAI({} as any)
expect(studioManager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.eq('abcdefg')
expect(parsedBody.errors[0].name).to.eq(error.name)
expect(parsedBody.errors[0].stack).to.eq(error.stack)
expect(parsedBody.errors[0].message).to.eq(error.message)
return true
}),
})
expect(studio.reportError).to.be.calledWithMatch(error, 'canAccessStudioAI', {})
})
})
describe('createInErrorManager', () => {
it('creates a studio manager in error state', () => {
const manager = StudioManager.createInErrorManager(new Error('foo'))
const error = new Error('foo')
const manager = StudioManager.createInErrorManager({
error,
cloudApi: {} as any,
studioHash: 'abcdefg',
projectSlug: '1234',
studioMethod: 'initializeRoutes',
})
expect(manager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.be.undefined
expect(parsedBody.errors[0].name).to.eq('Error')
expect(parsedBody.errors[0].stack).to.be.a('string')
expect(parsedBody.errors[0].message).to.eq('foo')
return true
}),
expect(reportStudioError).to.be.calledWithMatch({
error,
cloudApi: {} as any,
studioHash: 'abcdefg',
projectSlug: '1234',
studioMethod: 'initializeRoutes',
studioMethodArgs: undefined,
})
})
})
+1 -1
View File
@@ -13,7 +13,7 @@ const savedState = require(`../../lib/saved_state`)
const runEvents = require(`../../lib/plugins/run_events`)
const system = require(`../../lib/util/system`)
const { getCtx } = require(`../../lib/makeDataContext`)
const studio = require('../../lib/cloud/api/get_and_initialize_studio_manager')
const studio = require('../../lib/cloud/api/studio/get_and_initialize_studio_manager')
const api = require('../../lib/cloud/api').default
const { ProtocolManager } = require('../../lib/cloud/protocol')
const browsers = require('../../lib/browsers')
@@ -25,6 +25,7 @@ type AsyncRetry = <TArgs extends any[], TResult>(
) => (...args: TArgs) => Promise<TResult>
export interface StudioServerOptions {
studioHash?: string
studioPath: string
projectSlug?: string
cloudApi: StudioCloudApi
@@ -40,6 +41,11 @@ export interface StudioServerShape {
canAccessStudioAI(browser: Cypress.Browser): Promise<boolean>
addSocketListeners(socket: Socket): void
initializeStudioAI(options: StudioAIInitializeOptions): Promise<void>
reportError(
error: unknown,
studioMethod: string,
...studioMethodArgs: unknown[]
): void
destroy(): Promise<void>
}
+1 -1
View File
@@ -97,7 +97,7 @@ module.exports = async function (params) {
const cloudProtocolFileSource = await getProtocolFileSource(cloudProtocolFilePath)
const projectBaseFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/project-base.ts')
const projectBaseFileSource = await getStudioFileSource(projectBaseFilePath)
const getAndInitializeStudioManagerFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api/get_and_initialize_studio_manager.ts')
const getAndInitializeStudioManagerFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts')
const getAndInitializeStudioManagerFileSource = await getStudioFileSource(getAndInitializeStudioManagerFilePath)
await Promise.all([
@@ -2,7 +2,7 @@ process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV ?? 'producti
import path from 'path'
import fs from 'fs-extra'
import { retrieveAndExtractStudioBundle, studioPath } from '@packages/server/lib/cloud/api/get_and_initialize_studio_manager'
import { retrieveAndExtractStudioBundle, studioPath } from '@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager'
export const downloadStudioTypes = async (): Promise<void> => {
await retrieveAndExtractStudioBundle({ projectId: 'ypt4pf' })