Skip to content

Commit cda2823

Browse files
committed
feat: Implement timed events & remove transaction.measurements
Now, we interpret timed events with special attributes as timed events.
1 parent 3980730 commit cda2823

File tree

14 files changed

+142
-54
lines changed

14 files changed

+142
-54
lines changed
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
const transaction = Sentry.startInactiveSpan({
2-
name: 'some_transaction',
3-
forceTransaction: true,
1+
Sentry.startSpan({ name: 'some_transaction' }, () => {
2+
Sentry.setMeasurement('metric.foo', 42, 'ms');
3+
Sentry.setMeasurement('metric.bar', 1337, 'nanoseconds');
4+
Sentry.setMeasurement('metric.baz', 99, 's');
5+
Sentry.setMeasurement('metric.baz', 1, '');
46
});
5-
6-
transaction.setMeasurement('metric.foo', 42, 'ms');
7-
transaction.setMeasurement('metric.bar', 1337, 'nanoseconds');
8-
transaction.setMeasurement('metric.baz', 99, 's');
9-
transaction.setMeasurement('metric.baz', 1);
10-
11-
transaction.end();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.startSpan({ name: 'some_transaction' }, () => {
12+
Sentry.setMeasurement('metric.foo', 42, 'ms');
13+
Sentry.setMeasurement('metric.bar', 1337, 'nanoseconds');
14+
Sentry.setMeasurement('metric.baz', 99, 's');
15+
Sentry.setMeasurement('metric.baz', 1, '');
16+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should attach measurement to transaction', done => {
8+
createRunner(__dirname, 'scenario.ts')
9+
.expect({
10+
transaction: {
11+
transaction: 'some_transaction',
12+
measurements: {
13+
'metric.foo': { value: 42, unit: 'ms' },
14+
'metric.bar': { value: 1337, unit: 'nanoseconds' },
15+
'metric.baz': { value: 1, unit: '' },
16+
},
17+
},
18+
})
19+
.start(done);
20+
});

packages/core/src/semanticAttributes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin';
2222

2323
/** The reason why an idle span finished. */
2424
export const SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON = 'sentry.idle_span_finish_reason';
25+
26+
/** The unit of a measurement, which may be stored as a TimedEvent. */
27+
export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT = 'sentry.measurement_unit';
28+
29+
/** The value of a measurement, which may be stored as a TimedEvent. */
30+
export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE = 'sentry.measurement_value';

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ export {
1616
withActiveSpan,
1717
} from './trace';
1818
export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
19-
export { setMeasurement } from './measurement';
19+
export { setMeasurement, timedEventsToMeasurements } from './measurement';
2020
export { sampleSpan } from './sampling';
2121
export { logSpanEnd, logSpanStart } from './logSpans';
Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { MeasurementUnit, Span, Transaction } from '@sentry/types';
1+
import type { MeasurementUnit, Measurements, TimedEvent } from '@sentry/types';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
4+
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
5+
} from '../semanticAttributes';
26
import { getActiveSpan, getRootSpan } from '../utils/spanUtils';
37

48
/**
@@ -8,13 +12,28 @@ export function setMeasurement(name: string, value: number, unit: MeasurementUni
812
const activeSpan = getActiveSpan();
913
const rootSpan = activeSpan && getRootSpan(activeSpan);
1014

11-
if (rootSpan && rootSpanIsTransaction(rootSpan)) {
12-
// eslint-disable-next-line deprecation/deprecation
13-
rootSpan.setMeasurement(name, value, unit);
15+
if (rootSpan) {
16+
rootSpan.addEvent(name, {
17+
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: value,
18+
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: unit as string,
19+
});
1420
}
1521
}
1622

17-
function rootSpanIsTransaction(rootSpan: Span): rootSpan is Transaction {
18-
// eslint-disable-next-line deprecation/deprecation
19-
return typeof (rootSpan as Transaction).setMeasurement === 'function';
23+
/**
24+
* Convert timed events to measurements.
25+
*/
26+
export function timedEventsToMeasurements(events: TimedEvent[]): Measurements {
27+
const measurements: Measurements = {};
28+
events.forEach(event => {
29+
const attributes = event.attributes || {};
30+
const unit = attributes[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT] as MeasurementUnit | undefined;
31+
const value = attributes[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE] as number | undefined;
32+
33+
if (typeof unit === 'string' && typeof value === 'number') {
34+
measurements[event.name] = { value, unit };
35+
}
36+
});
37+
38+
return measurements;
2039
}

packages/core/src/tracing/sentryNonRecordingSpan.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,13 @@ export class SentryNonRecordingSpan implements Span {
5959
public isRecording(): boolean {
6060
return false;
6161
}
62+
63+
/** @inheritdoc */
64+
public addEvent(
65+
_name: string,
66+
_attributesOrStartTime?: SpanAttributes | SpanTimeInput,
67+
_startTime?: SpanTimeInput,
68+
): this {
69+
return this;
70+
}
6271
}

packages/core/src/tracing/sentrySpan.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
SpanOrigin,
99
SpanStatus,
1010
SpanTimeInput,
11+
TimedEvent,
1112
} from '@sentry/types';
1213
import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils';
1314
import { getClient } from '../currentScopes';
@@ -33,6 +34,8 @@ export class SentrySpan implements Span {
3334
protected _endTime?: number | undefined;
3435
/** Internal keeper of the status */
3536
protected _status?: SpanStatus;
37+
/** The timed events added to this span. */
38+
protected _events: TimedEvent[];
3639

3740
/**
3841
* You should never call the constructor manually, always use `Sentry.startSpan()`
@@ -65,6 +68,8 @@ export class SentrySpan implements Span {
6568
if (spanContext.endTimestamp) {
6669
this._endTime = spanContext.endTimestamp;
6770
}
71+
72+
this._events = [];
6873
}
6974

7075
/** @inheritdoc */
@@ -162,6 +167,32 @@ export class SentrySpan implements Span {
162167
return !this._endTime && !!this._sampled;
163168
}
164169

170+
/**
171+
* @inheritdoc
172+
*/
173+
public addEvent(
174+
name: string,
175+
attributesOrStartTime?: SpanAttributes | SpanTimeInput,
176+
startTime?: SpanTimeInput,
177+
): this {
178+
if (this._endTime) {
179+
return this;
180+
}
181+
182+
const time = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || timestampInSeconds();
183+
const attributes = isSpanTimeInput(attributesOrStartTime) ? {} : attributesOrStartTime || {};
184+
185+
const event: TimedEvent = {
186+
name,
187+
time: spanTimeInputToSeconds(time),
188+
attributes,
189+
};
190+
191+
this._events.push(event);
192+
193+
return this;
194+
}
195+
165196
/** Emit `spanEnd` when the span is ended. */
166197
private _onSpanEnded(): void {
167198
const client = getClient();
@@ -170,3 +201,7 @@ export class SentrySpan implements Span {
170201
}
171202
}
172203
}
204+
205+
function isSpanTimeInput(value: undefined | SpanAttributes | SpanTimeInput): value is SpanTimeInput {
206+
return (value && typeof value === 'number') || value instanceof Date || Array.isArray(value);
207+
}

