mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-02 04:50:06 -05:00
internal: (studio) capture anonymous telemetry for studio initialization (#31555)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions } from '@packages/types'
|
||||
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
import type { Socket } from 'socket.io'
|
||||
import Debug from 'debug'
|
||||
@@ -61,6 +61,15 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
}
|
||||
|
||||
async captureStudioEvent (event: StudioEvent): Promise<void> {
|
||||
if (this._studioServer) {
|
||||
// this request is not essential - we don't want studio to error out if a telemetry request fails
|
||||
return (await this.invokeAsync('captureStudioEvent', { isEssential: false }, event))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
addSocketListeners (socket: Socket): void {
|
||||
if (this._studioServer) {
|
||||
this.invokeSync('addSocketListeners', { isEssential: true }, socket)
|
||||
@@ -119,7 +128,7 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts invoking a synchronous method on the StudioServer instance, so we can handle
|
||||
* Abstracts invoking an asynchronous method on the StudioServer instance, so we can handle
|
||||
* errors in a uniform way
|
||||
*/
|
||||
private async invokeAsync <K extends StudioServerAsyncMethods> (method: K, { isEssential }: { isEssential: boolean }, ...args: Parameters<StudioServerShape[K]>): Promise<ReturnType<StudioServerShape[K]> | undefined> {
|
||||
@@ -139,7 +148,11 @@ export class StudioManager implements StudioManagerShape {
|
||||
actualError = error
|
||||
}
|
||||
|
||||
this.status = 'IN_ERROR'
|
||||
// only set error state if this request is essential
|
||||
if (isEssential) {
|
||||
this.status = 'IN_ERROR'
|
||||
}
|
||||
|
||||
this.reportError(actualError, method, ...args)
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SocketCt } from './socket-ct'
|
||||
import { SocketE2E } from './socket-e2e'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
import system from './util/system'
|
||||
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands } from '@packages/types'
|
||||
import { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands, StudioMetricsTypes } from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
import { createHmac } from 'crypto'
|
||||
import { ServerBase } from './server-base'
|
||||
@@ -431,6 +431,23 @@ export class ProjectBase extends EE {
|
||||
|
||||
const studio = await this.ctx.coreData.studioLifecycleManager?.getStudio()
|
||||
|
||||
try {
|
||||
studio?.captureStudioEvent({
|
||||
type: StudioMetricsTypes.STUDIO_STARTED,
|
||||
machineId: await this.ctx.coreData.machineId,
|
||||
projectId: this.cfg.projectId,
|
||||
browser: this.browser ? {
|
||||
name: this.browser.name,
|
||||
family: this.browser.family,
|
||||
channel: this.browser.channel,
|
||||
version: this.browser.version,
|
||||
} : undefined,
|
||||
cypressVersion: pkg.version,
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error capturing studio event:', error)
|
||||
}
|
||||
|
||||
if (this.spec && studio?.protocolManager) {
|
||||
const canAccessStudioAI = await studio?.canAccessStudioAI(this.browser) ?? false
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ class StudioServer implements StudioServerShape {
|
||||
addSocketListeners (socket: Socket): void {
|
||||
// This is a test implementation that does nothing
|
||||
}
|
||||
|
||||
captureStudioEvent (event: StudioEvent): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const studioServerDefault: StudioServerDefaultShape = {
|
||||
|
||||
@@ -75,6 +75,16 @@ describe('lib/cloud/studio', () => {
|
||||
expect(studioManager.status).to.eq('IN_ERROR')
|
||||
expect(studio.reportError).to.be.calledWithMatch(error, 'canAccessStudioAI', {})
|
||||
})
|
||||
|
||||
it('does not set state IN_ERROR when a non-essential async method fails', async () => {
|
||||
const error = new Error('foo')
|
||||
|
||||
sinon.stub(studio, 'captureStudioEvent').throws(error)
|
||||
|
||||
await studioManager.captureStudioEvent({} as any)
|
||||
|
||||
expect(studioManager.status).to.eq('ENABLED')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createInErrorManager', () => {
|
||||
|
||||
@@ -661,6 +661,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const mockSetupProtocol = sinon.stub()
|
||||
const mockBeforeSpec = sinon.stub()
|
||||
const mockAccessStudioAI = sinon.stub().resolves(true)
|
||||
const mockCaptureStudioEvent = sinon.stub().resolves()
|
||||
|
||||
this.project.spec = {}
|
||||
|
||||
@@ -672,6 +673,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const studioManager = new StudioManager()
|
||||
|
||||
studioManager.canAccessStudioAI = mockAccessStudioAI
|
||||
studioManager.captureStudioEvent = mockCaptureStudioEvent
|
||||
studioManager.protocolManager = {
|
||||
setupProtocol: mockSetupProtocol,
|
||||
beforeSpec: mockBeforeSpec,
|
||||
@@ -715,6 +717,18 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const { canAccessStudioAI } = await studioInitPromise
|
||||
|
||||
expect(canAccessStudioAI).to.be.true
|
||||
expect(mockCaptureStudioEvent).to.be.calledWith({
|
||||
type: 'studio:started',
|
||||
machineId: 'test-machine-id',
|
||||
projectId: 'test-project-id',
|
||||
browser: {
|
||||
name: 'chrome',
|
||||
family: 'chromium',
|
||||
channel: undefined,
|
||||
version: undefined,
|
||||
},
|
||||
cypressVersion: pkg.version,
|
||||
})
|
||||
|
||||
expect(mockSetupProtocol).to.be.calledOnce
|
||||
expect(mockBeforeSpec).to.be.calledOnce
|
||||
@@ -736,6 +750,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const mockSetupProtocol = sinon.stub()
|
||||
const mockBeforeSpec = sinon.stub()
|
||||
const mockAccessStudioAI = sinon.stub().resolves(true)
|
||||
const mockCaptureStudioEvent = sinon.stub().resolves()
|
||||
|
||||
this.project.spec = {}
|
||||
|
||||
@@ -747,14 +762,15 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const studioManager = new StudioManager()
|
||||
|
||||
studioManager.canAccessStudioAI = mockAccessStudioAI
|
||||
|
||||
studioManager.captureStudioEvent = mockCaptureStudioEvent
|
||||
const studioLifecycleManager = new StudioLifecycleManager()
|
||||
|
||||
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
|
||||
|
||||
// Set up the studio manager promise directly
|
||||
studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager)
|
||||
|
||||
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
|
||||
|
||||
// Create a browser object
|
||||
this.project.browser = {
|
||||
name: 'chrome',
|
||||
@@ -783,6 +799,18 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const { canAccessStudioAI } = await studioInitPromise
|
||||
|
||||
expect(canAccessStudioAI).to.be.false
|
||||
expect(mockCaptureStudioEvent).to.be.calledWith({
|
||||
type: 'studio:started',
|
||||
machineId: 'test-machine-id',
|
||||
projectId: 'test-project-id',
|
||||
browser: {
|
||||
name: 'chrome',
|
||||
family: 'chromium',
|
||||
channel: undefined,
|
||||
version: undefined,
|
||||
},
|
||||
cypressVersion: pkg.version,
|
||||
})
|
||||
|
||||
expect(mockSetupProtocol).not.to.be.called
|
||||
expect(mockBeforeSpec).not.to.be.called
|
||||
@@ -796,6 +824,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const mockSetupProtocol = sinon.stub()
|
||||
const mockBeforeSpec = sinon.stub()
|
||||
const mockAccessStudioAI = sinon.stub().resolves(false)
|
||||
const mockCaptureStudioEvent = sinon.stub().resolves()
|
||||
|
||||
this.project.spec = {}
|
||||
|
||||
@@ -807,6 +836,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const studioManager = new StudioManager()
|
||||
|
||||
studioManager.canAccessStudioAI = mockAccessStudioAI
|
||||
studioManager.captureStudioEvent = mockCaptureStudioEvent
|
||||
studioManager.protocolManager = {
|
||||
setupProtocol: mockSetupProtocol,
|
||||
beforeSpec: mockBeforeSpec,
|
||||
@@ -816,10 +846,10 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
|
||||
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
|
||||
|
||||
// Set up the studio manager promise directly
|
||||
studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager)
|
||||
|
||||
// Create a browser object
|
||||
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
|
||||
|
||||
this.project.browser = {
|
||||
name: 'chrome',
|
||||
family: 'chromium',
|
||||
@@ -847,6 +877,18 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
const { canAccessStudioAI } = await studioInitPromise
|
||||
|
||||
expect(canAccessStudioAI).to.be.false
|
||||
expect(mockCaptureStudioEvent).to.be.calledWith({
|
||||
type: 'studio:started',
|
||||
machineId: 'test-machine-id',
|
||||
projectId: 'test-project-id',
|
||||
browser: {
|
||||
name: 'chrome',
|
||||
family: 'chromium',
|
||||
channel: undefined,
|
||||
version: undefined,
|
||||
},
|
||||
cypressVersion: pkg.version,
|
||||
})
|
||||
|
||||
expect(mockSetupProtocol).not.to.be.called
|
||||
expect(mockBeforeSpec).not.to.be.called
|
||||
|
||||
@@ -242,6 +242,7 @@ describe('lib/routes', () => {
|
||||
status: 'INITIALIZED',
|
||||
initializeRoutes: sinon.stub(),
|
||||
isProtocolEnabled: false,
|
||||
captureStudioEvent: sinon.stub(),
|
||||
canAccessStudioAI: sinon.stub(),
|
||||
setProtocolDb: sinon.stub(),
|
||||
addSocketListeners: sinon.stub(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ProtocolManagerShape } from '../protocol'
|
||||
import type { StudioServerShape } from './studio-server-types'
|
||||
import type { StudioServerShape, StudioEvent } from './studio-server-types'
|
||||
|
||||
export * from './studio-server-types'
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface StudioManagerShape extends StudioServerShape {
|
||||
status: StudioStatus
|
||||
isProtocolEnabled: boolean
|
||||
protocolManager?: ProtocolManagerShape
|
||||
captureStudioEvent: (event: StudioEvent) => Promise<void>
|
||||
}
|
||||
|
||||
export interface StudioLifecycleManagerShape {
|
||||
|
||||
@@ -4,6 +4,26 @@ import type { Router } from 'express'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import type { Socket } from 'socket.io'
|
||||
|
||||
export const StudioMetricsTypes = {
|
||||
STUDIO_STARTED: 'studio:started',
|
||||
} as const
|
||||
|
||||
export type StudioMetricsType =
|
||||
(typeof StudioMetricsTypes)[keyof typeof StudioMetricsTypes]
|
||||
|
||||
export interface StudioEvent {
|
||||
type: StudioMetricsType
|
||||
machineId: string | null
|
||||
projectId?: string
|
||||
browser?: {
|
||||
name: string
|
||||
family: string
|
||||
channel?: string
|
||||
version?: string
|
||||
}
|
||||
cypressVersion?: string
|
||||
}
|
||||
|
||||
interface RetryOptions {
|
||||
maxAttempts: number
|
||||
retryDelay?: (attempt: number) => number
|
||||
@@ -47,6 +67,7 @@ export interface StudioServerShape {
|
||||
...studioMethodArgs: unknown[]
|
||||
): void
|
||||
destroy(): Promise<void>
|
||||
captureStudioEvent(event: StudioEvent): Promise<void>
|
||||
}
|
||||
|
||||
export interface StudioServerDefaultShape {
|
||||
|
||||
Reference in New Issue
Block a user