Skip to content

Commit 9396528

Browse files
authored
feat: Implement timed events & remove transaction.measurements (#11398)
Now, we interpret timed events with special attributes as timed events. For now, we ignore all other timed events.
1 parent e6b4efa commit 9396528

File tree

18 files changed

+146
-56
lines changed

18 files changed

+146
-56
lines changed

.github/workflows/flaky-test-detector.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ on:
33
workflow_dispatch:
44
pull_request:
55
paths:
6-
- 'dev-packages/browser-integration-tests/suites/**'
6+
- 'dev-packages/browser-integration-tests/suites/**/test.ts'
77
branches-ignore:
88
- master
99

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/browser/src/index.bundle.tracing.replay.feedback.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
startSpanManual,
1919
withActiveSpan,
2020
getSpanDescendants,
21+
setMeasurement,
2122
} from '@sentry/core';
2223

2324
export {

packages/browser/src/index.bundle.tracing.replay.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
startSpanManual,
1919
withActiveSpan,
2020
getSpanDescendants,
21+
setMeasurement,
2122
} from '@sentry/core';
2223

2324
export {

packages/browser/src/index.bundle.tracing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
startSpanManual,
1919
withActiveSpan,
2020
getSpanDescendants,
21+
setMeasurement,
2122
} from '@sentry/core';
2223

2324
export {

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import type {
88
SpanOrigin,
99
SpanStatus,
1010
SpanTimeInput,
11+
TimedEvent,
1112
} from '@sentry/types';
12-
import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils';
13+
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';
1314
import { getClient } from '../currentScopes';
15+
import { DEBUG_BUILD } from '../debug-build';
1416

1517
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
1618
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
@@ -33,6 +35,8 @@ export class SentrySpan implements Span {
3335
protected _endTime?: number | undefined;
3436
/** Internal keeper of the status */
3537
protected _status?: SpanStatus;
38+
/** The timed events added to this span. */
39+
protected _events: TimedEvent[];
3640

3741
/**
3842
* You should never call the constructor manually, always use `Sentry.startSpan()`
@@ -65,6 +69,8 @@ export class SentrySpan implements Span {
6569
if (spanContext.endTimestamp) {
6670
this._endTime = spanContext.endTimestamp;
6771
}
72+
73+
this._events = [];
6874
}
6975

7076
/** @inheritdoc */
@@ -162,6 +168,30 @@ export class SentrySpan implements Span {
162168
return !this._endTime && !!this._sampled;
163169
}
164170

171+
/**
172+
* @inheritdoc
173+
*/
174+
public addEvent(
175+
name: string,
176+
attributesOrStartTime?: SpanAttributes | SpanTimeInput,
177+
startTime?: SpanTimeInput,
178+
): this {
179+
DEBUG_BUILD && logger.log('[Tracing] Adding an event to span:', name);
180+
181+
const time = isSpanTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime || timestampInSeconds();
182+
const attributes = isSpanTimeInput(attributesOrStartTime) ? {} : attributesOrStartTime || {};
183+
184+
const event: TimedEvent = {
185+
name,
186+
time: spanTimeInputToSeconds(time),
187+
attributes,
188+
};
189+
190+
this._events.push(event);
191+
192+
return this;
193+
}
194+
165195
/** Emit `spanEnd` when the span is ended. */
166196
private _onSpanEnded(): void {
167197
const client = getClient();
@@ -170,3 +200,7 @@ export class SentrySpan implements Span {
170200
}
171201
}
172202
}
203+
204+
function isSpanTimeInput(value: undefined | SpanAttributes | SpanTimeInput): value is SpanTimeInput {
205+
return (value && typeof value === 'number') || value instanceof Date || Array.isArray(value);
206+
}

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
});

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export type {
101101
MetricSummary,
102102
} from './span';
103103
export type { SpanStatus } from './spanStatus';
104+
export type { TimedEvent } from './timedEvent';
104105
export type { StackFrame } from './stackframe';
105106
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
106107
export type { PropagationContext, TracePropagationTargets } from './tracing';

packages/types/src/span.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,9 @@ export interface Span {
227227
* This will return false if tracing is disabled, this span was not sampled or if the span is already finished.
228228
*/
229229
isRecording(): boolean;
230+
231+
/**
232+
* Adds an event to the Span.
233+
*/
234+
addEvent(name: string, attributesOrStartTime?: SpanAttributes | SpanTimeInput, startTime?: SpanTimeInput): this;
230235
}

packages/types/src/timedEvent.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { SpanAttributes, SpanTimeInput } from './span';
2+
3+
export interface TimedEvent {
4+
name: string;
5+
time: SpanTimeInput;
6+
attributes?: SpanAttributes;
7+
}

packages/types/src/transaction.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { MeasurementUnit } from './measurement';
21
import type { ExtractedNodeRequestData, WorkerLocation } from './misc';
32
import type { SentrySpanArguments, Span } from './span';
43

@@ -48,19 +47,7 @@ export interface TraceparentData {
4847
/**
4948
* Transaction "Class", inherits Span only has `setName`
5049
*/
51-
export interface Transaction extends Omit<TransactionArguments, 'name' | 'op' | 'spanId' | 'traceId'>, Span {
52-
/**
53-
54-
* Set observed measurement for this transaction.
55-
*
56-
* @param name Name of the measurement
57-
* @param value Value of the measurement
58-
* @param unit Unit of the measurement. (Defaults to an empty string)
59-
*
60-
* @deprecated Use top-level `setMeasurement()` instead.
61-
*/
62-
setMeasurement(name: string, value: number, unit: MeasurementUnit): void;
63-
}
50+
export interface Transaction extends Omit<TransactionArguments, 'name' | 'op' | 'spanId' | 'traceId'>, Span {}
6451

6552
/**
6653
* Context data passed by the user when starting a transaction, to be used by the tracesSampler method.

0 commit comments

Comments
 (0)