packages/core/src/tracing/transaction.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type {
22
Contexts,
33
Hub,
4-
MeasurementUnit,
5-
Measurements,
64
SpanJSON,
75
SpanTimeInput,
86
Transaction as TransactionInterface,
@@ -18,6 +16,7 @@ import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
1816
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes';
1917
import { getSpanDescendants, spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils';
2018
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
19+
import { timedEventsToMeasurements } from './measurement';
2120
import { SentrySpan } from './sentrySpan';
2221
import { getCapturedScopesOnSpan } from './utils';
2322

@@ -30,8 +29,6 @@ export class Transaction extends SentrySpan implements TransactionInterface {
3029

3130
protected _name: string;
3231

33-
private _measurements: Measurements;
34-
3532
private _contexts: Contexts;
3633

3734
private _trimEnd?: boolean | undefined;
@@ -46,7 +43,6 @@ export class Transaction extends SentrySpan implements TransactionInterface {
4643
*/
4744
public constructor(transactionContext: TransactionArguments, hub?: Hub) {
4845
super(transactionContext);
49-
this._measurements = {};
5046
this._contexts = {};
5147

5248
// eslint-disable-next-line deprecation/deprecation
@@ -69,15 +65,6 @@ export class Transaction extends SentrySpan implements TransactionInterface {
6965
return this;
7066
}
7167

72-
/**
73-
* @inheritDoc
74-
*
75-
* @deprecated Use top-level `setMeasurement()` instead.
76-
*/
77-
public setMeasurement(name: string, value: number, unit: MeasurementUnit = ''): void {
78-
this._measurements[name] = { value, unit };
79-
}
80-
8168
/**
8269
* @inheritDoc
8370
*/
@@ -169,15 +156,13 @@ export class Transaction extends SentrySpan implements TransactionInterface {
169156
}),
170157
};
171158

172-
const hasMeasurements = Object.keys(this._measurements).length > 0;
159+
const measurements = timedEventsToMeasurements(this._events);
160+
const hasMeasurements = Object.keys(measurements).length;
173161

174162
if (hasMeasurements) {
175163
DEBUG_BUILD &&
176-
logger.log(
177-
'[Measurements] Adding measurements to transaction',
178-
JSON.stringify(this._measurements, undefined, 2),
179-
);
180-
transaction.measurements = this._measurements;
164+
logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(measurements, undefined, 2));
165+
transaction.measurements = measurements;
181166
}
182167

183168
return transaction;

packages/opentelemetry/src/spanExporter.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Span } from '@opentelemetry/api';
22
import { SpanKind } from '@opentelemetry/api';
33
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
44
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
5-
import { captureEvent, getMetricSummaryJsonForSpan } from '@sentry/core';
5+
import { captureEvent, getMetricSummaryJsonForSpan, timedEventsToMeasurements } from '@sentry/core';
66
import {
77
SEMANTIC_ATTRIBUTE_SENTRY_OP,
88
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
@@ -138,7 +138,10 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] {
138138

139139
transactionEvent.spans = spans;
140140

141-
// TODO Measurements are not yet implemented in OTEL
141+
const measurements = timedEventsToMeasurements(span.events);
142+
if (Object.keys(measurements).length) {
143+
transactionEvent.measurements = measurements;
144+
}
142145

143146
captureEvent(transactionEvent);
144147
});

0 commit comments

Comments
 (0)