diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index a1ce01556f05..8a69daaf38e5 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -30,7 +30,7 @@ mainBuildFilters: &mainBuildFilters - /^release\/\d+\.\d+\.\d+$/ # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - 'update-v8-snapshot-cache-on-develop' - - 'ryanm/feat/unify-cdp-approach-in-electron' + - 'matth/chore/add-telemetry-realworld-app' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -139,7 +139,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "ryanm/feat/unify-cdp-approach-in-electron" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "matth/chore/add-telemetry-realworld-app" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -752,7 +752,7 @@ commands: command: description: Test command to run to start Cypress tests type: string - default: "yarn cypress:run" + default: "CYPRESS_INTERNAL_ENABLE_TELEMETRY=1 CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY CYPRESS_PROJECT_ID=ypt4pf yarn cypress:run" # if the repo to clone and test is a monorepo, you can # run tests inside a specific subfolder folder: @@ -822,7 +822,7 @@ commands: name: Run tests using browser "<>" working_directory: /tmp/<>/<> command: | - <> -- --browser <> + <> --browser <> --record false - unless: condition: <> steps: @@ -839,7 +839,7 @@ commands: - run: name: Run tests using browser "<>" working_directory: /tmp/<> - command: <> -- --browser <> + command: <> --browser <> --record false - unless: condition: <> steps: @@ -2753,6 +2753,7 @@ linux-x64-workflow: &linux-x64-workflow requires: - create-build-artifacts - test-binary-against-cypress-realworld-app: + context: test-runner:cypress-record-key <<: *mainBuildFilters requires: - create-build-artifacts diff --git a/packages/server/lib/cypress.js b/packages/server/lib/cypress.js index 03d3c9f73dcc..3d029d570edb 100644 --- a/packages/server/lib/cypress.js +++ b/packages/server/lib/cypress.js @@ -153,9 +153,7 @@ module.exports = { debug('from argv %o got options %o', argv, options) - if (options.key) { - telemetry.exporter()?.attachRecordKey(options.key) - } + telemetry.exporter()?.attachRecordKey(options.key) if (options.headless) { // --headless is same as --headed false diff --git a/packages/telemetry/src/span-exporters/cloud-span-exporter.ts b/packages/telemetry/src/span-exporters/cloud-span-exporter.ts index 61e62e0d7125..3dbfb06c8acd 100644 --- a/packages/telemetry/src/span-exporters/cloud-span-exporter.ts +++ b/packages/telemetry/src/span-exporters/cloud-span-exporter.ts @@ -33,14 +33,20 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { enc: OTLPExporterNodeConfigBasePlusEncryption['encryption'] | undefined projectId?: string recordKey?: string + requirementsToExport: 'met'| 'unmet' | 'unknown' sendWithHttp: typeof sendWithHttp constructor (config: OTLPExporterNodeConfigBasePlusEncryption = {}) { super(config) this.enc = config.encryption this.delayedItemsToExport = [] this.sendWithHttp = sendWithHttp + // when encryption is on, requirementsToExport will be set to unknown until projectId and/or record key are attached. + // We will delay sending spans until requirementsToExport is either met or unmet. If unmet we will fail all attempts to send. if (this.enc) { + this.requirementsToExport = 'unknown' this.headers['x-cypress-encrypted'] = '1' + } else { + this.requirementsToExport = 'met' } } @@ -50,6 +56,11 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { */ attachProjectId (projectId: string | null | undefined): void { if (!projectId) { + if (this.requirementsToExport === 'unknown') { + this.requirementsToExport = 'unmet' + this.abortDelayedItems() + } + return } @@ -65,6 +76,11 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { */ attachRecordKey (recordKey: string | null | undefined): void { if (!recordKey) { + if (this.requirementsToExport === 'unknown') { + this.requirementsToExport = 'unmet' + this.abortDelayedItems() + } + return } @@ -77,6 +93,7 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { */ setAuthorizationHeader () { if (this.projectId && this.recordKey) { + this.requirementsToExport = 'met' this.headers.Authorization = `Basic ${Buffer.from(`${this.projectId}:${this.recordKey}`).toString('base64')}` this.sendDelayedItems() } @@ -95,6 +112,14 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { } } + abortDelayedItems () { + this.delayedItemsToExport.forEach((item) => { + item.onError(new Error('Spans cannot be sent, exporter has unmet requirements, either project id or record key are undefined.')) + }) + + this.delayedItemsToExport = [] + } + /** * Overrides send if we need to encrypt the request. * @param objects @@ -113,6 +138,10 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { return } + if (this.requirementsToExport === 'unmet') { + onError(new Error('Spans cannot be sent, exporter has unmet requirements, either project id or record key are undefined.')) + } + let serviceRequest: string if (typeof objects !== 'string') { diff --git a/packages/telemetry/test/span-exporters/cloud-span-exporter.spec.ts b/packages/telemetry/test/span-exporters/cloud-span-exporter.spec.ts index 03269960bb33..cc1c44dab802 100644 --- a/packages/telemetry/test/span-exporters/cloud-span-exporter.spec.ts +++ b/packages/telemetry/test/span-exporters/cloud-span-exporter.spec.ts @@ -11,6 +11,7 @@ describe('cloudSpanExporter', () => { const exporter = new OTLPTraceExporter(genericRequest) expect(exporter.headers['x-cypress-encrypted']).to.equal('1') + expect(exporter.requirementsToExport).to.equal('unknown') expect(exporter.enc).to.not.be.undefined }) @@ -18,6 +19,7 @@ describe('cloudSpanExporter', () => { const exporter = new OTLPTraceExporter() expect(exporter.headers['x-cypress-encrypted']).to.be.undefined + expect(exporter.requirementsToExport).to.equal('met') expect(exporter.enc).to.be.undefined }) }) @@ -42,15 +44,20 @@ describe('cloudSpanExporter', () => { expect(callCount).to.equal(1) }) - it('does nothing if id is not passed', () => { - const exporter = new OTLPTraceExporter() + it('sets requirements to unmet if id is not passed', () => { + const exporter = new OTLPTraceExporter(genericRequest) let callCount = 0 + let abortCallCount = 0 exporter.setAuthorizationHeader = () => { callCount++ } + exporter.abortDelayedItems = () => { + abortCallCount++ + } + expect(exporter.headers['x-project-id']).to.be.undefined expect(exporter.projectId).to.be.undefined @@ -59,6 +66,9 @@ describe('cloudSpanExporter', () => { expect(exporter.headers['x-project-id']).to.be.undefined expect(exporter.projectId).to.be.undefined expect(callCount).to.equal(0) + + expect(exporter.requirementsToExport).to.equal('unmet') + expect(abortCallCount).to.equal(1) }) }) @@ -80,21 +90,29 @@ describe('cloudSpanExporter', () => { expect(callCount).to.equal(1) }) - it('does nothing if record key is not passed', () => { - const exporter = new OTLPTraceExporter() + it('sets requirements to unmet if record key is not passed', () => { + const exporter = new OTLPTraceExporter(genericRequest) let callCount = 0 + let abortCallCount = 0 exporter.setAuthorizationHeader = () => { callCount++ } + exporter.abortDelayedItems = () => { + abortCallCount++ + } + expect(exporter.recordKey).to.be.undefined exporter.attachRecordKey(undefined) expect(exporter.recordKey).to.be.undefined expect(callCount).to.equal(0) + + expect(exporter.requirementsToExport).to.equal('unmet') + expect(abortCallCount).to.equal(1) }) }) @@ -109,10 +127,9 @@ describe('cloudSpanExporter', () => { const authorization = exporter.headers.Authorization - console.log('auth', authorization) - // MTIzOjQ1Ng== is 123:456 base64 encoded expect(authorization).to.equal(`Basic MTIzOjQ1Ng==`) + expect(exporter.requirementsToExport).to.equal('met') }) }) @@ -206,6 +223,24 @@ describe('cloudSpanExporter', () => { }) }) + describe('abortDelayedItems', () => { + it('aborts any delayed items', (done) => { + const exporter = new OTLPTraceExporter() + + exporter.delayedItemsToExport.push({ + serviceRequest: 'req', + onSuccess: () => {}, + onError: (error) => { + expect(error.message).to.equal('Spans cannot be sent, exporter has unmet requirements, either project id or record key are undefined.') + done() + }, + }) + + exporter.abortDelayedItems() + expect(exporter.delayedItemsToExport.length).to.equal(0) + }) + }) + describe('send', () => { it('returns if shutdownOnce.isCalled is true', () => { const exporter = new OTLPTraceExporter() @@ -395,5 +430,30 @@ describe('cloudSpanExporter', () => { expect(exporter.delayedItemsToExport.length).to.equal(1) expect(exporter.delayedItemsToExport[0].serviceRequest).to.equal('string') }) + + it('errors if requirements are unmet', (done) => { + const exporter = new OTLPTraceExporter() + + exporter.requirementsToExport = 'unmet' + + exporter.convert = (objects) => { + throw 'convert should not be called' + } + + exporter.sendWithHttp = (collector, body, contentType, resolve, reject) => { + throw 'sendWithHttp should not be called' + } + + const onSuccess = () => { + throw 'onSuccess should not be called' + } + + const onError = (error) => { + expect(error.message).to.equal('Spans cannot be sent, exporter has unmet requirements, either project id or record key are undefined.') + done() + } + + exporter.send('string', onSuccess, onError) + }) }) })