diff --git a/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js new file mode 100644 index 000000000000..036e86201b18 --- /dev/null +++ b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js @@ -0,0 +1,8 @@ +const transaction = Sentry.startTransaction({ name: 'some_transaction' }); + +transaction.setMeasurement('metric.foo', 42, 'ms'); +transaction.setMeasurement('metric.bar', 1337, 'nanoseconds'); +transaction.setMeasurement('metric.baz', 99, 's'); +transaction.setMeasurement('metric.baz', 1); + +transaction.finish(); diff --git a/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts new file mode 100644 index 000000000000..e91231093bf3 --- /dev/null +++ b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts @@ -0,0 +1,18 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should attach measurement to transaction', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + const event = await getFirstSentryEnvelopeRequest(page, url); + + expect(event.measurements?.['metric.foo'].value).toBe(42); + expect(event.measurements?.['metric.bar'].value).toBe(1337); + expect(event.measurements?.['metric.baz'].value).toBe(1); + + expect(event.measurements?.['metric.foo'].unit).toBe('ms'); + expect(event.measurements?.['metric.bar'].unit).toBe('nanoseconds'); + expect(event.measurements?.['metric.baz'].unit).toBe(''); +}); diff --git a/packages/tracing/src/browser/metrics.ts b/packages/tracing/src/browser/metrics.ts index 8386b608f247..dffc39abb088 100644 --- a/packages/tracing/src/browser/metrics.ts +++ b/packages/tracing/src/browser/metrics.ts @@ -79,14 +79,14 @@ export class MetricsInstrumentation { if (entry.name === 'first-paint' && shouldRecord) { IS_DEBUG_BUILD && logger.log('[Measurements] Adding FP'); - this._measurements['fp'] = { value: entry.startTime }; - this._measurements['mark.fp'] = { value: startTimestamp }; + this._measurements['fp'] = { value: entry.startTime, unit: 'millisecond' }; + this._measurements['mark.fp'] = { value: startTimestamp, unit: 'second' }; } if (entry.name === 'first-contentful-paint' && shouldRecord) { IS_DEBUG_BUILD && logger.log('[Measurements] Adding FCP'); - this._measurements['fcp'] = { value: entry.startTime }; - this._measurements['mark.fcp'] = { value: startTimestamp }; + this._measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' }; + this._measurements['mark.fcp'] = { value: startTimestamp, unit: 'second' }; } break; @@ -115,12 +115,18 @@ export class MetricsInstrumentation { // start of the response in milliseconds if (typeof responseStartTimestamp === 'number') { IS_DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); - this._measurements['ttfb'] = { value: (responseStartTimestamp - transaction.startTimestamp) * 1000 }; + this._measurements['ttfb'] = { + value: (responseStartTimestamp - transaction.startTimestamp) * 1000, + unit: 'millisecond', + }; if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) { // Capture the time spent making the request and receiving the first byte of the response. // This is the time between the start of the request and the start of the response in milliseconds. - this._measurements['ttfb.requestTime'] = { value: (responseStartTimestamp - requestStartTimestamp) * 1000 }; + this._measurements['ttfb.requestTime'] = { + value: (responseStartTimestamp - requestStartTimestamp) * 1000, + unit: 'second', + }; } } @@ -162,7 +168,14 @@ export class MetricsInstrumentation { delete this._measurements.cls; } - transaction.setMeasurements(this._measurements); + Object.keys(this._measurements).forEach(measurementName => { + transaction.setMeasurement( + measurementName, + this._measurements[measurementName].value, + this._measurements[measurementName].unit, + ); + }); + tagMetricInfo(transaction, this._lcpEntry, this._clsEntry); transaction.setTag('sentry_reportAllChanges', this._reportAllChanges); } @@ -189,11 +202,11 @@ export class MetricsInstrumentation { } if (isMeasurementValue(connection.rtt)) { - this._measurements['connection.rtt'] = { value: connection.rtt as number }; + this._measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' }; } if (isMeasurementValue(connection.downlink)) { - this._measurements['connection.downlink'] = { value: connection.downlink as number }; + this._measurements['connection.downlink'] = { value: connection.downlink, unit: '' }; // unit is empty string for now, while relay doesn't support download speed units } } @@ -218,7 +231,7 @@ export class MetricsInstrumentation { } IS_DEBUG_BUILD && logger.log('[Measurements] Adding CLS'); - this._measurements['cls'] = { value: metric.value }; + this._measurements['cls'] = { value: metric.value, unit: 'millisecond' }; this._clsEntry = entry as LayoutShift; }); } @@ -234,8 +247,8 @@ export class MetricsInstrumentation { const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); const startTime = msToSec(entry.startTime); IS_DEBUG_BUILD && logger.log('[Measurements] Adding LCP'); - this._measurements['lcp'] = { value: metric.value }; - this._measurements['mark.lcp'] = { value: timeOrigin + startTime }; + this._measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; + this._measurements['mark.lcp'] = { value: timeOrigin + startTime, unit: 'second' }; this._lcpEntry = entry as LargestContentfulPaint; }, this._reportAllChanges); } @@ -251,8 +264,8 @@ export class MetricsInstrumentation { const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); const startTime = msToSec(entry.startTime); IS_DEBUG_BUILD && logger.log('[Measurements] Adding FID'); - this._measurements['fid'] = { value: metric.value }; - this._measurements['mark.fid'] = { value: timeOrigin + startTime }; + this._measurements['fid'] = { value: metric.value, unit: 'millisecond' }; + this._measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' }; }); } } @@ -392,7 +405,7 @@ export function _startChild(transaction: Transaction, { startTimestamp, ...ctx } /** * Checks if a given value is a valid measurement value. */ -function isMeasurementValue(value: any): boolean { +function isMeasurementValue(value: unknown): value is number { return typeof value === 'number' && isFinite(value); } diff --git a/packages/tracing/src/transaction.ts b/packages/tracing/src/transaction.ts index 6cbda0e04223..5384f15a6105 100644 --- a/packages/tracing/src/transaction.ts +++ b/packages/tracing/src/transaction.ts @@ -68,11 +68,15 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * Set observed measurements for this transaction. + * Set observed measurement for this transaction. + * + * @param name Name of the measurement + * @param value Value of the measurement + * @param unit Unit of the measurement. (Defaults to an empty string) * @hidden */ - public setMeasurements(measurements: Measurements): void { - this._measurements = { ...measurements }; + public setMeasurement(name: string, value: number, unit: string = ''): void { + this._measurements[name] = { value, unit }; } /** diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index d2a8744cd335..0367eafbb941 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -115,7 +115,7 @@ export interface SamplingContext extends CustomSamplingContext { request?: ExtractedNodeRequestData; } -export type Measurements = Record; +export type Measurements = Record; export type TransactionSamplingMethod = 'explicitly_set' | 'client_sampler' | 'client_rate' | 'inheritance';