diff --git a/packages/data-context/src/sources/HtmlDataSource.ts b/packages/data-context/src/sources/HtmlDataSource.ts index 961a1f7b856f..a8f377e2f1ea 100644 --- a/packages/data-context/src/sources/HtmlDataSource.ts +++ b/packages/data-context/src/sources/HtmlDataSource.ts @@ -120,7 +120,7 @@ export class HtmlDataSource { window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)}; window.__CYPRESS_TESTING_TYPE__ = '${this.ctx.coreData.currentTestingType}' window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.activeBrowser)} - ${telemetry.isEnabled() ? `window.__CYPRESS_TELEMETRY__ = ${JSON.stringify({ context: telemetry.getActiveContextObject() })}` : ''} + ${telemetry.isEnabled() ? `window.__CYPRESS_TELEMETRY__ = ${JSON.stringify({ context: telemetry.getActiveContextObject(), resources: telemetry.getResources() })}` : ''} ${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET ? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';` : ''} `) diff --git a/packages/frontend-shared/src/graphql/urqlClient.ts b/packages/frontend-shared/src/graphql/urqlClient.ts index 0a38abb54c16..f6ed7bb23e33 100644 --- a/packages/frontend-shared/src/graphql/urqlClient.ts +++ b/packages/frontend-shared/src/graphql/urqlClient.ts @@ -103,7 +103,6 @@ declare global { __RUN_MODE_SPECS__: SpecFile[] __CYPRESS_TESTING_TYPE__: 'e2e' | 'component' __CYPRESS_BROWSER__: Partial & {majorVersion: string | number} - __CYPRESS_TELEMETRY__?: {context: {traceparent: string}} __CYPRESS_CONFIG__: { base64Config: string namespace: AutomationElementId diff --git a/packages/telemetry/src/browser.ts b/packages/telemetry/src/browser.ts index 9b5a2a5a07ba..6bd164ba0046 100644 --- a/packages/telemetry/src/browser.ts +++ b/packages/telemetry/src/browser.ts @@ -1,4 +1,4 @@ -import type { Span } from '@opentelemetry/api' +import type { Span, Attributes } from '@opentelemetry/api' import type { startSpanOptions, findActiveSpanOptions, contextObject } from './index' import { Telemetry as TelemetryClass, TelemetryNoop } from './index' import { WebTracerProvider } from '@opentelemetry/sdk-trace-web' @@ -8,7 +8,7 @@ import { OTLPTraceExporter } from './span-exporters/websocket-span-exporter' declare global { interface Window { - __CYPRESS_TELEMETRY__?: {context: {traceparent: string}} + __CYPRESS_TELEMETRY__?: {context: {traceparent: string}, resources: Attributes} cypressTelemetrySingleton?: TelemetryClass | TelemetryNoop } } @@ -32,7 +32,7 @@ const init = ({ namespace, config }: { namespace: string, config: {version: stri throw ('Telemetry instance has already be initialized') } - const { context } = window.__CYPRESS_TELEMETRY__ + const { context, resources } = window.__CYPRESS_TELEMETRY__ // We always use the websocket exporter for browser telemetry const exporter = new OTLPTraceExporter() @@ -51,6 +51,7 @@ const init = ({ namespace, config }: { namespace: string, config: {version: stri // TODO: create a browser batch span processor to account for navigation. // See https://github.com/open-telemetry/opentelemetry-js/issues/2613 SpanProcessor: SimpleSpanProcessor, + resources, }) window.cypressTelemetrySingleton = telemetryInstance diff --git a/packages/telemetry/src/index.ts b/packages/telemetry/src/index.ts index d91ad3b0f613..92d213beb445 100644 --- a/packages/telemetry/src/index.ts +++ b/packages/telemetry/src/index.ts @@ -1,4 +1,4 @@ -import type { Span, SpanOptions, Tracer, Context } from '@opentelemetry/api' +import type { Span, SpanOptions, Tracer, Context, Attributes } from '@opentelemetry/api' import type { BasicTracerProvider, SimpleSpanProcessor, BatchSpanProcessor, SpanExporter } from '@opentelemetry/sdk-trace-base' import type { DetectorSync } from '@opentelemetry/resources' @@ -30,6 +30,7 @@ export interface TelemetryApi { findActiveSpan(fn: findActiveSpanOptions): Span | undefined endActiveSpanAndChildren (span?: Span | undefined): void getActiveContextObject (): contextObject + getResources (): Attributes shutdown (): Promise getExporter (): SpanExporter | undefined setRootContext (rootContextObject?: contextObject): void @@ -51,6 +52,7 @@ export class Telemetry implements TelemetryApi { version, SpanProcessor, exporter, + resources = {}, }: { namespace?: string Provider: typeof BasicTracerProvider @@ -59,6 +61,7 @@ export class Telemetry implements TelemetryApi { version: string SpanProcessor: typeof SimpleSpanProcessor | typeof BatchSpanProcessor exporter: SpanExporter + resources?: Attributes }) { // For troubleshooting, set the log level to DiagLogLevel.DEBUG // diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ALL) @@ -66,6 +69,7 @@ export class Telemetry implements TelemetryApi { // Setup default resources const resource = Resource.default().merge( new Resource({ + ...resources, [ SemanticResourceAttributes.SERVICE_NAME ]: 'cypress-app', [ SemanticResourceAttributes.SERVICE_NAMESPACE ]: namespace, [ SemanticResourceAttributes.SERVICE_VERSION ]: version, @@ -217,6 +221,14 @@ export class Telemetry implements TelemetryApi { return myCtx } + /** + * Gets a list of the resources currently set on the provider. + * @returns Attributes of resources + */ + getResources (): Attributes { + return this.provider.resource.attributes + } + /** * Shuts down telemetry and flushes any batched spans. * @returns promise @@ -266,6 +278,9 @@ export class TelemetryNoop implements TelemetryApi { getActiveContextObject (): contextObject { return {} } + getResources () { + return {} + } shutdown () { return Promise.resolve() } diff --git a/packages/telemetry/src/node.ts b/packages/telemetry/src/node.ts index bca9f8ffee75..bd218d5acb04 100644 --- a/packages/telemetry/src/node.ts +++ b/packages/telemetry/src/node.ts @@ -69,6 +69,7 @@ export const telemetry = { findActiveSpan: (arg: findActiveSpanOptions) => telemetryInstance.findActiveSpan(arg), endActiveSpanAndChildren: (arg?: Span): void => telemetryInstance.endActiveSpanAndChildren(arg), getActiveContextObject: () => telemetryInstance.getActiveContextObject(), + getResources: () => telemetryInstance.getResources(), shutdown: () => telemetryInstance.shutdown(), exporter: (): void | OTLPTraceExporterIpc | OTLPTraceExporterCloud => telemetryInstance.getExporter() as void | OTLPTraceExporterIpc | OTLPTraceExporterCloud, } diff --git a/packages/telemetry/src/span-exporters/cloud-span-exporter.ts b/packages/telemetry/src/span-exporters/cloud-span-exporter.ts index db57c3611217..61e62e0d7125 100644 --- a/packages/telemetry/src/span-exporters/cloud-span-exporter.ts +++ b/packages/telemetry/src/span-exporters/cloud-span-exporter.ts @@ -31,6 +31,8 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { onError: (error: OTLPExporterError) => void }[] enc: OTLPExporterNodeConfigBasePlusEncryption['encryption'] | undefined + projectId?: string + recordKey?: string sendWithHttp: typeof sendWithHttp constructor (config: OTLPExporterNodeConfigBasePlusEncryption = {}) { super(config) @@ -51,8 +53,10 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { return } + // Continue to send this header for passivity until the cloud is released. this.headers['x-project-id'] = projectId - this.sendDelayedItems() + this.projectId = projectId + this.setAuthorizationHeader() } /** @@ -64,15 +68,25 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { return } - this.headers['x-record-key'] = recordKey - this.sendDelayedItems() + this.recordKey = recordKey + this.setAuthorizationHeader() + } + + /** + * Sets the auth header based on the project id and record key. + */ + setAuthorizationHeader () { + if (this.projectId && this.recordKey) { + this.headers.Authorization = `Basic ${Buffer.from(`${this.projectId}:${this.recordKey}`).toString('base64')}` + this.sendDelayedItems() + } } /** * exports delayed spans if both the record key and project id are present */ sendDelayedItems () { - if (this.headers['x-project-id'] && this.headers['x-record-key']) { + if (this.headers.Authorization) { this.delayedItemsToExport.forEach((item) => { this.send(item.serviceRequest, item.onSuccess, item.onError) }) @@ -107,8 +121,8 @@ export class OTLPTraceExporter extends OTLPTraceExporterHttp { serviceRequest = objects } - // Delay items if we want encryption but don't have a project id and a record key - if (this.enc && !(this.headers['x-project-id'] && this.headers['x-record-key'])) { + // Delay items if we want encryption but don't have an authorization header + if (this.enc && !this.headers.Authorization) { this.delayedItemsToExport.push({ serviceRequest, onSuccess, onError }) return diff --git a/packages/telemetry/test/browser.spec.ts b/packages/telemetry/test/browser.spec.ts index 99db41445c13..1d53d398f916 100644 --- a/packages/telemetry/test/browser.spec.ts +++ b/packages/telemetry/test/browser.spec.ts @@ -78,10 +78,14 @@ describe('telemetry is disabled', () => { describe('telemetry is enabled', () => { before('init', () => { + // @ts-expect-error global.window.__CYPRESS_TELEMETRY__ = { context: { traceparent: '00-a14c8519972996a2a0748f2c8db5a775-4ad8bd26672a01b0-01', }, + resources: { + herp: 'derp', + }, } expect(telemetry.init({ @@ -90,13 +94,13 @@ describe('telemetry is enabled', () => { })).to.not.throw expect(window.cypressTelemetrySingleton).to.be.instanceOf(TelemetryClass) + expect(window.cypressTelemetrySingleton.getResources()).to.contain({ herp: 'derp' }) }) describe('attachWebSocket', () => { it('returns true', () => { telemetry.attachWebSocket('ws') - // @ts-expect-error expect(window.cypressTelemetrySingleton?.getExporter()?.ws).to.equal('ws') }) }) diff --git a/packages/telemetry/test/index.spec.ts b/packages/telemetry/test/index.spec.ts index adc257c4bef7..b9527b51fa5c 100644 --- a/packages/telemetry/test/index.spec.ts +++ b/packages/telemetry/test/index.spec.ts @@ -244,6 +244,35 @@ describe('getActiveContextObject', () => { }) }) +describe('getResources', () => { + it('returns the active resources', () => { + const exporter = new OTLPTraceExporterCloud() + + const tel = new Telemetry({ + namespace: 'namespace', + Provider: NodeTracerProvider, + detectors: [], + exporter, + version: 'version', + rootContextObject: { traceparent: 'id' }, + SpanProcessor: BatchSpanProcessor, + resources: { + herp: 'derp', + 'service.name': 'not overridden', + }, + }) + + expect(tel.getResources()).to.contain({ + 'service.name': 'cypress-app', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + herp: 'derp', + 'service.namespace': 'namespace', + 'service.version': 'version', + }) + }) +}) + describe('shutdown', () => { it('confirms shutdown is called', async () => { const exporter = new OTLPTraceExporterCloud() diff --git a/packages/telemetry/test/node.spec.ts b/packages/telemetry/test/node.spec.ts index 871cf46bcbf7..ae5137978a37 100644 --- a/packages/telemetry/test/node.spec.ts +++ b/packages/telemetry/test/node.spec.ts @@ -54,6 +54,12 @@ describe('telemetry is disabled', () => { }) }) + describe('getResources', () => { + it('returns an empty object', () => { + expect(telemetry.getResources()).to.not.be.undefined + }) + }) + describe('shutdown', () => { it('does not throw', () => { expect(telemetry.shutdown()).to.not.throw @@ -126,6 +132,18 @@ describe('telemetry is enabled', () => { }) }) + describe('getResources', () => { + it('returns an empty object', () => { + expect(telemetry.getResources()).to.include({ + 'service.name': 'cypress-app', + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'service.namespace': 'namespace', + 'service.version': 'version', + }) + }) + }) + describe('shutdown', () => { it('does not throw', () => { expect(telemetry.shutdown()).to.not.throw 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 fccea38cdaaa..03269960bb33 100644 --- a/packages/telemetry/test/span-exporters/cloud-span-exporter.spec.ts +++ b/packages/telemetry/test/span-exporters/cloud-span-exporter.spec.ts @@ -28,15 +28,17 @@ describe('cloudSpanExporter', () => { let callCount = 0 - exporter.sendDelayedItems = () => { + exporter.setAuthorizationHeader = () => { callCount++ } expect(exporter.headers['x-project-id']).to.be.undefined + expect(exporter.projectId).to.be.undefined exporter.attachProjectId('123') expect(exporter.headers['x-project-id']).to.equal('123') + expect(exporter.projectId).to.equal('123') expect(callCount).to.equal(1) }) @@ -45,15 +47,17 @@ describe('cloudSpanExporter', () => { let callCount = 0 - exporter.sendDelayedItems = () => { + exporter.setAuthorizationHeader = () => { callCount++ } expect(exporter.headers['x-project-id']).to.be.undefined + expect(exporter.projectId).to.be.undefined exporter.attachProjectId(undefined) expect(exporter.headers['x-project-id']).to.be.undefined + expect(exporter.projectId).to.be.undefined expect(callCount).to.equal(0) }) }) @@ -64,15 +68,15 @@ describe('cloudSpanExporter', () => { let callCount = 0 - exporter.sendDelayedItems = () => { + exporter.setAuthorizationHeader = () => { callCount++ } - expect(exporter.headers['x-record-key']).to.be.undefined + expect(exporter.recordKey).to.be.undefined exporter.attachRecordKey('123') - expect(exporter.headers['x-record-key']).to.equal('123') + expect(exporter.recordKey).to.equal('123') expect(callCount).to.equal(1) }) @@ -81,19 +85,37 @@ describe('cloudSpanExporter', () => { let callCount = 0 - exporter.sendDelayedItems = () => { + exporter.setAuthorizationHeader = () => { callCount++ } - expect(exporter.headers['x-record-key']).to.be.undefined + expect(exporter.recordKey).to.be.undefined exporter.attachRecordKey(undefined) - expect(exporter.headers['x-record-key']).to.be.undefined + expect(exporter.recordKey).to.be.undefined expect(callCount).to.equal(0) }) }) + describe('setAuthorizationHeader', () => { + it('sets the header if projectId and recordKey are present', () => { + const exporter = new OTLPTraceExporter() + + exporter.projectId = '123' + exporter.recordKey = '456' + + exporter.setAuthorizationHeader() + + const authorization = exporter.headers.Authorization + + console.log('auth', authorization) + + // MTIzOjQ1Ng== is 123:456 base64 encoded + expect(authorization).to.equal(`Basic MTIzOjQ1Ng==`) + }) + }) + describe('sendDelayedItems', () => { it('does not send if both project id and record key are not set', () => { const exporter = new OTLPTraceExporter() @@ -311,8 +333,7 @@ describe('cloudSpanExporter', () => { const exporter = new OTLPTraceExporter({ encryption, headers: { - 'x-project-id': '123', - 'x-record-key': '456', + Authorization: `Basic ${Buffer.from((`${123}:${456}`)).toString('base64')}`, }, }) @@ -340,83 +361,7 @@ describe('cloudSpanExporter', () => { exporter.send('string', onSuccess, onError) }) - it('delays the request if encryption enabled and project-id is missing', () => { - const encryption = { - encryptRequest: ({ url, method, body }) => { - throw 'encryptRequest should not be called' - }, - } - - const exporter = new OTLPTraceExporter({ - encryption, - headers: { - 'x-record-key': '456', - }, - }) - - 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 = () => { - throw 'onError should not be called' - } - - expect(exporter.delayedItemsToExport.length).to.equal(0) - - exporter.send('string', onSuccess, onError) - - expect(exporter.delayedItemsToExport.length).to.equal(1) - expect(exporter.delayedItemsToExport[0].serviceRequest).to.equal('string') - }) - - it('delays the request if encryption is enabled and record-key is missing', () => { - const encryption = { - encryptRequest: ({ url, method, body }) => { - throw 'encryptRequest should not be called' - }, - } - - const exporter = new OTLPTraceExporter({ - encryption, - headers: { - 'x-project-id': '123', - }, - }) - - 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 = () => { - throw 'onError should not be called' - } - - expect(exporter.delayedItemsToExport.length).to.equal(0) - - exporter.send('string', onSuccess, onError) - - expect(exporter.delayedItemsToExport.length).to.equal(1) - expect(exporter.delayedItemsToExport[0].serviceRequest).to.equal('string') - }) - - it('delays the request if encryption is enabled neither header is present', () => { + it('delays the request if encryption is enabled authorization is not present', () => { const encryption = { encryptRequest: ({ url, method, body }) => { throw 'encryptRequest should not be called'